2.4.实战:OutOfMemoryError异常
2.4.1.Java堆溢出
要解决这个区域的异常,一般的手段是首先通过内存映像分析工具(如 Eclipse Memory Analyzer)对 dump 出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
如果是内存泄漏,可进一步通过工具查看泄漏对象到 GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与 GC Roots 相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及 GC Roots 引用链的信息,就可以比较准确地定位出泄漏代码的位置。
如果不存在泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx 与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
内存泄漏是指分配出去的内存无法回收了。
内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况,是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存溢出是指程序要求的内存,超出了系统所能分配的范围,从而发生溢出。
内存溢是指在一个域中输入的数据超过它的要求而且没有对此作出处理引发的数据溢出问题,多余的数据就可以作为指令在计算机上运行。
import java.util.ArrayList;
import java.util.List;
/**
*
* 项目名称:Test
* 类名称:HeapTest
* 类描述:VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* 创建人:YinXiangBing
* 创建时间:2014-4-28 下午03:35:58
* @version 6.0
*
*/
public class HeapTest {
static class TestObject {
}
public static void main(String[] args) {
List<TestObject> list = new ArrayList<TestObject>();
while (true) {
list.add(new TestObject());
}
}
}
输出的结果:
[GC 8192K->4624K(19456K), 0.0188411 secs]
[GC 9837K->9747K(19456K), 0.0322771 secs]
[Full GC 16304K->11928K(19456K), 0.0516129 secs]
[Full GC 19456K->15528K(19456K), 0.0537815 secs]
[Full GC 17819K->17819K(19456K), 0.0547349 secs]
[Full GC 17819K->17806K(19456K), 0.0600322 secs]
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid4752.hprof ...
Heap dump file created [35317950 bytes in 0.454 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2760)
at java.util.Arrays.copyOf(Arrays.java:2734)
at java.util.ArrayList.ensureCapacity(ArrayList.java:167)
at java.util.ArrayList.add(ArrayList.java:351)
at HeapTest.main(HeapTest.java:20)
可以看出内存泄露了
2.4.2.虚拟机栈和本地方法栈溢出
由于在 HotSpot 虚拟机中并不区分虚拟机栈和本地方法栈,因此对于 HotSpot 来说,-Xoss 参数(设置本地方法栈大小)虽然存在,但实际上是无效的,栈容量只由-Xss 参数设定。关于虚拟机栈和本地方法栈,在 Java 虚拟机规范中描述了两种异常:
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError 异常。
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常。
/**
*
* 项目名称:Test
* 类名称:JavaVMStackTest
* 类描述: VM Args:-Xss128k
* 创建人:YinXiangBing
* 创建时间:2014-4-28 下午04:11:58
* @version 6.0
*
*/
public class JavaVMStackTest {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
JavaVMStackTest jvst = new JavaVMStackTest();
try {
jvst.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + jvst.stackLength);
throw e;
}
}
}
Exception in thread "main" stack length:2401
java.lang.StackOverflowError
at JavaVMStackTest.stackLeak(JavaVMStackTest.java:14)
at JavaVMStackTest.stackLeak(JavaVMStackTest.java:15)
at JavaVMStackTest.stackLeak(JavaVMStackTest.java:15)
at JavaVMStackTest.stackLeak(JavaVMStackTest.java:15)
at JavaVMStackTest.stackLeak(JavaVMStackTest.java:15)
at JavaVMStackTest.stackLeak(JavaVMStackTest.java:15)
实验结果表明:在单个线程下,无论是由于栈帧太大,还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是 StackOverflowError 异常。
/**
*
* 项目名称:Test
* 类名称:JavaVMStackTest
* 类描述: VM Args:-Xss128k
* 创建人:YinXiangBing
* 创建时间:2014-4-28 下午04:11:58
* @version 6.0
*
*/
public class JavaVMStackTest1 {
private void dontStop(){
while(true){
}
}
public void atackLeakByThread(){
while(true){
Thread thread = new Thread(new Runnable(){
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args )throws Throwable{
JavaVMStackTest1 jvst = new JavaVMStackTest1();
jvst.atackLeakByThread();
}
}
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
内存溢出异常与栈空间是否足够大并不存在任何联系,或者准确地说,在这种情况下,给每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。
原因:操作系统分配给每个进程的内存是有限制的,譬如32位的Windows限制为2GB。虚拟机提供了参数来控制 Java 堆和方法区的这两部分内存的最大值。剩余的内存为 2GB(操作系统限制)减去 Xmx(最大堆容量),再减去 MaxPermSize(最大方法区容量),程序计数器消耗内存很小,可以忽略掉。如果虚拟机进程本身耗费的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈“瓜分”了。每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。
建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。
2.4.3.运行时常量池溢出
如果要向运行时常量池中添加内容,最简单的做法就是使用 String.intern()这个 Native 方法。该方法的作用是:如果池中已经包含一个等于此 String 对象的字符串,则返回代表池中这个字符串的String 对象;否则,将此 String 对象包含的字符串添加到常量池中,并且返回此 String 对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize 和-XX:MaxPermSize 限制方法区的大小,从而间接限制其中常量池的容量。
import java.util.ArrayList;
import java.util.List;
/**
*
* 项目名称:Test
* 类名称:RuntimeConstantPoolTest
* 类描述:VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
* 创建人:YinXiangBing
* 创建时间:2014-4-28 下午04:57:47
* @version 6.0
*
*/
public class RuntimeConstantPoolTest {
public static void main(String[] args) {
// 使用 List 保持着常量池引用,避免 Full GC 回收常量池行为
List<String> list = new ArrayList<String>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at RuntimeConstantPoolTest.main(RuntimeConstantPoolTest.java:21)
从运行结果中可以看到,运行时常量池溢出,在 OutOfMemoryError 后面跟随的提示信息是“PermGen space”,说明运行时常量池属于方法区(HotSpot 虚拟机中的永久代)的一部分。
2.4.4.方法区溢出
方法区用于存放 Class 的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。
基本的思路是运行时产生大量的类去填满方法区,直到溢出。
使用JavaSE API 也可以动态产生类(如反射时的 GeneratedConstructorAccessor 和动态代理等)
CGLib直接操作字节码运行时,生成了大量的动态类。
如 Spring 和 Hibernate 对类进行增强时,都会使用到 CGLib 这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的 Class 可以加载入内存。
CGLIB(Code Generation Library)是一个开源项目!是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。Hibernate用它来实现PO(Persistant Object 持久化对象)字节码的动态生成。
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
/**
*
* 项目名称:Test
* 类名称:JavaMethodAreaTest
* 类描述:VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
* 创建人:YinXiangBing
* 创建时间:2014-4-29 下午04:57:55
* @version 6.0
*
*/
public class JavaMethodAreaTest {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TestObject.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 TestObject {
}
}
运行结果:
Caused by: java.lang.OutOfMemoryError: PermGen space
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
... 8 more
CLib 字节码应用到:大量JSP 或动态产生JSP 文件的应用(JSP 第一 次运行时需要编译为Java 类)、基于OSGi 的应用(即使是同一个类文件,被不同的加载器加载也 会视为不同的类)等。
2.4.5.本机直接内存溢出
DirectMemory 容量可通过-XX:MaxDirectMemorySize 指定,如果不指定,则默认与Java 堆的最大值(-Xmx 指定)一样。
按照jdk文档的官方说法,内存映射文件属于JVM中的直接缓冲区,还可以通过 ByteBuffer.allocateDirect() ,即DirectMemory的方式来创建直接缓冲区。他们相比基础的 IO操作来说就是少了中间缓冲区的数据拷贝开销。同时他们属于JVM堆外内存,不受JVM堆内存大小的限制。
其中,DirectMemory 默认的大小是等同于JVM最大堆,理论上说受限于进程的虚拟地址空间大小,比如 32位的windows上,每个进程有4G的虚拟空间除去 2G为OS内核保留外,再减去 JVM堆的最大值,剩余的才是DirectMemory大小。通过设置 JVM参数 -Xmx64M,即JVM最大堆为64M,然后执行以下程序可以证明DirectMemory不受JVM堆大小控制。
关于 JVM堆大小的设置是不受限于物理内存,而是受限于虚拟内存空间大小,理论上来说是进程的虚拟地址空间大小,但是实际上我们的虚拟内存空间是有限制的,一 般windows上默认在C盘,大小为物理内存的2倍左右。我做了个实验:我机子是 64位的win7,那么理论上说进程虚拟空间是几乎无限大,物理内存为4G,而我设置 -Xms5000M, 即在启动JAVA程序的时候一次性申请到超过物理内存大小的5000M内存,程序正常启动,而当我加到 -Xms8000M的时候就报OOM错误了,然后我修改增加 win7的虚拟内存,程序又正常启动了,说明 -Xms 受限于虚拟内存的大小。我设置-Xms5000M,即超过了4G物理内存,并在一个死循环中不断创建对象,并保证不会被GC回收。程序运行一会后整个电脑 几乎死机状态,即卡住了,反映很慢很慢,推测是发生了系统颠簸,即频繁的页面调度置换导致,说明 -Xms -Xmx不是局限于物理内存的大小,而是综合虚拟内存了,JVM会根据电脑虚拟内存的设置来控制。
MaxDirectMemorySize 与 NIO direct memory 的默认上限
-XX:MaxDirectMemorySize 是用来配置NIO direct memory上限用的VM参数。
C++代码
product(intx, MaxDirectMemorySize, -1, \
"Maximum total size of NIO direct-buffer allocations") \
但如果不配置它的话,direct memory默认最多能申请多少内存呢?这个参数默认值是-1,显然不是一个“有效值”。所以真正的默认值肯定是从别的地方来的。
当MaxDirectMemorySize参数没被显式设置时它的值就是-1,在Java类库初始化时maxDirectMemory()被java.lang.System的静态构造器调用,走的路径就是这条:
Java代码
if (s.equals("-1")) {
// -XX:MaxDirectMemorySize not given, take default
directMemory = Runtime.getRuntime().maxMemory();
}
而Runtime.maxMemory()在HotSpot VM里的实现是:
C++代码
JVM_ENTRY_NO_ENV(jlong, JVM_MaxMemory(void))
JVMWrapper("JVM_MaxMemory");
size_t n = Universe::heap()->max_capacity();
return convert_size_t_to_jlong(n);
JVM_END
这个max_capacity()实际返回的是 -Xmx减去一个survivor space的预留大小(G1除外)。
结论:MaxDirectMemorySize没显式配置的时候,NIO direct memory可申请的空间的上限就是-Xmx减去一个survivor space的预留大小。
-verbose:gc 与 -XX:+PrintGCDetails
经常能看到在推荐的标准参数里这两个参数一起出现。实际上它们有啥关系?
在Oracle/Sun JDK 6里,"java"这个启动程序遇到"-verbosegc"会将其转换为"-verbose:gc",将启动参数传给HotSpot VM后,HotSpot VM遇到"-verbose:gc"则会当作"-XX:+PrintGC"来处理。也就是说 -verbosegc、-verbose:gc、-XX:+PrintGC 三者的作用是完全一样的。
而当HotSpot VM遇到 -XX:+PrintGCDetails 参数时,会顺带把 -XX:+PrintGC 给设置上。
也就是说 -XX:+PrintGCDetails 包含 -XX:+PrintGC,进而也就包含 -verbose:gc。
既然 -verbose:gc 都被包含了,何必在命令行参数里显式设置它呢?
import java.lang.reflect.Field;
import sun.misc.Unsafe;
/**
*
* 项目名称:Test
* 类名称:DirectMemoryTest
* 类描述:VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
* 创建人:YinXiangBing
* 创建时间:2014-4-29 下午05:18:50
* @version 6.0
*
*/
public class DirectMemoryTest {
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);
}
}
}
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at DirectMemoryTest.main(DirectMemoryTest.java:27)