前篇《Java虚拟机之内存区域》做了一个提纲式的总结,这里将“对象探秘”部分做一个稍稍的展开,包括创建对象的基本流程、对象的内存布局、Class & Klass & KlassKlass浅析以及实例数据重排序等内容。
创建对象的基本流程
Java里创建对象通常有五种方式,new,Class类调用newInstance方法,Constructor类调用newInstance方法,clone方法以及反序列化,后四种反编译后可知使用的是invokevirtual指令来做方法调用,而第一种使用的是new和invokespecial指令。一般的,我们都是用new来创建对象,这里以new为例分析创建对象的基本流程,其大致如下:
- 检查指令参数能否定位符号引用;
- 检查符号引用代表的类是否已经被加载、解析和初始化过,没有就要先执行这些过程(《深》P210);
- 分配内存(CAS / TLAB);
- 初始化为零值(不包括对象头);
- 对对象头进行必要设置;
- 通常,执行
方法(由invokespecial决定);
类的初始化自有其它篇章进行介绍,这里需要知道的是,在遇到new等字节码指令时,需要先触发类的初始化。在类加载的连接过程中,准备阶段也会为类变量设置初始化零值,注意区别和类比。
普通字段的初始化器会在每个构造方法中被执行,而且是在写进构造方法内初始化内容执行前执行,但这一切初始化过程发生之前,会先在构造方法中调用父类的
对象内存布局简析
对象在内存中的布局包括三个部分:对象头、实例数据和对齐填充。
为了很好的研究这一部分,采用实验的方式来进行,这里用到了一个小工具jol,本人的Java环境为jdk1.8.0_171,64-bit HotSpot VM。
首先看看官方提供的JOLSample_01_Basic代码:
|
|
这个例子的作用注释已经说明清楚,这段代码的输出为:
|
|
前篇已经总结过,这里再提一下:对象头(object header)分为两个部分:一部分称为Mark Word,用于存储对象自身的运行时数据;另一部分是类型指针:用来确定对象是哪个类的实例。(当然,在数组里,对象头还记录了数组大小)
对以上输出结果做一个简要说明:
Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]对应:[Oop(Ordinary Object Pointer), boolean, byte, char, short, int, float, long, double]大小。
OFFSET是基于首地址偏移量,SIZE是大小,因为本人采用的Java虚拟机是64位,那么object header中Mark Word占据64位,也就是8个单位的大小,类型指针占据4个单位大小,所以一共占据12个单位。instance data占据一个单位大小,由于对象大小必须是8字节的整数倍,所以还要对齐填充,此时填充三个单位。
对象内存布局深入
首先贴出RednaxelaFX的PPT中的相关章节:
虽然图中的有些描述并不适用较新的Java版本——实际上这些图依据的是JDK6(或之前)的HotSpot VM,但大体来说,我们仍然可以从中有所收获。
图1描述了Mark Word存储了哪些信息,最后一句话提到了并非一次性记录所有信息;
图2中提到的PermGen在Java8里面已经被删除掉了(见文末参考链接),但通过图中的讲解,我们还是能get到有用的信息。
比如,Java对象实例中对象头存储的klass并不是Java语言级别的那个java.lang.Class类的实例。接下来就重点写写这方面的内容。
Class & Klass & KlassKlass
常常能听到有人问,对象的Class实例到底存在哪里。回答这个问题之前,先要搞清楚,想要问的真的是java.lang.Class实例吗?
我们继续欣赏RednaxelaFX的分享:
如果是第一次看到这张图是不是想感慨Java果然是面向对象语言:instanceOopDesc –> instanceKlass –> instanceKlassKlass –> KlassKlass。(后三个归为klassOopDesc包装层,包装在GC管理的klassOopDesc对象中)
RednaxelaFX的解释:
HotSpot VM里,Klass其实是用于描述能被GC的对象的类型信息的元数据对象。在JDK8之前的HotSpot VM里,类元数据存在由GC管理的PermGen区里。这些xxxKlass对象(例如instanceKlass的实例)自身也是被GC管理的,所以也需要有Klass对象去描述它们,叫做xxxKlassKlass。然后它们又…所以就有了KlassKlass这个终极的描述xxxKlassKlass对象的东西。
而:
从JDK8开始,既然元数据不由GC直接管理了,Klass这系对象就都不需要再被KlassKlass所描述,所以KlassKlass就全去除了。
看到这里可能会问,为什么元数据不由GC直接管理了,Klass就不需要KlassKlass描述了呢?
RednaxelaFX在留言区回答了热心网友:
因为HotSpot的GC框架通过Klass来获得被GC管理的对象的结构信息;在Metaspace里内存管理是人工写特化的代码做的而不是GC自动做的,就不需要这个KlassKlass结构来描述Klass了。
关于Klass所在的Metaspace的回收,RednaxelaFX也点到:
Metaspace有特殊的收集逻辑,跟GC可以联动但是不直接由GC管理
回到本小节起初提到的问题,也许一开始,我们并没有区分InstanceKlass和java.lang.Class,这两个是不同的东西。java.lang.Class对象从来都是存储在普通的Java堆中。那么它们彼此有什么关系呢?
每个Java对象的对象头里,_klass字段会指向一个VM内部用来记录类的元数据用的InstanceKlass对象;InsanceKlass里有个_java_mirror字段,指向该类所对应的Java镜像——java.lang.Class实例。HotSpot VM会给Class对象注入一个隐藏字段“klass”,用于指回到其对应的InstanceKlass对象。这样,klass与mirror之间就有双向引用,可以来回导航。java.lang.Class实例并不负责记录真正的类元数据,而只是对VM内部的InstanceKlass对象的一个包装供Java的反射访问用。
对象实例数据重排序
我们继续看图3中的内容,“宽度相同的字段总是相邻分配”?显然宽度相同的字段肯定不一定按顺序声明初始化,那么如果要满足引号中给出的要求,则需要重排序。下面本人写一个简单的例子:
|
|
首先按照一般逻辑分析对象的内存分布情况:
- Mark Word:八个单位;
- 类型指针:四个单位;
- i01:四个单位;
- i02:四个单位;
- l:八个单位;
- i03:四个单位;
布局应该是这样的:
实际输出结果如下:
|
|
没有对齐填充,实例的大小也只是32字节,也就是说,产生了重排序,布局见下图:
图中l在i02和i03之前,遵照的规则(排序的先后顺序)和Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]列出的顺序相反。i01在l之前是因为已经排好一部分后,剩下的空间能容纳size满足的实例数据,尽管这个数据并不是后续数据中最大的。
对基类是否生成对象的判断
创建派生类对象的过程中,基类会生成对象吗?
首先这个问题可以通过实验轻松验证,但是此问题是在本人看图3想到的,自然就用图3的逻辑稍微判断一下。
在图3中,有这么一句:“笼统的说,基类声明的实例字段会出现在派生类声明的实例字段之前。” 所以基类字段的数据是会在派生类对象中存储一份的,想象一下,如果每一个派生类生成实例时,都需要基类创建一个对象,那么字段等数据就会既存储在基类又存储在派生类中,造成资源浪费,在如今这个追求性能优化极致的时代,一款商业级虚拟机怎么会犯这种让人不能忍受的“错误”?
所以基类并没有生成对象,只是通过super()调用了构造器。
为了判断是否理解了本篇涉及的一些内容,这里举个例子,看看输出是多少:
|
|
这个小问题,涉及到对象的创建流程,涉及到基类是否生成对象等等,注意观察注解部分更改后的输出结果。
注:设置VM options参数为-XX:+TraceClassLoading,可以追踪类的加载过程。
鸣谢
看过《深入理解Java虚拟机》的读者对RednaxelaFX这个名字不会感到陌生,这篇小文引用了很多他在某平台分享的内容,得益于这些分享,本人对对象内存的布局有了更清晰的理解和认识,在此由衷感谢。
参考
Java8内存模型—永久代(PermGen)和元空间(Metaspace):通过实验印证永久代和元空间在不同Java版本下存在与否