深入探讨Hotspot虚拟机在Java堆中对象创建、布局和访问的过程

≯℡__Kan透↙ 提交于 2021-02-16 16:23:40

在我们了解了JVM的运行时数据区划分后,我们需要进一步了解JVM内存中数据进一步的细节,例如对象是如何创建的,如何布局,及如何访问这些对象。接下来会从3个部分来探讨下Java堆中对象的创建及其访问过程。

对象的创建

  • 检查,JVM收到new指令,首先去检查是否能在常量池中定位到一个class的符号引用,并检查这个引用代表的class是否被加载、解析和初始化过,如果没有则执行类加载过程。

  • 分配内存,类加载通过后,JVM会为这个class分配内存。分配内存有两种方式,一种叫指针碰撞,一种叫空闲列表

    • 指针碰撞:假设heap是绝对连续规整的,空闲的内存和已使用的内存各在一边,中间放置一个指针作为分界点的指示器,分配内存时只需要把指针挪向空闲内存的那边即可。
    • 空闲列表:如果heap不是规整的,空闲内存和已使用的内存交错分布,JVM会维护一个列表,记录哪些内存时可用的,分配内存是从列表里找一块足够存放对象的内存分配给它。

    选择哪种分配方式由JVM所采用的垃圾收集器是否带有压缩整理的功能决定,因此在使用SerialParNew等带有Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。 在分配内存过程中可能由于并发引起线程不安全的问题,JVM有两种解决方案,一、采用CAS+失败重试的方式保证更新操作的原子性(CAS调用的是C++方法);二、把内存分配的动作按照线程划分不同空间去进行,每个线程在heap中预分配一小块内存,称为本地线程分配缓冲区(Thread Local Allocation Buffer,TLAB)每个线程在自己的TLAB上分配。JVM是否使用TLAB可通过-XX:+/-UseTLAB参数来决定。

  • 初始化内存空间,内存分配完成后,JVM需要将分配到的内存空间初始化为0,这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就可以直接使用。

  • 设置对象头,接下来JVM要将存放在对象头(Object Header)的class实例、元数据信息、对象哈希码、对象的GC分代年龄等信息做必要的设置。

  • 初始化对象,执行完new指令后,会执行init方法,这样一个新对象就完全产生了。

对象的内存布局

在Hotspot虚拟机中,对象存储的布局分为3块区域:对象头(header)、实例数据(instance data)、对齐填充(padding)。

  • 对象头包含自身运行时数据和类型指针两部分。
    • 运行时数据包含hashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,也就是人们称之为的“Mark Word”。
    • 指针类型指的是对象指向它的元数据的指针,JVM通过这个指针来确定这个对象是哪个类的实例。如果这个对象是数组,对象头还必须有一块用于记录数组长度的数据区域。
  • 实例数据,这一部分是Java代码中所定义的各种类型的字段内容,包含父类继承下来的和子类定义的。
  • 对齐填充,这部分不是必要的,仅起着占位符的作用,Hotspot虚拟机要求对象起始地址必须是8的整数倍。

对象的访问

创建对象的目的是为了操作对象,我们的Java程序需要通过栈上的引用数据(reference)来操作堆上的具体对象。对象的访问取决于JVM的具体实现,目前主流的访问方式有句柄直接指针两种,Hotspot虚拟机使用的是直接指针方式。

  • 句柄的优势是reference中存储的是句柄地址,对象被垃圾回收时只会改变句柄中的实例数据指针,而reference本身无需修改。

  • 直接指针的优势是速度快,节省了一次指针定位的时间开销。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!