HotSpot对象创建、内存布局与访问完整原理
一行new User()背后涉及类初始化、JIT优化、对象大小计算、TLAB分配、内存置零、对象头、字段初始化和构造器。对象实际占用也不是字段类型大小简单相加,还受JVM位数、压缩指针、字段布局、数组长度和对象对齐影响。
本页以企业常见的JDK7/JDK8 HotSpot为主线,并明确标注现代JDK差异。精确布局必须在目标JDK、目标启动参数和目标硬件上测量,不能把某张64位JVM示意图当成JVM规范保证。
学习目标
学完后应能:
- 解释对象头、实例数据、对齐填充分别做什么。
- 区分Mark Word、Klass Pointer和数组长度。
- 说明Mark Word为何能复用保存哈希、年龄和锁状态。
- 解释JDK7/8偏向锁、轻量级锁、重量级锁与对象头的关系及版本边界。
- 解释
UseCompressedOops和UseCompressedClassPointers影响什么。 - 计算典型64位压缩指针配置下空对象和数组的示意大小。
- 说明字段为什么可能不按源码顺序排列。
- 区分Shallow Size、Retained Size和业务对象图总成本。
- 解释引用不一定存放在栈中,也不是Java可直接操作的裸地址。
- 说明HotSpot直接对象访问与GC移动对象为什么不冲突。
- 画出
new从字节码到构造完成的完整路径。 - 区分TLAB分配、逃逸分析/标量替换和“栈上分配”。
- 使用JDK7/8兼容Java Agent测量对象浅大小。
- 从百万DTO、HashMap和String对象图分析商业内存成本。
一、先建立对象布局模型
flowchart TD
A["HotSpot堆对象"] --> B["对象头Header"]
A --> C["实例数据Instance Data"]
A --> D["对齐填充Padding"]
B --> E["Mark Word"]
B --> F["Klass Pointer"]
B --> G["数组额外length"]
C --> H["父类与本类实例字段"]
D --> I["补齐对象对齐边界"]这是HotSpot常见逻辑布局,不是Java语言规范承诺的固定字节模板。其他JVM实现可以采用不同布局。
二、对象头由什么组成
2.1 Mark Word
Mark Word是一块会随对象状态复用的头部数据。JDK7/8 HotSpot常用于编码:
- 对象identity hash相关信息。
- GC年龄。
- 锁标志。
- 偏向线程/偏向时间戳相关位。
- 轻量级锁时指向栈中Lock Record的内容。
- 重量级锁时与ObjectMonitor关联的信息。
- GC过程中可能使用的转发/标记状态。
“Mark Word同时永久保存所有字段”是不准确的。同一批位会按锁状态、GC阶段和JVM版本复用,因此需要结合当前状态解释位模式。
2.2 Klass Pointer
对象需要让JVM知道它属于哪个运行时类,以便:
- 解释字段布局。
- 做虚方法分派。
- 执行
instanceof和类型检查。 - 找到GC需要的引用字段布局。
- 支持反射、同步和运行时服务。
HotSpot对象头包含指向内部Klass元数据的指针或压缩类指针。java.lang.Class镜像是堆对象;Klass类元数据在JDK7永久代/JDK8 Metaspace等HotSpot实现区域,二者不要混为一谈。
2.3 数组长度
数组对象必须额外保存length,因为JVM需要执行:
arraylength。- 数组访问边界检查。
- 计算元素地址。
- GC扫描引用数组的元素范围。
普通对象大小可由类布局确定,数组还需要每个实例自己的长度。
三、典型64位布局示例
下面只是常见HotSpot示意,假设:
64位JVM
对象对齐8字节
Mark Word 8字节
压缩Klass Pointer 4字节
压缩普通对象引用4字节3.1 空对象
Mark Word 8
Compressed Klass 4
实例字段 0
小计 12
对齐到8字节倍数 4
总浅大小 16字节3.2 int字段对象
class Counter {
int value;
}示意:
对象头 12
int 4
合计 163.3 long字段对象
class LongValue {
long value;
}可能为:
对象头 12
为字段布局产生空洞/重排 可能存在
long 8
最终按8字节对齐具体常见结果可能是24字节,但不能只做12+8=20→24就声称所有JDK必然如此;字段对齐和布局策略要实测。
3.4 int数组
典型压缩类指针下数组头:
Mark Word 8
Klass Pointer 4
length 4
数组头合计 16new int[3]元素12字节,总28,再按8对齐常见为32字节。
四、压缩普通对象指针与压缩类指针
4.1 为什么压缩
64位原生地址常需8字节。对象图中引用字段、数组元素和对象头类指针数量巨大,全部使用8字节会增加:
- Heap占用。
- CPU Cache压力。
- 内存带宽。
- GC扫描和复制数据量。
HotSpot可用编码后的较小值表示地址,再通过基址和位移解码。
4.2 两个Flag不要混淆
-XX:+UseCompressedOops
-XX:+UseCompressedClassPointers- CompressedOops主要影响普通对象引用。
- CompressedClassPointers主要影响对象头中的类元数据指针。
二者支持关系、默认值、可寻址范围与Heap布局受JDK版本、对象对齐和地址模式影响。
4.3 “Heap超过32GB一定关闭压缩指针”为什么不严谨
约32GB是8字节对齐、特定编码方式下常见经验阈值,不是所有HotSpot版本和参数的永久硬规则。调整对象对齐可改变可编码范围,但也增加对象内部/尾部浪费。
正确验证:
java -XX:+PrintFlagsFinal -version
jcmd <pid> VM.flags查UseCompressedOops、UseCompressedClassPointers和ObjectAlignmentInBytes最终值。
五、对象对齐为什么存在
HotSpot通常让对象起始地址满足某个对齐边界,常见为8字节。价值包括:
- 地址低位可用于压缩指针位移编码。
- CPU访问对齐数据更友好。
- 对象扫描和分配边界更规则。
对齐填充没有业务字段语义,但真实占内存。对象越小、数量越多,头部和填充占比越高。
ObjectAlignmentInBytes是高级参数,增大对齐可能扩大压缩指针可寻址范围,也可能显著浪费小对象空间;必须用真实对象分布压测,不能只为保留压缩指针盲调。
六、字段布局为什么不一定按源码顺序
HotSpot可能按字段类型大小、对齐、父类布局、压缩指针和实现策略组织实例字段,以减少空洞并满足访问要求。
例如源码:
class MixedFields {
boolean enabled;
long id;
byte state;
Object owner;
int count;
}不能直接按源码顺序做:
1 + 8 + 1 + 4/8 + 4再加对象头。还要考虑:
- long/double对齐。
- 父类字段布局和继承边界。
- 引用是4还是8字节。
- 字段重排策略随JDK变化。
- 最终对象对齐。
@Contended等高级能力还能为降低伪共享增加填充,但需要版本、可见性和启动参数支持,不属于普通对象默认布局。
七、Mark Word与identity hashCode
Object.hashCode()允许重写;这里讨论System.identityHashCode(object)或默认identity hash语义。
identity hash通常按需计算,不要求对象创建时立刻产生。计算后需要在对象生命周期中保持稳定,即使对象被移动,也不能因为地址变化而改变。
在JDK7/8偏向锁模型中,计算identity hash可能与Mark Word偏向编码冲突,导致撤销偏向或改变后续锁路径。具体位布局和行为依赖目标JDK,不能背一个固定二进制图覆盖所有版本。
八、Mark Word与JDK7/8锁状态
JDK7/8 HotSpot常用教学模型:
flowchart TD
A["无锁/可偏向"] --> B["偏向某线程"]
B --> C["竞争或撤销"]
A --> D["轻量级锁/Lock Record"]
C --> D
D --> E["竞争加剧或需要阻塞"]
E --> F["重量级ObjectMonitor"]8.1 偏向锁
目标是优化长期只有一个线程进入的同步块,避免每次CAS。Mark Word可记录偏向线程相关信息。
但版本边界必须说明:偏向锁在较新JDK中默认策略改变,JDK15默认禁用,之后被移除。不能把JDK8锁升级图直接当JDK21实现。
8.2 轻量级锁
线程栈中建立Lock Record,尝试通过CAS让对象头与锁记录关联。适合短时间、低竞争;自旋也消耗CPU。
8.3 重量级锁
竞争激烈或满足膨胀条件后与ObjectMonitor关联,线程可能进入阻塞/唤醒,由操作系统调度参与。重量级不等于“一定永久无法降级”的跨版本规范结论;锁实现持续演进,应针对目标JDK分析。
详细同步语义见synchronized全过程。
九、对象年龄与Mark Word
传统分代HotSpot会记录对象年龄相关位。对象在Young GC后仍存活,年龄可能增加;达到当前阈值、Survivor不足或动态年龄判定时可晋升Old。
Mark Word位数有限,所以MaxTenuringThreshold与可表示年龄、收集器策略相关。G1等收集器仍有年龄/Survivor概念,但对象头和GC内部处理不能只按一张固定Serial位图解释。
十、Shallow Size与Retained Size
10.1 Shallow Size
对象自身:
对象头 + 自身实例字段 + 对齐填充不包含它引用对象的大小。
class User {
long id;
String name;
}User浅大小只包含id和name引用槽,不包含String对象及其内部数组。
10.2 Retained Size
如果该对象不可达后,可以连带释放的独占对象图总大小。一个浅大小很小的HashMap可能支配数GB Entry、Key、Value和业务对象,因此MAT排查泄漏更重视Retained Heap和Dominator Tree。
10.3 Deep Size
从对象沿引用遍历可达对象的总大小,但共享对象是否重复计算、是否跨越静态/弱引用边界需要定义。不同工具口径可能不同,不能把Deep Size和Retained Size混用。
十一、String对象的版本差异
JDK7后期/JDK8常见String内部主要引用char[];早期JDK7曾有offset/count共享底层数组的历史实现,更新版发生变化。JDK9引入Compact Strings后常使用byte[]加coder表示Latin-1/UTF-16。
所以:
- 一百万个短String的成本不能只按“字符数×2”。
- 还包含String对象头、数组对象头、引用、长度/编码字段、对齐。
- JDK8与JDK17同一业务数据内存可能不同。
substring是否共享大数组必须看具体JDK版本,不能套旧结论。
十二、对象引用不一定在栈中
原始说法“对象在堆,引用一定在栈”是错误的。引用可以位于:
- 栈帧局部变量槽或JIT寄存器。
- 另一个堆对象的字段。
- 引用数组元素。
- 类静态字段。
- JNI句柄/本地结构。
- JIT优化后被消除或常量传播。
Java语言中的reference也不是允许业务代码做指针算术的C裸地址。JVM可以移动对象并更新所有受管理引用,而Java程序仍观察到同一对象身份。
十三、句柄访问与直接访问
13.1 句柄模型
引用指向稳定句柄,句柄再保存对象数据地址和类型信息。对象移动时可以只更新句柄中的对象地址,但访问多一级间接寻址,句柄池也有成本。
13.2 HotSpot常见直接对象访问
普通HotSpot引用通常直接或经压缩解码后定位对象,对象头Klass Pointer关联类型元数据。它减少句柄层,但移动GC必须找到并更新所有相关引用。
不能说“直接指针一定比句柄快一倍”。真实性能受CPU Cache、压缩解码、JIT、对象图和GC影响,没有固定2倍保证。
JNI引用通常又有句柄式管理语义,以便GC移动对象和控制本地代码生命周期,因此“HotSpot绝对不用任何句柄”也不正确。
十四、移动GC怎样更新引用
复制/整理收集器移动对象时大致需要:
flowchart TD
A["找到存活对象旧地址"] --> B["在目标区域分配新地址"]
B --> C["复制对象内容"]
C --> D["记录转发关系"]
D --> E["修正Roots和对象字段引用"]
E --> F["旧区域整体/部分回收"]业务线程通常在需要一致引用更新的阶段STW;ZGC等现代收集器使用染色指针/加载屏障等技术并发处理更多工作,但这不是JDK7/8主线。
对象地址不是稳定业务标识,不能把地址保存到数据库或用Unsafe地址推导对象永久身份。
十五、new的字节码链路
源码:
User user = new User(1L, "Alice");字节码通常包含:
new User
dup
lconst_1
ldc "Alice"
invokespecial User.<init>(JLjava/lang/String;)V
astore...new请求创建未初始化对象引用。dup保留一份引用供构造后继续使用。- 参数压入操作数栈。
invokespecial <init>执行构造链。- 最后把引用保存到局部变量或其他位置。
十六、对象创建完整过程
flowchart TD
A["执行new"] --> B["解析类符号并确保初始化"]
B --> C["JIT是否可消除完整对象"]
C -- "可标量替换" --> D["拆成标量/寄存器,可能无堆对象"]
C -- "仍需对象" --> E["计算对象布局与大小"]
E --> F["优先TLAB快速分配"]
F --> G{"TLAB是否足够"}
G -- "是" --> H["移动本线程top"]
G -- "否" --> I["申请新TLAB或共享Eden慢路径"]
I --> J{"Heap空间是否足够"}
J -- "否" --> K["GC/扩堆/最终OOM"]
J -- "是" --> H
H --> L["内存置零"]
L --> M["设置Mark Word、Klass、数组长度"]
M --> N["执行实例字段初始化和构造器"]
N --> O["安全发布后供其他线程使用"]16.1 类检查
new常量池符号需要解析到合法运行时类,并确保类已按规则初始化。接口、抽象类不能被普通new实例化,访问权限也会检查。
16.2 逃逸分析可能消除分配
热点代码由C2证明对象不逃逸后,可做标量替换、锁消除和分配消除。更准确的说法不是“完整对象一定搬到Java栈”,而是对象可能不再以完整堆对象形态存在。
16.3 TLAB快速路径
Eden为线程划分TLAB,大多数小对象只需移动本线程分配指针,避免所有线程竞争共享Eden top。TLAB仍属于Heap/Eden,不是线程栈。
16.4 TLAB不足
JVM按对象大小、剩余空间和浪费策略决定:
- 使用剩余TLAB。
- 放弃尾部并申请新TLAB。
- 大对象绕过TLAB走共享区域。
- Eden不足触发GC/扩容路径。
16.5 并发分配
共享Eden路径需要CAS或其他同步协调分配指针。碎片化区域还可能使用空闲列表/Region策略,不是所有分配都只是全局指针碰撞。
十七、内存置零与Java默认值
Java保证实例字段在构造器显式赋值前具有默认值:
数字 0
boolean false
引用 nullHotSpot通常在分配路径中把需要的实例区域置零;TLAB申请时还可能批量预清零/延迟处理,具体实现可优化,但Java语义必须成立。
对象头不是简单全部清零,而要写入类指针、锁/标记初始状态和数组长度。
十八、实例初始化和构造器顺序
创建子类对象时大致:
- 对象整块内存先具有Java默认零值语义。
- 执行父类构造链。
- 按编译后的顺序执行当前类实例字段初始化和实例初始化块。
- 执行构造器正文。
实际上字段初始化和实例块会被编译进每个相关<init>,并在super()之后按源码顺序执行。
构造器中调用可覆盖方法可能在子类字段完成初始化前动态分派到子类,产生“看见默认0/null”的问题,应避免构造期间调用可覆盖业务方法。
十九、this逸出与半初始化对象
class Listener {
int value;
Listener(EventBus bus) {
bus.register(this);
value = 42;
}
}其他线程可能从EventBus拿到this,但构造器尚未完成,看到value=0或不完整状态。这不是对象分配失败,而是安全发布问题。
避免:
- 构造器中注册this到全局容器。
- 构造器启动线程并暴露this。
- 构造器调用外部可重入代码。
- 非正确同步下发布可变对象。
final字段有专门JMM初始化安全性,但仍不能让this在构造完成前逸出。
二十、JDK7/8可运行Agent Demo:测量浅大小
不新增项目依赖,使用JDK自带Instrumentation。getObjectSize返回实现相关的对象浅大小近似值,不包含引用对象图,也不保证与其他JVM实现一致。
20.1 Agent
import java.lang.instrument.Instrumentation;
public class ObjectSizeAgent {
private static volatile Instrumentation instrumentation;
public static void premain(String args, Instrumentation inst) {
instrumentation = inst;
}
public static long sizeOf(Object value) {
if (instrumentation == null) {
throw new IllegalStateException("start with -javaagent");
}
return instrumentation.getObjectSize(value);
}
}20.2 测量类
public class ObjectLayoutDemo {
static class Empty {
}
static class OneInt {
int value;
}
static class OneLong {
long value;
}
static class Mixed {
boolean enabled;
long id;
Object owner;
int count;
}
public static void main(String[] args) {
print("Object", new Object());
print("Empty", new Empty());
print("OneInt", new OneInt());
print("OneLong", new OneLong());
print("Mixed", new Mixed());
print("int[0]", new int[0]);
print("int[3]", new int[3]);
print("Object[3]", new Object[3]);
}
private static void print(String name, Object value) {
System.out.println(name + "="
+ ObjectSizeAgent.sizeOf(value));
}
}20.3 打包与运行
MANIFEST.MF:
Premain-Class: ObjectSizeAgent文件末尾需要换行。
javac ObjectSizeAgent.java ObjectLayoutDemo.java
jar cfm object-size-agent.jar MANIFEST.MF ObjectSizeAgent.class
java -javaagent:object-size-agent.jar ObjectLayoutDemo检查Flags:
java -XX:+PrintFlagsFinal -version | grep -E "UseCompressedOops|UseCompressedClassPointers|ObjectAlignmentInBytes"Windows可用findstr。
20.4 对照实验
在目标JDK支持且Heap允许时分别运行:
java -XX:+UseCompressedOops -javaagent:object-size-agent.jar ObjectLayoutDemo
java -XX:-UseCompressedOops -javaagent:object-size-agent.jar ObjectLayoutDemo观察引用字段和引用数组大小变化。关闭CompressedOops时CompressedClassPointers行为要读取最终Flags,不能只凭命令推测。
在一套64位HotSpot、8字节对齐的实测环境中得到:
| 对象 | 默认压缩配置 | 关闭CompressedOops |
|---|---|---|
| Object/Empty | 16 | 16 |
| OneInt | 16 | 16 |
| OneLong | 24 | 24 |
| Mixed | 32 | 40 |
| int[0] | 16 | 16 |
| int[3] | 32 | 32 |
| Object[3] | 32 | 40 |
这个结果说明普通引用字段和引用数组受CompressedOops影响,而primitive int数组元素不受普通对象引用宽度影响。表格只作为实验样例,不是所有JDK固定答案。
二十一、为什么Instrumentation结果不是对象图总内存
getObjectSize(new User(...))只返回User浅大小,不递归计算:
- String name。
- String内部数组。
- 集合元素。
- 共享对象。
要分析泄漏:
- Histogram看类数量与Shallow总量。
- MAT Dominator Tree看支配关系。
- Retained Heap看删除某对象可释放多少。
- Path To GC Roots看为什么仍可达。
二十二、商业场景:百万条DTO为什么远超字段估算
假设DTO:
class PatientRecord {
long id;
Integer age;
String name;
String idCard;
java.util.Map<String, String> attributes;
}错误估算:
long 8 + age 4 + 三个引用12 ≈ 24字节遗漏:
- PatientRecord对象头和对齐。
- Integer对象及缓存边界。
- 两个String对象和各自字符/字节数组。
- HashMap对象、table数组、Node、Key、Value。
- ArrayList/Object[]容器。
- ORM、JSON、日志和批处理中的临时副本。
- 一百万引用槽本身。
flowchart TD
A["PatientRecord浅对象"] --> B["Integer"]
A --> C["name String+数组"]
A --> D["idCard String+数组"]
A --> E["HashMap"]
E --> F["table数组"]
F --> G["多个Node"]
G --> H["Key/Value对象"]正确容量验证
- 用真实脱敏数据构造代表性对象。
- Histogram确认对象数量和浅大小。
- Heap Dump看支配对象图和共享关系。
- 测试分页/流式前后峰值。
- 把容器、序列化缓冲和Heap外内存一起纳入预算。
二十三、商业场景:对象头成本放大
一千万个只有一个boolean的小对象,业务数据约10MB,但每个对象都可能有十几字节头部和对齐,总量远高于10MB。
优化方向不是用Unsafe强行压缩,而是:
- 批量使用primitive数组。
- 位图/BitSet表达大量布尔状态。
- 减少HashMap Node和装箱对象。
- 调整数据模型,避免一条记录拆成大量微对象。
- 以可维护性、查询模式和GC代价共同评估。
二十四、生产对象内存排查Runbook
24.1 先确认JVM与布局配置
java -version
jcmd <pid> VM.flags
jcmd <pid> VM.command_line记录:
- JDK完整版本和发行版。
- 32/64位。
- Heap大小。
- CompressedOops/ClassPointers。
- ObjectAlignment。
- 收集器。
24.2 判断是数量还是单体过大
jcmd <pid> GC.class_histogram
jmap -histo <pid>比较多次:
- 同类数量是否持续增长。
- byte[]/char[]是否与业务批量相关。
- HashMap$Node、ArrayList、String是否成倍出现。
- Class/ClassLoader是否增长。
24.3 需要引用链时
评估副本、STW、磁盘和敏感数据后导出Heap Dump,用MAT查看:
- Dominator Tree。
- Retained Heap。
- Path To GC Roots。
- Duplicate Strings。
- ClassLoader Explorer。
24.4 形成结论
不能只写“User对象太多”,应写:
User数量由10万增长到300万
→ 由static cache持有
→ 每个User浅大小X,连带String/Map平均Y
→ Retained Heap总计Z
→ 缓存无TTL和最大容量二十五、常见误区
| 误区 | 正确理解 |
|---|---|
| 对象大小等于字段大小相加 | 还包含头、字段布局、引用对象和对齐 |
| 64位引用永远8字节 | CompressedOops常让Heap引用为4字节编码 |
| Heap超过32GB必关闭压缩 | 阈值受对齐、地址模式和JDK影响,查最终Flag |
| 对象头永远固定12字节 | 位数、压缩类指针、数组和JDK都会改变 |
| Mark Word同时永久保存所有信息 | 位会按锁/GC状态复用 |
| 对象在堆、引用一定在栈 | 引用可在堆字段、静态字段、寄存器和JNI结构 |
| HotSpot直接指针一定快句柄2倍 | 无固定倍数,取决于系统和访问模式 |
| TLAB是栈内存 | TLAB属于Eden/Heap |
| 逃逸分析就是把完整对象放栈上 | 常见优化是标量替换和分配消除 |
| getObjectSize包含整个对象图 | 只测浅大小 |
| 对象移动后identity hash会变化 | identity语义必须稳定,不等于地址 |
| 构造器完成前对象不可能被看到 | this逸出可暴露半初始化对象 |
二十六、面试标准回答
HotSpot对象由哪些部分组成
逻辑上由对象头、实例数据和对齐填充组成。对象头通常包含Mark Word和Klass Pointer,数组再保存length。Mark Word按锁/GC状态复用编码identity hash、年龄、锁记录或Monitor等信息。具体字节数取决于32/64位、CompressedOops/ClassPointers、数组和对象对齐,不能背固定12字节覆盖所有JVM。
为什么空对象常见16字节
在典型64位HotSpot、8字节对齐、压缩Klass Pointer配置下,Mark Word 8字节、类指针4字节,小计12,再填充4到16。但这是特定配置示例;关闭压缩类指针或改变对齐后结果会变化,应查看Flags并用JOL/Instrumentation实测。
CompressedOops解决什么
它用较小编码表示普通对象引用,降低引用字段、引用数组、Cache和GC带宽成本;CompressedClassPointers主要压缩对象头类指针。可编码范围取决于Heap布局、对象对齐和JDK实现,不能只用32GB固定线判断,必须查看最终Flags。
new对象完整过程
new先解析类并确保初始化;JIT可能通过逃逸分析和标量替换消除完整对象。仍需分配时计算布局,优先在Eden的线程TLAB指针碰撞,失败则申请TLAB或走共享慢路径,空间不足触发GC/扩堆/最终OOM。随后置零实例数据、设置对象头和数组长度,执行字段初始化及
<init>构造链,最后还要安全发布给其他线程。
Shallow Size和Retained Size区别
Shallow只包含对象头、自身字段和填充,不包含引用目标;Retained表示该对象不可达后可连带释放的独占对象图总量。一个浅小的HashMap可能支配数GB Entry和业务对象,泄漏分析更关注Dominator Tree、Retained Heap和Path To GC Roots。
二十七、关联知识点
- 对象分配、TLAB、晋升与GC
- JIT逃逸分析与标量替换
- Class文件与字节码结构
- 类加载五阶段
- GC可达性与回收算法
- synchronized全过程
- JMM与安全发布
- JVM参数与诊断命令
- OOM生产级排查
- JavaSE面试标准回答
二十八、学习验收
- [ ] 能画出普通对象与数组对象布局。
- [ ] 能在给定压缩指针/对齐假设下估算空对象和int数组。
- [ ] 能说明Mark Word为何随状态复用。
- [ ] 能解释JDK8偏向/轻量/重量锁与现代JDK边界。
- [ ] 能区分CompressedOops和CompressedClassPointers。
- [ ] 能解释字段重排和对齐为何让源码相加不准确。
- [ ] 能区分Shallow、Deep和Retained Size。
- [ ] 能解释引用不一定在栈以及移动GC如何修正引用。
- [ ] 能完整讲出new、TLAB、慢路径、置零、对象头和构造链。
- [ ] 能运行Instrumentation Demo并结合最终Flags解释结果。
- [ ] 能发现this逸出导致的半初始化对象。
- [ ] 能为百万DTO建立对象图级内存预算。
本章小结
HotSpot对象不是“字段数据加一个地址”,而是带有运行时身份、锁/GC状态、字段布局和对齐约束的结构。对象引用也不是固定裸地址,GC可以移动对象并修正受管理引用。理解头部、压缩指针、浅/保留大小和完整new路径后,才能准确解释锁升级、GC年龄、百万对象成本、Heap Dump和安全发布问题。
