文章目录
- 如何通过JProfile排查OOM或内存泄漏问题
- 1、启动工具观测程序执行状态
- 2、使用默认设置采样
- 3、查看memory,Run GC无效
- 4、查看 Live Memory发现两个byte大数组存在
- 5、通过快照查看堆中的内存使用情况
- 6、找到Full GC无法清除的对象
- 通过大对象列表定位内存泄漏问题
- 怎么解决内存泄漏
- 内存溢出和内存泄漏
- 内存溢出
- 内存泄漏
如何通过JProfile排查OOM或内存泄漏问题
先编写一个代码模拟内存泄漏:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author XiuJun
* @date 2024/9/7
*/
public class OOMDemo {
// 定义一个 ThreadLocal 变量,存储一个大对象(如 byte 数组)
private static ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 2; i++) {
executorService.submit(() -> {
try {
// 为 ThreadLocal 设置一个大对象
threadLocal.set(new byte[1024 * 1024 * 100]); // 10 MB 的数组
System.out.println(Thread.currentThread().getName() + " 设置了大对象");
// 模拟执行任务
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 如果不调用 remove,可能会导致内存泄漏
// threadLocal.remove();
}
});
}
for (int i = 0; i < 3; i++) {
executorService.submit(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 线程复用了");
// 模拟执行任务
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 如果不调用 remove,可能会导致内存泄漏
// threadLocal.remove();
}
});
}
// 关闭线程池
// executorService.shutdown();
}
}
这里通过线程池,先通过两个线程执行两个任务,每个线程会有一个100MB的ThreadLocal变量。执行完之后,没有通过remove方法移除ThreadLocal变量,之后任务执行完,线程空闲然后被其他任务复用了线程。通过JProfile工具查看就可以发现两个byte数组还存在,并且无法被GC回收,因为他们还被Entry强引用,无法被标记为可回收。最后就会发生内存泄漏。
1、启动工具观测程序执行状态
2、使用默认设置采样
3、查看memory,Run GC无效
4、查看 Live Memory发现两个byte大数组存在
5、通过快照查看堆中的内存使用情况
6、找到Full GC无法清除的对象
通过大对象列表定位内存泄漏问题
怎么解决内存泄漏
到此,就可以发现是两个线程的ThreadLocal导致的内存泄漏问题。加上remove操作后,再次手动GC就可以发现两个byte数组会被清理掉,内存泄漏问题解决。
内存溢出和内存泄漏
解决OOM问题的重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。内存泄漏是存在着不健康的对象占用了有用的空间导致最终发生OOM,而内存溢出则是因为需要使用的对象所需内存大于堆内存,需要优化程序或者加大堆空间。
-
内存泄漏就是有大量的引用指向某些对象,但是这些对象以后不会使用了,但是因为它们还和 GC ROOT 有关联,所以导致以后这些对象也不会被回收,这就是内存泄漏的问题。
-
垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序的结束,被保留的空间无法被其它对象使用,甚至可能导致内存溢出。
-
如果是内存泄漏,可进一步通过工具(JProfile)查看泄漏对象到 GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与 GCRoots 相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及 GCRoots 引用链的信息,就可以比较准确地定位出泄漏代码的位置。
-
如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx 与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
内存溢出
javadoc 中对 outofMemoryError 的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。
首先说没有空闲内存的情况:说明 Java 虚拟机的堆内存不够。原因有二:
- Java 虚拟机的堆内存设置不够。
- 比如:可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定 JVM 堆大小或者指定数值偏小。我们可以通过参数-Xms 、-Xmx 来调整。
- 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
- 对于老版本的 oracle JDK,因为永久代的大小是有限的,并且 JVM 对永久代垃圾回收(如,常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现 OutOfMemoryError 也非常多见,尤其是在运行时存在大量动态类型生成的场合;类似 intern 字符串缓存占用太多空间,也会导致 OOM 问题。对应的异常信息,会标记出来和永久代相关:“java.lang.OutOfMemoryError:PermGen space"。
- 随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的OOM 有所改观,出现 OOM,异常信息则变成了:“java.lang.OutofMemoryError:Metaspace"。直接内存不足,也会导致 OOM。
在抛出 OutofMemoryError 之前,通常垃圾收集器会被触发,尽其所能去清理出空间。
当然,也不是在任何情况下垃圾收集器都会被触发的。比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM 可以判断出垃圾收集并不能解决这个问题,所以直接抛出 OutofMemoryError。
内存泄漏
严格来说,只有对象不会再被程序用到了,但是 GC 又不能回收他们的情况,才叫内存泄漏。但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致 00M,也可以叫做宽泛意义上的“内存泄漏”。
尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现 outofMemory 异常,导致程序崩溃。
实际应用中,我们可以通过定期去进行GC,然后查看GC后的内存是否在稳步增大,这个现象很有可能就是由于存在某些对象无法被回收导致的内存泄漏问题,需要进行堆转储后查看是哪些对象,进行下一步的分析,判断是不是需要使用的对象。