在源码编译阶段将源码编译为JVM字节码,JVM字节码是一种中间代码的方式,要由JVM在运行期对其进行解释并执行,这种称为字节码解释执行方式
(每一个线程有一到多个栈帧)
栈帧是用于支持虚拟机进行方法调用和执行的数据结构,每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些附加信息
是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量表的容量以变量槽(slot)为最小单位,一个slot可以存放一个32位以内的数据类型,而java中占32位以内的数据类型有boolean、byte、char、short、int、float、reference(也可以64位)和return Address八种类型
Java语句中明确规定的64位的数据类型只有long和double两种(reference可能是32位,也可能是64位)故long和double不是原子操作,只是局部变量表建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的slot是否是原子操作,都不会引起数据安全问题
操作数栈的每一个元素,可以是任意的Java数据,包括long和double。32位数据类型所占的栈容量为1,而64位则为2
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,方法执行过程中,各种字节码指令向操作数栈写入和提取内容,也就是入栈出栈操作
Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈
符号引用一部分会在类加载阶段或第一次使用的时候转换成为直接引用,这种转换称为静态解析。另外一部分将在每一次的运行期间转换为直接引用,这部分称为动态引用
当一个方法被执行后,有两种方式退出这个方法。
第一种是执行引擎,遇到一个方法返回的字节码指令,这时可能会返回值传递给上层的方法调用者。这种退出方式为正常完成出口
另一种是遇到异常并且没有在方法体内得到处理(throws不属于方法体内处理),这种退出方式是不会给它的上层调用者产生任何返回值的。
一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息
方法退出的实质
实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表盒操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等
(1)方法调用不等同于方法执行,方法调用唯一任务是确定被调用方法的版本(调用了哪一个方法),暂时还不涉及方法内部的具体运行过程
(2)Class文件的编译过程不包含传统编译中的连接步骤,故一切方法调用在class文件里面存储的都只是符号引用,而不是直接引用(直接引用:方法在实际运行时内存布局中的入口地址)
(3)Java中符合“编译期可知,运行期不可变”的需求的方法主要由静态方法和私有方法两大类
(4)调用目标在程序代码写好、编译器进行编译时必须确定下来,这类方法的调用称为解析。它们都不可能通过继承或别的方式重写出其他版本,因此它们都适合在类加载阶段进行解析
(5)非虚方法:在解析阶段能确定唯一的调用版本,符合的有静态方法、私有方法、实例构造方法和父类方法(即可以在解析阶段就把符号引用解析为直接引用的称为非虚方法,加上final方法),除这些外的是虚方法
(6)解析调用一定是静态的过程,而分派调用则可能是静态,也可能是动态的
(7)静态分派——重载
虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的,并且静态类型是编译期可知的,所以在编译阶段,javac编译器就根据参数的静态类型决定使用哪个重载版本。所有依赖静态类型来定位方法执行版本的分派动作都称为静态分派。静态分派最典型的应用就是方法重载
注:字面量(非引用的基本类型)没有显式的静态类型,它的静态类型只能通过语言上的规则去理解和推断
(8)动态分派——重写
重写的本质,动态分派过程。虚拟机是如何知道要调用哪个方法
使用invokevirtual指令进行多态查找,步骤如下:
1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记为C
2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找结束。不通过则返回java.lang.IllegalAccessError异常
3)否则,按照继承关系从下往上依次对C的各父类进行第2步的搜索和验证
4)如果始终没找到合适方法,则抛出java.lang.AbstractMethodError异常
(1)许多Java虚拟机的执行引擎在执行java代码的时候都有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行,物理机才能执行的)两种选择
(2)笼统地说“是解释执行”对整个java语言是无意义的,因为在早期版本的JDK中就只有解释器,但后面的版本出现了即时编译器,故只有确定了谈论对象是某种具体的Java实现版本和执行引擎运行模式时,谈解释执行还编译执行才会比较确切
(3)大部分的程序代码到物理机的目标代码或虚拟机能执行的指令集之前,都需要经过一下各步骤(如图)
Java语言中,javac编译器完成了程序代码经过词法分析、语法分析道抽象语法树,再遍历语法树生成线性的字节码指令流的过程。因为这一部分是在java虚拟机之外进行的;而解释器在虚拟机内部,故Java程序的编译是半独立的实现
(4)基于栈的指令集合和基于寄存器的指令集
Java编译器输出的指令流,基本上是一种基于栈的指令集架构。与之相对的是基于寄存器的指令集(现在主流PC中直接支持的指令集架构)
区别和优缺点:
基于栈的指令集最主要有点是可移植性,而寄存器由硬件直接提供
栈架构指令集主要缺点是执行速度相对来说比较慢一些。(出、入栈操作产生相当多的指令)
(5)基于栈的解释器的执行过程
public int tt(){ int a = 100; int b = 200; int c = 300; return (a + b) * c; }
首先,执行偏移地址为0的指令,把100推入操作数栈顶
执行偏移地址1的指令,把操作数栈顶的整型值出栈并存放到第一个局部变量slot中
后面做同样的事情将200,300放入局部变量表(即把变量a,b,c赋值100,200,300)
然后分别将局部变量表的100,200入栈,然后将操作数栈前两个栈顶元素出栈,做整型加法
然后把结果重新入栈。
然后执行乘指令时,把局部变量表第三个变量slot中的300入栈到操作数栈,这时操作栈为两个300.
下一条指令将操作数栈中前两个栈顶元素出栈做整型乘法,然后把结果入栈
最后执行方法返回指令,将结束方法执行并将操作数栈顶的整型值返回给此方法的调用者
(6)对Sun JDK基于栈的体系结构的理解
一个线程创建后,都会产生程序计数器(PC或称PC寄存器)和栈(栈中存放栈帧)
程序计数器:存放下一条要执行的指令在方法内的偏移量;
栈帧:包括局部变量表和操作数栈(局部变量表:存放方法中的局部变量和参数;操作数栈:存放方法执行过程中产生的中间结果)
(1)解释执行的效率较低,为提升代码的执行性能,Sun JDK提供了将字节码编译为机器码的支持,编译在运行时进行,通常称为JIT编译器(即时编译器)
(2)Sun JDK在执行过程中对执行频率高的代码进行编译,对执行部频繁的代码则继续采用解释的方式,因此Sun JDK又称为HotSpot VM,在编译上Sun JDK提供了两种模式Client compiler和Server compiler
说明:(默认情况下)client模式下,当方法被调用1500此,server模式下是10000此就会编译成机器码,也可以通过启动时添加 -xx:CompilerThreshold=10000来设置
(3)Client compiler又称为C1,较为轻量级。占用内存较少。适用于桌面交互应用
Server compiler又称为C2,较为重量级,占内存较多,适合于服务器端应用
默认情况下,Sun JDK根据机器配置来选择Client或Server模式。当机器配置CPU超过2核且内存超过2GB即默认为Server模式,但在32位的Windows机器上始终选择的都是Client模式,可以启动时通过 -client 或 -server来强制指定
问题一:Sun JDK没有在启动时就使用HotSpot这个里面的即时编译器来编译或译成机器码,为什么呢?
原因1:静态编译并不能根据程序的运行状况来优化执行的代码,C2这种方式是根据运行状况来进行动态编译的,如分支判断、逃逸分析等,这些措施会对提升程序执行的性能会起很大的帮助;在静态编译的情况下是无法实现的。给C2收集运行数据越长,编译出来的代码会越优
原因2:解释执行比编译执行更节省内存
原因3:启动时解释执行的启动速度比编译再启动更加快
(1)基于反射技术可动态调用某对象实例中对应的方法、访问查看对象的属性等,无需在编写代码时就确定要创建的对象
(2)最常见的应用Java框架中,如:MVC框架中通常要调用实现类中的execute方法,但框架在编写时是无法知道实现类的,故可以使用反射机制去调用
代码示例:
1)Class actionClass = Class.forName(外部实现类名): 2)Method method = actionClass.getMethod("execute", null); 3)Object action = actionClass.newInstance(); 4)method.invoke(action, null);反射和直接创建对象的实例,调用方法的最多不同在于创建的过程,方法调用的过程是动态的
1)是调用本地方法,使用调用者所在的ClassLoader来加载创建出的class对象
2)校验class是否是public类型(权限检查)——>调用privateGetDeclaredMethods来获取class中多有方法。扫描方法集合列表,复制生成一个新的Method对象返回,如该类没有则继续扫描父类
3)校验class权限——>获取构造器对象——执行构造器对象的newInstance方法生成或得到(缓存中有)对象——生成字节码——加载到当前的ClassLoader中并实例化
4)与上一步类似
总结:反射整个过程比直接编译成字节码的调用复杂多了,还需要权限验证等,故性能较低