Files
www/blogs/java/jvm/JVM-类加载.md
zhuhjay 22e48d9558 build(www): 添加 Drone CI 流水线配置
- 新增 .drone.yml 文件用于定义 CI/CD 流程
- 配置了基于 Docker 的部署步骤
- 设置了工作区和卷映射以支持持久化数据
- 添加了构建准备阶段和 Docker 部署阶段
- 定义了环境变量和代理设置
- 配置了 artifacts 目录的处理逻辑
- 添加了 timezone 映射以确保时间同步
- 设置了 docker.sock 映射以支持 Docker in Docker
2025-11-01 13:36:00 +08:00

1185 lines
36 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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` 工具查看
![1670419734596](/jvm/1670419734596.png)
### 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对象只存在字节码信息不会进行解析
![1670427087000](/jvm/1670427087000.png)
而如果直接创建了对象那么就会存在类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` 可以修改膨胀阈值