类执行机制
类执行机制
在完成将class文件信息加载到JVM并产生Class对象后,就可执行Class对象的静态方法或实例化对象进行调用了。在源码编译阶段将源码编译为JVM字节码,JVM字节码是一种中间代码的方式,要由JVM在运行期对其进行解释并执行,这种方式称为字节码解释执行方式。
字节码解释执行
由于采用的为中间码的方式,JVM有一套自己的指令,对于面向对象的语言而言,最重要的是执行方法的指令,JVM采用了invokestatic、invokevirtual、invokeinterface和invokespecial四个指令来执行不同的方法调用。invokestatic对应的是调用static方法,invokevirtual对应的是调用对象实例的方法,invokeinterface对应的是调用接口的方法,invokespecial对应的是调用private方法和编译源码后生成的方法,此方法为对象实例化时的初始化方法,例如下面一段代码:
- 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所示。
PC寄存器--局部变量区,操作数栈(Stack Frame栈帧)
下面来看一个方法执行时过程的例子,代码如下:
- public class Demo(){
- public static void foo(){
- int a=1;
- int b=2;
- int c=(a+b) * 5;
- }
- }
编译以上代码后,foo方法对应的字节码为以及相应的解释如下:
- public static void foo();
- Code:
- 0: iconst_1 //将类型为int、值为1的常量放入操作数栈;
- 1: istore_0 //将操作数栈中栈顶的值弹出放入局部变量区;
- 2: iconst_2 //将类型为int、值为2的常量放入操作数栈;
- 3: istore_1 //将操作数栈中栈顶的值弹出放入局部变量区;
- 4: iload_0 //装载局部变量区中的第一个值到操作数栈;
- 5: iload_1 //装载局部变量区中的第二个值到操作数栈;
- 6: iadd //执行int类型的add指令,并将计算出的结果放入操作数栈;
- 7: iconst_5 //将类型为int、值为5的常量放入操作数栈;
- 8: imul //执行int类型的mul指令,并将计算出的结果放入操作数栈;
- 9: istore_2 //将操作数栈中栈顶的值弹出放入局部变量区;
- 10: return // 返回
1.指令解释执行
2.栈顶缓存
3.部分栈帧共享
编译执行
解释执行的效率较低,为提升代码的执行性能,Sun JDK提供将字节码编译为机器码的支持,编译在运行时进行,通常称为JIT编译器。Sun JDK在执行过程中对执行频率高的代码进行编译,对执行不频繁的代码则继续采用解释的方式,因此Sun JDK又称为Hotspot VM,在编译上Sun JDK提供了两种模式:client compiler(-client)和server compiler(-server)。
client compiler又称为C1 ,较为轻量级,只做少量性能开销比高的优化,它占用内存较少,适合于桌面交互式应用。在寄存器分配策略上,JDK 6以后采用的为线性扫描寄存器分配算法 ,在其他方面的优化主要有:方法内联、去虚拟化、冗余削除等。
1.方法内联
2.去虚拟化
3.冗余消除
反射执行
反射执行是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所示: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编译后代码的执行速度得到了大幅提升。