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                                                │
└──────────────────────────────────────────────────────────────────┘
区域线程存放内容OOM 类型
共享对象实例、数组Java heap space
Metaspace共享类元数据、运行时常量池等(总池;开启压缩类指针时,其内划出 Compressed Class Space 子区域存放 Klass,见下文 Metaspace / Compressed class space
Code Cache共享JIT 编译机器码CodeCache is full
直接内存共享堆外 BufferDirect buffer memory
虚拟机栈私有栈帧StackOverflowError
本地方法栈私有native 方法StackOverflowError
程序计数器私有字节码行号不会 OOM

规范中的方法区是逻辑概念;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
OOMPermGen spaceMetaspace

扩容与 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 SpaceMetaspace 的子区域,专放压缩 Klass;有独立上限
关系CCS ⊂ Metaspace;关闭 -UseCompressedClassPointers 时 Klass 直接落在 Metaspace 普通区,无独立 CCS
-XX:+UseCompressedClassPointers     # 默认开启(堆 ≤ 32g 且 64 位)
-XX:CompressedClassSpaceSize=1g     # CCS 上限,默认约 1g

OOM 区分

  • 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=2SurvivorRatio=8):

-Xmx=1200m
  新生代 = 1200 / (2+1) = 400m
  老年代 = 800m
  Eden   = 400 × 8/10 = 320m
  每个 Survivor = 400 × 1/10 = 40m

新生代:Eden 与 Survivor

区域作用
Eden几乎所有新对象在此分配(经 TLAB 或共享区)
SurvivorMinor 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

对象在堆上的内存布局

对象主题:布局、创建、分配以本文为权威;并发锁语义见 JUC,可见性见 JMM,总览 对象

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 GCEden 满新生代(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 类型标记说明
EdenE新对象
SurvivorS复制幸存
OldO老年代
HumongousH超过 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 exceededGC 时间占比过高(默认 > 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 到可用的权威路径(分配细节见 对象分配路径;方式总览 对象的创建):

  1. 检查类是否已加载(未加载则先 类加载,占用 Metaspace)
  2. 分配内存(Eden / TLAB;指针碰撞或空闲列表,CAS 保证线程安全)
  3. 初始化零值(字段默认值)
  4. 设置对象头(Mark Word:哈希码、GC 年龄、锁状态等)
  5. 执行 <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重量级锁定
11GC 标记

为何锁升级写在 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 预热,但失去运行时动态优化。


相关链接

  • 对象 — Java 对象总览(引用、布局、生命周期入口)
  • 垃圾回收 — Minor/Full GC、收集器选型与调优(堆回收细节)
  • 本文 · 堆 — 分代、TLAB、G1 Region、堆参数
  • 类加载 — 类元数据如何进入 Metaspace
  • JIT — 热点编译与 Code Cache 行为
  • 启动参数调优 — 堆、Metaspace、Code Cache 生产参数
  • JMM — 并发可见性(与本文内存布局不同概念)