Java虚拟机执行引擎知识总结
执行引擎
也只有几个概念,JVM方法调用和执行的基础数据结构是栈帧,是内存区域中虚拟机栈中的栈元素,每一个方法的执行就对应着一个栈帧在虚拟机栈中出栈入栈的过程.
栈帧:则是包含有局部变量表,操作数栈,动态连接,方法返回地址,附加信息.
1局部变量表:
存储单位是slot,一个slot占据32位,对于64位的数据类型,则是分配连续两个slot空间.而对于一个非静态方法而言,有一个隐藏参数,为this,而在局部变量表中的变量存储顺序则是
this->方法参数->方法体内的变量(slot可以重用,超出作用域即可复用.)方法在编译完成后,其所需的空间已经确定.
(这里也是需要注意的一个地方,变量的作用域常常会覆盖整个方法,即使变量已经不再使用,但只要还在作用域内,其slot空间就无法给其他变量使用,因此,最好是在需要使用到变量时,定义在合理的作用域范围内.)
2操作数栈:
在操作数栈中需要注意,其数据类型必须与字节码指令的序列严格匹配.
3动态连接:稍后详解
4方法返回地址:
方法有两种退出方式,正常退出,异常退出,当正常退出后,会恢复上层方法的局部变量表,操作数栈,并把方法返回结果压入调用者的操作数栈.
方法调用
方法调用阶段的唯一目的是,确定调用方法的版本究竟是哪一个.
在Java虚拟机中提供了5条方法调用的相关指令:
invokestatic:调用静态方法
invokespecial:调用实例构造器方法,私有方法,父类方法
invokevirtual:调用所有的虚方法
invokeinterface:调用所有的接口方法
invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法.
虚方法是非虚方法的补集,什么是非虚方法呢?能够在编译器就确定将要调用的究竟是哪个方法,进而将该方法的符号引用转换为相应的直接引用的方法就被称作非虚方法.
我们知道在类加载时,在相应的类信息中,存有对应方法的相关信息,常量池中存有相关直接引用.在类加载的解析阶段,即会将这部分的符号引用转换为直接引用.
那么什么方法才满足这种条件呢?
能够被invokespecial和invokestatic指令调用的方法,都是可以在编译器确定的方法,即静态方法,私有方法,父类方法(super.),实例构造器.
在final方法是个特殊点,虽然final方法的执行为invokevirtual,但它依然属于非虚方法,不难理解,final方法不能够被重写.
方法分派(dispatch)
1静态分派
对于代码
Humanman=newMan();
其中Human被称为变量的静态类型,也叫外观类型,而Man则是变量的实际类型.而一个变量的静态类型,在声明时即已经确定,仅仅在使用时才能够临时转换静态类型,但变量本身的静态类型并不会改变,实际类型的变化只有在运行期才能确定.
//实际类型变化 Humanman=newMan(); man=newWoman();//静态类型的变化 method((Man)man); method((Woman)man);
而当我们在重载方法时,向方法中传入的参数类型,即是静态类型.因此重载是一种可以在编译期就被确定执行方法版本的行为.
2动态分派
动态分派与重写息息相关.
staticclassHuman{voidsayHello(){ System.out.println("humansayhello"); } }staticclassManextendsHuman{@Override voidsayHello(){ System.out.println("mansayhello"); } }voidsayHello(Humanman){ man.sayHello(); }publicstaticvoidmain(String[]args){ Humanman=newMan(); Humanhuman=newHuman();newMain().sayHello(man);newMain().sayHello(human); }//out: mansayhello humansayhello
结果不必多做解释,而现在的问题在于,虚拟机如何知道,究竟调用的是哪个方法?
0:new#3//classMain$Man 3:dup4:invokespecial#4//MethodMain$Man."":()V7:astore_18:new#5//classMain$Human 11:dup12:invokespecial#6//MethodMain$Human." ":()V15:astore_216:new#7//classMain19:dup20:invokespecial#8//Method" ":()V23:aload_124:invokevirtual#9//MethodsayHello:(LMain$Human;)V 27:new#7//classMain30:dup31:invokespecial#8//Method" ":()V34:aload_235:invokevirtual#9//MethodsayHello:(LMain$Human;)V 38:return
其中主要关注几个方法的执行点,invokespecial不用多说,之前提到过,是执行构造器方法时的指令
而invokevirtual则正是执行main.sayHello(),方法的指令,指令的运行时解析过程大致如下:
而其中的关键点就在于,取到的是对象的实际类型.
1找到操作数栈顶的第一个元素的所指对象的实际类型,记做C
2如果在C中找到与描述符和简单名称都相符的方法,进行访问校验,如果可以则返回方法的直接引用,否则抛出IllegalAccessError异常
3否则按照继承关系从下向上对C的各个父类进行第二步的搜索验证过程.
4如果始终找不到,抛出异常.
动态类型语言
这也是要提到的关于invokedynamic指令的主要目的。
动态类型语言的概念是:意思就是类型的检查是在运行时做的而非编译期。
而Java本身则是静态类型语言,这一点又在哪里能够体现呢?
obj.println("language");
如果处在java环境中,且obj的静态语言类型是java.io.PrintStream,那么obj本身的实际类型也必须是PrintStream的子类才行,哪怕本身存在println方法也不可以,但同样的问题放在javascript中就不同了,只要实际类型中存在println方法,执行就不会有任何问题.
这点就是因为,java在编译时已经将其完整的符号引用生成出来,如果注意到的话,会发现无论是动态分派还是静态分派,在编译的指令中都是已经精确到相应类的某一个方法中了,如此,自然只能够在有限的范围内略做调整,如果超出了当前类的范围,就无法调用了.
jvm虚拟机并不仅仅是java语言的虚拟机,那么如何为动态类型语言提供支持就是一个问题了,并且在目前java8中的lamda表达式中也应用的是invokedynamic指令.
MethodHandle
而与之相关的jar包则是java.lang.invoke,相关的类则是MethodHandle.
在这里我也并不想再谈MethodHandle的使用方法,网上资料实在不少.
需要提到的是,它的功能和java的反射略有相似,通过方法名,class,就可以调用相应的方法.但它比起反射要轻量级;且Reflection是在模拟Java代码的调用,MethodHandle是在模仿字节码层面的调用.
这个方法不失为是在动态调用中除了反射之外的另一种选择.
基于栈解释器的执行过程
其实本文更像是在前一篇博客中java内存区域中的虚拟机栈的一种补充说明.
而真实的执行流程,我想通过下文的代码来看:
publicintadd(){inta=100;intb=200;intc=300;return(a+b)*c; } --javap-verboseMainpublicintadd();//返回类型为intdescriptor:()Iflags:ACC_PUBLICCode://需要深度为2的操作数栈,4个slot的局部变量空间,有一个参数为this stack=2,locals=4,args_size=1 //将100推入操作数栈顶,栈:1000:bipush100 //将栈顶的数据出栈并存储到局部变量表的第一个slot中(从0开始) //此时:栈:-局部变量表:slot11002:istore_1//与上面类似,重复过程3:sipush2006:istore_27:sipush300 //此时:栈:-局部变量表:slot1100slot2200slot330010:istore_3//将局部变量表slot1的值复制到栈顶11:iload_1//将局部变量表slot2的值复制到栈顶此时:栈:20010012:iload_2//栈顶两个元素出栈,并相加,结果重新入栈.此时:栈:30013:iadd//将局部变量表slot3的值复制到栈顶此时:栈:30030014:iload_3//将栈顶元素相乘,结果重新入栈15:imul//将栈顶的结果返回给方法调用者.方法执行结束16:ireturnLineNumberTable: line85:0 line86:3 line87:7 line88:11
基于栈的执行引擎正是通过这样出栈入栈的方式完成指令,而基于寄存器的则不然,是将操作数存入寄存器,同时将输入值也就是指令参数与某寄存器的存储值相加.区别就在于存储位置,以及参数问题,基于栈的大部分指令都是无参数指令,指令很明确的规定了要用哪几个栈元素,栈元素的类型是什么.
我们平常所使用的电脑,其X86指令集,正是基于寄存器的指令集.
优缺点则是:基于栈,可移植性较强,但速度比较慢,慢的原因一是需要许多冗余操作,代码.二是基于栈是基于内存的操作方式,而内存的速度比起寄存器更是要慢上许多.
总结
本文大致介绍这样几点:
java多态在jvm层次的实现.
为什么说jvm执行引擎是基于栈的执行引擎,以及究竟是怎样一个流程.
以上就是Java虚拟机执行引擎知识总结的详细内容,更多关于Java虚拟机执行引擎的资料请关注毛票票其它相关文章!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。