HotSpot虚拟机对象探秘(Java对象具体是如何创建的)
下文主要探讨HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程。
对象的创建
在 Java 语言层面上,我们创建一个对象是如此简单:ClassA intance = new ClassA(); 但是在虚拟机内部,其实经历了非常复杂的过程才完成了这一个程序语句。
1、虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,就得执行类的加载过程;
2.、在类加载检查通过后,虚拟机将为新生对象分配内存。对象所需内存大小再类加载完成后便可确定。目前有两种做法,使用哪种方式是由垃圾收集器是否带有压缩整理功能决定的:
-
指针碰撞(Bump the Pointer):假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
-
空闲列表(Free List):如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那么虚拟机维护一个列表,并更新列表山的记录。
分配内存过程中还需要解决线程安全问题。 一个修改指针操作,就会带来隐患:对象 A 正分配内存呢,指针还没来得急修改,突然对象 B 又同时使用了原来的指针来分配 B 的内存。解决方案也有两种:
- 同步处理——实际上虚拟机采用 CAS 配上失败重试来保证更新操作的原子性
- 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块内存,成为本地线程分配缓存(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的 TLAB 上分配,用完并分配新的TLAB时,才需要同步锁定
TLAB
那么在分配内存的时候,可能会出现多线程问题,解决方式有 CAS 乐观锁,以及 TLAB方式。TLAB的全称是 Thread Local Allocation Buffer。
每个线程 JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB(Thread-local allocation buffer)。默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。
也就是说,Java中每个线程都会有自己的缓冲区称作TLAB(Thread-local allocation buffer),每个TLAB都只有一个线程可以操作,TLAB结合bump-the-pointer技术可以实现快速的对象分配,而不需要任何的锁进行同步,也就是说,在对象分配的时候不用锁住整个堆,而只需要在自己的缓冲区分配即可。
3、内存清零:给内存分配了空间之后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)。如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。
4、 接下来要对对象进行必要的设置,比如
- 这个对象是哪个类的实例
- 如何才能找到类的元数据信息
- 对象的 hashcode 值是多少
- 对象的 GC 分代年龄等信息
这些信息都放在对象头中。
5、 上面的步骤都完成后,从虚拟机角度来看,一个新的对象已经产生了,但是从 Java 程序的视角来看,对象创建才刚刚开始——<init>
方法还没有执行,所有的字段都还为零。把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
对象的内存布局
首先我们要知道的是:在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instantce Data)、对齐补充(Padding)。当然,我们不必要知道太深入,大概知道每个部分的作用即可:
- 对象头(Header):包含两部分信息
- 第一部分用于存储对象自身的运行时数据,如 hashcode 值、GC 分代的年龄、锁状态标志、线程持有的锁等,官方称为“Mark Word”。
- 第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
- 实例数据(Instance Data):就是程序代码中所定义的各种类型的字段内容
- 内存对齐:由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍;当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
对象的访问定位
Java程序需要通过栈上的reference数据来操作堆上的具体对象。访问方式有使用句柄和直接指针两种。
-
句柄访问 Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息!
句柄访问
-
直接指针访问 Java堆对象的布局中必须考虑如何放置访问类型数据的相关信息,reference中存储的就是对象地址!
直接地址访问
两种访问方式的比较
-
使用句柄访问最大的好处是reference中存储的是稳定的句柄地址,在对象被移动(GC时移动对象是很普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改
-
使用直接指针访问方式的最大好处是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本
HotSpot虚拟机采用指针访问方式进行对象访问,从整个软件开发范围看,各种语言和框架使用句柄来访问的情况也非常常见。