JVM
Java的JVM(Java虚拟机)是运行Java程序的关键部件。它不直接理解或执行Java源代码,而是与Java编译器生成的字节码(Bytecode)进行交互。下面是对Java JVM更详尽的解释:
1.字节码:
当你使用Java编译器(javac)编译Java源代码时,会生成一种中间语言——字节码(.class文件)。字节码是一种平台无关的代码格式,设计目的是为了实现跨平台的兼容性。
字节码(Bytecode)是一种介于高级编程语言和机器语言之间的低级程序表示形式。它是许多编程语言(如Java、Python等)在编译或解释执行过程中产生的中间代码。字节码的主要特点和作用包括:
-
平台无关性:字节码不针对任何特定的硬件架构,它是一种抽象的、与具体处理器无关的指令集。这使得编译后的字节码可以在任何支持该字节码格式的平台上运行,实现了“一次编写,到处运行”的跨平台特性。
-
简化编译过程:相比直接生成机器语言,产生字节码的过程更为简单,因为字节码的指令集通常比机器语言指令集更小、更通用。
-
安全性增强:在执行字节码前,虚拟机(如Java虚拟机JVM)可以对其进行验证,确保代码的安全性,比如检查类型安全,防止非法访问内存等。
-
优化机会:JVM或其他虚拟机可以在运行时对字节码进行动态优化,如即时编译(JIT),将频繁执行的字节码转换为更高效的本地机器码,提高程序执行效率。
-
易于分析与变换:由于字节码的结构较为简单且有明确的规范,工具和框架可以更容易地对其进行分析、修改或转换,这对于代码混淆、程序分析、动态代理等技术尤为重要。
例如,在Java中,源代码被编译成.class
文件中的字节码,这些字节码随后由Java虚拟机(JVM)解释或即时编译成机器码执行。这一层抽象极大地增强了Java程序的可移植性和安全性。
2.加载与验证:
当一个Java程序开始运行时,JVM负责加载所需的字节码文件。加载后,JVM会对这些字节码进行验证,确保它们没有违反Java的安全规范,如类型安全等。
Java 虚拟机(JVM)的类加载过程主要包括五个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)和初始化(Initialization)。下面是对加载与验证阶段的详细介绍:
加载(Loading)
-
寻找并加载类的二进制数据:JVM查找并读取类的字节码文件(通常是
.class
文件),这个过程可以通过不同的类加载器来完成,包括启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、系统类加载器(Application ClassLoader)或自定义类加载器。 -
创建
java.lang.Class
实例:JVM为每个加载的类创建一个对应的Class
对象,作为方法区中类数据的访问入口。这个对象封装了类的各种信息,如类名、父类、实现的接口、常量池、字段、方法等。
验证(Verification)
验证阶段是确保加载的字节码是否符合JVM规范要求,防止恶意代码损害系统安全。验证过程分为四个主要步骤:
3.文件格式验证:
检查.class
文件的格式是否正确,包括魔数(Magic Number)、版本号、常量池等,确保文件结构合法。
Java虚拟机(JVM)的文件格式验证是类加载验证过程的第一个阶段,旨在确保.class
文件的格式遵守Java Class File Format规范,确保字节码文件的基本结构正确无误,可以被正确解析。此阶段的验证内容主要包括以下几个方面:
-
魔数验证(Magic Number Verification):每个
.class
文件的开头都有一个特殊的4字节序列(十六进制表示为CAFEBABE
),被称为魔数。验证器首先检查文件头的魔数是否正确,以此确认文件是否为有效的Java字节码文件。 -
版本信息验证:接下来验证
.class
文件的版本号,包括主版本号和次版本号,确保其与当前JVM的版本兼容。如果不兼容,JVM将拒绝加载该类。 -
常量池计数和验证:验证常量池的数量是否与文件中声明的一致,并对常量池中的每一项(如类名、字符串、数字、方法引用等)进行基本的格式验证,确保它们的类型和结构正确。
-
访问标志验证:检查类或接口的访问修饰符是否合法,例如确保一个类不能同时被声明为
abstract
和final
。 -
类索引、父类索引和接口计数验证:确保类索引指向的常量池项确实代表一个有效的类,父类索引(对于非
java.lang.Object
的类)指向的是一个有效的超类,接口计数正确无误,且所有接口引用有效。 -
字段表和方法表验证:验证类的字段和方法数量是否与声明相符,以及字段和方法的描述符是否合法,包括返回类型、参数类型等。
-
属性表验证:检查类、字段和方法的属性表,确保属性的数量、类型和长度正确,特别是对Code属性(包含方法的字节码)的初步检查,以确保其格式合理。
文件格式验证主要是对.class
文件的结构进行检查,确保其基本的格式和一致性,是整个类加载验证流程的基础。如果这一阶段检测到任何错误,类加载过程就会终止,并抛出相关错误信息。
4.元数据验证:
校验类的元数据信息,如检查类是否有父类(除了java.lang.Object
外)、检查final类是否被继承、检查方法的重写是否遵循规则等。
Java虚拟机(JVM)的元数据验证是类加载验证过程的第二个阶段,紧随文件格式验证之后。这一阶段主要关注类结构的语义检查,确保类的元数据信息(如类、接口、字段、方法等)遵循Java语言规范,不会引入逻辑上的冲突或不一致性。元数据验证的具体内容包括但不限于以下几个方面:
-
类结构验证:确保类的继承关系合法,例如一个类只能有一个直接父类(除了
java.lang.Object
外),接口不能有父类,类不能直接或间接实现自己,也不能直接或间接扩展自己。 -
字段和方法重载验证:检查类中的字段和方法名称是否唯一(考虑重载的情况下),以及它们的访问权限、修饰符(如
static
、final
、private
等)是否合法。 -
final类和方法验证:确保声明为
final
的类没有子类,final
的方法没有被子类重写。 -
抽象类和方法验证:如果一个类声明为抽象类,验证它是否有子类;如果一个方法声明为抽象方法,确保它在非抽象类中不被实现。
-
接口验证:确保接口不包含实例字段(除了
static final
),所有方法都是抽象的(Java 8之后允许默认方法和静态方法),接口不能继承自非接口类型。 -
常量池中的符号引用验证:虽然符号引用的完全解析发生在解析阶段,但在元数据验证期间也会对常量池中的某些符号引用进行基本的格式检查,确保它们指向的描述符(如类、方法、字段的描述符)语法上是正确的。
元数据验证是确保类定义逻辑正确性的关键步骤,它帮助JVM识别那些在文件格式上看似正确但实际上违反了Java语言规范的类。通过这一系列严格的检查,JVM能够排除那些在类结构上有潜在问题的类,从而提升系统的稳定性和安全性。
5.字节码验证:
这是最复杂的一个阶段,通过数据流分析和控制流分析等手段,确保字节码的语义是合法的,不会引起 JVM 执行时的异常,比如类型转换错误、跳转指令的合法性等。
Java虚拟机(JVM)的字节码验证是类加载验证过程中的第三个阶段,也是最为复杂和关键的一个环节。这一阶段的目标是确保字节码的语义正确性,防止非法或有害的操作,保证程序执行的安全性。字节码验证主要关注以下几个方面:
-
类型安全验证:这是字节码验证的核心部分,涉及数据流分析和控制流分析。它检查每个操作码(opcode)对操作数栈和局部变量表的操作是否类型安全,确保不会发生类型不匹配的错误,比如错误的类型转换、非法的运算操作等。例如,确保加法操作的两个操作数都是数值类型,且类型兼容。
-
控制流验证:分析字节码的控制流图,确保程序的控制流逻辑是合理的,没有死循环,也不会跳转到不存在的指令或非法的代码段。此外,还检查异常处理表的正确性,确保try-catch块的范围合理且捕获的异常类型与抛出的异常匹配。
-
操作数栈和局部变量表的平衡性验证:确保每个方法的执行过程中,操作数栈和局部变量表的使用前后保持平衡,即每条指令执行前后栈深度和局部变量的使用状态符合预期,不会出现栈溢出或下溢的情况。
-
方法体验证:详细检查方法内部的代码,包括对方法的Code属性中的字节码指令序列进行验证,确保指令的顺序、分支、跳转逻辑正确,以及对方法返回类型和异常处理的合规性检查。
-
对常量池引用的额外验证:在这一阶段,虽然大部分符号引用的解析发生在解析阶段,但对于直接涉及到的常量池条目(如方法调用、字段访问等),会进一步验证这些引用的有效性和类型兼容性。
字节码验证是JVM安全机制的重要组成部分,通过严格的逻辑检查,可以有效防止恶意代码利用字节码层面的漏洞进行攻击,保证了Java程序的健壮性和安全性。尽管这一过程相对耗时,但它对维护Java“一次编写,到处运行”的承诺至关重要。
6.符号引用验证:
在解析之前对类自身以外的信息(如常量池中的类、方法、字段符号引用)进行校验,确保可以成功解析到对应的类、方法或字段。
符号引用验证是Java虚拟机(JVM)类加载验证过程中的第四个阶段,发生在解析之前。这一阶段主要关注常量池中的符号引用(Symbolic References),确保这些引用在实际解析时能够成功定位到目标类、字段或方法。符号引用验证包括以下几个方面:
-
有效性验证:检查常量池中的符号引用是否格式正确,比如类或接口的全限定名、字段的名称和描述符、方法的名称、描述符及参数类型等,确保这些信息符合Java语言规范。
-
可访问性验证:确保当前类对符号引用所指的目标具有合法的访问权限。例如,私有(private)成员不能被外部类访问,包外的类不能访问包内未声明为public的成员等。
-
存在性验证:验证被引用的类、字段、方法是否真实存在。虽然这一步骤在实际解析时会更加彻底,但在符号引用验证阶段也会进行初步检查,避免明显的无效引用。
-
类型兼容性验证:对于方法调用和字段访问,检查调用者和被调用者之间是否存在兼容性问题,比如方法的参数类型、返回类型与预期是否一致,字段类型是否兼容等。
-
接口合法性验证:如果符号引用指向的是接口方法或接口本身,确保符合Java接口的使用规则,比如类实现接口的所有抽象方法,或者接口不能继承自非接口等。
符号引用验证的目的在于提前发现潜在的引用错误,减少在解析阶段因引用问题导致的失败,从而提高类加载的效率和稳定性。虽然解析阶段会进一步验证和具体化这些符号引用,但前期的符号引用验证依然是必要的,它作为一道安全网,有助于维护Java程序的健壮性。
验证阶段是非常重要的,它确保了类文件的格式正确、语义合法,是JVM安全机制的重要组成部分。如果在验证阶段发现错误,JVM将抛出相关异常,阻止有问题的类被加载到内存中执行。
-
解释与即时编译(JIT):早期的JVM通过逐行解释字节码来执行程序,这种方式效率较低。现代JVM大多采用即时编译技术(Just-In-Time Compilation),即在运行时将频繁执行的字节码编译为对应平台的本地机器码,从而显著提升执行效率。
-
内存管理:JVM自动管理程序运行时的内存分配和回收。它将内存划分为不同的区域,如堆(Heap)用于存储对象实例,栈(Stack)用于方法调用和局部变量,以及方法区(Method Area)用于存储类的元数据等。垃圾收集器(Garbage Collector, GC)是JVM的一部分,负责自动回收不再使用的内存空间,减少程序员手动管理内存的工作。
-
多线程支持:JVM内置了对多线程的支持,使得Java程序可以轻松创建和管理多个线程。JVM负责调度线程、管理线程生命周期,并提供了一套内存模型来保证线程间的通信正确性。
-
安全性:JVM通过字节码验证、安全沙箱(限制了程序访问本地系统资源的能力)、类加载器体系结构等多种机制来保障Java程序的执行安全。
正因为有了JVM,Java程序员编写的代码可以在任何安装了JVM的硬件和操作系统上运行,无需重新编译,实现了高度的可移植性和跨平台能力。
GC算法(垃圾回收算法)
Java虚拟机中的垃圾回收算法主要有以下几种:
-
标记-清除(Mark and Sweep):
- 这是最基础的垃圾回收算法。首先遍历所有可达的对象并做上标记,然后再次遍历堆内存,未被标记的对象被视为垃圾并回收其空间。
- 缺点是会产生内存碎片,导致后续分配大对象时可能无法找到足够的连续空间。
-
复制(Copying):
- 将内存分为两个相等的区域,每次只使用其中一个区域。当这一区域满时,将存活对象复制到另一个区域,然后清空之前使用的区域。
- 这种方法简单高效,能解决内存碎片问题,但代价是内存使用率只有50%。
-
标记-整理(Mark and Compact):
- 结合了标记-清除和复制的优点,首先标记出所有活动对象,然后将存活的对象向一端移动,最后清理掉边界外的内存空间。
- 这个过程既解决了碎片问题,又不需要两倍的内存空间。
-
分代收集(Generational Collection):
- 基于“大多数对象都是朝生夕灭”的假设,将堆内存分为新生代和老年代。
- 新生代通常使用复制算法,因为它频繁GC但每次回收的大多是短命对象。
- 老年代则常用标记-清除或标记-整理算法,因为这里的对象生命周期长,回收频率低。
垃圾回收器
JVM提供了多种垃圾回收器,它们实现了上述算法的不同组合,以适应不同场景的需求。以下是一些常见的垃圾回收器:
-
Serial GC:适用于单CPU环境,新生代和老年代都采用串行回收的方式,简单高效但无法充分利用多核处理器的优势。
-
Parallel GC(也称为Throughput Collector):在多CPU环境中并行进行垃圾回收,提高吞吐量,但可能会引起较长的暂停时间。
-
Concurrent Mark and Sweep (CMS) GC:老年代垃圾回收器,目标是减少停顿时间,大部分工作与用户线程并发执行,但在极端情况下可能会出现“ Concurrent Mode Failure”。
-
G1(Garbage First)GC:设计用于大型堆的服务器应用,它将堆内存划分为多个大小相等的区域(Region),并采用分代收集策略,同时努力达到低延迟和高吞吐量的目标。
类加载机制
Java的类加载机制主要包括以下步骤:
-
加载(Loading):查找并加载类的二进制数据,通常是
.class
文件。 -
验证(Verification):检查加载的类是否有正确的内部结构,并符合Java语言规范。
-
准备(Preparation):为类的静态变量分配内存,并将其初始化为默认值。
-
解析(Resolution):将常量池中的符号引用转换为直接引用的过程,如果需要的话。
-
初始化(Initialization):执行类的静态初始化代码,给静态变量赋予程序员设定的初始值。
类加载器分为四种主要类型:Bootstrap ClassLoader、Extension ClassLoader、Application ClassLoader 和 Custom ClassLoader(用户自定义)。它们形成了一个层次结构,负责加载不同来源的类,且遵循双亲委派模型原则,即类加载请求先委托给父加载器处理,如果父加载器不能处理再由自己尝试加载。