虚拟机之字节码执行
1、字节码执行从栈帧说起
栈帧就是Java虚拟机里执行由字节码编译成的机器代码的地方,包含两个结构:局部变量表和操作栈。
局部变量表时一组变量值的存储空间,代码里用到的变量值都会放到局部变量表中,其最小单位为slot,每一个slot都是可复用的,而没被复用或清空的slot对象仍存在关联时,该slot不会被GC回收。
-
实例方法中第一个即第0位索引slot是“this”参数以方便调用当前对象,减少开销。
-
操作栈时写入和读取各种字节码指令的地方,也即是方法真正执行的地方。
-
一般来说,两个栈帧可能会出现重叠的地方,原因在于下面栈帧的部分操作栈需要用到上面栈帧的部分变量,因此将这部分变量所在的局部变量表和用到这部分变量的操作栈重叠起来可以减少额外进行参数复制传递的开销。
2、动态链接
动态链接指的是运行期虚拟机将可变代码中的符号引用转换为直接引用的过程,与之相对应的有静态解析,静态解析即是在编译期将不可变代码中的符号引用转为直接引用的过程。
- 静态解析中出现的不可变代码即不可变方法亦被称为非虚方法,他们的特点都是不能被重写,常见的不可变方法为static、private、构造器和父类方法。一样不可变的final方法也是非虚方法,但不出现在静态解析中。
由于存在重载方法和方法重写,动态链接必须处理决定使用哪个版本的方法,这个过程称为分派。分派分为静态分派和动态分派,单分派和多分派之别。再介绍分派之前,我们需要先清楚一些概念——静态类型和实际类型。
- 静态类型和实际类型:声明一个变量时,该变量被定义成的类型称为变量的静态类型,而被赋值时的类型称为实际类型,举个例子比较容易明白:
Human man=new Man();这个句子中,Human是变量man的静态类型,而Man是其实际类型。
而由静态类型和实际类型可以区分静态分派和动态分派
-
静态分派:根据方法参数的静态类型进行分派,也就是对应方法重载执行版本的依据
-
动态分派:根据方法接收者的实际类型进行的分派,也就是对应方法重写执行版本的依据
-
为了实现重写的动态分派特性,在方法表中,父类和子类各自有自己的重写方法入口
-
实际上重载也需要考虑方法接收者是否是该重载版本的合理接收者,但在不存在重写的情况下不需要根据方法接收者的实际类型进行分派,因此,典型的重载可以说不是动态分派。
在区分动态分派和静态分派之前也需要清楚一个概念——宗量
- 宗量:方法接收者(即方法的调用者)和参数的统称
而根据宗量多少可以区分单分派和多分派
-
单分派:根据一个宗量进行的分派,根据方法的调用对象分派的重写是单分派
-
多分派:根据多个宗量进行的分派,同时根据方法调用对象和方法参数的静态类型的重载是多分派
因此我们可有总结:Java的分派是静态多分派,动态单分派。
3、方法返回地址
存放方法返回变量的地址
4、补充
Java使用了栈指令集,而不采用寄存器指令集(单片机的典型指令集)的原因需要从两者比较中得出
-
栈指令集:以底层物理器件为基础而建立的栈结构操作执行指令集,指令不依赖操作平台,用户在使用时可以忽略底层物理物理器件。
-
寄存器指令集:直接使用底层物理器件操作指令集
也就是说栈指令集中的指令在不同的机器平台上的构成是不一样的,而寄存器指令集是一个机器平台上的原生指令集,不同机器平台的寄存器指令集是不同的,因此Java使用了栈指令集来摆脱堆机器平台的依赖性,实现可移植性,但不可避免地带来了开销,导致速度下降。
5、字节码相关技术
-
I.生成字节码的例子:JSP编译器、AOP框架和动态代理技术
- 动态代理的实现:我们在main()方法中加入System.getProperties().put(“sun.misc.proxyGenerator.saveGeneratorFiles”,true);后将会产生一个名为“$Proxy0.class”的代理类class文件,反编译后可以发现动态代理通过调用InvocationHandler类的invoke()方法类执行被代理对象的方法和invoke()内的增强代码。
-
II.Retrotranslator——字节码替换和插入
JDK升级新增功能大致分为4类
-
①编译器层面,对代码进行插入和替换
-
②对API的代码进行增强——增强类库
-
③在字节码指令集进行改动
-
④虚拟机改进
一般来说,前两种出现的比较多,而由于字节码和虚拟机层面属于十分底层的东西,设计之初预留的改进空间较少,因此出现的比较少。而Retrotranslator技术就能模拟前2类,通过插入、替换字节码和独立类库实现将由高版本JDK生成的class文件转换为由低版本的JDK生成的class文件
-