JVM内存溢出的方式
了解了Java虚拟机五个内存区域的作用后,下面我们来继续学习下在什么情况下
这些区域会发生溢出。
1.虚拟机参数配置
-Xms:初始堆大小,默认为物理内存的1/64(<1GB);默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制。
-Xmx:最大堆大小,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。
-Xss:每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。应根据应用的线程所需内存大小进行适当调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。一般小的应用, 如果栈不是很深, 应该是128k够用的,大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。
-XX:PermSize:设置永久代(perm gen)初始值。默认值为物理内存的1/64。
-XX:MaxPermSize:设置持久代最大值。物理内存的1/4。
-Xmx:最大堆大小,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。
-Xss:每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。应根据应用的线程所需内存大小进行适当调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。一般小的应用, 如果栈不是很深, 应该是128k够用的,大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。
-XX:PermSize:设置永久代(perm gen)初始值。默认值为物理内存的1/64。
-XX:MaxPermSize:设置持久代最大值。物理内存的1/4。
2.方法区溢出
因为方法区是保存类的相关信息的,所以当我们加载过多的类时就会导致方法区
溢出。在这里我们通过JDK动态代理和CGLIB代理两种方式来试图使方法区溢出。
2.1 JDK动态代理
package com.cdai.jvm.overflow; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class MethodAreaOverflow { static interface OOMInterface { } static class OOMObject implements OOMInterface { } static class OOMObject2 implements OOMInterface { } public static void main(String[] args) { final OOMObject object = new OOMObject(); while (true) { OOMInterface proxy = (OOMInterface) Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader(), OOMObject.class.getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Interceptor1 is working"); return method.invoke(object, args); } } ); System.out.println(proxy.getClass()); System.out.println("Proxy1: " + proxy); OOMInterface proxy2 = (OOMInterface) Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader(), OOMObject.class.getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Interceptor2 is working"); return method.invoke(object, args); } } ); System.out.println(proxy2.getClass()); System.out.println("Proxy2: " + proxy2); } } }
虽然我们不断调用Proxy.newInstance()方法来创建代理类,但是JVM并没有内存溢出。
每次调用都生成了不同的代理类实例,但是代理类的Class对象没有改变。是不是Proxy
类对代理类的Class对象有缓存?具体原因会在之后的《JDK动态代理与CGLIB》中进行
详细分析。
2.2 CGLIB代理
CGLIB同样会缓存代理类的Class对象,但是我们可以通过配置让它不缓存Class对象,
这样就可以通过反复创建代理类达到使方法区溢出的目的。
package com.cdai.jvm.overflow; import java.lang.reflect.Method; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; public class MethodAreaOverflow2 { static class OOMObject { } 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 method.invoke(obj, args); } }); OOMObject proxy = (OOMObject) enhancer.create(); System.out.println(proxy.getClass()); } } }
3.堆溢出
堆溢出比较简单,只需通过创建一个大数组对象来申请一块比较大的内存,就可以使
堆发生溢出。
package com.cdai.jvm.overflow; public class HeapOverflow { private static final int MB = 1024 * 1024; @SuppressWarnings("unused") public static void main(String[] args) { byte[] bigMemory = new byte[1024 * MB]; } }
4.栈溢出
栈溢出也比较常见,有时我们编写的递归调用没有正确的终止条件时,就会使方法不断
递归,栈的深度不断增大,最终发生栈溢出。
package com.cdai.jvm.overflow; public class StackOverflow { private static int stackDepth = 1; public static void stackOverflow() { stackDepth++; stackOverflow(); } public static void main(String[] args) { try { stackOverflow(); } catch (Exception e) { System.err.println("Stack depth: " + stackDepth); e.printStackTrace(); } } }