写点什么

☕【JVM 技术之旅】让你完全攻克内存溢出(OOM)这一难题(上)

发布于: 2021 年 05 月 24 日
☕【JVM 技术之旅】让你完全攻克内存溢出(OOM)这一难题(上)

每日一句

只有经历地狱般的磨练,才能创造出天堂般的力量


堆(Heap)内存不足

报错信息:

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

导致原因

  1. 代码中可能存在大对象分配

  2. 可能存在内存泄露,导致在多次 GC 之后,还是无法找到一块足够大的内存容纳当前对象

  3. 业务场景会剧增对象数据,应该提升内存空间

解决方法

  1. 检查是否存在大对象的分配,最有可能的是大数组分配

  2. 通过 jmap 命令,把堆内存 dump 下来,使用 mat 工具分析一下,检查是否存在内存泄露的问题

  3. 如果没有找到明显的内存泄露,使用 -Xms/-Xmx 加大堆内存

  4. 还有一点容易被忽略,检查是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性

方法区溢出

报错信息:

java.lang.OutOfMemoryError: PermGen spacejava.lang.OutOfMemoryError: Metaspace
复制代码

导致原因

  • JDK8 之前,永久代是 HotSot 虚拟机对方法区的具体实现,存放了被虚拟机加载的类信息、常量、静态变量、JIT 编译后的代码等。

  • JDK8 后,元空间替换了永久代,元空间使用的是本地内存,还有其它细节变化:

  • 字符串常量由永久代转移到堆中

  • 和永久代相关的 JVM 参数已移除

  • 出现永久代或元空间的溢出的原因可能有如下几种:

  • 在 Java7 之前,频繁的错误使用 String.intern 方法。

  • 生成了大量的代理类,导致方法区被撑爆,无法卸载。

  • 应用长时间运行,没有重启。

解决方法

  • 永久代/元空间 溢出的原因比较简单,解决方法有如下几种

  • 检查是否永久代空间或者元空间设置的过小。

  • 检查代码中是否存在大量的反射操作或者 class 加载操作以及生产 class 字节码。

  • dump 之后通过 mat 检查是否存在大量由于反射生成的代理类

  • 放大招,重启 JVM

GC overhead limit exceeded

报错信息

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

导致原因

这个是 JDK6 新加的错误类型,一般都是堆太小导致的


Sun 官方对此的定义:超过 98%的时间用来做 GC 并且回收了不到 2%的堆内存时会抛出此异常

解决方法

  1. 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码

  2. 添加参数-XX:-UseGCOverheadLimit 禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space

  3. dump 内存,检查是否存在内存泄露,如果没有,加大内存

虚拟机栈和本地方法栈溢出

由于在 HotSpot 虚拟机中并不区分虚拟机栈和本地方法栈,因此对于 HotSpot 来说,-Xoss 参数(设置本地方法栈大小)虽然存在,但实际上是无效的,栈容量只由-Xss 参数设定。关于虚拟机栈和本地方法栈,在 Java 虚拟机规范中描述了两种异常:


  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError 异常。

  • 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常。


这里把异常分成两种情况看似更加严谨,但却存在着一些互相重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已

虚拟机的 StackOverflowError 异常

-Xss 参数减小栈内存的容量,然后不断调用方法造成栈溢出,StackOverflowError 异常。


public class JVMStackSOF {    private int stacklength = 1;   // 记录栈深度
// 调用这个递归方法以造成栈溢出 public void stackPush(){ stacklength++; stackPush(); } public static void main(String[] args) throws Throwable{ JVMStackSOF sof = new JVMStackSOF(); try{ sof.stackPush(); }catch(Throwable e){ System.out.println("stack length = " + sof.stacklength); throw e; } }}
复制代码


openjdk@ubuntu:~$ java -Xss256k -cp/home/openjdk/NetBeansProjects/JavaApplication1/build/classes test_JVMStackSOF.JVMStackSOFstack length = 1888Exception in thread "main" java.lang.StackOverflowError   at test_JVMStackSOF.JVMStackSOF.stackPush(JVMStackSOF.java:17)   at test_JVMStackSOF.JVMStackSOF.stackPush(JVMStackSOF.java:18)
复制代码


-Xss256K:设置参数栈内存容量为 256K


  • 在单个线程下,无论是由于栈帧太大,还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是 StackOverflowError 异常。


使用-Xss 参数减少栈内存容量。结果:抛出 StackOverflowError 异常,异常出现时输出的栈深度相应缩小。


定义了大量的本地变量,增加此方法帧中本地变量表的长度。结果:抛出 StackOverflowError 异常时输出的栈深度相应缩小。




虚拟机栈隔离的,每个线程都有自己独立的虚拟机栈。


在 Java 虚拟机规范中,对虚拟机栈这个区域规定了两种异常状况:


  1. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;

  2. 如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可动态扩展),在扩展时无法申请到足够的内存时会抛出 OutOfMemoryError 异常。

虚拟机的 OutOfMemoryError 异常

通过-Xss2M 参数增大栈内存的容量,然后不断开启新的线程,抛出 OutOfMemoryError 异常


public class JVMStackOOM {
private void dontStop() { while (true) { } }
public static void main(String[] args) { // 不断开启新的线程消耗虚拟机栈空间 while (true) { new Thread(new Runnable() { @Override public void run() { dontStop(); } }).start(); } }}
复制代码

原理

  • 主要是因为-Xss 参数设置的是一个线程的栈大小前面已经说过虚拟机栈是线程私有的,即每个线程都有一个自己的栈


操作系统分配给每个进程的内存是有限制的,譬如 32 位的 Windows 限制为 2GB。Java 虚拟机提供了参数来控制 Java 堆和方法区的这两部分内存的最大值


2GB(操作系统限制的内存大小)减去 Xmx(最大堆容量),再减去 MaxPermSize(最大方法区容量),程序计数器消耗内存很小,可以忽略掉。如果虚拟机进程本身耗费的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈“瓜分”了


所以每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。第一例中把栈空间占满而抛出 StackOverflowError 异常,第二例中把内存消耗完而抛出 OutOfMemoryError 异常



方法栈溢出(从属于虚拟机栈的异常)

报错信息

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

导致原因

出现这种异常,基本上都是创建的了大量的线程导致的,以前碰到过一次,通过 jstack 出来一共 8000 多个线程

解决方法

  1. 通过 -Xss 降低的每个线程栈大小的容量

  2. 线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制


/proc/sys/kernel/pid_max/proc/sys/kernel/thread-maxmax_user_process(ulimit -u)/proc/sys/vm/max_map_count
复制代码

非常规溢出

下面这些 OOM 异常,可能大部分的同学都没有碰到过,但还是需要了解一下

分配超大数组

报错信息

java.lang.OutOfMemoryError: Requested array size exceeds VM limit
复制代码


这种情况一般是由于不合理的数组分配请求导致的,在为数组分配内存之前,JVM 会执行一项检查。要分配的数组在该平台是否可以寻址(addressable),如果不能寻址(addressable)就会抛出这个错误。


解决方法就是检查你的代码中是否有创建超大数组的地方

swap 区溢出

报错信息 :

java.lang.OutOfMemoryError: Out of swap space
复制代码


这种情况一般是操作系统导致的,可能的原因有:


  1. swap 分区大小分配不足;

  2. 其他进程消耗了所有的内存。

解决方案

  1. 其它服务进程可以选择性的拆分出去

  2. 加大 swap 分区大小,或者加大机器内存大小

本地方法溢出

报错信息 :

java.lang.OutOfMemoryError: stack_trace_with_native_method
复制代码


本地方法在运行时出现了内存分配失败,和之前的方法栈溢出不同,方法栈溢出发生在 JVM 代码层面,而本地方法溢出发生在 JNI 代码或本地方法处

本机直接内存溢出

  • 直接内存可以通过:-XX:MaxDirectMemorySize 来设置大小,如果不设置,默认和堆在最大值-Xmx 一样大。

  • 设置本机直接内存的原则就是,各种内存大小+本机直接内存大小<机器物理内存。


下面程序利用 DirectByteBuffe 模拟直接内存溢出的情况


import java.nio.ByteBuffer;import java.util.ArrayList;import java.util.List;public class DirectBufferOom {  public static void main(String[] args) {    final int _1M = 1024 * 1024;    List<ByteBuffer> buffers = new ArrayList<>();    int count = 1;    while (true) {      ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M);      buffers.add(byteBuffer);      System.out.println(count++);    }  }}
复制代码


在命令行运行 java -XX:MaxDirectMemorySize=10M DirectBufferOom ,很快控制台就会出现异常


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


其实它并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常。下面的程序利用 Unsafe 类模拟直接内存溢出


import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class UnsafeOom { private static final int _1M = 1024 * 1024;
public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException { Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe"); unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); while (true) { unsafe.allocateMemory(_1M); } }}
复制代码


在命令行运行 java -XX:MaxDirectMemorySize=10M UnsafeOom ,结果如下


Exception in thread"main"java.lang.OutOfMemoryErrorat sun.misc.Unsafe.allocateMemory(Native Method)at org.fenixsoft.oom.DMOOM.main(DMOOM.java:20)
复制代码


由 DirectMemory 导致的内存溢出,一个明显的特征是在 Heap Dump 文件中不会看见明显的异常,如果读者发现 OOM 之后 Dump 文件很小,而程序中又直接或间接使用了 NIO ,那就可以考虑检查一下是不是这方面的原因。

用户头像

我们始于迷惘,终于更高水平的迷惘。 2020.03.25 加入

🏆 【酷爱计算机技术、醉心开发编程、喜爱健身运动、热衷悬疑推理的”极客狂人“】 🏅 【Java技术领域,MySQL技术领域,APM全链路追踪技术及微服务、分布式方向的技术体系等】 🤝未来我们希望可以共同进步🤝

评论

发布
暂无评论
☕【JVM 技术之旅】让你完全攻克内存溢出(OOM)这一难题(上)