内存溢出异常

JVM参数

-Xms:初始堆大小,默认为物理内存的1/64(<1GB),默认空余堆内存(MinHeapFreeRatio)小于40%时

    JVM就会增大堆大小直到-Xmx的最大限制。

-Xmx:最大堆大小,默认空余堆内存(MaxHeapFreeRatio)大于70%时JVM会减少堆大小直到 -Xms的最小限制。

    注:初始堆大小和最大堆大小设置成相同的值是导致堆不能自动扩展。

-Xmn:新生代的内存空间大小,整个堆大小=新生代大小 + 老生代大小。在保证堆大小不变的情况下

    增大新生代后,将会减小老生代大小。推荐配置为整个堆的3/8。

-Xss:每个线程的虚拟机栈大小。

-XX:+PrintGCDetails:打印GC详细日志

-XX:SurvivorRatio=8:新生代中Eden区域与Survivor区域的容量比值,默认值为8。两个Survivor区与一个Eden区的比值为2:8

     一个Survivor区占整个年轻代的1/10。

-XX:NewRatio:堆中新生代和老年代的容量比值,默认为2。即默认的新生代占堆内存的1/3,老年代占2/3。

-XX:+HeapDumpOnOutOfMemoryError:堆内存溢出时的“堆转储快照”

-XX:PermSize:方法区(永久代)初始大小

-XX:MaxPermSize:最大方法区(永久代)大小

-XX:MaxDirectMemorySize:本机直接内存大小,不设置的话和堆最大内存大小相同

Java堆溢出

 Run Args设置JVM启动参数

package com.wjz.demo;

import java.util.ArrayList;
import java.util.List;

public class HeapOOM {
    static class OOMObject {}
    /**
     * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
     * @param args
     */
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        while(true) {
            list.add(new OOMObject());
        }
    }
}

输出:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

解决思路

通过内存映射分析工具(Eclipse Memory Analyzer)对Dump出的堆转储内存快照进行分析。重点确定内存中的对象是否是必要的也就是明确是内存泄漏还是内存溢出。

内存泄漏的话通过工具查看泄漏对象到GC Roots的引用链,分析为何GC无法回收对象,定位出泄漏代码的位置。

内存溢出的话检查JVM堆参数和机器物理内存看是否还有调大堆内存的可能,代码上检查是否存在对象生命周期过长、持有时间过长的情况。

虚拟机栈和本地方法栈溢出

HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此-Xoss参数无效,栈容量只能由-Xss参数设定。

线程请求栈深度大于虚拟机所允许的最大深度将抛出StackOverflowError异常。

虚拟机在扩展栈时无法申请到足够的内存空间则抛出OutOfMemoryError异常。

单线程下无论是虚拟机栈容量过小还是方法帧栈太大(定义大量的本地变量,局部变量表长度过大)都只会抛出StackOverflowError异常。

package com.wjz.demo;

public class JavaVMStackSOF {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }
    /**
     * JVM args: -Xss128k
     * @param args
     * @throws Throwable
     */
    public static void main(String[] args) throws Throwable {
        JavaVMStackSOF sof = new JavaVMStackSOF();
        try {
            sof.stackLeak();
        } catch (Throwable e) {
            System.out.println(sof.stackLength);
            throw e;
        }
    }
}

输出:
    
98820
    Exception in thread "main" java.lang.StackOverflowError

不限于单线程,通过不断的创建线程可以导致内存溢出但是这和栈空间大小无关。此时每个线程分配的栈内存越大越容易内存溢出。

原因是操作系统为每个进程分配的内存是有限的,虚拟机提供了参数控制堆区和方法区的内存大小。

虚拟机栈 = 进程总内存 - Xmx(最大堆区容量) - MaxPermSize(最大方法区容量)。程序计数器和进程本身内存忽略不计。

由于每天线程对应一个虚拟机栈,每个线程分配到的虚拟机栈容量越大时,可以建立的线程数就会减少,建立线程时就越容易将剩余内存耗尽。

方法区和运行时常量池溢出

package com.wjz.demo;

import java.util.ArrayList;
import java.util.List;

public class RuntimeConstantPoolOOM {
    /**
     * JDK1.6及以前会内存溢出,JDK1.7及以后逐步"去永久代"
     * JVM args : -XX:PermSize=10M -XX:MaxPermSize=10M
     * @param args
     */
    public static void main(String[] args) {
        // 使用list保持着常量池的引用,避免 Full GC 回收常量池
        List<String> list = new ArrayList<String>();
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}

JDK 1.6及以前和JDK 1.7及以后的常量池区别

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);
}

JDK 1.6
输出 : false, false
JDK 1.7
输出 : true, false

JDK 1.6及以前intern方法首次遇到字符串实例时会将其复制到常量池中,返回的也是该字符串实例的引用,StringBuilder创建的字符串是在堆上的,对象地址不同

JDK 1.7及以后不再复制字符串实例而是保存一个首次出现的实例引用。

str1的intern方法返回的实例引用和StringBuilder创建的字符串引用相等。“计算机软件”符合首次出现原则。

str2的intern方法返回的是常量池的那个“java”实例引用和StringBuilder创建的字符串引用不相等。“java”不符合首次出现原则。

方法区用于存放Class的相关信息如类名、访问修饰符、常量池、字段描述、方法描述等。当产生大量的类时可导致方法区溢出。

package com.wjz.demo;

import java.lang.reflect.Method;

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

public class JavaMethodAreaOOM {
    /**
     * JVM args : -XX:PermSize=10M -XX:MaxPermSize=10M
     * @param args
     */
    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    return proxy.invoke(obj, args);
                }
            });
            enhancer.create();
        }
    }
    static class OOMObject {
    }
}

输出 : Exception: java.lang.OutOfMemoryError

经常动态生成Class时注意类的回收,大量JSP或动态产生JSP的应用(JSP第一次运行时需要编译为Java类)也容易溢出等。

本机直接内存溢出

package com.wjz.demo;

import java.lang.reflect.Field;
import sun.misc.Unsafe;

public class DirectMemoryOOM {
    /**
     * Unsafe是Native方法可用于分配内存,扩充内存,释放内存
     * JVM args : -Xmx20M -XX:MaxDirectMemorySize=10M
     * @param args
     */
    private static final int _1MB = 1024*1024;
    public static void main(String[] args) throws Throwable {
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while (true)
            unsafe.allocateMemory(_1MB);
    }
}

输出 : java.lang.OutOfMemoryError

直接内存溢出的特征是Heap Dump文件看不到明显异常且文件很小,程序中使用了NIO。

原文地址:https://www.cnblogs.com/BINGJJFLY/p/7607720.html