基础款玩具"空仔"new Object()

70 阅读5分钟

将用更生动的故事和更深入的技术细节,带你看清一个空Object的内存结构,特别是Mark Word内部的精妙设计!


Java堆内存的"玩具工厂"扩建记

想象Java堆是个巨型玩具工厂,今天厂长(JVM)接到订单:生产一个基础款玩具"空仔"(new Object())。你以为它是个空壳?错!它需要携带复杂的"身份档案"才能生存。


"空仔"的完整内存档案(64位JVM+压缩指针)

组成部分大小技术名称故事比喻
身份档案头12字节对象头 (Header)玩具的身份证+说明书
├─ 核心状态卡8字节Mark Word多功能智能芯片(可变形)
└─ 设计图链接4字节Klass Pointer设计图纸仓库的地址
实际内容0字节实例数据空仔没有内部零件
填充泡沫4字节对齐填充 (Padding)工厂强迫症的产物
总计16字节

深入Mark Word:8字节的"变形金刚芯片"

这8字节(64位)的Mark Word是Java对象最精妙的设计!它像变形金刚一样,根据对象状态改变内部结构:

锁状态与GC年龄的比特级分配(以64位JVM为例)

                                64位 Mark Word
┌────────────────────┬───────────────┬───────┬───┬───┐
│  锁状态            │  字段分配      │ 位数  │值 │说明│
├────────────────────┼───────────────┼───────┼───┼───┤
│ 无锁 (Normal)      │ unused        │ 25位  │ - │    │
│                   │ identity_hash │ 31位  │ - │哈希│
│                   │ unused        │ 1位   │ - │    │
│                   │ age           │ 4位   │0-15│GC龄│
│                   │ biased_lock   │ 1位   │ 0 │偏向│
│                   │ lock_state    │ 2位   │ 01│锁标│
├────────────────────┼───────────────┼───────┼───┼───┤
│ 偏向锁 (Biased)    │ thread ID     │ 54位  │ - │线程│
│                   │ epoch         │ 2位   │ - │时间│
│                   │ unused        │ 1位   │ - │    │
│                   │ age           │ 4位   │0-15│GC龄│
│                   │ biased_lock   │ 1位   │ 1 │偏向│
│                   │ lock_state    │ 2位   │ 01│锁标│
├────────────────────┼───────────────┼───────┼───┼───┤
│ 轻量锁 (Lightweight) ptr_to_lock_record │ 62位 │ - │栈针│
│                   │ lock_state    │ 2位   │ 00│锁标│
├────────────────────┼───────────────┼───────┼───┼───┤
│ 重量锁 (Heavyweight) ptr_to_monitor │ 62位 │ - │管程│
│                   │ lock_state    │ 2位   │ 10│锁标│
├────────────────────┼───────────────┼───────┼───┼───┤
│ GC标记 (Marked)    │ CMS/GC数据    │ 62位  │ - │回收│
│                   │ lock_state    │ 2位   │ 11│锁标│
└────────────────────┴───────────────┴───────┴───┴───┘

关键字段深度解析

1. GC年龄计数器(age:4位)

  • 位置:仅存在于无锁/偏向锁状态

  • 工作方式

    // 伪代码:GC过程
    void minorGC() {
        if (object.survived) {
            // 从age字段读取当前值
            int currentAge = markWord.getAge();
            
            if (++currentAge > 15) { // 4位最大值为15
                promoteToOldGen(); // 晋升老年代
            } else {
                markWord.setAge(currentAge); // 更新age
            }
        }
    }
    
  • 为什么是4位?
    基于"弱代假说":99%对象活不过第一次GC。15次GC(默认MaxTenuringThreshold=15)足够判断长期存活对象

2. 锁状态机(lock_state:2位)

  • 状态转换路线

    无锁 → (线程访问) → 偏向锁 → (竞争) → 轻量锁 → (竞争加剧) → 重量锁
    
  • 锁升级原理

    • 偏向锁:在Mark Word烙上线程ID(54位足够存储)
    • 轻量锁:将Mark Word复制到线程栈,用指针(62位)指向它
    • 重量锁:指向操作系统级别的Monitor对象

3. 哈希码(identity_hash:31位)

  • 延迟计算:调用hashCode()时才生成
  • 一旦写入不可变:因为锁状态会覆盖这部分空间
  • 31位的原因:避免溢出(2^31≈21亿),足够覆盖所有对象

实战:用JOL工具透视内存布局

添加依赖:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.17</version>
</dependency>

运行代码:

public class ObjectDissector {
    public static void main(String[] args) {
        // 创建一个未经历GC的纯净对象
        Object virginObject = new Object();
        
        System.out.println("===== 处女对象的内存布局 =====");
        System.out.println(ClassLayout.parseInstance(virginObject).toPrintable());

        // 触发hashCode计算
        virginObject.hashCode();
        
        System.out.println("===== 拥有HashCode后的布局 =====");
        System.out.println(ClassLayout.parseInstance(virginObject).toPrintable());
        
        // 模拟GC:增加对象年龄
        for (int i = 0; i < 5; i++) {
            System.gc(); // 实际需要配合-XX:+PrintGCDetails观察
        }
    }
}

输出解析

java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 
  // 解读:最后3位001 -> lock=01(无锁) + biased_lock=0
  8   4        (object header: class)    0xf80001e5  // Klass指针
 12   4        (alignment gap)           // 对齐填充

调用hashCode后:
  0   8        (object header: mark)     0x0000000e8a1cc001 
  // 中间31位:0x0e8a1cc0 = 244,000,000 (哈希值)

跨JVM的字节差异表

JVM配置Mark WordKlass Pointer总大小
64位+压缩指针(默认)8字节4字节16字节
64位无压缩指针8字节8字节16字节*
32位JVM4字节4字节8字节

*注:64位无压缩时,由于8+8=16字节已对齐,无需填充


为什么需要内存对齐?

CPU读取内存不是逐字节操作,而是以缓存行(通常64字节)为单位。就像工厂搬运货物:

  • 用标准集装箱(8字节)运输效率最高
  • 零散货物需要拼箱,降低搬运速度
  • 对齐填充就是给货物加"空包装",确保每个对象从集装箱起始位置开始存放

高级知识点:压缩指针的魔法

  • 开启条件-XX:+UseCompressedOops(默认开启)

  • 原理:用32位地址表示35位实际地址

    // 伪代码:指针解压缩
    real_address = compressed_address << 3 + base_address;
    
  • 地址计算
    假设堆从0x0000_0000_0000_0000开始
    压缩指针0x12345678 → 真实地址0x0000_0000_9A2B_3C00
    0x12345678 << 3 = 0x91A2B3C0,对齐后)


总结:小对象的生存智慧

  1. 16字节是生存成本:即使空对象也要携带身份档案
  2. Mark Word是核心芯片:8字节承载5种状态转换
  3. 锁与GC共享空间:通过2位锁标志切换数据结构
  4. 对齐是性能保障:牺牲4字节换取CPU高速访问
  5. 年龄计数器是生命沙漏:4位空间记录15次GC历程

下次你写new Object()时,请记住:这个"空"对象正在堆中佩戴着它的8字节智能芯片,准备迎接锁竞争和GC风暴的考验!这就是Java对象在内存中的生存之道。