目录
3.1.2. 类型指针(Class Metadata Pointer)
一. 前言
作为一名 Java 程序员,我们在日常工作中使用这门面向对象的编程语言时,做的最频繁的操作大概就是去创建一个个的对象了。对象的创建方式有很多,可以通过 new、Spring 管理 Bean、反射、clone、反序列化等不同方式来创建,但最终使用时对象都要被放到内存中,那么你知道在内存中的 Java 对象是由哪些部分组成以及是怎么存储的吗?这篇文章可带你深入了解 Java 对象的内存布局。
二. Java 对象的内存布局
Java 对象的内存布局分为两种,普通对象和数组对象。


通过图中可以看出,数组对象只是在对象头里多了数组长度这一项,普通对象(非数组对象)没有这一项,也不分配内存空间。
三. Java 对象结构
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐补全。
3.1. 对象头
对象头由三部分组成:
- Mark Word:存储自身的运行时数据,例如 HashCode、GC 年龄、锁相关信息等内容。
- Class Metadata Pointer:类型指针指向它的类元数据的指针。
- Length:记录数组长度。只有对象是数组的情况下,才有这部分数据,若对象不是数组,则没有这部分,不分配空间。
3.1.1. Mark Word
用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持久的锁、偏向线程的 ID 等,通过存储的内容得知对象头是锁机制和 GC 的重要基础。
32位虚拟机的 Mark Word 的字节分配:
- 无锁 —— 对象的 hashcode:25bit;存放对象分代年龄:4bit; 存放是否偏向锁的标志位:1bit; 存放锁标志位为01:2bit。
- 偏向锁 —— 在偏向锁中划分更细。开辟 25bit 的空间,其中存放线程 ID:23bit;存放Epoch:2bit;存放对象分代年龄:4bit;存放是否偏向锁标志:1bit (0表示无锁,1表示偏向锁);锁的标志位为01:2bit。
- 轻量级锁 —— 在轻量级锁中直接开辟 30bit 的空间存放指向栈中锁记录的指针,2bit 存放锁的标志位,其标志位为00。
- 重量级锁 —— 在重量级锁中和轻量级锁一样,30bit 的空间用来存放指向重量级锁的指针,2bit 存放锁的标志位,其标志位为10。
- GC 标记 —— 开辟 30bit 的内存空间却没有占用,2bit 空间存放锁标志位为11。其中无锁和偏向锁的锁标志位都是01,只是在前面的 1bit 区分了这是无锁状态还是偏向锁状态。
64位虚拟机的 Mark Word 的字节分配:
- 无锁 —— unused:25bit;对象的 hashcode:31bit;Cmc_free:1bit;存放对象分代年龄:4bit; 存放是否偏向锁的标志位:1bit; 存放锁标志位为01:2bit。
- 偏向锁 —— 偏向线程 ID:54bit;存放Epoch:2bit;Cmc_free:1bit;存放对象分代年龄:4bit;存放是否偏向锁标识:1bit (0表示无锁,1表示偏向锁);锁的标志位为01:2bit。
- 轻量级锁 —— 在轻量级锁中直接开辟 62bit 的空间存放指向栈中锁记录的指针,2bit 存放锁的标志位,其标志位为00。
- 重量级锁 —— 在重量级锁中和轻量级锁一样,62bit 的空间用来存放指向重量级锁的指针,2bit 存放锁的标识位,为10。
- GC 标记 —— 开辟 62bit 的内存空间却没有占用,2bit 空间存放锁标志位为11。其中无锁和偏向锁的锁标志位都是01,只是在前面的 1bit 区分了这是无锁状态还是偏向锁状态。
3.1.2. 类型指针(Class Metadata Pointer)
类型指针指向类的元数据地址,JVM 通过这个指针确定对象是哪个类的实例。32位的 JVM 占32位,4个字节,64位的 JVM 占64位,8个字节,但是64位的 JVM 默认会开启指针压缩,压缩后也只占4字节。
如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的 JVM 将会比32位的JVM 多耗费50%的内存。所以会默认开启指针压缩(如不开启,类型指针将占用8字节),UseCompressedOops 是默认开启的,该参数表示开启指针压缩,会将原来64位的指针压缩为32位(即由原8字节压缩到4字节大小)。
-XX:+UseCompressedClassPointers //开启压缩类指针
-XX:-UseCompressedClassPointers //关闭压缩类指针
// 这个JVM参数依赖UseCompressedOops这个参数,UseCompressedOops开启,UseCompressedClassPointers默认开启,可手工关闭,
// UseCompressedOops关闭,UseCompressedClassPointers不管开启还是关闭都不生效即不压缩。
3.1.3. 数组长度(Length)
如果对象是普通对象(非数组对象),则没有这部分,不占用空间。如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着 JVM 架构的不同而不同:32位的JVM上,长度为32位(4字节);64位 JVM 则为64位(8字节)。64位 JVM 如果开启+UseCompressedOops 选项,该区域长度也将由64位压缩至32位。
3.2. 实例数据
普通对象:实例数据。
数组对象:数组中的实例数据。
实例数据存放的是非静态的属性,也包括父类的所有非静态属性(private 修饰的也在这里,不区分可见性修饰符),基本类型的属性存放的是具体的值,引用类型及数组类型存放的是引用指针。
实例数据不同的类型所占的空间不同:
| 数据类型 | 占用空间 |
|---|---|
| char | 2个字节 |
| boolean | 1个字节 |
| byte | 1个字节 |
| short | 2个字节 |
| int | 4个字节 |
| long | 8个字节 |
| float | 4个字节 |
| double | 8个字节 |
| 对象引用 | 对象指针压缩默认是被开启的,占用4个字节,配置 JVM 参数 -XX:+UseCompressedOops 后,占用8个字节 |
64 位的 JVM 默认开启普通对象指针压缩 -XX:+UseCompressedOops (OOP 即 ordinary object pointer),由原8字节压缩到4字节大小。
3.3. 对齐填充(Padding)
用于补齐对象内存长度的。因为 JVM 要求 Java 代码的对象必须是 8bit 的倍数。如果一个对象用不到 8N 个字节则需要对其填充,以此来补齐对象头和实例数据占用内存之后剩余的空间大小。如果对象头和实例数据已经占满了 JVM 所分配的内存空间,那么就不用再进行对齐填充了。所有的对象分配的字节总 SIZE 需要是8的倍数,如果前面的对象头和实例数据占用的总 SIZE 不满足要求,则通过对齐数据来填满。
四. JVM 之 指针压缩
4.1. 压缩指针的由来
计算机操作系统分32位和64位,这里的位在计算机里是用0和1来表示的,用32个(或64个)二进制0和1的组合来表示内存地址。
以32位为例,在普通的内存中,对象的大小最小是以1字节来计算的,通过0和1的排列组合,能够表示寻址的内存空间最大就是个,换算成内存空间就是
/ 1024 / 1024 / 1024 = 4G,也就是说32位的操作系统最大能寻址的内存空间只有4G。
同理,64位的操作系统(查阅资料显示其实没有用到64位,最多只用到了48位,这个可自行查阅资料,反正肯定比32位大的多) / 1024 / 1024 / 1024 / 1024 = 256TB,这样内存就足够大了,但是目前还没有厂商能生产出这么大的内存。
4G 对于现在的 Java 应用系统来说,内存已经算小的了,那我们就会想到使用64位的系统,这样内存就可以更大了,但是当我们准备将32位系统切换到64位系统,起初我们可能会期望系统性能会立马得到提升,但现实情况可能并不是这样的,为什么呢?
- 32位系统对象指针是4字节,64位系统对象指针是8字节(1位表示1bit,8个bit 表示1字节),这样64位系统中的对象引用占用的内存空间是32位系统中的两倍大小,因此间接的导致了在64位系统中更多的内存消耗以及更频繁的 GC 发生,GC 占用的 CPU 时间越多,那么我们的应用程序占用 CPU 的时间就越少,响应会变慢,吞吐量会降低。
- 对象的引用变大了,那么 CPU 可缓存的对象相对就少了,降低了 CPU 缓存命中率,增加了对内存的访问,CPU 对 CPU 缓存的访问速度可比对内存的访问速度快太多了,所以大量的对内存访问,会降低 CPU 的执行效率,增加了执行时间,从而影响性能。
既然32位系统内存不够,64位内存够但又影响性能,那有没有折中方案来解决这两个问题呢,于是聪明的 JVM 开发者想到了利用压缩指针,在64位的操作系统中利用32位的对象指针引用获得超过 4G 的内存寻址空间。
4.2. 如何压缩指针
由于在 JVM 里,对象都是以8字节对齐的(即对象的大小都是8的倍数),所以不管用32位还是64位的二进制表示,末尾3位始终都是0。既然 JVM 已经知道了这些对象的内存地址后三位始终是0,那么这些无意义的0就没必要在堆中继续存储。相反,我们可以利用存储0的这3位 bit 存储一些有意义的信息,这样我们就多出3位 bit 的寻址空间,也就是说如果我们继续使用32位来存储指针,只不过后三位原本用来存储0的 bit 现在被我们用来存放有意义的地址空间信息,当寻址的时候,JVM 将这32位的对象引用左移3位即可(后三位补0)。我们原本32位的内存寻址空间一下变成了35位,可寻址的内存空间变为 / 1024 / 1024 / 1024 = 32G,也就是说在64位系统 JVM 的内存可扩大到 32G 了,基本上可满足大部分应用的使用了。
所以在64位系统下,通过压缩指针我们可以继续使用32位来处理(引用指针由8字节可降低到4字节),存储的时候右移3位,寻址的时候左移3位,如下图所示:

这样一来,JVM 虽然额外的执行了一些位运算但是极大的提高了寻址空间,并且将对象引用占用内存大小降低了一半,节省了大量空间,况且这些位运算对于 CPU 来说是非常容易且轻量的操作,可谓是一举两得。
4.3. 如何进一步扩大寻址空间
前边提到我们在 Java 虚拟机堆中对象起始地址均需要对齐至8的倍数,不过这个数值我们可以通过 JVM 参数 -XX:ObjectAlignmentInBytes 来改变(默认值为8)。当然这个数值的必须是2的次幂,数值范围需要在 8 - 256 之间。
正是因为对象地址对齐至8的倍数,才会多出3位 bit 让我们存储额外的地址信息,进而将 4G 的寻址空间提升至 32G。
同样的道理,如果我们将 ObjectAlignmentInBytes 的数值设置为16呢?
对象地址均对齐至16的倍数,那么就会多出4位 bit 让我们存储额外的地址信息。寻址空间变为 / 1024 / 1024 / 1024 = 64G。
通过以上规律,我们就能知道,在64位系统中开启压缩指针的情况,寻址范围的计算公式:4G * ObjectAlignmentInBytes = 寻址范围。
但是并不建议大家贸然这样做,因为增大了 ObjectAlignmentInBytes 虽然能扩大寻址范围,但是这同时也可能增加了对象之间的字节填充,导致压缩指针没有达到原本节省空间的效果。
4.4. JVM 压缩指针参数
可以通过以下命令查看 Java 命令默认的启动参数:
java -XX:+PrintCommandLineFlags -version

通过下面这个命令,可以看到所有JVM参数的默认值:
java -XX:+PrintFlagsFinal -version

关于压缩指针的两个参数:
- UseCompressedClassPointers:压缩类指针(开启时类指针占4字节,关闭时类指针占8字节);
- UseCompressedOops:压缩普通对象指针(开启时引用对象指针占4字节,关闭时引用对象指针占8字节)。
Oops 是 Ordinary object pointers 的缩写,这两个参数默认是开启的,即 -XX:+UseCompressedClassPointers,-XX:+UseCompressedOops,也可手动设置,如下所示:
-XX:+UseCompressedClassPointers //开启压缩类指针
-XX:-UseCompressedClassPointers //关闭压缩类指针
-XX:+UseCompressedOops //开启压缩普通对象指针
-XX:-UseCompressedOops //关闭压缩普通对象指针
注:32位 HotSpot VM 是不支持 UseCompressedOops 参数的,只有64位 HotSpot VM 才支持。Oracle JDK 从6 update 23开始在64位系统上会默认开启压缩指针。
五. Java 对象大小计算
5.1. 对象大小计算公式
以下表格展示了对象中各部分所占空间大小,单位:字节。
| 类型 | 所属部分 | 占用空间大小(压缩开启) | 占用空间大小(压缩关闭) |
|---|---|---|---|
| Markwork | 对象头 | 8 | 8 |
| 类型指针 | 对象头 | 4 | 8 |
| 数组长度 | 对象头 | 4 | 4 |
| byte | 对象体 | 1 | 1 |
| boolean | 对象体 | 1 | 1 |
| short | 对象体 | 2 | 2 |
| char | 对象体 | 2 | 2 |
| int | 对象体 | 4 | 4 |
| float | 对象体 | 4 | 4 |
| long | 对象体 | 8 | 8 |
| double | 对象体 | 8 | 8 |
| 对象引用指针 | 对象体 | 4 | 8 |
| 对齐填充 | 对齐填充 | 对象头+对象体是8的倍数?0 :8 -(对象头+对象体)% 8 | 对象头+对象体是8的倍数?0 :8 -(对象头+对象体)% 8 |
计算公式:对象大小 = 对象头 + 对象体(对象是数组时,对象体的大小=引用指针占用空间大小 *对象个数) + 对齐填充。
64位操作系统 32G 内存以下,默认开启对象指针压缩,对象头是12字节,关闭指针压缩,对象头是16字节。内存超过 32G 时,则自动关闭指针压缩,对象头占16字节。
5.2. 对象分析
使用 JOL 工具分析 Java 对象大小:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version>
</dependency>
常用类及方法:
查看对象内部信息: ClassLayout.parseInstance(obj).toPrintable();
查看对象外部信息:GraphLayout.parseInstance(obj).toPrintable();
查看对象占用空间总大小:GraphLayout.parseInstance(obj).totalSize();
查看类内部信息:ClassLayout.parseClass(Object.class).toPrintable()。
使用到的测试类:
@Setter
class Goods {
private byte b;
private char type;
private short age;
private int no;
private float weight;
private double price;
private long id;
private boolean flag;
private String goodsName;
private LocalDateTime produceTime;
private String[] tags;
public static String str;
public static int temp;
}
5.3. 非数组对象,开启指针压缩
64位 JVM,堆内存小于 32G 的情况下,默认是开启指针压缩的。
public static void main(String[] args) {
Goods goods = new Goods();
goods.setAge((short) 10);
goods.setNo(123456);
goods.setId(111L);
goods.setGoodsName("方便面");
goods.setFlag(true);
goods.setB((byte)1);
goods.setPrice(1.5d);
goods.setProduceTime(LocalDateTime.now());
goods.setType('A');
goods.setWeight(0.065f);
goods.setTags(new String[] {"food", "convenience", "cheap"});
Goods.str = "test";
Goods.temp = 222;
System.out.println(ClassLayout.parseInstance(goods).toPrintable());
}
先不看输出结果,按上面的公式计算一下对象的大小:
- 对象头:8字节(Mark Word)+4字节(类指针)=12字节;
- 对象体:1字节(属性 b)+ 2字节(属性 type)+ 2字节(属性 age)+ 4字节(属性 no)+ 4字节(属性 weight)+ 8字节(属性 price)+ 8字节(属性 id)+ 1字节(属性 flag) + 4字节(属性 goodsName 指针) + 4字节(属性 produceTime 指针) + 4字节(属性 tags 指针)= 42字节(注意:静态属性不参与对象大小计算);
- 对齐填充:8 -(对象头+对象体)% 8 = 8 - (12 + 42) % 8 = 2字节;
- 对象大小 = 对象头 + 对象体 + 对齐填充 = 12字节 + 42字节 + 2字节 = 56字节。
执行看运行结果:
com.star95.study.jvm.Goods object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x2000c043
12 4 int Goods.no 123456
16 8 double Goods.price 1.5
24 8 long Goods.id 111
32 4 float Goods.weight 0.065
36 2 char Goods.type A
38 2 short Goods.age 10
40 1 byte Goods.b 1
41 1 boolean Goods.flag true
42 2 (alignment/padding gap)
44 4 java.lang.String Goods.goodsName (object)
48 4 java.time.LocalDateTime Goods.produceTime (object)
52 4 java.lang.String[] Goods.tags [(object), (object), (object)]
Instance size: 56 bytes
Space losses: 2 bytes internal + 0 bytes external = 2 bytes total
这里有一个特殊的地方,打印输出的属性顺序跟代码里的顺序不一致,这是因为 JVM 进行优化,也就是指令重排序,会根据属性类型的大小、执行的先后顺序对结果是否有影响、最小填充大小等因素计算出对象最小应占用的空间。
5.4. 非数组对象,关闭指针压缩
关闭压缩指针,类指针和引用对象指针都占8字节,推算一下对象大小:
- 对象头:8字节(Mark Word)+ 8字节(类指针)= 16字节;
- 对象体:1字节(属性 b)+ 2字节(属性 type)+ 2字节(属性 age)+ 4字节(属性 no)+ 4字节(属性 weight)+ 8字节(属性 price)+ 8字节(属性 id)+ 1字节(属性 flag) + 8字节(属性 goodsName 指针) + 8字节(属性 produceTime 指针) + 8字节(属性 tags 指针)= 54字节(注意:静态属性不参与对象大小计算);
- 对齐填充:8 -(对象头+对象体)% 8 = 8 - (16 + 54) % 8 = 2字节;
- 对象大小 = 对象头 + 对象体 + 对齐填充 = 16字节 + 54字节 + 2字节 = 72字节。
运行时增加JVM参数如下:
-XX:-UseCompressedClassPointers -XX:-UseCompressedOops
public class ObjectLayOut1 {
public static void main(String[] args) {
Goods goods = new Goods();
goods.setAge((short) 10);
goods.setNo(123456);
goods.setId(111L);
goods.setGoodsName("方便面");
goods.setFlag(true);
goods.setB((byte)1);
goods.setPrice(1.5d);
goods.setProduceTime(LocalDateTime.now());
goods.setType('A');
goods.setWeight(0.065f);
goods.setTags(new String[] {"food", "convenience", "cheap"});
Goods.str = "test";
Goods.temp = 222;
System.out.println(ClassLayout.parseInstance(goods).toPrintable());
}
}
执行看运行结果:
com.star95.study.jvm.Goods object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 8 (object header: class) 0x00000000175647b8
16 8 double Goods.price 1.5
24 8 long Goods.id 111
32 4 int Goods.no 123456
36 4 float Goods.weight 0.065
40 2 char Goods.type A
42 2 short Goods.age 10
44 1 byte Goods.b 1
45 1 boolean Goods.flag true
46 2 (alignment/padding gap)
48 8 java.lang.String Goods.goodsName (object)
56 8 java.time.LocalDateTime Goods.produceTime (object)
64 8 java.lang.String[] Goods.tags [(object), (object), (object)]
Instance size: 72 bytes
Space losses: 2 bytes internal + 0 bytes external = 2 bytes total
5.5. 数组对象开启指针压缩
默认是开启压缩指针的,类指针和引用对象指针都占4字节,推算一下对象大小:
- 对象头:8字节(Mark Word)+ 4字节(类指针) + 4字节(数组长度)= 16字节;
- 对象体:4字节 * 3 = 12字节;
- 对齐填充:8 -(对象头+对象体)% 8 = 8 - (16字节 + 12字节)% 8 = 4字节;
- 对象大小 = 对象头 + 对象体 + 对齐填充 = 16字节 + 12字节 + 4字节 = 32字节。
public class ObjectLayOut1 {
public static void main(String[] args) {
Goods goods = new Goods();
goods.setAge((short) 10);
goods.setNo(123456);
goods.setId(111L);
goods.setGoodsName("方便面");
goods.setFlag(true);
goods.setB((byte)1);
goods.setPrice(1.5d);
goods.setProduceTime(LocalDateTime.now());
goods.setType('A');
goods.setWeight(0.065f);
goods.setTags(new String[] {"food", "convenience", "cheap"});
Goods.str = "test";
Goods.temp = 222;
Goods[] goodsArr = new Goods[3];
goodsArr[0] = goods;
System.out.println(ClassLayout.parseInstance(goodsArr).toPrintable());
}
}
执行看运行结果:
[Lcom.star95.study.jvm.Goods; object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x2000c18d
12 4 (array length) 3
16 12 com.star95.study.jvm.Goods Goods;.<elements> N/A
28 4 (object alignment gap)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
5.6. 数组对象关闭指针压缩
关闭压缩指针,类指针和引用对象指针都占8字节,推算一下对象大小:
- 对象头:8字节(Markword)+8字节(类指针) + 4字节(数组长度)= 20字节;
- 对象体:8字节 * 3 = 24字节;
- 对齐填充:8 -(对象头+对象体)% 8 = 8 - (20+ 24) % 8 = 4字节;
- 对象大小 = 对象头 + 对象体 + 对齐填充 = 20字节 + 24字节 + 4字节 = 48字节。
运行时增加 JVM 参数如下:
-XX:-UseCompressedClassPointers -XX:-UseCompressedOops
public class ObjectLayOut1 {
public static void main(String[] args) {
Goods goods = new Goods();
goods.setAge((short) 10);
goods.setNo(123456);
goods.setId(111L);
goods.setGoodsName("方便面");
goods.setFlag(true);
goods.setB((byte)1);
goods.setPrice(1.5d);
goods.setProduceTime(LocalDateTime.now());
goods.setType('A');
goods.setWeight(0.065f);
goods.setTags(new String[] {"food", "convenience", "cheap"});
Goods.str = "test";
Goods.temp = 222;
Goods[] goodsArr = new Goods[3];
goodsArr[0] = goods;
System.out.println(ClassLayout.parseInstance(goodsArr).toPrintable());
}
}
执行看运行结果:
[Lcom.star95.study.jvm.Goods; object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 8 (object header: class) 0x0000000017e04d70
16 4 (array length) 3
20 4 (alignment/padding gap)
24 24 com.star95.study.jvm.Goods Goods;.<elements> N/A
Instance size: 48 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
通过以上对象分析,我们看到在开启压缩指针的情况下,对象的大小会小很多,节省了内存空间。
六. 总结
通过以上的分析,基本已经把 Java 对象的结构讲清楚了,另外对象占用内存空间大小也计算出来了,有助于进行 JVM 调优分析,64位的虚拟机内存在 32G 以下时默认是开启压缩指针的,超过32G 自动关闭压缩指针,主要目的都是为了提高寻址效率。
另外,本文是通过 JOL 工具计算对象占用空间的大小,不包括引用对象实际占用的内存大小,因为计算时是按引用对象的指针占用空间大小计算的,可能跟其他工具计算的结果不一样,具体跟工具的计算逻辑有关,比如跟 JDK 自带的 jvisualvm 工具通过堆 dump 出来看到的对象大小不一样,感兴趣的可自行验证。
本文深入介绍Java对象的内存布局,包括对象头(Mark Word、类型指针、数组长度)、实例数据和对齐填充。还阐述了JVM指针压缩的由来、方法及相关参数,给出了Java对象大小的计算公式,并通过JOL工具对不同情况下的对象大小进行分析,有助于JVM调优。
1351

被折叠的 条评论
为什么被折叠?



