目录
- 一、垃圾回收机制(GC)
-
- [1. 什么是垃圾?](#1. 什么是垃圾?)
- [2. 怎么找垃圾?](#2. 怎么找垃圾?)
- [3. 3 种回收算法](#3. 3 种回收算法)
- [4. 主流垃圾收集器](#4. 主流垃圾收集器)
- [5. 选型](#5. 选型)
- [二、JVM 参数调优](#二、JVM 参数调优)
-
- [1. 标准前缀 `-`](#1. 标准前缀
-) - [2. 非标准基础前缀 `-X`(调优内存必用)](#2. 非标准基础前缀
-X(调优内存必用)) - [3. 高级非标准前缀 `-XX`(调优核心)](#3. 高级非标准前缀
-XX(调优核心)) - [4. 系统属性前缀 `-D`](#4. 系统属性前缀
-D) - [5. 终极记忆口诀](#5. 终极记忆口诀)
- [6. 关键执行规则](#6. 关键执行规则)
- [7. 最常用基础参数](#7. 最常用基础参数)
- [8. 生产通用最佳配置](#8. 生产通用最佳配置)
- [1. 标准前缀 `-`](#1. 标准前缀
- 三、线上问题排查(OOM、频繁GC、CPU飙高、死锁)
-
- [1. OOM 排查(最常见)](#1. OOM 排查(最常见))
- [2. 频繁 GC / 卡顿排查](#2. 频繁 GC / 卡顿排查)
- [3. CPU 飙高排查](#3. CPU 飙高排查)
- [4. 死锁排查](#4. 死锁排查)
- [四、内存泄漏完整排查 & 解决手册](#四、内存泄漏完整排查 & 解决手册)
-
- 1、先判断:是不是内存泄漏?
- 2、标准排查步骤(线上直接照做)
-
- [1). 第一步:先抓堆快照(必须)](#1). 第一步:先抓堆快照(必须))
- [2). 第二步:用 MAT 打开快照(最关键)](#2). 第二步:用 MAT 打开快照(最关键))
- [3). 第三步:定位泄漏根因(90% 都是这几类)](#3). 第三步:定位泄漏根因(90% 都是这几类))
- [3、MAT 怎么看?](#3、MAT 怎么看?)
- 4、通用解决套路(万能)
- 5、最简单判断代码是否泄漏的方法
- 五、必备工具
-
- [1. 命令行工具(JDK自带)](#1. 命令行工具(JDK自带))
- [2. 可视化/高级工具](#2. 可视化/高级工具)
一、垃圾回收机制(GC)
1. 什么是垃圾?
没有任何引用指向的对象。
2. 怎么找垃圾?
- 引用计数:简单但无法解决循环引用
- 可达性分析 (JVM 默认)
从 GC Roots(栈变量、类静态变量、本地方法引用)往下找,找不到就是垃圾。
3. 3 种回收算法
- 标记-清除:简单、碎片多
- 标记-复制:适合存活少的场景(新生代)
- 标记-整理:适合存活多的场景(老年代)
4. 主流垃圾收集器
① G1(JDK9+ 默认)
- 面向低延迟 + 大堆
- 把堆分成很多 Region
- 可指定停顿时间:
-XX:MaxGCPauseMillis=200 - 适用:8G 以下堆,通用场景
② ZGC(JDK11+ 生产级)
- 亚毫秒级停顿(<10ms)
- 不分代,基于 Region 颜色指针
- 适用:大内存(32G+)、低延迟要求(金融、支付)
③ Shenandoah
- 和 ZGC 对标,开源社区主流
- 超低停顿,跨平台兼容好
5. 选型
- 普通服务:G1
- 低延迟金融/电商核心:ZGC
二、JVM 参数调优
在 JVM 调优和 Java 程序启动中,所有参数只有 4 个核心前缀,分为两大类:
- JVM 虚拟机参数 (控制 JVM 自身运行、内存、GC):
-、-X、-XX - Java 系统属性参数 (给 Java 程序传参,和 JVM 调优无关):
-D
| 前缀符号 | 官方定义 | 核心含义 | 主要用途 | JDK 版本兼容程度 | 最常用示例 | 关键备注 |
|---|---|---|---|---|---|---|
- |
标准参数 | 通用基础命令 | 基础启动、查信息、指定运行方式 | 🌟🌟🌟🌟🌟(100%稳定) | -version(查版本) -jar(启动jar) -cp(指定类路径) |
所有JVM都兼容,不用于调优,纯基础命令 |
-X |
非标准参数 | 扩展基础参数 | JVM 核心内存配置(调优基础) | 🌟🌟🌟🌟(极稳定) | -Xms(初始堆) -Xmx(最大堆) -Xss(线程栈大小) |
调优必用!所有商用JVM都支持,几乎不废弃 |
-XX |
高级非标准参数 | 底层调优参数 | GC算法、性能调优、底层开关(调优核心) | 🌟🌟🌟(版本兼容) | -XX:+UseG1GC(开启G1) -XX:MaxGCPauseMillis=200 |
专业调优90%用它,必须大写XX,HotSpot专属 |
-D |
系统属性参数 | 程序自定义参数 | 给Java应用传配置(Spring/环境变量) | 🌟🌟🌟🌟🌟(100%稳定) | -Dspring.profiles.active=prod -Dfile.encoding=utf-8 |
不是JVM调优参数!是给程序用的,启动位置和JVM参数一致 |
1. 标准前缀 -
-
最基础、最通用,所有 Java 虚拟机都必须支持
-
用途:只是告诉 JVM 「怎么运行」,不调优内存、GC
-
例子:
bashjava -version # 查看JDK版本 java -jar app.jar # 启动jar包
2. 非标准基础前缀 -X(调优内存必用)
-
JVM 官方扩展参数,专门用来配置内存大小
-
是 JVM 调优的入门必备,所有生产环境都会用
-
固定用法:直接跟数值/单位
-
核心记忆:
-X管内存 -
最常用 4 个:
bash-Xms1g # 初始堆内存 1G -Xmx1g # 最大堆内存 1G(生产建议和Xms一致) -Xss512k # 单个线程栈大小 512k -Xmn512m # 年轻代内存 512m
3. 高级非标准前缀 -XX(调优核心)
-
JVM 底层专业调优参数,控制垃圾回收、性能、日志
-
只有 HotSpot 虚拟机(Oracle JDK/OpenJDK)支持
-
两种固定写法(大小写严格区分 ):
- 开关型 (开启/关闭功能):
-XX:+参数/-XX:-参数 - 赋值型 (设置数值):
-XX:参数=值
- 开关型 (开启/关闭功能):
-
核心记忆:
-XX管GC和高级性能 -
常用例子:
bash-XX:+UseG1GC # 开启G1垃圾回收器(生产主流) -XX:MaxGCPauseMillis=200 # GC最大停顿时间200ms -XX:MetaspaceSize=256m # 元空间初始大小
4. 系统属性前缀 -D
-
和 JVM 调优毫无关系!
-
作用:给 Java 程序传递自定义配置(比如 Spring 环境、编码格式)
-
例子:
bash-Dspring.profiles.active=prod # 指定Spring生产环境 -Duser.timezone=GMT+8 # 设置时区
5. 终极记忆口诀
-基础命令(查版本、启动jar)-X管内存(堆、栈、年轻代)-XX管调优(GC、性能、底层)-D管程序(传配置、环境变量)
6. 关键执行规则
所有 4 类前缀的参数,都必须写在:
java [所有参数] -jar app.jar
(参数放在 java 和 -jar 中间,放在后面无效)
7. 最常用基础参数
-Xms2g 初始堆内存
-Xmx2g 最大堆内存(生产必须 = Xms,避免扩容停顿)
-Xss1m 线程栈大小
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
# G1 配置
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 期望最大停顿毫秒
8. 生产通用最佳配置
-Xms4g -Xmx4g -Xss1m
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/logs/gc.log
三、线上问题排查(OOM、频繁GC、CPU飙高、死锁)
1. OOM 排查(最常见)
现象:java.lang.OutOfMemoryError: Java heap space
步骤:
-
加参数让 OOM 时自动导出堆快照
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/logs/dump.hprof -
用 MAT/JProfiler 打开快照
-
看:谁占用最大、谁无法回收(内存泄漏)
典型泄漏场景:
- static Map 一直放对象不清理
- 线程池无界队列
- 连接未关闭(数据库/HTTP)
2. 频繁 GC / 卡顿排查
现象:服务卡顿、GC 时间超长、YGC 每秒多次
命令
bash
jstat -gc 进程ID 1000 # 每秒打印GC情况
看 2 个指标
- YGC 次数过高:Eden 太小
- FGC 频繁:老年代满、内存泄漏
解决
- 加大堆
- 检查内存泄漏
- 换 G1/ZGC
3. CPU 飙高排查
步骤
- top 找到 CPU 最高的 Java 进程
- 打印线程栈
bash
jstack 进程ID > stack.log
- 找到耗 CPU 的线程 NID
- 定位代码:死循环、复杂计算、正则失控
4. 死锁排查
命令
bash
jstack 进程ID
拉到最后,会直接显示:
Found 1 deadlock.
直接告诉你哪两个线程、锁互相等待。
解决:统一锁的获取顺序、加超时、用线程池。
四、内存泄漏完整排查 & 解决手册
1、先判断:是不是内存泄漏?
满足下面 3 条,基本就是泄漏:
- 运行时间越长,内存占用只升不降
- Full GC 越来越频繁,但老年代回收不掉多少
- 最终 OOM
不是泄漏的情况:
- 一次查询 100w 数据直接 OOM
- 堆设置太小(Xmx 只有 256M)
- 一次性创建超大对象
2、标准排查步骤(线上直接照做)
1). 第一步:先抓堆快照(必须)
在 Java 启动参数加上(生产必加):
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump/
OOM 时自动导出 xxx.hprof 快照。
如果没加参数,想在线抓:
bash
jps -l # 找到进程 PID
jmap -dump:format=b,file=my.hprof <PID>
2). 第二步:用 MAT 打开快照(最关键)
打开 MAT → 导入 hprof 文件
看 3 个地方:
-
Dominator Tree(支配树)
看谁占内存最大,谁是根引用。
-
Histogram(直方图)
看哪个类的对象数量爆炸式增长。
-
Path to GC Roots
右键对象 → 排除弱引用/软引用
看谁一直牵着这个对象不让 GC 回收
3). 第三步:定位泄漏根因(90% 都是这几类)
① 静态集合(最常见)
java
private static List<User> CACHE = new ArrayList<>();
只 add 不 remove,永远不回收。
MAT 表现:ArrayList/HashMap 对象超多,GC Root 是 static 修饰。
② ThreadLocal 没 remove
java
ThreadLocal<Object> tl = new ThreadLocal<>();
tl.set(new Object());
// 用完不 remove
线程池线程复用 → 对象永远泄漏。
MAT 表现:ThreadLocalMap$Entry 数量巨大。
③ 未关闭的资源
- Connection
- InputStream
- Socket
- 自定义的池化资源
MAT 表现:大量 IO 相关对象、连接对象无法回收。
④ 内部类持有外部类引用(匿名内部类)
java
class A {
void test() {
new Thread(() -> { }).start();
}
}
线程没结束 → A 对象无法回收。
⑤ 监听器/回调注册了没取消
java
eventBus.register(this);
// 没 unregister
3、MAT 怎么看?
假设你看到:
com.xxx.User对象有 100w 个- 右键 → Path to GC Roots → exclude all phantom/weak/soft
结果指向:
static com.xxx.service.UserService.cache
那就是:
UserService 里有个 static Map 一直存 User,不清理 → 内存泄漏
4、通用解决套路(万能)
-
静态容器一定要加过期/淘汰
- 使用
LinkedHashMap做 LRU - 或直接用
Caffeine/Guava Cache(带自动过期)
- 使用
-
ThreadLocal 必须用完 remove
java
try {
tl.set(obj);
} finally {
tl.remove();
}
- 流、连接必须关闭
java
try (InputStream in = ...) {
}
-
监听器/回调必须成对注册/取消
-
线程池任务执行完必须释放大对象
不要让任务持有长生命周期引用。
5、最简单判断代码是否泄漏的方法
在代码里搜这几个关键词,基本一抓一个准:
static Mapstatic ListThreadLocalnew Thread()不用线程池register没有unregister- 没有
close()的 IO/连接
五、必备工具
1. 命令行工具(JDK自带)
- jps:看 Java 进程
- jstack:查死锁、CPU 高、线程栈
- jmap:导出堆快照
- jstat:实时 GC 监控
2. 可视化/高级工具
- MAT:分析堆泄漏(最强)
- Arthas :阿里开源,线上无重启排查
- 看方法执行耗时
- 看方法入参出参
- 热更代码
- GCEasy:GC 日志在线分析