JVM实战总结笔记

第一章:内存调优 (Memory Tuning)

1.1 核心概念辨析

  • 内存泄漏 (Memory Leak) :
    • 定义: 也就是"占着茅坑不拉屎"。对象已经不再被应用程序使用,但在 JVM 的 GC Root 引用链上依然存在引用,导致垃圾回收器(GC)无法回收它。
    • 后果: 积少成多,如同滚雪球,最终导致堆内存被占满,引发 OOM。
    • 范围 : 绝大多数内存泄漏都发生在 堆内存 (Heap)
  • 内存溢出 (Out Of Memory - OOM) :
    • 定义: 内存不够用了。新对象申请内存时,堆空间不足,且 GC 后仍无法释放足够空间。
    • 关系: 持续的内存泄漏最终会导致内存溢出;但并发量过大导致的瞬间内存不足也会引发 OOM。

1.2 代码级内存泄漏的六大根源与解法

这是开发中最容易踩的坑,请逐一对照检查:

1. equals()hashCode() 实现错误
  • 原理: HashMap 依赖这两个方法判断 Key 是否重复。如果重写错误(例如 hashCode 随机,或 equals 逻辑不对),导致放入 Map 的对象实际上是"同一个"业务对象,但 Map 认为是"新的",从而无限增加。
  • 案例代码 : Student 类作为 Key,但未重写 hashCode。
  • 修正: 使用 IDE 自动生成或 Lombok,确保使用唯一标识(如 ID)参与计算。
2. 内部类引用 (Inner Class)
  • 原理 : 非静态内部类匿名内部类 会隐式持有外部类(Outer Class)的 this 引用。
  • 场景 : 如果在外部类的方法中创建了一个内部类对象(如 new ArrayList {``{ add(...) }}),并将该对象返回或长期持有,那么外部类对象也无法被回收。
  • 修正 :
    • 改为 静态内部类 (static class),切断与外部类的引用。
    • 使用静态方法创建对象,避免匿名内部类捕获 this
3. ThreadLocal 使用不当
  • 场景: 配合线程池使用时。线程池中的线程(Worker)是长期存活的。
  • 泄漏链 : Thread -> ThreadLocalMap -> Entry -> Value (强引用)。即使 Key (ThreadLocal) 是弱引用被回收了,Value 依然被线程持有。
  • 修正 : 必须在拦截器的 afterCompletionfinally 块中显式调用 ThreadLocal.remove()
    • 反例 : 在 postHandle 中清理。如果 Controller 抛异常,postHandle 不会执行,导致泄漏。
4. String.intern() (特定于 JDK 6)
  • 历史包袱 : JDK 6 中字符串常量池在永久代 (PermGen) ,空间极小。大量调用 intern() 会导致 java.lang.OutOfMemoryError: PermGen space
  • 现状: JDK 7+ 常量池移入堆,风险降低,但仍需注意不要无节制将随机字符串 intern。
5. 静态字段 (Static Fields)
  • 原理 : Static 变量属于类,类由 ClassLoader 加载。只要 ClassLoader 不卸载,Static 变量引用的对象(如一个巨大的 List)就永远不会回收。
  • 修正 : 尽量少用静态集合缓存数据;如果必须用,需实现懒加载 (@Lazy) 或设置过期/清理机制。
6. 资源未关闭
  • 场景: JDBC Connection, ResultSet, FileInputStream 等。
  • 修正 : 使用 JDK 7 推出的 try-with-resources 语法自动关闭。

1.3 诊断工具箱

A. 基础命令 (Linux)
  • top: 查看系统整体负载。
    • 关注 RES (Resident Memory, 常驻内存): 进程实际使用的物理内存。
    • 关注 SHR (Shared Memory, 共享内存): 也就是类库等共享占用的。
    • 计算: Java 进程实际私有内存 ≈ RES - SHR。
B. VisualVM (可视化监控)
  • 功能: 监控 CPU、堆、类加载、线程数;生成 Heap Dump。

  • 插件 : Visual GC (必装),直观展示 Eden/Survivor/Old 区的动态变化。

  • 远程连接 JMX 配置 :

    bash 复制代码
    -Djava.rmi.server.hostname=服务器IP
    -Dcom.sun.management.jmxremote
    -Dcom.sun.management.jmxremote.port=9122
    -Dcom.sun.management.jmxremote.ssl=false
    -Dcom.sun.management.jmxremote.authenticate=false
C. Arthas (阿里神器)
  • 特点: 命令行交互,无需 GUI,支持集群管理。
  • Arthas Tunnel : 解决微服务集群管理问题。
    1. 部署 arthas-tunnel-server.jar
    2. 微服务配置 yml: arthas.agent.tunnel-server: ws://ip:port/ws
    3. 通过 Web 界面统一管理所有服务。
  • 核心命令 :
    • dashboard: 实时看板。
    • heapdump: 导出内存快照。
    • stack: 追踪方法调用栈(用于定位对象在哪创建)。
D. Prometheus + Grafana
  • 架构 : 微服务 (Micrometer) -> 暴露 /actuator/prometheus -> Prometheus 抓取 -> Grafana 展示。
  • 指标: JVM 内存 (Heap/Non-Heap)、GC 次数/耗时、线程数等。

1.4 内存泄漏实战排查流程 (MAT)

当发生 OOM 或发现内存异常增长时:

  1. 获取堆内存快照 (Heap Dump):

    • 被动触发 (推荐): 启动参数加上 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/dump.hprof
    • 主动获取: jmap -dump:live,format=b,file=dump.hprof [pid]arthas heapdump
    • 注意 : live 参数只导出存活对象,能减小文件体积,但分析内存泄漏时最好不加 live,以查看所有未回收对象。
  2. 使用 MAT (Memory Analyzer Tool) 分析:

    • 支配树 (Dominator Tree) : 最核心的功能。显示对象间的支配关系。
      • Shallow Heap (浅堆): 对象本身占用内存(如 int 占 4 字节,引用占 4 字节)。
      • Retained Heap (深堆/保留集) : 对象被回收后,能释放出的总内存 (包含它引用的所有对象)。找内存泄漏主要看这个值
    • 直方图 (Histogram): 查看哪类对象数量最多。
    • OQL: 类似 SQL 查询对象。

1.5 高频 OOM 案例复盘

案例一:分页查询杀手
  • 现象: 某次大促,查询文章列表接口直接 OOM。
  • 原因 : 前端未传 limit,后端默认查询所有。SQL 返回了 10000+ 条记录,且每条记录包含 content (大文本)。单次请求占用 100MB+ 内存,并发 10 次即崩溃。
  • 优化 :
    1. 防御性编程 : size = Math.min(size, 100),强制限制最大条数。
    2. SQL 瘦身 : 列表页接口禁止查询 content 大字段。
案例二:MyBatis 拼接风暴
  • 现象: 批量校验 ID 接口 OOM。
  • 原因 : MyBatis 的 <foreach> 标签在拼接 SQL 时,会在内存中为每个元素创建 AST 节点和 SQL 片段。当 List 包含 10 万个 ID 时,占用的内存远超 ID 本身。
  • 优化 :
    1. 限制批量操作的数量(如每次 1000 条)。
    2. 业务变更:将 ID 校验逻辑移至 Redis 缓存,而非查数据库。
案例三:导出 Excel 灾难
  • 现象: 导出 50 万条数据,服务器宕机。
  • 原因 : POI 的 XSSFWorkbook 会将整个 Excel DOM 结构加载到内存,极度消耗内存。
  • 优化路线 :
    1. SXSSFWorkbook: POI 提供的流式 API,利用磁盘临时文件缓存,降低内存。
    2. EasyExcel (最佳实践): 阿里开源,基于事件驱动(SAX),逐行读写,内存占用极低且恒定。

第二章:GC 调优 (Garbage Collection Tuning)

2.1 判断是否需要调优的"三把尺子"

  1. 吞吐量 (Throughput) : 也就是 CPU 干正事的时间比例。公式 = 用户代码执行时间 / (用户代码时间 + GC时间)
  2. 延迟 (Latency) : 最关键指标。即 GC 造成的 STW (Stop-The-World) 停顿时间。要求:比如所有请求 99% 在 200ms 内返回。
  3. 内存占用 (Footprint): 程序运行需要的堆大小。

2.2 GC 日志解读与分析

  • 参数设置 :
    • JDK 8: -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:./gc.log
    • JDK 9+: -Xlog:gc*:file=./gc.log
  • 日志关键项 :
    • GC/Full GC: 区分是轻量级还是全量回收。
    • PSYoungGen, ParOldGen, Metaspace: 各个区域回收前后的容量变化。
    • real: 实际停顿时间。
  • 分析神器 :
    • GCViewer: 离线 JAR 包,生成趋势图。
    • GCeasy: 网页版 AI 分析工具,能给出"GC 导致了多少暂停"、"内存泄漏概率"等结论。

2.3 典型 GC 模式

  1. 正常: 锯齿状 (Sawtooth)。分配 -> 飙升 -> GC -> 回落到底部。
  2. 缓存过多: 回落后水位线较高,老年代长期占用。
  3. 内存泄漏: 水位线呈阶梯状上升,GC 无法拉低水位,直至 OOM。
  4. 持续 Full GC :
    • 现象: CPU 100%,系统卡死,日志疯狂刷 Full GC。
    • 原因 A: 高并发下对象创建速度 > 回收速度。
    • 原因 B: 元空间 (Metaspace) 设置过小,加载类过多。
    • 原因 C: CMS 发生并发模式失败 (Concurrent Mode Failure),降级为 Serial Old。

2.4 调优实操手段 (按推荐顺序)

第一招:优化基础 JVM 参数
  • -Xmx-Xms : 必设!且设为相同值
    • 理由: 避免 JVM 在运行时向 OS 申请/释放内存导致的性能抖动;防止扩容时物理内存不足。
    • 计算: 物理内存的 1/2 到 2/3 (预留给 OS、堆外内存、Metaspace)。
  • -XX:MaxMetaspaceSize : 必设! (建议 256M - 512M)。
    • 理由: 默认是无限大,可能吃光物理内存导致操作系统杀进程。
  • -Xss (栈大小) : 建议缩小 (如 256k)。
    • 理由: 默认 1MB 太大,现代框架栈深度有限,缩小可支持更多线程。
  • 不建议动 : -Xmn (年轻代大小) 和 -XX:SurvivorRatio。G1 等收集器会自动调整,手动设置反而可能适得其反。
第二招:减少对象产生 (根本解决)
  • 使用对象池(慎用,仅针对重对象)。
  • 优化数据结构(如避免深层嵌套对象)。
  • 调整业务逻辑(如减少不必要的对象拷贝)。
第三招:更换垃圾回收器
  • JDK 8: 默认是 Parallel Scavenge + Parallel Old (PS+PO)。特点是吞吐量高,但 STW 长。
  • 策略 : 如果对延迟敏感,升级为 G1 (-XX:+UseG1GC)。
  • 实测数据: 在 200 并发下,PS+PO 最大响应时间 930ms,G1 降至 248ms,效果立竿见影。
第四招:微调回收器参数 (CMS 为例)
  • 场景: CMS 老年代回收太晚,导致 Full GC。
  • 参数 : -XX:CMSInitiatingOccupancyFraction=92 (默认 -1,动态计算)。
  • 配合 : -XX:+UseCMSInitiatingOccupancyOnly (强制生效)。
  • 效果: 提前触发 Old 区回收,避免堆满后的单线程 Full GC。

第三章:性能调优 (Performance Tuning)

3.1 CPU 100% 排查标准步骤

当监控报警 CPU 飙高时:

  1. top -c: 找到占用 CPU 最高的 Java 进程 ID (假设 PID=12345)。
  2. top -p 12345 -H : 查看该进程下的线程列表,找到 CPU 最高的线程 ID (假设 TID=12399)。
  3. printf '%x\n' 12399 : 将 TID 转为十六进制 (比如得到 306f)。
  4. jstack 12345 > thread.log: 导出进程的线程栈快照。
  5. 搜索 : 在 thread.log 中搜索 306f (十六进制 TID)。
  6. 分析 :
    • 如果状态是 RUNNABLE: 正在死循环、进行复杂计算(如 JSON 序列化大对象)。
    • 如果状态是 WAITING 但频繁出现: 可能是锁竞争激烈。
    • 注意: 频繁的 GC 线程也会导致 CPU 高,需结合 GC 日志排除。

3.2 接口响应慢排查 (Arthas 技巧)

当某个接口慢,但 CPU/内存 正常时:

  1. trace (追踪) :
    • 命令: trace com.example.Controller method '#cost > 1000' (只追踪耗时 > 1s 的调用)。
    • 效果: 打印出方法内部调用路径及每一步的耗时,一眼看出是 SQL 慢、Redis 慢还是代码慢。
  2. watch (观测) :
    • 命令: watch com.example.Controller method '{params, returnObj}' -x 2
    • 效果: 抓取实时的入参和返回值。常用于复现难以重现的 Bug。
  3. 火焰图 (Flame Graph) :
    • 命令: profiler start -> 压测 -> profiler stop --format html
    • 分析 : 找平顶 (Flat top) 最宽的绿色块。宽度代表 CPU 时间占比。
    • 案例 : 发现 ArrayList.add 占用极宽 -> 源码分析发现未指定初始容量,导致频繁 Arrays.copyOf 扩容 -> 优化:new ArrayList<>(size)

3.3 线程问题

  • 死锁 (Deadlock) :
    • 现象: 程序部分功能彻底卡死。
    • 发现: jstack 只要执行,最后会自动输出 Found one Java-level deadlock 及其涉及的线程和锁。
  • 线程耗尽 :
    • 现象: 无法建立新连接,报错 Unable to create new native thread
    • 原因: 线程数超过 OS 限制 (ulimit) 或 内存不足 (栈空间不够)。

3.4 JMH 基准测试 (Java Microbenchmark Harness)

为什么不能用 System.currentTimeMillis 测性能?

  • 原因:由于 JIT (即时编译) 的存在,代码会被优化(如死代码消除),且存在预热过程,简单循环测试极不准确。
  • JMH 最佳实践 :
    • @Benchmark: 标记测试方法。
    • @Warmup(iterations=5): 预热 5 轮,不计入成绩,让 JIT 完成优化。
    • @Fork(1): 独立进程运行,隔离干扰。
    • Blackhole : 如果测试方法的返回值没被使用,JIT 会删掉这段代码。必须用 Blackhole.consume() 吞掉结果,骗过 JIT。

3.5 综合代码优化案例

目标:优化"获取用户详情列表"接口。

  1. V1 (嵌套循环): 外层遍历详情 List,内层遍历 User List 匹配 ID。复杂度 O(N*M)。
  2. V2 (Map 加速) : 先将 User List 转为 HashMap<Id, User>,查找变为 O(1)。复杂度 O(N)。
  3. V3 (日期优化) : 使用线程安全的 LocalDateTime + DateTimeFormatter 替代性能差且非线程安全的 SimpleDateFormat
  4. V4 (并行流) : 使用 parallelStream() 利用多核 CPU 并行处理(需注意线程安全和公共 ForkJoinPool 的瓶颈)。
相关推荐
摇滚侠1 小时前
2025最新 SpringCloud 教程,Seata-基础-架构原理-整合 Seata 完成,笔记68,笔记69
笔记·spring cloud·架构
廋到被风吹走3 小时前
【Spring】Spring Data JPA Repository 自动实现机制深度解析
java·后端·spring
MX_93593 小时前
Spring中Bean的配置(一)
java·后端·spring
sg_knight7 小时前
Spring 框架中的 SseEmitter 使用详解
java·spring boot·后端·spring·spring cloud·sse·sseemitter
郑州光合科技余经理9 小时前
同城系统海外版:一站式多语种O2O系统源码
java·开发语言·git·mysql·uni-app·go·phpstorm
一只乔哇噻9 小时前
java后端工程师+AI大模型开发进修ing(研一版‖day60)
java·开发语言·人工智能·学习·语言模型
Dolphin_Home9 小时前
笔记:SpringBoot静态类调用Bean的2种方案(小白友好版)
java·spring boot·笔记
暗然而日章10 小时前
C++基础:Stanford CS106L学习笔记 4 容器(关联式容器)
c++·笔记·学习
MetaverseMan10 小时前
Java虚拟线程实战
java