前言
我是[提前退休的java猿],一名7年java开发经验的开发组长,分享工作中的各种问题!(抖音、公众号同号)
🔈PS: 最近多了一个新的写作方向,就是会把出版社寄来的【java深度调试技术】按照书的骨架提取其中干货进行丰盈写成博客,大家可以关注我的新专栏
内存泄露分析和堆内存设置
这一章节主要介绍了垃圾回收器的基础知识,然后再举出内存泄漏的案例让我们能够轻松的理解内存泄露的原理。 接着又介绍了内存泄露的症状以及定位和分析的手段,然后就是介绍了内存泄露的方法。
最后也是重点讲解了 java内存和垃圾回收的设置原则和关键参数。
内存泄露
首先肯定要明白的是什么是内存泄露?
在Java中,内存泄露指的是程序在运行过程中,已经不再需要的对象无法被垃圾回收器正常回收,导致这些对象持续占用内存空间的现象。随着时间推移,泄露的内存不断累积,最终可能导致内存耗尽和程序崩溃。
回收机制
Java的垃圾回收(GC)采用可达性分析算法,通过GC Roots对象作为起点,构建引用链。只有与GC Roots存在引用链的对象才会被认为是存活对象,其余对象则被标记为可回收。
GC Roots包括:
- 虚拟机栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
✅重要注意事项:将对象设置为null并不能避免内存泄露
这是一个常见的误解。将引用设置为null只是解除了一个引用关系,如果对象仍然被其他活跃引用持有,它依然无法被GC回收。
java
public class NullAssignmentMyth {
private static Map<String, Object> globalCache = new HashMap<>();
public void commonMistake() {
Object expensiveObject = new byte[10 * 1024 * 1024]; // 10MB对象
globalCache.put("key", expensiveObject);
// 错误认知:认为设置为null就能释放内存
expensiveObject = null;
// 实际上,对象仍然被globalCache引用,无法被GC回收
}
public void correctApproach() {
Object expensiveObject = new byte[10 * 1024 * 1024];
globalCache.put("key", expensiveObject);
// 正确的释放方式
globalCache.remove("key"); // 先从集合中移除引用
expensiveObject = null; // 再设置为null
}
}
如何定位内存泄露
首先要知道内存泄露的特征有哪些,在发生崩溃之前提前定位原因。
内存泄露的特征
系统越来越慢,并伴随着内存的使用率过高,因为内存不足 GC频率增高(Full GC频率增加,回收前后可能内存五无明显变化),GC操作是CPU密集型操作,所以存在内存泄露的场合最后必然CPU使用率100%。或者出现OutOfMemory了。简单概括就下面几点:
- Minor GC/Full GC越来越频繁
- 每次GC后堆内存使用率不下降或持续上升
- GC暂停时间逐渐变长
- 老年代使用率持续增长
- 最后就系统崩溃OOM
定位内存泄漏
通过内存泄漏的特征也可以看到,GC回收的频率和效果 是内存泄漏的重要特征,也是关乎系统性能的重要指标(如果GC频率高,效果差 对系统性能的影响是非常大的,特别是FULL GC)。
不管是定位内存泄漏问题,还是保障系统的稳定运行这两个步都是必不可少的。第一步 GC日志的打印及分析,第二步 导出堆转储文件。
打印GC日志:命令各垃圾回收均有点区别,jdk 17 G1 GC日志打印 -Xlog:gc*,g1*:file=/path/to/gc.log:time,level,tags,输出结果如下:
js
[2025-10-25T21:35:17.927+0800][info] GC(3838) Pause Young (Normal) (G1 Evacuation Pause) 4043M->4043M(4044M) 0.445ms
//-- FULL GC 回收前4043 回收后 4043 总内存4044M ,回收时间为12.924ms
[2025-10-25T21:35:17.940+0800][info] GC(3839) Pause Full (G1 Compaction Pause) 4043M->4043M(4044M) 12.924ms
[2025-10-25T21:35:17.956+0800][info] GC(3840) Pause Full (G1 Compaction Pause) 4043M->4043M(4044M) 15.300ms
[2025-10-25T21:35:17.957+0800][info] GC(3841) Pause Young (Concurrent Start) (G1 Evacuation Pause) 4043M->4043M(4044M) 0.647ms
[2025-10-25T21:35:17.957+0800][info] GC(3843) Concurrent Mark Cycle
[2025-10-25T21:35:17.974+0800][info] GC(3842) Pause Full (G1 Compaction Pause) 4043M->4043M(4044M) 16.871ms
[2025-10-25T21:35:17.991+0800][info] GC(3844) Pause Full (G1 Compaction Pause) 4043M->4043M(4044M) 17.143ms
[2025-10-25T21:35:17.991+0800][info] GC(3843) Concurrent Mark Cycle 34.722ms
通过上面的GC日志发现,FULL GC 前后的内存空间没有发生变化,因此多半是内存泄露。
导出堆转储文件,用工具打开就行了,下图也是之前解决OOM时候记录JProfiler打开的图片,很清晰的能看到各对象的大小,以及对象的GC ROOT 我们去排查各大对象对应的代码就行了:

分享一个OOM 和 FULL GC 的案例:
总结
java 深度调试技术 第3章 【内存泄露分析和堆内存设置】讲了还是很多基础的理论,比如GC回收的机制,详细解释了内存泄露的原理,内存和垃圾回收设置等。本篇文章只是把其中最核心的知识提取出来。比如内存泄露的特征,GC日志的打印分析,堆文件的打印分析这些基础掌握之后就在实际开发中解决问题了。但是对于java堆内存模型,垃圾回收算法这些本篇文章就没过多的描述了(推荐深入理解java虚拟机)。