Java 对象内存布局:压缩指针、对齐填充与 JOL 实测
一个 Object 占多少字节?boolean 占几位?压缩指针怎么省内存?本文用 JOL 实测,把 Java 对象内存布局拆解到每一个字节。
一、对象内存布局三部分
scss
┌───────────────────────────────────────────────────────┐
│ Java 对象内存布局 │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 对象头 (Object Header) │ │
│ │ ┌───────────────────────────────────────────┐ │ │
│ │ │ Mark Word (64 bit) │ │ │
│ │ │ 锁信息 / HashCode / GC 年龄 / Epoch │ │ │
│ │ ├───────────────────────────────────────────┤ │ │
│ │ │ 类型指针 (Klass Pointer) │ │ │
│ │ │ 指向 Class 元数据 │ │ │
│ │ └───────────────────────────────────────────┘ │ │
│ │ (数组对象额外有 4 字节 array length) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 实例数据 (Instance Data) │ │
│ │ 各字段的值,按类型排列 │ │
│ │ (字段之间可能有 padding) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 对齐填充 (Padding) │ │
│ │ 对象总大小必须是 8 的整数倍 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────┘
二、对象头详解
Mark Word(64 位)
css
64 位 JVM 下 Mark Word 占 8 字节,不同锁状态存储不同内容:
┌────────────┬──────────────────────────────────────────────┐
│ 锁状态 │ Mark Word 内容 (64 bit) │
├────────────┼──────────────────────────────────────────────┤
│ 无锁 │ hash(25) | unused(31) | unused(1) | age(4) │ 0 │ 01 │
│ 偏向锁 │ threadId(54) | epoch(2) | age(4) | 1 │ 01 │
│ 轻量级锁 │ 指向 Lock Record 的指针(62) │ 00 │
│ 重量级锁 │ 指向 ObjectMonitor 的指针(62) │ 10 │
│ GC 标记 │ unused(62) │ 11 │
└────────────┴──────────────────────────────────────────────┘
锁标志位: 01=无锁/偏向, 00=轻量级, 10=重量级, 11=GC标记
类型指针(Klass Pointer)
ruby
类型指针指向 JVM 内部的 Class 元数据
开启压缩指针 (-XX:+UseCompressedOops, 默认开启):
┌──────────────┐
│ 4 bytes │ → 指向 Class 元数据
└──────────────┘
32 位指针可以寻址 4GB
但 HotSpot 使用 8 字节对齐 → 可寻址 32GB
关闭压缩指针 (-XX:-UseCompressedOops):
┌──────────────┐
│ 8 bytes │ → 指向 Class 元数据
└──────────────┘
64 位指针可以寻址 16EB
数组长度(仅数组对象)
python
数组对象额外有 4 字节的 length 字段:
┌──────────────────────────────┐
│ Mark Word 8 bytes │
├──────────────────────────────┤
│ Klass Pointer 4 bytes │ (压缩指针)
├──────────────────────────────┤
│ Array Length 4 bytes │ ← 数组独有
├──────────────────────────────┤
│ Array Data ... │
└──────────────────────────────┘
三、实例数据排列规则
java
// 字段在内存中的排列遵循以下规则:
// 1. 按类型大小降序排列 (long/double → int → short/char → byte/boolean → reference)
// 2. 父类字段在子类字段之前
// 3. 相同宽度的字段放在一起 (减少 padding)
// 4. 可以通过 @Contended 让字段独占缓存行
public class FieldLayout {
// 实际内存排列顺序可能与声明顺序不同
boolean b; // 1 byte
int i; // 4 bytes
long l; // 8 bytes
Object obj; // 4 bytes (压缩指针) / 8 bytes (非压缩)
double d; // 8 bytes
// JVM 实际排列 (宽度降序):
// [long l: 8B] [double d: 8B] [int i: 4B] [Object obj: 4B] [boolean b: 1B] [padding: 3B]
}
arduino
字段类型与大小:
┌──────────────┬───────┬────────────────────┐
│ 类型 │ 大小 │ 说明 │
├──────────────┼───────┼────────────────────┤
│ long/double │ 8 B │ 最宽,排在前面 │
│ int/float │ 4 B │ │
│ short/char │ 2 B │ │
│ byte/bool │ 1 B │ │
│ reference │ 4/8 B │ 压缩指针 4B │
└──────────────┴───────┴────────────────────┘
四、压缩指针
ruby
压缩指针原理:
64 位 JVM 默认开启压缩指针 (-XX:+UseCompressedOops)
原理: 64 位对象引用压缩为 32 位
为什么 32 位指针能寻址超过 4GB?
因为 Java 对象是 8 字节对齐的
→ 对象地址都是 8 的倍数
→ 地址的低 3 位一定是 000
→ 可以省去低 3 位 → 32 位指针实际寻址 2^35 = 32GB
┌────────────────────────────────────────────────┐
│ 64 位地址: 0x00000001A0000000 │
│ ↓ 除以 8 │
│ 32 位压缩: 0x00000340 (省去低3位) │
│ ↓ 乘以 8 还原 │
│ 64 位地址: 0x00000001A0000000 │
└────────────────────────────────────────────────┘
超过 32GB 后压缩指针失效 (-XX:+UseCompressedOops 自动关闭)
→ 引用从 4B 变为 8B → 内存占用激增!
→ 堆设为 31GB 比 32GB 更省内存!
bash
压缩指针效果:
堆 = 16GB, 1 亿个对象引用:
开启压缩 (4B/ref): 400 MB 引用内存
关闭压缩 (8B/ref): 800 MB 引用内存
节省: 400 MB!
堆 = 32GB:
压缩指针自动关闭
引用全部变成 8B → 比 31GB 堆还费内存!
五、JOL 实测
java
// JOL (Java Object Layout) - 官方工具,分析对象内存布局
// Maven 依赖:
// <dependency>
// <groupId>org.openjdk.jol</groupId>
// <artifactId>jol-core</artifactId>
// <version>0.17</version>
// </dependency>
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VMCurrent;
public class JOLDemo {
public static void main(String[] args) {
// 查看 JVM 信息
System.out.println(VMCurrent.current().details());
// 输出包含: 压缩指针状态、对象对齐、堆信息
// 1. 空对象
class Empty {}
System.out.println(ClassLayout.parseClass(Empty.class).toPrintable());
}
}
python
JOL 输出: 空对象 (Object)
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 8 (object header: Mark) 0x0000000000000001
8 4 (object header: Klass) 0x00010001
12 4 (alignment padding)
Instance size: 16 bytes
Space losses: 0 internal + 4 external = 4 bytes
分析:
├── Mark Word: 8 bytes (offset 0-7)
├── Klass Pointer: 4 bytes (offset 8-11) (压缩指针)
├── Padding: 4 bytes (offset 12-15) (对齐到 8 的倍数)
└── 总计: 16 bytes
arduino
JOL 输出: 包含各类型字段的对象
class Data {
boolean b; // 1 byte
char c; // 2 bytes
int i; // 4 bytes
long l; // 8 bytes
double d; // 8 bytes
Object obj; // 4 bytes (压缩指针)
}
OFFSET SIZE TYPE DESCRIPTION VALUE
0 8 (object header) (Mark)
8 4 (object header) (Klass)
12 8 long Data.l 0
20 8 double Data.d 0.0
28 4 int Data.i 0
32 4 Object Data.obj null
36 2 char Data.c 0
38 1 boolean Data.b false
39 1 (alignment padding)
Instance size: 40 bytes
排列顺序 (宽度降序):
[Mark:8B] [Klass:4B] [long:8B] [double:8B] [int:4B] [Object:4B] [char:2B] [boolean:1B] [pad:1B]
注意: 声明顺序是 b,c,i,l,d,obj,但实际排列按宽度降序!
python
JOL 输出: 数组对象
int[] arr = new int[10];
OFFSET SIZE TYPE DESCRIPTION VALUE
0 8 (object header) (Mark)
8 4 (object header) (Klass)
12 4 (object header) (array length) = 10
16 40 int [I.<elements> N/A
56 8 (alignment padding)
Instance size: 64 bytes
分析:
├── 对象头: 8 (Mark) + 4 (Klass) + 4 (Length) = 16 bytes
├── 数组数据: 10 * 4 bytes = 40 bytes
├── 对齐填充: 8 bytes (64 - 56 = 8, 对齐到 8 的倍数)
└── 总计: 64 bytes
六、各类型对象大小汇总
| 对象 | 压缩指针开启 | 压缩指针关闭 |
|---|---|---|
| new Object() | 16B | 16B |
| 1 字段 int | 16B | 24B |
| 2 字段 long+int | 24B | 32B |
| new int0 | 16B | 24B |
| new int10 | 64B | 64B |
| new String("ab") | ~56B | ~64B |
| ArrayList() | ~48B | ~56B |
java
// String 对象内存分析
// JDK 9+ 的 String 使用 byte[] 存储 (Compact Strings)
// String "ab" (2字符)
// ┌──────────────────────────────────────┐
// │ Mark Word: 8 B │
// │ Klass Pointer: 4 B │
// │ hash (int): 4 B │
// │ value (byte[] ref): 4 B (压缩) │ → byte[] 对象: 16B 头 + 2B 数据 + 6B padding = 24B
// │ coder (byte): 1 B │
// │ byte (flag): 1 B │
// │ padding: 2 B │
// └──────────────────────────────────────┘
// String 对象: 24B + byte[] 对象: 24B = 48B
七、@Contended 与缓存行
java
// 缓存行伪共享 (False Sharing) 问题
// CPU 缓存行通常 64 字节
// 两个不相关的变量在同一缓存行 → 一个变量修改导致另一个缓存失效
// 不使用 @Contended: 可能伪共享
class SharedData {
volatile long x; // 可能和 y 在同一缓存行
volatile long y; // 修改 x 导致 y 的缓存行失效
}
// 使用 @Contended: 独占缓存行
class PaddedData {
@jdk.internal.vm.annotation.Contended
volatile long x; // 独占一个缓存行 (前后各 padding 64B)
@jdk.internal.vm.annotation.Contended
volatile long y; // 独占另一个缓存行
}
// 需要 JVM 参数: -XX:-RestrictContended
java
缓存行对齐:
无 @Contended:
┌────────────────────────────────────────────────────┐
│ x (8B) │ y (8B) │ padding (48B) │ ← 同一缓存行
└────────────────────────────────────────────────────┘
线程1 写 x → 线程2 的缓存行(含 y)失效 → 伪共享!
有 @Contended:
┌──────────────┬──────────────────────────┬──────────────┐
│ padding 64B │ x (8B) + padding (56B) │ padding 64B │
└──────────────┴──────────────────────────┴──────────────┘
┌──────────────┬──────────────────────────┬──────────────┐
│ padding 64B │ y (8B) + padding (56B) │ padding 64B │
└──────────────┴──────────────────────────┴──────────────┘
x 和 y 各自独占缓存行 → 无伪共享
代价: 每个变量额外占用 128B (前后各 64B)
八、常见面试追问
Q1:一个空 Object 占多少字节?
16 字节。Mark Word 8B + Klass Pointer 4B(压缩)+ Padding 4B = 16B。
Q2:为什么堆设 31GB 比 32GB 更省内存?
32GB 堆会自动关闭压缩指针(寻址不够),所有引用从 4B 变成 8B,导致内存占用激增。31GB 堆仍然可以用压缩指针,引用只需 4B。
Q3:字段声明顺序影响对象大小吗?
理论上不影响,因为 JVM 会按宽度降序重排字段以减少 padding。但极少数情况下不同排列可能导致不同的 padding。实践中不需要关心。
Q4:boolean 占多少字节?
在对象中占 1 字节。在 boolean 数组中每个元素也是 1 字节(JVM 规范未严格定义,HotSpot 实现为 1 字节)。
Q5:如何减少对象内存占用?
- 使用基本类型替代包装类
- 避免不必要的字段
- 使用压缩指针(默认开启)
- 对于大量小对象,考虑扁平化数据结构
- 使用数组替代 ArrayList(避免包装类和额外对象)
九、总结
csharp
Java 对象内存布局:
对象头: Mark Word (8B) + Klass Pointer (4B/8B) + [Array Length (4B)]
实例数据: 字段按宽度降序排列 (long/double → reference → int → short → byte)
对齐填充: 总大小对齐到 8 字节
压缩指针:
开启 (-XX:+UseCompressedOops, 默认): 引用 4B, 堆 ≤ 32GB
关闭: 引用 8B, 堆 > 32GB
堆 31GB 比 32GB 更省内存!
关键数据:
new Object() = 16 bytes
new int[10] = 64 bytes (16B 头 + 40B 数据 + 8B padding)
理解对象内存布局是性能优化的基础。在百万级对象场景下,每个对象省 8 字节就能节省 8MB 内存。压缩指针和缓存行对齐是两个高频面试考点。