一、内存泄露与内存溢出

内存泄露(memory leak):是指本应该被GC回收的无用对象没有被回收,导致的内存空间的浪费,当内存泄露严重时会导致OOM(内存溢出简称,下文称OOM)。
Java内存泄露根本原因是:长生命周期的对象持有短生命周期对象的引用,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被GC回收。(这篇文章不做详细的举例介绍,后面总结完垃圾回收机制之后,进行详细的举例分析)
内存溢出 (out of memory):是指程序在申请内存时,没有足够的内存空间供其使用,出现OOM。比如申请了一个int,但给它存了long才能存下的数,那就是内存溢出。
本文连接上文JVM系列(1)——java内存区域从各个内存区域可能发生的OOM和栈溢出异常(StackOverflowException)具体举例进行说明,并提供相应的解决办法。
栈内存溢出(StackOverflowError):程序所要求的栈深度过大导致。

二、idea设置jvm内存参数

我们利用idea进行jvm内存参数的设置,以进行异常测试。如图所示:

onlyoffice集成java项目 java loom项目_内存溢出

二、堆内存溢出

java堆用来存储对象实例,不断的创建对象而不去GC,当对象内存占用达到堆最大值的限制后,就会发生内存溢出。

/**
 * -Xms5m  最大可用内存
 * -Xmx5m 初始内存
 */
public class HeapOOM {

    public static void main(String[] args) {
        int count = 0;
        List<Object> list = new ArrayList<Object>();
        while(true){
            list.add(new Object());
            System.out.println(++count);
        }
    }

运行结果,在160065个对象后,发生OOM:

160065
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:265)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
	at java.util.ArrayList.add(ArrayList.java:462)
	at com.jvm.HeapOOM.main(HeapOOM.java:16)

随着-Xmx参数值的增大,java堆中可以存储的对象也越多。

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

对于HotSpot虚拟机来说,栈容量只有-Xss参数设置(HotSpot虚拟机不区分虚拟机栈和本地方法栈)。
(1)如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常;
(2)如果虚拟机栈扩展时无法申请到足够的内存时会抛出 OutOfMemoryError 异常。

package com.jvm;

public class JvmStackSOF {
    private int stackLength = 1;

    //反复调用
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }

    /**
     * -Xss128k  栈内存容量
     */
    public static void main(String[] args) throws Throwable {
        JvmStackSOF oom = new JvmStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
    }
}

运行结果如下:

stack length:11415
Exception in thread "main" java.lang.StackOverflowError
	at com.jvm.JvmStackSOF.stackLeak(JvmStackSOF.java:8)
	at com.jvm.JvmStackSOF.stackLeak(JvmStackSOF.java:9)
	at com.jvm.JvmStackSOF.stackLeak(JvmStackSOF.java:9)
	at com.jvm.JvmStackSOF.stackLeak(JvmStackSOF.java:9)
	at com.jvm.JvmStackSOF.stackLeak(JvmStackSOF.java:9)

在单个线程下,无论是由于栈帧太大,还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是 StackOverflowError 异常,而不是OOM。
如果是多线程的情况下,不断建立线程,会出现OOM。代码如下:

package com.jvm;

public class JvmStackSOF {

    /**
     * -Xss2M  栈内存容量
     */
    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) {
        JvmStackSOF oom = new JvmStackSOF();
        oom.stackLeakByThread();
    }
}

运行结果如下:

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:717)
	at com.jvm.JvmStackSOF.stackLeakByThread(JvmStackSOF.java:21)
	at com.jvm.JvmStackSOF.main(JvmStackSOF.java:27)

这种情况下产生的内存溢出异常与栈空间是否足够大并不存在任何联系,或者准确地说,在这种情况下,给每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。

原理如图:

onlyoffice集成java项目 java loom项目_栈容量_02


操作系统分配给每个进程的内存是有限制的,譬如 32 位的 Windows 限制为 2GB。虚拟机提供了参数来控制 Java 堆和方法区的这两部分内存的最大值。剩余的内存为 2GB(操作系统限制)减去 Xmx(最大堆容量),再减去 MaxPermSize(最大方法区容量),程序计数器消耗内存很小,可以忽略掉。如果虚拟机进程本身耗费的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈“瓜分”了。每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。

解决方案:出现栈内存OOM,在不能减少线程数或者更换 64 位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。