方法调用不是方法执行, 方法调用是让 jvm 确定调用哪个方法, 所以, 程序运行时的它是最普遍, 最频繁的操作. jvm 需要在类加载期间甚至运行期间才能确定方法的直接引用.
解析
所有方法在 Class 文件都是一个常量池中的符号引用, 类加载的解析阶段会将其转换成直接引用, 这种解析的前提是: 要保证这个方法在运行期是不可变的. 这类方法的调用称为解析.
jvm 提供了 5 条方法调用字节码指令:
[ ] invokestatic: 调用静态方法
[ ] invokespecial: 调用构造器方法, 私有方法和父类方法
[ ] invokevirtual: 调用所有的虚方法.
[ ] invokeinterface: 调用接口方法, 会在运行时期再确定一个实现此接口的对象
[ ] invokedynamic: 现在运行时期动态解析出调用点限定符所引用的方法, 然后再执行该方法, 在此之前的 4 条指令, 分派逻辑都是固化在虚拟机里面的, 而 invokedynamic 指令的分派逻辑是由用户所设定的引导方法决定的. InvokeDynamic 指令详细请点击 InvokeDynamic 指令 https://blog.csdn.net/zxhoo/article/details/38387141
被 invokestatic 和 invokespecial 指令调用的方法, 都能保证方法的不可变性, 符合这个条件的有静态方法, 私有方法, 实力构造器, 父类方法 4 类. 这些方法称为非虚方法.
- public class Main {
- public static void main(String[] args) {
- //invokestatic 调用
- Test.hello();
- //invokespecial 调用
- Test test = new Test();
- }
- static class Test{
- static void hello(){
- System.out.println("hello");
- }
- }
- }
复制代码
解析调用一定是一个静态的过程, 在编译期间就可以完全确定, 在类装载的解析阶段就会把涉及的符号引用全部转化为可确定的直接引用, 不会延迟到运行期去完成. 而分派调用可能是静态的也可能是动态的, 根据分派一句的宗量数可分为单分派和多分派. 因此分派可分为: 静态单分派, 静态多分派, 动态单分派, 动态多分派.
静态分派(方法重载)
所有依赖静态类型来定位方法执行版本的分派动作成为静态分派.
- public class Test {
- static class Phone{}
- static class Mi extends Phone{}
- static class Iphone extends Phone{}
- public void show(Mi mi){
- System.out.println("phone is mi");
- }
- public void show(Iphone iphone){
- System.out.println("phone is iphone");
- }
- public void show(Phone phone){
- System.out.println("phone parent class be called");
- }
- public static void main(String[] args) {
- Phone mi = new Mi();
- Phone iphone = new Iphone();
- Test test = new Test();
- test.show(mi);
- test.show(iphone);
- test.show((Mi)mi);
- }
- }
复制代码
执行结果:
- phone parent class be called
- phone parent class be called
- phone is mi
复制代码
我们把上面代码中的 Phone 称为变量的静态类型或者叫外观类型, 吧 Mi 和 Iphone 称为实际类型, 静态类型仅仅在使用时发生变化, 编译可知; 实际类型在运行期才知道结果, 编译器在编译程序的时候并不知道一个对象的实际类型是什么.
所以, jvm 重载时是通过参数的静态类型而不是实际类型作为判定依据. 下图可以证明:
根据上面的代码也可以看出, 我们可以使用强制类型转换来使静态类型发生改变.
动态分派(方法覆盖)
- public class Test2 {
- static abstract class Phone{
- abstract void show();
- }
- static class Mi extends Phone{
- @Override
- void show() {
- System.out.println("phone is mi");
- }
- }
- static class Iphone extends Phone{
- @Override
- void show() {
- System.out.println("phone is iphone");
- }
- }
- public static void main(String[] args) {
- Phone mi = new Mi();
- Phone iphone = new Iphone();
- mi.show();
- iphone.show();
- mi = new Iphone();
- mi.show();
- }
- }
复制代码
- phone is mi
- phone is iphone
- phone is iphone
复制代码
这个结果大家肯定都能猜到, 但是你又没有想过编译器是怎么确定他们的实际变量类型的呢. 这就关系到了 invokevirtual 指令, 该指令的第一步就是在运行期确定接受者的实际类型. 所以两次调用 invokevirtual 指令吧常量池中的类方法符号引用解析到了不同的直接引用上.
invokevirtual 指令的运行时解析过程大致分为以下几个步骤.
(1)找到操作数栈顶的第一个元素 (对象引用) 所指向的对象的实际类型, 记作 C; (2)如果在类型 C 中找到与常量中的描述符和简单名称都相符的方法, 则进行访问权限校验, 如果通过则返回这个方法的直接引用, 查找过程结束; 如果不通过, 则返回
java.lang.IllegalAccessError.
(3)否则, 按照继承关系从下往上依次对 C 的各个父类进行第 2 步的搜索和验证. (4)如果始终没有找到合适的方法, 则抛出
java.lang.AbstractMethodError 异常
.
动态类型语言支持
动态语言的关键特征是它的类型检查的主体过程是在运行期间而不是编译期. 相对的, 在编译期间进行类型检查过程的语言 (java,c++) 就是静态类型语言.
运行时异常: 代码只要不运行到这一行就不会报错. 连接时异常: 类加载抛出异常.
那动态, 静态类型语言谁更好?
它们都有自己的优点. 静态类型语言在编译期确定类型, 可以提供严谨的类型检查, 有很多问题编码的时候就能及时发现, 利于开发稳定的大规模项目. 动态类型语言在运行期确定类型, 有很大的灵活性, 代码更简洁清晰, 开发效率高.
- public class MethodHandleTest {
- static class ClassA {
- public void show(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 最终是哪个实现类, 下面这句都能正确调用到 show 方法.
- getPrintlnMH(obj).invokeExact("fantj");
- }
- private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable {
- // MethodType: 代表 "方法类型", 包含了方法的返回值 (methodType() 的第一个参数)和具体参数 (methodType() 第二个及以后的参数).
- MethodType mt = MethodType.methodType(void.class, String.class);
- // lookup()方法来自于 MethodHandles.lookup, 这句的作用是在指定类中查找符合给定的方法名称, 方法类型, 并且符合调用权限的方法句柄.
- // 因为这里调用的是一个虚方法, 按照 Java 语言的规则, 方法第一个参数是隐式的, 代表该方法的接收者, 也即是 this 指向的对象, 这个参数以前是放在参数列表中进行传递, 现在提供了 bindTo()方法来完成这件事情.
- return lookup().findVirtual(reveiver.getClass(), "show", mt).bindTo(reveiver);
- }
- }
复制代码
fantj 复制代码
无论 obj 是何种类型 (临时定义的 ClassA 抑或是实现 PrintStream 接口的实现类 System.out), 都可以正确调用到 show() 方法.
仅站在 Java 语言的角度看, MethodHandle 的使用方法和效果上与 Reflection 都有众多相似之处. 不过, 它们也有以下这些区别:
Reflection 和 MethodHandle 机制本质上都是在模拟方法调用, 但是 Reflection 是在模拟 Java 代码层次的方法调用, 而 MethodHandle 是在模拟字节码层次的方法调用. 在
MethodHandles.Lookup
上的三个方法 findStatic(),findVirtual(),findSpecial()正是为了对应于 invokestatic,
invokevirtual & invokeinterface
和 invokespecial 这几条字节码指令的执行权限校验行为, 而这些底层细节在使用 Reflection API 时是不需要关心的.
Reflection 中的
java.lang.reflect.Method
对象远比 MethodHandle 机制中的
java.lang.invoke.MethodHandle
对象所包含的信息来得多. 前者是方法在 Java 一端的全面映像, 包含了方法的签名, 描述符以及方法属性表中各种属性的 Java 端表示方式, 还包含有执行权限等的运行期信息. 而后者仅仅包含着与执行该方法相关的信息. 用开发人员通俗的话来讲, Reflection 是重量级, 而 MethodHandle 是轻量级.
由于 MethodHandle 是对字节码的方法指令调用的模拟, 那理论上虚拟机在这方面做的各种优化(如方法内联), 在 MethodHandle 上也应当可以采用类似思路去支持(但目前实现还不完善). 而通过反射去调用方法则不行.
MethodHandle 与 Reflection 除了上面列举的区别外, 最关键的一点还在于去掉前面讨论施加的前提 "仅站在 Java 语言的角度看" 之后: Reflection API 的设计目标是只为 Java 语言服务的, 而 MethodHandle 则设计为可服务于所有 Java 虚拟机之上的语言, 其中也包括了 Java 语言而已.
invokedynamic 指令
- public class InvokeDynamicTest {
- public static void main(String[] args) throws Throwable {
- INDY_BootstrapMethod().invokeExact("icyfenix");
- }
- public static void testMethod(String s) {
- System.out.println("hello String:" + s);
- }
- public static CallSite BootstrapMethod(MethodHandles.Lookup lookup, String name, MethodType mt) throws Throwable {
- return new ConstantCallSite(lookup.findStatic(InvokeDynamicTest.class, name, mt));
- }
- private static MethodType MT_BootstrapMethod() {
- return MethodType.fromMethodDescriptorString("(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", null);
- }
- private static MethodHandle MH_BootstrapMethod() throws Throwable {
- return lookup().findStatic(InvokeDynamicTest.class, "BootstrapMethod", MT_BootstrapMethod());
- }
- private static MethodHandle INDY_BootstrapMethod() throws Throwable {
- CallSite cs = (CallSite) MH_BootstrapMethod().invokeWithArguments(lookup(), "testMethod", MethodType.fromMethodDescriptorString("(Ljava/lang/String;)V", null));
- return cs.dynamicInvoker();
- }
- }
来源: https://juejin.im/post/5b7265a5f265da27f8036ba8