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
工具查看
# 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)
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");
}
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
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 { }
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对象只存在字节码信息,不会进行解析
而如果直接创建了对象,那么就会存在类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();
}
}
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
使用以下代码对以上进行验证(依次执行)
- 当只有一个空的 mian 方法进行执行时,会有
main init
:main 方法所在的类,总会被首先初始化 final static double b = 5.0
不会导致初始化过程产生:访问类的 static final 静态常量(基本类型和字符串)不会触发初始化B.class
不会触发初始化new B[0]
不会触发初始化c1.loadClass("com.zhuhjay.demo4.B")
会加载类B以及父类A,但不会触发初始化Class.forName("com.zhuhjay.demo4.B", false, c2)
会加载类B以及父类A,但不会触发初始化static int a = 0
在初始化阶段才进行赋值,此时会触发初始化static boolean c = false
在初始化阶段进行赋值,子类初始化触发会引发父类初始化- 使用子类访问父类
static int a = 0
,只会触发父类的初始化 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");
}
}
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");
}
}
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");
}
}
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");
}
}
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
2
# 2.2 扩展类加载器
编译执行以下代码
public class G {
static {
System.out.println("ext classpath G init");
}
}
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());
}
}
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");
}
}
2
3
4
5
再次执行 Load7 得到结果
- 执行的类G是在 JDK 目录下的,并且由 Extension ClassLoader 进行该类的创建,使得自己写的类G失效,印证了双亲委派(启动>扩展>应用>自定义)
ext classpath G init
sun.misc.Launcher$ExtClassLoader@7f31245a
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;
}
}
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"));
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");
}
}
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());
看看源码中 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);
}
}
}
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
文件)
之后就可以使用以下 ServiceLoader 加载器来加载信息
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
while(driversIterator.hasNext()) {
driversIterator.next();
}
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);
}
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
}
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 自定义类加载器
什么时候需要自定义类加载器
- 想加载非 classpath 随意路径中的类文件
- 都是通过接口来使用实现,希望解耦时,常用在框架设计
- 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
步骤:
- 继承 ClassLoader 父类
- 要遵从双亲委派机制,重写 findClass 方法 注意不是重写 loadClass 方法,否则不会走双亲委派机制
- 读取类文件的字节码
- 调用父类的 defineClass 方法来加载类
- 使用者调用该类加载器的 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);
}
}
}
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));
}
}
}
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
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;
}
2
3
将其调用
System.out.println(square((9)));
如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、 粘贴到调用者的位置:
System.out.println(9 * 9);
还能够进行常量折叠 (Constant Folding) 的优化
System.out.println(81);
进行实验代码测试
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));
}
}
}
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
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- 可以看见 square 方法被优化了三次,第三次甚至被标记为热点代码
-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
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
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
2
3
4
5
6
可能还需要以下 maven 坐标
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-math3</artifactId>
<version>3.6.1</version>
</dependency>
2
3
4
5
以下为基准测试代码,三个测试方法都是对数字进行累加的操作
@Warmup(iterations = 2, time = 1)
- 进行几轮热身,让程序预热,使得对其进行充分的优化
@Measurement(iterations = 5, time = 1)
- 设置进行几轮测试,然后取平均值,较为直观
@Benchmark
未来对该注解标识的方法进行测试@CompilerControl(CompilerControl.Mode.INLINE)
控制被调用方法使用方法内联- 三个测试方法区别:
- 直接使用成员变量进行累加
- 使用局部变量进行累加
- 使用 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();
}
}
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
2
3
4
接下来禁用 doSum 方法内联
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
static void doSum(int x) {
sum += x;
}
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
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
}
}
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:
这三种方法分别进行了优化
- 如果没有关闭方法内联,则虚拟机将自动进行优化
- 使用了局部变量,避免多次向成员变量进行访问,进行了手动优化
- 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();
}
}
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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
method.invoke(null)
前面 0 ~ 15 次调用使用的是 MethodAccessor
的 NativeMethodAccessorImpl
实现,是个本地方法
- 步入到
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);
}
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

可以使用 arthas 工具来进行查看
- 进入到对应的运行进程
- 输入命令
jad sun.reflect.GeneratedMethodAccessor1
进行反编译

注意
通过查看
ReflectionFactory
源码可知
sun.reflect.noInflation
可以用来禁用膨胀(直接生成GeneratedMethodAccessor1
,但首 次生成比较耗时,如果仅反射调用一次,不划算)sun.reflect.inflationThreshold
可以修改膨胀阈值