部分内容来自以下博客:
https://www.cnblogs.com/xrq730/p/4827590.html
https://www.cnblogs.com/gw811/p/2730117.html
https://www.cnblogs.com/jhxxb/p/11001238.html
1 内存区域模型
Java虚拟机定义了程序在运行时需要使用到的内存区域,模型如下:
之所以要划分这么多区域出来是因为这些区域都有自己的用途,以及创建和销毁的时间。
有些区域随着虚拟机进程的启动而存在,有的区域则依赖用户线程的启动和结束而销毁和建立。
图中绿色部分就是所有线程之间共享的内存区域,包括方法区和堆。而白色部分则是线程运行时独有的数据区域,包括虚拟机栈,本地方法栈,程序计数器。
2 运行时数据区
2.1 PROGRAM COUNTER REGISTER,程序计数器
2.1.1 作用
用来存储指向下一条指令的地址,也就是将要执行的指令代码,以便由执行引擎读取下一条指令。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。
2.1.2 线程私有
为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储。
2.1.3 异常
此内存区域是唯一一个在虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
2.2 JAVA STACK,虚拟机栈
2.2.1 作用
虚拟机栈描述的是Java方法执行的内存模型,每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法返回地址等信息。
每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程,这个过程遵循先进后出的规则。
在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。
2.2.2 线程私有
与程序计数器一样,虚拟机栈也是线程私有的,它的生命周期与线程相同,每创建一个线程时就会对应创建一个虚拟机栈,所以虚拟机栈也是线程私有的内存区域。
2.2.3 异常
在Java虚拟机规范中,对这个区域规定了两种异常状况:
1)StackOverflowError
递归调用。如果线程请求的栈深度大于虚拟机所允许的深度,即递归调用次数太多,将抛出StackOverflowError异常:
public static void test() {
test();
}
public static void main(String[] args) throws Exception {
test();
}
数据过大。局部数组过大,当函数内部定义的数组过大时,有可能导致内存溢出,抛出StackOverflowError异常。
2)OutOfMemoryError
如果虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成拓展,或者在建立新线程的时候没有足够的内存去创建对应的虚拟机栈,那虚拟机将会抛出OutOfMemoryError异常。
2.2.4 栈帧
一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
1)局部变量表(Local Variable Table)
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,包括各种基本类型、引用类型和返回地址类型(指向了一条字节码指令的地址)。
如果是基本类型变量,其变量名和值是存储在方法栈中。如果是引用类型变量,其变量值存储的实际上是内存地址值,所指向的对象是存储在堆中的。
局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小,具体大小可在编译后的class文件中看到。
局部变量表的容量以变量槽(Variable Slot)为最小单位,每个变量槽都可以存储32位长度的内存空间。
64位长度的long类型和double类型的数据会占用2个变量槽,其余的数据类型只占用1个变量槽。
2)操作数栈(Operand Stack)
操作数栈同样也可以在编译期确定大小。
栈帧被创建时,操作栈是空的。方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。例如在做算术运算的时候是通过操作数栈来进行的,又或者在调用其它方法的时候是通过操作数栈来进行参数传递的。
3)动态链接(Dynamic Linking)
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。
在类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解析。另外的一部分将在运行时转化为直接引用,这部分称为动态链接。
4)返回地址(Return Address)
方法开始执行后,只有两种方式可以退出:方法返回指令,异常退出。
无论采用任何退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息。
2.3 NATIVE METHOD STACK,本地方法栈
2.3.1 作用
本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
2.3.2 线程私有
与虚拟机栈一样,本地方法栈也是线程私有的。
2.3.3 异常
与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError异常和OutOfMemoryError异常。
2.3.4 本地方法
Java诞生的时候是C/C++横行的时候,要想立足,必须有调用C/C++程序,于是使用native关键字标识调用了C/C++程序的方法,把这种方法称为Native方法。
为了和用于处理Java方法的虚拟机栈区分开来,又在内存中专门开辟了一块区域用于处理Native方法,这块区域就被称为本地方法栈。
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备。在企业级应用中已经比较少见,因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用WebService等等。
比如Object类中的hashCode()方法就是本地方法:
public native int hashCode();
2.4 HEAP,堆
2.4.1 作用
此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
2.4.2 线程共享
堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,存储的数据不是线程安全的。
2.4.3 异常
根据Java虚拟机规范的规定,堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的。
如果在养老区中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
一般出现异常的原因是:
1)堆内存设置不够。
2)代码中创建了大量对象,并且长时间不能被垃圾回收器收集。
2.4.4 划分
堆是垃圾收集器管理的主要区域,因此很多时候也被称做GC堆(Garbage Collected Heap)。
如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以堆还可以细分为新生代和老年代。
1)新生代
新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低。
在新生代中,常规应用进行一次垃圾收集一般可以回收70%到95%的空间,回收效率很高。
新生代又可细分为Eden空间、From Survivor空间、To Survivor空间,默认比例为8:1:1。这么划分是为了方便垃圾回收(Garbage Collected,简称GC)。
2)老年代
大对象会直接分配到老年代中,长期存活的对象也会分配到老年代。
老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
当老年代内存满了之后会触发一次GC,如果GC后内存空间仍不满足,则会触发OOM异常。
2.5 METHOD AREA,方法区
2.5.1 作用
方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
虽然虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与堆区分开来。
2.5.2 线程共享
方法区与堆一样,是各个线程共享的内存区域。
2.5.3 异常
根据虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
一般出现异常的原因是:
1)内存设置不够。
2)加载大量第三方Jar包。
3)存在大量调用反射的代码。
2.5.4 永久代
对于习惯在HotSpot虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为永久代(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,对于其他虚拟机(如BEA JRockit、IBM J9等)来说是不存在永久代的概念的。
因为永久代参数设置不合理会产生问题,参数过小容易产生OOM,参数过大会导致空间浪费,因此在JDK1.8中使用元空间取代了永久代。
此外,移除永久代是为融合HotSpot与JRockit而做出的努力,JRockit没有永久代,不需要配置永久代。
2.5.5 常量池
常量池(Runtime Constant Pool),用于存放编译期生成的各种字面量(常量和字符串)和符号引用(全类名,属性名,方法名,修饰符)。
JDK1.6及之前有永久代,常量池在方法区。JDK1.7时有永久代,但已经逐步去永久代,常量池在堆中。JDK1.8及之后无永久代,常量池在元空间。
将常量池放在堆中,能及时回收内存,避免内存浪费。
2.5.6 垃圾回收
虚拟机规范对这个区域的限制非常宽松,除了和堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。
这个区域垃圾回收的任务:
1)对废弃常量的回收。
2)对类型的卸载。
3 对象内存结构
3.1 内存结构图
对象的内存结构图如下:
对象内部结构分为对象头、实例数据、对齐填充。
3.2 对象头
对象头分为对象标记和类型指针,如果是数组对象还有数组长度。
3.2.1 对象标记
对象标记存储的数据会根据对象的锁状态进行复用,在运行期间,对象标记中的数据会随着锁标志位的变化而变化。
在64位的HotSpot虚拟机下,对象标记占用8个字节。
3.2.2 类型指针
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
在开启压缩的情况下占用4个字节,否则占用8个字节,JDK1.8默认开启压缩。
3.2.3 数组长度
如果对象是一个数组,那在对象头中还必须有一块用于记录数组长度的数据。
因为虚拟机可以通过普通对象的元数据信息确定对象的大小,但是从数组的元数据中却无法确定数组的大小。
数组长度占用4个字节。
3.3 实例数据
用于存储对象的有效信息,包括程序代码中定义的各种类型的字段(包括继承自父类的和自身声明的),规则如下:
1)相同宽度的字段总被分配在一起。
2)父类中定义的变量会出现在子类之前。
3)如果虚拟机CompactFields参数为true(默认为true),子类的窄变量可能插入到父类变量的空隙。
3.4 对齐填充
虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
4 创建对象
4.1 方式
使用new关键字:最常见的方式。
使用Class对象的newInstance()方法:通过反射的方式,只能调用空参的构造器,必须是public权限。
使用Constructor对象的newInstance()方法:通过反射的方式,可以调用任何定义的构造器,没有权限要求。
使用clone()方法:不调用任何构造器,当前类需要实现Cloneable接口,实现clone()方法。
使用反序列化:从文件和网络中获取对象的二进制流,然后反序列化为对象。
使用第三方库:例如通过Objenesis创建。
4.2 步骤
4.2.1 加载
检查能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、链接和初始化。如果没有,那么必须先执行类的初始化过程。
4.2.2 分配内存
类加载检查通过后,虚拟机为新生对象分配内存。对象所需内存大小在类加载完成后便可以完全确定,为对象分配空间无非就是从堆中划分出一块确定大小的内存而已。
有两种方式分配内存:
1)指针碰撞法
如果内存是规整的,那么虚拟机将采用的是指针碰撞法来为对象分配内存。
所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离罢了。
如果垃圾收集器选择的是SerialOld、ParallelOld这种基于标记-整理算法的,虚拟机采用这种分配方式。
2)空闲列表法
如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的是空闲列表法来为对象分配内存。
虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。
如果垃圾收集器选择的是CMS这种基于标记-清除算法的,虚拟机采用这种分配方式。
4.2.3 处理并发安全问题
接下来要解决的问题是如何保证创建对象时的线程安全。
因为可能出现虚拟机正在给对象A分配内存,指针还没有来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
使用CAS(Compare and Swap,即比较并替换,是一种实现并发算法时常用到的技术)失败重试和TLAB(Thread Local Allocation Buffer,划分出多个线程私有的分配缓冲区)区域加锁解决并发安全的问题。
4.2.4 分配空间
内存分配结束,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。
这一步保证了对象的实例字段在代码中可以不用赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。
4.2.5 设置对象头
将对象的所属类、对象的哈希值、对象的GC信息、锁信息等数据存储在对象的对象头中。
这个过程的具体设置方式取决于JVM实现。
4.2.6 初始化
初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。
5 访问对象
5.1 说明
假如方法体中有下面的这句代码:
Object obj = new Object();
“Object obj”表示的是栈的本地变量表中的一个reference类型的数据。
“new Object()”表示的是堆的一块存储了Object类型所有实例数据值(Instance Data,对象中各个实例字段的数据)的结构化内存。
另外,在堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。
5.2 访问方式
由于reference类型在虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同。
主流的访问方式有两种:
1)使用句柄访问的方式,堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。
2)直接指针访问的方式(HotSpot采用的方式),堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址。
两种对象访问方式的区别:
使用句柄访问方式的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。
就主要虚拟机HotSpot而言,它是使用第二种方式进行对象访问的,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。
6 逃逸分析
6.1 定义
逃逸分析(Escape Analysis)简单来讲就是,虚拟机可以分析新创建对象的使用范围,并决定是否在堆上分配内存的一项技术。
一个创建在方法中的对象,可能在方法结束后返回被外部方法所引用,也可能在方法中调用其他方法时作为参数传入,以上两种情况都称之为对象逃逸。
根据作用域可分为下面三种情况:
GlobalEscape(全局逃逸):对象的引用逃出了方法或者线程。例如:对象的引用赋值给类变量或者静态变量,对象跟随者方法返回至另一个方法的变量中,或者存储在一个已经逃逸的对象当中。
ArgEscape(参数级逃逸):在方法中调用其他方法时,对象的引用作为参数传递至其他方法。
NoEscape(没有逃逸):对象的作用域范围就只在本方法中,随着方法栈帧的进栈而生,出栈而亡。这种情况下,对象可以分配在栈中,但不是一定分配在栈中。
6.2 使用
逃逸分析的JVM参数如下:
开启逃逸分析:-XX:+DoEscapeAnalysis
关闭逃逸分析:-XX:-DoEscapeAnalysis
显示分析结果:-XX:+PrintEscapeAnalysis
逃逸分析技术在JDK1.7以后开始支持,并默认设置为启用状态,可以不用额外加这个参数。
6.3 优化
使用逃逸分析,判断一个对象如果没有逃逸,编译器可以对代码进行优化。
6.3.1 同步消除
如果一个对象只能被一个线程访问到,那么将此对象作为锁,或者对于此对象的同步操作,在虚拟机优化后都可以忽略,这也就是多线程中的锁消除技术。
同步消除的JVM参数如下:
开启锁消除:-XX:+EliminateLocks
关闭锁消除:-XX:-EliminateLocks
同步消除在JDK1.8中都是默认开启的,并且要建立在逃逸分析的基础上。
6.3.2 标量替换
标量(Scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。
相对的,那些还可以分解的数据叫做聚合量(Aggregate)。Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。
在编译阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT编译器优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替,这个过程就是标量替换。
标量替换的JVM参数如下:
开启标量替换:-XX:+EliminateAllocations
关闭标量替换:-XX:-EliminateAllocations
显示标量替换详情:-XX:+PrintEliminateAllocations
标量替换同样在JDK1.8中都是默认开启的,并且要建立在逃逸分析的基础上。
6.3.3 栈上分配
如果一个对象没有发生逃逸,那么这个对象可能会被优化存储在栈中,但也并不是绝对存储在栈中,也有可能还是存储在堆中。
需要说明的是,在现有的虚拟机中,并没有真正的实现栈上分配,其实是通过标量替换实现的。
当对象没有发生逃逸时,该对象就可以通过标量替换分解为成员标量分配在栈内存中,和方法的生命周期一致,随着栈帧出栈时销毁,减少了GC压力,提高了应用程序性能。
7 代码演示
7.1 导包
在pom.xml文件中引入依赖:
<!-- JOL依赖 -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
在读取对象信息时,虚拟机是按照8位一组,从高位向低位读取的。
7.2 关闭压缩
在64位的HotSpot虚拟机下,类型指针需要占8个字节。
从JDK1.6开始,64位的虚拟机支持UseCompressedOops选项。其可对OOP(Ordinary Object Pointer,普通对象指针)进行压缩,使其只占用4个字节,以达到节约内存的目的。
在JDK1.8下,该选项默认启用。
可以通过添加虚拟机参数来显式进行配置:
-XX:+UseCompressedOops // 开启指针压缩
-XX:-UseCompressedOops // 关闭指针压缩
在IDEA中可以通过Run菜单中的Edit Configurations菜单修改VM options并添加参数:
7.3 执行代码并分析
7.3.1 查看Object对象
代码如下:
public static void main(String[] args){
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
}
结果如下:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 1c 53 1c (00000000 00011100 01010011 00011100) (475208704)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
参数说明:
OFFSET:偏移量,表示从第几个字节开始。
SIZE:占用的字节大小。
TYPE:Class中定义的类型。
DESCRIPTION:对类型的描述。
VALUE:在内存中的值。
分析如下:
一个空的Object对象占用16字节,对象标记占用8个字节,类型指针在关闭压缩后占用8个字节。
7.3.2 查看Integer对象
代码如下:
public static void main(String[] args){
System.out.println(ClassLayout.parseInstance(new Integer(1)).toPrintable());
}
结果如下:
java.lang.Integer object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e0 71 5e 1c (11100000 01110001 01011110 00011100) (475951584)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
16 4 int Integer.value 1
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
分析如下:
对比空的Object对象,总大小占用24个字节。其中,对象标记仍为8个字节并且内容相同,指针类型仍为8个字节但是内容有变化,增加了占用4个字节的实例数据,增加了占用4个字节的对齐填充。
因为int类型长度为32位,也就是4个字节,所以实例数据的大小也就是4个字节,为了保证总大小为8的倍数,额外增加了4个字节的对齐填充。
7.3.3 查看Long对象
代码如下:
public static void main(String[] args){
System.out.println(ClassLayout.parseInstance(new Long(1)).toPrintable());
}
结果如下:
java.lang.Long object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) f0 ac ba 1c (11110000 10101100 10111010 00011100) (481996016)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
16 8 long Long.value 1
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
分析如下:
对比Integer对象,总大小仍为24个字节。其中,对象标记和指针类型变化不大,但是实例数据占用的大小变为8个字节,并且没有对齐填充。
因为long类型长度为64位,也就是8个字节,所以实例数据就占用8个字节,并且不需要对齐填充。
7.3.4 查看数组对象
代码如下:
public static void main(String[] args) {
System.out.println(ClassLayout.parseInstance(new Integer[]{1, 2, 3}).toPrintable());
}
结果如下:
[Ljava.lang.Integer; object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 42 e6 1c (00000000 01000010 11100110 00011100) (484852224)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
16 4 (object header) 03 00 00 00 (00000011 00000000 00000000 00000000) (3)
20 4 (alignment/padding gap)
24 24 java.lang.Integer Integer;.<elements> N/A
Instance size: 48 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
分析如下:
数组对象在对象头中会增加数组长度,占用4个字节并且值为3表示长度为3,另外在对象标记中还需要4个字节的对齐填充。
实例数据占用了24个字节。
7.3.5 查看分代信息
代码如下:
public static void main(String[] args){
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
System.gc();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
结果如下:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 1c a2 1c (00000000 00011100 10100010 00011100) (480386048)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 09 00 00 00 (00001001 00000000 00000000 00000000) (9)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 1c a2 1c (00000000 00011100 10100010 00011100) (480386048)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
分析如下:
因为虚拟机在读取对象头时,是将每8位作为一组,从高往低读取的,所以在代表对象标记的8个字节中,首先打印的8位数字实际上是最后的8位数字。
对照对象头存储的信息,当没有被垃圾回收时,高8位表示如下:第1位是无效位,后4位表示分代年龄,后1位表示偏向锁,最后2位表示锁标志。
垃圾回收前,分代年龄是0,执行垃圾回收后,分代年龄变为1,4位的分代年龄表示最高年龄为15。