读书人

《分布式JAVA施用 基础与实践》 第三章

发布时间: 2012-12-26 14:39:29 作者: rapoo

《分布式JAVA应用 基础与实践》 第三章 3.1 Java代码的执行机制(二)

3.1.3? 类执行机制

在完成将class文件信息加载到JVM并产生Class对象后,就可执行Class对象的静态方法或实例化对象进行调用了。在源码编译阶段将源码编译为JVM字节码,JVM字节码是一种中间代码的方式,要由JVM在运行期对其进行解释并执行,这种方式称为字节码解释执行方式。

字节码解释执行

由于采用的为中间码的方式,JVM有一套自己的指令,对于面向对象的语言而言,最重要的是执行方法的指令,JVM采用了invokestatic、invokevirtual、invokeinterface和invokespecial四个指令来执行不同的方法调用。invokestatic对应的是调用static方法,invokevirtual对应的是调用对象实例的方法,invokeinterface对应的是调用接口的方法,invokespecial对应的是调用private方法和编译源码后生成的<init>方法,此方法为对象实例化时的初始化方法,例如下面一段代码:

public class Demo{?
??? public void execute(){?
??????? A.execute();?
??????? A a = new A();?
??????? a.bar();?
??????? IFoo b = new B();?
??????? b.bar();?
??? }?
}?

class? A{?
??? public static int execute(){?
??????? return 1+2;?
??? }?

??? public int bar(){?
??????? return 1+2;?
??? }?
}?

class B implements IFoo{?
??? public int bar(){?
??????? return 1+2;?
??? }?
}?

public interface IFoo{?
??? public int bar();?
}

通过javac编译上面的代码后,使用javap -c Demo查看其execute方法的字节码:

public void execute();?
? Code:?
?? 0:?? invokestatic??? #2; //Method A.execute:()I?
?? 3:?? pop?
?? 4:?? new #3; //class A?
?? 7:?? dup?
?? 8:?? invokespecial?? #4; //Method A." < init > ":()V?
?? 11:? astore_1?
?? 12:? aload_1?
?? 13:? invokevirtual?? #5; //Method A.bar:()I?
?? 16:? pop?
?? 17:? new #6; //class B?
?? 20:? dup?
?? 21:? invokespecial?? #7; //Method B." < init > ":()V?
?? 24:? astore_2?
?? 25:? aload_2?
?? 26:? invokeinterface #8,? 1; //InterfaceMethod IFoo.bar:()I?
?? 31:? pop?
?? 32:? return???

从以上例子可看到invokestatic、invokespecial、invokevirtual及invokeinterface四种指令对应调用方法的情况。

Sun JDK基于栈的体系结构来执行字节码,基于栈方式的好处为代码紧凑,体积小。

线程在创建后,都会产生程序计数器(PC)(或称为PC registers)和栈(Stack);PC存放了下一条要执行的指令在方法内的偏移量;栈中存放了栈帧(StackFrame),每个方法每次调用都会产生栈帧。栈帧主要分为局部变量区和操作数栈两部分,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果,栈帧中还会有一些杂用空间,例如指向方法已解析的常量池的引用、其他一些VM内部实现需要的数据等,结构如图3.5所示。

?

?

当point对象在后面的执行过程中未用到时,经过编译后,代码会变成类似下面的结构:

int? x = 1 ;?
int? y = 2 ;?
System.out.println(" point.x = "+x+" ;? point.y ="+y);??

?

之后基于此可以继续做冗余削除。

这种方式能带来的好处是,如果创建的对象并未用到其中的全部变量,则可以节省一定的内存。而对于代码执行而言,由于无须去找对象的引用,也会更快一些。

2. 栈上分配

在上面的例子中,如果p没有逃逸,那么C2会选择在栈上直接创建Point对象实例,而不是在JVM堆上。在栈上分配的好处一方面是更加快速,另一方面是回收时随着方法的结束,对象也被回收了,这也是栈上分配的概念。

3. 同步削除

同步削除是指如果发现同步的对象未逃逸,那也没有同步的必要了,在C2编译时会直接去掉同步。

例如有这么一段代码:

Point? point = new? Point(1,2);?
??? synchronized(point){?
??????? // do something?
}??

?

经过分析如果发现point未逃逸,在编译后,代码就会变成下面的结构:

Point? point = new? Point(1,2);?
// do something??

除了基于逃逸分析的这些外,C2还会基于其拥有的运行信息来做其他的优化,例如编译分支频率执行高的代码等。

运行后C1、C2编译出来的机器码如果不再符合优化条件,则会进行逆优化(deoptimization),也就是回到解释执行的方式,例如基于类层次分析编译的代码,当有新的相应的接口实现类加入时,就执行逆优化。

除了C1、C2外,还有一种较为特殊的编译为:OSR(On Stack Replace) 。OSR编译和C1、C2最主要的不同点在于OSR编译只替换循环代码体的入口,而C1、C2替换的是方法调用的入口,因此在OSR编译后会出现的现象是方法的整段代码被编译了,但只有在循环代码体部分才执行编译后的机器码,其他部分则仍然是解释执行方式。

默认情况下,Sun JDK根据机器配置来选择client或server模式,当机器配置CPU超过2核且内存超过2GB即默认为server模式,但在32位Windows机器上始终选择的都是client模式时,也可在启动时通过增加-client或-server来强制指定,在JDK 7中可能会引入多层编译的支持。多层编译是指在-server的模式下采用如下方式进行编译:

解释器不再收集运行状况信息,只用于启动并触发C1编译;

C1编译后生成带收集运行信息的代码;

C2编译,基于C1编译后代码收集的运行信息来进行激进优化,当激进优化的假设不成立时,再退回使用C1编译的代码。

从以上介绍来看,Sun JDK为提升程序执行的性能,在C1和C2上做了很多的努力,其他各种实现的JVM也在编译执行上做了很多的优化,例如在IBM J9、Oracle JRockit中做了内联、逃逸分析等 。Sun JDK之所以未选择在启动时即编译成机器码,有几方面的原因:

1)静态编译并不能根据程序的运行状况来优化执行的代码,C2这种方式是根据运行状况来进行动态编译的,例如分支判断、逃逸分析等,这些措施会对提升程序执行的性能会起到很大的帮助,在静态编译的情况下是无法实现的。给C2收集运行数据越长的时间,编译出来的代码会越优;

2)解释执行比编译执行更节省内存;

3)启动时解释执行的启动速度比编译再启动更快。

但程序在未编译期间解释执行方式会比较慢,因此需要取一个权衡值,在Sun JDK中主要依据方法上的两个计数器是否超过阈值,其中一个计数器为调用计数器,即方法被调用的次数;另一个计数器为回边计数器,即方法中循环执行部分代码的执行次数。下面将介绍两个计数器对应的阈值。

CompileThreshold

该值是指当方法被调用多少次后,就编译为机器码。在client模式下默认为1 500次,在server模式下默认为10 000次,可通过在启动时添加-XX:CompileThreshold=10 000来设置该值。

OnStackReplacePercentage

该值为用于计算是否触发OSR编译的阈值,默认情况下client模式时为933,server模式下为140,该值可通过在启动时添加-XX: OnStackReplacePercentage=140来设置,在client模式时,计算规则为CompileThreshold * (OnStackReplacePercentage/100),在server模式时,计算规则为(CompileThreshold * (OnStackReplacePercentage - InterpreterProfilePercentage))/100。InterpreterProfilePercentage的默认值为33,当方法上的回边计数器到达这个值时,即触发后台的OSR编译,并将方法上累积的调用计数器设置为CompileThreshold的值,同时将回边计数器设置为CompileThreshold/2的值,一方面是为了避免OSR编译频繁触发;另一方面是以便当方法被再次调用时即触发正常的编译,当累积的回边计数器的值再次达到该值时,先检查OSR编译是否完成。如果OSR编译完成,则在执行循环体的代码时进入编译后的代码;如果OSR编译未完成,则继续把当前回边计数器的累积值再减掉一些,从这些描述可看出,默认情况下对于回边的情况,server模式下只要回边次数达到10 700次,就会触发OSR编译。

用以下一段示例代码来模拟编译的触发。

public class Foo{?
??? public static void main(String[] args){?
??? Foo? foo = new? Foo();?
??????? for(int? i = 0 ;i < 10 ;i++){?
??????????? foo.bar();?
??????? }?
??? }?
??? public void bar(){?
??????? // some bar code?
??????? for(int? i = 0 ;i < 10700 ;i++){?
??????? bar2();?
??????? }?
??? }?
??? private void bar2(){?
??????? // bar2 method?
??? }?
}?

以上代码采用java -server方式执行,当main中第一次调用foo.bar时,bar方法上的调用计数器为1,回边计数器为0;当bar方法中的循环执行完毕时,bar方法的调用计数器仍然为1,回边计数器则为10 700,达到触发OSR编译的条件,于是触发OSR编译,并将bar方法的调用计数器设置为10 000,回边计数器设置为5 000。

当main中第二次调用foo.bar时,jdk发现bar方法的调用次数已超过compileThreshold,于是在后台执行JIT编译,并继续解释执行// some bar code,进入循环时,先检查OSR编译是否完成。如果完成,则执行编译后的代码,如果未编译完成,则继续解释执行。

当main中第三次调用foo.bar时,如果此时JIT编译已完成,则进入编译后的代码;如果编译未完成,则继续按照上面所说的方式执行。

由于Sun JDK的这个特性,在对Java代码进行性能测试时,要尤其注意是否事先做了足够次数的调用,以保证测试是公平的;对于高性能的程序而言,也应考虑在程序提供给用户访问前,自行进行一定的调用,以保证关键功能的性能。

反射执行

反射执行是Java的亮点之一,基于反射可动态调用某对象实例中对应的方法、访问查看对象的属性等,无需在编写代码时就确定要创建的对象。这使得Java可以很灵活地实现对象的调用,例如MVC框架中通常要调用实现类中的execute方法,但框架在编写时是无法知道实现类的。在Java中则可以通过反射机制直接去调用应用实现类中的execute方法,代码示例如下:

Class? actionClass =Class.forName(外部实现类);?
Method? method = actionClass .getMethod("execute",null);?
Object? action = actionClass .newInstance();?
method.invoke(action,null);

这种方式对于框架之类的代码而言非常重要,反射和直接创建对象实例,调用方法的最大不同在于创建的过程、方法调用的过程是动态的。这也使得采用反射生成执行方法调用的代码并不像直接调用实例对象代码,编译后就可直接生成对对象方法调用的字节码,而是只能生成调用JVM反射实现的字节码了。

要实现动态的调用,最直接的方法就是动态生成字节码,并加载到JVM中执行,Sun JDK采用的即为这种方法,来看看在Sun JDK中以上反射代码的关键执行过程。

??? Class? actionClass =Class.forName(外部实现类);
调用本地方法,使用调用者所在的ClassLoader来加载创建出的Class对象;
??? Method? method = actionClass .getMethod("execute",null);

校验Class是否为public类型,以确定类的执行权限,如不是public类型的,则直接抛出SecurityException。

调用privateGetDeclaredMethods来获取Class中的所有方法,在privateGetDeclaredMethods对Class中所有方法集合做了缓存,第一次会调用本地方法去获取。

扫描方法集合列表中是否有相同方法名及参数类型的方法,如果有,则复制生成一个新的Method对象返回;如果没有,则继续扫描父类、父接口中是否有该方法;如果仍然没找到方法,则抛出NoSuchMethodException,代码如下:

Object? action = actionClass .newInstance();?

校验Class是否为public类型,如果权限不足,则直接抛出SecurityException。

如果没有缓存的构造器对象,则调用本地方法获取构造器,并复制生成一个新的构造器对象,放入缓存;如果没有空构造器,则抛出InstantiationException。

校验构造器对象的权限。

执行构造器对象的newInstance方法。

判断构造器对象的newInstance方法是否有缓存的ConstructorAccessor对象,如果没有,则调用sun.reflect.ReflectionFactory生成新的ConstructorAccessor对象。

判断sun.reflect.ReflectionFactory是否需要调用本地代码,可通过sun.reflect.noInflation=true来设置为不调用本地代码。在不调用本地代码的情况下,可转交给MethodAccessorGenerator来处理。本地代码调用的情况在此不进行阐述。

MethodAccessorGenerator中的generate方法根据Java Class格式规范生成字节码,字节码中包括ConstructorAccessor对象需要的newInstance方法。该newInstance方法对应的指令为invokespecial,所需参数则从外部压入,生成的Constructor类的名字以sun/reflect/ GeneratedSerializationConstructorAccessor或sun/reflect/GeneratedConstructorAccessor开头,后面跟随一个累计创建对象的次数。

在生成字节码后将其加载到当前的ClassLoader中,并实例化,完成ConstructorAccessor对象的创建过程,并将此对象放入构造器对象的缓存中。

执行获取的constructorAccessor.newInstance,这步和标准的方法调用没有任何区别。

method.invoke(action,null);

这步的执行过程和上一步基本类似,只是在生成字节码时方法改为了invoke,其调用目标改为了传入对象的方法,同时类名改为了:sun/reflect/GeneratedMethodAccessor。

综上所述,执行一段反射执行的代码后,在debug里查看Method对象中的MethodAccessor对象引用(参数为-Dsun.reflect.noInflation=true,否则要默认执行15次反射调用后才能动态生成字节码),如图3.6所示:

?(点击查看大图)图3.6? 反射执行代码示例

Sun JDK采用以上方式提供反射的实现,提升代码编写的灵活性,但也可以看出,其整个过程比直接编译成字节码的调用复杂很多,因此性能比直接执行的慢一些。Sun JDK中反射执行的性能会随着JDK版本的提升越来越好,到JDK 6后差距就不大了,但要注意的是,getMethod相对比较耗性能,一方面是权限的校验,另一方面是所有方法的扫描及Method对象的复制,因此在使用反射调用多的系统中应缓存getMethod返回的Method对象,而method.invoke的性能则仅比直接调用低一点。一段对比直接执行、反射执行性能的程序如下所示:

??? // Server OSR编译阈值:10700?
??? 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});?
??????????? Demo demo=new Demo();?
??????????? // 保证反射能生成字节码及相关的测试代码能够被JIT编译?
??????????? for (int i = 0; i < 20; i++) {?
??????????????? demo.testDirectCall();?
??????????????? demo.testCacheMethodCall();?
??????????????? demo.testNoCacheMethodCall();?
??????????? }?
??????????? long beginTime=System.currentTimeMillis();?
??????????? demo.testDirectCall();?
??????????? long endTime=System.currentTimeMillis();?
??????????? System.out.println("直接调用消耗的时间为:"+
??? (endTime-beginTime)+"毫秒");?
??????????? beginTime=System.currentTimeMillis();?
??????????? demo.testNoCacheMethodCall();?
??????????? endTime=System.currentTimeMillis();?
??????????? System.out.println("不缓存Method,反射调用消耗的时间为:?
??? "+(endTime-beginTime)+"毫秒");?
??????????? beginTime=System.currentTimeMillis();?
??????????? demo.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"});?
??????????? }?
??? }?
??? public class ForReflection {?
??????????? private Map<String, String> caches=new
???? HashMap<String, String>();?
??????????? public void execute(String message){?
??????????????? String b=this.toString()+message;?
??????????????? caches.put(b, message);?
??????????? }?
??????? }

执行后显示的性能如下(执行环境: Intel Duo CPU E8400 3G, windows 7, Sun JDK 1.6.0_18,启动参数为-server -Xms128M -Xmx128M):

直接调用消耗的时间为5毫秒;

不缓存Method,反射调用消耗的时间为11毫秒;

缓存Method,反射调用消耗的时间为6毫秒。

在启动参数上增加-Xint来禁止JIT编译,执行上面代码,结果为:

直接调用消耗的时间为133毫秒;

不缓存Method,反射调用消耗的时间为215毫秒;

缓存Method,反射调用消耗的时间为150毫秒。

对比这段测试结果也可看出,C2编译后代码的执行速度得到了大幅提升。

?

?

读书人网 >编程

热点推荐