写点什么

工作中常见的 OOM?你了解 JVM 调优吗?

  • 2025-07-18
    福建
  • 本文字数:4904 字

    阅读完需:约 16 分钟

工作中常见的 6 种 OOM 问题


堆内存 OOM

堆内存 OOM 是最常见的 OOM 了。

出现堆内存 OOM 问题的异常信息如下:

java.lang.OutOfMemoryError: Java heap space
复制代码


此 OOM 是由于 JVM 中 heap 的最大值,已经不能满足需求了。

举个例子:

@Test public void test01() { 	List list = Lists.newArrayList(); 	while (true) { 		list.add(new OOMTests()); 	} }
复制代码


这里创建了一个 list 集合,在一个死循环中不停往里面添加对象。

执行结果:



出现了 java.lang.OutOfMemoryError: Java heap space 的堆内存溢出。

很多时候,excel 一次导出大量的数据,获取在程序中一次性查询的数据太多,都可能会出现这种 OOM 问题。

我们在日常工作中一定要避免这种情况。


栈内存 OOM


有时候,我们的业务系统创建了太多的线程,可能会导致栈内存 OOM。

出现堆内存 OOM 问题的异常信息如下:

java.lang.OutOfMemoryError: unable to create new native thread
复制代码


举个例子

public class StackOOMTest {     public static void main(String[] args) {         while (true) {         	new Thread().start();         }     }}
复制代码


使用一个死循环不停创建线程,导致系统产生了大量的线程。

如果实际工作中,出现这个问题,一般是由于创建的线程太多,或者设置的单个线程占用内存空间太大导致的。

建议在日常工作中,多用线程池,少自己创建线程,防止出现这个 OOM。


栈内存溢出


我们在业务代码中可能会经常写一些 递归调用,如果递归的深度超过了 JVM 允许的最大深度,可能会出现栈内存溢出问 题。

出现栈内存溢出问题的异常信息如下:

java.lang.StackOverflowError
复制代码


举个例子

@Testpublic void test03() { 	recursiveMethod();}public static void recursiveMethod() {     // 递归调用自身     recursiveMethod();}
复制代码



出现了 java.lang.StackOverflowError 栈溢出的错误。

我们在写递归代码时,一定要考虑递归深度。即使是使用 parentId 一层层往上找的逻辑,也最好加一个参数控制递归 深度。防止因为数据问题导致无限递归的情况,比如:id 和 parentId 的值相等。


直接内存 OOM


直接内存不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域。

它来源于 NIO ,通过存在堆中的 DirectByteBuffer 操作 Native 内存,是属于堆外内存 ,可以直接向系统申请的内存空间。 出现直接内存 OOM 问题时异常信息如下:

java.lang.OutOfMemoryError: Direct buffer memory
复制代码


例如:

private static final int BUFFER = 1024 * 1024 * 20;@Testpublic void test04() {     ArrayList<ByteBuffer> list = new ArrayList<>();     int count = 0;     try {         while (true) {             ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);             list.add(byteBuffer);             count++;             try {             	Thread.sleep(100);             } catch (InterruptedException e) {             	e.printStackTrace();             }         }     } finally {     	System.out.println(count);     }}
复制代码


会看到报出来 java.lang.OutOfMemoryError: Direct buffer memory 直接内存空间不足的异常。


GC OOM

GC OOM 是由于 JVM 在 GC 时,对象过多,导致内存溢出,建 议调整 GC 的策略。 出现 GC OOM 问题时异常信息如下:

java.lang.OutOfMemoryError: GC overhead limit exceeded
复制代码


为了方便测试,我先将 idea 中的最大和最小堆大小都设置成 10M,例如下面这个例子:

public class GCOverheadOOM {     public static void main(String[] args) {         ExecutorService executor = Executors.newFixedThreadPool(5);         for (int i = 0; i < Integer.MAX_VALUE; i++) {             executor.execute(() -> {                 try {                    Thread.sleep(10000);                 } catch (InterruptedException e) {                 }             });         }     }}
复制代码


出现这个问题是由于 JVM 在 GC 的时候,对象太多,就会报这 个错误。 我们需要改变 GC 的策略。 在老代 80%时就是开始 GC,并且将-XX:SurvivorRatio(- XX:SurvivorRatio=8)和-XX:NewRatio(- XX:NewRatio=4)设置的更合理。


元空间 OOM

JDK8 之后使用 Metaspace 来代替 永久代 ,Metaspace 是方 法区在 HotSpot 中的实现。

Metaspace 不在虚拟机内存中,而是使用本地内存也就是在 JDK8 中的 ClassMetadata ,被存储在叫做 Metaspace 的 native memory。

出现元空间 OOM 问题时异常信息如下:

java.lang.OutOfMemoryError: Metaspace
复制代码


为了方便测试,我们修改一下 idea 中的 JVM 参数,增加下面的配置:

-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
复制代码


指定了元空间和最大元空间都是 10M。 接下来,看看下面这个例子:

public class MetaspaceOOMTest {     static class OOM {     }     public static void main(String[] args) {         int i = 0;         try {             while (true) {                 i++;                 Enhancer enhancer = new Enhancer();                 enhancer.setSuperclass(OOM.class);                 enhancer.setUseCache(false);                 enhancer.setCallback(new MethodInterceptor() {                     @Override                     public Object intercept(Object o, Method method, Object[]                     	return methodProxy.invokeSuper(o, args);                     }                 });                 enhancer.create();             }         } catch (Throwable e) {         	e.printStackTrace();         }     }}
复制代码


程序最后会报 java.lang.OutOfMemoryError: Metaspace 的 元空间 OOM。 这个问题一般是由于加载到内存中的类太多,或者类的体积太 大导致的。


OOM 一定会导致 JVM 退出吗


在 Java 中,发生了 OutOfMemoryError(OOM)不一定会导致整个 JVM 退出。是否退出取决于发生 OOM 错误的线程和错误处理逻辑。这是一个复杂的问题,具体行为会因应用程序实现方式、错误发生的情境以及错误处理策略而异。

  • 主线程中未处理的 OOM: 如果在主线程中发生 OOM 且没有被捕获,JVM 通常会终止程序并退出。这是因为 JVM 中没有其他存活的非守护线程来保持程序运行。

  • 子线程中未处理的 OOM: 在非主线程中,如果 OOM 发生且未被捕获,该线程会停止执行。但如果其他非守护线程仍在运行,JVM 不会退出。

  • 捕获并处理 OOM: 如果在代码中捕获并正确处理了 OOM 错误,JVM 则可以继续执行其余的程序代码。合适的错误处理可能包括释放内存资源或提示用户进行适当的操作。


注意:

  • 不建议频繁捕获 OOM 并继续执行程序,因为这样可能表明程序有严重的内存管理问题,应尽量优化内存使用。

  • 在关键路径中发生 OOM 时,通常应记录日志并考虑安全停机,因为无法保证系统在内存压力下的正确性。


垃圾回收调优的主要目标是什么?


分别是最短暂停时间和高吞吐量

  1. 最短暂停时间:垃圾回收调优的首要目标是减少应用程序的停顿时间,确保在垃圾回收过程中尽量保持应用的响应能力,特别是对于实时或高并发应用

  2. 高吞吐量:第二个目标是提高应用的吞吐量,即在单位时间内完成更多的业务处理,通过合理的 GC 策略和配置,减少 GC 的频率和时间,从而提升整体性


针对最短暂停时间和高吞吐举个例子:

  • 方案一:每次 GC 停顿 100 ms,每秒停顿 5 次。

  • 方案二:每次 GC 停顿 200 ms,每秒停顿 2 次。

两个方案相对而言第一个时延低,第二个吞吐高,基本上两者不可兼得。所以调优时候需要明确应用的目标。


如何对 Java 的垃圾回收进行调优?


GC 调优这种问题肯定是具体场景具体分析,但是在面试中就不要讲太细,大方向说清楚就行,不需要涉及具体的垃圾收集器比如 CMS 调什么参数,G1 调什么参数之类的。


GC 调优的核心思路就是尽可能的使对象在年轻代被回收,减少对象进入老年代。

具体调优还是得看场景根据 GC 日志具体分析,常见的需要关注的指标是 Young GC 和 Ful GC 触发频率、原因、晋升的速率、老年代内存占用量等等。比加发现频繁产生 FullGC,分析日志之后发现没有内存泄漏,只是 Young GC 之后会有大量的对象进入老年代,然后最终触发 FullGC,所以就能得知是 Survivor 空间设置太小,导致对象过早进入老年代,因此调大 Survivor。或者是晋升年龄设置的太小,也有可能分析日志之后发现是内存泄漏、或者有第三方类库调用了 System.gc 等等。反正具体场景具体分析,核心思想就是尽量在新生代把对象给回收了,基本上这样说就行了,然后就等着面试官延伸了


常用的 JVM 配置参数有哪些?


记住前两个,其它的使用时再查就行

JVM(Java 虚拟机)的启动参数用于配置和调整 Java 应用程序的运行时行为。以下是一些常用的 JVM 启动参数:

  • -Xmx:指定 Java 堆内存的最大限制。例如,-Xmx512m 表示最大堆内存为 512 兆字节。

  • -Xms:指定 Java 堆内存的初始大小。例如,-Xms256m 表示初始堆内存为 256 兆字节。

  • -Xss:指定每个线程的堆栈大小。例如,-Xss256k 表示每个线程的堆栈大小为 256 千字节。

  • -XX:MaxPermSize(对于 Java 7 及之前的版本)或 -XX:MaxMetaspaceSize(对于 Java 8 及以后的版本):指定永久代(Java 7 及之前)或元空间(Java 8 及以后)的最大大小。

  • -XX:PermSize(对于 Java 7 及之前的版本)或 -XX:MetaspaceSize(对于 Java 8 及以后的版本):指定永久代(Java 7 及之前)或元空间(Java 8 及以后)的初始大小。

  • -Xmn:指定年轻代的大小。例如,-Xmn256m 表示年轻代大小为 256 兆字节。

  • -XX:SurvivorRatio:指定年轻代中 Eden 区与 Survivor 区的大小比例。例如,-XX:SurvivorRatio=8 表示 Eden 区与每个 Survivor 区的大小比例为 8:1。

  • -XX:NewRatio:指定年轻代与老年代的大小比例。例如,-XX:NewRatio=2 表示年轻代和老年代的比例为 1:2。

  • -XX:MaxGCPauseMillis:设置垃圾回收的最大暂停时间目标。例如,-XX:MaxGCPauseMillis=100 表示垃圾回收的最大暂停时间目标为 100 毫秒。

  • -XX:ParallelGCThreads:指定并行垃圾回收线程的数量。例如,-XX:ParallelGCThreads=4 表示使用 4 个线程进行并行垃圾回收。

  • -XX:+UseConcMarkSweepGC:启用并发标记清除垃圾回收器。

  • -XX:+UseG1GC:启用 G1(Garbage First)垃圾回收器。

  • -Dproperty=value:设置 Java 系统属性,可以在应用程序中使用 System.getProperty("property") 来获取这些属性的值。

这些是一些常见的 JVM 启动参数,可以根据应用程序的需求和性能调优的目标进行调整。JVM 启动参数的使用可以显著影响应用程序的性能和行为,因此在设置这些参数时需要谨慎。同时,JVM 支持的启动参数因不同的 JVM 版本和供应商而有所不同,建议查阅相关文档以获取更详细的信息。


你常用哪些工具来分析 JVM 性能?


  • jmap:用于生成堆转储的命令行工具,可以用于分析 JVM 内存使用情况,尤其是内存泄漏问题

  • jstack:用于生成线程转储的命令行工具,可以用于分析线程状态,排查死锁等问题

  • jistat:用于监控 JVM 统计信息的命令行工具,提供了实时的性能数据,如类加载、垃圾回收、编译器等信息

  • MAT:用于分析堆转储文件的工具,可以帮助识别内存泄漏和优化内存使用

  • jconsole:可以监控 JVM 的内存使用、垃圾回收、线程、类加载等信息

  • VisualVM:可实时显示 JVM 的内存使用、垃圾回收、类加载等信息,也可以分析 Heap Dump 等.

  • Arthas:一个强大的 Java 诊断工具,提供了实时监控和分析功能。通过命令行界面,可以查看 的状态、监控方法调用、追踪 SQL 查询、分析性能瓶颈等。


如何在 Java 中进行内存泄漏分析?


先确认是否真的发生了内存泄漏,即观察内存使用情况。利用 jstat 命令(jstat -gc <pid> <interal in ms>)来观察 gc 概要信息,如果发现,GC 后内存并没有明显的减少目还是持续增加持续触发 gc,那说明内存泄漏的概率很大。


此时可以利用 jmap(jmap -dump:format=b,fi1e=heapdump.hprof <pid> )生成 heap dump,然后将其导入 Ecdipse MAT 或者 VsuaVM 工具内进行分析,通过大量内存的占用可以找到对应的对象。

通过对象找到对应的代码分析,确认是否可能存在内存泄漏的场景,最终修复代码,解决内存泄漏的问题。


文章转载自:程序员Seven

原文链接:https://www.cnblogs.com/seven97-top/p/18980820

体验地址:http://www.jnpfsoft.com/?from=001YH

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
工作中常见的OOM?你了解JVM调优吗?_JVM_不在线第一只蜗牛_InfoQ写作社区