- 新增 .drone.yml 文件用于定义 CI/CD 流程 - 配置了基于 Docker 的部署步骤 - 设置了工作区和卷映射以支持持久化数据 - 添加了构建准备阶段和 Docker 部署阶段 - 定义了环境变量和代理设置 - 配置了 artifacts 目录的处理逻辑 - 添加了 timezone 映射以确保时间同步 - 设置了 docker.sock 映射以支持 Docker in Docker
1185 lines
36 KiB
Markdown
1185 lines
36 KiB
Markdown
---
|
||
title: JVM之类加载
|
||
date: 2022-12-09
|
||
sidebar: 'auto'
|
||
tags:
|
||
- JVM
|
||
categories:
|
||
- Java
|
||
---
|
||
## 1 类加载阶段
|
||
|
||
### 1.1 加载
|
||
|
||
- 将类的字节码载入方法区中,内部采用 C++ 的 `instanceKlass` 描述 java 类,它的重要 `field` 有:
|
||
|
||
- `_java_mirror` 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用,是一个桥梁
|
||
|
||
- `_super` 即父类
|
||
|
||
- `_fields` 即成员变量
|
||
|
||
- `_methods` 即方法
|
||
|
||
- `_constants` 即常量池
|
||
|
||
- `_class_loader` 即类加载器
|
||
|
||
- `_vtable` 虚方法表
|
||
|
||
- `_itable` 接口方法表
|
||
|
||
- 如果这个类还有父类没有加载,先加载父类
|
||
|
||
- 加载和链接可能是交替运行的
|
||
|
||
> **注意**
|
||
>
|
||
> - `instanceKlass` 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 `_java_mirror` 是存储在堆中
|
||
> - 可以通过前面介绍的 `HSDB` 工具查看
|
||
|
||

|
||
|
||
|
||
|
||
### 1.2 链接
|
||
|
||
#### 1) 验证
|
||
|
||
验证类是否符合 JVM规范,安全性检查
|
||
|
||
修改 `HelloWorld.class` 的魔数,在控制台运行
|
||
|
||
- `Incompatible magic value`:JVM 检查到了不兼容的魔数
|
||
|
||
```shell
|
||
G:\Idea_workspace\JVM\src\main\java>java HelloWorld
|
||
Error: A JNI error has occurred, please check your installation and try again
|
||
Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value 1514689864 in class file HelloWorld
|
||
at java.lang.ClassLoader.defineClass1(Native Method)
|
||
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
|
||
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
|
||
at java.net.URLClassLoader.defineClass(URLClassLoader.java:473)
|
||
at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
|
||
at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
|
||
at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
|
||
at java.security.AccessController.doPrivileged(Native Method)
|
||
at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
|
||
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
|
||
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
|
||
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
|
||
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:601)
|
||
```
|
||
|
||
|
||
|
||
#### 2) 准备
|
||
|
||
为 static 变量分配空间,设置默认值
|
||
|
||
- static 变量在 `JDK 7` 之前存储于 `instanceKlass` 末尾,从`JDK 7` 开始,存储于 `_java_mirror`末尾
|
||
- static 变量*分配空间和赋值是两个步骤*,分配空间在准备阶段完成,赋值在初始化阶段完成
|
||
- 如果 static 变量是 `final` 的*基本类型,以及字符串常量*,那么编译阶段值就确定了,赋值在**准备阶段**完成
|
||
- 如果 static 变量是 `final` 的,但属于*引用类型*,那么赋值也会在**初始化阶段**完成
|
||
|
||
```java
|
||
public class Load1 {
|
||
static int a;
|
||
static int b = 20;
|
||
static final int c = 30;
|
||
static final String d = "qwe";
|
||
static final String e = new String("asd");
|
||
}
|
||
```
|
||
|
||
以上代码的字节码为,印证以下结果
|
||
|
||
- static 关键字声明的常量在 **编译阶段完成** 空间分配,在 **初始化阶段** 完成赋值
|
||
- static + final 关键字声明的常量且是 *基本类型或字符串常量*,在 **编译阶段** 就有了确定的值,且在 **准备阶段** 完成赋值
|
||
- static + final 关键字声明的常量且是 *引用类型*,需要在 **初始化阶段** 完成赋值
|
||
|
||
```
|
||
static int a;
|
||
descriptor: I
|
||
flags: ACC_STATIC
|
||
|
||
static int b;
|
||
descriptor: I
|
||
flags: ACC_STATIC
|
||
|
||
static final int c;
|
||
descriptor: I
|
||
flags: ACC_STATIC, ACC_FINAL
|
||
ConstantValue: int 30
|
||
|
||
static final java.lang.String d;
|
||
descriptor: Ljava/lang/String;
|
||
flags: ACC_STATIC, ACC_FINAL
|
||
ConstantValue: String qwe
|
||
|
||
static final java.lang.String e;
|
||
descriptor: Ljava/lang/String;
|
||
flags: ACC_STATIC, ACC_FINAL
|
||
|
||
static {};
|
||
descriptor: ()V
|
||
flags: ACC_STATIC
|
||
Code:
|
||
stack=3, locals=0, args_size=0
|
||
0: bipush 20
|
||
2: putstatic #2 // 20 -> b
|
||
5: new #3 // class java/lang/String
|
||
8: dup
|
||
9: ldc #4 // <- "asd"
|
||
11: invokespecial #5 // Method java/lang/String."<init>":(Ljava/lang/String;)V
|
||
14: putstatic #6 // "asd" -> e
|
||
17: return
|
||
```
|
||
|
||
|
||
|
||
#### 3) 解析
|
||
|
||
将常量池中的符号引用解析为直接引用
|
||
|
||
```java
|
||
package com.zhuhjay.demo4;
|
||
public class Load2 {
|
||
public static void main(String[] args) throws Exception {
|
||
ClassLoader classLoader = Load2.class.getClassLoader();
|
||
// 使用 loadClass 方法加载的类不会触发类的解析和初始化
|
||
Class<?> aClass = classLoader.loadClass("com.zhuhjay.demo4.C");
|
||
System.in.read();
|
||
}
|
||
}
|
||
class C {
|
||
D d = new D();
|
||
}
|
||
class D { }
|
||
```
|
||
|
||
接下来打开 `HSDB` 工具(在 JDK 目录下执行命令) `java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB`
|
||
|
||
查看是否存在类 D,以及类 C 中的常量池信息
|
||
|
||
- 以上代码执行后,只会存在类C对象,而类D对象只存在字节码信息,不会进行解析
|
||
|
||

|
||
|
||
|
||
|
||
而如果直接创建了对象,那么就会存在类C和类D对象,此时类C和类D将会被解析完
|
||
|
||
```java
|
||
package com.zhuhjay.demo4;
|
||
public class Load2 {
|
||
public static void main(String[] args) throws Exception {
|
||
ClassLoader classLoader = Load2.class.getClassLoader();
|
||
// 使用 loadClass 方法加载的类不会触发类的解析和初始化
|
||
// Class<?> aClass = classLoader.loadClass("com.zhuhjay.demo4.C");
|
||
new C();
|
||
System.in.read();
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
|
||
### 1.3 初始化
|
||
|
||
#### `<clinit>()V`
|
||
|
||
初始化即调用 `<clinit>()V` ,虚拟机会保证这个类的『构造方法』的线程安全
|
||
|
||
|
||
|
||
#### 发生时机
|
||
|
||
概括得说,类初始化是【懒惰的】
|
||
|
||
- main 方法所在的类,总会被首先初始化
|
||
- 首次访问这个类的静态变量或静态方法时
|
||
- 子类初始化,如果父类还没初始化,会引发
|
||
- 子类访问父类的静态变量,只会触发父类的初始化
|
||
- Class.forName
|
||
- new 会导致初始化
|
||
|
||
|
||
|
||
不会导致类初始化的情况
|
||
|
||
- 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
|
||
- 类.class 不会触发初始化
|
||
- 创建该类的数组不会触发初始化
|
||
- 类加载器的 loadClass 方法
|
||
- Class.forName 的参数 2 为 false
|
||
|
||
|
||
|
||
使用以下代码对以上进行验证(依次执行)
|
||
|
||
0. 当只有一个空的 mian 方法进行执行时,会有 `main init` :main 方法所在的类,总会被首先初始化
|
||
1. `final static double b = 5.0` 不会导致初始化过程产生:访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
|
||
1. `B.class` 不会触发初始化
|
||
1. `new B[0]` 不会触发初始化
|
||
1. `c1.loadClass("com.zhuhjay.demo4.B")` 会加载类B以及父类A,但不会触发初始化
|
||
1. `Class.forName("com.zhuhjay.demo4.B", false, c2)` 会加载类B以及父类A,但不会触发初始化
|
||
1. `static int a = 0` 在初始化阶段才进行赋值,此时会触发初始化
|
||
1. `static boolean c = false` 在初始化阶段进行赋值,子类初始化触发会引发父类初始化
|
||
1. 使用子类访问父类 `static int a = 0`,只会触发父类的初始化
|
||
1. `Class.forName("com.zhuhjay.demo4.B")` 会触发初始化,并且父类先进行初始化
|
||
|
||
```java
|
||
package com.zhuhjay.demo4;
|
||
public class Load3 {
|
||
static {
|
||
System.out.println("main init");
|
||
}
|
||
|
||
public static void main(String[] args) throws Exception {
|
||
// 1. 静态常量不会触发初始化
|
||
System.out.println(B.b);
|
||
// 2. 类.class 不会触发初始化
|
||
System.out.println(B.class);
|
||
// 3. 创建类的数组不会触发初始化
|
||
System.out.println(new B[0]);
|
||
// 4. 不会初始化类B,但会加载 B、A
|
||
ClassLoader c1 = Thread.currentThread().getContextClassLoader();
|
||
c1.loadClass("com.zhuhjay.demo4.B");
|
||
// 5. 不会初始化类B, 但会加载 B、A
|
||
ClassLoader c2 = Thread.currentThread().getContextClassLoader();
|
||
Class.forName("com.zhuhjay.demo4.B", false, c2);
|
||
|
||
// 6. 首次访问这个类的静态变量或静态方法时
|
||
System.out.println(A.a);
|
||
// 7. 子类初始化,如果父类还没初始化,会引发
|
||
System.out.println(B.c);
|
||
// 8. 子类访问父类的静态变量,只会触发父类的初始化
|
||
System.out.println(B.a);
|
||
// 9. 会初始化类B,并先初始化类A
|
||
Class.forName("com.zhuhjay.demo4.B");
|
||
}
|
||
}
|
||
class A {
|
||
static int a = 0;
|
||
static {
|
||
System.out.println("a init");
|
||
}
|
||
}
|
||
class B extends A {
|
||
final static double b = 5.0;
|
||
static boolean c = false;
|
||
static {
|
||
System.out.println("b init");
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
|
||
### 1.4 练习
|
||
|
||
判断以下代码哪些会触发E的初始化
|
||
|
||
- `public static final Integer c = 20` 在编译阶段会转换为 `public static final Integer c = Integer.valueOf(20)`,访问变量c会触发初始化方法
|
||
|
||
```java
|
||
public class Load4 {
|
||
public static void main(String[] args) {
|
||
System.out.println(E.a);
|
||
System.out.println(E.b);
|
||
System.out.println(E.c);
|
||
}
|
||
}
|
||
class E {
|
||
public static final int a = 10;
|
||
public static final String b = "qwe";
|
||
public static final Integer c = 20;
|
||
static {
|
||
System.out.println("E init");
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
|
||
懒惰初始化单例模式
|
||
|
||
- 懒惰的、线程安全的
|
||
- 只有在调用 `getInstance()` 方法的时候, 静态内部类 `LazyHolder` 才会被初始化
|
||
|
||
```java
|
||
public class Load5 {
|
||
public static void main(String[] args) throws Exception {
|
||
Singleton.test();
|
||
}
|
||
}
|
||
class Singleton {
|
||
private Singleton() {}
|
||
private static class LazyHolder {
|
||
static final Singleton INSTANCE = new Singleton();
|
||
static {
|
||
System.out.println("LazyHolder init");
|
||
}
|
||
}
|
||
public static Singleton getInstance() {
|
||
return LazyHolder.INSTANCE;
|
||
}
|
||
public static void test() {
|
||
System.out.println("test method");
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
|
||
## 2 类加载器
|
||
|
||
以 JDK 8 为例:
|
||
|
||
| 名称 | 加载哪的类 | 说明 |
|
||
| ----------------------- | --------------------- | ----------------------------- |
|
||
| Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
|
||
| Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
|
||
| Application ClassLoader | classpath | 上级为 Extension |
|
||
| 自定义类加载器 | 自定义 | 上级为 Application |
|
||
|
||
在加载一个类时,首先会向上级的类加载器"询问"是否可以进行类的加载,如果上级加载器可以加载,则由上级加载器加载,否则由 Application ClassLoader 进行类的加载。这就是**双亲委派机制**
|
||
|
||
- 由于 Bootstrap ClassLoader 是由 C++ 运行的,所以该类加载器无法直接访问,以 null 存在
|
||
|
||
|
||
|
||
### 2.1 启动类加载器
|
||
|
||
用 Bootstrap 类加载器加载类:
|
||
|
||
```java
|
||
package com.zhuhjay.demo4;
|
||
public class Load6 {
|
||
public static void main(String[] args) throws Exception {
|
||
Class<?> aClass = Class.forName("com.zhuhjay.demo4.F");
|
||
System.out.println(aClass.getClassLoader());
|
||
}
|
||
}
|
||
class F {
|
||
static {
|
||
System.out.println("Bootstrap F init");
|
||
}
|
||
}
|
||
```
|
||
|
||
将上述代码编译执行一次过后,打开对应的class文件输出目录,执行命令 `java -Xbootclasspath/a:. com.zhuhjay.demo4.Load6` 会出现以下输出结果,`aClass.getClassLoader()` 结果为 `null` 即为 Bootstrap 类加载器
|
||
|
||
- `-Xbootclasspath` 表示设置 `bootclasspath`
|
||
- 其中 `/a:.` 表示将当前目录追加至 `bootclasspath` 之后
|
||
- 可以用这个办法替换核心类(修改 Bootstrap 类加载器 加载的目录)
|
||
- ` java -Xbootclasspath:<new bootclasspath>`
|
||
- `java -Xbootclasspath/a:<追加路径>`:后追加
|
||
- `java -Xbootclasspath/p:<追加路径>`:前追加
|
||
|
||
```
|
||
Bootstrap F init
|
||
null
|
||
```
|
||
|
||
|
||
|
||
### 2.2 扩展类加载器
|
||
|
||
编译执行以下代码
|
||
|
||
```java
|
||
public class G {
|
||
static {
|
||
System.out.println("ext classpath G init");
|
||
}
|
||
}
|
||
```
|
||
|
||
```java
|
||
public class Load7 {
|
||
public static void main(String[] args) throws Exception {
|
||
Class<?> aClass = Class.forName("com.zhuhjay.demo4.G");
|
||
System.out.println(aClass.getClassLoader());
|
||
}
|
||
}
|
||
```
|
||
|
||
移步至 class 输出目录下,使用命令 `jar -cvf my.jar com/zhuhjay/demo4/G.class` 对类G生成jar包,将生成后的jar包复制到 JDK 目录下的 `jre/lib/ext` 中,然后将 G 改为以下
|
||
|
||
```java
|
||
public class G {
|
||
static {
|
||
System.out.println("classpath G init");
|
||
}
|
||
}
|
||
```
|
||
|
||
再次执行 Load7 得到结果
|
||
|
||
- 执行的类G是在 JDK 目录下的,并且由 Extension ClassLoader 进行该类的创建,使得自己写的类G失效,印证了双亲委派(启动>扩展>应用>自定义)
|
||
|
||
```
|
||
ext classpath G init
|
||
sun.misc.Launcher$ExtClassLoader@7f31245a
|
||
```
|
||
|
||
|
||
|
||
### 2.3 双亲委派模式
|
||
|
||
所谓的双亲委派,就是指调用类加载器的 `loadClass` 方法时,查找类的规则
|
||
|
||
- 双亲委派并没有**继承关系**,而是一种委托
|
||
|
||
```java
|
||
protected Class<?> loadClass(String name, boolean resolve)
|
||
throws ClassNotFoundException
|
||
{
|
||
synchronized (getClassLoadingLock(name)) {
|
||
// First, check if the class has already been loaded
|
||
// 1. 检查该类是否已经加载
|
||
Class<?> c = findLoadedClass(name);
|
||
if (c == null) {
|
||
long t0 = System.nanoTime();
|
||
try {
|
||
if (parent != null) {
|
||
// 2. 如果存在上级加载器,那么将委托执行 loadClass
|
||
c = parent.loadClass(name, false);
|
||
} else {
|
||
// 3. 如果没有上级(ExtClassLoader),则委派 BootstrapClassLoader
|
||
c = findBootstrapClassOrNull(name);
|
||
}
|
||
} catch (ClassNotFoundException e) {
|
||
// ClassNotFoundException thrown if class not found
|
||
// from the non-null parent class loader
|
||
}
|
||
|
||
if (c == null) {
|
||
// If still not found, then invoke findClass in order
|
||
// to find the class.
|
||
long t1 = System.nanoTime();
|
||
// 4. 都找不到,那么就调用 findClass(每个类加载器自己扩展)来加载
|
||
c = findClass(name);
|
||
|
||
// this is the defining class loader; record the stats
|
||
// 5. 记录耗时
|
||
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
|
||
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
|
||
sun.misc.PerfCounter.getFindClasses().increment();
|
||
}
|
||
}
|
||
if (resolve) {
|
||
resolveClass(c);
|
||
}
|
||
return c;
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
|
||
### 2.4 线程上下文类加载器
|
||
|
||
我们在使用 JDBC 的时候,都需要加载 Driver 驱动,发现不写 `Class.forName("com.mysql.jdbc.Driver");` 也能正确加载该驱动并进行使用(需要引入驱动jar)
|
||
|
||
```java
|
||
// 注册驱动
|
||
// Class.forName("com.mysql.jdbc.Driver");
|
||
// 使用 DriverManager 获取连接
|
||
Connection connection = DriverManager.getConnection("jdbc:mysql:///test?useSSL=false", "root", "root");
|
||
Statement statement = connection.createStatement();
|
||
ResultSet resultSet = statement.executeQuery("select * from user");
|
||
resultSet.next();
|
||
System.out.println(resultSet.getString("username"));
|
||
```
|
||
|
||
看看 DriverManager 源码,可以发现它好像自动去加载了驱动。
|
||
|
||
```java
|
||
public class DriverManager {
|
||
// 注册驱动的集合
|
||
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers =
|
||
new CopyOnWriteArrayList<>();
|
||
static {
|
||
// 初始化驱动
|
||
loadInitialDrivers();
|
||
println("JDBC DriverManager initialized");
|
||
}
|
||
}
|
||
```
|
||
|
||
先获取看看 DriverManager 的类加载器,可以发现打印结果为 null,证明说 DriverManager 是通过 Bootstrap ClassLoader 进行加载的,会到 `JAVA_HOME/jre/lib` 目录下进行类的搜索,但是显然 mysql驱动 文件并不是出现在 `JAVA_HOME/jre/lib` 目录下面,那为什么它却可以加载到 `com.mysql.jdbc.Driver`?
|
||
|
||
```java
|
||
System.out.println(DriverManager.class.getClassLoader());
|
||
```
|
||
|
||
|
||
|
||
看看源码中 `loadInitialDrivers()` 方法如何进行工作的
|
||
|
||
- `Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());` 打破双亲委派机制,直接使用应用类加载器来进行驱动的类加载
|
||
|
||
```java
|
||
private static void loadInitialDrivers() {
|
||
String drivers;
|
||
try {
|
||
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
|
||
public String run() {
|
||
return System.getProperty("jdbc.drivers");
|
||
}
|
||
});
|
||
} catch (Exception ex) {
|
||
drivers = null;
|
||
}
|
||
// 1) 使用 ServiceLoader 机制加载驱动,即 SPI
|
||
AccessController.doPrivileged(new PrivilegedAction<Void>() {
|
||
public Void run() {
|
||
|
||
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
|
||
Iterator<Driver> driversIterator = loadedDrivers.iterator();
|
||
|
||
try{
|
||
while(driversIterator.hasNext()) {
|
||
driversIterator.next();
|
||
}
|
||
} catch(Throwable t) {
|
||
// Do nothing
|
||
}
|
||
return null;
|
||
}
|
||
});
|
||
println("DriverManager.initialize: jdbc.drivers = " + drivers);
|
||
|
||
// 2) 使用 jdbc.drivers 定义的驱动名加载驱动
|
||
if (drivers == null || drivers.equals("")) {
|
||
return;
|
||
}
|
||
String[] driversList = drivers.split(":");
|
||
println("number of Drivers:" + driversList.length);
|
||
for (String aDriver : driversList) {
|
||
try {
|
||
println("DriverManager.Initialize: loading " + aDriver);
|
||
// 这里的 ClassLoader.getSystemClassLoader() 就是应用程序加载器
|
||
Class.forName(aDriver, true,
|
||
ClassLoader.getSystemClassLoader());
|
||
} catch (Exception ex) {
|
||
println("DriverManager.Initialize: load failed: " + ex);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
`1)` 中使用的就是大名鼎鼎的 `Service Provider Interface (SPI)`
|
||
|
||
- 约定如下,在 jar 包的 `META-INF/services` 包下,以接口全限定名名为文件,文件内容是实现类名称(就像 `SpringBoot` 中使用的 `spring.factories` 文件)
|
||
|
||
<img src="/jvm/1670493540488.png" alt="1670493540488" style="zoom:50%;" />
|
||
|
||
之后就可以使用以下 ServiceLoader 加载器来加载信息
|
||
|
||
```java
|
||
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
|
||
Iterator<Driver> driversIterator = loadedDrivers.iterator();
|
||
while(driversIterator.hasNext()) {
|
||
driversIterator.next();
|
||
}
|
||
```
|
||
|
||
体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:
|
||
|
||
- JDBC
|
||
- Servlet 初始化器
|
||
- Spring 容器
|
||
- Dubbo(对 SPI 进行了扩展)
|
||
|
||
|
||
|
||
接着看看 `ServiceLoader.load(Driver.class)` 方法
|
||
|
||
```java
|
||
public static <S> ServiceLoader<S> load(Class<S> service) {
|
||
// 获取线程上下文类加载器
|
||
ClassLoader cl = Thread.currentThread().getContextClassLoader();
|
||
return ServiceLoader.load(service, cl);
|
||
}
|
||
```
|
||
|
||
线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由 `Class.forName` 调用了线程上下文类加载器完成类加载,具体代码在 `ServiceLoader` 的内部类 `LazyIterator` 中:
|
||
|
||
```java
|
||
private S nextService() {
|
||
if (!hasNextService())
|
||
throw new NoSuchElementException();
|
||
String cn = nextName;
|
||
nextName = null;
|
||
Class<?> c = null;
|
||
try {
|
||
c = Class.forName(cn, false, loader);
|
||
} catch (ClassNotFoundException x) {
|
||
fail(service,
|
||
"Provider " + cn + " not found");
|
||
}
|
||
if (!service.isAssignableFrom(c)) {
|
||
fail(service,
|
||
"Provider " + cn + " not a subtype");
|
||
}
|
||
try {
|
||
S p = service.cast(c.newInstance());
|
||
providers.put(cn, p);
|
||
return p;
|
||
} catch (Throwable x) {
|
||
fail(service,
|
||
"Provider " + cn + " could not be instantiated",
|
||
x);
|
||
}
|
||
throw new Error(); // This cannot happen
|
||
}
|
||
```
|
||
|
||
|
||
|
||
### 2.5 自定义类加载器
|
||
|
||
什么时候需要自定义类加载器
|
||
|
||
1. 想加载非 classpath 随意路径中的类文件
|
||
2. 都是通过接口来使用实现,希望解耦时,常用在框架设计
|
||
3. 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
|
||
|
||
|
||
|
||
步骤:
|
||
|
||
1. 继承 ClassLoader 父类
|
||
2. 要遵从双亲委派机制,重写 findClass 方法 注意不是重写 loadClass 方法,否则不会走双亲委派机制
|
||
3. 读取类文件的字节码
|
||
4. 调用父类的 defineClass 方法来加载类
|
||
5. 使用者调用该类加载器的 loadClass 方法
|
||
|
||
|
||
|
||
```java
|
||
public class CustomClassLoader extends ClassLoader{
|
||
@Override
|
||
protected Class<?> findClass(String name) throws ClassNotFoundException {
|
||
// 类加载路径
|
||
String path = "G:\\classpath\\" + name + ".class";
|
||
try (ByteArrayOutputStream os = new ByteArrayOutputStream();) {
|
||
Files.copy(Paths.get(path), os);
|
||
// 获取字节码数据的 byte 数组
|
||
byte[] bytes = os.toByteArray();
|
||
// 加载 Class 文件
|
||
return defineClass(name, bytes, 0, bytes.length);
|
||
} catch (IOException e) {
|
||
e.printStackTrace();
|
||
throw new ClassNotFoundException("类文件未找到", e);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
> **Tip**
|
||
>
|
||
> 判断两个类是否是同一个类:**同样的包名、类名和使用相同的类加载器**
|
||
|
||
|
||
|
||
## 3 运行期优化
|
||
|
||
### 3.1 即时编译
|
||
|
||
#### 1) 分层编译
|
||
|
||
TieredCompilation
|
||
|
||
现来执行以下代码
|
||
|
||
```java
|
||
public class JIT1 {
|
||
public static void main(String[] args) {
|
||
for (int i = 0; i < 200; i++) {
|
||
long start = System.nanoTime();
|
||
for (int j = 0; j < 1000; j++) {
|
||
new Object();
|
||
}
|
||
long end = System.nanoTime();
|
||
System.out.printf("%d\t%d\n", i, (end -start));
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
输出一下结果(进行了截断获取),发现
|
||
|
||
- 执行到差不多 69 次,发现已经快了将近4倍
|
||
- 执行到差不多 147 次,发现已经快了将近100倍
|
||
|
||
```
|
||
0 30200
|
||
1 23800
|
||
...
|
||
67 18400
|
||
68 9100
|
||
...
|
||
145 9600
|
||
146 400
|
||
```
|
||
|
||
原因是什么呢?
|
||
|
||
JVM 将执行状态分成了 5 个层次:
|
||
|
||
- 0 层,解释执行(Interpreter)
|
||
- 1 层,使用 C1 即时编译器编译执行(不带 profiling)
|
||
- 2 层,使用 C1 即时编译器编译执行(带基本的 profiling)
|
||
- 3 层,使用 C1 即时编译器编译执行(带完全的 profiling)
|
||
- 4 层,使用 C2 即时编译器编译执行
|
||
|
||
> profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的 回边次数】等
|
||
|
||
|
||
|
||
即时编译器(JIT)与解释器的区别
|
||
|
||
- 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
|
||
- JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
|
||
- 解释器是将字节码解释为针对所有平台都通用的机器码
|
||
- JIT 会根据平台类型,生成平台特定的机器码
|
||
|
||
对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运 行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速 度。 执行效率上简单比较一下 `Interpreter < C1 < C2`,总的目标是发现热点代码(`hotspot`名称的由来),优化之
|
||
|
||
|
||
|
||
刚才的一种优化手段称之为【逃逸分析】,发现新建的对象是否逃逸。可以使用 `-XX:-DoEscapeAnalysis` 关闭逃逸分析,再运行刚才的示例观察结果
|
||
|
||
|
||
|
||
[参考资料: Java HotSpot Virtual Machine Performance Enhancements (oracle.com)](https://docs.oracle.com/en/java/javase/12/vm/java-hotspot-virtual-machine-performance-enhancements.html#GUID-D2E3DC58-D18B-4A6C-8167-4A1DFB4888E4)
|
||
|
||
|
||
|
||
#### 2) 方法内联
|
||
|
||
Inlining
|
||
|
||
定义一个计算方法 square
|
||
|
||
```java
|
||
private static int square(final int i) {
|
||
return i * i;
|
||
}
|
||
```
|
||
|
||
将其调用
|
||
|
||
```java
|
||
System.out.println(square((9)));
|
||
```
|
||
|
||
如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、 粘贴到调用者的位置:
|
||
|
||
```java
|
||
System.out.println(9 * 9);
|
||
```
|
||
|
||
还能够进行常量折叠 (Constant Folding) 的优化
|
||
|
||
```java
|
||
System.out.println(81);
|
||
```
|
||
|
||
|
||
|
||
进行实验代码测试
|
||
|
||
```java
|
||
public class JIT2 {
|
||
private static int square(final int i) {
|
||
return i * i;
|
||
}
|
||
|
||
public static void main(String[] args) {
|
||
int x = 0;
|
||
for (int i = 0; i < 500; i++) {
|
||
long start = System.nanoTime();
|
||
for (int j = 0; j < 1000; j++) {
|
||
x = square(9);
|
||
}
|
||
long end = System.nanoTime();
|
||
System.out.printf("%d\t%d\n", i, (end -start));
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
输出一下结果(进行了截断获取),发现
|
||
|
||
- 越后面的执行越快,已经被 JVM 优化了
|
||
|
||
```
|
||
0 1335400
|
||
1 38700
|
||
2 83100
|
||
...
|
||
64 9800
|
||
65 6400
|
||
...
|
||
252 0
|
||
253 0
|
||
```
|
||
|
||
|
||
|
||
接下来进行添加虚拟机参数来查看一些优化信息
|
||
|
||
- `-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining `:打印方法内联的信息
|
||
|
||
执行信息如下(只截取了一部分)
|
||
|
||
- 可以看见 square 方法被优化了三次,第三次甚至被标记为热点代码 `inline (hot)`
|
||
|
||
```
|
||
...
|
||
@ 28 com.zhuhjay.demo5.JIT2::square (4 bytes)
|
||
@ 38 java.lang.System::nanoTime (0 bytes) intrinsic
|
||
@ 11 java.lang.System::nanoTime (0 bytes) intrinsic
|
||
@ 28 com.zhuhjay.demo5.JIT2::square (4 bytes)
|
||
@ 38 java.lang.System::nanoTime (0 bytes) intrinsic
|
||
@ 28 com.zhuhjay.demo5.JIT2::square (4 bytes) inline (hot)
|
||
@ 38 java.lang.System::nanoTime (0 bytes) (intrinsic)
|
||
@ 11 java.lang.System::nanoTime (0 bytes) (intrinsic)
|
||
```
|
||
|
||
- `-XX:CompileCommand=dontinline,*JIT2.square`:禁止某个方法的方法内联,因为 Java 中也有很多使用了方法内联的,全部禁用会影响效率
|
||
|
||
执行信息如下(只截取部分进行展示)
|
||
|
||
- 到了最后一次执行,也不能够优化为 0
|
||
|
||
```
|
||
CompilerOracle: dontinline *JIT2.square
|
||
0 106000
|
||
1 24000
|
||
...
|
||
65 6300
|
||
66 6600
|
||
...
|
||
498 5500
|
||
499 4800
|
||
```
|
||
|
||
- `-XX:+PrintCompilation`:打印编译信息
|
||
|
||
|
||
|
||
#### 3) 字段优化
|
||
|
||
JMH 基准测试请参考:[http://openjdk.java.net/projects/code-tools/jmh/](http://openjdk.java.net/projects/code-tools/jmh/)
|
||
|
||
为了保证配置的正确性,建议使用 archetype 生成 JMH 配置项目。cmd 运行下面这段代码:
|
||
|
||
- 解读:创建一个文件夹 jmh-test,里面包含完整的 jmh 配置 pom 文件
|
||
|
||
```shell
|
||
mvn archetype:generate ^
|
||
-DinteractiveMode=false ^
|
||
-DarchetypeGroupId=org.openjdk.jmh ^
|
||
-DarchetypeArtifactId=jmh-java-benchmark-archetype ^
|
||
-DarchetypeVersion=1.25 ^
|
||
-DgroupId=com.zhuhjay ^
|
||
-DartifactId=jmh-test ^
|
||
-Dversion=1.0.0
|
||
```
|
||
|
||
将以上创建的项目导入到 IDEA,并进行 Maven 加载,可能需要将 Maven 中的两个 jar 包使用 [mvnrepository](https://mvnrepository.com/) 进行下载,手动使用maven 命令添加到仓库中,如果需要则使用以下命令进行安装
|
||
|
||
```shell
|
||
mvn install:install-file ^
|
||
-Dfile=/filepath/jmh-generator-annprocess-1.25.jar ^
|
||
-DgroupId=org.openjdk.jmh ^
|
||
-DartifactId=jmh-generator-annprocess ^
|
||
-Dversion=1.25 ^
|
||
-Dpackaging=jar
|
||
```
|
||
|
||
```shell
|
||
mvn install:install-file ^
|
||
-Dfile=/filepath/jmh-core-1.25.jar ^
|
||
-DgroupId=org.openjdk.jmh ^
|
||
-DartifactId=jmh-core ^
|
||
-Dversion=1.25 ^
|
||
-Dpackaging=jar
|
||
```
|
||
|
||
可能还需要以下 maven 坐标
|
||
|
||
```xml
|
||
<dependency>
|
||
<groupId>org.apache.commons</groupId>
|
||
<artifactId>commons-math3</artifactId>
|
||
<version>3.6.1</version>
|
||
</dependency>
|
||
```
|
||
|
||
|
||
|
||
以下为基准测试代码,三个测试方法都是对数字进行累加的操作
|
||
|
||
- `@Warmup(iterations = 2, time = 1)`
|
||
- 进行几轮热身,让程序预热,使得对其进行充分的优化
|
||
- `@Measurement(iterations = 5, time = 1)`
|
||
- 设置进行几轮测试,然后取平均值,较为直观
|
||
- `@Benchmark` 未来对该注解标识的方法进行测试
|
||
- `@CompilerControl(CompilerControl.Mode.INLINE)` 控制被调用方法使用方法内联
|
||
- 三个测试方法区别:
|
||
1. 直接使用成员变量进行累加
|
||
2. 使用局部变量进行累加
|
||
3. 使用 foreach 对成员变量进行累加
|
||
|
||
```java
|
||
@Warmup(iterations = 2, time = 1)
|
||
@Measurement(iterations = 5, time = 1)
|
||
@State(Scope.Benchmark)
|
||
public class Benchmark1 {
|
||
int[] elements = randomInts(1_000);
|
||
|
||
private static int[] randomInts(int size) {
|
||
Random random = ThreadLocalRandom.current();
|
||
int[] values = new int[size];
|
||
for (int i = 0; i < size; i++) {
|
||
values[i] = random.nextInt();
|
||
}
|
||
return values;
|
||
}
|
||
|
||
@Benchmark
|
||
public void test1() {
|
||
for (int i = 0; i < elements.length; i++) {
|
||
doSum(elements[i]);
|
||
}
|
||
}
|
||
|
||
@Benchmark
|
||
public void test2() {
|
||
int[] local = this.elements;
|
||
for (int i = 0; i < local.length; i++) {
|
||
doSum(local[i]);
|
||
}
|
||
}
|
||
|
||
@Benchmark
|
||
public void test3() {
|
||
for (int element : elements) {
|
||
doSum(element);
|
||
}
|
||
}
|
||
|
||
static int sum = 0;
|
||
|
||
@CompilerControl(CompilerControl.Mode.INLINE)
|
||
static void doSum(int x) {
|
||
sum += x;
|
||
}
|
||
|
||
public static void main(String[] args) throws RunnerException {
|
||
Options opt = new OptionsBuilder()
|
||
.include(Benchmark1.class.getSimpleName())
|
||
.forks(1)
|
||
.build();
|
||
new Runner(opt).run();
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
|
||
首先启用 doSum 的方法内联,测试结果如下(每秒吞吐量,分数越高的更好)
|
||
|
||
```
|
||
Benchmark Mode Cnt Score Error Units
|
||
Benchmark1.test1 thrpt 5 3109424.624 ± 253596.436 ops/s
|
||
Benchmark1.test2 thrpt 5 3354988.133 ± 91779.242 ops/s
|
||
Benchmark1.test3 thrpt 5 3192089.287 ± 811172.283 ops/s
|
||
```
|
||
|
||
接下来禁用 doSum 方法内联
|
||
|
||
```java
|
||
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
|
||
static void doSum(int x) {
|
||
sum += x;
|
||
}
|
||
```
|
||
|
||
此时测试结果如下,跟以上结果对比,发现吞吐量小了10倍左右
|
||
|
||
```
|
||
Benchmark Mode Cnt Score Error Units
|
||
Benchmark1.test1 thrpt 5 357337.402 ± 131498.372 ops/s
|
||
Benchmark1.test2 thrpt 5 394812.201 ± 434387.776 ops/s
|
||
Benchmark1.test3 thrpt 5 377967.137 ± 377008.086 ops/s
|
||
```
|
||
|
||
|
||
|
||
分析:
|
||
|
||
在刚才的示例中,doSum 方法是否内联会影响 elements 成员变量读取的优化:
|
||
|
||
如果 doSum 方法内联了,刚才的 test1 方法会被优化成下面的样子(伪代码):
|
||
|
||
```java
|
||
@Benchmark
|
||
public void test1() {
|
||
// elements 首次读取会缓存起来 -> int[] local(机器码级别缓存,后续就不需要再访问 elements 成员变量了)
|
||
for (int i = 0; i < elements.length; i++) { // 后续 999 次 求长度 <- local
|
||
sum += elements[i]; // 1000 次取下标 i 的元素 <- local
|
||
}
|
||
}
|
||
```
|
||
|
||
可以节省 1999 次 Field 读取操作
|
||
|
||
但如果 doSum 方法没有内联,则不会进行上面的优化
|
||
|
||
|
||
|
||
> 练习:在内联情况下将 elements 添加 volatile 修饰符会发生什么?
|
||
>
|
||
> 运行结果如下,发现方法一的吞吐量大大降低了
|
||
>
|
||
> ```
|
||
> Benchmark Mode Cnt Score Error Units
|
||
> Benchmark1.test1 thrpt 5 660532.339 ± 41637.563 ops/s
|
||
> Benchmark1.test2 thrpt 5 3356062.311 ± 32060.956 ops/s
|
||
> Benchmark1.test3 thrpt 5 3359952.483 ± 30073.397 ops/s
|
||
> ```
|
||
|
||
|
||
|
||
> **Tip**:
|
||
>
|
||
> 这三种方法分别进行了优化
|
||
>
|
||
> 1. 如果没有关闭方法内联,则虚拟机将自动进行优化
|
||
> 2. 使用了局部变量,避免多次向成员变量进行访问,进行了手动优化
|
||
> 3. foreach 语法糖编译结果与 2 相同,也属于编译器优化
|
||
>
|
||
> **注意**:若要对这种类似的进行优化,可以考虑多使用局部变量。
|
||
|
||
|
||
|
||
### 3.2 反射优化
|
||
|
||
运行以下反射代码,进行代码执行分析
|
||
|
||
```java
|
||
public class Reflect1 {
|
||
public static void foo() {
|
||
System.out.println("foo method");
|
||
}
|
||
|
||
public static void main(String[] args) throws Exception {
|
||
Method method = Reflect1.class.getMethod("foo");
|
||
for (int i = 0; i <= 16; i++) {
|
||
System.out.printf("%d\t", i);
|
||
method.invoke(null);
|
||
}
|
||
System.in.read();
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
|
||
在方法反射的 invoke 源码中,会发现反射调用是通过一个接口 `MethodAccessor` 来执行的
|
||
|
||
```java
|
||
@CallerSensitive
|
||
public Object invoke(Object obj, Object... args)
|
||
throws IllegalAccessException, IllegalArgumentException,
|
||
InvocationTargetException
|
||
{
|
||
if (!override) {
|
||
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
|
||
Class<?> caller = Reflection.getCallerClass();
|
||
checkAccess(caller, clazz, obj, modifiers);
|
||
}
|
||
}
|
||
MethodAccessor ma = methodAccessor; // read volatile
|
||
if (ma == null) {
|
||
ma = acquireMethodAccessor();
|
||
}
|
||
return ma.invoke(obj, args);
|
||
}
|
||
```
|
||
|
||
|
||
|
||
`method.invoke(null)` 前面 0 ~ 15 次调用使用的是 `MethodAccessor` 的 `NativeMethodAccessorImpl` 实现,是个本地方法
|
||
|
||
- 步入到 `ReflectionFactory.inflationThreshold()` 中可以发现该值为 15,是一个膨胀阈值,就是执行本地方法反射的最大执行次数
|
||
- 当超过了膨胀阈值之后,就会使用 ASM 动态生成的新实现代替本地实现,执行速度较本地快 20 倍左右
|
||
|
||
```java
|
||
class NativeMethodAccessorImpl extends MethodAccessorImpl {
|
||
private final Method method;
|
||
private DelegatingMethodAccessorImpl parent;
|
||
private int numInvocations;
|
||
|
||
NativeMethodAccessorImpl(Method var1) {
|
||
this.method = var1;
|
||
}
|
||
|
||
public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
|
||
// 判断是否超过预设的膨胀阈值
|
||
if (++this.numInvocations > ReflectionFactory.inflationThreshold() &&
|
||
!ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
|
||
// 使用 ASM 动态生成新的反射调用
|
||
MethodAccessorImpl var3 =
|
||
(MethodAccessorImpl)(new MethodAccessorGenerator())
|
||
.generateMethod(
|
||
this.method.getDeclaringClass(),
|
||
this.method.getName(),
|
||
this.method.getParameterTypes(),
|
||
this.method.getReturnType(),
|
||
this.method.getExceptionTypes(),
|
||
this.method.getModifiers());
|
||
this.parent.setDelegate(var3);
|
||
}
|
||
return invoke0(this.method, var1, var2);
|
||
}
|
||
|
||
void setParent(DelegatingMethodAccessorImpl var1) {
|
||
this.parent = var1;
|
||
}
|
||
|
||
private static native Object invoke0(Method var0, Object var1, Object[] var2);
|
||
}
|
||
```
|
||
|
||
当调用到第 16 次(从0开始算)时,会采用运行时生成的类代替掉最初的实现,可以通过 debug 得到 类名为 `sun.reflect.GeneratedMethodAccessor1`
|
||
|
||
<img src="/jvm/1670569560207.png" alt="1670569560207" style="zoom:67%;" />
|
||
|
||
|
||
|
||
可以使用 arthas 工具来进行查看
|
||
|
||
- 进入到对应的运行进程
|
||
- 输入命令 `jad sun.reflect.GeneratedMethodAccessor1` 进行反编译
|
||
|
||
<img src="/jvm/1670570534000.png" alt="1670569967783" style="zoom:67%;" />
|
||
|
||
|
||
|
||
> **注意**
|
||
>
|
||
> 通过查看 `ReflectionFactory` 源码可知
|
||
>
|
||
> - `sun.reflect.noInflation` 可以用来禁用膨胀(直接生成 `GeneratedMethodAccessor1`,但首 次生成比较耗时,如果仅反射调用一次,不划算)
|
||
> - `sun.reflect.inflationThreshold` 可以修改膨胀阈值
|