JVM之类加载

2022/12/9 JVM

# 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

# 1.2 链接

# 1) 验证

验证类是否符合 JVM规范,安全性检查

修改 HelloWorld.class 的魔数,在控制台运行

  • Incompatible magic value:JVM 检查到了不兼容的魔数
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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 2) 准备

为 static 变量分配空间,设置默认值

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从JDK 7 开始,存储于 _java_mirror末尾
  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果 static 变量是 final基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
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");
}
1
2
3
4
5
6
7

以上代码的字节码为,印证以下结果

  • 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 3) 解析

将常量池中的符号引用解析为直接引用

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 { }
1
2
3
4
5
6
7
8
9
10
11
12
13

接下来打开 HSDB 工具(在 JDK 目录下执行命令) java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB

查看是否存在类 D,以及类 C 中的常量池信息

  • 以上代码执行后,只会存在类C对象,而类D对象只存在字节码信息,不会进行解析

1670427087000

而如果直接创建了对象,那么就会存在类C和类D对象,此时类C和类D将会被解析完

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
2
3
4
5
6
7
8
9
10

# 1.3 初始化

# <clinit>()V

初始化即调用 <clinit>()V ,虚拟机会保证这个类的『构造方法』的线程安全

# 发生时机

概括得说,类初始化是【懒惰的】

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

不会导致类初始化的情况

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
  • 类.class 不会触发初始化
  • 创建该类的数组不会触发初始化
  • 类加载器的 loadClass 方法
  • Class.forName 的参数 2 为 false

使用以下代码对以上进行验证(依次执行)

  1. 当只有一个空的 mian 方法进行执行时,会有 main init :main 方法所在的类,总会被首先初始化
  2. final static double b = 5.0 不会导致初始化过程产生:访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
  3. B.class 不会触发初始化
  4. new B[0] 不会触发初始化
  5. c1.loadClass("com.zhuhjay.demo4.B") 会加载类B以及父类A,但不会触发初始化
  6. Class.forName("com.zhuhjay.demo4.B", false, c2) 会加载类B以及父类A,但不会触发初始化
  7. static int a = 0 在初始化阶段才进行赋值,此时会触发初始化
  8. static boolean c = false 在初始化阶段进行赋值,子类初始化触发会引发父类初始化
  9. 使用子类访问父类 static int a = 0,只会触发父类的初始化
  10. Class.forName("com.zhuhjay.demo4.B") 会触发初始化,并且父类先进行初始化
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 1.4 练习

判断以下代码哪些会触发E的初始化

  • public static final Integer c = 20 在编译阶段会转换为 public static final Integer c = Integer.valueOf(20),访问变量c会触发初始化方法
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");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

懒惰初始化单例模式

  • 懒惰的、线程安全的
  • 只有在调用 getInstance() 方法的时候, 静态内部类 LazyHolder 才会被初始化
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");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 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 类加载器加载类:

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");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

将上述代码编译执行一次过后,打开对应的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
1
2

# 2.2 扩展类加载器

编译执行以下代码

public class G {
    static {
        System.out.println("ext classpath G init");
    }
}
1
2
3
4
5
public class Load7 {
    public static void main(String[] args) throws Exception {
        Class<?> aClass = Class.forName("com.zhuhjay.demo4.G");
        System.out.println(aClass.getClassLoader());
    }
}
1
2
3
4
5
6

移步至 class 输出目录下,使用命令 jar -cvf my.jar com/zhuhjay/demo4/G.class 对类G生成jar包,将生成后的jar包复制到 JDK 目录下的 jre/lib/ext 中,然后将 G 改为以下

public class G {
    static {
        System.out.println("classpath G init");
    }
}
1
2
3
4
5

再次执行 Load7 得到结果

  • 执行的类G是在 JDK 目录下的,并且由 Extension ClassLoader 进行该类的创建,使得自己写的类G失效,印证了双亲委派(启动>扩展>应用>自定义)
ext classpath G init
sun.misc.Launcher$ExtClassLoader@7f31245a
1
2

# 2.3 双亲委派模式

所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则

  • 双亲委派并没有继承关系,而是一种委托
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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 2.4 线程上下文类加载器

我们在使用 JDBC 的时候,都需要加载 Driver 驱动,发现不写 Class.forName("com.mysql.jdbc.Driver"); 也能正确加载该驱动并进行使用(需要引入驱动jar)

// 注册驱动
// 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"));
1
2
3
4
5
6
7
8

看看 DriverManager 源码,可以发现它好像自动去加载了驱动。

public class DriverManager {
    // 注册驱动的集合
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = 
        	new CopyOnWriteArrayList<>();
    static {
        // 初始化驱动
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
}
1
2
3
4
5
6
7
8
9
10

先获取看看 DriverManager 的类加载器,可以发现打印结果为 null,证明说 DriverManager 是通过 Bootstrap ClassLoader 进行加载的,会到 JAVA_HOME/jre/lib 目录下进行类的搜索,但是显然 mysql驱动 文件并不是出现在 JAVA_HOME/jre/lib 目录下面,那为什么它却可以加载到 com.mysql.jdbc.Driver

System.out.println(DriverManager.class.getClassLoader());
1

看看源码中 loadInitialDrivers() 方法如何进行工作的

  • Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); 打破双亲委派机制,直接使用应用类加载器来进行驱动的类加载
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

1) 中使用的就是大名鼎鼎的 Service Provider Interface (SPI)

  • 约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称(就像 SpringBoot 中使用的 spring.factories 文件)

    1670493540488

之后就可以使用以下 ServiceLoader 加载器来加载信息

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
while(driversIterator.hasNext()) {
    driversIterator.next();
}
1
2
3
4
5

体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:

  • JDBC
  • Servlet 初始化器
  • Spring 容器
  • Dubbo(对 SPI 进行了扩展)

接着看看 ServiceLoader.load(Driver.class) 方法

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 获取线程上下文类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
1
2
3
4
5

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由 Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类 LazyIterator 中:

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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 2.5 自定义类加载器

什么时候需要自定义类加载器

  1. 想加载非 classpath 随意路径中的类文件
  2. 都是通过接口来使用实现,希望解耦时,常用在框架设计
  3. 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:

  1. 继承 ClassLoader 父类
  2. 要遵从双亲委派机制,重写 findClass 方法 注意不是重写 loadClass 方法,否则不会走双亲委派机制
  3. 读取类文件的字节码
  4. 调用父类的 defineClass 方法来加载类
  5. 使用者调用该类加载器的 loadClass 方法
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);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Tip

​ 判断两个类是否是同一个类:同样的包名、类名和使用相同的类加载器

# 3 运行期优化

# 3.1 即时编译

# 1) 分层编译

TieredCompilation

现来执行以下代码

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));
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

输出一下结果(进行了截断获取),发现

  • 执行到差不多 69 次,发现已经快了将近4倍
  • 执行到差不多 147 次,发现已经快了将近100倍
0	30200
1	23800
...
67	18400
68	9100
...
145	9600
146	400
1
2
3
4
5
6
7
8

原因是什么呢?

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) (opens new window)

# 2) 方法内联

Inlining

定义一个计算方法 square

private static int square(final int i) {
    return i * i;
}
1
2
3

将其调用

System.out.println(square((9)));
1

如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、 粘贴到调用者的位置:

System.out.println(9 * 9);
1

还能够进行常量折叠 (Constant Folding) 的优化

System.out.println(81);
1

进行实验代码测试

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));
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

输出一下结果(进行了截断获取),发现

  • 越后面的执行越快,已经被 JVM 优化了
0	1335400
1	38700
2	83100
...
64	9800
65	6400
...
252	0
253	0
1
2
3
4
5
6
7
8
9

接下来进行添加虚拟机参数来查看一些优化信息

  • -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)
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  • -XX:CompileCommand=dontinline,*JIT2.square:禁止某个方法的方法内联,因为 Java 中也有很多使用了方法内联的,全部禁用会影响效率

    执行信息如下(只截取部分进行展示)

    • 到了最后一次执行,也不能够优化为 0
    CompilerOracle: dontinline *JIT2.square
    0	106000
    1	24000
    ...
    65	6300
    66	6600
    ...
    498	5500
    499	4800
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  • -XX:+PrintCompilation:打印编译信息

# 3) 字段优化

JMH 基准测试请参考:http://openjdk.java.net/projects/code-tools/jmh/ (opens new window)

为了保证配置的正确性,建议使用 archetype 生成 JMH 配置项目。cmd 运行下面这段代码:

  • 解读:创建一个文件夹 jmh-test,里面包含完整的 jmh 配置 pom 文件
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
1
2
3
4
5
6
7
8

将以上创建的项目导入到 IDEA,并进行 Maven 加载,可能需要将 Maven 中的两个 jar 包使用 mvnrepository (opens new window) 进行下载,手动使用maven 命令添加到仓库中,如果需要则使用以下命令进行安装

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
1
2
3
4
5
6
mvn install:install-file ^
-Dfile=/filepath/jmh-core-1.25.jar ^
-DgroupId=org.openjdk.jmh ^
-DartifactId=jmh-core ^
-Dversion=1.25 ^
-Dpackaging=jar
1
2
3
4
5
6

可能还需要以下 maven 坐标

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-math3</artifactId>
    <version>3.6.1</version>
</dependency>
1
2
3
4
5

以下为基准测试代码,三个测试方法都是对数字进行累加的操作

  • @Warmup(iterations = 2, time = 1)
    • 进行几轮热身,让程序预热,使得对其进行充分的优化
  • @Measurement(iterations = 5, time = 1)
    • 设置进行几轮测试,然后取平均值,较为直观
  • @Benchmark 未来对该注解标识的方法进行测试
  • @CompilerControl(CompilerControl.Mode.INLINE) 控制被调用方法使用方法内联
  • 三个测试方法区别:
    1. 直接使用成员变量进行累加
    2. 使用局部变量进行累加
    3. 使用 foreach 对成员变量进行累加
@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();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

首先启用 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
1
2
3
4

接下来禁用 doSum 方法内联

@CompilerControl(CompilerControl.Mode.DONT_INLINE)
static void doSum(int x) {
    sum += x;
}
1
2
3
4

此时测试结果如下,跟以上结果对比,发现吞吐量小了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
1
2
3
4

分析:

在刚才的示例中,doSum 方法是否内联会影响 elements 成员变量读取的优化:

如果 doSum 方法内联了,刚才的 test1 方法会被优化成下面的样子(伪代码):

@Benchmark
public void test1() {
    // elements 首次读取会缓存起来 -> int[] local(机器码级别缓存,后续就不需要再访问 elements 成员变量了)
    for (int i = 0; i < elements.length; i++) { // 后续 999 次 求长度 <- local
        sum += elements[i]; // 1000 次取下标 i 的元素 <- local
    }
}
1
2
3
4
5
6
7

可以节省 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
1
2
3
4

Tip:

这三种方法分别进行了优化

  1. 如果没有关闭方法内联,则虚拟机将自动进行优化
  2. 使用了局部变量,避免多次向成员变量进行访问,进行了手动优化
  3. foreach 语法糖编译结果与 2 相同,也属于编译器优化

注意:若要对这种类似的进行优化,可以考虑多使用局部变量。

# 3.2 反射优化

运行以下反射代码,进行代码执行分析

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();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在方法反射的 invoke 源码中,会发现反射调用是通过一个接口 MethodAccessor 来执行的

@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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

method.invoke(null) 前面 0 ~ 15 次调用使用的是 MethodAccessorNativeMethodAccessorImpl 实现,是个本地方法

  • 步入到 ReflectionFactory.inflationThreshold() 中可以发现该值为 15,是一个膨胀阈值,就是执行本地方法反射的最大执行次数
  • 当超过了膨胀阈值之后,就会使用 ASM 动态生成的新实现代替本地实现,执行速度较本地快 20 倍左右
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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

当调用到第 16 次(从0开始算)时,会采用运行时生成的类代替掉最初的实现,可以通过 debug 得到 类名为 sun.reflect.GeneratedMethodAccessor1

1670569560207

可以使用 arthas 工具来进行查看

  • 进入到对应的运行进程
  • 输入命令 jad sun.reflect.GeneratedMethodAccessor1 进行反编译
1670569967783

注意

通过查看 ReflectionFactory 源码可知

  • sun.reflect.noInflation 可以用来禁用膨胀(直接生成 GeneratedMethodAccessor1,但首 次生成比较耗时,如果仅反射调用一次,不划算)
  • sun.reflect.inflationThreshold 可以修改膨胀阈值