likes
comments
collection
share

解构Java虚拟机——执行引擎

作者站长头像
站长
· 阅读数 4

在Java虚拟机(JVM)的错综复杂的景观中,执行引擎占据中心舞台,发挥着解释字节码并执行即时(JIT)编译以进行性能优化的关键作用。字节码是Java源代码与JVM之间的中介语言,在程序执行期间,执行引擎动态地将其转换为本机机器代码。JVM采用的基于堆栈的执行模型操作操作数栈,在解释字节码指令时推入和弹出操作数。虽然字节码解释确保了平台的独立性,但由于额外的抽象层,它无法始终提供最佳性能。

为了解决性能挑战,JVM引入了JIT编译。这种策略性的优化技术识别频繁执行的代码段,或称热点,并在运行时动态将它们编译为本机机器代码。通过选择性地优化热点,JVM平衡了可移植性和性能,显着提高了Java应用程序的执行速度。本章深入探讨了字节码解释和JIT编译的细微差别,揭示了这些过程如何协同作用,使JVM成为Java程序的强大且适应性强的运行时环境。

在本章中,我们将探讨以下主题:

  • 执行的基础
  • 系统操作层
  • 解析JVM执行
  • JIT编译
  • 类加载

执行的基础

在深入理解将Java源代码转换为类文件和字节码的编译过程后,我们现在将重点放在JVM执行的迷人领域上。这个关键阶段是魔法发生的地方,因为JVM接管了控制权,使我们的Java程序活了过来。

当JVM接收到包含字节码的编译类文件时,执行引擎开始工作。字节码是我们Java程序的中介表示,在基于堆栈的执行模型中进行解释。执行引擎动态执行字节码指令,操作一个操作数栈。这种基于堆栈的方法允许JVM有效地处理指令,将操作数推入和弹出堆栈。虽然字节码解释确保了平台的独立性,但可能引入性能考虑,这将引导我们进入执行旅程的下一个关键步骤。

当执行JVM程序时,会展开几个步骤,将Java应用程序带入生活:

  1. 加载:类加载器定位并将编译的Java类文件(字节码)加载到JVM中。这包括核心Java库和任何用户定义的类。
  2. 验证:加载的字节码经过验证过程,以确保其符合Java语言规范,防止潜在有害代码的执行。
  3. 准备:为类变量和静态字段分配内存空间,并用默认值初始化它们。
  4. 解析:字节码中的符号引用解析为具体引用,确保类和方法可以正确链接。
  5. 初始化:执行类的静态块和变量,为使用初始化类。
  6. 执行:调用main()方法或指定的入口点,并开始程序的执行。

随着Java类文件在JVM中占据主导地位,一系列复杂的流程展开,为Java应用程序的执行铺平道路。类文件作为Java源代码的编译表示,成为焦点,因为JVM的类加载器精心地定位并将其加载到运行时环境中。一旦加载,JVM会执行一系列步骤,从验证字节码的遵循语言规范到解析符号引用和初始化类变量。这些步骤的结果是转换后的类文件在JVM中运行。通过调用main()方法或指定的入口点,应用程序开始其运行时旅程,每行代码都会动态解释和执行。类文件、JVM和运行的应用程序之间的协同作用展示了在JVM的灵活和适应性环境中执行Java程序的复杂舞蹈,如下图所示:

解构Java虚拟机——执行引擎

每次执行Java应用程序时,JVM都会创建一个独特的运行时环境。然而,重要的是要注意,JVM利用各种技术在每个运行时内优化性能。一个显著的优化技术是JIT编译器。在同一应用程序的重复执行中,JVM会识别特定运行时中频繁执行的代码路径,称为热点,并将它们动态编译为本机机器代码。这些编译的代码存储在同一运行时内存中,减少了对相同字节码的重复解释的需求,并显著提高了在特定运行时内的执行速度。此外,JVM实现可能会使用缓存机制来存储频繁访问的类和资源,进一步优化应用程序在每个运行时内的性能。

总之,JVM在每个运行时内优化性能,而JIT编译和缓存机制的好处适用于单个执行实例,确保应用程序在其特定的运行时环境中高效运行。尽管有这些优化,但重要的是要注意,当Java应用程序停止时,它们将丢失。因此,每次在同一台或另一台计算机上运行应用程序时,整个优化和本地代码编译的过程都必须重新进行。持续进行的项目,如Leyden项目(openjdk.org/projects/le…),旨在解决这一挑战。Leyden项目的主要目标是通过让开发人员对应用了哪些优化进行更多控制,改进Java程序的启动时间、达到性能峰值的时间以及总体占用空间。然而,值得注意的是,在项目的当前状态下,对这种控制的范围可能受到限制。在这个背景下,另一个值得注意的项目是CRaC(Coordinated Restore at Checkpoint)(docs.azul.com/core/crac/c…),这是一个旨在优化Java程序启动时间和资源利用率的JDK项目。CRaC允许您在第一次事务开始时更快地启动Java程序,并且需要更少的资源来达到完整代码速度。它通过在Java进程完全热身时拍摄Java进程的快照(检查点)来实现这一点。然后,它使用该快照从捕获的状态启动多个JVM,利用本机Linux功能。值得一提的是,还存在诸如Open Liberty的InstantOn等替代方案,这些都是专有技术。此外,AWS Lambda SnapStart使用了CRaC API,展示了此检查点方法的实际应用。流行的框架,如Spring、Micronaut和Quarkus,也支持CRaC检查点,使其成为进一步优化Java应用程序性能的有希望的方法。

字节码解释器是JVM中的一个关键组件,它在执行Java程序时起着至关重要的作用。当启动Java应用程序时,JVM加载先前编译的Java源代码生成的字节码,通常打包到JAR文件中。然后,字节码解释器会精确地解释这些字节码,按照一步一步的过程获取、解码和执行每个指令。

在其核心,字节码解释器遵循平台独立原则。在任何装有JVM的设备上执行相同的字节码,使得Java应用程序能够在不同的环境中无缝运行,无需修改。这种适应性是Java著名的“一次编写,到处运行”哲学的基础,使开发人员摆脱了对底层硬件和操作系统的担忧。

在基于堆栈的模型上运行,解释器在执行操作时通过将操作数推入和从堆栈中弹出来导航字节码指令。这种基于堆栈的方法使得字节码处理高效,并为Java应用程序的适应性和快速启动时间做出了贡献。虽然解释的代码可能无法与本地编译的代码速度匹配,但字节码解释器通过提供快速启动的灵活性以及在跨平台开发中定义Java实力的可移植性,取得了平衡。

系统操作层

系统操作层构成了支撑现代计算机顺畅功能的基础架构。这些层次结构是分层的,每一层在编排硬件和软件之间的协作中都有着独特的作用。让我们揭示这些层的重要性,并理解它们对计算机系统运行的关键性:

  • 硬件层:在最低层,硬件层由计算机系统的物理组件组成——处理器、内存、存储设备和输入/输出设备。它为所有更高级别的操作和软件功能提供了基础。
  • ISA层:在硬件层之上是ISA层,定义了软件与硬件之间的接口。它包括处理器理解的指令集和体系结构。ISA层充当桥梁,允许软件与和利用底层硬件资源进行通信。
  • 操作系统层:坐落在ISA层之上的是操作系统,它是应用软件和硬件之间的重要中介。它管理资源,为应用程序提供运行时环境,并促进软件与硬件组件之间的通信。
  • 应用层:最顶层包括应用软件,包括为满足特定用户需求而设计的程序和工具。这一层与操作系统交互,以有效地执行任务并利用硬件资源。

在这个视觉快照中,见证计算机层层叠叠的芭蕾舞,硬件作为有形的动力源奠定了基础。ISA层是一个至关重要的桥梁,定义了软件和硬件之间的语言。上升至操作系统,它是一个指挥家,编排着动态的互动。这个图表概括了计算层的本质,展示了将我们的数字景观带入生活的相互关联的舞蹈。

当我们从JVM的微妙工作转向更广泛的视角时,我们的旅程现在在系统操作的层面展开。系统的基础,即硬件层,提供原始动力,而指令集架构(ISA)层则是中介语言。在这些层面之上,操作系统编排资源的和谐,为应用程序层铺平道路。当我们探索每个层面的重要性时,我们揭示了JVM如何与硬件合作、通过ISA进行通信、与操作系统协同,最终在计算交响乐的顶峰上显现出Java应用程序。让我们开始这个分层的探险,以理解系统操作的复杂动态。

解构Java虚拟机——执行引擎

在计算的错综复杂编排中,JVM如同一位优雅的舞者,无缝地跨越系统层之间的鸿沟。当我们探索JVM与硬件、ISA以及操作系统等基础层之间的共生关系时,一个引人入胜的故事在眼前展开:

与ISA和硬件的互动:JVM通过操作系统间接与ISA层和硬件交互。它依赖于ISA层的指令集来执行字节码,而操作系统则代表JVM管理硬件资源。

与操作系统的协作:JVM与操作系统层密切合作,利用其服务进行内存管理、文件操作和其他系统相关任务。JVM将底层硬件和操作系统的差异抽象化,为Java应用程序提供一个与平台无关的执行环境。

应用程序执行:JVM是Java应用程序在应用程序层中的运行时环境。它解释和执行Java字节码,确保Java程序能够在各种平台上一致运行,而不需要直接考虑底层硬件或操作系统的具体细节。

实质上,JVM在高级应用程序层与低级系统层之间扮演着至关重要的桥梁角色,将硬件和操作系统的细节抽象化,为Java应用程序提供了一个标准化和可移植的执行环境。

随着我们结束对系统操作层及其错综复杂的舞蹈的探索,我们发现自己站在一个更深刻的启示的边缘——JVM的微妙执行。在这一点上,我们已经领悟了抽象、资源管理、互操作性和安全性的重要性,见证了这些支柱如何塑造了计算的本质。我们的旅程推动我们解开JVM执行下面的层次。在下一节中,让我们深入探讨JVM执行的复杂性,解码当Java应用程序活跃起来时所发生的魔法。我们的探索连续不断,承诺更深入地理解JVM与我们揭示的层次之间的共生关系。

解析JVM执行

在JVM执行的编排中,性能在不同阶段展现出来,每个阶段都为Java应用程序的顺畅功能做出贡献。序曲始于JVM的加载,其中类加载器勤勉地获取和加载类文件和字节码到内存中,为接下来的性能表演做好准备。

当帷幕升起时,JVM的执行引擎开始主导,以堆栈为基础的执行模型动态解释字节码。同时,数据区被精心初始化,为堆和栈等运行时组件分配内存空间。这场编排的舞蹈以与本地元素的集成达到高潮,将本地库无缝地链接起来,以增强应用程序的能力。请加入我们在接下来的部分,我们将更深入地探讨JVM执行的复杂性,揭示Java应用程序在这个精心调节的交响乐中如何活跃起来的魔力。

当JVM应用程序执行时,它遵循以下步骤:

  • 序曲始于JVM本身的加载。这个关键阶段涉及到类加载器定位和加载必要的类文件和字节码到JVM的内存中。类加载器充当了一个门卫,确保所需的类可供执行。
  • 随着舞台的搭建,JVM的执行引擎开始登场。最初,字节码以堆栈为基础的执行模型进行解释。随着每个字节码指令的动态执行,应用程序开始成形,JVM将高级代码转化为可执行的指令。
  • 与此同时,JVM初始化其数据区,为程序的运行时组件划分内存空间。其中包括堆的位置,用于分配对象,以及栈,用于管理方法调用和局部变量。数据区的精心组织确保了应用程序生命周期中的有效内存管理。
  • 随着应用程序的发展壮大,JVM与本地环境无缝集成。这涉及链接本地库并将其纳入执行中。本地集成弥合了Java与特定平台功能之间的差距,增强了应用程序的能力和性能。下图显示了这一流程的流程:

解构Java虚拟机——执行引擎

这一系列阶段的交响乐概括了Java应用程序在JVM内的动态执行过程。从最初的加载到字节码的解释,再到数据区的精心组织以及与本地元素的无缝集成,每个阶段都为Java应用程序的和谐性能做出了贡献。请加入我们在下一节,深入探讨每个阶段,揭示JVM执行的复杂性,并解开Java的适应性和跨平台能力背后的魔法。

在JVM执行的复杂交响乐中,我们已经穿越了加载、字节码解释、数据区初始化和本地集成等阶段,见证了将Java应用程序带入生活的无缝编排。随着本章的结束,它作为对即将进行的JIT编译转换领域的探索的序曲。在下一节中,我们将揭开由JIT编译进行的动态优化,其中字节码在运行时被转换为本机机器代码,为Java应用程序开启新的性能维度。请加入我们,深入探讨JVM执行的不断发展的交响乐,探索即时优化的艺术以及JIT编译为Java编程世界带来的无与伦比的适应性。

JIT编译

JIT编译是JVM中的一个关键组件,彻底改变了Java应用程序的执行方式。与传统的静态编译不同,静态编译是在执行之前将整个代码转换为机器码,而JIT编译是在运行时动态进行的。这种即时编译将Java字节码在执行前转换为本机机器码,通过优化性能和适应机器的方式来考虑代码中使用最多且需要优化的部分。这种动态优化过程确保了JVM专注于代码中最频繁执行的部分,有效地增强了性能,并适应了特定运行时条件。

JVM内部采用JIT编译的目标在于在可移植性和性能之间取得平衡。通过最初解释字节码,然后有选择地将频繁执行的代码路径编译成本机机器码,JVM利用了解释和编译两种方法的优势。这种方法使得Java应用程序能够保持平台独立性,同时达到与原生编译语言相当的性能。

在JVM执行的错综复杂中,JIT编译的层次起着平衡适应性和性能的关键作用。让我们深入了解这些级别,理解它们的存在原因以及它们如何共同增强Java应用程序的执行。

多个JIT编译级别的存在使JVM能够在解释的优势和本机机器码的性能优势之间取得微妙的平衡。解释器提供了敏捷性和平台独立性,而JIT编译器则优化了热点,确保Java应用程序动态地适应其执行环境。这种自适应的编译方法在不牺牲Java跨平台特性的前提下,实现了高性能的结果。请加入我们在即将到来的部分,我们将深入剖析JIT编译的内部工作原理,揭示这些级别如何协同合作,为Java运行时环境赋予强大的能力:

解释器级别:在解释器级别,JVM利用解释器动态执行Java字节码。这个解释器是平台无关字节码和底层硬件之间的初始桥梁。当Java程序被执行时,解释器逐个读取字节码指令,并在执行过程中即时将它们转换为机器码。尽管这种方法具有快速启动和平台独立性等优点,但由于解释过程的存在,它引入了固有的开销,这可能会影响执行速度。

解释器本质上充当着迅速的执行者,使Java应用程序能够在不需要预编译本机代码的情况下在任何平台上运行。然而,由于在执行过程中将字节码实时转换为机器码,整体性能可能没有得到最优化。这就是随后的JIT编译级别发挥作用的地方,它旨在通过选择性地将频繁执行的代码路径(即热点)翻译和优化为本机机器码,以提高性能。因此,解释器级别在敏捷性和适应性之间提供了平衡,为更高级的JIT编译阶段奠定了基础。

基准JIT编译:基准JIT编译代表JVM内动态编译过程中的下一个层次。在对字节码进行初始解释后,JVM识别出特定部分频繁执行的代码,称为热点。这些热点是进一步优化以增强整体性能的候选项。这就是基准JIT编译器介入的地方。

在基准JIT编译阶段,编译器采用选择性编译,针对识别出的热点而不是整个程序。着重于代码的频繁执行部分,将它们在执行前转换为本机机器码。基准JIT编译器强调快速编译以立即提升性能,利用简单快速的翻译技术,明显改善了重复解释。动态适应至关重要,因为编译器持续监控应用程序的执行,识别并选择性地编译热点。这种敏捷的响应确保了优化工作集中在最具影响力的领域,与不断变化的运行时行为保持一致,并针对即时性能提升进行优化。

动态适应:基准JIT编译中的动态适应是指编译器对Java应用程序不断变化的运行时行为的敏捷响应。编译器不断监控执行过程,识别频繁执行的代码部分或热点,并将它们编译成本机机器码。这种自适应策略确保了基准JIT编译器将其优化工作集中在最具影响力的领域,优化即时性能提升。

动态适应的重要性在于它能够平衡快速编译和有效性能提升。通过根据运行时行为调整其策略,编译器对工作负载的变化保持敏感,调整其策略以匹配Java程序的演变执行模式。它确保了基准JIT编译器,也称为C1编译器,保持动态和有效,实时优化Java应用程序,使其能够应对各种各样和动态的工作负载。

值得注意的是,这种动态适应与AOT编译代码的主要区别。这种代码始终以相同的方式工作,不能适应当天的使用情况,而JIT编译器则能够完美地处理这一点。JIT编译器根据运行时行为调整其优化策略的能力,使其成为在各种场景中最大化Java应用程序性能的强大工具。

在我们探索JIT编译的过程中,我们见证了其在动态优化Java字节码方面所具有的转变性力量。从解释器的快速适应性到基准JIT编译器的选择性编译能力,JIT的错综复杂的舞蹈已经展开。随着本章的落幕,舞台为更深层次的揭示——类加载在Java运行时动态中的角色铺设了基础。请加入我们在下一节中,我们将揭示类加载的细微差别,探索将类动态加载到JVM中如何构建Java的可扩展性和动态性的基石。我们的旅程的延续承诺着从JIT的动态编译编排到类加载的后台奇迹的无缝过渡。

类加载

在这个启发性的部分中,我们深入探讨了类加载的复杂世界,这是Java动态和可扩展性的基石。加入我们解开动态类加载背后的机制,它允许Java应用程序在运行时适应和扩展其功能。我们将探索ClassLoader,这个不为人知的英雄负责将Java类动态加载到JVM中。深入了解类加载器层次结构的微妙之处,理解不同类加载器如何协同工作,组装Java应用程序的丰富纹理。从系统类加载器到自定义类加载器,我们将穿越支撑Java能够动态包含新类并扩展其功能的层次。准备好进入Java运行时动态的核心,那里类加载的魔力展现出来。

在Java中,类加载的领域由两个明确定义的实体划分:引导类加载器(bootstrap class loader),作为JVM的一个重要部分,以及用户定义的类加载器。每个用户定义的类加载器都是ClassLoader抽象类的子类的实例,它赋予应用程序定制JVM动态生成类的方式。这些用户定义的类加载器充当了扩展JVM创建类的传统方式的渠道,允许从典型类路径之外的源包含类。

当JVM将定位类或接口N的二进制表示委派给类加载器L(记为L时),它启动了一个动态的过程。类加载器L在接收到此请求后,加载与N相关联的指定类或接口C。这种加载可以直接发生,L获取二进制表示,并指示JVM从中实例化C。另外,L也可以选择间接加载方法,将任务委托给另一个类加载器。这种间接加载可能涉及到委派的类加载器直接加载C,或者使用进一步的委派层,直到最终加载C为止。这种灵活性使得Java应用程序能够无缝集成来自各种源的类,包括从网络获取的类、即时生成的类或从加密文件中提取的类。因此,用户定义的类加载器的动态特性在塑造Java应用程序的可扩展性和适应性方面发挥了关键作用。

理解类加载和创建对于Java的适应性至关重要,有助于在运行时动态添加类。通过引导类加载器,JVM检查它是否将此加载器记录为给定类或接口的初始化器。如果有记录,并且已识别的类或接口存在,则流程结束。否则,引导类加载器会找到一个表示,并指示JVM从中派生类,然后创建它。

用户定义的类加载器为此过程引入了一个动态层。JVM检查用户定义的类加载器是否已记录为已识别类或接口的初始化器。如果有记录,并且类或接口存在,则不会采取进一步的操作。否则,JVM调用类加载器的loadClass方法,指示其直接从获得的字节加载和创建类或接口,或者将加载过程委派给另一个类加载器。

无论是通过引导类加载器还是用户定义的类加载器,类加载和创建的动态性赋予了Java应用程序无与伦比的灵活性。这种适应性允许从各种源集成类,有助于定义Java编程语言的可扩展性和动态性。我们对类加载的探索为理解Java如何在运行时无缝适应和演变奠定了基础,为深入探讨Java运行时环境错综复杂的交响乐提供了舞台。