0%

JVM进程内存分类

本文默认基于64位的JDK 8(其虚拟机实现是HotSpot),除非特别说明。

一、前言

1.1、进程使用内存上限

进程所能使用内存上限受到硬件和操作系统限制,比如32位Windows操作系统下进程所能使用内存上限为2G。

1.2、虚拟机进程

我们平时说的Java进程其实是虚拟机进程——JVM进程。通过执行java命令触发运行JVM进程,比如java com.dslztx.Mainjava -version等。

1.3、JVM进程内存管理

如“1.1、进程使用内存上限”所述,JVM进程使用内存上限受到硬件和操作系统限制。根据虚拟机规范,JVM进程内存管理采取内存分类、内存动态分配、垃圾收集等技术。采用该种内存管理机制的优点是内存管理更加方便简单,缺点是一旦出现内存泄漏和溢出等问题,较难排查问题。

二、JVM进程内存管理之内存分类

分配给进程使用的一块内存区域被称为Native Memory(有时候也被称为Native Heap),如“1.1、进程使用内存上限”所述,Native Memory上限受到硬件和操作系统限制。


Native Memory如何被使用由进程自身决定,虚拟机规范人为对JVM进程使用的Native Memory进行了划分,虽然其实底层都使用了相同的方式操作和管理内存,比如都调用C/C++语言的malloc/free函数。JVM进程内存管理对所使用的Native Memory的具体分类为:程序计数器,Java虚拟机栈,本地方法栈,Java堆,方法区,显式调用JNI方法直接分配内存,JVM进程运行自身所需内存

关于上述内存分类有两点说明:

  1. 基于是否由业务线程共享(这个共享是狭义的,基本可认为只有“Java堆”和“方法区”才算是线程共享的)可对上述内存分类进行再划分:
    • 线程私有:程序计数器、Java虚拟机栈、本地方法栈
    • 线程共享:Java堆、方法区
    • 其他类型:JVM进程运行自身所需内存、显式调用JNI方法直接分配内存(深究起来,该部分内存也是广义线程共享的,但这里只讨论狭义线程共享)
  2. 虽然底层都使用C/C++语言的内存分配/回收函数,比如“malloc/free”,但是除了“显式调用JNI方法直接分配内存”之外的内存分类,内存操作没有JNI方法调用中间层

上述具体分类示例如图1。

图1

结合“作用”、“使用过程中可能遇到问题”和“其他描述”等信息,对内存分类进行深入介绍,如表1。

表1

内存分类 英文名称 线程私有/线程共享/其他类型 作用 使用过程中可能遇到问题 其他描述
程序计数器 Program Counter Register 线程私有 执行Java方法时,存储线程当前执行字节码指令地址;执行本地方法(Native Method)时,存储空值 理论上不会抛出任何异常 内存空间小
Java虚拟机栈 Java Virtual Machine Stack 线程私有 存储Java方法执行时创建的栈帧 可能抛出StackOverflowErrorOutOfMemoryError异常 一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法调用生成栈帧,结束释放栈帧
本地方法栈 Native Method Stack 线程私有 存储本地方法(Native Method)执行时创建的栈帧 可能抛出StackOverflowErrorOutOfMemoryError异常 本地方法栈跟Java虚拟机栈非常类似,只是:一个针对Java方法,另外一个针对本地方法(Native Method)
Java堆 Java Heap 线程共享 存储几乎所有的对象实例 可能抛出OutOfMemoryError异常 内存空间较大
方法区 Method Area 线程共享 存储已被虚拟机加载的类信息,即时编译器编译以后的代码等数据 可能抛出OutOfMemoryError异常 JDK 6使用“永久代”实现方法区,JDK 8使用“元空间”实现方法区:实现策略升级解决一些已知存在问题,具体原因没有必要深究[4]
JVM进程运行自身所需内存 / 其他类型 JVM进程运行自身所需 理论上不会抛出任何异常 /
显式调用JNI方法直接分配内存 / 其他类型 直接使用Native Memory 可能抛出OutOfMemoryError异常 有两类:
1)JDK提供的JNI API,比如sun.misc.Unsafe类下的public native long allocateMemory(long var1)方法;
2)自己实现的JNI方法

使用过程中需要显式声明和释放内存,否则会有内存泄漏。

有必要再介绍一个特殊的内存区域——直接内存,其英文名为Direct Memory,它是NIO引入的新机制,本质是调用sun.misc.Unsafe.allocateMemoryJNI API而直接使用Native Memory,再作了一定封装,被特别命名为直接内存

三、内存溢出异常实验

3.1、Java虚拟机栈和本地方法栈OutOfMemoryErrorStackOverflowError异常

3.1.1、测试代码

测试OutOfMemoryError异常代码如下:

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
26
27
28
29
30
/**
* 测试OutOfMemoryError异常
*/
public class JavaVMStackOOM {
private void doNotStop() {
while (true) {
try {
Thread.sleep(1000L);
} catch (Exception e) {
e.printStackTrace();
}
}
}

public void createThreadForever() {
while (true) {
Thread thread = new Thread(new Runnable() {
public void run() {
doNotStop();
}
});
thread.start();
}
}

public static void main(String[] args) throws Throwable {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.createThreadForever();
}
}

测试StackOverflowError异常代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 测试StackOverflowError异常
*/
public class JavaVMStackSOF {
private int stackLength = 1;

public void recurseExecution() {
stackLength++;

recurseExecution();
}

public static void main(String[] args) throws Throwable {
JavaVMStackSOF sof = new JavaVMStackSOF();
try {
sof.recurseExecution();
} catch (Throwable e) {
System.out.println("stack length: " + sof.stackLength);
throw e;
}
}
}

3.1.2、执行

测试OutOfMemoryError异常执行命令如下:

1
2
javac JavaVMStackOOM.java
java -Xss200m JavaVMStackOOM > execution.out.2 2>&1

执行结果:

1
2
3
4
5
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:717)
at JavaVMStackOOM.createThreadForever(JavaVMStackOOM.java:19)
at JavaVMStackOOM.main(JavaVMStackOOM.java:25)

测试StackOverflowError异常执行命令如下:

1
2
javac JavaVMStackSOF.java
java -Xss256k JavaVMStackSOF > execution.out 2>&1

执行结果:

1
2
3
4
5
6
stack length: 3167
Exception in thread "main" java.lang.StackOverflowError
at JavaVMStackSOF.recurseExecution(JavaVMStackSOF.java:10)
at JavaVMStackSOF.recurseExecution(JavaVMStackSOF.java:10)
at JavaVMStackSOF.recurseExecution(JavaVMStackSOF.java:10)
...

3.1.3、说明

根据上述可知,一个Java线程最多可关联两个栈——Java虚拟机栈和本地方法栈,分别存储“Java方法执行时创建的栈帧”和“本地方法执行时创建的栈帧”,但是在HotSpot虚拟机实现中,不区分Java虚拟机栈和本地方法栈,故此时一个线程只会关联一个栈,该栈同时存储以上两种栈帧

本文是基于HotSpot虚拟机实现进行说明的,故后续统一以“栈”作为称呼。

关于栈的StackOverflowErrorOutOfMemoryError异常作几点说明如下:

  • 栈的大小通过“-Xss”虚拟机参数设定
  • 栈是线程私有的,因此当N个线程申请栈时,就需要N *(Xss虚拟机参数设定值)内存,如果内存不足,则会抛出OutOfMemoryError异常
  • 假定一个栈帧大小为S,当一个栈内需要分配K个栈帧(栈帧数量只跟方法调用深度有关)时,如果S * K > Xss虚拟机参数设定值,则抛出StackOverflowError异常

3.2、Java堆OutOfMemoryError异常

3.2.1、测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.ArrayList;
import java.util.List;

public class HeapOOM {
static class OOMObject {

}

public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();

while (true) {
list.add(new OOMObject());
}
}
}

3.2.2、执行

执行命令:

1
2
javac HeapOOM.java
java -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError HeapOOM

执行结果:

1
2
3
4
5
6
7
8
9
10
11
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid2927.hprof ...
Heap dump file created [27538433 bytes in 0.310 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:267)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233)
at java.util.ArrayList.add(ArrayList.java:464)
at HeapOOM.main(HeapOOM.java:13)

3.2.3、说明

通过“-Xms20m”和“-Xmx20m”虚拟机参数设定Java堆大小为固定的20MB,避免自动扩展;通过“-XX:+HeapDumpOnOutOfMemoryError”虚拟机参数设定使用Java堆出现OutOfMemoryError异常时,自动Dump出堆转储快照,以便事后分析。
Java堆的OutOfMemoryError异常后面还跟有提示信息:Java heap space,表明异常发生于使用“Java堆”的过程中。

3.3、方法区OutOfMemoryError异常

3.3.1、测试代码

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
26
27
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;


public class JavaMethodAreaOOM {
public static void main(final 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[] objects, MethodProxy methodProxy)
throws Throwable {
return methodProxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}

static class OOMObject {

}
}

3.3.2、执行命令

执行命令:

1
2
3
javac -cp ".:./cglib-3.2.4.jar:./asm-5.1.jar" JavaMethodAreaOOM.java

java -cp ".:./cglib-3.2.4.jar:./asm-5.1.jar" -XX:MaxMetaspaceSize=10m JavaMethodAreaOOM

执行结果:

1
2
3
4
5
6
7
8
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:345)
at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:114)
at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:291)
at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
at JavaMethodAreaOOM.main(JavaMethodAreaOOM.java:20)

3.3.3、说明

由以上描述可知,方法区存储已被虚拟机加载的类信息,即时编译器编译以后的代码等数据,通过CGLIB库提供的字节码操作技术,我们可以在运行时动态生成大量的新类,从而使得方法区的内存不够分配,最终导致抛出OutOfMemoryError异常。
JDK 8使用“元空间”实现方法区,可通过JVM参数“-XX:MaxMetaspaceSize”配置元空间的最大值。以上例子中,抛出的OutOfMemoryError异常后面还跟有提示信息:Metaspace,表明异常发生于使用“元空间”的过程中。

3.4、直接内存OutOfMemoryError异常

3.4.1、测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

public class DirectMemoryOOM {

public static void main(String[] args) throws InterruptedException {
int i = 0;
List<ByteBuffer> buffers = new ArrayList<>();

while (true) {
ByteBuffer bb = ByteBuffer.allocateDirect(1024 * 1024 * 1);
buffers.add(bb);

Thread.sleep(1000); //为了便于观察,休眠1s

System.out.println(i++);
}
}

}

3.4.2、执行

执行命令:

1
2
javac DirectMemoryOOM.java
java -XX:MaxDirectMemorySize=10m DirectMemoryOOM

执行结果:

1
2
3
4
5
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:695)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at DirectMemoryOOM.main(DirectMemoryOOM.java:12)

3.4.3、说明

可通过JVM参数“-XX:MaxDirectMemory”配置直接内存的最大值,以上例子中,抛出的OutOfMemoryError异常后面还跟有提示信息:Direct buffer memory,表明异常发生于使用“直接内存”的过程中。


参考文献

[1]http://www.ibm.com/developerworks/library/j-nativememory-linux/
[2]https://geosmart.github.io/2016/03/07/JVM%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%8C%EF%BC%89Java%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E4%B8%8E%E5%86%85%E5%AD%98%E6%BA%A2%E5%87%BA%E5%BC%82%E5%B8%B8/
[3]http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html
[4]https://openjdk.java.net/jeps/122

您的支持将鼓励我继续分享!