OutOfMemoryError和StackOverflowError
OutOfMemoryError和StackOverflowError是什么?
- OutOfMemoryError为内存溢出,可能由内存不够或内存泄漏造成
- StackOverflowError为栈溢出,当栈容量不足以容纳新栈帧时出现
如下程序可通过eclipse-Run-Run Configurations-Java Application-Arguments-VM arguments设置虚拟机参数
堆溢出
若堆中内存已用完且无法扩展,则抛出OutOfMemoryError
如下设置堆最小、最大值为20M(不可扩展),出现OutOfMemoryError时Dump出内存堆转储快照用于分析
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
如下程序循环创建对象
public class Test {
static class OOMObject{
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while(true) {
list.add(new OOMObject());
}
}
}
会导致堆溢出
对于堆溢出,应先分析是内存泄漏还是内存溢出
- 内存泄漏:需分析泄漏对象到GC Roots的引用链,定位代码的具体位置
- 内存溢出:确认对象是否都需要存活,检查或调整堆参数设置,检查代码某些对象生命周期是否过长等
栈溢出
- 若线程请求的栈深度大于虚拟机所允许的深度,则抛出StackOverflowError
- 若栈可扩展但无法申请到足够的内存,则抛出OutOfMemoryError,Hotspot不允许扩展,无法出现这个情况
情况一
如下设置栈大小为180k
-Xss180k
通过递归不断创建栈帧入栈
public class Test {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args)throws Throwable {
Test test = new Test();
try {
test.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + test.stackLength);
throw e;
}
}
}
当栈太小,栈无法容纳新的栈帧时报错StackOverflowError
情况二
如下定义一连串变量,不断创建大栈帧
public class Test {
private static int stackLength = 0;
public static void test() {
long l1, l2, l3, l4, l5, l6, l7, l8, l9, l10,
l11, l12, l13, l14, l15, l16, l17, l18, l19, l20,
l21, l22, l23, l24, l25, l26, l27, l28, l29, l30,
l31, l32, l33, l34, l35, l36, l37, l38, l39, l40,
l41, l42, l43, l44,l45, l46, l47, l48, l49, l50;
stackLength++;
test();
l1 = l2 = l3 = l4 = l5 = l6 = l7 = l8 = l9 = l10 =
l11 = l12 = l13 = l14 = l15 = l16 = l17 = l18 = l19 = l20 =
l21 = l22 = l23 = l24 = l25 = l26 = l27 = l28 = l29 = l30 =
l31 = l32 = l33 = l34 = l35 = l36 = l37 = l38 = l39 = l40 =
l41 = l42 = l43 = l44 = l45 = l46 = l47 = l48 = l49 = l50 = 0;
}
public static void main(String[] args) {
try {
test();
} catch (Error e) {
System.out.println("stack length:" + stackLength);
throw e;
}
}
}
栈帧太大,新的栈帧无法继续分配时,Hotspot抛出StackOverflowError,若是允许动态扩展栈的虚拟机则抛出OutOfMemoryError
情况三
上面情况都是在单线程中运行的,在多线程情况下,可通过不断创建线程抛出OutOfMemoryError(但和栈空间没什么关系),原因是操作系统的剩余内存不足以分配给新的线程
public class Test {
private void dontStop(){
while (true){
}
}
public void stackLeakByThread(){
while (true){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) {
Test test = new Test();
test.stackLeakByThread();
}
}
如上代码不断创建新线程,最终将报错OutOfMemoryError(未实际运行,因为java线程直接映射到window内核线程,可能会卡死)
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread
出现这种情况时应该考虑减少线程数量、更换64位系统,或减少堆、栈容量来换取更多的线程
方法区溢出
若方法区内存已用完,则抛出OutOfMemoryError,
运行时常量池溢出
在JDK6时,运行时常量池存放在永久代,如下设置永久代大小为6M
-XX:PermSize=6M -XX:MaxPermSize=6M
通过String的intern()方法不断创建常量到常量池
public class Test {
public static void main(String[] args) {
Set<String> set =new HashSet<String>();
short i = 0;
while(true) {
set.add(String.valueOf(i++).intern());
}
}
}
最终会导致永久代溢出
在JDK7之后的JDK版本运行上面代码不会溢出,因为自JDK7后运行时常量池存放在堆中,可通过调整堆的大小重现堆中运行时常量池的OutOfMemoryError
intern()
JDK6中输出两个false,JDK6的intern()会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,故inern()返回的是永久代中的字符串引用,而StringBuilder返回的是java堆中字符串引用,两者不等
public class Test {
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);
}
}
JDK7输出true、false,JDK7的intern()返回堆中常量池记录字符串首次出现的实例引用,对于首次创建的字符串相等,非首次创建的字符串("java"字符串之前已在别的地方创建了)不等
public class Test {
public static void main(String[] args) {
System.out.println("java" == "java".intern()); //地址都为常量池
System.out.println("ja" + "va" == "java".intern()); //地址都为常量池
String str1 = new String("java");
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str1 == str2); //地址都为堆
}
}
上面程序返回true、true、false,说明字符串及其intern()返回的是常量池的地址(里面保存其第一次出现的引用),而非首次构造的字符串返回堆的地址
类溢出
如下设置永久代为10M
-XX:PermSize=10M -XX:MaxPermSize=10M
利用cglib-nodep-3.1.jar创建大量的类
public class Test {
public static void main(String[] args) {
while(true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
static class OOMObject{
}
}
在JDK7中会导致方法区溢出(这个和书上报错不太一样)
JDK8后的参数
JDK8后元空间取代永久代
- -XX:MaxMetaspaceSize:元空间最大值,默认为-1,不限制
- -XX:MetaspaceSize:元空间初始大小,单位为字节,达到该值时会触发gc并扩容
- -XX:[Min/Max]MetaspaceFreeRatio:gc后控制最小/大的元空间剩余容量的百分比,可减少元空间不足导致的gc频率
本机直接内存溢出
直接内存可通过-XX:MaxDirectMemorySize来指定,若未指定则默认与Java堆最大值(-Xmx)一致,下面设置直接内存和堆
-Xmx20M -XX:MaxDirectMemorySize=10M
程序通过反射调用Unsafe的allocateMemory()方法直接申请内存
import sun.misc.Unsafe;
public class Test {
private static final int _1MB = 1024*1024;
public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe)unsafeField.get(null);
while(true){
unsafe.allocateMemory(_1MB);
}
}
}
会导致直接内存溢出,这种情况通常出现在NIO中