14 实战OOM

1. Java堆溢出

//OutOfMemoryError类继承关系
java.lang.Object	
    java.lang.Throwable	
        java.lang.Error	
            java.lang.VirtualMachineError	
                java.lang.OutOfMemoryError
  • Java堆用于存储对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。
  • JVM参数设置:-Xms5m -Xmx5m -XX:+HeapDumpOnOutOfMemoryError
    • -Xms5m:将堆的最小值设置为5m;
    • -Xmx5m:将堆的最大值设置为5m;
    • 将堆的最小值参数-Xms与堆的最大值参数-Xmx设置为一样的,此堆即不可自动扩展;
    • -XX:+HeapDumpOnOutOfMemoryError:让虚拟机在出现内存溢出异常的时候Dump出当前的内存堆转储快照以便进行后续分析。
package com.oom.heap;

import java.util.ArrayList;

/**
 * @author rrqstart
 * VM Args : -Xms5m -Xmx5m -XX:+HeapDumpOnOutOfMemoryError
 */
public class HeapOOMTest {
    public static void main(String[] args) {
        ArrayList<HeapOOMTest> list = new ArrayList<>();
        
        while (true) {
            list.add(new HeapOOMTest());
        }
    }
}
//运行结果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid26588.hprof ...
Heap dump file created [28276070 bytes in 0.066 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
...
  • 要解决Java堆内存区域的异常,常规的处理方法是首先通过内存映像分析工具对Dump出来的堆转储快照进行分析。
  • 第一步先确认内存中导致OOM的对象是否是必要的,也就是先确定是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
  • 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。
  • 如果不是内存泄漏,即内存中的对象都必须是存活的,那此时应该检查JVM的堆参数-Xms-Xmx的设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

  • 这个细节信息表示在Java堆中无法再分配对象。这个错误并不代表你的程序一定发生了内存泄漏,可能就是一个配置的问题,可能默认的堆内存无法满足应用的需求。
  • 也有可能是在一些长时间运行的程序中,可能是一直保持着对某些对象的引用(实际上这些对象已经不需要了),这会阻止垃圾回收器收集内存从而无法分配新的内存空间,这就等同于是一个内存泄漏。
  • 另外一个潜在的原因可能是对于 finalize 方法的过度使用。如果某个类具有 finalize 方法,那么属于这种类的对象在垃圾回收时就不会回收空间。而是在垃圾回收之后,对象会在一个队列中等待析构,这通常会发生的迟一些。在 Oracle 公司的实现中,finalizer 是通过一个为 finalization 队列提供服务的守护线程来执行。如果 finalizer 线程的速度没有办法跟上 finalization 队列速度的时候,那么Java堆就会填满接着就会抛出OOM异常。

Exception in thread "main" java.lang.OutOfMemoryError: GC Overhead limit exceeded

  • 这个异常信息一般表示Java程序运行很缓慢并且垃圾回收器一直在运行。在垃圾回收之后,如果Java进程花费超过98%的时间来做垃圾回收,在连续的5次垃圾回收中恢复少于2%的堆内存,就会抛出该异常。
  • 一般这种情况是因为生成大量的数据占用Java堆内存从而没有办法分配新的内存。即垃圾回收器回收的速度还没有办法跟上内存分配的速度。
  • 对于GC Overhead limit exceeded可以通过参数-XX:-UseGCOverheadLimit来进行关闭,虽然最终还是可能还是会抛出OOM异常。

Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit

  • 这个异常信息表示应用程序尝试给数组分配一个大于堆大小的数组。比如,如果程序尝试分配一个512MB 大小的数组,但是堆大小最大只有256MB,那么该异常则会被抛出。
  • 导致这种异常信息的原因一般要么就是配置的问题(堆内存太小),要么就是程序的BUG,尝试分配太大的数组。

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

  • 由于HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是没有任何效果的,栈容量只能由-Xss参数来设置。
  • 关于虚拟机栈和本地方法栈,在《Java虚拟机规范》中描述了两种异常:
    • StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的最大深度时将抛出此异常。
    • OutOfMemoryError:如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时将抛出此异常。
  • 《Java虚拟机规范》明确允许Java虚拟机实现自行选择是否支持栈的动态扩展,而HotSpot虚拟机选择不支持栈的动态扩展,所以除非在创建线程申请内存时就因无法获得足够的内存而出现OutOfMemoryError异常,否则在线程运行时是不会因为扩展而导致内存溢出的,只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。
  • 无论是由于栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候,HotSpot虚拟机抛出的都是StackOverflowError异常。
  • 在单线程环境中,使用-Xss参数减少栈内存的容量或者定义大量的本地变量以增加此方法栈帧中本地变量表的长度(在某方法内定义大量的局部变量),都将导致结果为:Exception in thread “main” java.lang.StackOverflowError
  • 对于不同版本的JVM和不同的操作系统,栈容量最小值可能会有所限制,这主要取决于操作系统内存分页大小。
  • 在某些情况下,如果设置的栈容量低于这个最小栈容量的限制,HotSpot虚拟机启动时会给出如下提示:
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

The stack size specified is too small, Specify at least 108k

Process finished with exit code 1
package com.oom.stack;

/**
 * @author rrqstart
 * VM Args : -Xss128k
 */
public class JVMStackSOF {
    private int stackLength = 1;

    public void stackLeak() {
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) {
        JVMStackSOF sof = new JVMStackSOF();
        sof.stackLeak();
        System.out.println("stackLength = " + sof.stackLength);
    }
}
//运行结果:
Exception in thread "main" java.lang.StackOverflowError
...
  • 如果不限于单线程,通过不断建立线程的方式,在HotSpot上也是可以产生内存溢出异常的。但是这样产生的内存溢出异常和栈空间是否足够并不存在任何直接的关系,主要取决于操作系统本身的内存使用状态。在这种情况下,给每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。操作系统分配给每个进程的内存是有限制的。HotSpot虚拟机提供了参数可以控制Java堆和方法区这两部分的内存的最大值,剩余内存即为操作系统限制的每个进程的内存最大值减去最大堆容量和最大方法区容量之后的,由于程序计数器消耗内存很小,可以忽略不计,如果把直接内存和虚拟机进程本身耗费的内存也去掉的话,剩下的内存就由虚拟机栈和本地方法栈来分配了。因此,为每个线程分配到的栈内存越大,可以建立的线程数目自然就越少,建立线程时就越容易把剩下的内存耗尽。
package com.oom.stack;

/**
 * @author rrqstart
 * VM Args : -Xss20m
 */
public class JVMStackOOM {
    private void dontStop() {
        while (true) {

        }
    }

    public void stackLeakByThread() {
        while (true) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }

    public static void main(String[] args) {
        JVMStackOOM oom = new JVMStackOOM();
        oom.stackLeakByThread();
    }
}
//运行结果:                                                                                                                                         
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
...
  • 出现StackOverflowError异常时,会有明确错误堆栈可供分析,相对而言比较容易定位到问题所在。如果使用HotSpot虚拟机默认参数,栈深度在大多数情况下到达1000~2000是没问题的,对于正常的方法调用,这个深度应该完全够用。但是,如果是建立过多线程导致的内存溢出,在不能减少线程数量的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。从JDK7开始,以上提示信息中“unable to create new native thread”后面,虚拟机会特别注明原因可能是“possibly out of memory or process/resource limits reached”。

3. 方法区和运行时常量池溢出

  • String类的intern()方法是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并返回此String对象的引用。
  • JDK8开始,HotSpot虚拟机提供的元空间的设置参数有:
    • -XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。
    • -XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。
    • -XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。
    • -XX:MaxMetaspaceFreeRatio:用于控制最大的元空间剩余容量的百分比。

Exception in thread "main" java.lang.OutOfMemoryError: Metaspace

  • Java类metadata(Java 类虚拟机内部的表示) 使用原生内存来进行分配。如果用于metadata的metaspace耗尽了,那么具有这个异常信息的OOM异常就会被抛出。
public class InternTest {
    public static void main(String[] args) {
        String str1 = new StringBuffer("j").append("d").toString();
        System.out.println(str1); //jd
        System.out.println(str1.intern()); //jd
        System.out.println(str1 == str1.intern()); //true
        System.out.println("----------");
        String str2 = new StringBuffer("ja").append("va").toString();
        System.out.println(str2); //java
        System.out.println(str2.intern()); //java
        System.out.println(str2 == str2.intern()); //false
        /**
         * 在加载System类时,会加载System类的静态方法initializeSystemClass(),
         * 在initializeSystemClass()静态方法内部有一句这样的代码:sun.misc.Version.init();
         * 在sun.misc.Version类内部声明了这样一个字段:private static final String launcher_name = "java";
         * String str2 = new StringBuffer("ja").append("va").toString();在执行之前"java"就已经出现了
         * sun.misc.Version类(在rt.jar包下)会在JDK类库的初始化过程中被加载并初始化。
         */
    }
}

4. 本机直接内存溢出

  • 直接内存大小可以通过-XX:MaxDirectMemorySize参数进行设置。如果不指定,默认与堆的最大值-Xmx参数值一致。
package com.oom.nativememory;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * @author rrqstart
 * VM Args : -Xmx20m -XX:MaxDirectMemorySize=10m
 */
public class DirectMemoryOOMTest {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) throws IllegalAccessException {
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while (true) {
        	//allocateMemory()方法申请分配直接内存
            unsafe.allocateMemory(_1MB);
        }
    }
}
//运行结果:
Exception in thread "main" java.lang.OutOfMemoryError
	at sun.misc.Unsafe.allocateMemory(Native Method)
	at com.oom.nativememory.DirectMemoryOOMTest.main(DirectMemoryOOMTest.java:19)
  • 由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况,如果发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(典型的间接使用就是 NIO),那么可以考虑重点检查一下直接内存方面的原因。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值