参考:《深入理解Java虚拟机》
当讨论对象如何创建?如何布局以及如何访问这些问题的时候。 必须把谈论范围限定在具体虚拟机和集中在某一块区域内才有意义。《深入理解Java虚拟机》的作者对HotSpot虚拟机在Java堆中对象的内存分配、布局以及访问的全过程进行了深入的探讨。本人对这些问题加以理解和学习,加深对JVM的认识。
问题一:使用new关键字创建对象的时候,对象是如何创建的呢?
当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析、初始化过。如果没有,那必须先执行相应的类加载过程(类加载过程在之后的博客中会详细的探讨和学习)
在类加载后,接下来是为我们创建的对象分配内存空间,那接下来探讨的问题就呼之欲出了。
问题二:JVM进行类加载之后,Java堆是如何为创建的对象分配内存空间的呢?
Java堆为对象分配内存的方式有两种:指针碰撞和空闲列表
指针碰撞(Bump The Pointer):假设Java堆中的内存都放在一起,所有是使用过的内存放到一边,空闲的内存放到另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就仅仅把指针相空闲空间的方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”
空闲列表(Free List):当使用内存和空闲内存交织在一起时,JVM需要维护一个列表,记录哪些内存是可用的,在给对象分配内存时从列表中找到一块足够大的内存空间划分给对象,并更新列表上的记录。这种分配方式称之为“空闲列表”
问题三:JVM的堆内存会使用哪种内存分配方式呢?
Java堆内存采用哪种分配方式取决于Java堆内存是否带有空间压缩整理的能力决定。也就是说有垃圾回收器采用哪种回收算法来决定的。当垃圾回收器采用的是标记-清理算法,如CMS垃圾回收器,那么使用的就是空闲列表的分配方式。当使用Serial、parNew等带有压缩整理过程的收集器,采用的是指针碰撞的内存分配方式
上面已经讨论完了Java堆是如何为对象分配内存空间的的问题,那接下讨论的就是对象在堆内存中如何进行存储布局?
问题四:对象在Java堆如何进行存储布局?
对象在堆内存的存储布局可以划分为三个部分:对象头(Header)、实例数据(InstanceData)和对齐填充(Padding)。
对象头(header):HotSpot虚拟机的对象头包含两部分信息:
- 第一部分用于存储自身运行时数据:如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间等
- 对象头的另一部分是类型指针,即指向对象类型元数据的指针,Java虚拟机通过该指针来确定对象是那个类的实列
实例数据(InstanceData):实例数据部分是对象正在存储的又有效信息,即程序代码中所定义的各种类型字段,包括从父类中继承下来的字段信息。
对齐填充(Padding):对齐填充仅仅是起到占位符的作用,任何对象的大小都必须是8个字节的整数倍,如果对象实列数据部分没有对齐的话,需要通过对齐填充来补全。对象头的信息已经被精心设计成正好是8个字节的整数倍。
上面已经学习了堆内存如何为对象分配内存。对象在内存中如何进行布局。那么接下去探讨Java栈的局部变量表中的reference数据如何来定位和访问到Java堆内存中对象的具体位置呢
问题五:对象的访问定位?
对象的访问方式主流的有两种:句柄访问方式和直接指针访问方式
句柄访问方式:在Java堆中划分一块内存来作为句柄池,句柄池中包含了对象数据和对象类型的具体地址信息。通过这些信息可以访问到对象实例数据和对象类型数据。Java栈中的reference存储对象的句柄池地址即可。
上图为句柄访问方式示意图
直接指针访问方式:reference中存储的就是对象地址。
这两种访问方式各有优缺点:句柄访问方式的优势就是稳定,reference中存储的是稳定的句柄地址,在对象移动时只会改变句柄中实例数据的指针,而reference本身不被修改。
直接访问方式的优势就是速度快,节省了指针定位的时间开销
HotSpot虚拟机采用的是直接指针访问方式
这是第一次采用提问的方式写博客,学习效果还不错。对对象的创建,Java虚拟机的堆内存有了更深的认识和了解