目录

zrlong 的个人博客

希望大家都能保护好自己身上的特质,无论是五年还是十年,永远善良,不服输,热爱你所热爱。在漫长岁月的变迁里,是这些让你永远迷人,富有生命力。

X

JVM之对象探秘

对象的创建

Java是一门面向对象的语言,在程序运行过程中每时每刻都有对象被创建出来。当我们需要对象时,我们就可以直接new一个,看似十分简单,但是其中的原理是什么呢?

检查

当Java虚拟机遇到一条字节码new指令时,首先会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,同时也会检查这个符号引用代表的类是否已经被加载、解析和初始化。如果没有则先进行类的加载过程(后续再进行总结)。

分配内存

对象所需内存的大小在类加载完成后就可以完全确定了。接下来就是给对象分配内存了。

指针碰撞(Bump The Pointer)

假设Java对内存空间是完全规整的,被使用过的对象都放在一边,空闲的空间在另一边,中间有一个指针作为分界的指示器,分配的内存就是把指针向空闲的地方移动一段与对象大小相等的内存。

空闲列表(Free List)

如果Java堆内存的空间是不规整的,已使用和未使用的内存交错在一起,此时就无法使用“指针碰撞”的方法了。因此,虚拟机需要记录下来那些空间内存是能够使用的,在分配对象是选择足够大的空间。

选择那种方式是取决于堆内存是否规整的,Java堆内存的规整跟垃圾回收算法有关系,看他们是否带有空间压缩整理(Compact)的能力。

问题

对象创建在虚拟机中是很频繁的行为,指针修改的行为也是非常的频繁,因此在并发的情况下是线程不安全的。可能会出现正在给A对象分配内存,指针还没来得及修改,B对象就占用了指针。

解决方法

1.对分配内存空间的动作进行同步处理,虚拟机采用的是**CAS(Compare And Swap)**配合失败重试的方式保证更新操作的原子性。

2.把内存分配的动作按照线程划分在不同的空间进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程需要分配内存时,先在此线程的本地缓冲区中分配,本地缓冲区的内存用完时,才需要同步锁定。可以使用-XX:+/-UseTLAB来决定是否适用TLAB。

内存初始化

内存分配完成,虚拟机还必须将分配的内存初始化零值(对象头除外),如果使用了TLAB,则可以提前到TLAB分配时进行。意义是为了保证对象的实例字段在Java代码中不赋初始值就能直接使用,使程序能直接访问到这些字段的数据类型的零值。

对象头的设置

Java虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(实际上对象的哈希码是在调用Object::hashCode()方法时才计算出来的)、对象的年龄等、是否启用偏向锁(这个后面的并发编程总结中再进行详细解释)等信息。

构造函数

从虚拟机的角度,一个对象到上一步已经被创建出来了。但是从Java程序来看,对象的创建才刚刚开始。开始构造函数,Class文件中的< init >()方法还没执行,所有的字段都默认为零值,对象需要的其他资源和状态信息也还没有按照预定的意图构造好。一般来说,new指令后会跟随一个invokespecial指令调用< init >方法进行对象的初始化,但是直接通过别的方式产生的对象则不一定如此。到此,一个对象的创建才真正的完成。

对象的内存布局

HotSpot虚拟机中,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

对象头

HotSpot虚拟机的对象头部分包括两类信息。

运行时数据

第一类是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC年龄分代、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32和64位的虚拟中(未开启指针压缩)中分别为32个比特和64个比特,官方成为”Mark Word“。

类型指针

对象指向它的类型元数据的指针,Java虚拟机通过这个指针确定该对象是哪个类的实例。此外,如果对象是一个数组,那么对象头中还必须有一块用于记录数组长度的数据。因为如果数组的长度不确定,虚拟机无法推断数组大小。

实例数据

这是对象真正存储的有效信息,即我们在程序代码里所定义的各种类型的字段内容,无论是父类继承下来的还是在子类中定义的字段都必须记录起来。这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和字段Java源码中定义的顺序影响。HotSpot虚拟机的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),从以上默认的分配策略中可以看到,相同宽度的字段会被分配在一起存放,在满足这个条件的前提下,父类中定义的变量会出现在子类之前。如果HotSpot虚拟机的+XX:CompactFields参数值为true(默认为true),那子类中较窄的变量也允许插入父类变量的空隙中,节省出一点点空间。

对齐填充

这并不是必然存在的,仅仅起着占位符的作用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小必须是8字节的整数倍。对象头部分已经被精心设计为正好是8的整数倍(1或2倍),因此实例数据部分没有对齐,就需要对齐填充来补全。

对象的访问定位

由于《Java虚拟机规范》里只规定了reference是一个指向对象的引用,并没有定义怎么去引用,因此需要虚拟机实现而定。主流的主要有两种:句柄和直接指针两种:

  • 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
  • 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。比句柄访问少一次间接访问的开销。

在这里插入图片描述

这两种访问方式各有优势,句柄访问的最大好处就是reference中存储的是稳定的句柄地址,对象移动时只改变句柄中的实例数据指针,而reference本身不需要被修改。

使用直接指针的好处是速度更快,节省了一次指针定位的时间开销。由于对象访问在Java中很频繁,因此这类开销的节省也是很可观的执行成本。HotSpot虚拟机主要使用的就是这种,但是从整个软件开发的范围来看,句柄访问也是十分常见的。


标题:JVM之对象探秘
作者:zrlong
地址:http://blog.zrlong.top/articles/2022/04/10/1649552591507.html