JVM内存与GC笔记

JVM 堆内存分配与 GC 流程

一、堆内存结构

复制代码
                         堆(Heap)
┌─────────────────────────────────────────────┐
│              新生代(Young)                  │
│  ┌──────────────┬────────┬────────┐         │
│  │    Eden      │  S0    │  S1    │         │
│  │  (伊甸园)     │(幸存区) │(幸存区) │         │
│  │   占 8/10    │ 1/10   │ 1/10   │         │
│  └──────────────┴────────┴────────┘         │
│                                              │
│  ┌──────────────────────────────────────────┐│
│  │             老年代(Old)                  ││
│  │     长期存活的对象(Bean、连接池等)         ││
│  └──────────────────────────────────────────┘│
└─────────────────────────────────────────────┘

二、对象分配与 GC 完整流程(Parallel / Serial GC)

初始状态

复制代码
Eden: 空
S0:   空    ← from 区(存放上次 GC 活下来的对象)
S1:   空    ← to  区(这次 GC 要往这挪)

第 1 次:new 对象填满 Eden

复制代码
① new 对象全部放在 Eden
② Eden 满了 → 触发 Minor GC
③ 暂停所有业务线程(STW)
④ 标记 Eden 里哪些对象还在被引用(还活着)
⑤ 把活着的对象 → 挪到 S0(此时 S0 是 to 区,接收方)
⑥ dead 的直接扔掉
⑦ 清空 Eden
⑧ 恢复业务线程

结果:

复制代码
Eden: 空
S0:   存活对象 A(age=1)    ← from(这里存着活下来的对象)
S1:   空                   ← to(下次的接收方)

第 2 次:Eden 又满了

复制代码
① 新对象再次填满 Eden
② 触发 Minor GC
③ STW
④ 标记 Eden 中存活的对象
⑤ Eden 存活对象 → 挪到 S1(S1 是 to 区)
⑥ S0 里的对象 A 如果还活着 → 也挪到 S1,年龄+1
   如果 A 已经死了(没人引用)→ 直接扔掉
⑦ 清空 Eden + S0
⑧ 恢复业务线程

结果:

复制代码
Eden: 空
S0:   空                   ← to(角色互换)
S1:   存活对象 A(age=2), 新存活对象 B(age=1)   ← from

第 3 次:Eden 又满了

复制代码
① 触发 Minor GC
② STW
③ Eden 存活对象 → 挪到 S0(S0 现在是 to 区)
④ S1 里的 A 和 B 如果还活着 → 挪到 S0,年龄继续+1
⑤ 如果 A 年龄达到阈值(默认 15 次)→ 升到老年代,不进 S0
⑥ 清空 Eden + S1

结果:

复制代码
Eden: 空
S0:   A(→老年代), B(age=3), C(age=1)  ← from
S1:   空                               ← to

以后每次循环

复制代码
Eden 满 → Minor GC →
  ① Eden 存活对象 → 挪到 to 区
  ② from 区存活对象 → 也挪到 to 区(年龄+1)
  ③ 年龄到阈值(默认 15)→ 升老年代
  ④ 死了 → 直接扔掉
  ⑤ 清空 Eden + from 区
  ⑥ S0/S1 角色互换(from ↔ to)

关键点总结

概念 说明
from 区 上次 GC 存活对象所在的位置
to 区 空的,这次 GC 要往这挪
每次 GC 完 from 清空,from ↔ to 互换
对象年龄 每熬过一次 GC 就 +1
晋升阈值 默认 15 次(-XX:MaxTenuringThreshold
S0/S1 为什么不用一个 避免碎片。两个区交替用,每次 to 区是紧凑排列
两个 Survivor 都放不下 直接升老年代(Handle Promotion Failure)

三、Full GC 完整流程

触发条件

条件 说明
老年代占用 > 92%(默认) JVM 提前触发,不等满
Minor GC 前预测装不下 新生代要升对象,老年代剩余 < 新生代存活总量,直接 Full GC
Metaspace 满了 类定义太多,扩容失败
System.gc() 代码里主动调,或 JVM 参数 -XX:+ExplicitGCInvokesConcurrent
CMS GC 失败(G1 的并发模式失败) GC 线程跑得没业务线程快

Full GC 执行过程(以 Parallel GC 为例)

复制代码
① STW(Stop The World)--- 暂停所有业务线程
      ↓
② 标记阶段 --- 从 GC Roots 出发,扫描整个堆
   (新生代 + 老年代 + Metaspace)
   标记所有存活的对象
      ↓
③ 清理阶段 --- 回收所有没被标记的对象
      ↓
④ 压缩阶段 --- 把存活对象往老年代前端搬,消除碎片
      ↓
⑤ 恢复业务线程

Full GC 的代价

复制代码
堆大小    预计停顿时间
──────────────────────
256m      ~0.1 秒
1g        ~0.3-0.5 秒
4g        ~1-2 秒
8g        ~2-5 秒
16g       ~5-10+ 秒

Full GC 期间:你的服务完全不可用,所有请求排队等 GC 结束。

所以生产上要尽量避免 Full GC。如果发生,最好是凌晨低峰期。

什么样的对象会在 Full GC 中被回收

只回收"从 GC Roots 出发找不到"的对象,不看它在哪个区。

复制代码
不会被回收(引用链完整):
  GC Root → ApplicationContext → Bean → 其他 Bean → 连接池

会被回收(引用链断了):
  方法返回后 → 局部变量出栈 → 临时对象变成垃圾 → Full GC 看到就收

Spring 管理的 Bean、数据库连接池等,只要引用链完整,Full GC 100% 不动它们。


四、各 GC 回收器对比

回收器 JDK 默认 S0/S1 互换 特点
Serial JDK 5/6(客户端默认) 单线程,小堆 < 100m
Parallel JDK 8 默认 多线程,高吞吐,大堆 STW 长
G1 JDK 9+ 默认 分区 Region,每次只清最脏的区,停顿可控
ZGC JDK 21 可用 不分代,几乎不停顿

按场景选

场景 推荐 原因
普通微服务(JDK 8) Parallel(默认不改) 最稳
普通微服务(JDK 17+) G1(默认不改) 默认就是 G1
用户量大、GC 卡顿被投诉 G1 控制停顿 200ms
极致低延迟 ZGC 几乎零停顿
开发环境 IDEA 跑微服务 G1 分区清理,体感卡顿少

五、生产中需要关注的 JVM 参数

5.1 必设参数

ini 复制代码
# 堆大小(最重要的两个)
-Xms4g                # 初始堆,建议 = -Xmx
-Xmx4g                # 最大堆,设物理内存的 50%-70%

# 元空间
-XX:MetaspaceSize=256m     # 触发 Full GC 的阈值,提前扩容到位
-XX:MaxMetaspaceSize=256m  # 上限,防止类加载器泄漏撑爆内存

# 栈大小(一般不动)
-Xss1m                # 每个线程 1m,线程太多时可降为 512k

5.2 堆大小计算公式

复制代码
机器内存 → 分配给 JVM 堆的比例

机器内存    建议堆(单服务独占)
────────────────────────
4G          -Xmx2g
8G          -Xmx4g~5g
16G         -Xmx8g~10g
32G         -Xmx16g
                     剩下内存留给:操作系统 + 文件缓存 + 其他进程

多个服务分摊:
  8 个微服务 + 32G 机器 → 每个 -Xmx3g
  先保守,监控发现堆长期用不满再降

5.3 GC 相关参数

ini 复制代码
# 选择 GC
-XX:+UseG1GC                          # 用 G1(JDK 9+ 默认,JDK 8 需手动加)
-XX:+UseParallelGC                    # 用 Parallel(JDK 8 默认)

# G1 调优
-XX:MaxGCPauseMillis=200              # 目标 GC 停顿不超过 200ms
-XX:ParallelGCThreads=4               # 并行 GC 线程数
-XX:ConcGCThreads=2                   # 并发 GC 线程数(与业务线程并行跑的)

# Parallel 调优
-XX:ParallelGCThreads=4               # GC 线程数
-XX:MaxTenuringThreshold=15           # 对象升老年代年龄阈值

# 通用
-XX:CICompilerCount=2                 # JIT 编译线程数(开发环境设小,省 CPU)
-XX:SoftRefLRUPolicyMSPerMB=50        # 软引用缓存回收策略,设小减少 Full GC

5.4 排查诊断参数

ini 复制代码
# GC 日志(线上必开,几乎无性能损耗)
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/path/gc.log
-XX:+UseGCLogFileRotation              # 日志轮转
-XX:NumberOfGCLogFiles=10
-XX:GCLogFileSize=10M

# OOM 时自动 dump 堆(方便事后分析)
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/heap.hprof

# 内存泄漏时拒绝 GC 回收
-XX:+DisableExplicitGC                # 禁止 System.gc()

5.5 参数实战组合

开发环境(IDEA 跑微服务):

ini 复制代码
-Xms256m -Xmx768m
-XX:+UseG1GC
-XX:CICompilerCount=2
-XX:ParallelGCThreads=4
-XX:SoftRefLRUPolicyMSPerMB=50
-XX:+HeapDumpOnOutOfMemoryError

生产环境(8G 机器单服务):

ini 复制代码
-Xms4g -Xmx4g
-XX:MetaspaceSize=256m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:ParallelGCThreads=4
-XX:ConcGCThreads=2
-XX:+HeapDumpOnOutOfMemoryError
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/app/logs/gc.log
-XX:+DisableExplicitGC

六、线上排查命令

6.1 看 GC 状态

bash 复制代码
jstat -gcutil <PID> 2000

输出说明:

字段 含义 正常值
E Eden 使用率 30-80%
O 老年代使用率 持续上升不降 → 泄漏
YGC Minor GC 次数 一直涨是正常的
YGCT Minor GC 总耗时 / YGC = 单次 < 50ms
FGC Full GC 次数 最好为 0
FGCT Full GC 总耗时 / FGC = 单次 > 1s 说明堆太大

6.2 看各区用量

bash 复制代码
jcmd <PID> GC.heap_info

关注:Eden、Old Gen 的 used 值,看趋势是稳还是涨。

6.3 看什么对象占内存

bash 复制代码
jmap -histo:live <PID> | head -20

看排前面的类名,如果某个业务类实例数异常多,就是泄漏点。

6.4 看 JVM 参数

bash 复制代码
jcmd <PID> VM.flags

确认 -Xmx、GC 类型等参数是否生效。

6.5 堆 Dump 分析

bash 复制代码
# 手动触发 dump(线上谨慎,会 STW)
jmap -dump:format=b,file=heap.hprof <PID>

然后用 MAT(Eclipse Memory Analyzer)打开,看 Leak Suspects 报告。

相关推荐
XS0301061 小时前
并发编程 六
java·后端
yaoxin5211232 小时前
419. 现代 Java IO 最佳实践 - 写入文本文件
java·windows·python
雪宫街道2 小时前
synchronized 锁的范围:对象锁、类锁与代码块锁
java·jvm·后端·面试
x***r1512 小时前
linux安装 jdk-8u291-linux-x64.tar.gz 详细步骤(解压配置环境变量)
java
星恒随风2 小时前
C语言数据结构排序算法详解(下):冒泡排序、快速排序、归并排序和计数排序
c语言·数据结构·笔记·学习·排序算法
极光代码工作室2 小时前
基于SpringBoot的校园论坛系统
java·springboot·web开发·后端开发
XS0301063 小时前
Spring Bean 作用域 & 生命周期
java·后端·spring
NagatoYukee3 小时前
Spring Security基础部分学习
java·学习·spring
彦为君3 小时前
JavaSE-07-异常机制
java·开发语言·后端·python·spring