哪部分区域、什么样的代码和操作可能导致内存溢出异常?(实战:OutOfMemoryError 异常)

来源:互联网 发布:淘宝统计消费 编辑:程序博客网 时间:2024/05/16 02:42

Java内存区域与内存溢出异常

1. 概述(为什么要去了解虚拟机是怎样使用内存的?)
2. 运行时数据区域(虚拟机中的内存是如何划分的?)
3. HotSpot 虚拟机对象探秘 (HotSpot 虚拟机在 Java 堆中对象是如何创建、如何布局以及如何访问的?)
4. 实战:OutOfMemoryError 异常(哪部分区域、什么样的代码和操作可能导致内存溢出异常?)


4、哪部分区域、什么样的代码和操作可能导致内存溢出异常?

Q:为什么要学习此章内容? 两个目的!
①、通过代码验证 Java 虚拟机规范中描述的各个运行时区域存储的内容;
②、希望读者在工作中遇到实际的内存溢出异常时,能根据异常的信息快速判断是哪个区域的内存溢出,知道什么样的代码可能会导致这些区域内存溢出,以及出现这些异常后该如何处理

以下代码开头都注释了执行时所需要设置的虚拟机启动参数(注释中“VM Args”后面跟着的参数)。
如何设置虚拟机启动参数?【提醒:点击蓝色字体查看详情!】

以下的代码都是基于 Sun 公司的 HotSpot 虚拟机运行的。

4.1 Java 堆溢出

Q:怎么通过代码去验证此区域存储的内容?
Java 堆用于存储对象实例,所以只要不断地创建对象,并且保证 GC Roots 到对象之间有可达路径来避免垃圾回收机制清除这些对象。当对象数量到达最大堆的容量限制后就会产生内容溢出异常。

根据测试,只要设置最大值 -Xmx 参数即可避免堆自动扩展

import java.util.ArrayList;import java.util.List;/** * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * 限制 Java 堆的大小为 20MB,不可扩展(将堆的最小值 -Xms 参数与最大值 -Xmx 参数设置为一样即可避免堆自动扩展) * 通过参数 -XX:+HeapDumpOnOutOfMemoryError 可以让虚拟机在出现内存溢出异常时 Dump 出当前的内存堆转储快照以便事后进行分析。 * * @author TinyDolphin *         2017/7/5 14:14. */public class HeapOOM {    static class OOMObject {    }    public static void main(String[] args) {        List<OOMObject> list = new ArrayList<OOMObject>();        while (true) {            list.add(new OOMObject());        }    }}

运行结果:
这里写图片描述

结论:
当出现 Java 堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”

怎么解决这个区域的异常呢?
一般的手段:通过内存映像分析工具(如 Eclipse Memory Analyzer)对 Dump 出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄露(Memory Leak)还是内存溢出(Memory Overflow 即 out of memory)

如果是内存泄露,可进一步通过工具查看泄露对象到 GC Roots 的引用链。于是就能找到泄露对象是通过怎样的路径与 GC Roots 相关联并导致垃圾收集器无法自动回收它们的。

如果不存在泄露(内存中的对象确实都还必须存活着),那就应当检查虚拟机的堆参数(-Xmx 与 -Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗

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

由于在 HotSpot 虚拟机中并不区分虚拟机栈本地方法栈,因此,对于 HotSopt来说,虽然 -Xoss 参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由 -Xss 参数设定。

Q:什么情况下,该区域会产生异常?
①、如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError 异常
②、如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常

/** * 虚拟机栈和本地方法栈 OOM 测试  * VM Args: -Xss128k * 设置栈内存容量为:128k * @author dolphinzhou * */public class JavaVMStackSOF {    private int stackLength = 1;    public void stackLeak() {        stackLength++;        stackLeak();    }    public static void main(String[] args) throws Throwable {        JavaVMStackSOF oom = new JavaVMStackSOF();        try {            oom.stackLeak();        } catch (Throwable e) {            System.out.println("stack length:" + oom.stackLength + "\n");            throw e;        }    }}

运行结果:
这里写图片描述

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

如果测试时不限于单线程,通过不断的建立线程的方式倒是可以产生内存溢出异常。代码如下:

/** * 创建线程导致内存溢出异常 * VM Args: -Xss2M(这时候不妨设置大些) 设置栈内存容量为:2M * @author dolphinzhou * */public class JavaVMStackOOM {    private void dontStop() {        while (true) {        }    }    public void stackLeakByThread() {        while (true) {            Thread thread = new Thread(new Runnable() {                public void run() {                    dontStop();                }            });            thread.start();        }    }    public static void main(String[] args) throws Throwable {        JavaVMStackOOM oom = new JavaVMStackOOM();        oom.stackLeakByThread();    }}

运行结果: 本人的电脑虚拟机是 64 位,就算设置了很大的栈内存容量,也无法让虚拟机产生下列异常。
Exception in thread “main” java.lang.OutOfMemoryError: unable to create new native thread

Q:为什么为每个线程的栈分配的内存越大,反而越容易产生内存溢出异常?
操作系统分配给每个进程的内存是有限制的,譬如 32 位的 windows 限制为 2GB。
剩余的内存为 2GB(操作系统限制) = Xmx(最大堆容量) + MaxPermSize(最大方法区容量) + 程序计数器(消耗内存很小,可以忽略掉)+ 虚拟机进程本身消费的内存 + 虚拟机栈和本地方法栈
所以,每个线程分配到的栈容量越大,可以建立的线程数量就越少,建立线程时就越容易把剩下的内存耗尽

Q:怎么解决这个区域产生的内存溢出?
如果使用虚拟机默认参数,栈深度在大多数情况下(因为每个方法压入栈的帧大小并不是一样的,所以只能说在大多数情况下)达到 1000~2000 完全没有问题,对于正常的方法调用(包括递归),这个深度应该完全够用了。
但如果是建立过多的线程导致的内存溢出,在不能减少线程数或者更换 64 位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。

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

Q:为什么放在一块进行溢出测试?
因为运行时常量池是方法区的一部分。

String.intern() 是一个 Native 方法,它的作用:如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回代表池中这个字符串的 String 对象;否则,将此 String 对象包含的字符串添加到常量池中,并且返回此 String 对象的引用

import java.util.ArrayList;import java.util.List;/** * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M * 限制方法区的大小,从而间接限制其中常量池的容量 * @author dolphinzhou * */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) {            list.add(String.valueOf(i++).intern());        }    }}

运行结果:JDK1.6下才会报异常
这里写图片描述

结论:
运行时常量池溢出,会报 OutOfMemoryError: PermGen space 错误,说明运行时常量池属于方法区(HotSpot 虚拟机中的永久代)的一部分。 JDK1.7 中,不会保错。

方法区用于存放 Class 的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。
对于此区域的测试思路: 运行时产生大量的类去填满方法区,直到溢出。

/** * 借助 CGLib 使方法区出现内存溢出异常 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M * @author dolphinzhou */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() {                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {                    return proxy.invokeSuper(obj, args);                }            });            enhancer.create();        }    }    static class OOMObject {    }}

运行结果:

    Exception in thread "main" java.lang.OutOfMemoryError: PermGen space      at java.lang.String.intern(Native Method)      at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18) 

方法区溢出也是一种常见的内存溢出异常。
经常生成大量 Class 的场景:程序使用了 CGLib 字节码增强和动态语言、大量 JSP 或动态产生 JSP 文件的应用、基于 OSGi 的应用等。

4.4 本机直接内存溢出

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

import java.lang.reflect.Field;import sun.misc.Unsafe;/** * VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M * @author dolphinzhou * */public class DirectMemoryOOM {    private static final int _1MB = 1024 * 1024;    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);        }    }}

这里写图片描述

阅读全文
0 0
原创粉丝点击