熟悉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”菜单
第二步:选择“VM Options”选项,输入你要设置的VM参数
输入参数内容为:
-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限制方法区的大小,从而间接限制其中常量池的容量。
下面将展示代码实例,在运行代码之前同样需要设置虚拟机参数,如下:
代码如下:
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