最近在读讲Java虚拟机(深入理解Java虚拟机)的书,果然是另一片天地。
做了实验,现在总结一下Java VM的内存区域划分和内存的溢出异常。
内存区域划分
- 程序计数器
- 虚拟机栈
- 本地方法栈
- 堆
- 方法区
- 运行时常量池(方法区的一部分,用于存放各种字面量和符号引用)
- 直接内存
空间分配
以32位Win为例,每个进程可以分到最多2G的内存,要分给Xmx(最大堆容量),还有MaxPermSize(最大方法区容量),计数器消耗很小,还有VM进程本身消耗的内存,剩下的内存就是VM栈和本地方法栈了。
内存溢出的异常
听说QQ有一Java面试题就是问你如何把Java虚拟机搞挂。好吧,其实很容易,其实挂掉也分好多的异常。
Java堆异常
看看下面这代码:
|
public class HeapOOM { public static void main(String[] args) { List<HeapOOM> list = new ArrayList<HeapOOM>(); while (true) { list.add(new OOMObject()); } } } |
可以加一些VM的运行参数:
|
-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError |
参数限定了VM运行的堆的大小为20M,当Out of memory时,会生成Heap的Dump文件 ,方便我们调试。我们可以用Eclipse Memory Analyzer分析。
死循环运行的结果可想而知,VM抛出了内存异常,像这样,
|
java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid5832.hprof ... Heap dump file created [24727781 bytes in 2.408 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Unknown Source) at java.util.Arrays.copyOf(Unknown Source) at java.util.ArrayList.ensureCapacity(Unknown Source) at java.util.ArrayList.add(Unknown Source) at HeapOOM.main(HeapOOM.java:10) |
虚拟机栈和本地方法栈溢出
Java默认的Hotspot VM中不区分虚拟机栈和本地方法栈,我们可以用-Xss参数设定栈大小(-Xoss本来是设置本地方法栈的大小,但实际上无效)。这里可能有两种异常,
- 如果栈深度大于VM所允许的最大深度,就抛StackOverflowError。
- 如果VM扩展栈时无法申请足够的空间,就抛OutOfMemoryError。(但此异常在实际实验中,无法抛出。)
用函数递归就可以把VM搞挂了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
public class VMStackSOF { private int stackLength = 1; public void stackLeak() { // 加入本地变量 stackLength++; stackLeak(); } public static void main(String[] args) throws Throwable { VMStackSOF sof = new VMStackSOF(); try { sof.stackLeak(); } catch (Throwable e) { System.out.println("stack length:" + sof.stackLength); throw e; } } } |
会抛出StackOverflowError的异常,
|
stack length:158198 Exception in thread "main" java.lang.StackOverflowError at VMStackSOF.stackLeak(VMStackSOF.java:24) at VMStackSOF.stackLeak(VMStackSOF.java:24) at VMStackSOF.stackLeak(VMStackSOF.java:24) at VMStackSOF.stackLeak(VMStackSOF.java:24) ... |
我们可以调整本地变量的数量和用-Xss调整栈空间,来看对stackLength的影响。
变量表越多,-Xss越小,stackLength越浅,反之越深。
运行时常量池溢出
使用Sting.intern向运行时常量池中加内容。因为运行时常量池是方法区的一部分,所以可以用-XX:PermSize和-XX:MaxPermSize限制方法区的大小,间接控制常量池的大小。
|
public class PoolOOM { /** * @param args */ public static void main(String[] args) { List<String> list = new ArrayList<String>(); int i = 0; while (true) { list.add(String.valueOf(i++).intern()); } } } |
VM参数用-XX:PermSize=2M -XX:MaxPerSize=2M来限制方法区的最大容量,注意不能太小,不然VM是不能启动的。
出错信息如下:
|
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space at java.lang.String.intern(Native Method) at PoolOOM.main(PoolOOM.java:14) |
PermGen是永久带(Permanent Generation)的意思,只在HotSpot里才有这个概念。
方法区溢出
让我们来把方法区搞挂,方法就是动态产生大量的类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
public class PGOOM { /** * @param args */ public static void main(String[] args) { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(PGOOM.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(); } } } |
VM参数:-XX:PermSize=5M -XX:MaxPermSize=5M
执行,好吧,它挂了!
|
Caused by: java.lang.OutOfMemoryError: PermGen space at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClassCond(Unknown Source) at java.lang.ClassLoader.defineClass(Unknown Source) ... 8 more |
如大量产生JSP网页时,就有可能出现方法区溢出。
本机直接内存溢出
DirectMemory可以通过-XX: MaxDirectMemorySize来指定,如不指定,则默认与Java堆的最大值相同。通过Unsafe实例对内存进行直接分配(因为Unsafe被设计成只有rt.jar中的类才能使用,所以要用反射的方法强行改变它的访问权限)。
|
import java.lang.reflect.Field; import sun.misc.Unsafe; public class DmOOM { private static final int _1MB = 1024*1024; /** * @param args */ public static void main(String[] args) throws Exception { Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); } } |
VM参数用-XX:MaxDirectMemorySize=10M (No space between XX: and M),程序就挂了。
|
Exception in thread "main" java.lang.OutOfMemoryError at sun.misc.Unsafe.allocateMemory(Native Method) at DmOOM.main(DmOOM.java:15) |
NOTE:这段代码在Eclipse下面默认是不会编译通过的。要改一个默认选项。Preferences-〉Java-〉Compiler-〉Errors/Warnings-〉Deprecated and restricted API-〉Forbidden reference (access rules): 改成Warning!
好了,现在能挂的都挂了! 😎