JVM
→ 返回 Java 基础
→ 对象主题:堆上实例的布局、创建、分配见本文;总览与跨文档索引见 对象
运行时内存全景
JVM 内存分为堆、非堆(Native Memory) 和线程私有区。调 -Xmx 只限制堆;Metaspace、Code Cache、线程栈等走本地内存,需单独关注。
┌────────────────────────── JVM 进程内存 ──────────────────────────┐
│ │
│ 【堆 Heap】线程共享 · -Xms / -Xmx 控制 │
│ ├─ 新生代 Eden + Survivor │
│ └─ 老年代 Old │
│ │
│ 【非堆 · Native Memory】线程共享 · 不受 -Xmx 限制 │
│ ├─ Metaspace 类元数据(Java 8+ 替代 PermGen) │
│ │ └─ Compressed Class Space ← Metaspace 的子区域(非并列) │
│ ├─ Code Cache JIT 编译后的本地机器码 │
│ └─ 直接内存 Direct Memory NIO DirectBuffer 等(-XX:MaxDirectMemorySize)│
│ │
│ 【线程私有】 │
│ ├─ 虚拟机栈(栈帧、局部变量) │
│ ├─ 本地方法栈 │
│ └─ 程序计数器 PC │
└──────────────────────────────────────────────────────────────────┘
规范中的方法区是逻辑概念;HotSpot 在 Java 8 前用 PermGen(永久代,在堆内),Java 8+ 用 Metaspace(本地内存) 实现。CCS 不单独占「与 Metaspace 并列」的一行,而是 Metaspace 内部的子区域。
类元数据在 Metaspace,对象实例在堆;对象主题地图见 对象。类加载见 类加载;回收见 垃圾回收。
Metaspace(元空间)
Java 8 起,类的元数据从堆内 PermGen 迁到本地内存的 Metaspace。
存放什么
| 内容 | 说明 |
|---|---|
| 类的 Klass 结构 | 类名、父类、方法表、字段布局等 |
| 运行时常量池 | 编译期无法确定的常量、符号引用 |
| 方法元数据 | 字节码、异常表、行号表 |
| 注解、泛型签名 | 反射与泛型所需信息 |
不在 Metaspace 的:对象实例(在堆)、静态变量的值(在堆上的对象或基本类型在堆/元数据视实现而定——HotSpot 中 static 对象引用指向堆对象)。
与 PermGen 对比
| PermGen(Java 7 及以前) | Metaspace(Java 8+) | ||
|---|---|---|---|
| 位置 | 堆内固定大小 | 本地内存,默认上限受系统内存约束 | |
| 调参 | -XX:PermSize / MaxPermSize | -XX:MetaspaceSize / MaxMetaspaceSize | |
| Full GC | 必须 Full GC 才回收类 | 达阈值可触发 GC;类卸载仍多伴随 Full GC | |
| OOM | PermGen space | Metaspace |
扩容与 GC
类加载([[类加载]])→ 在 Metaspace 分配 Klass
│
▼
Metaspace 使用量达 MetaspaceSize 阈值 → 触发 GC 尝试卸载无用类
│
├── 成功回收 → 继续使用
└── 仍不足 → 扩容 Metaspace(不超过 MaxMetaspaceSize)
│
└── 达上限 → OutOfMemoryError: Metaspace
类卸载条件(需同时满足):该类的 ClassLoader 可回收、该类所有实例已回收、该类无 JNI 引用。动态代理、Groovy、JSP、OSGi 反复加载类易撑爆 Metaspace。
常用参数
-XX:MetaspaceSize=256m # 初始阈值,达此值触发 GC(非硬上限)
-XX:MaxMetaspaceSize=512m # 硬上限,生产建议显式设置
-XX:MinMetaspaceFreeRatio=40 # GC 后空闲率低于此值则扩容
-XX:MaxMetaspaceFreeRatio=70 # GC 后空闲率高于此值则缩容Compressed Class Space(压缩类空间)
是的:CCS 是 Metaspace 的子集,不是与 Metaspace 并列的另一套内存系统。
64 位 HotSpot 默认开启 压缩类指针(-XX:+UseCompressedClassPointers,常与 -XX:+UseCompressedOops 一起使用),Klass 元数据从 Metaspace 总配额内单独划出一块 Compressed Class Space 存放,以 32 位偏移引用类结构,节省内存。
Metaspace(总池,受 MaxMetaspaceSize 约束)
├── 普通元数据区 方法元数据、常量池、注解等
└── Compressed Class Space(子区域,受 CompressedClassSpaceSize 约束)
└── 压缩模式下的 Klass 结构
64 位 JVM(默认压缩开启)
堆内对象引用 UseCompressedOops → 32 位偏移指向堆(最大约 32 GB 堆)
类元数据 Klass UseCompressedClassPointers → 分配在 Metaspace 内的 CCS 子区域
| 概念 | 说明 |
|---|---|
| Metaspace | 总元空间池;CCS 占用计入 Metaspace 总用量 |
| Compressed Class Space | Metaspace 的子区域,专放压缩 Klass;有独立上限 |
| 关系 | CCS ⊂ Metaspace;关闭 -UseCompressedClassPointers 时 Klass 直接落在 Metaspace 普通区,无独立 CCS |
-XX:+UseCompressedClassPointers # 默认开启(堆 ≤ 32g 且 64 位)
-XX:CompressedClassSpaceSize=1g # CCS 上限,默认约 1gOOM 区分:
OutOfMemoryError: Metaspace— 元空间整体不足(类太多)OutOfMemoryError: Compressed class space— Klass 结构区满,常见于加载大量类且 CCS 偏小
查看:jcmd <pid> VM.native_memory summary(需 -XX:NativeMemoryTracking=summary)
Code Cache(代码缓存)
Code Cache 存放 JIT 编译器(C1/C2)生成的本地机器码,以及部分适配器代码。详见 JIT。
字节码(解释执行,慢)
│ 热点探测(方法/回边计数器)
▼
JIT 编译(C1 → C2 分层编译)
│
▼
本地机器码写入 Code Cache(快,可复用)
│
└── 缓存满 → 停止编译新代码 / 卸载旧 nmethod → 性能下降
| 特性 | 说明 |
|---|---|
| 位置 | 非堆,Native Memory |
| 与堆关系 | 不受 -Xmx 限制 |
| 满溢表现 | CodeCache is full 日志;JIT 停止编译,回退解释执行 |
| 常见原因 | 大量类与方法被编译、ReservedCodeCacheSize 过小 |
-XX:ReservedCodeCacheSize=256m # Code Cache 总上限(JDK 8 默认约 240m)
-XX:InitialCodeCacheSize=64m
-XX:+PrintCodeCache # 打印使用情况(JDK 8)
-Xlog:codecache:* # JDK 9+ 统一日志排查:jstat -compiler <pid> 看编译数;Code Cache 满时增大 ReservedCodeCacheSize 或排查是否动态生成海量类(CGLib、反射)。
堆(Heap)
堆是 JVM 中体积最大、最常调优的区域,线程共享,存放几乎所有对象实例与数组。由 -Xms(初始)与 -Xmx(最大)控制;GC 主要工作对象就是堆。
回收算法、收集器选型、调优案例见 垃圾回收。本文侧重堆的结构、分配与参数。
存放什么
| 在堆上 | 不在堆上 |
|---|---|
new 出来的对象 | 类元数据 → Metaspace |
数组(int[]、Object[]) | JIT 机器码 → Code Cache |
| 静态变量引用的对象本身 | 局部变量引用 → 虚拟机栈 |
| 字符串常量池中的 String 对象(JDK 7+ 在堆) | 基本类型局部变量 → 栈 |
Java 8 起:字符串常量池、静态变量引用的对象均在堆;PermGen 移除后堆职责更纯粹——实例数据。
整体结构(传统分代)
HotSpot 默认采用分代模型:按对象生命周期把堆划分为新生代与老年代,配合不同 GC 算法。
堆(Heap) 总大小 = -Xms / -Xmx(建议两者设成相同)
│
├── 新生代 Young Generation 默认约占堆 1/3
│ │ (-Xmn 或 -XX:NewRatio 控制)
│ │
│ ├── Eden 新对象首选分配区(约 80% 新生代)
│ ├── Survivor From (S0) 幸存区(约 10%)
│ └── Survivor To (S1) 幸存区(约 10%)
│ ※ 同一时刻只有一个 Survivor 存放 Minor GC 后的存活对象
│ ※ Minor GC 后 S0 ↔ S1 角色互换
│
└── 老年代 Old Generation 默认约占堆 2/3
长期存活对象、大对象、晋升对象
比例计算示例(默认 NewRatio=2,SurvivorRatio=8):
-Xmx=1200m
新生代 = 1200 / (2+1) = 400m
老年代 = 800m
Eden = 400 × 8/10 = 320m
每个 Survivor = 400 × 1/10 = 40m
新生代:Eden 与 Survivor
| 区域 | 作用 |
|---|---|
| Eden | 几乎所有新对象在此分配(经 TLAB 或共享区) |
| Survivor | Minor GC 后存活对象的复制目的地;对象每熬过一次 GC,分代年龄 +1 |
Minor GC(Young GC) 使用复制算法:
Minor GC 前:
Eden: [存活 A,B] S0: [存活 C] S1: [空]
Minor GC 后(复制到 S1,清空 Eden 和 S0):
Eden: [空] S0: [空] S1: [A,B,C 年龄+1]
下次 Minor GC 则复制到 S0,如此往复
为什么需要两个 Survivor? 复制算法需要「from / to」两块交替,保证始终有一块为空接收存活对象,避免碎片。
老年代
| 进入老年代的路径 | 说明 |
|---|---|
| 年龄晋升 | Survivor 中对象年龄 ≥ MaxTenuringThreshold(默认 15) |
| 动态年龄判定 | 同年龄对象总大小超过 Survivor 一半,≥ 该年龄的全部晋升 |
| Survivor 溢出 | 单次 Minor GC 存活对象 Survivor 放不下 |
| 大对象 | 超过阈值直接进老年代(避免 Eden 间大量复制) |
| 空间分配担保失败 | Minor GC 前老年代无法担保 → 可能 Full GC |
老年代 GC 多用标记-整理或标记-清除,STW 通常比 Minor GC 长。老年代满 → Major GC / Full GC。
对象在堆上的内存布局
64 位 HotSpot 对象结构(不含数组长度等变长部分):
对象在堆中
├── 对象头 Object Header
│ ├── Mark Word(8 字节) 哈希码、GC 年龄(4bit)、锁状态
│ └── Klass Pointer(4/8 字节) 指向 Metaspace 中类元数据
├── 实例数据 Instance Data 字段值(对齐填充)
└── 对齐 Padding 8 字节对齐
压缩指针(Compressed Oops)(64 位默认开启,堆 ≤ 约 32 GB):
-XX:+UseCompressedOops(默认)
引用类型字段占 4 字节(32 位偏移),基址 + 偏移定位堆内对象
堆上限 practical 约 32 GB;超过则自动关闭压缩或使用特殊布局
-XX:+UseCompressedClassPointers
Klass 指针也压缩 → 对应 Compressed Class Space
数组对象额外有数组长度(4 字节)与元素数据。
对象创建步骤见下文 对象的创建过程;Mark Word 详见 对象头(Mark Word)。
对象分配路径
new 对象
│
├─ 逃逸分析判定可栈上分配? ──是──► 栈帧内分配(无堆、无 GC) ← JIT 优化
│
└─ 否 → 堆分配
│
├─ 大对象? ──是──► 老年代(或 G1 Humongous Region)
│
└─ 否 → Eden
│
├─ 线程 TLAB 有余量? ──是──► TLAB 内 bump-the-pointer(无锁)
│
└─ 否 → Eden 共享区 CAS 分配 / 刷新 TLAB
TLAB(Thread Local Allocation Buffer):每个线程在 Eden 预占一小块,本地指针递增分配,减少多线程 CAS 竞争。
-XX:+UseTLAB # 默认开启
-XX:TLABSize=... # 一般自动, rarely 手工调
-XX:ResizeTLAB # 允许动态调整 TLAB 大小堆分配两种实现(由堆是否规整决定):
| 方式 | 条件 | 做法 |
|---|---|---|
| 指针碰撞(Bump) | 堆内存规整(复制 GC 后) | 移动 allocation pointer |
| 空闲列表 | 有碎片(标记-清除后) | 从 free list 找合适块 |
与 GC 的关系(概要)
| GC 类型 | 触发条件 | 回收范围 |
|---|---|---|
| Minor GC | Eden 满 | 新生代(Eden + Survivor) |
| Major GC | 老年代满 | 老年代 |
| Full GC | 老年代不足、Metaspace、System.gc() 等 | 整堆 + 常含类卸载 |
分代假设:大部分对象朝生夕灭(Eden 回收极快),少量长期存活(进老年代)。详细流程、收集器、参数见 垃圾回收。
G1 下的堆:Region 模型
JDK 9+ 默认 G1 不再物理切分固定 Eden/Survivor/Old 连续块,而是把整堆划为等长 Region(1~32 MB):
堆(G1)
├── Region 1 [E] Eden
├── Region 2 [E]
├── Region 3 [S] Survivor
├── Region 4 [O] Old
├── Region 5 [O]
├── Region 6 [H] Humongous(超大对象,占连续多个 Region)
└── ...
| Region 类型 | 标记 | 说明 |
|---|---|---|
| Eden | E | 新对象 |
| Survivor | S | 复制幸存 |
| Old | O | 老年代 |
| Humongous | H | 超过 Region 一半的对象,直接进 H,避免复制 |
逻辑上仍有「年轻代 / 老年代」比例(-XX:G1NewSizePercent / G1MaxNewSizePercent),但物理上 Region 可动态改角色。回收:Young GC → 并发标记 → Mixed GC(部分 Old Region)。
ZGC 与分代 ZGC
ZGC(JDK 15+)默认不分代整堆管理,着色指针 + 并发整理,停顿与堆大小弱相关。
JDK 21+ 可选 分代 ZGC(-XX:+ZGenerational):为短生命周期对象增加年轻代回收,降低分配速率压力。
常用堆参数
# 堆大小(生产建议 -Xms = -Xmx,避免运行时扩堆)
-Xms2g
-Xmx2g
# 新生代(二选一:直接指定 或 比例)
-Xmn512m
-XX:NewRatio=2 # 老年代:新生代 = 2:1
-XX:SurvivorRatio=8 # Eden : 单个 Survivor = 8:1
# 晋升与大对象
-XX:MaxTenuringThreshold=15 # 晋升年龄阈值(≤ 15)
-XX:PretenureSizeThreshold=... # 大对象直进老年代(仅部分收集器)
# 压缩指针
-XX:+UseCompressedOops # 默认
-XX:ObjectAlignmentInBytes=8 # 对象对齐
# G1
-XX:+UseG1GC
-XX:G1HeapRegionSize=4m
-XX:G1NewSizePercent=5
-XX:G1MaxNewSizePercent=60
# 容器(K8s / Docker)
-XX:MaxRAMPercentage=75.0 # 按 cgroup 内存的 75% 作堆上限
-XX:InitialRAMPercentage=75.0完整模板见 启动参数调优。
监控与 OOM
jmap -heap <pid> # 堆配置与各分代容量
jstat -gcutil <pid> 1000 # S0/S1/E/O/M(M=Metaspace)利用率
jcmd <pid> GC.heap_info
jcmd <pid> VM.flags | grep -i heap| OOM / 现象 | 常见原因 |
|---|---|
Java heap space | -Xmx 过小、内存泄漏、大缓存 |
| Minor GC 极频 | Eden 过小、创建大量短生命周期对象 |
| Full GC 频繁 | 老年代满、晋升过快、Metaspace 连带 |
GC overhead limit exceeded | GC 时间占比过高(默认 > 98% 且回收 < 2%) |
排查: -XX:+HeapDumpOnOutOfMemoryError + MAT 分析 dominator tree。
垃圾回收算法
| 算法 | 原理 | 缺点 |
|---|---|---|
| 标记-清除 | 标记可回收对象,直接清除 | 内存碎片 |
| 标记-整理 | 标记后将存活对象压缩到一端 | 移动对象开销 |
| 复制 | 将存活对象复制到另一块区域 | 需要预留空间 |
| 分代收集 | 新生代用复制,老年代用标记-整理 | 现代 JVM 标准 |
如何判断对象可回收:
引用计数法(循环引用问题,Java 不用)- 可达性分析:从 GC Roots 出发,不可达的对象可回收
- GC Roots:栈中引用的对象、静态变量引用的对象、JNI 引用的对象、活跃线程等
收集器选型与 Mixed GC、ZGC 等详见 垃圾回收。
垃圾收集器(概览)
| 收集器 | 区域 | 特点 | 适用场景 |
|---|---|---|---|
| Serial | 新生代 | 单线程,STW | 客户端、小内存 |
| ParNew | 新生代 | 多线程,STW | 配合 CMS(已废弃) |
| Parallel Scavenge | 新生代 | 吞吐量优先 | 后台运算 |
| G1 | 全堆 Region | 可预测停顿 | JDK 9+ 默认 |
| ZGC | 全堆 | 停顿 < 1ms | 超大堆、低延迟 |
| Shenandoah | 全堆 | 低停顿 | OpenJDK,类似 ZGC |
栈帧结构
每次方法调用都会创建一个栈帧压入虚拟机栈:
栈帧
├─ 局部变量表(Local Variable Table)
├─ 操作数栈(Operand Stack)
├─ 动态链接(指向运行时常量池的方法引用)
└─ 返回地址
递归过深 → 栈帧不断压栈 → StackOverflowError
JVM 参数常用配置
# 堆
-Xms512m -Xmx2g
-Xmn256m # 新生代(与 NewRatio 二选一)
# Metaspace
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m
# Code Cache
-XX:ReservedCodeCacheSize=256m
# 收集器
-XX:+UseG1GC
-XX:+UseZGC
# GC 日志(JDK 9+)
-Xlog:gc*:file=gc.log:time,uptime,level,tags
# OOM dump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heapdump.hprof
# 本地内存追踪
-XX:NativeMemoryTracking=summary完整模板见 启动参数调优。
对象的创建过程
堆上实例从字节码 new 到可用的权威路径(分配细节见 对象分配路径;方式总览 对象的创建):
- 检查类是否已加载(未加载则先 类加载,占用 Metaspace)
- 分配内存(Eden / TLAB;指针碰撞或空闲列表,CAS 保证线程安全)
- 初始化零值(字段默认值)
- 设置对象头(Mark Word:哈希码、GC 年龄、锁状态等)
- 执行
<init>(构造方法赋值)
对象头(Mark Word)
64 位 JVM 中 Mark Word 占 8 字节(与 Klass 指针合称对象头),同一字段在不同阶段复用不同语义:
| 用途 | 说明 |
|---|---|
| 哈希码 | hashCode() 未计算前可为空;计算后写入(与锁状态可能互斥,由 JVM 协调) |
| GC 年龄 | 4 位,最大 15;与 -XX:MaxTenuringThreshold、晋升见 垃圾回收 |
| 锁状态 | 用 lock bits 区分无锁/偏向/轻量/重量(见下表);synchronized 的升级过程在 JUC |
锁与 wait/notify 的 Monitor 绑定在该对象上。Mark Word 低位 lock bits(与哈希、GC 年龄复用同一 8 字节)常见含义:
| lock bits | 状态 |
|---|---|
01 | 无锁 / 偏向 |
00 | 轻量级锁定 |
10 | 重量级锁定 |
11 | GC 标记 |
为何锁升级写在 JUC 而非本文:锁升级是 HotSpot 实现
synchronized/monitorenter时的优化路径,学习时与wait、线程状态、 ReentrantLock 放一起更顺;本文只记对象头里锁状态存在哪。ReentrantLock(AQS)不走这套 Mark Word 升级。
JIT 即时编译
执行流程
Java 源码 → javac → 字节码(.class) → JVM 解释执行
↓ 热点代码
JIT 编译
↓
写入 Code Cache(本地机器码)
Java 是解释 + 编译混合:启动解释执行;热点代码 JIT 编译后从 Code Cache 执行。详见 JIT。
热点探测
- 方法调用计数器、回边计数器超过阈值 → 触发 JIT(默认约 10000 次,
-XX:CompileThreshold) - OSR:循环体在栈上替换为已编译版本
分层编译
JDK 8+ 默认 -XX:+TieredCompilation:C1 快速编译 → C2 深度优化。
主要优化手段
| 优化 | 说明 |
|---|---|
| 内联 | 消除方法调用开销 |
| 逃逸分析 | 未逃逸对象可栈上分配,减轻 GC |
| 锁消除 / 锁粗化 | 减少同步开销 |
AOT
JDK 9+ AOT 在运行前编译为机器码(GraalVM Native Image),无 JIT 预热,但失去运行时动态优化。