前言
本文所介绍的是 JDK 1.8 版本,其他版本的 JDK 在这里并不一定正确;内容主要摘自周志明的《深入理解Java虚拟机》一书的关键点,并根据自身的理解进行记录。感兴趣的同学可以去阅读原著。
JVM 的内存结构,主要包括以下 5
个区域:
线程私有: 程序计数器、虚拟机栈、本地方法栈
线程共享: 方法区、堆
程序计数器
上面讲到,程序计数器是线程私有的,它指示的是当前线程所执行的字节码行号,控制跳转、循环,当线程过了 CPU 的时间片,就需要用它记录起来,方便后续恢复“执行现场”,这也是线程私有的原因。该区域是虚拟机规范中唯一
一个不会产生内存溢出(OutOfMemoryError
)的区域。
Java 虚拟机栈
Java 虚拟机栈也是线程私有的,在方法执行时会创建一个栈帧,该栈帧包括了局部变量表
、操作数栈
、动态链接
、方法出口
等信息。可以思考一下,以下方法执行时,对应的是哪一部分?
public boolean method(int methodParam) {
int localVariable = 1;
BigDecimal objReference = new BigDecimal("3.1415926");
double a = 1.1;
double b = 3.6;
double c = a + b;
return true;
}
- 局部变量表:编译期可知的各种 Java 虚拟机基本数据类型(boolean、byte、char、short、int、
float、long、double)、对象引用(reference类型)。 - 操作数栈:可理解为 Java 虚拟机栈中的一个用于计算的临时数据存储区,例如变量
b
与c
他们要相加,得先将它们加到操作数栈中,再执行相加的操作,这里用到的都是操作数栈,以下为字节码文件。
- 动态链接:这个可以类比 c、c++ 编译时会共用一个库文件以节省内存,linux 下为 .so、windows 下为 .dll。
- 方法出口:这个见名知意。
在 Java 虚拟机规范中,这个内存区域存在两种异常类型:达到栈最大深度产生 StackOverflowError
与 动态扩展无法申请到足够内存时产生OutOfMemoryError
。但书中说明在 HotSpot 虚拟机的栈容量是不可以动态扩展的,因此不会存在 OutOfMemoryError
。
本地方法栈
本地方法栈与虚拟机栈的作用相似,它的是为 Native 方法服务的,同样也能抛出 StackOverflowError
与 OutOfMemoryError
异常。
Java 堆
堆可谓是与我们打交道最多的一处了,我们进行调优也主要是调这个区域的内存分配与回收机制;它是线程共享的,主要用来存放对象的实例以及字节数组,但事实上并非所有的对象都分配在堆上,因为后面新版本的逃逸分析技术允许不在堆上分配对象空间。
一般的堆由新生代
、老年代
组成,新生代里面又细分为 Eden 区、Survivor 1、Survivor 2 区;大多数的对象都是朝生熄灭,很大可能会在新生代就被“消灭”。
在该区域会发生的异常为 OutOfMemoryError
,下面代码简单验证是否对象一定在堆上分配:
// 以下代码执行,如果在堆上分配将会出现 OOM,但实际却并没有
public static void main(String[] args) {
// 配置 -Xmx10m,如果在堆上分配会立即 OOM
while (true) {
new Clazz();
}
}
static class Clazz {
/**
* 5MB
*/
byte[] bytes = new byte[1024 * 1024 * 5];
}
方法区
该区域与堆一样,也是线程共享的,主要存储已被虚拟机加载的类信息
、常量
、静态变量
、即时编译后代码的缓存
等数据,可以将它认为是概念性的区域,不同版本有不同的实现,如 JDK 8 之前由永久代实现,JDK 8 由元空间(Meta Space)实现并放到了直接内存,不受 JVM 的参数限制,当物理内存不足也会抛出 OutOfMemoryError
。
随着发展实际上,在最终元空间主要只剩下了类信息,其他都没移出,下面是历程:
- 运行时常量池:是方法区的一部分,class 字节码文件除了有类版本号、字段、接口信息、方法信息外还有一份
常量池表
,常量池表主要存储编译期间生成的字面量(int a = 1,1就是字面量)与符号引用(类引用其他对象,编译期间无法得知,因此给个符号标识),这部分内容将在类加载后存放到方法区的运行时常量池中。
总结
本文基于 JDK 8,介绍了虚拟机的自动内存管理构成以及每个区域是数据线程私有还是共享,会发生哪些异常。
如有不对欢迎指正。