做有关 Java 程序的开发一定知道Java的内存自动管理,不用再像 C/C++ 程序那样手动控制(分配和释放)内存。开发人员可以把更多的精力放到业务的开发上面,但是这种内存的自动管理功能有利也有弊,当程序出现内存泄漏或内存溢出的问题时,如果不熟悉 Java 虚拟机(JVM - Java Virtual Machine)的内存模型,往往很难排查问题的根源。
掌握 Java 虚拟机的相关知识可以说是中级程序员必备的一项技能。另一方面,Java 虚拟机作为面试官的必问题目,熟悉相关的知识也是不错的充电和储备。
本文包括了 Java 虚拟机的内存区域和有关异常的内容。
运行时数据区
JVM 在运行的时候会把祂管理的内存划分为几个不同的数据区域,这些区域都有各自的用于,以及创建和销毁时间。
根据《Java 虚拟机规范》的定义,JVM 管理的内存将包括以下几个区域
程序计数器
程序计数器是一块较小的内存空间,可以看作是当前 线程 所执行的字节码的 行号 指示器。这句话不太好懂,个人理解,程序计数器其实就是用来记录 每个线程 执行到什么地方了,程序计数器是 线程私有 的,及内个线程拥有自己的内存空间,如上图所示,程序计数器属于 线程隔离数据区。
- 如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址
- 如果线程正在执行的是一个本地(Native)方法,这个计数器的值则为 未定义(Undefined)
Java 虚拟机栈
和程序计数器一样,Java 虚拟机栈也是 线程私有 的,它的生命周期与线程相同,随着线程的创建而创建,销毁而销毁。
虚拟机栈描述的是 Java方法 执行的内存模型:每个方法在执行的同时都会创建一个 栈帧(Stack Frame) 用于存储 局部变量表、操作数栈、动态链接、方法出口 等信息。每个方法从调用到执行完成的过程,就对应一个 栈帧 在 虚拟机栈 中入栈和出栈的过程。
该内存区域会抛出两种异常:
- 如果线程请求的栈深度大于虚拟机允许的深度,将抛出 StackOverflowError 异常。
- 如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,将抛出 OutOfMemoryError 异常。
下面通过代码来演示
public class StackOverflowTest {
public static void main(String[] args) {
new Test().stackOverflow();
}
private static class Test {
public void stackOverflow() {
stackOverflow();
}
}
}
运行以上代码将会得到 java.lang.StackOverflowError
本地方法栈
本地方法栈 与 Java 虚拟机栈非常相似,只不过不是执行的 Java 方法,而是 本地方法。最常见的 JVM 实现 HotSpot,将 Java虚拟机栈 和 本地方法栈 合二为一,即为同一内存区域。
Java 堆
- 堆(Heap) 应该是大家接触的最多一个内存区域,也是 JVM 中管理内存最大的一块。
- 堆 是 线程共享 的内存区域,即每个线程都可以访问到该内存区域。
- 堆 的唯一作用就是用来存放 Java 程序中的 对象(实例)
- 堆 也是 垃圾收集器 管理的主要区域,有时也会被称为 GC堆
- 堆还可以划分为:新生代 和 老年代
- 新生代还可以划分为:Eden 空间、From Survivor 空间 和 To Survivor 空间
- 堆 中还可以划分出多个 线程私有 的分配缓冲区(Thread Local Allocation Buffer,TLAB)
- 划分空间的作用时为了更有效的进行垃圾回收,不论怎样划分,堆 内存空间里面存储的都是 对象(实例)
- 如果堆中没有内存再分配新的对象时,会抛出 OutOfMemoryError 异常
下面通过代码来演示
public class OutOfMemoryTest {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
while (true) {
strings.add("");
}
}
}
运行以上代码将会得到
注意:异常错误信息中显示了内存溢出的区域为:Java heap space 即 Java 堆内存空间
方法区
- 与 堆 一样 方法区 也是 线程共享 的内存区域
- 该区域存储 已加载的类信息、常亮、静态变量、即时编译器(JIT)编译后的代码 等数据
- 当方法区中没有内存再分配新的对象时,会抛出 OutOfMemoryError 异常
- 方法区中有一部分是 运行时常量池,该区域用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载以后存放于方法区的 运行时常量池 中
HotSpot 虚拟机
上面介绍了 JVM 运行时数据区,我们通过下面的图再回顾一下
这些都是 JVM 规范所定义的,接下来说一说业界用的最多的 JVM 实现 -- HotSpot 的相关内容
关于如何查看使用的是什么 JVM 可以使用如下命令
java -version
如在 Windows 系统下会打印出以下内容
$ java -version
java version "1.8.0_191"
Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)
在 Linux 系统下会打印出以下内容
$ java -version
openjdk version "1.8.0_171"
OpenJDK Runtime Environment (build 1.8.0_171-b10)
OpenJDK 64-Bit Server VM (build 25.171-b10, mixed mode)
资料来源:周志明 ——《深入理解 Java 虚拟机》