JVM 虚拟机的编译器
编译器可以分为:前端编译器、JIT 编译器、AOT编译器。
前端编译器:源代码 --> 字节码
在Java语言中,JDK安装目录中的javac
就是编译器。它负责将Java源代码编译为字节码。因为处于编译的前期,javac
也叫做前端编译器。
JIT 编译器:字节码 --> 机器码
即时编译器(Just-In-Time Compiler,JIT)
Java源代码转化为 字节码之后,要运行它,有两种选择:
- 使用 Java 解释器,执行字节码。
- 使用 JIT(Just-In-Time Compiler) 编译器 将字节码 转化为 机器码。
JIT 编译器在程序运行时,对频繁执行的字节码进行即时编译,将其转换为本地机器码,从而提高程序的执行效率。
HotSpot 虚拟机的两个即时编译器
在 HotSpot 虚拟机内置了两个即时编译器,分别称为 Client Compiler 和 Server Compiler 。这两种不同的编译器衍生出两种不同的编译模式,我们分别称之为:C1 编译模式 ,C2 编译模式。
- C1 编译模式会将字节码编译为本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑。
- C2 编译模式,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。
AOT 编译器:源代码 --> 机器码
提前编译器(Ahead-of-Time Compiler,AOT)
AOT 编译器的基本思想是:在程序执行前生成 Java 方法的本地代码,以便在程序运行时直接使用本地代码。
HotSpot 虚拟机的三种运行模式
注意,对于 HotSpot 虚拟机来说,其一共有三种运行模式可选,分别是:
- 混合模式 (Mixed Mode) 。即 C1 和 C2 两种模式混合起来使用,这是默认的运行模式。如果你想单独使用 C1 模式或 C2 模式,使用
-client
或-server
打开即可。 - 解释模式 (Interpreted Mode)。即所有代码都解释执行,使用
-Xint
参数可以打开这个模式。 - 编译模式(Compiled Mode)。 此模式优先采用编译,但是无法编译时也会解释执行,使用 -Xcomp 打开这种模式。
在命令行中输入 java -version 可以看到,我机器上的虚拟机使用 Mixed Mode 运行模式。
JVM 虚拟机类加载过程
Java 中的类加载过程分为:加载(Loading) 、链接(Linking) 、初始化(Initialization) 。
-
加载(Loading) :阶段的目的是将类的
.class
文件加载到JVM中。在这个阶段,JVM 会根据类的全限定名来获取定义该类的二进制字节流,并将这个字节流所代表的静态存储结构 转化为方法区的运行时数据结构。 -
链接(Linking) :又分为 ++验证(Verification)++ 、++准备(Prepartion)++ 、++解析(Resolution)++。
- 验证 :校验类的正确性,包括:
- 文件格式验证:验证字节流是否符合Class文件格式的规范。比如,是否以
0xCAFEBABE
开头。 - 元数据验证:对字节码描述进行语义分析,是否遵循Java规范。比如一个类是否继承了多个类。
- 字节码验证:比如验证指令代码序列是否能够正常工作。
- 符号引用验证:确保解析动作能够正确执行。
- 文件格式验证:验证字节流是否符合Class文件格式的规范。比如,是否以
- 准备 :给 类中定义的变量(被
static
修饰的静态变量)分配内存,并设置初始值。 - 解析:虚拟机将 常量池 内的 符号引用 替换为 直接引用。(解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行)
- 验证 :校验类的正确性,包括:
-
初始化(Initialization) :初始化阶段是执行初始化方法
<clinit>()
方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。对于初始化阶段,有些情况必须立即初始化(如下)。其他情况,则是进行懒加载(首次用到的时候才初始化)
- 遇到
new
、getstatic
、putstatic
或invokestatic
这四条字节码指令时,会立即初始化。 - 使用反射机制时,立即初始化。
- 初始化一个类时,其父类没初始化,则会立即初始化父类。
- 遇到
-
卸载(Unloading):对象被垃圾回收。
类加载器
类加载器 主要作用于 JVM 虚拟机类加载过程中的 加载阶段。
如何比较两个类是否"相等"
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性。所以:比较两个类是否"相等",只有在这两个类是由同一个类加载器加载的前提下才有意义 ,否则,即使这两个类来源于同一个 Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
双亲委派机制
站在JAVA虚拟机的角度来讲,只存在两种不同的类加载器:
- 一种是启动类加载器(
Bootstrap ClassLoader
) ,这个类加载器使用C++语言实现,++是虚拟机自身的一部分++; - 另外一种就是其他所有的类加载器,++这些类加载器都由Java语言实现++ ,独立存在于虚拟机外部,并且全都继承自抽象类
java.lang.ClassLoader
。
每一个类都有一个对应它的类加载器。系统中的 ClassLoader
在协同工作的时候会默认使用 双亲委派模型。其工作过程为:
- 当一个类加载器收到了类加载的请求,不会自己去加载这个类,而是委派给父类加载器完成。
- 只有当父类加载器无法完成这个加载请求时(它搜索范围中没找到对应的类信息),子加载器才会尝试自己去完成加载。
父类加载器
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。
三层类加载器
双亲委派机制中的主要的三层类加载器,他们分别是:启动类加载器、扩展类加载器、应用程序类加载器。
- BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,负责加载
%JAVA_HOME%/lib
目录下的jar
包和类,或者被-Xbootclasspath
参数指定的路径中的所有类。 - ExtensionClassLoader(扩展类加载器) :主要负责加载
%JRE_HOME%/lib/ext
目录下的jar
包和类,或被java.ext.dirs
系统变量所指定的路径下的jar
包。 - AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用
classpath
下的所有 jar 包和类。
双亲委派机制的好处
双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类)。
如何破坏双亲委派机制
答:有如下几种方法:
-
通过自定义类加载器,重写
loadClass()
方法,可以改变这种默认的加载顺序,从而破坏双亲委派机制。比如,可以先尝试从自己指定的路径去加载类,如果找不到再调用父类的
loadClass
方法。 -
线程上下文类加载器:当 JVM 需要加载类且当前类加载器无法完成时,会尝试使用线程上下文类加载器,从而绕过双亲委派机制从特定类加载器加载类。
线程上下文类加载器
在双亲委托模型下,类的加载是由下至上的,即下层的类加载器会委托上层进行加载。
但对于SPI来说,有些接口是Java核心库所提供的,而Java核心库是由启动类加载器来加载的,而这些接口的实现却来自不同的Jar包(厂商提供JDBC的实现有Oracle,MySQL等), Java的启动类加载器不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求 。而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。
SPI(Service Provider Interface)即服务提供者接口。
- SPI 是一种用于实现框架扩展和插件化的机制。它定义了一组接口,允许第三方为这些接口提供具体的实现。
- 例如,Java 的数据库连接(JDBC)就是一个典型的 SPI 应用。JDBC 定义了一组用于与数据库进行交互的接口,而不同的数据库厂商提供了各自的实现。