Java8 Lambda表达式深入学习(2) -- InvokeDynamic指令详解

为了更好的支持动态类型语言,Java7通过JSR292给JVM增加了一条新的字节码指令:invokedynamic。之后,JVM上面的一些动态类型语言,比如Groovy(2.0+)JRuby(1.7.0+)都开始支持invokedynamic。不过让人意外的是,为动态语言量身定制的invokedynamic指令,居然也被用到了Java8的Lambda表达式(JSR335)实现上。本文会对invokedynamic(以下简写做indy)指令做出详细解释。

测试代码

Java7以及更早版本的Java语法是没有办法编译生成indy指令的,所以我用Java8(主要是利用它的Lambda表达式)写了一个测试类,如下:

public class InDyTest {

    public static void main(String[] args) {
        Runnable x = () -> {
            //System.out.println("Hello, World!");
        };
    }

}

InvokeDynamic指令

根据JVM规范的规定,indy的操作码是186(0xBA),格式是:

invokedynamic indexbyte1 indexbyte2 0 0

InDy指令有四个操作数,前两个操作数构成一个索引[ (indexbyte1 << 8) | indexbyte2 ],指向类的常量池,后两个操作数保留,必须是0。索引指向的常量池项的类型为CONSTANT_InvokeDynamic_info。编译InDyTest.java,然后用javap
-v -p
指令反编译生成的.class文件,找到main()方法,可以看到,确实生成了一条indy指令:

常量池索引为#2,查看常量池可知,确实是一个CONSTANT_InvokeDynamic_info:

-

CONSTANT_InvokeDynamic_info

CONSTANT_InvokeDynamic_info结构是Java7新引入class文件的,其用途就是给indy指令指定启动方法(bootstrap method)等信息。它本身又包含两个索引,如下所示:

CONSTANT_InvokeDynamic_info {
    u1 tag;
    u2 bootstrap_method_attr_index;
    u2 name_and_type_index;
}

其中name_and_type_index索引类常量池里的CONSTANT_NameAndType_info, 这个结构在Java7之前就有,所以不过多介绍。从上面的截图可以知道,它描述的是这样一个方法:

Runnable run() { ... } 

bootstrap_method_attr_index索引bootstrap_methods表,bootstrap_methods位于class文件的attributes表里。

BootstrapMethods属性

JVM规范规定,如果类的常量池中存在CONSTANT_InvokeDynamic_info的话,那么attributes表中就必定有且仅有一个BootstrapMethods属性。BootstrapMethods属性是个变长的表,结构如下所示:

BootstrapMethods_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 num_bootstrap_methods;
    {   u2 bootstrap_method_ref;
        u2 num_bootstrap_arguments;
        u2 bootstrap_arguments[num_bootstrap_arguments];
    } bootstrap_methods[num_bootstrap_methods];
}

每一个BootstrapMethod都包含一个bootstrap_method_ref和n个bootstrap_arguments。bootstrap_method_ref是个常量池索引,指向一个CONSTANT_MethodHandle_info。而每一个bootstrap_argument也都是常量池索引,可以指向下面这些结构:

  • CONSTANT_String_info
  • CONSTANT_Class_info
  • CONSTANT_Integer_info
  • CONSTANT_Long_info
  • CONSTANT_Float_info
  • CONSTANT_Double_info
  • CONSTANT_MethodHandle_info
  • CONSTANT_MethodType_info

下面我们继续看javap的反编译结果:

确实存在一个BootstrapMethods表,这个表中只有一个BootstrapMethod,它的bootstrap_method_ref是常量池#23,有三个bootstrap_arguments,分别指向常量池#24,#25和#24:

CONSTANT_MethodHandle_info

CONSTANT_MethodHandle_info结构包含两项信息,其结构如下所示:

CONSTANT_MethodHandle_info {
    u1 tag;
    u1 reference_kind;
    u2 reference_index;
}

reference_kind是一个1到9之间的整数,具体含义可以参考JVM规范。reference_index是常量池索引,但具体索引的是什么类型的常量,需要看reference_kind:

constant_pool entry reference_kind
CONSTANT_Fieldref_info  1 (REF_getField), 2 (REF_getStatic), 3 (REF_putField), or 4 (REF_putStatic)
CONSTANT_Methodref_info 5 (REF_invokeVirtual), 6 (REF_invokeStatic), 7 (REF_invokeSpecial), or 8 (REF_newInvokeSpecial)
CONSTANT_InterfaceMethodref_info 9 (REF_invokeInterface)

通过观察常量池#23可以看到,它的reference_kind是6(REF_invokeStatic),reference_index是#29,正好是个CONSTANT_Methodref_info:

-

也就是说,引用的是java.lang.invoke.LambdaMetafactory类的静态方法metafactory()。

引用关系图

由于Java类文件格式的原因,要想真正理解上面说的内容,确实有点困难。我画了一张图来帮助我们理解:

JVM如何执行indy指令

前面从class文件的角度,分析了indy指令。下面让我们看看JVM是如何执行indy指令的。

  • 每一个indy指令出现的地方,都叫做一个dynamic call site(动态调用点)
  • 根据indy指令的操作数,可以找到一个call site specifier(调用点说明符),根据前面的分析,这个说明符其实就常量池里的CONSTANT_InvokeDynamic_info
  • JVM解析(resolve)调用点说明符,得到下面三种信息:
    • 一个MethodHandle,指向bootstrap method(启动方法)
    • 方法名和方法描述,表示动态调用的方法
    • 其他提供给启动方法的参数
  • 接着JVM调用启动方法,并把上一步提到的信息通过参数传给启动方法
  • 启动方法必须返回一个CallSite对象,并且,这个CallSite对象将永久和这个动态调用点关联
  • 调用跟CallSite关联的MethodHandle指向的方法

下面是一张示意图,画出了关键点:

LambdaMetafactory.metafactory()

下面看一下LambdaMetafactory.metafactory()方法的源代码:

    public static CallSite metafactory(MethodHandles.Lookup caller,
                                       String invokedName, // run
                                       MethodType invokedType, // ()Runnable
                                       MethodType samMethodType, // ()void
                                       MethodHandle implMethod, // ()void
                                       MethodType instantiatedMethodType) // ()void
            throws LambdaConversionException {

        ...
    }

通过加断点运行InDyTest,可以看到传递给metafactory()方法的参数,我在上面的代码里进行了注释。

总结

indy指令看似简单,但实际上非常复杂。希望本文能够帮助你理解indy指令。

Java8 Lambda表达式深入学习(2) -- InvokeDynamic指令详解

时间: 2024-10-12 17:53:18

Java8 Lambda表达式深入学习(2) -- InvokeDynamic指令详解的相关文章

Java8 Lambda表达式深入学习(4) -- Java8实现方式

前几篇文章讨论了函数式接口和Lambda表达式语法.invokedynamic指令,以及Groovy2如何利用indy指令.本篇文章在前面几篇的基础之上,简要介绍Java8底层是如何实现Lambda表达式的. 示例代码 本文将以下面的代码为例展开讨论: import java.util.Arrays; import java.util.List; public class LambdaImplTest { public static void main(String[] args) { m1(A

【Java学习笔记之三十一】详解Java8 lambda表达式

Java 8 发布日期是2014年3月18日,这次开创性的发布在Java社区引发了不少讨论,并让大家感到激动.特性之一便是随同发布的lambda表达式,它将允许我们将行为传到函数里.在Java 8之前,如果想将行为传入函数,仅有的选择就是匿名类,需要6行代码.而定义行为最重要的那行代码,却混在中间不够突出.Lambda表达式取代了匿名类,取消了模板,允许用函数式风格编写代码.这样有时可读性更好,表达更清晰.在Java生态系统中,函数式表达与对面向对象的全面支持是个激动人心的进步.将进一步促进并行

深入学习Java8 Lambda表达式 -- 从函数式接口说起

希望本文能够成为Java8 Lambda表达式的快速入门指南. 函数式接口 理解Functional Interface(函数式接口,以下简称FI)是学习Java8 Lambda表达式的关键所在,所以放在最开始讨论.FI的定义其实很简单:任何接口,如果只包含唯一一个抽象方法,那么它就是一个FI.为了让编译器帮助我们确保一个接口满足FI的要求(也就是说有且仅有一个抽象方法),Java8提供了@FunctionalInterface注解.举个简单的例子,Runnable接口就是一个FI,下面是它的源

Java8 lambda表达式10个示例

本文由 ImportNew Java 8 刚于几周前发布,日期是2014年3月18日,这次开创性的发布在Java社区引发了不少讨论,并让大家感到激动.特性之一便是随同发布的lambda表达式,它将允许我们将行为传到函数里.在Java 8之前,如果想将行为传入函数,仅有的选择就是匿名类,需要6行代码.而定义行为最重要的那行代码,却混在中间不够突出.Lambda表达式取代了匿名类,取消了模板,允许用函数式风格编写代码.这样有时可读性更好,表达更清晰.在Java生态系统中,函数式表达与对面向对象的全面

Java8 Lambda表达式详解手册及实例

先贩卖一下焦虑,Java8发于2014年3月18日,距离现在已经快6年了,如果你对Java8的新特性还没有应用,甚至还一无所知,那你真得关注公众号"程序新视界",好好系列的学习一下Java8的新特性.Lambda表达式已经在新框架中普通使用了,如果你对Lambda还一无所知,真得认真学习一下本篇文章了. 现在进入正题Java8的Lambda,首先看一下发音 ([?l?md?])表达式.注意该词的发音,b是不发音的,da发[d?]音. 为什么要引入Lambda表达式 简单的来说,引入La

java8 lambda表达式初接触

环境是jdk8 代码如下: package memTest; /** * Created by PC on 2014/8/5. */ public class LambdaOne { public static void test(){ new Thread(()-> System.out.print("hi") ).start(); } public static void main(String[] args) { new LambdaOne().test(); } } ja

C++11 里lambda表达式的学习

最近看到很多关于C++11的文档,有些是我不怎么用到,所以就略过去了,但是lambda表达式还是比较常用的,其实最开始学习python的时候就觉得lambda这个比较高级,为什么C++这么弱.果然C++增加这个东西. 语法 [ capture ] ( params ) mutable exception attribute -> ret { body }      (1) [ capture ] ( params ) -> ret { body }                       

Java8 Lambda表达式应用案例 -- 单线程游戏服务器+异步数据库操作

前段时间我们游戏服务器的开发环境升级到了Java8,这两天我又把服务器的线程模型重新设计了一下,用上了Lambda表达式.Lambda表达式确实能够大幅简化Java代码,特别是丑陋不堪的匿名内部类,这篇文章主要就是想和大家分享这一点. 线程模型 首先简单介绍一下我们游戏服务器的线程模型,大致如下图所示: Netty线程池只处理消息的收发,当Netty收到消息之后,会交给游戏逻辑线程处理.由于是单线程在处理游戏逻辑,所以每一个消息必须很快处理完,也就是说,不能有数据库等耗时操作,不然逻辑线程很可能

Java8初体验(二)Stream语法详解

原文链接:http://ifeve.com/stream/ 1. Stream初体验 我们先来看看Java里面是怎么定义Stream的: A sequence of elements supporting sequential and parallel aggregate operations. 我们来解读一下上面的那句话: Stream是元素的集合,这点让Stream看起来用些类似Iterator: 可以支持顺序和并行的对原Stream进行汇聚的操作: 大家可以把Stream当成一个高级版本的