描述
本文翻译Understanding JVM Architecture部分。文中介绍JVM包含类加载子系统、运行时数据区、执行引擎。
JVM架构
JVM 只是一种规范,其实现因厂商而异。 现在,让我们了解一下普遍接受的 JVM 的体系结构。

1 Class Loader 子系统
JVM 驻留在 RAM 上。 在执行期间,使用 Class Loader 子系统,将类文件传送到 RAM。 这就是所谓的 Java 的动态类加载功能。 JVM 在运行中第一次引用一个类时(而不是编译时),加载、链接和初始化.class
文件。
1.1 加载
Class Loader 的主要任务是加载.class
文件到内存中。 通常,类加载过程从加载主类(即具有static main()
方法声明的类)开始。 所有后续的类加载尝试都是根据已经运行的类中的类引用完成的,如下面所提到的:
- 当字节码中对一个类进行静态引用时(例如:
System.out
) - 当字节码中创建一个对象时(例如:
Person person = new Person("John")
)
有3种类型的类加载器(继承关系) ,它们遵循4个主要原则。
1.1.1 能见度原则
这个原则指出,子类加载器可以看到父类加载器加载的类,但是父类加载器不能找到子类加载器加载的类。
1.1.2 唯一性原则
这个原则声明父类加载的类不应该再次被子类加载器加载,并确保不会发生重复的类加载。
1.1.3 授权等级原则
为了满足上述两个原则,JVM 遵循一个委托层次结构,为每个类加载请求选择类加载器。 这里,从最低的子级开始,Application Class Loader
将接收到的类加载请求委托给Extension Class Loader
,然后Extension Class Loader
将请求委托给Bootstrap Class Loader
。 如果在 Bootstrap 路径中找到请求的类,则加载该类。 否则,请求将再次传回Extension Class Loader
级别,以从扩展路径或自定义指定的路径查找类。 如果它也失败了,那么请求返回到Application Class Loader
,从 System 类路径查找类,如果Application Class Loader
加载请求的类也失败,那么我们就会得到运行时异常java.lang.ClassNotFoundException
。
1.1.4 不卸载原则
即使Class Loader
可以加载类,但是不能卸载已加载的类。 可以删除当前的Class Loader
而不是卸载,并创建一个新的Class Loader
。

- BootStrap Class Loader 从rt.jar中加载标准JDK类,比如引导路径
$JAVA_HOME/jre/lib
目录中核心Java API类(例如java.lang.*
包下的类)。它是用 c/c++ 这样的本地语言实现的,在 Java 中充当所有类加载器的父类。 - Extension Class Loader 将类加载请求委托给它的父级 Bootstrap,如果不成功,则从扩展目录
$JAVA_HOME/jre/lib/ext
或者由java.ext.dirs
系统属性指定的任何其他目录加载类。这个类加载器sun.misc.Launcher$ExtClassLoader
由 Java 实现。 - Application Class Loader 从系统类路径加载应用程序特定的类,调用程序时使用命令行选项
-cp
或-classpath
设置这些类。 它内部使用映射到java.class.path
的环境变量。 这个类加载器是sun.misc.Launcher$AppClassLoader
由 Java 实现的。
注意:除了上述3个类加载器之外,程序员可以在代码中直接创建自定义类加载器。类加载器委托模型保证了应用程序的独立性。这种方法用于网络应用服务器,比如 Tomcat,使网络应用和企业应用能够独立运行。
每个类加载器都有其存储已加载类的命名空间。 当类加载器加载一个类时,它会根据命名空间中存储的全限定类名(Fully Qualified Class Name)搜索类,以检查类是否已经加载。 即使该类具有相同的全限定类名但是有不同的命名空间,它也被视为不同的类。 不同的命名空间表示该类已由另一个类加载器加载过。
1.2 链接
链接涉及到验证和准备加载后的类或接口、它的直接超类和超接口,以及必需的元素类型,同时遵循以下属性。
- 类或接口必须在链接之前完成加载
- 类或接口在初始化(下一步)之前必须完成验证和准备
- 如果在链接过程中出现错误,它会被抛出到程序中的某个点上,在这个点上,程序将采取一些行动,这些行动可能直接或间接地要求链接到错误中涉及的类或接口
链接分为以下三个阶段。
-
验证:确保
.class
文件正确性。代码是否按照 Java 语言规范正确编写? 代码是否由一个有效的编译器根据 JVM 规范生成?这是类加载过程中最复杂的验证过程,并且花费的时间最长。尽管链接减慢了类加载过程,但在执行字节码时,它避免了多次执行这些检查,从而使整个执行高效。如果验证失败,它将抛出运行时错误java.lang.VerifyError
。例如,执行以下检查:- 符号表一致且格式正确
- final修饰的method/class 不能被重写/继承
- 方法遵循访问修饰符
- 方法参数数量和类型正确
- 字节码不能错误地操作栈
- 变量读之前必须初始化
- 变量值的类型正确
-
准备:为静态变量和 JVM 使用的任何数据结构(如方法表)分配内存。静态字段被创建并初始化默认值,但是,在这个阶段不会执行初始化器,因为这是初始化的一部分
-
解析:用直接引用替换类型中的符号引用。 它是通过搜索方法区来定位被引用的实体
1.3 初始化
在这里,将执行每个加载过的类或接口的初始化逻辑(例如调用类的构造函数)。由于 JVM 是多线程的,对类或接口的初始化应该非常小心地进行,并进行适当的同步,以避免其他线程同时尝试初始化同一个类或接口(确保初始化线程安全)。
这是类加载的最后阶段,所有的静态变量都被赋予了代码中定义的原始值,并且静态块将被执行(如果有的话)。 这在类中从上到下逐行执行,在类层次结构中从父级到子级执行。
2 运行时数据区
运行时数据区域是 JVM 程序在操作系统上运行时分配的内存区域。 除了阅读.class
文件,类加载器子系统还生成相应的二进制数据,并在方法区中为每个类分别保存以下信息。
- 加载过的类及其直接父类的完全限定名
-
.class
文件是否与 Class/Interface/Enum 相关 - 修饰符、静态变量和方法信息等
然后,为每一个加载过的.class
文件,创建一个 Class 对象来表示在方法区中定义的文件。 这个 Class 对象可以用来读取后面代码中的类级别信息(类名、父名、方法、变量信息、静态变量等)。
2.1 方法区(线程共享)
这是一个共享资源(每个 JVM 只有1个方法区)。 所有 JVM 线程都共享这个方法区,因此对方法数据和动态链接的访问必须是线程安全的。
方法区存储类级别数据(包括静态变量) ,如:
- Class Loader 引用
- 运行时常量池——数值常量、字段引用、方法引用、属性;以及每个类和接口的常量,它包含方法和字段的所有引用。 当引用某个方法或字段时,JVM 在运行时常量池中搜索该方法或字段在内存中的实际地址
- 字段数据——每个字段: 名称、类型、修饰符、属性
- 方法数据——每个方法: 名称、返回类型、参数类型(按顺序)、修饰符、属性
- 方法代码——每个方法: 字节码、操作数栈大小、局部变量大小、局部变量表、异常表;异常表中的每个异常处理程序:起始点、结束点、处理程序代码的程序计数器偏移量、捕获异常类的常量池索引
2.2 堆(线程共享)
这也是一个共享资源(每个 JVM 只有1个堆)。 所有对象及其对应的实例变量和数组的信息都存储在堆中。 由于方法区和堆是线程共享的,存储在方法区和堆中的数据不是线程安全的。 堆是 GC 的一个很好的目标。
2.3 栈(线程独有)
这不是一个共享的资源。 对于每个 JVM 线程,当线程启动时,将创建一个单独的运行时栈来存储方法调用。 对于每个方法调用,都会创建一个条目并将其添加(推送)到运行时栈的顶部,这个条目称为栈帧。
每个栈帧都具有局部变量表、 操作数栈和一个类的运行时常量池的引用,这个类就是正在执行的方法所属的类。 编译时确定局部变量表和操作数栈的大小。 因此,根据该方法可以确定栈帧的大小。
当方法正常返回或者在方法调用期间抛出未捕获的异常时,栈帧被移除(弹出)。 还要注意,如果发生任何异常,stack trace 的每一行(如 printStackTrace()这样的方法所示)表示一个栈帧。 栈是线程安全的,因为它不是共享资源。

栈帧可分为三个部分:
-
局部变量表:它的索引从0开始。 对于一个特定的方法,涉及的局部变量相应的值存储在这里。 下标0处存储该方法所属的类实例的引用。 从1开始,保存该方法的入参。 在方法参数确定后,保存方法中的局部变量
-
操作数栈:它充当运行时工作区来保存任何中间操作结果。 每个方法在操作数栈和局部变量表之间交换数据,并推入或弹出其他方法的调用结果。 在编译期间可以确定所需的操作数栈空间的大小
-
帧数据:(译者注:常量池引用等)所有与该方法相关的符号都存储在这里。 对于异常,catch 块信息也将保存在帧数据中
由于这些是运行时栈帧,线程终止后,其栈帧也将被 JVM 销毁。
栈大小可以是动态的或固定的。 如果线程需要比允许的更大的栈,则抛出 StackOverflowError。 如果一个线程需要一个新的栈帧,并且没有足够的内存来分配它,那么就会抛出 OutOfMemoryError。
2.4 PC寄存器(线程独有)
对于每个 JVM 线程,当线程启动时,将创建一个单独的 PC 寄存器(程序计数器),以保存当前正在执行的指令的地址(方法区中的内存地址)。 如果当前的方法是 native 的,那么 PC 是未定义的。 一旦执行结束,PC 寄存器将用下一条指令的地址进行更新。
2.5 本地方法栈(线程独有)
在 Java 线程和本机操作系统线程之间有一个直接的映射。 在为一个 Java 线程准备好所有状态之后,还会创建一个单独的本地栈,以便存储通过 JNI (Java本地接口)调用的本地方法信息(通常用 c/c++ 编写)。
一旦创建并初始化了本机线程,它就会调用 Java 线程中的 run()方法。 当 run()方法返回时,处理未捕获的异常(如果有的话) ,那么本机线程确认 JVM 是否需要由于线程终止而终止(即它是最后一个非 deamon 线程)。 当线程终止时,本机线程和 Java 线程的所有资源都被释放。
一旦 Java 线程终止,本机线程将被回收。 因此,操作系统负责调度所有线程并将它们分派到任何可用的 CPU。
3 执行引擎
字节码的实际执行发生在这里。 执行引擎通过读取上述运行时数据区的数据,逐行执行字节码中的指令。
3.1 Interpreter
解释器解释字节码并逐条执行指令。 因此,它可以快速地解释一行字节码,但是执行解释结果是一个较慢的任务。 缺点是,当一个方法被多次调用时,每次都需要新的解释和较慢的执行。
3.2 JIT 编译器(Just-In-Time)
当一个方法被多次调用时,如果只有解释器可用则每次解释都会发生,如何有效地处理这个冗余操作。 这在 JIT 编译器中已经成为可能。 首先,它将整个字节码编译为本地代码(机器代码)。 然后对于重复的方法调用,它直接提供本地代码,使用本地代码执行比逐条解释指令要快得多。 本地代码存储在缓存中,因此可以更快地执行。
然而,即使对于 JIT 编译器来说,编译也比解释器解释要花费更多的时间。 对于只执行一次的代码段,最好是解释它,而不是编译。 此外,本地代码存储在缓存中,这是一个昂贵的资源。 在这些情况下,JIT 编译器内部检查每个方法调用的频率,并决定只在选定的方法发生的次数超过一定级别时才编译这个方法。 这种自适应编译的思想在 Oracle Hotspot 虚拟机中得到了应用。
3.3 GC(Garbage Collector)
只要对象被引用,JVM 就认为它是活的。 一旦某个对象不再被引用,应用程序代码无法访问它,垃圾收集器将删除该对象并回收未使用的内存。 一般来说,垃圾收集发生在幕后,但是我们可以通过调用 System.gc()方法触发它(同样,执行也不能保证)。
4 Java本地接口(JNI)
此接口用于与执行所需的本地方法库进行交互,并提供本地库的功能(通常用 c/c++ 编写)。 这使 JVM 能够调用 c/c++ 库,也可由 c/c++ 库调用,这些库用于特定的硬件。
5 本地方法库
这是 c/c++ 本地库的集合,执行引擎需要这些库,可以通过提供的本机接口访问它们。
JVM线程
我们讨论了 Java 程序是如何执行的,但没有特别提到执行器。 实际上,为了执行我们前面讨论过的每个任务,JVM 并发运行多个线程。 其中一些线程带有应用逻辑,由程序(应用程序线程)创建,而其余的线程则由 JVM 本身创建,以执行系统中的后台任务(系统线程)。
主要的应用程序线程是主线程,它是作为调用public static void main (String [])
的一部分创建的,所有其他应用程序线程都是由这个主线程创建的。 应用程序线程执行一些任务,比如执行以 main ()方法开始的指令,如果在任何方法逻辑中发现new
关键字,则在堆中创建对象等等。
主要的系统线程如下:
- Compiler threads:在运行时,由这些线程将字节码编译成本地代码
- GC threads:所有与 GC 相关的活动都由这些线程执行
- Periodic task thread:定时器事件(即中断)来调度周期性操作的执行是由这个线程执行的
- Signal dispatcher thread:这个线程接收发送到 JVM 进程的信号,并通过调用适当的 JVM 方法在 JVM 内处理它们
- VM thread:作为前置条件,有些操作需要 JVM 到达一个安全点,在这个点上堆的修改不再发生。 此类场景的示例包括
stop-the-world
垃圾收集、线程栈转储、线程挂起和偏向锁撤销。 这些操作可以在一个称为 VM 线程的特殊线程上执行
原文引用
Understanding JVM Internals
JVM Internals
JVM Explained
The JVM Architecture Explained
How JVM Works — JVM Architecture?
Diffrenence between AppClassloader and SystemClassloader
网友评论