在Java虚拟机规范的描述中,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能
一、Java堆溢出
垃圾回收的标准是:GC Roots到对象之间没有可达路径即可回收。当对象数量到达堆所能容下的最大限制后就会产生内存溢出异常。
代码测试如下:
/**
* VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
打印如下:
注:-Xms20m -Xmx20m :将堆的最小值和最大值设置为一样即可避免堆自动扩展,-XX:+HeapDumpOnOutOfMemoryError:让虚拟机在出现内存溢出时异常时Dump出当前内存堆转储快照以便事后进行分析(如使用的是idea,现安装JProfiler插件)。
如果只观察程序发生Minor GC和Full GC的次数以及时常,加载类数量、线程数量等简单数据,可以使用jconsole,使用方式如下:
分析:要解决这个区域的异常,可以通过内存映像分析工具,如profiler,对dump出来的堆转储快照进行分析先分清楚是内存泄漏还是内存溢出
1. 内存泄漏
通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象与GC Roots关联从而垃圾回收器无法回收该对象的原因,掌握泄漏对象的类型信息,以及GC Roots引用链的信息,从而定位到泄露代码的位置。
例如:这三行代码就会导致内存泄漏
While(true){
HashMap<String, String> hashMap = new HashMap<String, String>();
list.add(hashMap);
hashMap=null;
}
2. 内存溢出
也就是说这些对象必须存活,应从三个方面考虑:分配的内存是否太小、从代码分析是否存在某些对象生命周期过长,持有状态时间过长的问题、创建的对象是否过多等
(1)若内存太小:可通过-Xms 与-Xmx调大内存的上限和下限(这要看是否有可用物理内存)
(2)对象生命周期过长:在对象不用的时候及时销毁,例如hashmap用完即毁不要占着内存活到代码结束。
(3)若对象太多:尽量使用单例模式,不要过多创建对象,否则既浪费资源又给垃圾回收造成压力。
二、虚拟机栈与本地方法栈溢出
HotSpot是不区分虚拟机栈和本地方法栈的,所以设置本地方法栈的-Xoss参数是不起作用的,栈容量由-Xss参数设置
Java虚拟机规范中描述了两种异常:
(1)如果线程请求的栈深度大于虚拟机所允许的最大深度,会抛出StackOverflowError异常。
(2)如果虚拟机在扩展栈时无法申请到足够的内存空间,会抛出OutOfMemoryError异常
但是以单线程为例:使用-Xss参数减少栈内存容量,抛出StackOverflowError异常。定义大量的本地变量,增加方法帧中本地方变量表的长度,同样会抛出StackOverflowError异常。两者异常输出时栈深度相应缩小。测试代码如下:
/**
* VM Args:-Xss128k
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
运行结果如下:
这说明当在单线程下,无论是由于栈帧太大,还是虚拟机栈容量太小,内存无法分配,虚拟机抛出的都是StackOverflowError异常
实际上操作系统分配给每个进程的内存是有限制的,以win32为例,
虚拟机栈和本地方法栈内存=2G内存(操作系统限制)-Xmx(最大堆内存容量)-MaxPermSize(最大方法区容量)-计数器消耗内存 ,计数器消耗内存很小可以忽略。
由这个公式可得,每个线程分配的内存越大,可建立的线程越少。如果使用虚拟机默认参数,栈深度大多数情况下可达1000~2000,符合正常情况下使用,若建立过多线程导致内存溢出,可通过减少最大堆内存和栈容量解决。堆线程数量要求不大的情况下可以减少线程数。
三、本机直接内存溢出
可直接内存通过-XX:MaxDirectMemorySize指定,默认与java堆的最大值(-Xmx指定)一样。使用DirectByteBuffer分配内存时,它会现通过计算判断内存是否够用,若够用就会向操作系统申请分配内存,否则也会抛出内存溢出异常 。真正申请分配内存的方法是unsafe.allocateMemory()。
学习资料:《深入理解Java虚拟机:JVM高级特性与最佳实践》