引子
在Java虚拟机规范的描述中,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError异常的可能。下面介绍几个实例来验证异常发生的场景,希望通过这些场景的学习,能帮助我们根据异常信息快速的判断出工作中遇到的内存溢出是哪个区域的内存溢出。
先介绍几个概念
- 内存泄露:对象已经不再使用,但由于某些原因导致对象无法被GC回收,就会造成内存泄露
- 内存溢出:内存太小无法满足应用程序对内存的需求时,就会造成内存溢出。
- -Xms :表示java虚拟机堆区内存初始内存分配的大小。
- -Xmx: 表示java虚拟机堆区内存可被分配的最大上限
Java堆溢出
Java堆用于存储对象实例,只要不断的创建对象,并保证GC Roots到对象之间有可达路径来避免对象被垃圾回收器回收,那么对象数量达到最大堆的容量限制后就会产生堆内存溢出溢出。
public static void main(String[] args) {
List<Object> list = new ArrayList<Object>();
while(true){
list.add(new Object());
}
}
运行结果就会出现OutOfMemoryError,异常堆栈信息“java.lang.OutOfMemoryError”后面会跟着进一步提示“java heap space”。要解决这个异常,一般的手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析,重点是确定内存中的对象是否是必要的,也就是要分清楚到底是内存泄露还是内存溢出。Eclipse Memory Analyzer的使用可以参照Eclipse Memory Analyzer简单使用笔记
如果是内存泄露,定位出问题后解决即可。如果是内存溢出,那就应当检查虚拟机参数(-Xmx与-Xms),与物理机内存对比看是否还可以调大,并从代码检查是否存在某些对象生命周期过长,持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
以上是处理Java堆内存问题的简单思路。
Java栈溢出
Java栈在Java虚拟机规范中描述了两种异常:
- StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的深度(如循环递归)
public static void method(){
for(;;)
method();
}
- OutOfMemoryError:当虚拟机动态扩展时无法请求到足够内存时。
public static void main(String[] args) {
List<Object> list = new ArrayList<Object>();
while(true){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while(true){
......
}
}
});
}
}
通常通过不断建立线程的方式可以产生内存溢出溢出。
如果使用虚拟机默认参数,栈深度在大多数情况下达到1000-2000完全没有问题,对于正常的方法调用(包括递归),这个深度应该完全够用了。但是,如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,可以通过减少最大堆和减少栈容量来换取更多的线程。
方法区和运行时常量池溢出
运行时常量池属于方法区的一部分。运行时常量池溢出,在OutOfMemoryError后面跟随的提示信息是“PermGen space”
方法去用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。当前很多主流框架,如spring、hibernate,在对类进行增强时,都会使用到GGLib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可以家载入内存。
本机直接内存溢出
直接内存溢出是并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常。
有DirectMemory异常的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常。