运行时数据区域

******本地代码编译成java字节码(.class文件) ==》 类加载进内存(思考保存进内存的哪里呢)******

一、运行时数据区

1)程序计数器(Program Counter Register)(线程私有)

是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器完成。
由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各线程之间的计数器互不影响,独立存储。
    1,如果线程正在执行的是一个 Java 方法,计数器记录的是正在执行的虚拟机字节码指令的地址;
    2,如果正在执行的是 Native 方法,这个计数器的值为空。
程序计数器是唯一一个没有规定任何 OutOfMemoryError 的区域。

2)Java 虚拟机栈(Java Virtual Machine Stacks)(线程私有)

生命周期与线程相同。
虚拟机栈描述的是 Java 方法执行的内存模型:每个方法被执行的时候都会创建一个栈帧(Stack Frame),存储:局部变量表、操作栈、动态链接、方法出口
每一个方法被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。这个区域有两种异常情况:
    1,StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度
    2,OutOfMemoryError:虚拟机栈扩展到无法申请足够的内存时

3)本地方法栈(线程私有)

  记住:一个方法对应一个栈帧;一个线程栈可以对应很多个栈帧。

虚拟机栈为虚拟机执行 Java 方法(字节码)服务。
本地方法栈(Native Method Stacks)为虚拟机使用到的 Native 方法服务。
1. Frame - 每个方法对应一个栈帧
   1. Local Variable Table(局部变量表就非常类似于寄存器)
   2. Operand Stack(操作数栈)
      对于long的处理(store and load),多数虚拟机的实现都是原子的
      jls 17.7,没必要加volatile
   3. Dynamic Linking(动态链接)
       https://blog.csdn.net/qq_41813060/article/details/88379473
    一个线程有它的线程栈,每个线程栈里面有一个个栈帧,每个栈帧里面有他的一个个操作数栈,还有一个动态链接(A方法里面有B方法,A方法就是通过B方法的动态链接去找的)
      jvms 2.6.3
   4. return address
      a() -> b(),方法a调用了方法b, b方法的返回值放在什么地方
    b方法把返回值给b方法,实际上是把返回值放进a方法栈帧里面的栈点

单条指令都有可能不是原子性的,更不用说多条指令

 

 

 

 

 

4)Java 堆(线程共享)

Java 堆(Java Heap)是 Java 虚拟机中内存最大的一块。Java 堆在虚拟机启动时创建,被所有线程共享。
作用:存放对象实例。垃圾收集器主要管理的就是 Java 堆。Java 堆在物理上可以不连续,只要逻辑上连续即可。

5)方法区:见标题二;

6)注意注意注意:java1.7之后,增加了一个直接内存(DirectMemory  NIO)

JVM可以直接访问的内核空间的内存 (OS 管理的内存)NIO , 提高效率,实现zero copy;之前是内核先把数据放到内存,然后JVM再从内存里面取数据,这样子就多了一步了嘛)

二、方法区(Method Area)(线程共享)

1)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据(不容易发生GC)(大的来说就是包括:class文件信息(所以可以理解为class文件在内存中的位置?)以及动态常量池);

2)和 Java 堆一样,不需要连续的内存,可以选择固定的大小,更可以选择不实现垃圾收集;

 

 

三、常量池

1)分类

class文件的常量池:又叫静态常量池(下面那个图好像表示的就是静态常量池),当class文件java虚拟机加载进来后存放在方法区的一些字面量和符号引用,符号引用其实引用的就是常量池里面的字符串,但符号引用不是直接存储字符串,而是存储字符串在常量池里的索引;

运行时常量池:又叫动态常量池,当class文件被加载完成后,将class文件中的常量池载入到内存中,并保存在方法区中,我们平时所说的常量池就是指这个。

****** 运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。

2)好处

常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
(1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
(2)节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。

3)双等号 == 的含义

基本数据类型之间应用双等号,比较的是他们的数值。
复合数据类型(类)之间应用双等号,比较的是他们在内存中的存放地址。

 4)面试一

        String s1 = "Hello";
        String s2 = "Hello";
        String s3 = "Hel" + "lo";
        String s4 = "Hel" + new String("lo");
        String s5 = new String("Hello");
        String s6 = s5.intern();
        String s7 = "H";
        String s8 = "ello";
        String s9 = s7 + s8; 
// s1 == s2这个非常好理解,s1、s2在赋值时,均使用的字符串字面量,说白话点,就是直接把字符串写死,在编译期间,
// 这种字面量会直接放入class文件的常量池中,从而实现复用,载入运行时常量池后,s1、s2指向的是同一个内存地址,所以相等。(放进一个StringTable里面,该数据结构为HashSet)
// String s1 = "Hello"的实现过程:首先栈区创建s1引用,然后在String池(独立于栈和堆而存在,存储不可变量)中寻找其指向的内容为"Hello"的对象,
// 如果String池中没有,则创建一个,然后s1指向String池中的对象,如果有,则直接将str1指向"Hello"";
// 如果后来又定义了字符串变量 s1 = "Hello",则直接将s1引用指向String池中已经存在的“Hello”,不再重新创建对象;当s1进行了赋值(s1=“abc”),
// 则str1将不再指向"Hello",而是重新指String池中的"abc",此时如果定义String str3 = "abc",s1 == str3操作,返回值为true,因为他们的值一样,地址一样,
System.out.println(s1 == s2);  // true
// s1 == s3这个地方有个坑,s3虽然是动态拼接出来的字符串,但是所有参与拼接的部分都是已知的字面量,在编译期间,这种拼接会被优化,编译器直接帮你拼好,
// 因此String s3 = "Hel" + "lo";在class文件中被优化成String s3 = "Hello",所以s1 == s3成立。只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中。
System.out.println(s1 == s3);  // true
// s1 == s4当然不相等,s4虽然也是拼接出来的,但new String("lo")这部分不是已知字面量,是一个不可预料的部分,编译器不会优化,
// 必须等到运行时才可以确定结果,结合字符串不变定理,鬼知道s4被分配到哪去了,所以地址肯定不同。对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中。
System.out.println(s1 == s4);  // false
// s1 == s9也不相等,道理差不多,虽然s7、s8在赋值的时候使用的字符串字面量,但是拼接成s9的时候,s7、s8作为两个变量,都是不可预料的,
// 编译器毕竟是编译器,不可能当解释器用,不能在编译期被确定,所以不做优化,只能等到运行时,在堆中创建s7、s8拼接成的新字符串,在堆中地址不确定,不可能与方法区常量池中的s1地址相同。
System.out.println(s1 == s9);  // false
// s4 == s5已经不用解释了,绝对不相等,二者都在堆中,但地址不同。
System.out.println(s4 == s5);  // false
// s1 == s6这两个相等完全归功于intern方法,s5在堆中,内容为Hello ,intern方法会尝试将Hello字符串添加到常量池中,
// 并返回其在常量池中的地址,因为常量池中已经有了Hello字符串,所以intern方法直接返回地址;而s1在编译期就已经指向常量池了,因此s1和s6指向同一地址,相等。
System.out.println(s1 == s6);  // true
View Code
 

  面试二

// 没有final,s9运行时(类加载时)才相加,所以不相等
public class ClassLoaderTest {
    static String s7 = "H";
    static String s8 = "ello";
    public static void main(String[] args) throws Exception {
        String s1 = "Hello";
        String s9 = s7 + s8;
        System.out.println(s1 == s9);  // false
    }
}

// 有final,s9编译期就已经确定了,所以相等
public class ClassLoaderTest {
    static final String s7 = "H";
    static final String s8 = "ello";
    public static void main(String[] args) throws Exception {
        String s1 = "Hello";
        String s9 = s7 + s8;
        System.out.println(s1 == s9);  // true
    }
}

// 虽然有final,但是没有值,s9编译期确定不了,类加载时才有值,但是那个时候已经在堆里面了,s1内存地址是在方法区的常量池,所以肯定不一样
public class ClassLoaderTest {
    static final String s7;
    static final String s8;
    static {
        s7 = "H";
        s8 = "ello";
    }
    public static void main(String[] args) throws Exception {
        String s1 = "Hello";
        String s9 = s7 + s8;
        System.out.println(s1 == s9);  // false
    }
}
View Code

 

 5)创建了几个对象

 String s1 = new String(“xyz”);

考虑类加载阶段和实际执行时。
(1)类加载对一个类只会进行一次。”xyz”在类加载时就已经创建并驻留了(如果该类被加载之前已经有”xyz”字符串被驻留过则不需要重复创建用于驻留的”xyz”实例)。驻留的字符串是放在全局共享的字符串常量池中的。
(2)在这段代码后续被运行的时候,”xyz”字面量对应的String实例已经固定了,不会再被重复创建。所以这段代码将常量池中的对象复制一份放到heap中,并且把heap中的这个对象的引用交给s1 持有。
纯属个人见解:如果之前有了 String s = “xyz”; 则String s1 = new String(“xyz”);只创建一个对象;如果之前没有,则创建了两个对象

 String test = “a” + “b” + “c”;

会创建几个字符串对象,在字符串常量池中保存几个引用么?
答案是只创建了一个对象,在常量池中也只保存一个引用。
看到了么,实际上在编译期间,已经将这三个字面量合成了一个。这样做实际上是一种优化,避免了创建多余的字符串对象,也没有发生字符串拼接问题。

 6)觉得写得很好

class BirthDate {
    private int day;
    private int month;
    private int year;    
    public BirthDate(int d, int m, int y) {
        day = d; 
        month = m; 
        year = y;
    }
    省略get,set方法………
}

public class Test{
    public static void main(String args[]){
        int date = 9;
        Test test = new Test();      
        test.change(date); 
        BirthDate d1= new BirthDate(7,7,1970);       
    }  

    public void change(int i){
     i = 1234;
    }
}

 对于以上这段代码,date为局部变量,i,d,m,y都是形参为局部变量,day,month,year为成员变量。下面分析一下代码执行时候的变化:

1. main方法开始执行:int date = 9;
date局部变量,基础类型,引用和值都存在栈中。
2. Test test = new Test();
test为对象引用,存在栈中,对象(new Test())存在堆中。
3. test.change(date);
i为局部变量,引用和值存在栈中。当方法change执行完成后,i就会从栈中消失。
4. BirthDate d1= new BirthDate(7,7,1970);  
d1为对象引用,存在栈中,对象(new BirthDate())存在堆中,其中d,m,y为局部变量存储在栈中,且它们的类型为基础类型,因此它们的数据也存储在栈中。day,month,year为成员变量,它们存储在堆中(new BirthDate()里面)。当BirthDate构造方法执行完之后,d,m,y将从栈中消失。
5.main方法执行完之后,date变量,test,d1引用将从栈中消失,new Test(),new BirthDate()将等待垃圾回收。

 

参考文章:

1)方法区和常量池

2) Java提高篇之常量池

3)常量池、栈、堆的比较

4)深入浅出java常量池

 

posted @ 2019-03-04 20:43  天马行空郭  阅读(146)  评论(0)    收藏  举报