JVM的内存分配原理

分配前的加载
当使用new关键字创建一个JAVA对象时,JVM首先会检查这个New指令的参数是否在常量池中定位到一个类的符号引用,然后检查与这个符号引用相对应的类是否已经成功经历过加载、解析和初始化等步骤。当类完成装载步骤之后,就已经完全可以确定出创建对象实例时所需要的内存空间大小,接下来JVM就会对其进行内存分配,以存储所生成的对象实例。
分配内存
基于分代的概念,Java的堆区还可以划分为新生代(YoungGen)和老年代(OldGen),其中新生代又可以划分为Eden空间、From Survivor空间和To Survivor空间。在JVM的运行时,堆区和方法区是线程共享区域,因此在并发环境下从堆区中划分内存空间是非线程安全的,所以要保证数据操作的原子性。基于线程安全的考虑,如果一个类在分配内存之前已经成功完成类装载步骤之后,JVM就会优先选择在TLAB(ThreadLocal Allocation 本地线程分配缓冲区)中为对象实例分配内存空间,TLAB在Java堆区中是一块线程私有区域,它包含在Eden空间内。一旦对象在TLAB中空间分配内存失败,JVM就会尝试通过使用加锁机制来确保数据操作的原子性,从而直接在Eden空间中分配内存,如果当在Eden空间中也无法分配内存时,JVM就会执行Minor GC,直到最终可以在Eden空间中分配内存为止。
初始化对象实例
当为对象成功分配好所需的内存空间后,JVM所做的就是初始化对象实例。JVM首先会对分配后的内存空间进行零值初始化,这一步操作确保了对象的实例字段在Java代码中不用赋初始值就能够直接使用,程序能够访问到这些字段的数据类型所对应的零值。零值初始化之后,JVM就会初始化对象头和实例数据(HotSpot中,内存空间所存储的对象信息主要包含着两个部分),最后将对象引用入栈,再更新PC寄存器 中的字节码指令地址。
经过上述一系列的操作之后,一个Java对象实例才算是真正的创建成功。

对象内存布局与OOP—Klass模型
当成功进行零值初始化后,JVM接下来就会对对象进行实例化,而实例化也就是初始化对象头和实例数据。

  1. 对象头:对象头中主要用于存储Mark Word和元数据指针等数据,其中Mark Word主要用于存储对象运行时的数据信息,比如HashCode、GC分代的年龄、线程持有的锁、锁状态标志、偏向线程Id等。元数据指针则是用于指向方法区中目标类的类型信息,也就是说通过元数据指针可以准确定位到当前对象的具体目标类型。
  2. 实例数据:实例数据主要用于存储定义在当前对象中的各种类型的字段信息(包括派生于超类的字段信息),存储在实例对象中的字段顺序除了会与字段在Java类中定义的顺序有关之外,还会受到JVM分配策略参数的影响。默认情况下,HotSpot会按照long/double,ints,shorts/chars、bytes/boolean,oops的分配顺序进行分配。二进制位数相同的字段总是会被分配在一起。大家要注意,在超类中定义的变量很有可能会出现在派生类之前。

java中的类以及对象实例
OOP-Klass模型就是用于表示Java类以及对象实例的一种数据结构,其中OOP(Ordinary Object Pointer 普通对象指针)用于描述对象的实例信息,而Klass则用于描述对象实例的目标类型。在JVM中对象头就是由对象instanceOopDesc来表示的(数组则是arrayOopDesc),而对象头中的元数据指针所指向的当前对象的目标类则是由Klass中的instanceKlass对象来表示。
JVM如何通过栈帧中的对象引用访问到其内部的对象实例呢?
JVM可以通过对象的引用准确定位到Java堆区的instanceOopDesc对象,这样就可以成功访问到对象的实例信息。当需要访问目标对象的具体类型时,JVM则会通过存储在instanceOopDesc中的元数据指针定位到存储在方法区中的instanceKlass对象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章