本文共 9042 字,大约阅读时间需要 30 分钟。
参考:
8.1.8.2. 运行时栈帧结构
8.2.1.局部变量表
8.2.2. 操作数栈
8.2.3. 动态连接
8.2.4. 方法返回地址:方法在调用结束后,恢复上层方法的局部变量表和操作数栈,并将返回值压到调用者的操作数栈中,同调整PC计数器的值以指向后面一条指令。
8.2.5. 附加信息:其余一些信息,如调试信息。
8.3. 方法调用
目的:确定条用那个版本的方法(重载重写),编译器确定还是运行期确定?这里主要讲的就是多态性!
方法调用的主要任务就是确定被调用方法的版本(即调用哪一个方法),该过程不涉及方法具体的运行过程。按照调用方式共分为两类: 1. 解析调用一定是静态的过程,在编译期间就完全确定目标方法。 2. 分派调用既可能是静态,也可能是动态的,根据分派标准可以分为单分派和多分派。两两组合有形成了静态单分派、静态多分派、动态单分派、动态多分派8.3.1. 解析调用
8.3.2 分派调用
public class BB { static abstract class Human { } static class Man extends Human { } static class Woman extends Human { } public void sayHello(Human guy) { System.out.println("hello guy"); } public void sayHello(Man guy) { System.out.println("hello man"); } public void sayHello(Woman guy) { System.out.println("hello woman"); } public static void main(String[] args) { BB b = new BB(); Human man = new Man();//静态类型为Human Human woman = new Woman(); b.sayHello(man); b.sayHello(woman); } } ``` stylus执行结果是:hello guyhello guy原因:虚拟机在重载时是通过参数的静态类型而不是实际类型作为判定依据,并且静态类型是编译期可知的,所以在编译阶段,javac编译器就根据参数的静态类型决定使用哪个重载版本,并把这个方法的符号引用写入invokevirtual指令的参数中。
public class Verload { private static void sayHello(char arg){ System.out.println("hello char"); } private static void sayHello(Object arg){ System.out.println("hello Object"); } private static void sayHello(int arg){ System.out.println("hello int"); } private static void sayHello(Character arg){ System.out.println("hello long"); } private static void sayHello(char... arg){ System.out.println("hello long"); } private static void sayHello(Serializable arg){ System.out.println("hello long"); } public static void main(String[] args) { sayHello('c'); } //一次查找:char-int-long-Character-Serializable-Object-char...}
public class AA { static abstract class Human { protected abstract void sayHello(); } static class Man extends Human { @Override protected void sayHello() { System.out.println("man say hello"); } } static class Woman extends Human { @Override protected void sayHello() { System.out.println("woman say hello"); } } public static void main(String[] args) { Human man = new Man(); Human woman = new Woman(); man.sayHello(); woman.sayHello(); man = new Woman(); man.sayHello(); } } 执行结果:man say hellowoman say hellowoman say hello原因:invokevirtual指令有多态查找的机制,该指令的运行时解析过程步骤如下:1.找到操作数栈顶的第一个元素所指向的对象的实际类型,记做c2.如果在类型c中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束,不通过则返回java.lang.IllegalAccessError.3.否则,按照继承关系从下往上依次对c的各个父类进行第二步的搜索和验证过程。4.始终没找到合适的方法,抛出java.lang.AbstractMethodError异常。这就是java语言中方法重写的本质。
public class disPatch { static class QQ { } static class _360 { } public static class Father { public void hardChoice(QQ arg) { System.out.println("father choose qq"); } public void hardChoice(_360 arg) { System.out.println("father choose _360"); } } public static class Son extends Father { public void hardChoice(QQ arg) { System.out.println("son choose qq"); } public void hardChoice(_360 arg) { System.out.println("son choose _360"); } } public static void main(String[] args) { Father father = new Father(); Father son = new Son(); father.hardChoice(new _360()); son.hardChoice(new QQ()); }}
* 方法的调用者和参数统称为方法的宗量,根据总量的数量,将分派分为单分派的多分派。我这里的理解就是单分派和双分派,调用者为一个宗量,所有的方法参数统称为另一个宗量。* java实际上是静态多分派和动态单分派* 静态多分派:比如上面的例子,son.hardChoice(new QQ()); 我们需要确定调用类型是Son还是father,同时还需要确定方法参数是360还是QQ,这个都是需要编译器指引的。* 动态单分派:还是上面这个例子,由于编译期已经确定了方法的参数即hardChoice(new QQ()),只是不知道需要确定调用者,运行期son实际类型为Father* 一句话说明:就是重载是编译期的事,重写是运行期的事;**只有运行期才能够确定调用的是父类还是子类方法。**
8.3.3. 动态语言支持
static class ClassA{ public void println(String s){ System.out.println(s); }}public static void main(String[] args) throws Throwable { Object obj = System.currentTimeMillis() % 2 == 0 ? System.out:new ClassA(); /* 无论obj最终是那个实现类,下面这句都能正确调用到println方法 */ getPrintlnMH(obj).invokeExact("icyfenix"); /* output: * icyfenix */}private static MethodHandle getPrintlnMH(Object receiver) throws Throwable{ /* MethodType: 代表“方法类型”,包含了方法的返回值(methodType()的第一个参数) * 和具体参数(methodType()第二个及以后的参数) */ MethodType mt = MethodType.methodType(void.class, String.class); /* lookup()方法来自于MethodHandles.lookup, * 这句的作用是在指定类中查找符合给定的方法名称、方法类型,并且符合调用权限的方法句柄。 * 因为这里调用的是一个虚方法,按照Java语言的规则,方法第一个参数是隐式的,代表该方法的接收者, * 也即是this指向的对象,这个参数以前是放在参数列表中进行传递的,而现在提供给了bindTo()方法来完成这件事情 */ return MethodHandles.lookup().findVirtual(receiver.getClass(), "println", mt) .bindTo(receiver);}
MethodHandle 的使用方法和效果与 Reflection 有众多相似之处,不过,它们还是有以下这些区别:
Reflection 是在模拟 Java 代码层次的方法调用,而 MethodHandle 是在模拟字节码层次的方法调用。 Reflection 是重量级,而 MethodHandle 是轻量级。 Reflection API 的设计目标是只为 Java 语言服务的,而 MethodHandle 则设计成可服务于所有 Java 虚拟机之上的语言,其中也包括 Java 语言。 * invokedynamic 指令 在某种程度上, invokedynamic 指令与 MethodHandle 机制的作用是一样的,都是为了解决原有 4 条” invoke” 指令方法分派规则固化在虚拟机之中的问题,把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码之中。 * 掌控方法分派规则后续未看:转载
4、基于栈的字节码解析执行引擎 4.1、解析执行 Java 语言中, Javac 编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。因为这一部分动作是在 Java 虚拟机之外进行的,而解释器在虚拟机的内部,所以 Java 程序的编译就是半独立的实现。4.2、基于栈的指令集与基于寄存器的指令集
Java 编译器输出的指令流,基本上[ 1] 是一种基于栈的指令集架构。基于栈的指令集主要的优点就是可移植,寄存器由硬件直接提供[ 2], 程序直接依赖这些硬件寄存器则不可避免地要受到硬件的约束。
栈架构指令集的主要缺点是执行速度相对来说会稍慢一些。
虽然栈架构指令集的代码非常紧凑,但是完成相同功能所需的指令数量一般会比寄存器架构多,因为出栈、入栈操作本身就产生了相当多的指令数量。更重要的是,栈实现在内存之中,频繁的栈访问也就意味着频繁的内存访问,相对于处理器来说,内存始终是执行速度的瓶颈。尽管虚拟机可以采取栈顶缓存的手段,把最常用的操作映射到寄存器中避免直接内存访问,但这也只能是优化措施而不是解决本质问题的方法。由于指令数量和内存访问的原因,所以导致了栈架构指令集的执行速度会相对较慢。
4.3、基于栈的解析执行过程
一段简单的算术代码的字节码表示 link 在 HotSpot 虚拟机中,有很多以” fast_” 开头的非标准字节码指令用于合并、替换输入的字节码以提升解释执行性能,而即时编译器的优化手段更加花样繁多[ 1]。