对象的大小以及内存布局与虚拟机的实现和设置有很大关系。
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划为三个部分:对象头、实例数据、对齐填充
1 对象头
HotSpot虚拟机对象的对象头(Mark Word)部分包括两类信息,
第一类是用于存储对象自身的运行时数据,另一类是类型指针。
1.1 对象自身的运行时数据
如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据的长度在32位和64位虚拟机(未开启指针压缩)中,分别为4个字节(32bit)和8个字节(64bit)。
对象头是一个有动态定义的数据结构。以便在极小的空间内存存储尽量多的数据,根据对象的状态复用存储空间。来节省存储成本和虚拟机的空间效率。
在不同的状态下对象头中所存储的内容会有所不同。
存储内容 | 标志位 | 状态 |
---|---|---|
对象哈希码、对象分代年龄 | 01 | 无锁状态 |
指向栈中锁记录的指针 | 00 | 轻量级锁 |
指向互斥量(重量级锁)的指针 | 10 | 重量级锁 |
空,不记录信息 | 11 | GC标记 |
偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 偏向锁,是否偏向锁为1 |
例如在64位的HotSpot虚拟机中,如对象处于无锁状态下时,对象头的64个字节中的26个bit处于空闲状态,HashCode占了31个bit,4个bit用来描述分代年龄,1个bit固定为0表示不是偏向锁,2个bit用于存储锁标志位。
1.2 类型指针
对象头的另一类是类型指针,即对象指向它的类型原数据的指针,Java虚拟机通过这个指针来确定对象时那个Class的实例,不是所有的虚拟机实现都需要在对象数据上存储类型指针。如果对象是数组时,在对象头中还需要有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是数组的长度不确定,就无法推断出整个数组的大小,无法分配内存
类指针占用内存大小:
-XX:+UseComparessedClassPointers 开启的话是4个字节,不开启则为8个字节
1.2.1 对象的访问定位
建立对象是为了使用对象,我们通过栈上的reference数据来操作堆上的具体对象。
由于reference类型在java虚拟机规范中只规定了一个指向对象的引用,并没有定义引用应该通过什么方式定位、访问堆中的对象具体位置,所以对象的访问方式取决于虚拟机的实现。
主流的方法分为句柄访问和直接指针访问两种,就HotSpot而言主要使用的是第二种直接指针访问进行对象访问(如果使用了ShenandoahGC收集器时会有额外的转发)
-
句柄访问
在使用句柄访问的时候,Java堆中将可能会划分出一块内存在作为句柄池,reference中存储的是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自具体的地址信息好处:reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不会改变
-
直接指针访问
在使用直接指针访问时,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象实例地址,如果只是访问对象本身的话,就不需要多一次间接的定位开销好处:访问速度更快,它节省了一次指针定位的时间开销
2 实例数据(成员变量)
实例数据部是对象真正存储的有效信息,即我们在代码中定义的各种成员变量
成员变量占用内存的大小:
-
引用类型:
-XX:+UseCOmparessedOops 开始的话4个字节,不开启则为8个字节 -
基本数据类型:
3 对齐填充
对象的对齐填充不是必然存在的,也没有特别的含义,仅仅起到了占位符的作用。
由于HotSpot虚拟机的自动内存管理系统要求任何对象的大小都必须是8字节的整数倍。对象头部分正好是8字节的整数倍,如果当实例数据部分不是8字节整数倍时就需要对齐填充占位符来补全。
假如这个对象前面几项占用的字节为15则会补1个字节,则这个对象占用16字节
4 实验——观察对象的大小
使用技术:java Agent
JavaAgent 是JDK 1.5 以后引入的,也可以叫做Java代理。
JavaAgent 是运行在 main方法之前的拦截器,它内定的方法名叫 premain,也就是说先执行 premain 方法然后再执行 main 方法。
class文件在loading到内存中的时候,可以通过javaAgent做代理,抓取class的二进制字节码进行操作。
1.新建项目ObjectSize
-
新建项目ObjectSize (1.8)
-
创建文件ObjectSizeAgent
package com.ls.jvm.agent; import java.lang.instrument.Instrumentation; /** * Created by 刘绍 on 2020/2/16. */ public class ObjectSizeAgent { private static Instrumentation inst; public static void premain(String agentArgs, Instrumentation _inst) { inst = _inst; } public static long sizeOf(Object o) { return inst.getObjectSize(o); } }
-
src目录下创建META-INF/MANIFEST.MF
Manifest-Version: 1.0 Created-By: mashibing.com Premain-Class: com.mashibing.jvm.agent.ObjectSizeAgent
注意Premain-Class这行必须是新的一行(回车 + 换行),确认idea不能有任何错误提示
-
打包jar文件
-
在需要使用该Agent Jar的项目中引入该Jar包 project structure - project settings - library 添加该jar包
-
运行时需要该Agent Jar的类,加入参数:
-javaagent:C:\work\ijprojects\ObjectSize\out\artifacts\ObjectSize_jar\ObjectSize.jar -
如何使用该类:
import com.ls.jvm.agent.ObjectSizeAgent; public class SizeOfAnObject { public static void main(String[] args) { System.out.println(ObjectSizeAgent.sizeOf(new Object()));//16 /*对象头8个字节+类指针4个字节,由于默认开启了-XX:+UseComparessedClassPointers 所以是4个字节,成员变量没有所以对象字节为8+4=12 最后Padding对齐, 最后结果为16字节*/ System.out.println(ObjectSizeAgent.sizeOf(new int[] {}));//16 /*对象头8个字节+类指针4个字节,由于默认开启了-XX:+UseComparessedClassPointers 所以是4个字节,数组长度4个字节,成员变量没有所以对象字节为8+4+4=16。 结果是8的倍数所以不需要最后Padding对齐,最后结果为16字节*/ /*在执行main方法的时候加入参数 -XX:-UseComparessedClassPointers 关闭类指针压缩 ObjectSizeAgent.sizeOf(new Object()) 为16字节 对象头8个字节+类指针8个字节=16字节 ObjectSizeAgent.sizeOf(new int[] {}) 对象头8个字节+类指针8个字节+数组长度4个字节+Padding对齐4个字节 = 24字节*/ System.out.println(ObjectSizeAgent.sizeOf(new P()));//32 //对象头8个字节+类指针4个字节+4+4+4+1+1+4+1+Padding对齐1个字节 = 24字节 } private static class P { //8 _markword //4 _oop指针 //-XX:+UseCOmparessedOops 成员变量类型引用指针。默认开启4字节,不开启为8字节 int id; //4 String name; //4 int age; //4 byte b1; //1 byte b2; //1 Object o; //4 byte b3; //1 } }
参考:《深入理解Java虚拟机》第3版 周志明著