《深入理解java虚拟机》——OutOfMemoryError异常

本节内容的目的:1.通过代码验证Java虚拟机规范中描述的各个运行时区域存储的内容;2.遇到实际的内存溢出异常时,能根据异常的信息快速判断是哪个区域的内存溢出,知道什么样的代码可能会导致这些区域内存溢出,以及出现这些异常后该如何处理。

一、前期准备

1.JVM参数的设置:eclipse.init中

2.JVM各种参数所表示的意义:https://blog.csdn.net/hua00shao/article/details/78258169

二、不同的溢出情况

1.Java堆溢出

(1)、原因:Java堆用于存储实例对象,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么对象数量在达到最大堆的容量限制后就会产生内存溢出的现象。

(2)、测试代码:

import java.awt.List;
import java.util.ArrayList;


public class HeapOOM {
	static class OOMObject{
	}
	
	public static void main(String[] args){
		ArrayList<OOMObject> list=new ArrayList<OOMObject>();
		while(true){
                        //不断地生成新对象
			list.add(new OOMObject());
		}
	}
}

(3)、参数设置

-Xms80m
-Xmx80m
-XX:+HeapDumpOnOutOfMemoryError

(4)、运行结果

(5)、利用Eclipse Memory Analyzer进行内存分析

重点是确认是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)

如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Root 的引用链。于是就能找泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息及GC Root ,就可以比较准确地定位出泄漏代码的位置

如果不存在泄漏,也就是说,内存中的对象确实都必须活着,处理思路如下

A、应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大。

B、从代码上检查是否存在某些对象生命周期过长,持有装填时间过长的情况,尝试减少程序运行期的内存消耗。

2.虚拟机栈和本地方法栈溢出

(1)、原因:Java虚拟机栈中规定了两种溢出情况:

A、如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常;

B、如果虚拟机在扩展栈时无法申请到足够的内存空间 ,则抛出OutOfMemoryError异常;

(这两个异常本质上是同一个异常。因为在JVM的参数设置中只有-Xss这一个参数是设置栈区大小的,也就是它并没有额外指定单个线程所能使用的最大栈容量。换句话说,虚拟机所允许的最大栈深度就是虚拟机当前剩余的栈区大小。那么当出现栈内存异常的时候,你即可以说它是由于线程请求的栈深度大于虚拟机所允许的最大深度而引起的异常;也可以说是虚拟机在扩展栈时无法申请待足够的内存空间而引起的异常。)

(2)、测试一——通过不断递归调用本身来无限扩展栈区,从而引起栈溢出异常

测试代码:

package outofmemory;

public class JVMStackSOF {
	private int stackLength=1;
	
	//通过无限递归一直扩展栈的大小
	public void stackLeak(){
		stackLength++;
		stackLeak();
	}
	public static void main(String[] args) throws Throwable {
		JVMStackSOF oom=new JVMStackSOF();
		try{
			oom.stackLeak();
		}catch(Throwable e){
			System.out.print("stack length:"+oom.stackLength);
			//把异常抛给JVM
			throw e;
		}
	}
}

参数设置:-Xss128k

运行结果:

分析:

实验结果表明在单个线程的情况下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError。如果测试不限于单线程,通过不断地建立线程的方法倒是可以导致产生内存溢出的异常。但是这样产生的内存溢出异常和栈空间是否足够大并不存在任何联系。接下来我们来看测试二。

(3)、测试二——通过不断建立线程来引起栈溢出异常

测试代码:

package outofmemory;

public class JavaVMStackOOM {
	public void dontStop(){
		while(true){		
		}
	}
	public void stackLeakByThread(){
		while(true){
			//建立一个新线程
			Thread thread=new Thread(new Runnable(){
				public void run() {
					// TODO Auto-generated method stub
					dontStop();
				}
			});
			//开始进程
			thread.start();
		}
	}
	public static void main(String[] args){
		JavaVMStackOOM oom=new JavaVMStackOOM();
		oom.stackLeakByThread();
	}
}

结果:无法再创建新的线程。

分析:操作系统分配给每个线程的内存是有限制的,32位的windows限制为2GB。剩余的内存2GB减去Xmx(最大堆容量),再减去MaxPermSize(最大方法区容量),程序计数器消耗的内存很小,可以忽略掉。如果虚拟机继承本身消耗的内存忽略不计,那么剩下的就由虚拟机方法栈和本地方法栈瓜分了。每个线程分配到的栈容量越大,可以建立的线程数自然就越少。

栈深度在大多数情况下达到1000-2000完全没有问题,对于正常的方法调用(包括递归),这个深度是完全够用的。如果在建立多线程的过程中导致的内存溢出,解决方案:A、减少线程数;B、更换64位虚拟机;C、通过减少最大堆和较少栈容量来换取更多的线程数。

3.方法区和运行时常量区溢出

由于运行时常量区是方法区的一部分,因此这两个区域的溢出常常放在一起讲。

(1)、测试一

测试代码:

package outofmemory;

public class RuntimeConstantPoolOOM2 {
	public static void main(String[] args){
		String str1=new StringBuilder("计算机").append("软件").toString();
		System.out.println(str1.intern()==str1);
		
		String str2=new StringBuilder("ja").append("va").toString();
		System.out.println(str2.intern()==str2);
	}
}

运行结果:

分析:在JDK1.6会得到两个false,而在JDK1.7中会得到一个true和一个false。原因如下:在JDK1.6中,intern()方法会把首次遇到的字符串实例赋值到永久代中,返回的也是永久代中这个字符串实例的一个引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是同一个引用,返回false。而JDK1.7的intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。对str2返回false是因为“java”这个字符串在执行StringBuilder之前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串则是首次出现的,因此返回true。

(二)、测试二

方法区溢出原因:方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。对于这些区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。当前的很多驻留框架,如Spring、Hibernate,在对类进行增强时,都会用到CGLib这类字节码,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载如内存。另外,JVM上的动态语言通常都会持续创建类来实现语言的动态性,随着这类语言的流行,也越来越容易遇到这种问题。

方法区溢出也是一种常见的内存溢出异常,一个类别要被垃圾收集器回收掉,判定条件是比较苛刻的。在经常动态生成大量Class的应用中,需要特别注意类的回收状况。这类场景有:A、程序使用了CGLib字节码增强类;B、动态语言;C、大量JSP或动态产生JSP文件的应用;D、基于OSGit的应用。

4.本机直接内存溢出

DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正地去向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请内存的方法是unsafe.allocateMemory()。

由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会有明显的异常,如果读者发现OOM之后Dump文件很小,而程序中又直接或间接地使用了NIO,那就可以考虑一下是不是这方面的原因。

博客内容来自《深入理解Java虚拟机》。

猜你喜欢

转载自blog.csdn.net/Alexwym/article/details/81774536
今日推荐