# JVM 的内存布局

不同虚拟机实现可能略微有所不同,但都会遵从 Java 虚拟机规范,Java 8 虚拟机规范规定,Java 虚拟机所管理的内存将会包括以下几个区域:

  • 程序计数器(Program Counter Register)

  • Java 虚拟机栈(Java Virtual Machine Stacks)

  • 本地方法栈(Native Method Stack)

  • Java 堆(Java Heap)

  • 方法区(Methed Area)

# 1. 程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

在 Java 虚拟机的概念模型里,字节码解析器的工作是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于 Java 虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的。在任意一个确定的时刻,一个处理器(对于多核处理器来说就是一个内核)都只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置,每个线程都有独立的程序计数器

如果线程正在执行 Java 中的方法,程序计数器记录的就是正在执行虚拟机字节码指令的地址,如果是 Native 方法,这个计数器就为空(undefined),因此该内存区域是唯一一个在 Java 虚拟机规范中没有规定 OutOfMemoryError 的区域。

# 2. Java 虚拟机栈

与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。

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

  • 如果线程请求的栈深度大于虚拟机所允许的栈深度就会抛出 StackOverflowError 异常。

  • 如果虚拟机是可以动态扩展的,如果扩展时无法申请到足够的内存就会抛出 OutOfMemoryError 异常。

# 3. 虚拟机栈和『栈内存空间』的关系

我们经常会沿用 C/C++ 中的内存分布结构,将 Java 内存空间分为堆内存(Heap)和栈内存(Stack)。这种说法不算错,但不够准确。

上述说法中的『栈内存』空间,严格说起来指的是指『虚拟机栈中局部变量表』部分。

局部变量表存放了编译期可知的各种 Java 虚拟机 8 种基本数据类型、对象引用和 returnAddress 类型(指向了一条字节码指令的地址)。这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中 64 位长度的 long 和 doubl e类型的数据会占用两个变量槽,其余的数据类型只占用一个。

局部变量表所需的内存空间在编 译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定 的,在方法运行期间不会改变局部变量表的大小。

# 4. 本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用Native 方法服务的。

在 Java 虚拟机规范中对于本地方法栈没有特殊的要求,虚拟机可以自由的实现它,因此在 Sun HotSpot 虚拟机直接把本地方法栈和虚拟机栈合二为一了。

与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出 StackOverflowError 和 OutOfMemoryError 异常。

# 5. Java 堆

Java 堆(Java Heap)是 JVM 中内存最大的一块,是被所有『线程共享』的,在虚拟机启动时候创建。在《Java 虚拟机规范》中对 Java 堆的描述是:所有的对象实例以及数组都应当在堆上分配

补充

随着 JIT 编译器的发展和逃逸分析技术的逐渐成熟,栈上分配、标量替换优化的技术将会导致一些微妙的变化,所有的对象都分配在堆上渐渐变得不那么绝对了。

Java 堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作 “GC 堆” 。

堆的内存空间是可以自定义大小的,同时也支持在运行时动态修改,通过 -Xms-Xmx 这两参数去改变堆的初始值和最大值。

-X 指的是JVM运行参数,ms 是 memory start 的简称,代表的是最小堆容量,mx 是 memory max 的简称,代表的是最大堆容量;如 -Xms256M 代表堆的初始值是 256M ,-Xmx1024M 代表堆的最大值是 1024M 。

由于堆的内存空间是可以动态调整的,所以在服务器运行的时候,请求流量的不确定性可能会导致我们堆的内存空间不断调整,会增加服务器的压力,所以我们一般都会将 JVM 的 Xms 和 Xmx 的值设置成一样,同样也为了避免在 GC(垃圾回收)之后调整堆大小时带来的额外压力。

从回收内存的角度看,由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以 Java 堆中经常会出现 新生代老年代永久代Eden 空间From Survivor 空间To Survivor 空间 等名词。这些区域划分仅仅是一部分垃圾收集器的共同特性或者说设计风格而已,而非某个 Java 虚拟机具体实现的固有内存布局,更不是《Java 虚拟机规范》里对 Java 堆的进一步细致划分。因此大家不要混淆了概念。

如果在堆中没有内存完成实例分配,并且堆不可以再扩展时,将会抛 OutOfMemoryError 。

# 6. 方法区

方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域。

方法区与传统语言种的编译代码存储区或者操作系统进程的正文段的作用非常类似,它存储了每一个类的结构信息。例如,运行时常量池、字段和方法数据、构造函数和普通方法的字节码等。

当方法无法满足内存分配需求时会抛出 OutOfMemoryError 异常。

注意,常量池它并非一个独立的区域,它是方法区的一部分。