Java 虚拟机运行时数据区域

JVM 对于是每个 Java 程序员掌握一定 Java 基础后,都需要学习的。因为很多代码问题,只能了解了 JVM 底层原理后才能解决。大多数 Java 后端开发者都知道堆(Heap)和栈(Stack)的概念,却没有真正理解其原理。推荐 《深入理解 Java 虚拟机(第二版)》— 周志明著 学习 JVM。

进程和线程

学习 JVM 前要了解进程和线程的概念。

以下是一个类比,来自 阮一峰 — 进程与线程的一个简单解释

  1. 计算机的 CPU 是像一座工厂,时刻在运行。
  2. 因工厂电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个 CPU 一次只能运行一个任务。
  3. 进程就好比工厂的车间,它代表 CPU 所能处理的单个任务。任一时刻,CPU 总是运行一个进程,其他进程处于非运行状态。
  4. 一个车间里,可以有很多工人。他们协同完成一个任务。
  5. 线程就好比车间里的工人。一个进程可以包括多个线程。

进程和线程里面还涉及到 “锁” 的知识,请在参考网址中学习。

下图是:Java 虚拟机运行时数据区

程序计数器(Program Counter Resiger)

首先程序计数器是一块较小的内存空间,“决定” 当前线程字节码的执行顺序,因为它存储字节码的行号。而在多线程中,每个线程都具有一个程序计数器,各条线程之间独立,计数器互不影响,独立存储。程序计数器是 “线程私有” 的内存。

如果线程执行 Java 方法,计数器记录的是虚拟机字节码指令的地址。而如果执行 Native 方法,值为空(Undefined),理解这一段文字需要理解 Native 方法是什么!Native 方法是非 Java 代码(比如 C)编写的方法。程序计数器是 Java 虚拟机规范中唯一没有 OutOfMemoryError 的内存区域。

Java 虚拟机栈(Java Virtual Machine Stacks)

虚拟机栈是 Java 方法(也就是字节码)运行时的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)[帧是一种数据结构] 用于存储局部变量表等。它是 “线程私有” 的,生命周期和线程相同。

我们常说的栈(Stacks)其中一种含义就是 Java 虚拟机栈,确切的说是虚拟机栈中局部变量表部分。局部变量表存放原始(基本)数据类型,其中 Long 和 double 占两个局部变量空间(Slot)-32 位。也存放对象引用和 ruturnAddress 类型(指向了一条字节码指令的地址?)。

局部变量表所需的内存空间在编译期间完成分配,当进入(运行)一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,这是因为局部变量表是有结构的,每个区块按照一定次序存放,所以可以明确知道每个区块的大小。局部变量就是在方法运行期间不会改变局部变量表的大小。

Java 虚拟机规范中规定此区域有两种异常:

  • 每次方法调用都会有一个栈帧压入虚拟机栈,JVM 分配给虚拟机栈的内存是有限的。如果方法调用过多,导致虚拟机栈满了就会溢出。如果线程请求的栈深度(栈帧的数量)大于虚拟机所允许的深度(虚拟机栈的内存),将抛出 StackOverflowError 异常
  • 虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。

本地方法栈(Native Method Stack)

本地方法栈与 Java 虚拟机栈类似,是虚拟机使用到的 Native 方法服务。Sun HotSpot 虚拟机直接本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

Java 堆(Java Heap)

Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。是被所有线程共享的一块内存区域。存放对象实例和数组(数组也是对象),现在不是那么 “绝对” 了。

Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做 “GC 堆”(Garbage Collected Heap)。

根据 Java 虚拟机规范,Java 堆只要逻辑上是连续的即可,就像磁盘空间。可以实现成固定大小的,也可以是可扩展的(大部分虚拟机都可动态扩展)。如果堆没有内存完成实例分配,且堆也无法再扩展时,会抛出 OutOfMemoryError 异常。

方法区(Method Area)

方法区(Method Area)不是存储方法,而是用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Java 虚拟机规范将方法区描述为堆的一个逻辑部分,是逻辑区,但为区分堆,别名叫 Non-Heap(非堆)。

方法区(Method Area)一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。此区域的内存回收目标主要是针对常量池的回收和(对类型的卸载?)。也可能抛出 OutOfMemoryError 异常。

运行时常量池(Runtime Constant Pool)

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic References),这部分内容将在类加载后进入方法区的运行时常量池中存放。

字面量比较接近于 Java 语言层面的常量概念,如文本字符串、 声明为 final 的常量值等。 而符号引用则属于编译原理方面的概念,包括了下面三类常量:

  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符

对于运行时常量池,Java 虚拟机规范没有做任何细节的要求。运行时常量池不同于 Class 文件常量池,运行期间也可能将新的常量放入池中。内存不足时会抛出 OutOfMemoryError 异常。

直接内存(Direct Memory)

直接内存通俗来说就是 I/O 方式使用 Native 堆直接分配堆外内存。

总结

一般来说,每个进程分配一个 “Heap”,每个线程分配一个 “Stack”。因为一个进程中有很多线程,所以 Java 堆等是线程共享的;而 “Stack” 的生命周期跟线程相同,即 “Stack” 是线程独占的,所以程序计数器、Java 虚拟机栈等是线程私有的。

image

“Stack” 的另外两种含义有:

  • 一种数据结构,即一组数据的存放方式,特点是 FILO—First In,Last Out(先进后出)
  • 一种代码运行方式,即 “ 调用栈 “(call Stack),表示函数或子例程像堆积木一样存放,以实现层层调用。

“Stack” 和 Heap 的主要区别是:”Stack” 是有结构的,”Heap” 是没有结构的。因此,”Stack” 的寻址速度要快于 “Heap”。

线程共享的好处:任何时候一个线程改变一些数据,其他线程可以看到它。

?号部分通读全文理解后解释。

参考资料

DeppWang wechat
个人公众号