JVM 底层与调优

目录

  • 一、垃圾回收机制(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. 生产通用最佳配置)
  • 三、线上问题排查(OOM、频繁GC、CPU飙高、死锁)
    • [1. OOM 排查(最常见)](#1. OOM 排查(最常见))
    • [2. 频繁 GC / 卡顿排查](#2. 频繁 GC / 卡顿排查)
    • [3. CPU 飙高排查](#3. CPU 飙高排查)
    • [4. 死锁排查](#4. 死锁排查)
  • [四、内存泄漏完整排查 & 解决手册](#四、内存泄漏完整排查 & 解决手册)
  • 五、必备工具
    • [1. 命令行工具(JDK自带)](#1. 命令行工具(JDK自带))
    • [2. 可视化/高级工具](#2. 可视化/高级工具)

一、垃圾回收机制(GC)

1. 什么是垃圾?

没有任何引用指向的对象。

2. 怎么找垃圾?

  • 引用计数:简单但无法解决循环引用
  • 可达性分析 (JVM 默认)
    从 GC Roots(栈变量、类静态变量、本地方法引用)往下找,找不到就是垃圾。

3. 3 种回收算法

  1. 标记-清除:简单、碎片多
  2. 标记-复制:适合存活少的场景(新生代)
  3. 标记-整理:适合存活多的场景(老年代)

4. 主流垃圾收集器

① G1(JDK9+ 默认)

  • 面向低延迟 + 大堆
  • 把堆分成很多 Region
  • 可指定停顿时间:-XX:MaxGCPauseMillis=200
  • 适用:8G 以下堆,通用场景

② ZGC(JDK11+ 生产级)

  • 亚毫秒级停顿(<10ms)
  • 不分代,基于 Region 颜色指针
  • 适用:大内存(32G+)、低延迟要求(金融、支付)

③ Shenandoah

  • 和 ZGC 对标,开源社区主流
  • 超低停顿,跨平台兼容好

5. 选型

  • 普通服务:G1
  • 低延迟金融/电商核心:ZGC

二、JVM 参数调优

在 JVM 调优和 Java 程序启动中,所有参数只有 4 个核心前缀,分为两大类:

  1. JVM 虚拟机参数 (控制 JVM 自身运行、内存、GC):--X-XX
  2. 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

  • 例子:

    bash 复制代码
    java -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)支持

  • 两种固定写法(大小写严格区分 ):

    1. 开关型 (开启/关闭功能):-XX:+参数 / -XX:-参数
    2. 赋值型 (设置数值):-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. 终极记忆口诀

  1. - 基础命令(查版本、启动jar)
  2. -X 管内存(堆、栈、年轻代)
  3. -XX 管调优(GC、性能、底层)
  4. -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

步骤

  1. 加参数让 OOM 时自动导出堆快照

    -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath=/logs/dump.hprof

  2. MAT/JProfiler 打开快照

  3. 看:谁占用最大、谁无法回收(内存泄漏)

典型泄漏场景

  • static Map 一直放对象不清理
  • 线程池无界队列
  • 连接未关闭(数据库/HTTP)

2. 频繁 GC / 卡顿排查

现象:服务卡顿、GC 时间超长、YGC 每秒多次

命令

bash 复制代码
jstat -gc 进程ID 1000  # 每秒打印GC情况

看 2 个指标

  • YGC 次数过高:Eden 太小
  • FGC 频繁:老年代满、内存泄漏

解决

  • 加大堆
  • 检查内存泄漏
  • 换 G1/ZGC

3. CPU 飙高排查

步骤

  1. top 找到 CPU 最高的 Java 进程
  2. 打印线程栈
bash 复制代码
jstack 进程ID > stack.log
  1. 找到耗 CPU 的线程 NID
  2. 定位代码:死循环、复杂计算、正则失控

4. 死锁排查

命令

bash 复制代码
jstack 进程ID

拉到最后,会直接显示:

复制代码
Found 1 deadlock.

直接告诉你哪两个线程、锁互相等待。

解决:统一锁的获取顺序、加超时、用线程池。

四、内存泄漏完整排查 & 解决手册

1、先判断:是不是内存泄漏?

满足下面 3 条,基本就是泄漏:

  1. 运行时间越长,内存占用只升不降
  2. Full GC 越来越频繁,但老年代回收不掉多少
  3. 最终 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 个地方:

  1. Dominator Tree(支配树)

    看谁占内存最大,谁是根引用。

  2. Histogram(直方图)

    看哪个类的对象数量爆炸式增长。

  3. 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、通用解决套路(万能)

  1. 静态容器一定要加过期/淘汰

    • 使用 LinkedHashMap 做 LRU
    • 或直接用 Caffeine/Guava Cache(带自动过期)
  2. ThreadLocal 必须用完 remove

java 复制代码
try {
    tl.set(obj);
} finally {
    tl.remove();
}
  1. 流、连接必须关闭
java 复制代码
try (InputStream in = ...) {
}
  1. 监听器/回调必须成对注册/取消

  2. 线程池任务执行完必须释放大对象

    不要让任务持有长生命周期引用。

5、最简单判断代码是否泄漏的方法

在代码里搜这几个关键词,基本一抓一个准

  • static Map
  • static List
  • ThreadLocal
  • new Thread() 不用线程池
  • register 没有 unregister
  • 没有 close() 的 IO/连接

五、必备工具

1. 命令行工具(JDK自带)

  • jps:看 Java 进程
  • jstack:查死锁、CPU 高、线程栈
  • jmap:导出堆快照
  • jstat:实时 GC 监控

2. 可视化/高级工具

  • MAT:分析堆泄漏(最强)
  • Arthas :阿里开源,线上无重启排查
    • 看方法执行耗时
    • 看方法入参出参
    • 热更代码
  • GCEasy:GC 日志在线分析
相关推荐
三棱球2 小时前
Java 基础教程 Day2:从数据类型到面向对象核心概念
java·开发语言
indexsunny2 小时前
互联网大厂Java面试实录:微服务+Spring Boot在电商场景中的应用
java·spring boot·redis·微服务·eureka·kafka·spring security
wuminyu2 小时前
专家视角看Java线程生命周期与上下文切换的本质
java·linux·c语言·jvm·c++
HHHHH1010HHHHH2 小时前
如何在 WordPress 中通过邮箱获取用户 ID
jvm·数据库·python
2301_782659182 小时前
C#怎么使用LINQ Contains包含判断 C#如何用Contains实现类似SQL IN查询的集合包含判断【语法】
jvm·数据库·python
程序猿乐锅2 小时前
Java第十三篇:Stream流
java·笔记
2301_773553622 小时前
如何优化深分页场景下的回表代价_延迟关联与主键游标分页
jvm·数据库·python
林三的日常2 小时前
SpringBoot + Druid SQL Parser 解析表名、字段名(纯Java,最佳方案)
java·spring boot·sql
deviant-ART2 小时前
java stream 的 findFirst 和 findAny 踩坑点
java·开发语言·后端