02JVM-内存溢出

上篇文章中,记录了Java内存模型中运行时数据区的划分,每个数据区域都会可能伴随着内存溢出异常。

大致分为2类:OutOfMemoryError、StackOverflowError

一、Java堆内存溢出

Java堆是用于存放Java对象,分别在新生代和老年代。可以使用。一般情况下,新创建的对象都会在新生代中,当对象生存到一定年龄后会被移动到老年代中,当然也有一些内存占用较大的对象直接分配到老年代中。

  • -Xms设置最小的Java堆和-Xmx设置最大的Java堆
  • 参数-XX:+HeapDumpOnOutOfMemoryError让当内存溢出时Dump出当前内存堆转储快照
import java.util.ArrayList;
import java.util.List;

/**
 * Created by ZRD on 2016/09/29.
 * 测试Java堆内存溢出
 */
public class HeapOutOfMemory {

    static class TestObject {    }

    /**
     * VM args: Xms20m Xmx20m -XX:+HeapDumpOnOutOfMemoryError XX:HeapDumpPath=D:\\maven2011\\workspace\\intellij_Idea\\gavin\\gavin-dev\\jvm
     */
    public static void testHeap() {
        System.out.println("Java Runtime >>>> total=" + Runtime.getRuntime().totalMemory()
                + "|free=" + Runtime.getRuntime().freeMemory());

        List<TestObject> list = new ArrayList<TestObject>();
        while (true) {
            TestObject oomObject = new TestObject();
            list.add(oomObject);
        }
    }

    public static void main(String[] args) {
        testHeap();
    }
}

运行结果:

Java Runtime >>>> total=20316160|free=19727648
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid46876.hprof ...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:2760)
    at java.util.Arrays.copyOf(Arrays.java:2734)
    at java.util.ArrayList.ensureCapacity(ArrayList.java:167)
    at java.util.ArrayList.add(ArrayList.java:351)
    at com.gavin.memory.HeapOutOfMemory.testHeap(HeapOutOfMemory.java:26)
    at com.gavin.memory.HeapOutOfMemory.main(HeapOutOfMemory.java:31)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)
Heap dump file created [24828355 bytes in 0.246 secs]

内存溢出时抛出 java.lang.OutOfMemoryError: Java heap space

当内存发生Java heap space时,有可能是内存泄露和内存溢出。
  • 内存泄露是GC无法回收已经不被使用的Java对象,到时内存没办法得到及时回收,因此需要查看是否存在对象不使用时GC Roots仍然有对对象保存着引用计数

  • 而内存溢出是JVM启动的堆内存大小不足以给新的对象分配内存空间。

解决Java堆内存溢出的问题,一般在生产环境中需要打印堆转存快照,然后使用内存映像分析工具对Dump出来的堆转存快照进行分析。要确认内存中的对象是否是必要存在,也就是要确定内存泄露(Memory Leak)还是内存溢出(Memory Overflow)

Java堆转存图

Java堆转存图

查看栈信息

  • 如果确定为内存泄露,可以进一步通过工具查看堆中的对象到GC Roots的引用链。掌握了引用链信息,确认内存泄露的代码位置,并推导出什么原因导致垃圾回收器无法自动回收他们。

  • 如果为内存溢出,需要检查虚拟机的参数-Xms -Xmx与物理机器对比。

另外上述的追查过程,优化结合代码去做,是否是程序编写存在问题,例如某些对象生命周期不宜多长等待

二、Java栈和Native栈

在HotSpot中,JVM参数的配置并不却分Java栈和Native栈。实际上对于HotSpot来说,虽然-Xoss(设置本地方法栈大小)是存在的,但是无效的。

Java虚拟机规范中规定了两种异常:
  1. 线程请求的栈深度大于虚拟机所允许的最大深度,则抛出StackOverflowError
  2. 虚拟机扩展栈时,无法申请到足够的内存空间,则抛出OutOfMemoryError
下面方式使用单线程模拟

两种方式测试:
1. 使用-Xss减少栈的内存容量
2. 在方法类定义大量的局部变量,目的为增大栈帧中本地变量表的长度,以增大栈帧的容量


/**
 * Created by ZRD on 2016/09/29.
 *
 * 测试虚拟机栈溢出
 */
public class StackOverflow {

    /**
     *
     * VM args: -Xss128k
     * 
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {

        StackOverflow stackOverflow = new StackOverflow();

        try {
            stackOverflow.stackLeak();
        } catch (Exception e) {

            System.out.println("stack lenth : " + stackOverflow.stackLen);

            throw e;
        }

    }

    private int stackLen = 1;

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

实验结论

无论栈帧太大还是虚拟机容量太小,当内存无法分配的时候,虚拟机都抛出StackOverflowError异常

上述的测试是单线程,如果不局限于单线程,通过不断地建立线程的方式会导致内存溢出异常。这种情况下,为每个线程的栈分配的内存越大,反而更容易产生内存异常

package com.gavin.jvm.memory;

/**
 * Created by ZRD on 2016/09/29.
 *
 * 测试虚拟机栈溢出(多线程模式下)
 */
public class StackOverflowMultiThread {

    /**
     *
     * VM args: -Xss2M (可以调大些来观察)
     *
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {

        StackOverflowMultiThread stackOverflow = new StackOverflowMultiThread();

        try {
            stackOverflow.stackLeakByThread();
        } catch (Exception e) {

            throw e;
        }

    }

    private void dontStop() {
        while (true) {

        }
    }

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

这个时候得出以下结论:
* 多线程环境下,物理机器内存一定(物理内存中除去堆容量,方法区容量,剩下的内存就是栈可以使用的内存),如果为每个线程分配的栈内存越大,虚拟机中可以创建的线程就会越少,过多时会容易产生内存溢出

运行上面2段代码时特别注意,windows系统会卡死

三、方法区和运行时常量池

  • 运行时常量池是方法区的一部分

String.intern()一个navtive方法,作用是:如果字符串常量池中已经包含一个等于此对象的字符串,则会返回常量池中的字符对象;否则将此字符串加入常量池中。
也就是,intern()会把首次遇到的字符串实例放入到永久代中,并返回永久代中字符串实例

下面代码使用String.intern()模拟永久代溢出,JDK6才会出现。

/**
 * Created by ZRD on 2017/02/13.
 * 测试方法区(永久代)溢出
 */
public class RuntimeConstantPoolOOM {

    /**
     * VM Args: -XX:+PermSize=10M -XX:MaxPermSize=10M
     * @param args
     */
    public static void main(String[] args) {

        // 使用List保持着常量池的引用,避免Full GC回收常量池行为
        List<String> list = new ArrayList<String>();

        int i = 0;

        while (true) {
            list.add(String.valueOf(i++).intern());
        }

    }
}

我是在JDK7中运行的,会一直运行下去。不会抛出OutOfMemoryError异常,也不会提示“PermGen space”。

四、本机直接内存溢出

直接内存容量 -XX:MaxDirectMemorySize指定,如果不指定,默认与-Xmx大小一致。

/**
 * Created by ZRD on 2017/02/13.
 *
 * 测试直接内存溢出
 *
 */
public class DirectMemoryOOM {

    private static final int _1MB = 1 * 1024 * 1024;

    /**
     * VM Args: -Xmx20m -XX:DirectMemorySize=10M
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);

        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while (true) {
            unsafe.allocateMemory(_1MB);
        }
    }

}

直接内存溢出

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值