内存 Profiling
内存问题分为两大类:内存泄漏和内存浪费。前者是对象无法被回收,后者是对象分配了超出需要的内存。内存 Profiling 是定位这两类问题的关键手段。
GC Roots 追踪
理解内存泄漏的关键是理解 GC Roots——那些永远不会被垃圾回收的对象集合:
GC Roots 包括:
- 活跃线程的局部变量
- 类的静态字段
- JNI 引用
- JVM 内部数据结构
所有从 GC Roots 出发可到达的对象都是「存活对象」,不可到达的对象才会被回收。
内存 Profiling 类型
分配剖析(Allocation Profiling)
分析"谁分配了最多的对象"。
分配剖析回答的问题:
- 哪个类分配了最多对象?
- 哪个方法触发了这些分配?
- 分配速率是多少?
存活剖析(Retention Profiling)
分析"谁持有最长引用"。
存活剖析回答的问题:
- 为什么这个对象没有被回收?
- 引用链是从 GC Root 到泄漏对象的?
- 哪个 GC Root 最需要关注?
内存泄漏 vs 内存浪费
async-profiler 内存剖析
分配剖析
存活剖析
使用 -r 选项采样存活对象:
JFR 内存剖析
配置
关键事件
JFR 收集的内存相关事件:
常见内存泄漏模式
模式一:静态集合未清理
典型泄漏
解决方案:使用 WeakHashMap 或添加过期机制。
模式二:监听器未注销
典型泄漏
解决方案:实现注销方法。
模式三:ThreadLocal 未清理
典型泄漏
解决方案:使用 try-finally 确保清理。
内存优化建议
减少对象分配
使用基本类型
本章小结
内存 Profiling 的两个方向:
- 分配剖析:找出"谁分配了最多对象"
- 存活剖析:找出"谁持有最长引用"
常见泄漏模式:静态集合、监听器、ThreadLocal。
实战经验:MAT 的「支配树(Dominator Tree)」和「GC Roots 最短路径」功能是定位泄漏的利器。
延伸思考
为什么 String 在 Java 中是内存浪费的大户?
因为:
- 字符串字面量容易被重复创建
- 字符串拼接会创建大量中间对象
- 集合类经常以 String 作为 key
优化建议:
- 使用
String.intern()复用字符串 - 使用
StringBuilder代替+拼接 - 考虑使用自定义对象替代 String key