一、java堆溢出
import java.util.ArrayList;
import java.util.List;
public class HeapOOM {
static class OOMobject{}
public static void main(String[] args) {
List<OOMobject> list = new ArrayList<HeapOOM.OOMobject>();
while(true){
list.add(new OOMobject());
}
}
}
上述代码将会产生下面的异常现象:
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 cn.xugui.HeapOOM.main(HeapOOM.java:11)
上面就是堆内存溢出异常。解决办法:
1.-XX:HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常的时候Dump内存堆转存快照;
2.通过工具分析上述中的堆转存快照;
3.主要分辨出堆内存中的对象是否是需要的,也就是分辨出上述异常到底是内存泄漏还是内存溢出,前者中的对象是不需要的,而后者中的对象是需要的;
如果是内存泄漏,需要进一步查看泄漏对象到GC Roots的引用链,了解GC Roots的对象是通过怎么样的路径和泄漏对象相互关联而导致GC 无法对其进行回收;如果是内存溢出,则纯粹是对内存不够,需要检查参数-xms和-xmx(设置的堆内存的最小值和最大值),需要和实际的物理内存比较之后,决定是否可以调大,再者,从代码上检测是否存在某些对象的生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗(尝试回收这些对象);
二、虚拟机栈和本地方法栈的溢出
该区域会产生两种类型的异常,描述如下:
第一种,如果需要申请栈的深度大于栈的深度,statckOverFlow异常;
第二种,如果栈扩展时无法申请到足够的内存空间,则会报出OutOfMemory异常。
第一种异常的代码:
public class StackOF {
private static int stackLength = 1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) {
StackOF stackOF = new StackOF();
try{
stackOF.stackLeak();
}catch (Exception e) {
System.out.println("stackLength:"+stackLength);
}
}
}
结果:
Exception in thread "main" java.lang.StackOverflowError
at cn.xugui.StackOF.stackLeak(StackOF.java:6)
at cn.xugui.StackOF.stackLeak(StackOF.java:7)
at cn.xugui.StackOF.stackLeak(StackOF.java:7)
at cn.xugui.StackOF.stackLeak(StackOF.java:7)
at cn.xugui.StackOF.stackLeak(StackOF.java:7)
at cn.xugui.StackOF.stackLeak(StackOF.java:7)
at cn.xugui.StackOF.stackLeak(StackOF.java:7)
at cn.xugui.StackOF.stackLeak(StackOF.java:7)
......
在多线程工作下会发生OOM异常(但是不是上述第二种锁表达的含义)
-xss(栈内存的大小,在Hotspot中不区分java虚拟机栈和本地方法栈的)越大,反而更加容易发生OOM异常,原因?
首先操作系统分配给每个进程的内存是有限制的,所以我们可以得到用户线程所能得到的内存为:操作系统分配给JVM进程的总内存 - Xmx(最大堆容量) - MaxPermSize(方法区容量) - 程序计数内存(忽略不计) - 虚拟机本身内存;所以当虚拟机栈的内存越大,线程所能拥有的内存越少,建立线程的数量会变少,建立线程也越容易把剩余的内存消耗殆尽,当线程内存不够的时候,将会产生OOM异常。
三、方法区和运行时常量池的溢出
首先需要解释的是:方法区和永久代的关系,参考https://www.jianshu.com/p/66e4e64ff278
总得来说,在Hotspot中永久代就是对方法区的一种实现。但是,在jdk1.8中,取消了永久代的概念,改为元空间,元空间存储在元空间,元空间被存储在与堆内存不连续的本地内存区域。所以,不会出现向永久代那样的内存溢出问题。而且,运行时常量池在jdk1.7之后被从方法区中剥离出来,放入堆中,因为方法区会因为大量字符串的存在而产生OOM异常。
String string = "12"; //常量,位于常量池中
System.out.println(string.intern() == string);
//结果:true
String str = new String("java"); //对象,是在java堆中
System.out.println(str.intern() == str);
//结果:false
我们需要理解常量池的本质,其实常量池就是java提供的一种缓存机制,而字符串常量池是一类的特殊的常量池。它主要提供的方法有两种:
其一是:通过双引号生成的字符串对象,直接被放入常量池中。
其二是:通过String提供的intern方法,如果该对象在常量池中已经存在则直接返回常量池中字符串的引用,反之不存在,则需要将字符串复制到常量池中,返回该常量池中字符串的引用。
注意:上述代码String str = new String("java");如果在字符串常量池中还没有“java”字符串变量时,该代码将会创建两个对象,一个对象是字符串常量池中的“java”对象,一个则是堆中的对象。
查看String的构造方法:
public String(String original) {
int size = original.count;
char[] originalValue = original.value; //这个字符数组用于存储的
char[] v;
if (originalValue.length > size) {//如果存储数组长度大于实际存放的数组长度
// The array representing the String is bigger than the new
// String itself. Perhaps this constructor is being called
// in order to trim the baggage, so make a copy of the array.
int off = original.offset;
v = Arrays.copyOfRange(originalValue, off, off+size);
} else {
// The array representing the String is the same
// size as the String, so no point in making a copy.
v = originalValue;
}
this.offset = 0;
this.count = size;
this.value = v;
}
其中Original数组就是字符串常量池中的字符串。
参考:https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html