整理自《深入理解 Java 虚拟机》。
Java 内存区域
Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为各个不同的数据区域,包括以下几个部分:
1. 程序计数器
线程私有,是当前线程所执行的字节码的行号指示器。字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是 Native 方法,则计数器值为空。
2. 虚拟机栈
线程私有,生命周期与线程相同。描述的是 Java 方法执行的内存模型,每个方法在执行的同时会创建一个栈帧用于储存局部变量表、操作数栈、动态链接、方法出口等信息。
3. 本地方法栈
线程私有,与虚拟机栈发挥的作用类似,虚拟机栈是为执行 Java 方法服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
4. 堆
所有线程共享的一段内存区域,此区域唯一目的是存放对象实例。
5. 方法区
各个线程共享,用于储存已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。其中的运行时常量池相对于 Class 文件常量池而言具备动态性,运行期间也可将新的常量放入池中。
运行时常量池是方法区的一部分,Class 文件中的常量池信息用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
内存溢出
1. 堆溢出
示例:
-Xms 参数设置堆的最小值。
-Xmx 参数设置堆的最大值。
-XX:+HeapDumpOnOutOfMemoryError 参数让虚拟机在出现内存溢出异常时 Dump 出当前内存堆转储快照以便进行事后分析。
/**
* VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* @author zzm
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
运行结果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid6468.hprof …
Heap dump file created [28156524 bytes in 0.483 secs]
要解决堆溢出异常,一般先通过内存映像分析工具对 Dump 出来的堆转储快照进行分析,确认内存中的对象是否是必要的,也就是分清到底是出现了内存泄露(无法释放已申请的内存空间)还是内存溢出(申请内存时,没有足够的空间供使用)。
如果是内存泄露,可进一步查看泄露对象到 GC Roots 的引用链,找出泄露对象无法被回收的原因。
如果不存在泄露,那首先检查虚拟机堆参数,看是否可调大。再者,从代码上检查是否存在某些对象生命周期过长、持有时间过长的情况,尝试减少程序运行期的内存消耗。
2. 虚拟机栈和本地方法栈溢出
当线程请求的栈深度大于虚拟机所允许的最大深度,将出现 StackOverflowError 异常。
示例:
-Xss 参数减少栈内存容量
/**
* VM Args:-Xss128k
* @author zzm
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
运行结果:
Exception in thread “main” java.lang.StackOverflowError
stack length:999
为每个线程的栈分配的内存越大,可以建立的线程数就越少,在建立过多线程时会导致内存溢出,出现 OutOfMemoryError 异常。
示例:
/**
* VM Args:-Xss2M (这时候不妨设大些)
* @author zzm
*/
public class JavaVMStackOOM {
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) throws Throwable {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
上面程序在 windows 环境下运行后造成系统假死,因为在 windows 平台的虚拟机中,Java 线程是映射到操作系统内核线程上的。
运行结果:
Exception in thread “main” java.lang.OutOfMemoryError: unable to create new native thread
这种情况下要减少最大堆和减少栈容量来换取更多的线程。
3. 方法区和运行时常量池溢出
运行时常量池在 JDK1.6 及之前的版本中分配在永久代中,很少回收,所以会出现运行时常量池溢出的情况,JDK1.7 开始逐步“去永久代”。
运行时产生大量的类去填满方法区就会出现方法区溢出。
示例:
借助 CGLib 直接操作字节码运行时产生大量的动态类。
/**
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
* @author zzm
*/
public class JavaMethodAreaOOM {
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.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
static class OOMObject {
}
}
方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判定条件是比较苛刻的。在经常动态生成大量 Class 的应用中,需要特别注意类的回收状况。
4. 本机直接内存溢出
直接内存不是虚拟机运行时数据区的一部分,但也被频繁使用。
DirectMemory 容量可通过 -XX:MaxDirectMemorySize 指定,默认与 -Xmx 值一样。
示例:
/**
* VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
* @author zzm
*/
public class DirectMemoryOOM {
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);
}
}
}
运行结果:
Exception in thread “main” java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
由 DirectMemory 导致的内存溢出,一个明显的特征是在 Heap Dump 文件中不会看见明显的异常,如果发现 OOM 之后 Dump 文件很小,而程序中又直接或间接使用了 NIO,就可以考虑是这方面的原因。NIO 可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。