OutOfMemoryError


Java虚拟机规范的描述中,除了程序计数器外,虚拟机内存的几个运行时区域都可能发生OutOfMemoryError(OOM)。

堆溢出

Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径避免垃圾回收机制清除这些对象,那么在对象数量达到最大堆的容量限制后就会产生内存溢出。

package org.hbin.oom;

import java.util.ArrayList;
import java.util.List;

/**
 * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 * @author Haley
 * @version 1.0
 * 2024/8/31
 */
public class HeapOOMTest {

    public static void main(String[] args) {
        List<MyObject> list = new ArrayList<>();
        while(true) {
            list.add(new MyObject());
        }
    }

    static class MyObject {}
}

# 运行结果
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid18439.hprof ...
Heap dump file created [27854978 bytes in 0.235 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:267)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233)
	at java.util.ArrayList.add(ArrayList.java:464)
	at org.hbin.oom.HeapOOMTest.main(HeapOOMTest.java:17)

要解决这种瓿,一般是先通过内存分析工具对Dump出来的堆转储快照文件进行分析,确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄露对象的类型信息及GC Roots引用链的信息,就可以比较准确的定位出泄露代码的位置。
如果不存在泄露,就是内存中的对象确实还必须存活,那就应当检查虚拟机的堆参数(-Xms和-Xmx)与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过
长,持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

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

HotSpot虚拟机中并不区分虚拟机栈和本地方法栈。因此,-Xoss参数无效,-Xss参数可以设定栈容量。Java虚拟机规范中描述了栈相关的两种异常:

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
  • 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常

在单线程中,尝试定义大量的本地变量、大量的方法参数均未产生OutOfMemoryError异常,运行结果都是StackOverflowError异常。代码如下:

package org.hbin.oom;

/**
 * VM args: -Xss256k
 * @author Haley
 * @version 1.0
 * 2024/9/1
 */
public class VMStackTest {
    private int stackLength;

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

    public static void main(String[] args) {
        new VMStackTest().test();
    }
}

运行结果:

Exception in thread "main" java.lang.StackOverflowError
	at org.hbin.oom.VMStackTest.test(VMStackTest.java:14)
	at org.hbin.oom.VMStackTest.test(VMStackTest.java:14)
	at org.hbin.oom.VMStackTest.test(VMStackTest.java:14)
	……

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

如果不限于单线程,通过不断地建立线程的方式可以产生内存溢出异常。但是这样产生内存溢出与栈空间是否足够大并不存在任何联系,或者准确地说,在这种情况下,为每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。

package org.hbin.oom;

/**
 * VM args: -Xss256k
 * @author Haley
 * @version 1.0
 * 2024/9/1
 */
public class VMStackTest2 {

    private void busy() {
        while(true);
    }

    private void test() {
        while(true) {
            new Thread(() -> busy()).start();
        }
    }

    public static void main(String[] args) {
        new VMStackTest2().test();
    }
}

运行结果:

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:719)
	at org.hbin.oom.VMStackTest2.test(VMStackTest2.java:17)
	at org.hbin.oom.VMStackTest2.main(VMStackTest2.java:22)

Windows平台中,该代码执行时可能会导致操作系统假死。如果尝试运行该代码,请务必先保存好当前的工作。

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

String.intern()是一个native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并返回此String对象的引用。
在JDK1.6及之前版本,常量池位于永久代中,可以通过-XX:PermSize 和 -XX:MaxPermSize限制方法区大小,从而间接限制常量池的容量。
在JDK8中,字符串常量池位于堆中,上述参数无效了,可以通过-Xmx指定最大堆空间。

package org.hbin.oom;

import java.util.ArrayList;
import java.util.List;


/**
 * JDK1.6 VM args: -XX:PermSize=10M -XX:MaxPermSize=10M
 * JDK8 VM args: -Xms20m -Xmx20m
 * @author Haley
 * @version 1.0
 * 2024/9/1
 */
public class ConstantPoolTest {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        int i = 0;
        while(true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}

运行结果:

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
	at java.lang.Integer.toString(Integer.java:401)
	at java.lang.String.valueOf(String.java:3099)
	at org.hbin.oom.ConstantPoolTest.main(ConstantPoolTest.java:22)

关于常量池,这里有一个有意思的现象:

package org.hbin.oom;

/**
 * @author Haley
 * @version 1.0
 * 2024/9/1
 */
public class ConstantPoolTest2 {

    public static void main(String[] args) {
        String s1 = new StringBuilder("hello").append("world").toString();
        System.out.println(s1.intern() == s1);

        String s2 = new StringBuilder("ja").append("va").toString();
        System.out.println(s2.intern() == s2);
    }
}

这段代码在不同版本的JDK运行结果可能有差异:

  • JDK 1.6:两个false
  • JDK8:true和false

产生上述差异的原因是:

  • JDK1.6中,intern会把首次遇到的字符串实例复制到永久代中,并返回永久代中这个字符串的引用
  • JDK8中,调用intern时,如果池已经包含与equals(Object)方法确定的相当于此String对象的字符串,则返回来自池的字符串。 否则,此String对象将添加到池中,并返回对此String对象的引用

至于str2比较返回false是因为java这个字符串已经存在于字符串常量池中。测试发现,字符串常量池中还存在其他字符串:java,jar,true,false,main,void,byte,short,int,long,char,boolean,float,double
使用CGLib操作字节码时也可能产生类似问题,代码如下:

package org.hbin.oom;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * VM args: -Xms20m -Xmx20m
 * @author Haley
 * @version 1.0
 * 2024/9/1
 */
public class MethodAreaTest {

    public static void main(String[] args) {
        while(true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(Object.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object obj, Method method, Object[] objects, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, objects);
                }
            });
            enhancer.create();
        }
    }
}

本机直接内存溢出

DirectMemory容量可以通过-XX:MaxDirectMemorySize指定,默认是Java堆最大值。

package org.hbin.oom;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * VM args: -Xmx20M -XX:MaxDirectMemorySize=10M
 * @author Haley
 * @version 1.0
 * 2024/9/1
 */
public class DirectMemoryTest {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) throws IllegalAccessException {
        Field field = Unsafe.class.getDeclaredFields()[0];
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        while(true) {
            unsafe.allocateMemory(_1MB);
        }
    }
}
package org.hbin.oom;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

/**
 * VM args: -Xmx20M -XX:MaxDirectMemorySize=10M
 * @author Haley
 * @version 1.0
 * 2024/9/1
 */
public class DirectMemoryTest2 {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) throws IllegalAccessException {
        List<ByteBuffer> list = new ArrayList<>();
        int i = 0;
        while(true) {
            ByteBuffer bf = ByteBuffer.allocateDirect(_1MB);
            list.add(bf);
            System.out.println(i ++);
        }
    }
}

运行结果:

0
1
2
3
4
5
6
7
8
9
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 org.hbin.oom.DirectMemoryTest2.main(DirectMemoryTest2.java:20)

由DirectMemory导致的内存溢出,明显特征是:在Heap Dump文件中不会看见明显的异常。

  • 20
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值