• 虚拟机字节码执行引擎——动态类型语言支持


    目录

    动态类型语言

    Java与动态类型

    java.lang.invoke包

    invokedynamic指令

    实战:掌控方法分派规则


    随着JDK 7的发布的字节码首位新成员——invokedynamic指令。这条新增加的指 令是JDK 7的项目目标:实现动态类型语言(Dynamically Typed Language)支持而进行的改进之一, 也是为JDK 8里可以顺利实现Lambda表达式而做的技术储备。

    动态类型语言

    动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编 译期进行的,满足这个特征的语言有很多,常用的包括:APL、Clojure、Erlang、Groovy、 JavaScript、Lisp、Lua、PHP、Prolog、Python、Ruby、Smalltalk、Tcl。

    如C++和Java等就是最常用的静态类型语言。

    Java与动态类型

    Java虚拟机层面对动态类型语言的支持一直都还有所欠缺,主要表现在方法调用方 面:JDK 7以前的字节码指令集中,4条方法调用指令(invokevirtual、invokespecial、invokestatic、 invokeinterface)的第一个参数都是被调用的方法的符号引用(CONSTANT_Methodref_info或者 CONSTANT_InterfaceMethodref_info常量)

    方法的符号引用在编译时产生而动态类型语言只有在运行期才能确定方法的接收者。这样,在Java虚拟机上实现的动态类型语言就不得不使用“曲线救国”的方式(如编译时留个占位符类型,运行时动态生成字节码实现具体类型到占位符类型的适配)来实现,但这样势必会让动态类型语言实现的复杂度增加,也会带来额外的性能和内存开销。因此,在Java虚拟机层面上提供动态类型的直接支持就成为Java平台发展必须解决的问题

    java.lang.invoke包

    Java语言没有办法单独把一个函数作为参数进行传递。普遍的做法是设计 一个带有compare()方法的Comparator接口,以实现这个接口的对象作为参数,例如Java类库中的 Collections::sort()方法就是这样定义的: void sort(List list, Comparator c)

    一种新的动态确定目标方法的机制,称为“方法句柄”(Method Handle)。

    在拥有方法句柄之后,Java语言也可以拥有类似于函数指针或者委托的方法别名这样的工 具了。代码清单8-12演示了方法句柄的基本用法,无论obj是何种类型(临时定义的ClassA抑或是实现 PrintStream接口的实现类System.out),都可以正确调用到println()方法。

    1. import static java.lang.invoke.MethodHandles.lookup;
    2. import java.lang.invoke.MethodHandle;
    3. import java.lang.invoke.MethodType;
    4. /**
    5. * JSR 292 MethodHandle基础用法演示
    6. * @author zzm
    7. */
    8. public class MethodHandleTest {
    9. static class ClassA {
    10. public void println(String s) {
    11. System.out.println(s);
    12. }
    13. }
    14. public static void main(String[] args) throws Throwable {
    15. Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
    16. // 无论obj最终是哪个实现类,下面这句都能正确调用到println方法。
    17. getPrintlnMH(obj).invokeExact("icyfenix");
    18. }
    19. private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable {
    20. // MethodType:代表“方法类型”,包含了方法的返回值(methodType()的第一个参数)和
    21. 具体参数(methodType()第二个及以后的参数)。
    22. MethodType mt = MethodType.methodType(void.class, String.class);
    23. // lookup()方法来自于MethodHandles.lookup,这句的作用是在指定类中查找符合给定的方法
    24. 名称、方法类型,并且符合调用权限的方法句柄。
    25. // 因为这里调用的是一个虚方法,按照Java语言的规则,方法第一个参数是隐式的,代表该方法的接
    26. 收者,也即this指向的对象,这个参数以前是放在参数列表中进行传递,现在提供了bindTo()
    27. 方法来完成这件事情。
    28. return lookup().findVirtual(reveiver.getClass(), "println", mt).bindTo(reveiver);
    29. }
    30. }

    方法getPrintlnMH()中实际上是模拟了invokevirtual指令的执行过程,只不过它的分派逻辑并非固 化在Class文件的字节码上,而是通过一个由用户设计的Java方法来实现。而这个方法本身的返回值 (MethodHandle对象),可以视为对最终调用方法的一个“引用”。

    MethodHandle在使用方法和效果上与Reflection(反射)有众多相似之 处。不过,它们也有以下这些区别:

    • Reflection和MethodHandle机制本质上都是在模拟方法调用,但是Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用。在MethodHandles.Lookup上的3个方法 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上也应当可以采用类似思路去支持(但目前实现还在继续完善 中),而通过反射去调用方法则几乎不可能直接去实施各类调用点优化措施。

    Reflection API的设计目标是只为Java语言服务的,而MethodHandle 则设计为可服务于所有Java虚拟机之上的语言,其中也包括了Java语言而已,而且Java在这里并不是主 角。

    invokedynamic指令

    invokedynamic指令与MethodHandle机制的作用是一样的,都是为了解决原有4 条“invoke*”指令方法分派规则完全固化在虚拟机之中的问题,把如何查找目标方法的决定权从虚拟机 转嫁到具体用户代码之中,让用户(广义的用户,包含其他程序语言的设计者)有更高的自由度。而 且,它们两者的思路也是可类比的,都是为了达成同一个目的,只是一个用上层代码和API来实现, 另一个用字节码和Class中其他属性、常量来完成。

    每一处含有invokedynamic指令的位置都被称作“动态调用点(Dynamically-Computed Call Site)”, 这条指令的第一个参数不再是代表方法符号引用的CONSTANT_Methodref_info常量,而是变为JDK 7 时新加入的CONSTANT_InvokeDynamic_info常量,

    从这个新常量中可以得到3项信息:引导方法 (Bootstrap Method,该方法存放在新增的BootstrapMethods属性中)、方法类型(MethodType)和 名称

    引导方法是有固定的参数,并且返回值规定是java.lang.invoke.CallSite对象,这个对象代表了真正要执行的目标方法调用

    根据CONSTANT_InvokeDynamic_info常量中提供的信息,虚拟机可以找到并且执行引导方法,从而获得一个CallSite对象,最终调用到要执行的目标方法上。

    1. import static java.lang.invoke.MethodHandles.lookup;
    2. import java.lang.invoke.CallSite;
    3. import java.lang.invoke.ConstantCallSite;
    4. import java.lang.invoke.MethodHandle;
    5. import java.lang.invoke.MethodHandles;
    6. import java.lang.invoke.MethodType;
    7. public class InvokeDynamicTest {
    8. public static void main(String[] args) throws Throwable {
    9. INDY_BootstrapMethod().invokeExact("icyfenix");
    10. }
    11. public static void testMethod(String s) {
    12. System.out.println("hello String:" + s);
    13. }
    14. public static CallSite BootstrapMethod(MethodHandles.Lookup lookup, String name, MethodType mt) throws Throwable {
    15. return new ConstantCallSite(lookup.findStatic(InvokeDynamicTest.class, name, mt));
    16. }
    17. private static MethodType MT_BootstrapMethod() {
    18. return MethodType
    19. .fromMethodDescriptorString(
    20. "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String; Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", null);
    21. }
    22. private static MethodHandle MH_BootstrapMethod() throws Throwable {
    23. return lookup().findStatic(InvokeDynamicTest.class, "BootstrapMethod", MT_BootstrapMethod());
    24. }
    25. private static MethodHandle INDY_BootstrapMethod() throws Throwable {
    26. CallSite cs = (CallSite) MH_BootstrapMethod().invokeWithArguments(lookup(), "testMethod",
    27. MethodType.fromMethodDescriptorString("(Ljava/lang/String;)V", null));
    28. return cs.dynamicInvoker();
    29. }
    30. }

    把上面的代码编译,再使用INDY转换后重新生成的字节码

    1. Constant pool:
    2. #121 = NameAndType #33:#30 // testMethod:(Ljava/lang/String;)V
    3. #123 = InvokeDynamic #0:#121 // #0:testMethod:(Ljava/lang/String;)V
    4. public static void main(java.lang.String[]) throws java.lang.Throwable;
    5. Code:
    6. stack=2, locals=1, args_size=1
    7. 0: ldc #23 // String abc
    8. 2: invokedynamic #123, 0 // InvokeDynamic #0:testMethod: (Ljava/lang/String;)V
    9. 7: nop
    10. 8: return
    11. public static java.lang.invoke.CallSite BootstrapMethod(java.lang.invoke.Method Handles$Lookup, java.lang.String, java.lang.invoke.MethodType) throws java.lang.Throwable;
    12. Code:
    13. stack=6, locals=3, args_size=3
    14. 0: new #63 // class java/lang/invoke/ConstantCallSite
    15. 3: dup
    16. 4: aload_0
    17. 5: ldc #1 // class org/fenixsoft/InvokeDynamicTest
    18. 7: aload_1
    19. 8: aload_2
    20. 9: invokevirtual #65 // Method java/lang/invoke/MethodHandles$ Lookup.findStatic:(Ljava/lang/Class;Ljava/ lang/String;Ljava/lang/invoke/Method Type;)Ljava/lang/invoke/MethodHandle;
    21. 12: invokespecial #71 // Method java/lang/invoke/ConstantCallSite. "":(Ljava/lang/invoke/MethodHandle;)V
    22. 15: areturn

    从main()方法的字节码中可见,原本的方法调用指令已经被替换为invokedynamic了,它的参数为 第123项常量(第二个值为0的参数在虚拟机中不会直接用到,这与invokeinterface指令那个的值为0的 参数一样是占位用的,目的都是为了给常量池缓存留出足够的空间): 2: invokedynamic #123, 0 // InvokeDynamic #0:testMethod:(Ljava/lang/String;)V

    从常量池中可见,第123项常量显示“#123=InvokeDynamic#0:#121”说明它是一项 CONSTANT_InvokeDynamic_info类型常量,常量值中前面“#0”代表引导方法取Bootstrap Methods属性 表的第0项(javap没有列出属性表的具体内容,不过示例中仅有一个引导方法,即 BootstrapMethod()),而后面的“#121”代表引用第121项类型为CONSTANT_NameAndType_info的常 量,从这个常量中可以获取到方法名称和描述符,即后面输出的“testMethod: (Ljava/lang/String;)V”。

    再看BootstrapMethod(),这个方法在Java源码中并不存在,是由INDY产生的,但是它的字节码很 容易读懂,所有逻辑都是调用MethodHandles$Lookup的findStatic()方法,产生testMethod()方法的 MethodHandle,然后用它创建一个ConstantCallSite对象。最后,这个对象返回给invokedynamic指令实 现对testMethod()方法的调用,invokedynamic指令的调用过程到此就宣告完成了。

    INDY下载地址:http://blogs.oracle.com/jrose/entry/a_modest_tool_for_writing。

    实战:掌控方法分派规则

    invokedynamic指令与此前4条传统的“invoke*”指令的最大区别就是它的分派逻辑不是由虚拟机决 定的,而是由程序员决定。

    1. import java.lang.invoke.MethodHandle;
    2. import java.lang.invoke.MethodHandles;
    3. import java.lang.invoke.MethodType;
    4. import java.lang.reflect.Field;
    5. class GrandFather {
    6. void thinking() {
    7. System.out.println("i am grandfather");
    8. }
    9. }
    10. class Father extends GrandFather {
    11. @Override
    12. void thinking() {
    13. System.out.println("i am father");
    14. }
    15. }
    16. class Son extends Father {
    17. public static void main(String[] args) {
    18. new Son().thinking();
    19. }
    20. @Override
    21. void thinking() {
    22. // 请读者在这里填入适当的代码(不能修改其他地方的代码)
    23. // 实现调用祖父类的thinking()方法,打印"i am grandfather"
    24. try {
    25. MethodType mt = MethodType.methodType(void.class);
    26. // 必须保证
    27. // findSpecial()查找方法版本时受到的访问约束(譬如对访问控制的限制、对参数类型的限制)应与使用
    28. // invokespecial指令一样,两者必须保持精确对等,包括在上面的场景中它只能访问到其直接父类中的方法版本。
    29. Field lookupImpl = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
    30. lookupImpl.setAccessible(true);
    31. MethodHandle mh = ((MethodHandles.Lookup) lookupImpl.get(null)).findSpecial(GrandFather.class, "thinking", mt, GrandFather.class);
    32. mh.invoke(this);
    33. } catch (Throwable e) {
    34. }
    35. }
    36. }

    在Son类的thinking()方法中根本无法获取到一个实际类型是GrandFather的对象引用, 而invokevirtual指令的分派逻辑是固定的,只能按照方法接收者的实际类型进行分派,这个逻辑完全固化在虚拟机中,程序员无法改变。最后使用MethodHandle来解决问题。

  • 相关阅读:
    【ACM组合数学 | 错排公式】写信
    公式编辑器Axmath+公式识别器SimpleTex+Markdown编辑器Typora
    [Linux]----文件操作(复习C语言+文件描述符)
    springboot学习二:springboot 初创建 web 项目,打包项目并测试成功
    Spring Cloud Loadbalancer
    【数据库与事务系列】分库分表中间件
    【论文极速读】Prompt Tuning——一种高效的LLM模型下游任务适配方式
    Maven高级
    2、Linux中静态IP与动态IP的修改
    YOLO目标检测——红火蚂蚁识别数据集+已标注yolo格式标签下载分享
  • 原文地址:https://blog.csdn.net/weixin_52383177/article/details/127561409