在java虚拟机的规范描述中,除了程序计数器外虚拟机内存的其他几个运行时区域都会发生OutOfMemoryError异常的可能。在Java语言里,可作为GC Roots对象的包括如下几种:
a.虚拟机栈(栈桢中的本地变量表)中的引用的对象
b.方法区中的类静态属性引用的对象
c.方法区中的常量引用的对象
d.本地方法栈中JNI的引用的对象
一、java堆溢出
该区用于存储对象的实例,只要不断的创建对象并且保证GC Roots到对象之间有可达路径来避免垃圾回收清除这些对象,在对象数量达到最大堆的容量限制后就会产生内存溢出,堆中的OOM异常是实际中最常见的内存溢出异常。对于这个区域的异常关键在于确认内存中的对象是否是必要的,要分清是内存泄漏还是内存溢出
1、内存泄漏memory leak:是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),或者申请到的那块内存你自己也不能再访问,而系统也不能再次将它分配给需要的程序。就相当于你租了个带钥匙的柜子,你存完东西之后把柜子锁上之后,把钥匙丢了或者没有将钥匙还回去,那么结果就是这个柜子将无法供给任何人使用,也无法被垃圾回收器回收,因为找不到他的任何信息。
2、内存溢出 out of memory:指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。 一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出。比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出。一般的原因是加载对象过大和相应资源和对象过多来不及回收释放或不能释放。
3、二者的关系:内存泄漏的堆积最终会导致内存溢出,内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误。
4、内存泄漏的分类(按发生方式来分类):
1.常发性内存泄漏:发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
2.偶发性内存泄漏:发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
3.一次性内存泄漏:发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
4.隐式内存泄漏:程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
常规的处理方式是通过内存映像分析工具对堆转储快照进行分析(转储文件又叫dump文件,是进程某一时刻的快照包含了该时间点程序的内存快照):
第一步确认内存中导致OOM的对象是否是必要的,也就是确认是内存泄漏还是内存溢出
第二步如果是内存泄漏就进一步查看泄露对象到GC Roots的引用链,找到泄露对象是通过怎样的路径与GC Roots产生关联并导致垃圾收集器无法自动回收,根据泄漏对象的类型信息以及到GC Roots引用链的信息,就可以确定出对象创建的位置。
第三步如果不是内存泄露是因为内存中的对象还必须存活,那就是内存溢出,通过调整虚拟机的堆参数,与机器物理内存对比看是否可以向上调整。或者从代码上检查是否存在对象生命期过长、持有状态时间过长,调整结构来减少内存的消耗。
(1)修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
(2)检查错误日志,查看“OutOfMemory”错误前是否有其 它异常或错误。
(3)对代码进行走查和分析,找出可能发生内存溢出的位置。
二、虚拟机栈和本地方法栈溢出
对于这两块空间java虚拟机规范描述了两种异常:
1.如果线程请求的栈深度大于虚拟机所允许的最大深度将抛出StackOverflowError异常
2.如果虚拟机在扩展栈时无法申请到足够的内存空间将抛出OutOfMemoryError异常
虽然把异常分为两种情况看似更加严谨但是却存在互相重叠的地方,HotSpot是不支持栈内存动态扩展的,除非出现创建线程申请内存时就无法获得足够的内存才会出现OutOfMemoryError异常,否则获得的都是因为栈容量不足而导致的StackOverflowError,但是这样产生的内存溢出和栈本身没有关系。
在单线程无论是因为栈帧过大还是虚拟机栈容量太小,当内存无法分配的时候虚拟机抛出的都是StackOverflowError异常。如果测试的时候不限于单线程,通过不断的建立线程的方式倒是可以产生OutOfMemoryError,在这种情况下为每个线程的栈分配的内存越大,反而越容易产生OutOfMemoryError异常。
原因就是操作系统分配给每个进程的内存是有限制的,虚拟机提供了参数来控制java堆和方法区的这两部分内存的最大值,剩余内存减去最大堆容量以及最大方法区容量,程序计数器消耗内存可以忽略并且虚拟机进程本身所占的内存不在计算之内,剩下的内存就由本地方法栈和虚拟机栈瓜分,每个线程分配到的栈容量越大可以建立的线程数量自然就越少,建立线程时就容易把剩下的内存耗尽。
栈深度在大多数情况下达到1000~2000完全没有问题,对于正常的方法调用这个深度足够使用,但是在建立多线程导致的内存溢出在不能减少线程数的情况下,就只能减少最大堆容量和减少栈容量来换取更多线程。
三、方法区和运行时常量池溢出
运行时常量池是方法区的一部分所以这两个放在一起,说到常量池就要说到一个方法那就是String.intern(),这个方法是一个Native方法它的作用是如果字符串常量池以及包含一个等于此String对象的字符串,则返回代表池中并且返回此String对象的引用。
在1.6版本之前的版本中由于常量池分配在永久代可以通过参数限制方法区大小,从而间接限制其中常量池的容量,当运行时常量池溢出时会给出PermGen space说明运行时常量池属于方法区,但是使用1.7就不会出现这种情况,因为1.7之后运行时常量池被移动到了堆里。
在将运行时常量池移动到了堆里之后,一些引用的使用也产生了变化:
public static void main(String[] args){
String str1 =new StringBulider("hhh").append("ooo").toString();
System.out.println(str1.intern()==str1);
String str2 = new StringBulider("ja").append("va").toString();
System.out.println(str2.,intern() == str2);
}
这段代码在jdk1.7和jdk1.6中运行得到的结果是不一样的,1.6版本中会得到两个false而在1.7会得到一个true和一个false。因为在1.6中intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中的这个字符串实例的引用,而由StringBulider创建的字符串实例实际在堆中,这两个具有的不是同一个引用所以将返回false。
而1.7版本的intern()的实现不会在复制实例,只是在常量池中记录首次出现的实例引用,因此intern()和StringBuilder创建的实例是一个,所以会返回true。对str2进行比较返回false的原因是java这个字符串在执行StringBulider.toString()之前出现过了,字符串常量池中已经有了它的引用不符合首次出现的原则。
方法区用于存放Class的信息,方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉判定条件比较苛刻,如果再运行的时候生成了大量动态类就需要注意类的回收情况,虽然元空间很难出现溢出异常,但是可以通过一些参数来进行预防。
(1)设置元空间最大值,默认是不限制只受限于本地内存大小
(2)指定元空间的大小当达到该值就会触发垃圾收集进行类型卸载,并且虚拟机会通过判断自动对该值进行升降、
四、本机直接内存溢出
直接内存容量可以通过-XX:MaxDirectMemorySize进行指定,如果不指定则默认与java堆最大值一致,由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会有明显的异常,如果OOM之后发现Dump文件很小而程序中又直接或者间接使用了NIO就可以考虑是不是直接内存溢出了。