三、类文件结构
1.类文件结构
1.1魔数
1.2版本
1.3常量池
1.常量池是一张表,容量计数(只有这个是)从1开始,第0项表示不引用任何一个常量池项目。
eg:
2.常量池主要存放两大类常量:字面量(Literal)和符号引用.
字面量:Java中的字符串,final修饰的常量值,
符号引用:
①类和接口的全限定名;
②字段的名称和描述符
③方法的名称和描述符
注: 字段的描述符是个UTF8字符串,String类型,包含字段所属的类
比如描述符
"Ljava/util/HashMap;"
标识字段是类型是HashMap.
3.常量池中的每一项也是一个表,主要有14种常量类型(表结构见numbers图),该表中第一个字段(占1个字节)表示类型

1.4访问标志与继承信息
1.5字段表 方法表 属性表
4.类加载阶段
4.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 工具查看
4.2 链接
验证
验证类是否符合 JVM规范,安全性检查
用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行
E:\git\jvm\out\production\jvm>java cn.itcast.jvm.t5.HelloWorld
Error: A JNI error has occurred, please check your installation and try again Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value 3405691578 in class file cn/itcast/jvm/t5/HelloWorld
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
1.文件格式验证:①是否以魔数开头②③
2.元数据验证:①这个类是否有父类(除了Object都有父类)②③
3.字节码验证:对类的方法体校验①②保证跳转指令不会跳转到方法体以外的字节码指令上③
准备
为 static 变量分配空间,设置默认值
- static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
- static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶 段完成
- 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
解析
将常量池中的符号引用解析为直接引用。
解析动作主要针对:类或接口(CONSTANT_Class_info)、字段(CONSTANT_Fieldref_info)、类方法(CONSTANT_Methodref_info)、接口方法、方法类型等7类符号引用进行。
package cn.itcast.jvm.t3.load;
/**
* 解析的含义
*/
public class Load2 {
public static void main(String[] args) throws ClassNotFoundException, IOException {
ClassLoader classloader = Load2.class.getClassLoader();
// loadClass 方法不会导致类的解析和初始化
Class> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
// new C();
System.in.read();
}
}
class C {
D d = new D();
}
class D {
}
符号引用:在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现
直接引用:可以是直接指向目标的指针、相对偏移量或是一个能定位到目标的句柄。
1.类或接口的解析: 假设当前所处的类为D
如果要把一个未解析过的符号引用N解析为一个类或接口C的直接引用,如果C是一个类,那么虚拟机将会把代表N的全限定名传递给D的类加载器去加载;如果过程成功,那么C在虚拟机中就成为一个有效的类或接口了。
4.3 初始化
<cinit>()V
方法
初始化即调用 <cinit>()V
,虚拟机会保证这个类的『构造方法』的线程安全
发生的时机
概括得说,类初始化是【懒惰的】
- main 方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new 会导致初始化
不会导致类初始化的情况
- 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
- 类对象.class 不会触发初始化
- 创建该类的数组不会触发初始化
- 类加载器的 loadClass 方法
- Class.forName 的参数 2 为 false 时
实验
class 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");
}
}
验证(实验时请先全部注释,每次只执行其中一个)
public class public class Load3 {
static {
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException {
// 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 cl = Thread.currentThread().getContextClassLoader(); cl.loadClass("cn.itcast.jvm.t3.B");
// 5. 不会初始化类 B,但会加载 B、A
ClassLoader c2 = Thread.currentThread().getContextClassLoader();
Class.forName("cn.itcast.jvm.t3.B", false, c2);
// 1. 首次访问这个类的静态变量或静态方法时
System.out.println(A.a);
// 2. 子类初始化,如果父类还没初始化,会引发
System.out.println(B.c);
// 3. 子类访问父类静态变量,只触发父类初始化
System.out.println(B.a);
// 4. 会初始化类 B,并先初始化类 A
Class.forName("cn.itcast.jvm.t3.B");
}
}
卸载
关于类的卸载,在单例模式讨论篇:单例模式与垃圾回收一文中有过描述,在类使用完之后,如果满足下面的情况,类就会被卸载:
- 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。
做java的朋友对于对象的生命周期可能都比较熟悉,对象基本上都是在jvm的堆区中创建,在创建对象之前,会触发类加载(加载、连接、初始化),当类初始化完成后,根据类信息在堆区中实例化类对象,初始化非静态变量、非静态代码以及默认构造方法,当对象使用完之后会在合适的时候被jvm垃圾收集器回收。读完本文后我们知道,对象的生命周期只是类的生命周期中使用阶段的主动引用的一种情况(即实例化类对象)。而类的整个生命周期则要比对象的生命周期长的多。
4.4 练习
从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始化
public class 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 = "hello";
public static final Integer c = 20;
}
典型应用 - 完成懒惰初始化单例模式
public final class public final class Singleton {
private Singleton() { } // 内部类中保存单例
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
// 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
以上的实现特点是:
- 懒惰实例化
- 初始化时的线程安全是有保障的
5.类加载器
以 JDK 8 为例:
名称 | 加载哪的类 | 说明 |
---|---|---|
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
Application ClassLoader | classpath | 上级为 Extension |
自定义类加载器 | 自定义 | 上级为 Application |
5.1 启动类加载器
用 Bootstrap 类加载器加载类:
package cn.itcast.jvm.t3.load;
public class F {
static {
System.out.println("bootstrap F init");
}
}
执行
package cn.itcast.jvm.t3.load; public class package cn.itcast.jvm.t3.load; public class Load5_1 {
public static void main(String[] args) throws ClassNotFoundException {
Class> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
System.out.println(aClass.getClassLoader());
}
}
输出
E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:. cn.itcast.jvm.t3.loadE:\git\jvm\out\production\jvm>java -Xbootclasspath/a:. cn.itcast.jvm.t3.load.Load5 bootstrap F init
null
- -Xbootclasspath 表示设置 bootclasspath
- 其中 /a:. 表示将当前目录追加至 bootclasspath 之后
- 可以用这个办法替换核心类
- java -Xbootclasspath:
- java -Xbootclasspath/a:<追加路径> //后面追加
- java -Xbootclasspath/p:<追加路径>//前面追加
- java -Xbootclasspath:
5.2 扩展类加载器
package cn.itcast.jvm.t3.load;
public class G {
static {
System.out.println("classpath G init");
}
}
执行
public class public class Load5_2 {
public static void main(String[] args) throws ClassNotFoundException {
Class> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
System.out.println(aClass.getClassLoader());
}
}
输出
classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2
写一个同名的类
package cn.itcast.jvm.t3.load;
public class G {
static {
System.out.println("ext G init");
}
}
打个 jar 包
E:\git\jvm\out\production\jvm>jar -cvf my.jar cn/itcast/jvm/t3/load/G.E:\git\jvm\out\production\jvm>jar -cvf my.jar cn/itcast/jvm/t3/load/G.class
已添加清单
正在添加: cn/itcast/jvm/t3/load/G.class(输入 = 481) (输出 = 322)(压缩了 33%)
将 jar 包拷贝到 JAVA_HOME/jre/lib/ext
重新执行 Load5_2
输出
ext G init
sun.misc.Launcher$ExtClassLoader@29453f44
5.3 双亲委派模式
所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则
注意 :这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系
protected Class<?> loadClass(String name, boolean resolve) protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 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) {
}
if (c == null) {
long t1 = System.nanoTime();
// 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
c = findClass(name);
// 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;
}
}
例如:
public class public class Load5_3 {
public static void main(String[] args) throws ClassNotFoundException {
Class> aClass = Load5_3.class.getClassLoader().loadClass("cn.itcast.jvm.t3.load.H");
System.out.println(aClass.getClassLoader());
}
}
执行流程为:
sun.misc.Launcher$AppClassLoader
//1 处, 开始查看已加载的类,结果没有sun.misc.Launcher$AppClassLoader
// 2 处,委派上级sun.misc.Launcher$ExtClassLoader.loadClass()
sun.misc.Launcher$ExtClassLoader
// 1 处,查看已加载的类,结果没有sun.misc.Launcher$ExtClassLoader
// 3 处,没有上级了,则委派BootstrapClassLoader
查找BootstrapClassLoader
是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有sun.misc.Launcher$ExtClassLoader
// 4 处,调用自己的 findClass 方法,是在JAVA_HOME/jre/lib/ext
下找 H 这个类,显然没有,回到sun.misc.Launcher$AppClassLoader
的 // 2 处继续执行到
sun.misc.Launcher$AppClassLoader
// 4 处,调用它自己的 findClass 方法,在classpath 下查找,找到了
5.4 线程上下文类加载器
我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写
Class.forName(Class.forName("com.mysql.jdbc.Driver")
也是可以让 com.mysql.jdbc.Driver
正确加载的,你知道是怎么做的吗?
让我们追踪一下源码:
public class public class DriverManager {
// 注册驱动的集合
private final static CopyOnWriteArrayList registeredDrivers = new CopyOnWriteArrayList<>();
// 初始化驱动
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
}
先不看别的,看看 DriverManager
的类加载器:
System.out.println(DriverManager.class.System.out.println(DriverManager.class.getClassLoader());
打印 null
,表示它的类加载器是 Bootstrap ClassLoader
,会到 JAVA_HOME/jre/lib
下搜索类,但 JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar
包,这样问题来了,在 DriverManager
的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver
呢?
继续看 loadInitialDrivers()
方法:
private static void loadInitialDriversprivate 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);
}
}
}
先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此 可以顺利完成类加载
再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)
约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称
这样就可以使用
ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while(iter.hasNext()) {
iter.next();
}
来得到实现类,体现的是【面向接口编程 +解耦】的思想,在下面一些框架中都运用了此思想:
- JDBC
- Servlet 初始化器
- Spring 容器
- Dubbo(对 SPI 进行了扩展)
接着看 ServiceLoader.load
方法:
public static <S> ServiceLoader<S> loadpublic static <S> ServiceLoader<S> load(Class service) {
// 获取线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由 Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader
的内部类 LazyIterator
中:
private S nextServiceprivate 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
}
5.5 自定义类加载器
问问自己,什么时候需要自定义类加载器
- 1)想加载非 classpath 随意路径中的类文件
- 2)都是通过接口来使用实现,希望解耦时,常用在框架设计
- 3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
步骤:
- 继承 ClassLoader 父类
- 要遵从双亲委派机制,重写 findClass 方法 注意不是重写 loadClass 方法,否则不会走双亲委派机制
- 读取类文件的字节码
- 调用父类的 defineClass 方法来加载类
- 使用者调用该类加载器的 loadClass 方法 示例: 准备好两个类文件放入 E:\myclasspath,它实现了 java.util.Map 接口,可以先反编译看一下:
6.运行期优化
6.1 即时编译
分层编译
(TieredCompilation) 先来个例子
public class 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));
}
}
}
0 0 119975
1 67136
2 75695
3 58978
4 67313
5 60876
6 56382
7 61951
8 60086
9 54721
10 68217
11 63125
12 62252
13 59107
14 62179
15 62967
16 60759
17 58174
18 59051
19 62752
20 71248
21 55236
22 58342
23 61940
24 62008
25 64776
26 61496
27 75748
28 65983
29 127398
30 66444
31 64651
32 69238
33 66131
34 64187
35 63607
36 65584
37 64174
38 57855
39 57224
40 58237
41 76636
42 43331
43 61726
44 58682
45 60252
46 62083
47 42899
48 39704
49 59313
50 94486
51 67481
52 61291
53 63868
54 63354
55 67037
56 60304
57 62245
58 65261
59 62093
60 122181
61 65958
62 83911
63 28903
64 23913
65 25771
66 21745
67 23870
68 21767
69 25245
70 94944
71 19041
72 20332
73 21841
74 21780
75 19802
76 20878
77 304639
78 29303
79 23766
80 21913
81 25633
82 23539
83 22527
84 24435
85 28616
86 20838
87 20450
88 23945
89 24184
90 23959
91 23396
92 22115
93 27420
94 27556
95 23114
96 26840
97 30181
98 31160
99 24057
100 26253
101 157559
102 151652
103 23938
104 30776
105 26687
106 19810
107 25983
108 24691
109 25251
110 33804
111 22614
112 18195
113 22940
114 24277
115 23970
116 21482
117 24731
118 120710
119 31968
120 634
121 489
122 558
123 528
124 501
125 542
126 578
127 473
128 646
129 528
130 534
131 572
132 606
133 568
134 561
135 575
136 469
137 538
138 732
139 482
140 572
141 466
142 658
143 549
144 613
145 575
146 726
147 572
148 556
149 506
150 614
151 565
152 639
153 621
154 677
155 583
156 617
157 460
158 599
159 444
160 517
161 471
162 508
163 701
164 624
165 563
166 695
167 689
168 643
169 572
170 418
171 643
172 489
173 530
174 635
175 569
176 606
177 489
178 861
179 618
180 608
181 553
182 491
183 589
184 603
185 733
186 455
187 425
188 513
189 700
190 769
191 615
192 595
193 496
194 529
195 561
196 529
197 423
198 554
199 500
原因是什么呢?
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 关闭逃逸分析,再运行刚才的示例观察结果
方法内联
(Inlining)
private static int square(final private static int square(final int i) {
return i * i;
}
System.out.println(square(9));
如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、 粘贴到调用者的位置:
System.out.println(9 * System.out.println(9 * 9);
还能够进行常量折叠(constant folding)的优化
System.out.println(System.out.println(81);
实验:
public class public class JIT2 {
// -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining (解锁隐藏参数)打印 inlining 信息
// -XX:CompileCommand=dontinline,*JIT2.square 禁止某个方法 inlining
// -XX:+PrintCompilation 打印编译信息
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\t%d\n", i, x, (end - start));
}
}
private static int square(final int i) {
return i * i;
}
}
字段优化
JMH 基准测试请参考:http://openjdk.java.net/projects/code-tools/jmh/
创建 maven 工程,添加依赖如下
<<dependency>
<groupId>org.openjdk.jmhgroupId>
<artifactId>jmh-coreartifactId>
<version>${jmh.version}version>
dependency>
<dependency>
<groupId>org.openjdk.jmhgroupId>
<artifactId>jmh-generator-annprocessartifactId>
<version>${jmh.version}version>
<scope>providedscope>
dependency>
编写基准测试代码:
package test;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.Random;import java.util.concurrent.ThreadLocalRandom;
@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 Samples Score Score error Units
t.Benchmark1.test1 thrpt 5 2420286.539 390747.467 ops/s
t.Benchmark1.test2 thrpt 5 2544313.594 91304.136 ops/s
t.Benchmark1.test3 thrpt 5 2469176.697 450570.647 ops/s
接下来禁用 doSum 方法内联
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
static void doSum(int x) {
sum += x;
}
测试结果如下:
Benchmark Mode Samples Score Score error Units
t.Benchmark1.test1 thrpt 5 296141.478 63649.220 ops/s
t.Benchmark1.test2 thrpt 5 371262.351 83890.984 ops/s
t.Benchmark1.test3 thrpt 5 368960.847 60163.391 ops/s
分析: 在刚才的示例中,doSum 方法是否内联会影响 elements 成员变量读取的优化: 如果 doSum 方法内联了,刚才的 test1 方法会被优化成下面的样子(伪代码):
@Benchmark public void test1@Benchmark public void test1() {
// elements.length 首次读取会缓存起来 -> int[] local
for (int i = 0; i < elements.length; i++) {
// 后续 999 次 求长度 <- local
sum += elements[i];
// 1000 次取下标 i 的元素 <- local
}
}
可以节省 1999 次 Field 读取操作 但如果 doSum 方法没有内联,则不会进行上面的优化
练习:在内联情况下将 elements 添加 volatile 修饰符,观察测试结果
6.2 反射优化
package cn.itcast.jvm.t3.reflect;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Reflect1 {
public static void foo() {
System.out.println("foo...");
}
public static void main(String[] args) throws Exception {
Method foo = Reflect1.class.getMethod("foo");
for (int i = 0; i <= 16; i++) {
System.out.printf("%d\t", i);
foo.invoke(null);
}
System.in.read();
}
}
foo.invoke
前面 0 ~ 15 次调用使用的是 MethodAccessor
的 NativeMethodAccessorImpl
实现
package sun.reflect;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import sun.reflect.misc.ReflectUtil;
class NativeMethodAccessorImpl extends MethodAccessorImpl {
private final Method method;
private DelegatingMethodAccessorImpl parent;
private int numInvocations;
NativeMethodAccessorImpl(Method method) {
this.method = method;
}
public Object invoke(Object target, Object[] args) throws IllegalArgumentException, InvocationTargetException {
// inflationThreshold 膨胀阈值,默认 15
if (++this.numInvocations > ReflectionFactory.inflationThreshold()
&& !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
// 使用 ASM 动态生成的新实现代替本地实现,速度较本地实现快 20 倍左右
MethodAccessorImpl generatedMethodAccessor =
(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(generatedMethodAccessor);
}
// 调用本地实现
return invoke0(this.method, target, args);
}
void setParent(DelegatingMethodAccessorImpl parent) {
this.parent = parent;
}
private static native Object invoke0(Method method, Object target, Object[] args);
}
当调用到第 16 次(从0开始算)时,会采用运行时生成的类代替掉最初的实现,可以通过 debug 得到 类名为 sun.reflect.GeneratedMethodAccessor1
可以使用阿里的 arthas 工具:
java -jar arthas-bootjava -jar arthas-boot.jar
[INFO] arthas-boot version: 3.1.1
[INFO] Found existing java process, please choose one and hit RETURN.
* [1]: 13065 cn.itcast.jvm.t3.reflect.Reflect1
选择 1 回车表示分析该进程
1 [INFO] arthas home: /root/.arthas/lib/3.1.1/arthas [INFO] Try to attach process 13065 [INFO] Attach process 13065 success.
[INFO] arthas-client connect 127.0.0.1 3658
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-' | .-. || '--'.' | | | .--. || .-. |`. `-. | | | || |\ \ | | | | | || | | |.-' `--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'
|
wiki https://alibaba.github.io/arthas
tutorials https://alibaba.github.io/arthas/arthas-tutorials
version 3.1.1
pid 13065
time 2019-06-10 12:23:54
再输入【jad + 类名】来进行反编译
$ jad sun.reflect.GeneratedMethodAccessor1
ClassLoader:
+-sun.reflect.DelegatingClassLoader@15db9742
+-sun.misc.Launcher$AppClassLoader@4e0e2f2a
+-sun.misc.Launcher$ExtClassLoader@2fdb006e
Location:
/*
* Decompiled with CFR 0_132.
*
* Could not load the following classes:
* cn.itcast.jvm.t3.reflect.Reflect1
*/
package sun.reflect;
import cn.itcast.jvm.t3.reflect.Reflect1;
import java.lang.reflect.InvocationTargetException;
import sun.reflect.MethodAccessorImpl;
public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
/*
* Loose catch block * Enabled aggressive block sorting
* Enabled unnecessary exception pruning
* Enabled aggressive exception aggregation
* Lifted jumps to return sites
*/
public Object invoke(Object object, Object[] arrobject) throws InvocationTargetException {
// 比较奇葩的做法,如果有参数,那么抛非法参数异常
block4 : {
if (arrobject == null || arrobject.length == 0) break block4;
throw new IllegalArgumentException();
} try {
// 可以看到,已经是直接调用了
Reflect1.foo();
// 因为没有返回值
return null;
} catch (Throwable throwable) {
throw new InvocationTargetException(throwable);
} catch (ClassCastException | NullPointerException runtimeException) {
throw new IllegalArgumentException(Object.super.toString());
}
}
}
Affect(row-cnt:1) cost in 1540ms.
注意
通过查看 ReflectionFactory源码可知
- sun.reflect.noInflation 可以用来禁用膨胀(直接生成 GeneratedMethodAccessor1,但首 次生成比较耗时,如果仅反射调用一次,不划算)
- sun.reflect.inflationThreshold 可以修改膨胀阈值
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!