深入理解Java虚拟机系列——JVM的堆分配

为了展示虚拟机如何使用方法区中的信息,下面来举例说明:

class Lava {

    private int speed = 5;
    void flow(){
        
    }
}
 
public class Volcano {
    
    public static void main(String[] args){
        Lava lava = new Lava();
        lava.flow();
    }
}

不同的虚拟机实现可能会用完全不同的方法来操作,下面描述的只是其中一种可能——但并不是仅有的一种。

  要运行Volcano程序,首先得以某种“依赖于实现的”方式告诉虚拟机“Volcano”这个名字。之后,虚拟机将找到并读入相应的class文件“Volcano.class”,然后它会从导入的class文件里的二进制数据中提取类型信息并放到方法区中。通过执行保存在方法区中的字节码,虚拟机开始执行main()方法,在执行时,它会一直持有指向当前类(Volcano类)的常量池(方法区中的一个数据结构)的指针。

  注意:虚拟机开始执行Volcano类中main()方法的字节码的时候,尽管Lava类还没被装载,但是和大多数(也许所有)虚拟机实现一样,它不会等到把程序中用到的所有类都装载后才开始运行。恰好相反,它只会需要时才装载相应的类。

  main()的第一条指令告知虚拟机为列在常量池第一项的类分配足够的内存。所以虚拟机使用指向Volcano常量池的指针找到第一项,发现它是一个对Lava类的符号引用,然后它就检查方法区,看Lava类是否已经被加载了。

  这个符号引用仅仅是一个给出了类Lava的全限定名“Lava”的字符串。为了能让虚拟机尽可能快地从一个名称找到类,虚拟机的设计者应当选择最佳的数据结构和算法。

  当虚拟机发现还没有装载过名为“Lava”的类时,它就开始查找并装载文件“Lava.class”,并把从读入的二进制数据中提取的类型信息放在方法区中。

  紧接着,虚拟机以一个直接指向方法区Lava类数据的指针来替换常量池第一项(就是那个字符串“Lava”),以后就可以用这个指针来快速地访问Lava类了。这个替换过程称为常量池解析,即把常量池中的符号引用替换为直接引用。

  终于,虚拟机准备为一个新的Lava对象分配内存。此时它又需要方法区中的信息。还记得刚刚放到Volcano类常量池第一项的指针吗?现在虚拟机用它来访问Lava类型信息,找出其中记录的这样一条信息:一个Lava对象需要分配多少堆空间。

  JAVA虚拟机总能够通过存储与方法区的类型信息来确定一个对象需要多少内存,当JAVA虚拟机确定了一个Lava对象的大小后,它就在堆上分配这么大的空间,并把这个对象实例的变量speed初始化为默认初始值0。

  当把新生成的Lava对象的引用压到栈中,main()方法的第一条指令也完成了。接下来的指令通过这个引用调用Java代码(该代码把speed变量初始化为正确初始值5)。另一条指令将用这个引用调用Lava对象引用的flow()方法。


  Java程序在运行时创建的所有类实例或数组都放在同一个堆中。而一个JAVA虚拟机实例中只存在一个堆空间,因此所有线程都将共享这个堆。又由于一个Java程序独占一个JAVA虚拟机实例,因而每个Java程序都有它自己的堆空间——它们不会彼此干扰。但是同一个Java程序的多个线程却共享着同一个堆空间,在这种情况下,就得考虑多线程访问对象(堆数据)的同步问题了。

  JAVA虚拟机有一条在堆中分配新对象的指令,却没有释放内存的指令,正如你无法用Java代码去明确释放一个对象一样。虚拟机自己负责决定如何以及何时释放不再被运行的程序引用的对象所占据的内存。通常,虚拟机把这个任务交给垃圾收集器。

对象分配规则

  • 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。

  • 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。

  • 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。

  • 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。

  • 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。


数组的内部表示

  在Java中,数组是真正的对象。和其他对象一样,数组总是存储在堆中。同样,数组也拥有一个与它们的类相关联的Class实例,所有具有相同维度和类型的数组都是同一个类的实例,而不管数组的长度(多维数组每一维的长度)是多少。例如一个包含3个int整数的数组和一个包含300个整数的数组拥有同一个类。数组的长度只与实例数据有关

  数组类的名称由两部分组成:每一维用一个方括号“[”表示,用字符或字符串表示元素类型。比如,元素类型为int整数的、一维数组的类名为“[I”,元素类型为byte的三维数组为“[[[B”,元素类型为Object的二维数组为“[[Ljava/lang/Object”。

  多维数组被表示为数组的数组。比如,int类型的二维数组,将表示为一个一维数组,其中的每一个元素是一个一维int数组的引用,如下图:


    在堆中的每个数组对象还必须保存的数据时数组的长度、数组数据,以及某些指向数组的类数据的引用。虚拟机必须能够通过一个数组对象的引用得到此数组的长度,通过索引访问其元素(期间要检查数组边界是否越界),调用所有数组的直接超类Object声明的方法等等。



猜你喜欢

转载自blog.csdn.net/milhua/article/details/80563471
今日推荐