【JVM原理探索】让你完全攻克内存溢出(OOM)这一难题

简介: 【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 space
java.lang.OutOfMemoryError: Metaspace
复制代码




导致原因


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


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


  • 字符串常量由永久代转移到堆中
  • 和永久代相关的JVM参数已移除



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


  1. 在Java7之前,频繁的错误使用String.intern方法。
  2. 生成了大量的代理类,导致方法区被撑爆,无法卸载。
  3. 应用长时间运行,没有重启。




解决方法


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


  1. 检查是否永久代空间或者元空间设置的过小。
  2. 检查代码中是否存在大量的反射操作或者class加载操作以及生产class字节码。
  3. dump之后通过mat检查是否存在大量由于反射生成的代理类
  4. 放大招,重启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.JVMStackSOF
stack length = 1888
Exception 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 异常;


  1. 如果虚拟机栈可以动态扩展(当前大部分的 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-max
max_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.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at org.fenixsoft.oom.DMOOM.main(DMOOM.java:20)
复制代码



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




相关文章
|
3天前
|
算法 调度 UED
深入理解操作系统内存管理:原理与实践
【4月更文挑战第23天】 在现代计算机系统中,操作系统的内存管理是保证系统高效、稳定运行的关键组成部分。本文旨在深入探讨操作系统中内存管理的理论基础、关键技术以及实际操作过程,通过对内存分配策略、虚拟内存技术、分页与分段机制等核心概念的详细解析,为读者提供一个清晰、全面的内存管理视角。此外,文章还将通过案例分析,展示内存管理在解决实际问题中的应用,以期加深读者对操作系统内存管理复杂性的认识和理解。
|
存储 安全 算法
深入剖析JVM内存管理与对象创建原理
JVM内存管理,JVM运行时区域,直接内存,对象创建原理。
40 2
|
28天前
|
存储 缓存 Java
金石原创 |【JVM盲点补漏系列】「并发编程的难题和挑战」深入理解JMM及JVM内存模型知识体系机制(1)
金石原创 |【JVM盲点补漏系列】「并发编程的难题和挑战」深入理解JMM及JVM内存模型知识体系机制(1)
37 1
|
1月前
|
存储 算法 编译器
【C++ 内存管理 重载new/delete 运算符 新特性】深入探索C++14 新的/删除的省略(new/delete elision)的原理与应用
【C++ 内存管理 重载new/delete 运算符 新特性】深入探索C++14 新的/删除的省略(new/delete elision)的原理与应用
46 0
|
28天前
|
缓存 Java C#
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍(一)
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍
79 0
|
10天前
|
监控 前端开发 安全
JVM工作原理与实战(十四):JDK9及之后的类加载器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了JDK8及之前的类加载器、JDK9及之后的类加载器等内容。
19 2
|
10天前
|
监控 Java 关系型数据库
JVM工作原理与实战(十三):打破双亲委派机制-线程上下文类加载器
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了打破双亲委派机制的方法、线程上下文类加载器等内容。
14 2
|
10天前
|
存储 XML 监控
JVM工作原理与实战(三):字节码文件的组成
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了字节码文件的基础信息、常量池、方法、字段、属性等内容。
26 6
|
14天前
|
存储 前端开发 安全
JVM内部世界(内存划分,类加载,垃圾回收)(上)
JVM内部世界(内存划分,类加载,垃圾回收)
48 0
|
17天前
|
存储 算法
深入理解操作系统内存管理:原理与实践
【4月更文挑战第8天】 在现代计算机系统中,操作系统扮演着关键角色,特别是在内存资源的管理上。本文将深入探讨操作系统中的内存管理机制,包括虚拟内存、物理内存的分配与回收,以及页面置换算法等关键技术。通过分析不同内存管理策略的优势与局限性,本文旨在为读者提供一套系统的内存管理知识框架,帮助理解操作系统如何高效、安全地管理有限的内存资源以满足多任务处理的需求。