这部分博客对应的是《深入理解 Java 虚拟机》第二章的部分内容,关于 OOM 异常的部分后面再填坑。

运行时数据区 Runtime Data Area

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区
域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而
存在,有些区域则依赖用户线程的启动和结束而建立和销毁。根据Java虚拟机规范的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如下图所示。

Runtime data area
Runtime data area

单个线程内共享的区:

  • PC Register 寄存器
  • JVM Stack 虚拟机栈
  • Native Method Stack 本地方法栈

所有线程共享的区:

  • Heap 堆
  • Method Area 方法区,including Runtime Constant Pool 常量池

PC Register

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能
会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选
取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需
要依赖这个计数器来完成。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,
在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线
程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立
的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私
有”的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值是未定义的。

JVM Stack

每个线程对应一个Java虚拟机栈。在线程创建时同时创建。用于存储栈帧。JVM栈和一般语言,比如 C 语言的栈类似:它存储局部变量和与一些尚未算好的结果,在方法调用和返回时起到很重要作用。因为除了栈帧的出栈和入栈之外,Java 虚拟机栈不会再受其他因素的影响,所以栈帧可以在堆中分配。Java 虚拟机栈所使用的内存不需要保证是连续的。

(请注意避免混淆 Stack、Heap 和 Java Stack、Java Heap 的概念,Java 虚拟机的实现本质上是由其他语言所编写的应用程序,Java 语言程序里分配在 Java Stack 中的数据,从实现虚拟机的程序角度上看则可能分配在 Heap 之中)

native stack

Java 虚拟机实现可能会使用到传统的栈(通常称为 C stack)来支持 native 方法(指使
用 Java 以外的其他语言编写的方法)的执行,这个栈就是本地方法栈(native method stack)。
当 Java 虚拟机使用其他语言(例如 C 语言)来实现指令集解释器时,也可以使用本地方法
栈。如果 Java 虚拟机不支持 native 方法,或是本身不依赖传统栈,那么可以不提供本地
方法栈,如果支持本地方法栈,那这个栈一般会在线程创建的时候按线程分配。
Java 虚拟机规范允许本地方法栈实现成固定大小或者根据计算来动态扩展和收缩。如
果采用固定大小的本地方法栈,那么每一个线程的本地方法栈容量可以在创建栈的时候独立
选定。

Java Heap

Java 虚拟机中,堆(heap)是可供各个线程共享的运行时内存区域,也是供所有类实
例和数组对象分配内存的区域。
Java 堆在虚拟机启动的时候就被创建,它存储了被自动内存管理系统(automatic storage management system,也就是常说的 garbage collector(GC, 垃圾收集器))所管理的各种对象。

方法区

在 Java 虚拟机中,方法区(method area)是可供各个线程共享的运行时内存区域。方法
区与传统语言中的编译代码存储区(storage area for compiled code)或者操作系统进程的正文段(text segment)的作用非常类似,它存储了每一个类的结构信息,例如,运行时常量池(runtime constant pool)、字段和方法数据、构造函数和普通方法的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法。
方法区在虚拟机启动的时候创建,虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实
现可以选择在这个区域不实现垃圾收集与压缩。这个版本的 Java 虚拟机规范也不限定实现方
法区的内存位置和编译代码的管理策略。方法区的容量可以是固定的,也可以随着程序执行的
需求动态扩展,并在不需要过多空间时自动收缩。方法区在实际内存空间中可以是不连续的。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于
存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
Java虚拟机对Class文件每一部分(自然也包括常量池)的格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行,但对于运行时常量池,Java虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。

Java堆栈示意图
Java堆栈示意图

Class 文件简介

概述

class文件是一种8位字节的二进制流文件, 各个数据项按顺序紧密的从前向后排列, 相邻的项之间没有间隙, 这样可以使得class文件非常紧凑, 体积轻巧, 可以被JVM快速的加载至内存, 并且占据较少的内存空间。 我们的Java源文件, 在被编译之后, 每个类(或者接口)都单独占据一个class文件, 并且类中的所有信息都会在class文件中有相应的描述, 由于class文件很灵活,它甚至比Java源文件有着更强的描述能力。

class文件中的信息是一项一项排列的, 每项数据都有它的固定长度, 有的占一个字节, 有的占两个字节, 还有的占四个字节或8个字节, 数据项的不同长度分别用u1, u2, u4, u8表示, 分别表示一种数据项在class文件中占据一个字节, 两个字节, 4个字节和8个字节。 可以把u1, u2, u3, u4看做class文件数据项的“类型” 。

class文件
class文件

class文件结构

类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count - 1
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attribute_count 1
attribute_info attributes attributes_count

魔数、版本号

magic

在class文件开头的四个字节, 存放着class文件的魔数, 这个魔数是class文件的标志,他是一个固定的值: 0XCAFEBABE 。 也就是说他是判断一个文件是不是class格式的文件的标准, 如果开头四个字节不是0XCAFEBABE, 那么就说明它不是class文件, 不能被JVM识别。

minor_version 和 major_version

紧接着魔数的四个字节是class文件的此版本号和主版本号。 随着Java的发展, class文件的格式也会做相应的变动。 版本号标志着class文件在什么时候, 加入或改变了哪些特性。 举例来说, 不同版本的javac编译器编译的class文件, 版本号可能不同, 而不同版本的JVM能识别的class文件的版本号也可能不同, 一般情况下, 高版本的JVM能识别低版本的javac编译器编译的class文件, 而低版本的JVM不能识别高版本的javac编译器编译的class文件。 如果使用低版本的JVM执行高版本的class文件, JVM会抛出java.lang.UnsupportedClassVersionError 。

常量池

在class文件中, 位于版本号后面的就是常量池相关的数据项。 常量池是class文件中的一项非常重要的数据。 常量池中存放了文字字符串, 常量值, 当前类的类名, 字段名, 方法名, 各个字段和方法的描述符, 对当前类的字段和方法的引用信息, 当前类中对其他类的引用信息等等。 常量池中几乎包含类中的所有信息的描述, class文件中的很多其他部分都是对常量池中的数据项的引用,比如后面要讲到的this_class, super_class, field_info, attribute_info等, 另外字节码指令中也存在对常量池的引用, 这个对常量池的引用当做字节码指令的一个操作数。 此外, 常量池中各个项也会相互引用。