在我们了解了JVM的运行时数据区划分后,我们需要进一步了解JVM内存中数据进一步的细节,例如对象是如何创建的,如何布局,及如何访问这些对象。接下来会从3个部分来探讨下Java堆中对象的创建及其访问过程。
对象的创建
-
检查,JVM收到
new
指令,首先去检查是否能在常量池中定位到一个class
的符号引用,并检查这个引用代表的class
是否被加载、解析和初始化过,如果没有则执行类加载过程。 -
分配内存,类加载通过后,JVM会为这个
class
分配内存。分配内存有两种方式,一种叫指针碰撞,一种叫空闲列表- 指针碰撞:假设heap是绝对连续规整的,空闲的内存和已使用的内存各在一边,中间放置一个指针作为分界点的指示器,分配内存时只需要把指针挪向空闲内存的那边即可。
- 空闲列表:如果heap不是规整的,空闲内存和已使用的内存交错分布,JVM会维护一个列表,记录哪些内存时可用的,分配内存是从列表里找一块足够存放对象的内存分配给它。
选择哪种分配方式由JVM所采用的垃圾收集器是否带有压缩整理的功能决定,因此在使用
Serial
、ParNew
等带有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本身无需修改。
-
直接指针的优势是速度快,节省了一次指针定位的时间开销。
来源:oschina
链接:https://my.oschina.net/codingcloud/blog/4953166