JVM 加载class文件后,已经将class文件中的常量信息、类信息、方法代码等放入方法区中了,但JVM具体是如何调用class中的方法的呢,本章节就具体来讲解下JVM的执行机制。
JVM通过执行引擎来完成字节码的执行,在执行过程中JVM采用的是自己的一套指令系统,每个线程在创建后,都会产生一个程序计数器(pc)和栈(Stack),其中程序计数器中存放了下一条将要执行的指令,Stack中存放Stack Frame,表示的为当前正在执行的方法,每个方法的执行都会产生Stack Frame,Stack Frame中存放了传递给方法的参数、方法内的局部变量以及操作数栈,操作数栈用于存放指令运算的中间结果,指令负责从操作数栈中弹出参与运算的操作数,指令执行完毕后再将计算结果压回到操作数栈,当方法执行完毕后则从Stack中弹出,继续其他方法的执行。
在执行方法时JVM提供了invokestatic、invokevirtual、invokeinterface和invokespecial四种指令来执行,对应的在Java程序中就是如下四种方式,结合实际例子来讲述下JVM的具体执行机制。
调用类的static方法
代码片段示例如下:
public class A{ public static voidexecute(String message){ // 执行代码 } }
-
public class B{ public static void main(String[] args){ A.execute(“Hello World”); } }
编译后B中的代码转变为了如下字节码(通过调用javap –c B查看):
public static void main(java.lang.String[]); Code: 0: ldc #16; //String Hello World 2: invokestatic #18; //Method sample/A.execute:(Ljava/lang/String;)V 5: return
当main线程启动后,JVM为此线程创建一个PC和Stack,执行ldc #16;//String Hello World,并同时将invokestatic放入PC,接着将Hello World的引用压入Stack的参数变量中,继续执行invokestatic指令,该指令后跟随的//Method部分的内容称为<method-spec>内容,此内容的规范为:包名/类名.方法名:(参数类型)返回值类型,invokestatic指令根据此method-spec找到此Class的方法,根据method-spec中的参数信息找到参数的个数,从stack frame中弹出这些参数,然后创建一个新的stack frame,将参数信息压入,然后重复上述过程,完成整个方法的执行。(V型知识库 www.vxzsk.com整理改编)
l 调用对象实例的方法
代码片段示例如下:
public class A{ public void execute(String message){ // 执行 } }
-
public class B{ public static void main(String[] args){ A a=new A(); a.execute(“Hello World”); } }
对于实例的调用,不同点在于调用a时采用的指令为invokevirtual,在执行A a=new A()时,JVM会负责将a对象的引用压入栈中,当执行invokevirtual指令时,JVM从栈中将a对象的引用弹出,找到a对象引用所属的Class,通过此Class找到符合method-spec中描述的方法,如此Class中没有此方法,则寻找父类,在找到方法后弹出方法所需的参数,创建stack frame,压入参数,继续并完成方法的执行。(V型知识库 www.vxzsk.com整理改编)
在调用实例的方法时,还有一种常用的方式为将属性定义为接口来进行调用,例如:
public interface IA{ public void execute(String message); }
-
public class A implements IA{ public void execute(String message){ // 执行 } }
-
public class B{ public static void main(String[] args){ IA a=new A(); a.execute(“Hello World”); } }
这种方式在JVM的执行中其实和invokevirtual调用是不一样的,一方面是指令改为了采用invokeinterface,<method-spec>改为了包名/接口名.方法名<参数类型>返回值类型;另一方面是调用时寻找方法的方式不一样,在invokevirtual方式时,所调用的对象的方法其在方法表中的位置是固定的,在实现时可直接采用固定偏移的方式来找到方法,而对于接口来说则不行,毕竟每个实现接口的类的方法在方法表中的位置并不一定是一样的,因此接口调用时JVM必须每次都去搜索查找类的方法表,当然,这其实只会造成很小的性能影响,相对于接口编程带来的好处而言,基本可以忽略。
JVM对于初始化对象(Java构造器的方法为:<init>)以及调用对象实例中的私有方法时,采用的是invokespecial指令,其不同点在于其查找方法时不需要根据运行时的对象引用查找类的方法表的方式,而是直接在编译期就已经指定了,因此相对而言invokespecial指令的执行是快过invokevritual和invokeinterface的。
l 反射执行
反射机制是Java的亮点之一,基于反射可动态调用某对象实例中对应的方法、访问查看对象的属性等,而无需在编写代码时就确定需要创建的对象,这使得Java可以实现很灵活的实现对象的调用,例如MVC框架中通常要调用实现类中的execute方法,但框架在编写时是无法知道实现类,在Java中则可以通过反射机制直接去调用应用实现类中的execute方法,代码示例如下:
Class actionClass=Class.forName(外部实现类); Method method=actionClass.getMethod(“execute”,null); Object action=actionClass.newInstance(); method.invoke(action,null);
这种方式对于框架之类的代码而言非常的重要,但反射和直接new实例,然后调用方法的最大不同之处在于实创建的过程、方法调用的过程是动态的,这也就使得采用反射生成执行方法调用的代码并不像直接调用实例对象代码样,在编译后就可直接生成对对象方法调用的字节码,而是只能生成调用jvm反射实现的字节码了,各种jvm的反射实现会有些不同,在此来阐述下Sun JDK中反射的实现。
要实现动态的调用,最明显的方法就是动态的生成字节码,加载到JVM中并执行,Sun JDK也是采用的这种方法,来看看在Sun JDK中以上反射代码的关键执行过程。
l Class actionClass=Class.forName(外部实现类);
调用本地方法,使用调用者所在的ClassLoader来加载创建出Class对象;
l Method method=actionClass.getMethod(“execute”,null);
校验此Class是否为public类型的,以确定类的执行权限,如不是public类型的,则直接抛出SecurityException;
调用privateGetDeclaredMethods来获取到此Class中所有的方法,在privateGetDeclaredMethods对此Class中所有的方法的集合做了缓存,在第一次时会调用本地方法去获取;
扫描方法集合列表中是否有相同方法名以及参数类型的方法,如有则复制生成一个新的Method对象返回;
如没有则继续扫描父类、父接口中是否有此方法,如仍然没找到方法则抛出
NoSuchMethodException;
l 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,这步和标准的方法调用没有任何区别。
l method.invoke(action,null);
这步执行的过程和上一步基本类似,只是在生成字节码时生成的方法改为了invoke,其调用的目标改为了传入的对象的方法,同时生成的类名改为了:sun/reflect/GeneratedMethodAccessor。
按照上面的阐述,执行一段反射执行的代码后在debug中查看Method对象中的MethodAccessor对象引用(参数为-Dsun.reflect.noInflation=true,否则默认要执行15次反射调用后才会是动态生成字节码):
Sun JDK采用以上方式提供了反射的实现,大大提升了代码编写的灵活性,但也可以看出,由于其整个过程比直接已经编译成字节码的调用复杂很多,因此性能比直接执行的慢了一些,但其实也慢不到太多,Sun JDK中的反射执行的性能也是随着JDK版本的提升越来越好,到了JDK 6后差距已经非常小了,但要注意的是getMethod是非常耗性能的,一方面是权限的校验,另外一方面所有方法的扫描以及Method对象的复制,因此在使用反射调用多的系统中应缓存getMethod返回的Method对象,至于method.invoke,其性能仅比直接调用低一点点,差不多是纳秒级的,一段典型的对比直接执行、反射执行性能的程序:
Object object=new Object(); int loop=1000000; long beginTime=System.currentTimeMillis(); for (int i = 0; i < loop; i++) { object.toString(); } long endTime=System.currentTimeMillis(); System.out.println("直接调用消耗的时间为:"+(endTime-beginTime)+"毫秒"); beginTime=System.currentTimeMillis(); for (int i = 0; i < loop; i++) { Method method=Object.class.getMethod("toString", new Class<?>[]{}); method.invoke(object, new Object[]{}); } endTime=System.currentTimeMillis(); System.out.println("不缓存Method,反射调用消耗的时间为:"+(endTime-beginTime)+"毫秒"); beginTime=System.currentTimeMillis(); Method method=Object.class.getMethod("toString", new Class<?>[]{}); for (int i = 0; i < loop; i++) { method.invoke(object, new Object[]{}); } endTime=System.currentTimeMillis(); System.out.println("缓存Method,反射调用消耗的时间为:"+(endTime-beginTime)+"毫秒");
执行后显示的性能为(执行环境: Intel Dual Core 2.5G, windows XP, JVM Heap 128M, Sun JDK 1.6.0_12):
直接调用消耗的时间为:313毫秒
不缓存Method,反射调用消耗的时间为:1375毫秒
缓存Method,反射调用消耗的时间为:375毫秒
JVM在执行字节码时有三种方式:解释、即时编译和自适应编译,在1999年后Sun JDK就已采用自适应编译的方式,即俗称的Hotspot JVM,分别来看看这三种方式在执行时的不同。
解释
解释执行方式,也就是每次都由JVM来解释字节码,进行执行的方式,无疑这种方式会使得性能较低,毕竟每次执行都需要进行解释,然后调用机器指令完成执行。
即时编译
即时编译方式,也就是每次执行字节码时,JVM都将其首先转为机器码,然后进行执行,固然这种方式性能较高,但每次转化为机器码,也使得系统执行会受到不小的影响。
自适应编译
自适应编译方式,简称Hotspot方式,它是Sun JDK保证Java程序高性能执行的基础,Hotspot方式的特点为:在运行期根据代码的执行频率来决定是否要编译为机器码,如达到了执行次数的条件,那么就编译成机器码,这也就使得对于那些经常被执行的代码而言,执行的性能是非常高的,并且做到尽可能少的影响整体应用性能,这个执行次数的条件可通过-XX:CompileThreshold=10000来设置,默认情况下为10000次,也可通过-XX:+PrintCompilation参数来查看被编译成机器码的方法。
对于get/set类型的方法,Sun JDK还提供了一个可供选择的优化参数:-XX:+UseFastAccessorMethods,在指定了这个参数后,JDK会将所有的get/set方法都转为本地代码。
感觉本站内容不错,读后有收获?小额赞助,鼓励网站分享出更好的教程