Java类执行是在完成将class文件信息加载到jvm中并且产生Class对象之后。字节码只是一种中间代码形式,在实际运行时要由jvm解释执行。
字节码分析
因为Java采取的是中间码的形式,应用程序可以在不同的操作系统上由JVM解释执行,因此具有良好的扩展性。那么JVM肯定会有一套自己的执行方法的指令。在JVM中:
invokestatic调用static方法
invokevirtual调用对象实例方法
invokeinterface调用接口方法
invokespecial调用private方法和init方法
每个方法每次调用都会产生栈帧,栈帧包含局部变量区和操作数栈。局部变量区存放方法的局部变量和参数、操作数栈存放中间结果。下面是一段代码以及对应的字节码信息
代码
public static int foo() {
int a = 1;
int b = 2;
int c = (a + b) * 5;
return c;
}
字节码
public static void noFoo();
descriptor: ()V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=0
0: iconst_1 // 将类型为int,值为1的常量放到操作数栈
1: istore_0 // 将操作数栈顶的值弹出放到局部变量区的位置1
2: iconst_2 // 将类型为int,值为2的常量放到操作数栈
3: istore_1 // 将操作数栈顶的值弹出放到局部变量区的位置2
4: iload_0 // 装载局部变量区位置1的变量到操作数栈
5: iload_1 // 装载局部变量区位置2的变量到操作数栈
6: iadd // 执行int类型的add操作,并将计算结果放到操作数栈
7: iconst_5 // 将类型为int,值为5的常量放到操作数栈
8: imul // 执行int类型的mul操作,并将计算结果放到操作数栈
9: istore_2 // 将操作数栈顶的值弹出放到局部变量区的位置3
10: return // 返回
LineNumberTable:
line 29: 0
line 30: 2
line 31: 4
line 32: 10
解释执行
指令解释执行有switch-threading、token-threading、direct-threading、subroutine-threading、inline-threading等分派方式
Sun JDK主要在解释器上主要采用token-threading方式,另外还有栈顶缓存以及部分栈帧共享等优化手段。
栈顶缓存:即将本来位于操作数栈顶的值直接缓存在寄存器上。
部分栈帧共享:后一方法可以将前一方法的操作数栈作为当前方法的局部变量。
编译执行
很有名的JIT编译器,主要为了提高解释执行的效率。编译上分为Client和Server两种,即C1和C2,在启动jvm时可以通过参数-client 或者-server启动。
编译时优化手段有方法内联、去虚拟化、冗余剔除。
C1:
方法内联:多个方法调用时,通常要参数传递,返回值传递以及跳转等。所以可以直接将被调用方法的代码放在调用代码块中。 注:35字节 & -XX:PrintInlining参数
去虚拟化:装载class文件后,进行类层次分析,如果发现类中的方法只有一个实现类,那么调用此方法的地方也进行方法内联。
冗余剔除:个人认为有点像语法糖那种方式的优化。如果方法中的代码块肯定不会执行,那么这段代码在编译时就相当于没有。
boolean debug = false;
if (debug) {
// debug log
}
C2:
比较重要的是逃逸分析。所谓逃逸分析就是比如说方法中的一个变量不会被外部所读取,那么这个变量就不是逃逸的。编译时的优化手段有标量替换、栈上分配和同步削除。
标量替换:用标量替换聚合量,比如说仅仅为了打印一个对象中的成员变量值,而new了一个对象,那么就用一个局部的标量来替换这个对象。好处就是如果创建的对象并没有用到其中的全部变量,则节省一定的内存。对于代码块而言不会去寻找对象引用,效率更高。
栈上分配:如果变量没有逃逸,那么直接在栈帧上分配,而不是Java堆。更加快速,回收更方便。
同步削除:如果同步的对象不是逃逸的,那么会自动去掉同步,毕竟同步会造成开销。
注:逆优化:运行后C1和C2编译出来的机器码不再符合优化条件,则会进行逆优化,也就是回到解释执行的方式。
除了C1、C2还有OSR编译方法,比较特殊。
OSR:
OSR编译方式只替换循环代码体的入口,而C1、C2替换的是方法调用的入口。也就是说方法的整段代码都被编译,但是直有循环部分才执行编译 的机器码,其他部分还是解释执行方式。
程序在未编译期间解释执行会比较慢,Sun JDK主要依据方法上的两个计数器是否超过阀值,来决定是否编译。一个计数器为调用计数器,方法被调用一次,会+1,另一个计数器为回边计数器,即方法中循环执行部分代码的执行次数。
CompileThreshold:调用计数器阀值,调用计数器超过此阀值会编译为机器码
通过-XX:CompileThreshold=10000设置。
OnStackReplacePercentage:用于计算出发OSR编译的阀值
OSR编译阀值计算方法:
client模式下规则为:CompileThreshold * (OnStackReplacePercentage / 10)
server模式下规则为:(CompileThreshold*(OnStackReplacePercentage-IntepreterProfilePercentage)) / 100
当方法上回边计数器达到这些规则计算出来的阀值时,会OSR编译成机器码。并且将调用计数器设置为CompileThreshold的值,回边计数器的值设置为CompileThreshold/2,这样的目的是为了OSR被频繁触发,同时方法被再次调用时可以触发正常编译(C1、C2编译),当累计的回边计数器的值再次达到阀值时,如果OSR编译完成,那么执行编译的代码,如果OSR编译未完成,那么就会将回边计数器的值继续减掉一些。
下面是模拟触发OSR编译的代码:
OSRTest.java
package excution;
import java.lang.reflect.Method;
/**
* 测试结果如下:
* ①java -server -Xms128M -Xmx128M OSRTest
* 直接调用消耗的时间为:5毫秒
* 不缓存Method,反射调用消耗的时间为:5毫秒
* 缓存Method,反射调用消耗的时间为:4毫秒
*
* ②java -server -Xms128M -Xmx128M -Xint OSRTest
* 直接调用消耗的时间为:246毫秒
* 不缓存Method,反射调用消耗的时间为:416毫秒
* 缓存Method,反射调用消耗的时间为:277毫秒
*/
public class OSRTest {
private static final int WARMUP_COUNT = 10700;
private ForReflection testClass = new ForReflection();
private static Method method = null;
public static void main(String[] args) throws Exception {
method = ForReflection.class.getMethod("execute", new Class<?>[]{String.class});
OSRTest osrTest = new OSRTest();
for (int i = 0;i < 20;i++) {
osrTest.testDirectCall();
osrTest.testCacheMethodCall();
osrTest.testNoCacheMethodCall();
}
long beginTime = System.currentTimeMillis();
osrTest.testDirectCall();
long endTime = System.currentTimeMillis();
System.out.println("直接调用消耗的时间为:" + (endTime-beginTime) + "毫秒");
beginTime = System.currentTimeMillis();
osrTest.testNoCacheMethodCall();
endTime = System.currentTimeMillis();
System.out.println("不缓存Method,反射调用消耗的时间为:" + (endTime-beginTime) + "毫秒");
beginTime = System.currentTimeMillis();
osrTest.testCacheMethodCall();
endTime = System.currentTimeMillis();
System.out.println("缓存Method,反射调用消耗的时间为:" + (endTime-beginTime) + "毫秒");
}
public void testDirectCall() {
for (int i = 0;i < WARMUP_COUNT;i++) {
testClass.execute("hello");
}
}
public void testCacheMethodCall() throws Exception {
for (int i = 0;i < WARMUP_COUNT;i++) {
method.invoke(testClass, new Object[]{"hello"});
}
}
public void testNoCacheMethodCall() throws Exception {
for (int i = 0;i < WARMUP_COUNT;i++) {
Method testMethod = ForReflection.class.getMethod("execute", new Class<?>[]{String.class});
testMethod.invoke(testClass, new Object[]{"hello"});
}
}
}
ForReflection.java
package excution;
import java.util.HashMap;
import java.util.Map;
public class ForReflection {
private Map<String, String> caches = new HashMap<>();
public void execute(String message) {
String b = this.toString() + message;
caches.put(b, message);
}
}
通过代码执行结果可以看出反射执行虽然很动态灵活,但是会影响效率。所以对于同一个方法需要很多此的反射执行,可以将该Method缓存起来。这样会提高效率。
此博客为读书笔记
本文深入解析Java类执行机制,涵盖字节码分析、指令解释执行的不同分派方式、栈顶缓存及部分栈帧共享优化手段等内容。同时,详细介绍了JIT编译器的工作原理及其优化手段,包括方法内联、去虚拟化、冗余剔除等。

3万+

被折叠的 条评论
为什么被折叠?



