文章目录
- 概述
- 1. 对象头 (Header)
- Mark Word
- 1. 32位HotSpot虚拟机中的MarkWord
- 2. 64位HotSpot虚拟机中的MarkWord
- Class Pointer
- Array Length
- 指针压缩原理
- 指针压缩测试
- 2. 实例数据 (Instance Data)
- 3. 填充数据 (Padding Data)
- 查看 Java 对象的内存结构
- 使用反射和VisualVM、JConsole等工具
- 使用JOL
- 引入Maven依赖
- 测试样例
- 参考
概述
Java 对象的内存结构对于理解 Java 内存管理和性能优化至关重要。本文将详细介绍 Java 对象的内部结构,并提供查看这些信息的方法。
Java 对象由三个主要部分组成:
- 对象头 (Header)
- 实例数据 (Instance Data)
- 填充数据 (Padding Data)
1. 对象头 (Header)
对象头主要用于存储对象自身的运行时数据,如哈希码、分代年龄、锁状态等。
一般对象头分为三部分:
- Mark Word:存储对象的哈希值、分代年龄、锁状态等。
- Class Pointer:存储对象的类的元数据的指针。
- Array Length:用于存储数组的长度,占4字节。
Mark Word
Mark Word 是对象头中最关键的部分,它包含了对象的哈希值、分代年龄、锁状态等。Mark Word 的具体内容会根据锁的状态改变而改变。
众所周知,计算机分为32位和64位,操作系统也分为32位和64位。32位计算机需要安装32位的操作系统,64位计算机需要安装64位系统。32位操作系统最大内存是4GB,因为32位二进制能够表示的最大数是2^32
,即22102410241024。而64位操作系统能够访问的最大内存是2^64
,即16EB1。
1. 32位HotSpot虚拟机中的MarkWord
2. 64位HotSpot虚拟机中的MarkWord
考虑到现在基本都是在64位操作系统上安装了64位的JDK,JVM自然也是64位的。本文以64位HotSpot为例,介绍Mark Word结构,其他JVM中的Java对象内存结构可能有所差异,大家可以自行查阅相关资料。
Mark Word中各字段是什么含义呢?
- hashCode:对象本身的哈希码。调用方法 System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord中没有足够的空间保存它,因此会被移动到线程 Monitor中。
- 分代年龄:在GC中,当survivor区中对象复制一次,年龄加1。如果到15之后会移动到老年代,并发GC的年龄阈值为6。
- 是否偏向锁:对象是否存在偏向锁标记
- 锁标志位:对象的锁标志状态。在并发的情况下,可以通过锁标志判断象是否被线程占用。01是初始状态,未加锁。随着锁级别的不同,对象头里会存储不同的内容。
- 偏向锁存储的是当前占用此对象的线程ID(此处的线程ID是操作系统层面的线程唯一ID,与Java中的线程ID是不一致的,了解即可)。
- 轻量级锁存储的是指向线程栈中锁记录的指针
- 重量级锁存储的是指向互斥锁的指针
- Epoch:偏向锁的时间戳
(是否偏向锁) | 锁标志位 2bit | 锁状态 |
---|---|---|
0(代表无锁) | 01 | 无锁态(new) |
1(偏向锁) | 01 | 偏向锁 |
- | 00 | 轻量级锁(自旋锁、无锁、自适应自旋锁) |
- | 10 | 重量级锁 |
- | 11 | GC 标记 |
TODO:以上各种锁的应用及升级情形也是一个大工程,容我以后再补。
^_^
可以看到64位虚拟机其实是浪费了一部分空间的,JVM支持通过-XX:+UseCompressedOops -XX:-UseCompressedClassPointers参数来进行指针压缩。
Class Pointer
Class Pointer,也叫Class Metadata Address,这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过它确定对象是哪个类的实例。该指针的位长度为JVM的一个Word大小,即 32位的JVM 为 32位,64位的JVM为 64位。
如果应用的对象过多,使用 64位的指针将浪费大量内存,统计而言,64位的 JVM将会比 32位的 JVM多耗费 50%的内存。为了节约内存可以使用选项 +UseCompressedOops
开启指针压缩,其中,oop即 ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:
- 每个 Class的属性指针(即静态变量)
- 每个对象的属性指针(即对象变量)
- 普通对象数组的每个元素指针
当然,也不是所有的指针都会压缩,一些特殊类型的指针 JVM不会优化,比如指向 PermGen的 Class对象指针(JDK8中指向元空间的 Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。
在64位JVM虚拟机中Mark Word、Class Pointer这两部分都是64位的,所以也就是需要128位大小(16 bytes)。从JDK1.6 update14开始,在64位操作系统中,JVM开始支持指针压缩。JDK1.8默认开启了指针压缩。
参数 | 作用 |
---|---|
-XX:+UseCompressedOops | 开启对象指针压缩 |
-XX:-UseCompressedOops | 关闭对象指针压缩 |
-XX:+UseCompressedClassPointers | 开启Class Pointer指针压缩 |
-XX:-UseCompressedClassPointers | 关闭Class Pointer指针压缩 |
一般只有虚拟机内存达到32G以上,4个字节已经无法满足寻址需求时,才需要关闭指针压缩
Array Length
用于存储数组的长度,这是数组类型的对象独有的,占用4字节。
指针压缩原理
在JVM里,对象都是以8字节对齐的,即对象的大小都是8的倍数,所以不管在32位还是64位的JVM里对象地址的末尾3位始终都是0。
例如:
- 08 = 00001000
- 16 = 00010000
- 24 = 00011000
- 32 = 00100000
既然JVM已经知道了这些对象的内存地址后三位始终是0,那么这些0就没必要在内存中继续存储。相反,我们可以利用这3位存储一些有意义的信息,这样我们就多出3位的寻址空间,也就是说如果我们继续使用32位来存储指针,只不过后三位被我们用来存放有意义的地址空间信息,当寻址的时候,JVM将这32位的对象引用左移3位(后三位补0)即可。我们原本32位的内存寻址空间一下变成了35位,可寻址的内存空间变为2 ^ 35,2 ^ 5 * 1024 * 1024 * 1024 = 32G,也就是说在32位系统JVM的内存可扩大到32G了,基本上可满足大部分应用的使用了。
现在电脑内存普遍在8GB、16GB,而16GB内存的地址空间范围是0000000000000000000000000000000000 - 1111111111111111111111111111111111。
假如创建对象时申请到一个内存地址,位置是8589934616(2 ^ 33 + 3 * 8),下面以这个地址演示指针压缩的原理。
指针压缩有两个过程,分别是编码和解码。
- 编码:编码是指获得到内存地址后,右移三位,得到压缩后的内存地址。
8589934616转换为二进制为1000000000000000000000000000011000,右移三位得到1000000000000000000000000000011,即1073741827,它就是压缩后的内存地址。 - 解码:解码是在操作内存前,把原来地址左移三位,得到真实的内存地址。
1073741827转换为二进制为1000000000000000000000000000011,左移三位得到1000000000000000000000000000011000,即8589934616,它就是真实的内存地址。
32位系统的最大内存地址是2 ^ 32,即4294967296。它小于上述演示地址8589934616,所以32位系统不能直接操作它,但借助一些辅助技术就可以直接进行操作了。
其实上面涉及到JVM的一个参数:ObjectAlignmentInBytes,这个参数表示Java堆中的对象,需要按照几字节对齐,范围是[8-256],必须是2的n次方。在JDK1.8中该参数默认值是8,也就是Java默认是8字节对齐。此时,如果配置最大堆内存超过32GB(实际略小于32GB就会失效),那么指针压缩会失效。
你可以使用-XX:ObjectAlignmentInBytes=16
修改该配置为16,那么最大堆内存超过64GB时指针压缩才会失效。同理,配置为32时,超过128GB才会失效。
虽然增大ObjectAlignmentInBytes能扩大寻址范围,这同时也会增加对象之间的填充数据,浪费内存和缓存空间,从而导致压缩指针没有达到原本节省空间的效果。
-XX:ObjectAlignmentInBytes=16
# 查看ObjectAlignmentInBytes配置
java -XX:+PrintFlagsFinal -version | grep ObjectAlignmentInBytes
intx ObjectAlignmentInBytes = 8 {lp64_product}
java version "1.8.0_421"
Java(TM) SE Runtime Environment (build 1.8.0_421-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.421-b09, mixed mode)
指针压缩测试
写一个简单的User类,使用JOL(不熟悉的请参考后面的教程)对它进行测试,代码如下:
package org.hbin.jol;
import org.openjdk.jol.info.ClassLayout;
import java.util.Date;
/**
* @author Haley
* @version 1.0
* 2024/8/22
*/
public class JOLTest {
public static void main(String[] args) {
System.out.println(ClassLayout.parseInstance(new User()).toPrintable());
}
static class User {
long id;
String name;
int age;
Date birthday;
Date createTime;
int[] arr;
}
}
测试结果:
UseCompressedOops | UseCompressedClassPointers | 对象大小 |
---|---|---|
+ | + | 40 bytes |
- | - | 64 bytes |
+ | - | 48 bytes |
- | + | 64 bytes1 |
2. 实例数据 (Instance Data)
实例数据部分存储了对象的实际数据,也就是对象属性的值。这部分的大小取决于对象的属性数量和类型。
3. 填充数据 (Padding Data)
为了方便虚拟机的寻址,JVM要求对象结构的内存大小是8字节的整数倍。如果对象的大小的字节数不是8的倍数,那么 JVM 会添加一些额外的字节达到8的倍数来满足对齐要求。
例如对象头12个字节,实例数据只有一个byte变量,则填充数据是3个字节,12+1+3=16,是8的倍数。
填充数据不是必须存在的,仅仅是为了字节对齐。
只有存在填充数据时才有,否则为0字节
查看 Java 对象的内存结构
使用反射和VisualVM、JConsole等工具
使用反射可以来获取对象的内部信息,如属性信息、方法信息等。
利用VisualVM、JConsole等工具也可以查看对象在JVM中占用的空间信息、回收信息等。
使用JOL
Java对象布局(Java Object Layout),也叫JOL,是一个用来分析 JVM 中对象内存布局的小工具。可以用于查看对象在内存中的占用情况,实例对象的引用情况等。
引入Maven依赖
当前最新版本为0.17,你也可以根据需要自行选择或查找最新版本。
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version>
</dependency>
测试样例
package org.hbin.jol;
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
/**
* @author Haley
* @version 1.0
* 2024/8/22
*/
public class JOLTest {
public static void main(String[] args) {
System.out.println(VM.current().details());
System.out.println(ClassLayout.parseInstance(new int[]{1, 2, 3}).toPrintable());
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
}
}
输出信息:
# VM mode: 64 bits
# Compressed references (oops): 3-bit shift
# Compressed class pointers: 3-bit shift
# Object alignment: 8 bytes
# ref, bool, byte, char, shrt, int, flt, lng, dbl
# Field sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8
# Array base offsets: 16, 16, 16, 16, 16, 16, 16, 16, 16
[I object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800016d
12 4 (array length) 3
16 12 int [I.<elements> N/A
28 4 (object alignment gap)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以切换指针压缩的开关,查看各对象在内存中占用的空间信息是否有变化。
参考
- JVM–对象的结构
- JAVA基础篇–JVM–3对象结构
- JOL(java object layout): java 对象内存布局
- 常见面试-JVM-指针压缩
关闭UseCompressedOops而打开UseCompressedClassPointers的情况下会输出一个警告:
Java HotSpot(TM) 64-Bit Server VM warning: UseCompressedClassPointers requires UseCompressedOops
,这是老版本遗留的BUG。从 Java 15 Build 23 开始, UseCompressedClassPointers 已经不再依赖 UseCompressedOops 了,两者在大部分情况下已经独立开来。 ↩︎ ↩︎