JVM从浅入深

目录

一.JVM内存结构

1.程序计数器

定义​

作用

2.虚拟机栈

2.1定义

2.2问题辨析(线程安全)

2.3栈内存溢出

2.4线程运行判断

3.本地方法栈

4.堆

堆内存溢出

堆内存诊断

5.方法区

[5.1 定义](#5.1 定义)

[5.2 组成](#5.2 组成)

[5.3 方法区内存溢出](#5.3 方法区内存溢出)

[5.4 运行时常量池](#5.4 运行时常量池)

[5.5 StringTable(串池)](#5.5 StringTable(串池))

[5.6 StringTable特性](#5.6 StringTable特性)

[5.7 StringTable位置](#5.7 StringTable位置)

[5.8 StringTable垃圾回收](#5.8 StringTable垃圾回收)

[5.9 StringTable性能调优](#5.9 StringTable性能调优)

口诀总结

6.直接内存

[6.1 定义](#6.1 定义)

[6.2 基本使用](#6.2 基本使用)

[6.3 内存溢出(OOM)](#6.3 内存溢出(OOM))

[6.4 释放原理(重点)](#6.4 释放原理(重点))

[6.5 禁用显式回收对直接内存影响](#6.5 禁用显式回收对直接内存影响)

二.垃圾回收

1.如何判断对象可以回收

[1.1 引用计数法](#1.1 引用计数法)

[1.2 可达性分析算法](#1.2 可达性分析算法)

[1.3 五种引用](#1.3 五种引用)

引用队列​

2.垃圾回收算法

3.分代垃圾回收

4.垃圾回收器

[4.1 串行](#4.1 串行)

[4.2 吞吐量优先](#4.2 吞吐量优先)

[4.3 响应时间优先](#4.3 响应时间优先)

[4.4 G1](#4.4 G1)

(1)G1回收阶段

[(2)Young Collection(年轻代收集)](#(2)Young Collection(年轻代收集))

[(3)Young Collection + CM(并发标记)](#(3)Young Collection + CM(并发标记))

[(4)Mixed Collection(混合收集)](#(4)Mixed Collection(混合收集))

[(5) FUll GC](#(5) FUll GC)

[(6)Young GC(跨代引用)](#(6)Young GC(跨代引用))

[① 为什么会有"跨代引用"](#① 为什么会有“跨代引用”)

[② 卡表(Card Table)------"老年代→新生代"的 粗粒度地图](#② 卡表(Card Table)——“老年代→新生代”的 粗粒度地图)

[③ 后置写屏障(Post-Write Barrier)+ 脏卡队列](#③ 后置写屏障(Post-Write Barrier)+ 脏卡队列)

[④ Remembered Set(RSet)------"卡表→Region"的 精确定位](#④ Remembered Set(RSet)——“卡表→Region”的 精确定位)

[⑤ Young GC 时的工作流程(结合图)](#⑤ Young GC 时的工作流程(结合图))

(7)Remark

[① 问题背景:并发标记时对象图还在变](#① 问题背景:并发标记时对象图还在变)

[② SATB(Snapshot-At-The-Beginning) 思路](#② SATB(Snapshot-At-The-Beginning) 思路)

[③ 预写屏障(Pre-Write Barrier)(底层实现)](#③ 预写屏障(Pre-Write Barrier)(底层实现))

[④ SATB 标记队列(SATB Mark Queue)→ 全局队列](#④ SATB 标记队列(SATB Mark Queue)→ 全局队列)

[⑤ 时间线(配合图)](#⑤ 时间线(配合图))

(8)JDK8u20字符串去重

[(9)JDK 8u40 并发标记类卸载](#(9)JDK 8u40 并发标记类卸载)

[(10)JDK 8u60 回收巨型对象](#(10)JDK 8u60 回收巨型对象)

(11)JDK9并发标记起始时间的调整


一.JVM内存结构

区域 线程私有? 说明 生命周期
PC 寄存器(Program Counter) 当前字节码行号 与线程同生同灭
Java 虚拟机栈(Java Stack) 局部变量、操作数栈、方法出口 与线程同生同灭
本地方法栈(Native Method Stack) JNI native 方法用的 C 栈 与线程同生同灭
(Heap) 所有对象实例、数组 JVM 启动到退出
方法区(Metaspace / PermGen) 类元数据、常量池、静态变量 JVM 启动到退出
直接内存(Direct Memory) ByteBuffer.allocateDirect、NIO 映射 JVM 启动到退出

1.程序计数器

定义

作用

2.虚拟机栈

2.1定义

栈顶部的栈帧就是活动栈帧

2.2问题辨析(线程安全)

垃圾回收不涉及栈内存: 栈帧(方法调用的内存块)随方法调用而创建,随方法结束而自动销毁(由编译器预设的字节码指令控制,无需GC干预)。

栈内存分配不是越大越好: 栈内存是"线程私有的、一次性全额申请、生命周期同线程、永不归还"的内存 。它不像堆那样"按需分配、GC 回收",所以给得越大,只是白白预占、浪费、甚至直接创不出线程

2.3栈内存溢出

2.4线程运行判断

3.本地方法栈

4.堆

堆内存溢出

可通过设置较小的堆内存加速问题的排查

堆内存诊断

5.方法区

5.1 定义

方法区域是 JVM 中的一个逻辑内存区域,用于存储每个类的相关结构,例如运行时常量池、字段和方法数据、方法和构造函数的代码,以及类执行所需的其他元数据。

5.2 组成

5.3 方法区内存溢出

方法区(Java 8 以后叫 Metaspace )存的是"类的元数据"------类名、字段、方法、字节码、常量池、注解、JIT 编译后的机器码等。
"方法区内存溢出" 就是这片区域被撑爆,JVM 无法再加载/扩展类信息,于是抛出:

  • Java 7 及之前:java.lang.OutOfMemoryError: PermGen space 永久代空间

  • Java 8 及之后:java.lang.OutOfMemoryError: Metaspace 元空间

场景 典型触发方式 观察指标
1. 动态类过多 多次 Class.forName、反射、代理、JSON/Bean 拷贝库(CGLIB、Objenesis、Javassist)、Groovy/Scala/Kotlin 脚本引擎、JSP 热加载 加载类数(jstat -loader)持续上升,GC 不下降
2. 热部署 / 热替换 Tomcat、Spring Boot DevTools、OSGi、Arthas retransform,旧 ClassLoader 应该被回收却还被引用 类加载器+类实例双重泄漏
3. 巨型常量池 代码里写了几万个 String s = "xxx"static final String,或 MyBatis 拼接 SQL 用 ++ 拼出大量不同字面量 常量池占 Metaspace 70 % 以上
4. JIT 代码缓存过大 -XX:ReservedCodeCacheSize 太小,或大量 lambda/invokedynamic 生成巨量机器码 Code Cache 接近上限
5. 框架 Bug 早期 Hibernate、Spring CGLIB 重复生成代理类未复用 同一接口出现 UserService$$EnhancerBySpringCGLIB$$a3c4d5f2 几百次

5.4 运行时常量池

5.5 StringTable(串池)

5.6 StringTable特性

第一行:

读取到常量加入串池["a","b"]

new了对象加入堆new String("a") new String("b") new String("ab")

s="ab"来自堆中

第二行:

s2=s.intern由于串池中没有"ab",将堆中"ab"加入串池并返回给s2(1.8版本)

String x="ab"由于串池中已经存在"ab"所以直接取串池中的

s2==x 为ture s==x为ture

(1.6版本)s.intern在串池没有的情况下,会拷贝一个加入串池并返回

此时s2为串池中拷贝的,x取出串池中拷贝的"ab"与s不同

s2==x ture s==x false

5.7 StringTable位置

5.8 StringTable垃圾回收

版本 所在区域 触发 GC 类型 说明
JDK≤6 PermGen(永久代) Full GC 老年代或永久代不足时才触发,回收频率极低,容易堆积无用字符串。
JDK≥7 Java 堆 Young GC / Full GC 只要 Minor GC 发生就可能清理,回收及时性大幅提高。

5.9 StringTable性能调优

复制代码
# 启动参数
-XX:StringTableSize=200003          # 桶数放大 3 倍
-XX:+PrintStringTableStatistics     # 事后对账
-Xms4g -Xmx4g -XX:+UseG1GC          # 保持 GC 轻快

# 代码模板
private static final Interner<String> POOL = WeakInterners.create();
String key = POOL.intern(raw);       // 高重复但可丢弃用弱引用池

口诀总结

"先监控、再桶数、后代码,GC 友好并发怕。"

只要 桶数 > 1.5 × 预计条目数不乱 intern()GC 健康 ,StringTable 就能一直保持 O(1) 的查询速度。

6.直接内存

6.1 定义

直接内存(Direct Memory)是 JVM 堆外 的一块内存区域,不在 Java 堆里 ,由操作系统直接分配和释放,Java 代码通过 sun.misc.Unsafejava.nio.ByteBuffer.allocateDirect() 访问。

6.2 基本使用

  1. 最典型入口:
    ByteBuffer.allocateDirect(size) → 返回 DirectByteBuffer 对象。

  2. 背后实现:

    通过**Unsafe.allocateMemory** 向操作系统 malloc 申请一块堆外 内存,地址存在 address 字段。

  3. 使用场景:

    NIO、Netty、Kafka、RocketMQ、G1 的巨型对象、零拷贝、JNI 库(TensorFlow、OpenCV)等。

  4. 优势:

    • 不受 -Xmx 限制,只在 MaxDirectMemorySize(默认 ≈ 堆大小)以内。

    • 省去一次 堆内↔堆外 拷贝,IO 性能高。

    • 减轻 GC 压力(逻辑上不在 GC 堆)。

6.3 内存溢出(OOM)

  1. 异常信号:
    java.lang.OutOfMemoryError: Direct buffer memory

  2. 触发条件:

    • 已分配量 ≥ MaxDirectMemorySize;

    • 或 物理内存 + swap 被耗尽。

  3. 定位三板斧:

    jmap -dump → MAT / YourKit 看 DirectBuffer 对象个数;

    ② NMT(Native Memory Tracking)jcmd VM.native_memory summary

    -XX:MaxDirectMemorySize=size 调大做对比实验。

  4. 常见代码级泄漏:

    • allocateDirectrelease(Netty 未 Unpooled.release);

    • 使用 FileChannel.map() 反复映射文件,忘记 unmap()

    • JNI 库内部 malloc 不报 JVM 账目。

6.4 释放原理(重点)

  1. 分配对象 vs 分配内存

    • JVM 端DirectByteBuffer 实例(很小)仍在 Java 堆,受 GC 管理。

    • OS 端 :真正的堆外页由 malloc / mmap 完成,GC 不负责

  2. 两种回收路径:

    1. 显式
      ((DirectBuffer) buf).cleaner().clean()Unsafe.freeMemory(address) 立即归还 OS。

    2. 依赖 GC(默认)

      • 对象进入老年代 → Full GC 或 System.gc() 触发 → Cleaner(PhantomReference)钩子执行 → 调用 freeMemory

      • 因此「堆越慢,直接内存越晚释放」。

  3. 关键源码:
    jdk/internal/misc/Unsafe.allocateMemory
    java.nio.DirectByteBuffer.Deallocator.run()

6.5 禁用显式回收对直接内存影响

  1. 参数:-XX:+DisableExplicitGC(生产常用,防代码里频繁 System.gc())。

  2. 副作用:

    • 直接内存的 Cleaner 钩子仍会被 Full GC 触发 ,但 Young GC 不再处理

    • 若应用从不触发老年代收集 (对象很快死亡或晋升阈值高),堆外内存可能长时间得不到释放,看起来就像"泄漏"。

  3. 官方建议:

    • 开启 -XX:+DisableExplicitGC 时,主动定期触发 Full GC手动 clean

    • 或者使用 Netty-4 的 -Dio.netty.maxDirectMemory=0 让 Netty 自己管理计数并强制 clean;

    • JDK 17+ 的 Foreign Memory APIMemorySegment) 已支持 scoped 释放,无需依赖 GC。

二.垃圾回收

1.如何判断对象可以回收

1.1 引用计数法

1.2 可达性分析算法

JVM 进行垃圾回收时,并不会一次性遍历整个堆,而是从这些 Roots 出发,标记所有仍然存活的对象从 Roots 无法到达的对象,就被判定为"已死",可以回收

MAT 中的名称 对应 JVM 内部概念 为什么能成为 Root
System Class Bootstrap/Extension 类加载器 加载的类(如 java.lang.* 这些类永远不会被卸载,其静态字段引用的对象始终存活。
Co Native Stack JNI 本地方法栈中的引用 本地 C/C++ 代码可能通过 JNI 访问 Java 对象,JVM 必须保证它们存活。
Thread 正在执行的 Java 线程 每个线程的 栈帧局部变量表 里的引用都是活的。
Busy Monitor 被 synchronized 加锁的对象(Monitor 入口集) 同步块还没退出,锁对象必须存活,否则解锁时会崩溃。

1.3 五种引用

引用类型 回收时机 与队列配合 典型场景
强引 Strong Object o = new Object() 永远不会被 GC 日常代码
软引用 Soft SoftReference<Object>(obj) 内存不足时(OOM 前) 可以带队列,用于 缓存清理通知 Guava 内存缓存、图片缓存
弱引用 Weak WeakReference<Object>(obj) 下一次 GC 无论内存够不够 必带队列 ,用于 清理通知 ThreadLocalMap 的 key、WeakHashMap
虚引用 Phantom PhantomReference<Object>(obj, queue) 对象已被回收后get() 永远 null 必须带队列 ,用于 回收后置动作 DirectByteBuffer 堆外内存释放(Cleaner)
终引用 Finalizer (仅内部 FinalizerReference 第一次不可达 → 加入 Finalizer 队列 → 执行 finalize() → 再次 GC 才回收 JVM 内部队列,开发者无感知 兼容历史 Object.finalize()性能差,已废弃

**方法1:**当List与byte[ ]为强引用时,内存溢出不会执行GC垃圾回收。

方法2:当List强引用一个软引用对象,而软引用对象再引用byte[ ]时,当内存不足时(OOM 前)执行GC

引用队列

2.垃圾回收算法

3.分代垃圾回收

4.垃圾回收器

4.1 串行

4.2 吞吐量优先

参数 含义 示例值
-XX:+UseParallelGC 新生代用 Parallel Scavenge(并行复制) 默认即开启
-XX:+UseParallelOldGC 老年代用 Parallel Old(并行标记-整理) 与上面配对
-XX:+UseAdaptiveSizePolicy 自动调整新生代大小、Survivor 比例等,以达到目标停顿/吞吐量 默认开启
-XX:GCTimeRatio=ratio 设定 吞吐量目标1 / (1 + ratio) 的时间用于 GC,默认 99 → 允许 1% 时间做 GC 99
-XX:MaxGCPauseMillis=ms 最大停顿时间目标(毫秒),软目标,JVM 会尽量缩小新生代来缩短停顿 200
-XX:ParallelGCThreads=n GC 线程数,默认 = CPU 核心数,可手动减少避免抢占业务线程 n

吞吐量和停顿时间为相反的目标。

4.3 响应时间优先

参数 作用
-XX:+UseConcMarkSweepGC 开启 CMS 老年代收集器(JDK 8 默认即启用)
-XX:+UseParNewGC 新生代配套使用 ParNew(并行复制,只有它支持 CMS)
-XX:ParallelGCThreads=n STW 阶段(初始标记、重新标记)的并行线程数
-XX:ConcGCThreads=threads 并发阶段 (并发标记、并发清理)的线程数,一般设为 (n+3)/4
-XX:CMSInitiatingOccupancyFraction=percent 老年代使用率 ≥ percent% 时启动 CMS GC,默认 68
-XX:+CMSScavengeBeforeRemark 在"重新标记"前先做一次 Young GC,减少跨代引用扫描量
  1. 初始标记(Initial Mark)

    • Stop-The-World(图里 CPU0~3 短暂阻塞)

    • 只标记 GC Roots 直接关联的对象,耗时极短。

  2. 并发标记(Concurrent Mark)

    • 与应用线程并发执行(CPU 继续跑业务)

    • 从 Roots 出发,标记 所有可达对象

  3. 重新标记(Remark)

    • 第二次 STW(图里 CPU0/2/3 再次短暂阻塞)

    • 修正并发阶段因用户线程运行而产生的 变动

  4. 并发清理(Concurrent Sweep)

    • 与应用线程并发执行

    • 回收 标记为垃圾的对象,不产生压缩,因此有碎片。

优点

  • 大部分 GC 工作与业务并发,停顿时间极短(通常 < 200 ms)。

  • 适合 Web/API、交互式应用 等对响应时间敏感的场景。

缺点

  • 内存碎片 + 浮动垃圾(并发清理时用户线程仍在分配)。

  • JDK 9 已废弃,JDK 14 移除;官方推荐 G1ZGC 替代。


一句话记忆

"CMS = ** Initial STW → 并发标记 → Remark STW → 并发清理 **;
参数控线程数、触发阈值、预清理
停顿短,但碎片多,已过时。"

4.4 G1

(1)G1回收阶段

(2)Young Collection**(年轻代收集)**

  • Stop-The-World ,但极短(毫秒级)。

  • 并行复制 Eden + Survivor → 新 Survivor / Old Region。

  • 完成后 Eden Region 被清空,存活对象年龄 +1

(3)Young Collection + CM(并发标记)

  • 与应用线程并发执行,无停顿。

  • 从 GC Roots 出发,三色标记 整个堆,记录每个 Region 的存活比例

  • 子阶段:

    • 初始标记(STW,极短)

    • 根区域扫描

    • 并发标记

    • 最终标记(STW,修正并发期间变动)

  • 目的:选出"垃圾最多"的 Old Region ,为下一步 Mixed GC 做准备。

(4)Mixed Collection(混合收集)

  • Stop-The-World ,但只回收垃圾最多的部分 Old Region + 全部 Young Region

  • 用户可设 停顿目标-XX:MaxGCPauseMillis),G1 根据历史成本动态挑选 Region 数量,保证在目标时间内完成。

  • 连续多次 Mixed 直到 Old Region 垃圾占比 < G1MixedGCLiveThresholdMixed 次数上限

(5)FUll GC

收集器 新生代算法 Minor GC 停顿时长 老年代算法 Full GC 停顿时长 特点
Serial Serial (单线程) Serial Old (单线程) 简单,小堆
Parallel ParNew (多线程) Parallel Old 较长(多线程) 吞吐量优先
CMS ParNew Concurrent Mark-Sweep 两次极短 STW + 并发清理 响应优先,碎片
G1 Parallel Region 可预测短 Region-Based Mixed 可预测短(只收垃圾最多 Region) 大堆、低停顿

(6)Young GC(跨代引用)

"卡表 + 脏卡 + 后置写屏障" = G1 在 Young GC 时快速找到 老年代→新生代 的引用,避免全堆扫描。


① 为什么会有"跨代引用"

Young GC 只收 Eden/Survivor,但
老年代对象可能持有新生代对象的引用 (图里箭头),如果 Young GC 时不扫描"老年代 → 新生代"的引用 ,就会出现 "存活对象被误回收" ,导致 数据丢失甚至 JVM 崩溃。。

若不做记录,就必须 扫描整个老年代 才能确定存活 → 代价太大。


② 卡表(Card Table)------"老年代→新生代"的 粗粒度地图
  • 整个堆 划分成 512 字节的 卡片(Card)

  • 每个卡片对应 1 字节卡表项

    • 0 = 干净(无跨代引用)

    • 1 = 脏(Dirty)(可能持有新生代引用)


③ 后置写屏障(Post-Write Barrier)+ 脏卡队列

应用线程 修改字段时,JVM 插入一段 极小汇编代码

复制代码
; 伪汇编
cmp   rdi, young_start
jb    skip             ; 如果写入目标是老年代,继续
mov   byte [card_table + addr>>9], 1   ; 把对应卡标记为脏
skip:
  • 写入 老年代→新生代 引用 → 当前卡被标记为

  • 为了 不阻塞应用线程 ,真正的"脏卡"先放进 Dirty Card Queue ,由 并发 refinement 线程 异步处理。


④ Remembered Set(RSet)------"卡表→Region"的 精确定位
  • 每个 Region 维护一个 RSet (HashMap 结构):
    Key = 其他 Region 的起始地址
    Value = 这些 Region 中 **哪些卡** 指向我

  • Young GC 时 只扫描 RSet 记录的脏卡 → 瞬间得到 跨代引用集合,无需全堆扫描。


⑤ Young GC 时的工作流程(结合图)
  1. Root Scan → 扫描线程栈、JNI 等 固定根

  2. Scan RSet → 只遍历 脏卡里的对象 ,找到 老→新引用

  3. Copy & Update → 把存活对象复制到 Survivor/Old,并 更新指针

  4. Clear Dirty Card → 当前 Region 的脏卡清零,等待下次写入。


一句话记忆

"写操作 → 写屏障 → 脏卡 → RSet → Young GC 只扫脏卡 ",
"卡表是地图,RSet是坐标,写屏障是 GPS 更新器"

(7)Remark

这是 CMS / G1"Remark"(最终标记)阶段 用来 修正并发标记期间被改动的引用 的核心机制:
"预写屏障 + SATB 标记队列" 保证 "并发标记快照" 的完整性。

① 问题背景:并发标记时对象图还在变
  • 并发标记阶段 应用线程仍在运行,字段随时被改写。

  • 若不做记录,新产生的跨代引用存活对象被误标为垃圾 就会漏标 → 漂浮垃圾 甚至 误回收


② SATB(Snapshot-At-The-Beginning) 思路
  • 并发标记开始那一刻 拍一张快照 S

  • 只要当时快照里是活的,就必须被标记为活(即使后面变成垃圾也无所谓)。

  • 新分配的对象 默认 黑色 (存活),旧对象的引用字段被删除/修改 时才需要记录。


③ 预写屏障(Pre-Write Barrier)(底层实现)

位置 :在 应用线程putfield/putstatic/A*STORE 指令前插入一段 极小汇编(几乎无性能损耗):

复制代码
; 伪汇编
oldValue = [field]          ; 先读旧值
if oldValue != null
    satb_enqueue(oldValue)  ; 把旧值压入 SATB 队列
[field] = newValue          ; 再写新值
  • 写前 先把 旧引用 记录下来(不是新引用)。

  • 记录内容:旧目标对象地址 → 压入 线程私有的 SATB 标记队列(SATB Mark Queue)


④ SATB 标记队列(SATB Mark Queue)→ 全局队列
  • 每个应用线程一个 本地 SATB 队列(无锁,极快)。

  • 队列满后 批量刷新到全局 SATB 队列

  • Remark 阶段 只需 扫描全局队列里的对象 ,就能 把"被删掉"的引用重新标记为存活 ,保证 快照 S 的完整性


⑤ 时间线(配合图)
  1. 并发标记开始 → 生成快照 S

  2. 应用线程 A 执行obj.f = newObj(原 f 指向 X)

    预写屏障记录 XX 进入 SATB 队列

  3. Remark 阶段扫描 SATB 队列把 X 重新标记为活

  4. 结果X 不会被 Young GC/Mixed GC 误回收浮动垃圾 最多留到下次。


一句话记忆

"预写屏障 = 写前拍照SATB 队列 = 照片底片Remark = 把底片洗出来保证并发标记不漏活对象。"

(8)JDK8u20字符串去重

(9)JDK 8u40 并发标记类卸载

(10)JDK 8u60 回收巨型对象

(11)JDK9并发标记起始时间的调整

相关推荐
why1516 小时前
面经整理——操作系统
java·开发语言·jvm
.生产的驴10 小时前
泛微E10二开 组织架构、人员信息、分部信息基本操作
java·jvm·spring·架构·tomcat·intellij-idea·hibernate
是一个Bug10 小时前
中高级Java开发岗位 技术框架
java·jvm·面试
alien爱吃蛋挞10 小时前
【JavaEE】万字详解JVM
java·jvm·java-ee
不会写程序的未来程序员10 小时前
JVM 运行时内存模型
java·开发语言·jvm
yong999011 小时前
C# 入门级库存管理系统
jvm·oracle·c#
没有bug.的程序员12 小时前
GC 调优实战:从慢到快的真实案例
java·jvm·测试工具·gc·gc调优
dllxhcjla12 小时前
IO流.java
jvm
7ioik1 天前
jvm内存结构深入
jvm