美文网首页
理解JVM架构

理解JVM架构

作者: 紫色红色黑色 | 来源:发表于2020-01-07 21:52 被阅读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()方法声明的类)开始。 所有后续的类加载尝试都是根据已经运行的类中的类引用完成的,如下面所提到的:

    • 当字节码中对一个类进行静态引用时(例如: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

    Java 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()这样的方法所示)表示一个栈帧。 栈是线程安全的,因为它不是共享资源。

    jvm stack

    栈帧可分为三个部分:

    • 局部变量表:它的索引从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

    相关文章

      网友评论

          本文标题:理解JVM架构

          本文链接:https://www.haomeiwen.com/subject/mawdactx.html