--- 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."":(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 初始化 #### `()V` 初始化即调用 `()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:` - `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 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() { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } // 1) 使用 ServiceLoader 机制加载驱动,即 SPI AccessController.doPrivileged(new PrivilegedAction() { public Void run() { ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class); Iterator 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` 文件) 1670493540488 之后就可以使用以下 ServiceLoader 加载器来加载信息 ```java ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class); Iterator driversIterator = loadedDrivers.iterator(); while(driversIterator.hasNext()) { driversIterator.next(); } ``` 体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想: - JDBC - Servlet 初始化器 - Spring 容器 - Dubbo(对 SPI 进行了扩展) 接着看看 `ServiceLoader.load(Driver.class)` 方法 ```java public static ServiceLoader load(Class 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 org.apache.commons commons-math3 3.6.1 ``` 以下为基准测试代码,三个测试方法都是对数字进行累加的操作 - `@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` 1670569560207 可以使用 arthas 工具来进行查看 - 进入到对应的运行进程 - 输入命令 `jad sun.reflect.GeneratedMethodAccessor1` 进行反编译 1670569967783 > **注意** > > 通过查看 `ReflectionFactory` 源码可知 > > - `sun.reflect.noInflation` 可以用来禁用膨胀(直接生成 `GeneratedMethodAccessor1`,但首 次生成比较耗时,如果仅反射调用一次,不划算) > - `sun.reflect.inflationThreshold` 可以修改膨胀阈值