实战:OutOfMemoryError 异常
参考:《深入理解Java虚拟机》-jvm高级特性与最佳实现(周志明著)
之前的两篇中介绍Java虚拟机中各个运行时内存区域的作用,这节中通过人为异常的方式验证各个运行时区存储的内容
一、Java堆溢出
Java堆中用于存储对象的实例,所以只要不断创建对象,并且保证GC Roots到对象之间有可达路径(保证对象有引用,而不会被GC回收)来避免垃圾回收机制清除这些对象。那么在数量达到最大堆容量的限制后就会产生内存溢出。
import java.util.ArrayList;
import java.util.List;
/**
* 测试堆内存溢出
* VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\app\file\java-log\
*
* 启动参数设置Java堆大小为20m,不可扩展,通过设置-XX:HeapDumpOnOutOfMemoryError 参数
* 可以让虚拟机在出现内存溢出时Dump出当前的内存堆转存储快照
*/
public class HeapOOM {
static class OOMObject{
}
public static void main(String[] args) {
// list 引用保证GC Roots 到对象之间有可达路径
List<OOMObject> list = new ArrayList<OOMObject>();
while (true){
list.add( new OOMObject());
System.out.println("list中添加了"+list.size()+"个对象");
}
}
}
// 异常堆栈
……
list中添加了810325个对象
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid7576.hprof ...
Heap dump file created [28502312 bytes in 0.088 secs]
Disconnected from the target VM, address: '127.0.0.1:53242', transport: 'socket'
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:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at com.xiaozhameng.jvm.HeapOOM.main(HeapOOM.java:21)
当出现Java堆内存溢出时,异常堆栈信息java.lang.OutOfMemoryError: 会跟着进一步提示:Java heap space
要解决这个区域的异常,一般的手段是先通过内存映象分析工具对Dump出来的堆转存储快照进行分析,重点是要确认堆内存中的对象是否是必要的,也就是要确定到底出现了内存泄漏还是内存溢出
可以使用堆转存快照分析工具(如MAT)进行分析,定位内存溢出的问题所在,这里不再赘述,后面有时间再罗列
二、虚拟机栈和本地方法栈溢出
1、如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverFlowError 异常
2、如果虚拟机在扩展时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
对于第一点,可以通过设置大量的局部变量,增大本方法中的本地变量表的长度,最终抛出StackOverFlowError。
可以通过尝试不断创建线程的方式使得虚拟机栈发生OutOfMemoryError异常,但是这样产生的内存溢出与栈空间是否足够大没有任何联系,或者更加准确地说,在这种情况下,为每个线程的栈分配的内存越大,反而越容易发生内存溢出。
其原因是操作系统分配给每个进程的内存时有限制的,虚拟机提供了参数来控制Java堆和方法区的这两部分内存的最大值。剩余的内存为操作系统内存减去Xmx(最大堆内存),再减去最大方法区容量(MaxPermSize),程序计数器的内存消耗很小,可以忽略。如果虚拟机进程耗费的内存不算在内。剩下的内存就由虚拟机栈和本地方法栈瓜分,每个线程分配到的容量越大,可以建立的线程数就越少。
这一点需要特别注意,如果是因为建立过多线程导致内存栈内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆内存。或者减少栈容量来获取更多的线程。
package com.xiaozhameng.jvm;
import org.junit.Test;
/**
* Java 虚拟机栈和本地方法栈溢出测试
*
* 在HotSpot虚拟机中,虚拟机栈和本地方法栈合二为一,因此对于HotSpot虚拟来说,栈容量的大小设置只取决于 -Xss参数设置,关
* 于虚拟机栈和本地方法栈,Java虚拟机规范中定义了两种异常
* 1、如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverFlowError 异常
* 2、如果虚拟机在扩展时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
*/
public class JavaVMStackOFE {
private int stackLenth = 1;
/**
* VM Agars : -Xss128k
* 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverFlowError 异常
*
* ---------- 异常堆栈
* 方法中已经有935个局部变量
* Disconnected from the target VM, address: '127.0.0.1:57839', transport: 'socket'
*
* java.lang.StackOverflowError
* at sun.nio.cs.UTF_8.updatePositions(UTF_8.java:77)
* at sun.nio.cs.UTF_8.access$200(UTF_8.java:57)
* at sun.nio.cs.UTF_8$Encoder.encodeArrayLoop(UTF_8.java:636)
* at sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691)
* at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579)
* at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271)
* at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)
* at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207)
* at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129)
* at java.io.PrintStream.write(PrintStream.java:526)
* at java.io.PrintStream.print(PrintStream.java:669)
* at java.io.PrintStream.println(PrintStream.java:806)
* at com.xiaozhameng.jvm.JavaVMStackOFE.testStack_StackOverFlowError(JavaVMStackOFE.java:24)
*
*/
@Test
public void testStack_StackOverFlowError(){
stackLenth ++;
System.out.println("方法中已经有"+stackLenth+"个局部变量");
testStack_StackOverFlowError();
}
/**
* 空方法
*/
private void dontStop(){
while (true){
}
}
/**
* 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverFlowError 异常
*/
public void testStack_OutOfMemoryError(){
while (true){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
/**
* 执行该方法有较大的风险,可能会导致操作系统假死
* @param args
*/
public static void main(String[] args) {
new JavaVMStackOFE().testStack_OutOfMemoryError();
}
}
三、方法区和运行时常量池溢出
方法区存放的Class的相关信息,如类名,访问修饰符,常量池,字段描述,方法描述等。关于这个区域的测试,思路是运行时产生大量的类去填满方法区
运行时常量池属于方法区的一部分,关于这个区域的测试,可以借助String.intern() 方法,它的作用是,如果字符串常量池中已经包含此字符串,则返回常量池中这个字符串对象;否则将此String字符串包含的对象添加到常量池中并返回对象的引用。
四、本机直接内存溢出:DirectMemory容量的设置可以通过-XX:MaxDirectMemorySize 设定,若不指定,则默认跟堆最大内存一直。