2025-06-27
实战设计
0

目录

内存泄漏
内存泄漏产生的原因分析
代码问题导致资源没释放
长生命周期的对象持有短生命周期对象的引用
静态集合类使用不恰当导致内存泄漏
资源未关闭造成内存泄漏
内存泄漏的排查思路

在工作过程中,各种性能问题的出现总是让人猝不及防,出现问题不可怕,怕的对于自身解决问题的能力不到位。

其实性能问题总结来说无非两种,一种cpu飙升问题,一种是内存问题。今天就来聊下内存问题中内存泄漏的排查思路。

内存泄漏

在工作中一般是如何定位到当前问题是内存泄漏问题呢?

我们首先需要了解什么是内存泄漏,内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。

造成的现象就是随着垃圾回收器活动的增加以及内存占用的不断增加,程序性能会逐渐表现出来下降,极端情况下,会引发 java.lang.OutOfMemoryError 导致程序崩溃。

内存泄漏的本质 就是我们常用的虚拟机是使用 可达性分析 来判断对象是否可回收,其实现实判断一个对象是否还被引用,如果没有引用则回收。在开发的过程中,由于代码的编写问题就会出现很多种内存泄漏问题,让GC垃圾回收器误以为此对象还在引用中,无法回收,造成内存泄漏。

内存泄漏和内存溢出辨析

  • 内存溢出:实实在在的内存空间不足导致;
  • 内存泄漏:该释放的对象没有释放,常见于使用容器保存元素的情况下。

内存泄漏产生的原因分析

其实产生的原因说到底还是代码编写的问题,总会有各种各样的情况导致内存泄漏,大多数是以下几种情况

代码问题导致资源没释放

ThreadLocal 是 Java 中的一个线程局部变量工具,它为每个使用该变量的线程都提供一个独立的变量副本,每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。不过,若使用不当,ThreadLocal 可能会造成内存泄漏。 ThreadLocal 的底层实现是通过 ThreadLocalMap,ThreadLocal 作为键,存储的值作为值。当线程执行完任务后,如果没有调用 remove() 方法,ThreadLocalMap 中仍然保留着对 BigObject 的引用。由于线程池中的线程是复用的,这些 BigObject 对象不会被垃圾回收,从而导致内存泄漏。

java
// 自定义大对象类 class BigObject { private final byte[] data = new byte[1024 * 1024]; // 1MB 的数据 public BigObject() { System.out.println("BigObject created"); } } public class ThreadLocalMemoryLeakExample { // 创建一个 ThreadLocal 变量 private static final ThreadLocal<BigObject> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { // 创建一个固定大小的线程池 ExecutorService executorService = Executors.newFixedThreadPool(2); for (int i = 0; i < 10; i++) { executorService.submit(() -> { // 为当前线程设置 ThreadLocal 变量 threadLocal.set(new BigObject()); // 模拟一些业务操作 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // 这里没有调用 threadLocal.remove() 方法 System.out.println("Task completed"); }); } // 关闭线程池 executorService.shutdown(); } }

长生命周期的对象持有短生命周期对象的引用

Lambda 表达式也会捕获外部变量,这类似于匿名内部类持有外部类的引用。若 Lambda 表达式的生命周期较长,就可能造成内存泄漏。

java
public class LambdaLeakExample { public void startScheduledTask() { ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); // Lambda 表达式 executor.scheduleAtFixedRate(() -> { System.out.println("Value from outer: " + 20); }, 0, 1, TimeUnit.SECONDS); } public static void main(String[] args) { LambdaLeakExample example = new LambdaLeakExample(); example.startScheduledTask(); // 即使 example 不再被使用,由于 Lambda 表达式持有其引用,example 无法被回收 example = null; } }

静态集合类使用不恰当导致内存泄漏

静态集合类(如static List、static Map)的生命周期与应用程序一致。如果在集合中添加了对象,但未及时移除不再使用的对象,这些对象将一直被引用,无法被回收。

java
private static List<Object> staticList = new ArrayList<>(); staticList.add(new Object()); // 对象未被移除,长期驻留内存

资源未关闭造成内存泄漏

平常使用的数据库连接、网络连接和文件IO连接等,只有连接被关闭后,垃圾回收器才会回收对应的对象。

java
FileInputStream in = new FileInputStream("file.txt"); // 未调用 in.close(),资源未释放

从代码上解决内存泄漏的几种方式

  1. 尽量减少使用静态变量,或者使用完及时清空或者赋值为 null。

  2. 明确内存对象的有效作用域,尽量缩小对象的作用域,能用局部变量处理的不用成员变量,因为局部变量弹栈会自动回收;

  3. 减少长生命周期的对象持有短生命周期的引用;

  4. 各种连接(数据库连接,网络连接,文件IO连接)操作,务必显示调用close关闭。

  5. 对于不需要使用的对象手动设置null值,不管GC何时会开始清理,我们都应及时的将无用的对象标记为可被清理的对象;

内存泄漏的排查思路

这个时候可以根据生产问题的情况进行定位了,首先开始确认是否是内存泄漏问题,它主要表现就是内存不足,接下来开始排查

1、查询内存情况

主要分析重点:查找重复出现的自定义类,大数量对象,分析内存空间使用率

sh
# 堆内存信息 jcmd <pid> GC.heap_info

2、分析GC运行情况

主要分析重点:查看Old区、Metaspace使用率,查看 Full GC 次数

sh
# 每秒输出GC数据 jstat -gcutil <pid> 1000 # 每2秒打印GC原因 jstat -gccause <pid> 2000 # 强制触发 GC jcmd <pid> GC.run
  • S0 — Heap上的 Survivor space 0 区已使用空间的百分比
  • S1 — Heap上的 Survivor space 1 区已使用空间的百分比
  • E — Heap上的 Eden space 区已使用空间的百分比
  • O — Heap上的 Old space 区已使用空间的百分比
  • P — Perm space 区已使用空间的百分比
  • YGC — 从应用程序启动到采样时发生 Young GC 的次数
  • YGCT– 从应用程序启动到采样时 Young GC 所用的时间(单位秒)
  • FGC — 从应用程序启动到采样时发生 Full GC 的次数
  • FGCT– 从应用程序启动到采样时 Full GC 所用的时间(单位秒)
  • GCT — 从应用程序启动到采样时用于垃圾回收的总时间(单位秒)
  • LGCC - 进行GC的原因(低版本jdk可能没有这一列)

从这里观察gc是否异常,也可以根据这个进行jvm内存分配调优,来提高性能降低gc对性能的损耗

3、查看内存占用情况

把JVM中的对象全部打印出来, 但是这样太多了,那么我们选择前20的对象展示出来,

sh
# 堆内存直方图,查看前20条 jcmd <pid> GC.class_histogram | head -20 # 输出到文件 jcmd <pid> GC.class_histogram > histo.txt

image.png

根据以上对象,确认下版本新增代码的改动,尽快从代码上找出问题。在这一步,是快速定位问题的关键,如果根据对象分析出问题所在,那到此也就结果了;如果无法分析出问题,那我们还得继续排查

非内存泄漏问题去调优解决

调优过程:首先确认逻辑问题,查看内存中对象的数量和大小,判断是否在合理的范围,如果在合理的范围内,增大内存配置,调整内存比例就可以了。

4、导出Dump日志进行分析(优先命令分析,分析不出再导出Dump日志)

在此之前,优化开启以下参数

sh
-XX:+HeapDumpOnOutOfMemoryError -XX:OnError -XX:+ShowMessageBoxOnError

或者使用 jinfo 调整参数,添加 HeapDumpOnOutOfMemoryError

sh
jinfo -flag +HeapDumpOnOutOfMemoryError 9356

好,这个时候重新跑,我们就能够得到 Dump 日志

JDK提供jhat(JVM Heap Analysis Tool)命令与jmap搭配使用,但是jmap会响应服务性能,此处采用 jcmd 来生成堆转储快照。

sql
jcmd <pid> GC.heap_dump ./dump.hprof

5、内存分析工具 - VisualVM 分析Dump

用于生成堆转储快照(一般称为heapdump或dump文件),接下来我们使用 VisualVM 进行分析

image.png

image.png

image.png

image.png

image.png

VisualVm 属于比较寒酸的工具,基本上跟jmap之类的命令没多少区别,它只是可以事后看,通过dump信息来看,里面没有多少可以做分析的功能。

我们一般都是采用 MAT 进行分析,对于MAT如何分析,请看 【线上性能问题 - 玩转MAT分析内存泄漏】。

本文作者:柳始恭

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!