1. 程序计数器(Program Counter Register)
当前线程所执行的字节码的行号指示器。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是native方法,这个计数器值则为空(Undefined)。线程私有。唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
2. Java虚拟机栈(Java Virtual Machine Stack)
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程。线程私有,随线程创建而生,线程结束而死。大小通过-Xss设置,例如:-Xss1m(HotSpot默认);此参数的大小和能创建的最大线程数量有着密切的关系,在堆内存固定的情况下此参数值越大能够创建的线程越小。在此参数固定的情况下堆内存越大能够创建的线程越少,如下是32位HotSpot虚拟机1.7.0_80版本下不同-Xss值能够创建的最大线程的数量:
堆内存 | 虚拟机栈 | 最大线程数量 |
-Xmx1g | -Xss128k | 2000 |
-Xmx1g | -Xss256k | 1512 |
-Xmx1g | -Xss512k | 1017 |
-Xmx1g | -Xss1m | 620 |
之所以会出现这种情况是因为操作系统分配给每个进程的内存是有限制的,譬如32位的Windows限制为2GB。虚拟机提供了参数来控制Java堆和方法区的这两部分内存的最大值。剩余的内存为2GB(操作系统限制)减去Xmx(最大堆内存),再减去MaxPermSize(最大方法区容量),程序计数器消耗内存很少,可以忽略不计。如果虚拟机进程本身消耗的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈瓜分了。每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。当不能创建更多线程的时候就会抛出java.lang.OutOfMemoryError: unable to create new native thread异常,例如下面的程序:
package com.zws.jvm;import java.util.concurrent.atomic.AtomicInteger;/** * JVM args: -server -Xmx1g -Xss512k * @author wensh.zhu * */public class XssMaxThread { public static final AtomicInteger counter = new AtomicInteger(); public static void main(String[] args) { try { while (true) { new Thread(new Runnable() { public void run() { try { counter.incrementAndGet(); Thread.sleep(1000 * 60 * 60); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } } catch (Throwable e) { System.out.println(counter.get()); e.printStackTrace(); } }}
输出如下:
980java.lang.OutOfMemoryError: unable to create new native thread at java.lang.Thread.start0(Native Method) at java.lang.Thread.start(Thread.java:714) at com.zws.jvm.XssMaxThread.main(XssMaxThread.java:24)
综上此异常可通过适当减小堆内存或虚拟机栈内存解决。Java虚拟机规范中有两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。如下代码:
package com.zws.jvm;/** * JVM args:-server -Xss128k * @author wensh.zhu * */public class StackOver { int num = 0; public static void main(String[] args) { StackOver stackOver = new StackOver(); try { stackOver.count(); } catch (Throwable e) { System.out.println("stack length:" + stackOver.num); e.printStackTrace(); } } public void count() { num ++; count(); }}
输出如下:
stack length:2105java.lang.StackOverflowError at com.zws.jvm.StackOver.count(StackOver.java:17)
3. 本地方法栈(Native Method Stack)
类似于Java虚拟机栈,服务于native方法。HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一;此内存空间会抛出StackOverflowError和OutOfMemoryError异常。
4. Java堆(Java Heap)
用于存放对象实例和数组,java虚拟机规范规定:所有对象实例以及数组都要在堆上分配。可细分为:新生代和老年代。新生代又分为:Eden空间、From Survivor空间、To Survivor空间。之所以这么分是为了优化GC性能。大小通过-Xms和-Xmx两个参数设置其初始大小和最大大小。例如将初始大小和最大大小都设置为1g:-Xms1024m -Xmx1024m。还可以通过以下参数设置各个空的大小以及比例:
-XX:NewRatio 老年代大小与新生代大小的比例,默认-XX:NewRatio=2-XX:NewSize 新生代初始大小,默认-XX:NewSize=2m-XX:MaxNewSize 最大新生代大小,默认值通过-XX:NewRatio计算得来-XX:SurvivorRatio Eden和其中一个Survivor的比值 默认-XX:SurvivorRatio=8,即Edem:from:to = 8:1:1
此内存空间线程共享。可抛出OutOfMemoryError异常。如下代码:
package com.zws.jvm;import java.util.LinkedList;import java.util.List;/** * JVM args:-server -Xmx128m * @author wensh.zhu * */public class OOMTest { private int[] numbers = new int[1024]; public static void main(String[] args) { List<OOMTest> testObjs = new LinkedList<>(); for (;;) { testObjs.add(new OOMTest()); } } public int[] getNumbers() { return numbers; } public void setNumbers(int[] numbers) { this.numbers = numbers; }}
输出如下:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at com.zws.jvm.OOMTest.<init>(OOMTest.java:11) at com.zws.jvm.OOMTest.main(OOMTest.java:16)
5. 方法区(Method Area)
用于存储已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。HotSpot虚拟机习惯上称之为永久代,可通过-XX:PermSize和-XX:MaxPermSize两个参数设置其初始值以和最大值,例如:-XX:PermSize=128m、-XX:MaxPermSize=128m,在Java8中这两个参数以及被移除,取而代之的是-XX:MetaspaceSize和-XX:MaxMetaspaceSize;线程共享。可抛出OutOfMemoryError异常。
6. 运行时常量池(Runtime Constant Pool)
方法区的一部分。用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
7. 直接内存
直接内存(DirectMemory)并不是虚拟机运行时数据区的一部分,在JDK1.4中新加入了NIO(New Iput/Output)类,引入一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBufTer对象作为这块内存的引用进行操作。这样能在一些场景中显著提髙性能,闪为避免了在Java堆和Native堆中来回复制数据。显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样。当Direct ByteBuffer分配的堆外内存到达指定大小后,即触发Full GC。如下代码浮现了直接内存溢出异常:
package com.zws.jvm;import java.lang.reflect.Field;import sun.misc.Unsafe;/** * * @author wensh.zhu * */public class DirectMemoryOOM { private static final int _1MB = 1024 * 1024; public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException { Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); while (true) { unsafe.allocateMemory(_1MB); } }}
异常信息:
Exception in thread "main" java.lang.OutOfMemoryError at sun.misc.Unsafe.allocateMemory(Native Method) at com.zws.jvm.DirectMemoryOOM.main(DirectMemoryOOM.java:15)
由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果发现OOM之后Dump文件很小,而程序中有直接或间接使用了NIO,那就可以考虑检查一下是不是这方面的原因。
参考周志明《深入理解Java虚拟机 JVM高级特性与最佳实践》