【译】关于JVM — JAVA虚拟机的一篇简单科普转载
JAVA的美丽是JVM。
只要你使用java,你迟早会接触到Java虚拟机(JVM)。
背景
1995年,James Gosling为Sun Microsystems设计了JAVA,Java是一种多范式(即面向对象类、结构、命令式、泛型、反光、并发)编程语言,深受数百万开发人员的喜爱。在任何给定的排名指数上,Java成为过去15年来最受欢迎的语言。在过去的15年里,开发的数万个企业应用程序大多是用Java编写的,使其成为构建企业级生产软件系统的首选语言。尽管自从我意识到Java生态系统性能方面的力量以来,我一直在使用Java,它激励着更深入地挖掘Java的世界。
JVM — JAVA虚拟机
JVM是Java生态系统的核心,它使基于Java的软件程序能够遵循WORA“写一次,在任何地方运行”方法。您可以在一台机器上编写Java代码,并使用JVM在任何其他机器上运行。
JVM最初设计为仅支持Java。然而,随着时间的推移,Java平台采用了许多其他语言,如Scala、Kotlin和Groovy。所有这些语言统称为JVM语言。
JVM的工作原理,以及各种组件。
你觉得呢?什么是虚拟机?
在我们看到JVM的概念之前,让我们看看虚拟机(VM)的概念。
虚拟机是物理计算机的虚拟表示。我们可以称虚拟机为来宾机,它运行的物理计算机是主机。
一台物理机器可以运行多台虚拟机,每台虚拟机都有自己的操作系统和应用程序。这些虚拟机彼此隔离,它们看起来就像一个真正的计算机操作系统。
什么是Java虚拟机(JVM)?
在C和C++等编程语言中,代码首先编译成特定于平台的机器代码。这些语言被称为编译语言。
另一方面,在JavaScript和Python等语言中,计算机直接执行指令,而无需编译它们。这些语言被称为解释语言。
Java使用这两种技术的组合。Java代码首先编译为字节代码以生成类文件。然后,Java虚拟机为底层平台解释此类文件。同一类文件可以在任何平台和操作系统上运行的任何版本的JVM上执行。
与虚拟机类似,JVM在主机上创建一个隔离空间。无论机器的平台或操作系统如何,此空间都可以用于执行Java程序。
Java虚拟机架构
JVM由三个不同的组件组成:
- 类加载器
- 运行时数据区域
- 执行引擎
现在我们将详细看到他们每个人。
类加载器
当您编译.java
源文件时,它会作为.class
文件转换为字节代码。当您尝试在程序中使用此类时,类加载程序会将其加载到主内存中。
加载到内存中的第一个类通常是包含main()
方法的类。
类加载过程有三个阶段:加载、链接和初始化。
正在加载 —
加载涉及获取具有特定名称的类或接口的二进制表示(字节码),并从中生成原始类或接口。
Java中有三个内置的类加载程序:
Bootstrap类加载程序—这是根类加载程序。它是扩展类加载器的超类,并加载标准Java软件包,如java.lang
、java.net
、java.util
、java.io
等。这些软件包存在于rt.jar
文件和$JAVA_HOME/jre/lib
目录中的其他核心库中。
扩展类加载器—这是Bootstrap类加载程序的子类和应用程序类加载程序的超类。这加载了$JAVA_HOME/jre/lib/ext
目录中存在的标准Java库的扩展。
应用程序类加载程序—这是扩展类加载程序的最后一个类加载程序和子类。它加载类路径上存在的文件。默认情况下,类路径设置为应用程序的当前目录。也可以通过添加-classpath
或-cp
命令行选项来修改类路径。
JVM使用ClassLoader.loadClass()
方法将类加载到内存中。它试图根据完全限定的名称加载类。
如果父类加载程序找不到类,它会将工作委托给子类加载程序。如果最后一个子类加载程序也无法加载该类,它将抛出NoClassDefFoundError
或ClassNotFoundException
。
链接 —
类加载到内存中后,它会经历链接过程。链接类或接口涉及将程序的不同元素和依赖项组合在一起。
链接包括以下步骤:
验证—此阶段通过根据一组约束或规则检查.class
文件来检查其结构正确性。如果验证因某种原因失败,我们将获得VerifyException
。
例如,如果代码是使用Java 11构建的,但在安装了Java 8的系统上运行,则验证阶段将失败。
准备—在此阶段,JVM为类或接口的静态字段分配内存,并使用默认值初始化它们。
例如,假设您在类中声明了以下变量:
private static final boolean doit = true;
在准备阶段,JVM为变量doit
分配内存,并将其值设置为布尔值的默认值,这是false
的。
分辨率—在此阶段,符号引用被运行时常量池中存在的直接引用所取代。例如,如果您对其他类中存在的其他类或常量变量有引用,它们将在此阶段得到解决,并替换为它们的实际引用。
初始化 —
初始化涉及执行类或接口的初始化方法(称为<clinit>
)。这可以包括调用类的构造函数,执行静态块,并为所有静态变量分配值。这是类加载的最后阶段。
例如,当我们早些时候声明以下代码时:
private static final boolean doit= true;
在准备阶段,变量doit
被设置为false
的默认值。在初始化阶段,该变量被分配其true
值。
注意—JVM是多线程的。可能会发生多个线程试图同时初始化同一类。这可能会导致并发问题。您需要处理线程安全,以确保程序在多线程环境中正常工作。
运行时数据区域
运行时数据区域内有五个组件
方法区域—
所有类级数据,如运行时常量池、字段和方法数据,以及方法和构造函数的代码,都存储在这里。
如果方法区域中可用的内存不足以启动程序,JVM会抛出OutOfMemoryError
。
方法区域是在虚拟机启动时创建的,每个JVM只有一个方法区域。
堆面积—
所有对象及其相应的实例变量都存储在这里。这是分配所有类实例和数组内存的运行时数据区域。
该堆是在虚拟机启动时创建的,每个JVM只有一个堆区域。
注意:由于方法和堆区域为多个线程共享相同的内存,因此存储在这里的数据不是线程安全的。
堆栈区域-
每当在JVM中创建新线程时,也会同时创建一个单独的运行时堆栈。所有局部变量、方法调用和部分结果都存储在堆栈区域中。
如果线程中正在完成的处理需要比可用堆栈大小更大的堆栈大小,JVM会抛出StackOverflowError
。
对于每个方法调用,堆栈内存中都有一个条目,称为堆栈框架。当方法调用完成后,堆栈框架将被销毁。
堆栈框架分为三个子部分:
- 局部变量—每个帧都包含一个称为其局部变量的变量数组。所有局部变量及其值都存储在这里。此数组的长度在编译时确定。
- 操作数堆栈—每个帧都包含一个最后入先出(LIFO)堆栈,称为其操作数堆栈。这充当执行任何中间操作的运行时工作区。此堆栈的最大深度在编译时确定。
- 帧数据—与该方法对应的所有符号都存储在这里。在出现异常时,这也存储捕获块信息。
注意:由于堆栈区域不是共享的,因此它本质上是线程安全的。
程序计数器(PC)寄存器-
JVM同时支持多个线程。每个线程都有自己的PC寄存器来保存当前执行的JVM指令的地址。指令执行后,PC寄存器将更新为下一个指令。
原生方法堆栈-
JVM包含支持本机方法的堆栈。这些方法是用Java以外的语言编写的,例如C和C++。对于每个新线程,还分配一个单独的本机方法堆栈。
执行引擎—
一旦字节码加载到主内存中,并且运行时数据区域中有详细信息,下一步就是运行程序。执行引擎通过执行每个类中存在的代码来处理这个问题。
但是,在执行程序之前,字节码需要转换为机器语言指令。JVM可以使用解释器或JIT编译器作为执行引擎。
口译员—
解释器逐行读取和执行字节码指令。由于逐行执行,解释器相对较慢。
解释器的另一个缺点是,当多次调用方法时,每次都需要新的解释。
JIT编译器—
JIT编译器克服了口译员的缺点。执行引擎首先使用解释器执行字节代码,但当它找到一些重复的代码时,它使用JIT编译器。
然后,JIT编译器编译整个字节码并将其更改为本机代码。此原生机器代码直接用于重复的方法调用,这提高了系统的性能。
JIT编译器具有以下组件:
- 中级代码生成器 - 生成中间代码
- 代码优化器—优化中间代码以获得更好的性能
- 目标代码生成器-将中间代码转换为本机代码
- 分析器—查找热点(重复执行的代码)
注意:JIT编译器编译代码比解释器逐行解释代码花费的时间要多。如果您只运行一次程序,使用解释器会更好。
垃圾收集器—
垃圾收集器(GC)从堆区收集和删除未引用的对象。这是通过销毁运行时未使用的内存自动回收它们的过程。
垃圾收集使Java内存高效,因为它从堆内存中删除了未引用的对象,并为新对象提供了可用空间。它涉及两个阶段:
- 标记—在此步骤中,GC标识内存中未使用的对象
- 扫描—在此步骤中,GC删除上一阶段确定的对象
垃圾收集由JVM定期自动完成,无需单独处理。它也可以通过调用System.gc()
触发,但不能保证执行。
JVM包含3种不同类型的垃圾收集器:
- 串行GC—这是GC最简单的实现,专为在单线程环境中运行的小型应用程序而设计。它使用单个线程进行垃圾收集。当它运行时,它会导致一个“停止世界”事件,整个应用程序被暂停。使用串行垃圾收集器的JVM参数是
-XX:+UseSerialGC
- 并行GC—这是JVM中GC的默认实现,也称为吞吐量收集器。它使用多个线程进行垃圾收集,但在运行时仍然暂停应用程序。使用并行垃圾收集器的JVM参数是
-XX:+UseParallelGC
。 - 垃圾优先(G1)GC——G1GC专为具有大堆尺寸(超过4GB)的多线程应用程序而设计。它将堆划分为一组大小相等的区域,并使用多个线程扫描它们。G1GC识别垃圾最多的区域,并首先在该区域执行垃圾收集。使用G1垃圾收集器的JVM参数是
-XX:+UseG1GC
Java本机接口(JNI)-
有时,有必要使用本机(非Java)代码(例如C/C++)。这可以在我们需要与硬件交互或克服Java中的内存管理和性能约束的情况下。Java支持通过Java本机接口(JNI)执行本机代码。
JNI充当桥梁,允许C、C++等其他编程语言的支持包。在您需要编写Java不完全支持的代码的情况下,这特别有帮助,例如一些只能用C编写的平台特定功能。
您可以使用本native
键字来指示方法实现将由本机库提供。您还需要调用System.loadLibrary()
将共享的本机库加载到内存中,并将其功能提供给Java。
原生方法库-
原生方法库是用其他编程语言编写的库,如C、C++和汇编。这些库通常以.dll
或.so
文件的形式存在。这些本机库可以通过JNI加载。
常见的JVM错误
- ClassNotFoundExcecption — 当类加载程序尝试使用
Class.forName()
、ClassLoader.loadClass()
或ClassLoader.findSystemClass()
加载类,但没有找到具有指定名称的类的定义时,就会发生这种情况。 - NoClassDefFoundError — 当编译器成功编译了类,但Class Loader无法在运行时找到类文件时,就会发生这种情况。
- OutOfMemoryError — 当JVM因内存不足而无法分配对象,并且垃圾收集器无法提供更多内存时,就会发生这种情况。
- StackOverflowError — 如果JVM在处理线程时创建新堆栈帧时空间不足,就会发生这种情况。
结论
在此过程中,我们讨论了Java虚拟机的架构及其各种组件。当我们的代码工作时,它是如何工作的。
只有当出现问题时,我们需要拉动JVM或修复内存泄漏时,我们才会尝试了解其内部机制。
原文作者:Faizan Shaikh