熟悉Java内存划分及运行的首要目的就是预防JVM抛出内存溢出相关的异常,或者说当发生这样异常是该如何排查问题,定位问题并且给出合理的解决方案,这对于开发工作以及后期维护工作的顺利进行尤为重要。

一、在Java语言中,对象访问是如何进行的?

    即使是最简单的访问,也会涉及Java栈、Java堆、方法区这三个最重要内存区域之间的关联关系。比如下面这行代码:

Object obj = new Object();

    简单分析一下这行看似普通的代码:

  • 首先,“Object obj ”这部分的语义将会反映到Java栈的本地变量表中,作为一个reference类型数据出现。
  • 其次,“new Object()”这部分语义将会反映到Java堆中,形成一块存储了Object类型所有实例数据值的结构化内存。
  • 另外,在Java堆中还必须包含能查到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型的数据类型存储在方法区中。

    而且,不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄直接指针。这两种方式应用都十分广泛。

  • 句柄:Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。优点是:reference中存储的是稳定的句柄地址只移动指针,不会修改reference本身。
  • 直接指针:Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址。优点是:速度快,节省开销。

二、有关于OutOfMemoryError异常的实战处理

目的:

  • 通过代码验证Java虚拟机规范中描述的各个运行时区域储存的内容。
  • 希望开发者能根据异常的信息快速判断是哪个区域的内存溢出,知道怎样的代码可能会导致这些区域的内存溢出,以及出现这些异常后该如何处理。

1.Java堆溢出

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

   首先,限制Java堆的大小为20MB,不可扩展(将堆的最小值-Xms参数与最大值-Xms参数设置为一样即可避免堆自动扩展),通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后进行分析。 以下是在Mac下使用idea设置虚拟机参数:

第一步:打开“Run->Edit Configurations”菜单

Java 查询数据量多导致内存溢出解决办法 java如何排查内存溢出_OOM

第二步:选择“VM Options”选项,输入你要设置的VM参数

Java 查询数据量多导致内存溢出解决办法 java如何排查内存溢出_Java_02

输入参数内容为:

-verbose:gc
-Xms20M
-Xmx20M
-Xmn10M
-XX:+PrintGCDetails
-XX:SurvivorRatio=8

第三步:点击“Apply->OK”按钮

第四步:代码如下。

package com.OOM;

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

/**
 * @description: OOM异常例子剖析
 * <p>
 * 运行一下代码之前需要限制Java堆的大小为20MB,不可扩展(将堆的最小值-Xms参数与最大值-Xms参数设置为一样即可避免堆自动扩展),
 * 通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后进行分析。
 * 如何设置虚拟机运行参数的博文连接:https://www.cnblogs.com/huiAlex/p/8227980.html
 * @author: Mr.Wang
 * @create: 2019-02-03 14:23
 **/
public class HeapOOM {
    static class OOMObject {
    }

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

    }

}

    以上代码的运行结果:不光给出了错误原因是OutOfMemoryError,而且指明了是在堆内存中发生的。下面还有堆中各个分区的信息,相比于eclipse,IDEA人性化了很多。

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.OOM.HeapOOM.main(HeapOOM.java:22)
	
Heap
 PSYoungGen      total 9216K, used 7786K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 95% used [0x00000007bf600000,0x00000007bfd9aa80,0x00000007bfe00000)
  from space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
  to   space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
 ParOldGen       total 10240K, used 8783K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  object space 10240K, 85% used [0x00000007bec00000,0x00000007bf493f08,0x00000007bf600000)
 Metaspace       used 3122K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 341K, capacity 388K, committed 512K, reserved 1048576K

那么,应该如何解决这个异常呢?以下是解决方向:

  • 通过内存映像分析功能根据对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄露还是内存溢出。
  • 如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链。
  • 如果不存在泄露,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象的生命周期过长,持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

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

    由于在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是无效的,栈容量只由-Xss参数设定。关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:

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

测试代码(一):

  • 使用-Xss参数减少栈内存容量。结果:抛出StackOverflowError异常,异常出现时输出的栈深度相应缩小。
  • 定义了大量的本地变量,增加此方法帧中本地变量表的长度。结果:抛出StackOverflowError异常时输出的栈深度相应缩小。
package com.OOM;

/**
 * @description: 虚拟机栈和本地方法栈OOM测试    -Xss128k
 * @author: Mr.Wang
 * @create: 2019-02-03 22:14
 **/
public class JavaVMstackSOF {
    private int stackLength = 1;

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

    public static void main(String[] args) {
        JavaVMstackSOF oom = new JavaVMstackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable throwable) {
            System.out.println("stack length:" + oom.stackLength);
            throw throwable;
        }
    }
}

输出结果为:

stack length:20898
Exception in thread "main" java.lang.StackOverflowError
	at com.OOM.JavaVMstackSOF.stackLeak(JavaVMstackSOF.java:13)

实验结果表明:

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

    但是,如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。

测试代码(二):

package com.OOM;

/**
 * @description: 创建线程导致内存溢出异常
 * @author: Mr.Wang
 * @create: 2019-02-03 23:55
 **/
public class JavaVMStackOOM {
    private void dontStop() {
        while (true) {

        }
    }

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

    public static void main(String[] args) {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}

注意:在windows平台的虚拟机执行以上代码有较大风险,可能会造成操作系统假死。

运行结果:

Exception in thread "main" java.lang.OutOfMemoryError:unable to create new native thread

3.运行时常量池溢出

    如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。

    下面将展示代码实例,在运行代码之前同样需要设置虚拟机参数,如下:

Java 查询数据量多导致内存溢出解决办法 java如何排查内存溢出_Java_03

代码如下:

package com.OOM;

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

/**
 * @description: 运行时常量池导致的内存溢出异常  -XX:PermSize=10M -XX:MaxPermSize=10M
 * @author: Mr.Wang
 * @create: 2019-02-04 00:15
 **/
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        // 使用list保持着常量池引用,避免FULL GC回收常量池行为
        List<String> list = new ArrayList<String>();
        // 10MB的PermSize在integer范围内足够产生OOM了
        int i = 0;
        while (true) {
            System.out.println("i:" + i);
            list.add(String.valueOf(i++).intern());
        }
    }
}

运行结果:

Exception in thread "main" java.lang.OutOfMemoryError:PermGen space

结论:

    运行结果表示,产生OOM的部分是PermGen space,说明运行时常量池属于方法区(HotSpot虚拟机中的永久代)的一部分。

4.方法区溢出

    方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。

    以下代码是模拟方法区溢出时的情形,在Spring和hibernate对类进行增强时也会用到CGLib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载入内存。

代码如下:

package com.OOM;

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

import java.lang.reflect.Method;

/**
 * @description: 借助CGLib使得方法区出现内存溢出异常 -XX:PermSize=10M -XX:MaxPermSize=10M
 * @author: Mr.Wang
 * @create: 2019-02-04 11:40
 **/
public class JavaMethodAreaOOM {
    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                    return methodProxy.invokeSuper(o, objects);
                }
            });
            enhancer.create();

        }
    }
    static class OOMObject {
    }
}

运行结果:

Caused by:java.lang.OutOfMemoryError:PermGen space

需要注意的是,如果你的JDK版本是1.8.那么控制台会提示你:

Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=10M; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=10M; support was removed in 8.0

也就是说JDK1.8版本不支持这两个参数配置。

5.本机直接内存溢出

    DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆的最大值(-Xms指定)一样。

代码如下:

package com.OOM;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * @description: 使用unsafe分配本机内存 -Xmx20M -XX:MaxDirectMemorySize=10M
 * @author: Mr.Wang
 * @create: 2019-02-04 11:53
 **/
public class DirectMemoryOOM {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) throws Exception {
        Field unsafedField = Unsafe.class.getDeclaredFields()[0];
        unsafedField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafedField.get(null);
        while (true) {
            unsafe.allocateMemory(_1MB);
        }
    }
}

运行结果:

# 引用原书中的运行结果
Exception in thread "main" java.lang.OutOfMemoryError