Java 对象内存布局:压缩指针、对齐填充与 JOL 实测

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:如何减少对象内存占用?

  1. 使用基本类型替代包装类
  2. 避免不必要的字段
  3. 使用压缩指针(默认开启)
  4. 对于大量小对象,考虑扁平化数据结构
  5. 使用数组替代 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 内存。压缩指针和缓存行对齐是两个高频面试考点。

相关推荐
东华果汁哥1 小时前
【Spring AI 实战】从零开始调用大模型接口,构建 Java AI 应用
java·人工智能·spring
Song_RsHt2 小时前
Redis 与微服务,什么场景下无可替代
java·redis·后端·学习·微服务
学长毕设2 小时前
基于SpringBoot的建筑材料管理系统-附源码
java·spring boot·后端
摸爬滚打的小李3 小时前
关于 Arthas
java·后端
砚底藏山河3 小时前
python量化实操:冷热分层存储
java·开发语言·python·金融
yuniko-n3 小时前
【JUC】原子类
java
惊讶的猫3 小时前
Query 改写
java·rag
信誓旦旦的程序猿3 小时前
【Python量化实战宝典 #18】MACD 金叉死叉手册:DIF、DEA、柱体全拆解(Python 可跑)
java·python·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
Misnearch4 小时前
第7章 面向对象技术
java·开发语言·软考