理解JVM架构

2020-01-07  本文已影响0人  紫色红色黑色

描述

本文翻译Understanding JVM Architecture部分。文中介绍JVM包含类加载子系统、运行时数据区、执行引擎。

JVM架构

JVM 只是一种规范,其实现因厂商而异。 现在,让我们了解一下普遍接受的 JVM 的体系结构。

JVM 架构

1 Class Loader 子系统

JVM 驻留在 RAM 上。 在执行期间,使用 Class Loader 子系统,将类文件传送到 RAM。 这就是所谓的 Java 的动态类加载功能。 JVM 在运行中第一次引用一个类时(而不是编译时),加载、链接和初始化.class文件。

1.1 加载

Class Loader 的主要任务是加载.class文件到内存中。 通常,类加载过程从加载主类(即具有static main()方法声明的类)开始。 所有后续的类加载尝试都是根据已经运行的类中的类引用完成的,如下面所提到的:

有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

Java Class Loader双亲委派

注意:除了上述3个类加载器之外,程序员可以在代码中直接创建自定义类加载器。类加载器委托模型保证了应用程序的独立性。这种方法用于网络应用服务器,比如 Tomcat,使网络应用和企业应用能够独立运行。

每个类加载器都有其存储已加载类的命名空间。 当类加载器加载一个类时,它会根据命名空间中存储的全限定类名(Fully Qualified Class Name)搜索类,以检查类是否已经加载。 即使该类具有相同的全限定类名但是有不同的命名空间,它也被视为不同的类。 不同的命名空间表示该类已由另一个类加载器加载过。

1.2 链接

链接涉及到验证和准备加载后的类或接口、它的直接超类和超接口,以及必需的元素类型,同时遵循以下属性。

链接分为以下三个阶段。

1.3 初始化

在这里,将执行每个加载过的类或接口的初始化逻辑(例如调用类的构造函数)。由于 JVM 是多线程的,对类或接口的初始化应该非常小心地进行,并进行适当的同步,以避免其他线程同时尝试初始化同一个类或接口(确保初始化线程安全)。

这是类加载的最后阶段,所有的静态变量都被赋予了代码中定义的原始值,并且静态块将被执行(如果有的话)。 这在类中从上到下逐行执行,在类层次结构中从父级到子级执行。

2 运行时数据区

运行时数据区域是 JVM 程序在操作系统上运行时分配的内存区域。 除了阅读.class文件,类加载器子系统还生成相应的二进制数据,并在方法区中为每个类分别保存以下信息。

然后,为每一个加载过的.class文件,创建一个 Class 对象来表示在方法区中定义的文件。 这个 Class 对象可以用来读取后面代码中的类级别信息(类名、父名、方法、变量信息、静态变量等)。

2.1 方法区(线程共享)

这是一个共享资源(每个 JVM 只有1个方法区)。 所有 JVM 线程都共享这个方法区,因此对方法数据和动态链接的访问必须是线程安全的。

方法区存储类级别数据(包括静态变量) ,如:

2.2 堆(线程共享)

这也是一个共享资源(每个 JVM 只有1个堆)。 所有对象及其对应的实例变量和数组的信息都存储在堆中。 由于方法区和堆是线程共享的,存储在方法区和堆中的数据不是线程安全的。 堆是 GC 的一个很好的目标。

2.3 栈(线程独有)

这不是一个共享的资源。 对于每个 JVM 线程,当线程启动时,将创建一个单独的运行时栈来存储方法调用。 对于每个方法调用,都会创建一个条目并将其添加(推送)到运行时栈的顶部,这个条目称为栈帧。

每个栈帧都具有局部变量表、 操作数栈和一个类的运行时常量池的引用,这个类就是正在执行的方法所属的类。 编译时确定局部变量表和操作数栈的大小。 因此,根据该方法可以确定栈帧的大小。

当方法正常返回或者在方法调用期间抛出未捕获的异常时,栈帧被移除(弹出)。 还要注意,如果发生任何异常,stack trace 的每一行(如 printStackTrace()这样的方法所示)表示一个栈帧。 栈是线程安全的,因为它不是共享资源。

jvm stack

栈帧可分为三个部分:

由于这些是运行时栈帧,线程终止后,其栈帧也将被 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关键字,则在堆中创建对象等等。

主要的系统线程如下:

原文引用

Understanding JVM Internals
JVM Internals
JVM Explained
The JVM Architecture Explained
How JVM Works — JVM Architecture?
Diffrenence between AppClassloader and SystemClassloader

上一篇下一篇

猜你喜欢

热点阅读