虚拟机系列:内存溢出OOM以及解决思路

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

内存溢出(OutOfMemoryError,简称OOM)是让程序员头疼的问题,出现这种问题一般是内存空间要被用完了,没有足够的空间供程序使用。而在Java程序中,出现内存溢出的原因也有很多,常见的有堆内存溢出,直接内存溢出,永久区/元空间溢出。

堆溢出

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
复制代码

堆溢出这种是最常见的一种,在Java中堆是重要的一个空间,Java的大量对象都是直接在堆上分配的(参考内存分配)。当大量对象占据了堆空间而且都是强引用,使之始终无法被回收,当所有对象大小之和大于参数-Xmx指定的值时,就会出现溢出了。

导致溢出的原因可能有很多,这里列举下常见的原因:

  • 服务工程中的代码类太多,堆空间不够用,可能在启动的时候就会出现堆溢出的错误。
  • 代码中存在循环或者死循环,产生过多的实体对象。
  • 数据库查询的时候一次查询大量数据。
  • 服务启动的参数-Xmx设置过小,也就是第一点。

例如我们不断往ArrayList中加入对象且无法回收导致出现堆溢出

/**
* 启动参数限制最大堆和最小堆:-Xms10m -Xmx10m
*/
public class Test2 {
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();
        for (int i=0;i<10;i++){
            list.add(new byte[1024*1024]);
        }
    }
}
复制代码

启动出现错误:

com.ajisun.coding.ajisunmybatis.Test2
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  at com.ajisun.coding.ajisunmybatis.Test2.main(Test2.java:23)
复制代码

错误中的 Java heap space 表明了是堆内存的溢出。

如何处理

  • 首选检查代码是否存在循环或者死循环,是否能够不断的创建对象。
  • 查看启动参数-Xmx-Xms 设置的堆内存是否过小,不足以加载服务中的所有类,可以适当增加。
  • 检查代码中是否存在数据库查询,没有分页一次性返回大量数据。
  • 还可以通过MAT或者VisualVM工具分析,找到占用大量堆空间的对象,然后做出合理优化。

直接内存溢出

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
复制代码

这个问题遇到的一般比较少,直接内存不是运行时数据区的一部分。

Java中NIO(New IO)是支持直接使用直接内存的,可以直接获取一块堆外空间使用,而这块空间是直接向操作系统申请的。直接内存的申请速度一般比堆内存慢,但是其访问速度要快于堆内存,所以如果存在可复用且经常被访问的空间,使用直接内存可以提高系统的性能。但是直接内存没有被Java完全托管,使用不当容易出现溢出的问题。

Java中的DirectByteBuffer就是直接内存申请的,使用unsafe的native方法allocateMemory

配置启动参数-XX:MaxDirectMemorySize设置直接内存的最大值,如果不配置此参数则默认最大可用直接内存是-Xmx的值

/**
* 启动参数:-XX:MaxDirectMemorySize=5m
*/
public class Test2 {
    public static void main(String[] args) {
    ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*6);
    }
}
复制代码

运行出现错误Direct buffer memory

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
  at java.nio.Bits.reserveMemory(Bits.java:694)
  at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
  at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
复制代码

如何处理:

  • 检查程序中使用直接内存的代码是否恰当。
  • 检查参数-Xmx和-XX:MaxDirectMemorySize 的大小是否合理,可以根据实际情况调整其大小。

线程过多导致OOM

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
复制代码

这个错误是程序中创建的线程过多,而线程需要的空间却不够了。

我们运行程序创建的线程都是需要一定内存的,而机器的内存大小是一定的,当线程的数量过多的时候,就会导致OOM。

如下用死循环不断的创建新的线程。

public class Test2 {
    public static void main(String[] args) {
        while(true){
            new Thread(new Runnable(){
                @Override
                public void run() {
                    try {
                        Thread.sleep(10000000);
                    } catch(InterruptedException e) { }
                }
            }).start();
        }
    }
}
复制代码

运行出现异常,说明系统创建的线程数量已经到达最大值。

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
  at java.lang.Thread.start0(Native Method)
  at java.lang.Thread.start(Thread.java:717)
  at com.ajisun.coding.ajisunmybatis.Test2.main(Test2.java:39)
复制代码

如何处理:

  • 首先检查代码,是否有优化的空间,减少不必要的线程创建,或者使用线程池复用线程。
  • 减少堆空间的内存,这样操作系统可以预留更多的内存用于线程创建。
  • 减少每个线程内存空间的占用,使用-Xss可以指定栈空间的大小(注意:太小会出现栈溢出)。
  • 如果真的需要的线程特别多,但是受到操作系统的限,这种需要修改操作系统的最大线程数。

永久代/元空间溢出

Java.lang.outofMemoryError:PermGen Space( 1.8之前)

或者

java.lang.OutOfMemoryError: Metaspace( 1.8之后)

这种错误是永久代或者元空间溢出,在jdk1.8之前会出现这种错误,之后hotspot用元空间代替了永久代来存储class信息。如果一个系统在不断的创建新的类(不是对象实例),那么最终会导致元空间溢出的。

如何处理:

  • 增加元空间的大小,设置其对应参数的值 -XX:MaxMetaspaceSize=512m
  • 减少系统需要的类的数量,检查是否有不需要的类并且清除掉。
  • 使用ClassLoader合理的装载各个类,并定期进行回收。

GC效率低下引起的OOM

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

这个错误比较少见。

Java中主要特点就是垃圾回收,而GC是内存回收的关键,如果效率低下,会严重影响系统的整体性能。如果系统的堆空间太小,那么GC所占的时间就会比较多,并且回收所释放的内存也比较少。根据GC占用系统时间以及内存释放的大小,可以评估出GC的效率,一旦虚拟机认为GC的效率过低,就可能直接抛出OOM异常。

一般情况下,虚拟机会检查以下几种情况来判断效率是否低下:

  • 花在GC上的时间是否超过了98%。
  • 老年代释放的内存是否小于2%。
  • eden 区释放的内存是否小于2%。
  • 是否连续最近5次GC都出现了上述情况(是同时出现,不是出现一个)。

只有满足上面的所有条件才会出现上面的OOM

如何处理:

  • 可以通过开关-XX:-UseGCOverheadLimit禁止OOM的产生。这样就会出现堆溢出的错误,然后根据堆溢出的问题来处理。
  • 优化代码,检查新生代,老年代中对象是否正常,是否有过多的对象无法释放。
  • dump内存,检查是否存在内存泄露,如果没有,加大内存。

本文讲解了几种内存溢出的情况,以及相对应的解决思路。其中常见的错误是堆溢出和线程过多的OOM。

原文地址


我是纪先生,用输出倒逼输入而持续学习,持续分享技术系列文章,以及全网值得收藏好文,欢迎关注公众号,做一个持续成长的技术人。

个人网站

JVM虚拟机系列历史文章

1. 虚拟机系列:jvm运行时堆内存如何分代;

2. 虚拟机系列:jvm中的垃圾回收算法;

3. 虚拟机系列:jvm运行时数据区域;

4. 虚拟机系列:JVM中对象的创建,内存布局和访问定位;

5. 虚拟机系列:JVM中的垃圾收集器;

6. 虚拟机系列:JVM中的内存分配;

7. 虚拟机系列:搞懂虚拟机的日志和日志参数;

8. 虚拟机系列:虚拟机性能监控基础工具;

9. 虚拟机系列:虚拟机性能监控基础工具-jstat;

10. 虚拟机系列:性能监控可视化工具-JConsole;

11. 虚拟机系列:图形化监控工具-VisualVM;

猜你喜欢

转载自juejin.im/post/7102976491608080392