HotSpot虚拟机对象探秘
1、对象的创建
在语言层次上创建对象(例如克隆,反序列化),通常也就是“new”简单。使用了new关键字就创建出来了。但是在jvm中,对象(本文讨论的对象限于普通对象,不包括数组和class对象等)的创建是怎样的过程呢?
(1)类加载
1、虚拟机碰到new指令时会检查指令的参数能否定位到常量池中类的符号引用
2、检测这个符号引用代表的类是否已经被加载、解析、初始化过。如果没有则进行相应的流程。
(2)内存分配(对象初始化)
- 指针碰撞法(堆完整时采用)
- 空闲列表法(堆不完整时采用)
1、选择哪种分配方式由堆是否完整而定,而堆是否完整又由采用的垃圾收集器是否带有压缩整理功能决定。
2、使用Serial、ParNew等带Compat过程的收集器时采用的分配算法是指针碰撞
3、使用CMS基于Mark-Sweep算法收集时,通常采用空闲列表。
(3)解决对象创建的线程安全问题
两种方案:
1、对分配内存空间的动作进行同步处理:jvm采用CAS+失败重试的方式保证操作更新的原子性
2、把内存分配的动作按照线程划分在不同的空间进行。即每个线程在java堆中预先分配一小块内存,成为本地线程分配缓冲(Thread local Allocation Buffer,TLAB)哪个线程需要内存,就在哪个线程的TLAB中进行分配,只有TLAB用完并分配新的TLAB时才需要同步锁定。虚拟机是否使用TLAB可通过-XX:+/-UserTLAB参数设定
(4)初始化内存空间
1、内存分配完成后,虚拟机将分配到的内存空间都初始化为零值(不包含对象头),如果使用了TLAB,这一工作过程还可以提前至TLAB分配时进行。
2、这一步操作保证了象实例的字段在java代码中不赋值初始值就可以直接使用。程序能够访问到这些字段对应类型的默认零值(比如int类型为0,string类型为空)
(5)设置对象
1、jvm开始对对象进行必要的设置。例如是哪个类的实例、如何找到类的元数据信息、对象的hash码、对象的GC分代年龄等信息。
2、上述设置的信息存放在对象头中(Object Head)
3、根据虚拟机当前的运行状态不同对象头有不同的设置方式。
(6)初始化
1、经过上述工作后,jvm认为对象已经创建完成,但是从程序的角度考虑对象的创建才刚刚开始,因为我们还没进行初始化,这时所有的字段还是零值。
2、执行new指令后便执行<init>方法,按照程序员的意愿进行初始化。
2、对象的内存布局
HotSpot虚拟机中对象在内存中存储的布局分为三块区域:
1、对象头
2、实例数据
3、对其填充
ps:参考下文对象内存布局图解
(1)对象头
HotSpot虚拟机的对象头包含两部分信息:
1、第一部分用于存储对象自身的运行时数据(Mark word)。
对象自身的运行时数据:hash码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程id、偏向时间戳等。
这部分数据的长度在32、64位的虚拟机上分别为32bit、64bit,官方称之为mark word。但是对象需要存储的运行时数据很多,其实已经超出了32位、64位Bitmap结构所记录的限制。但是对象头存储的数据是与对象自身的自定义数据无关的额外存储成本,考虑到这Mark word被设计成非固定的数据结构,根据对象的自身状态复用自己的存储空间。
2、另一部分为类型指针。
- 虚拟机通过这个指针可以确定此对象为哪个类的实例。
- 并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身。
3、如果一个对象为数组,那么对象头中还有一块用于记录数组长度的数据。因为虚拟机通过对象的元数据能够确定对象的大小,但是通过数组的元数据确定不了数组的大小。
(2)实例数据
- 对象真正存储的有效信息,也是代码中定义的各种类型的字段内容。
- 无论是从父类继承的数据还是子类定义的数据都会被记录起来
- 这部分存储顺序受到虚拟机分配策略参数、字段在java源码中定义的顺序影响
- hotspot默认分配策略:longs/doubles、ints、shorts/chars、bytes/booleans、oops。相同宽度的字段总是被分配到一起。
- 满足默认分配策略的前提下父类中定义的变量会出现在子类之前。如果CompactFile参数为true(默认为true)子类中较窄的变零也会插入到父类的变量空隙中。
(3)对其填充
- 这部分不是必然存在,没啥意义,仅仅起着占位符的作用
- HotSpot要求对象的起始地址必须为8字节的整数倍,而对象头正好时8的整数倍(如上对象头为32或者64位正好为8字节的1倍、2倍),当对象实例数据部分没有对齐时,这部分正好补上。
3、对象的访问定位:如何定位使用?
1、Java程序通过虚拟机栈中的局部变量表中的reference数据来操作Java堆上的具体对象
2、虚拟机规范规定了通过栈的局部变量表的引用来指向堆中的对象,但是并没有定义通过各种方式去定位、访问堆中的对象的具体位置,所以对象访问方法也取决于虚拟机的实现而定的。目前主流的访问方式有使用句柄和直接指针两种。
(1)访问方式
- 句柄
- 直接指针
(2)句柄方式
1、在堆中划分一块内存作为句柄池。
2、栈的reference中存储的就是对象的句柄地址
3、句柄中包含了对象实例数据与类型数据各自的具体信息(如下图)
(3)直接指针访问
栈reference中存储的直接就是对象地址
(4)两种方式对比
1、句柄式好处reference中存储的是稳定的句柄地址,对象被移动时(垃圾收集时移动是普遍现象)只会改变句柄中实例数据指针,而reference本身不需要修改
2、直接指针方式的好处,访问速度快,节省了一次指针定位的时间开销,也是HotSpot VM使用的方式