java内存区域

jvm运行时数据区域

java虚拟机在执行java程序的过程中将它所管理的内存划分为以下几个运行时数据区域:

  • 程序计数器(Program Counter Register)
  • 虚拟机栈(VM Stack)
  • 本地方法栈(Native Method Stack)
  • 堆(Heap)
  • 方法区(Method Area)

线程私有区域(程序计数器、虚拟机栈、本地方法栈),线程共享区域(堆、方法区),直接内存。线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束 而 创建/销毁(在 Hotspot VM 内)。线程共享区域随虚拟机的启动/关闭而创建/销毁。直接内存并不是 JVM 运行时数据区的一部分。
image

1. 程序计数器

程序计数器是当前线程执行的字节码的行号指示器,为了线程切换后能够恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,个线程之间的程序计数器互不影响,各自独立存储。这类内存区域为“线程私有”的区域。此内存区域是唯一一个没有OutOfMemoryError的区域。

2. 虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。栈容量只由-Xss参数设定

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverFlowError
  • 如果虚拟机在扩展时无法申请到足够的内存空间,则抛出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
/**
* VM Args: -Xss160k
* -Xss的最小值为160k
**/
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;
}
}
}

程序输出:

1
2
3
4
stack length : 772
Exception in thread "main" java.lang.StackOverflowError
at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:11)
at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)

3. 本地方法栈

本地方法栈与虚拟机栈的作用相似,区别在于虚拟机方法栈为虚拟机执行Java方法服务,本地方法栈为虚拟机使用Native方法服务。Hotspot虚拟机将本地方法栈和虚拟机方法栈合二为一。

4. 堆

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。所有创建的对象和数组都存放在堆上。Java堆是垃圾收集器管理的主要区域,垃圾收集器主要基于====分代收集算====法,因此从GC的角度对堆进行划分:新生代(Eden空间、From Survivor空间、To Survivor空间)、老年代。如果在堆中没有内存完成示例分配、并且堆也无法再扩展,将会抛出OutOfMemoryError异常。堆的大小可以通过-Xmx(jvm运行时堆的最大值)和-Xms(jvm运行时堆的最小值)控制。

以下代码测试堆的OutOfMemoryError异常,将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展,参数-XX:+HeapDumpOnOutOfMemoryError可让虚拟机在出现内存溢出是Dump出当前的内存堆转储快照以便时候进行分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
**/
public class HeapOOM {

static class OOMObject {

}

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

输出如下:

1
2
3
4
5
6
7
8
9
10
11
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid3421.hprof ...
Heap dump file created [27644178 bytes in 0.129 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:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at HeapOOM.main(HeapOOM.java:18)
  • -Xms : 设置堆的最小值,例如-Xms20, 分配20M
  • -Xmx :设置堆的最大值,例如-Xmx200, 分配200M
  • -XX:+HeapDumpOnOutOfMemoryError :让虚拟机在出现内存溢出是Dump出当前的内存堆转储快照以便时候进行分析

5. 方法区

方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池。java虚拟机对class文件每一部分的格式都有严格的规定,每一个字节用于存储哪种数据都必须符号规范上的要求才会被虚拟机认可、装载和执行。

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError的异常,测试方法区和运行时常量池溢出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* jdk 1.8及之后,VM Args: -XX:MetaspaceSize=2m -XX:MaxMetaspaceSize=2m
* jdk 1.8及之前,VM Args: -XX:PermSize=2m -XX:MaxPermSize=2m
**/
public class RuntimeConstantPoolOOM {

public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}

程序输入:

1
2
3
Error occurred during initialization of VM
java.lang.OutOfMemoryError: Metaspace
<<no stack trace available>>

6. 直接内存(Direct Memory)

直接内存(Direct Memory)并不是虚拟机运行时的数据区的一部分,也不是java虚拟机规范中定义的内存区域,不会受到java堆大小的限制,不受jvm gc的管理。这部分内存如果频繁使用,也可能导致OutOfMemoryError异常, 测试本机直接内存溢出,DirectMemory容量可通过-XX:MaxDirectMemorySize设置,如果不指定,默认与Java堆最大值(-Xmx指定)一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
**/
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);
}
}
}

通过测试,始终没有抛出OutOfMemoryError异常,测试环境jdk1.8, 开发工具IDEA, 目前还不知道为什么,如果有哪位朋友知道为什么,大家一起讨论下

结构梳理

image

引用参考

  • 深入理解Java虚拟机-周志明 著