前言:
通过这篇博文,我们可以学习到:第一,通过代码验证Java 虚拟机规范中描述的各个运行时区域存储的内容;第二,希望我们在工作中遇到实际的内存溢出异常时,能根据异常的信息快速判断是哪个区域的内存溢出,知道什么样的代码可能会导致这些区域内存溢出,以及出现这些异常后该如何处理。
Tips:
下文代码的开头都注释了执行时所需要设置的虚拟机启动参数(注释中“VM Args” 后面跟着的参数),这些参数对实验的结果有直接的影响,读者调试代码的时候千万不要忽略,如果读者使用控制台命令来执行程序,那直接跟在java命令之后书写就可以。如果读者使用 IntelliJ,可参考下图配置:
Java 堆溢出:
代码清单如下:
package com.alipay.functest.aaa_test;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName HeapOOM
* @Description 通过设定不同的 VM arguments来测试各个运行时区域的内存溢出异常
* VM Args: -Xms20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError
* @Author 文竹
* @Date 2019/8/22 14:50
* @Version 1.0
**/
public class HeapOOM{
static class OOMObject{}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true){
list.add(new OOMObject());
}
}
}
执行结果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid120800.hprof ...
Heap dump file created [2314735266 bytes in 16.550 secs]
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.CommandLineWrapper.main(CommandLineWrapper.java:67)
Caused by: 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.alipay.functest.aaa_test.HeapOOM.main(HeapOOM.java:24)
... 5 more
Process finished with exit code 1
java 堆内存的OOM 异常是实际应用中常见的内存溢出异常情况。当出现Java 堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。
要解决这个区域的异常,一般的手段是先通过内存映像分析工具(如 Eclipse Memory Analyzer)对 Dump 出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow).
堆转储文件:
如果内存泄漏,可进一步通过工具查看泄漏对象到GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots 相关联并导致垃圾收集器无法自动回收他们的。掌握了泄漏对象的类型信息及GC Roots 引用链的信息,就可以比较准确的定位出泄漏代码的位置。
如果不存在泄漏,换句话说,就是内存中的对象却是都还必须存活着,那就应该检查虚拟机的堆参数(-Xmx 与 -Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否在某些对象生命周期过长,持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
https://blog.youkuaiyun.com/u013066244/article/details/82356260(使用 Eclipse Memory Analyzer 进行堆转储文件分析)
虚拟机栈和本地方法栈溢出:
由于在HotSpot 虚拟机中并不区分虚拟机栈和本地方法栈,因此,对于HotSpot 来说,虽然 -Xoss参数(设置本地方法栈大小)存在,但实际上是无效对的,栈容量只由 -Xss 参数设定。关于虚拟机栈和本地方法栈,在java虚拟机规范中描述了两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将跑出 StackOverflowError 异常。
- 如果虚拟机在扩展栈时无法申请到足够的内存空间,则跑出OutOfMemoryError 异常。
这里把异常分成两种情况,看似更加严谨,但却存在着一些相互重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已。
限于篇幅太长,这里就不展开了,可参考《深入理解 Java 虚拟机》第二版, 第二章 Java 内存区域与内存溢出异常,2.4.2 虚拟机栈和本地方法栈溢出相关内容。