性能文章>JVM系列之:关于方法句柄的那些事>

JVM系列之:关于方法句柄的那些事原创

1月前
189216

前言

Java 字节码中与调用相关的指令共有五种。

  1. invokestatic:用于调用静态方法。
  2. invokespecial:用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法。
  3. invokevirtual:用于调用非私有实例方法。
  4. invokeinterface:用于调用接口方法。
  5. invokedynamic:用于调用动态方法。

invokestaticinvokespecial 是静态绑定的,invokevirtualinvokeinterface 是动态绑定的。

关于前四种指令在之前的文章中做过讲解,这四种指令与包含目标方法类名、方法名以及方法描述符的符号引用**。在实际运行之前,Java 虚拟机将根据这个符号引用链接到具体的目标方法。

接下来我们演示一个例子,动物的赛跑方式都不同,我们如何用同一种方式来调用它们的赛跑方法?

class Horse {
      
  public void race() {
      
    System.out.println("Horse.race()"); 
  }
}

class Deer {
      
  public void race() {
      
    System.out.println("Deer.race()");
  }
}

class Cobra {
      
  public void race() {
      
		System.out.println("Cobra.race()");
  }
}

根据要求,我们有两种解决方法:一是抽象 race()方法或者使用适配器;二是利用反射机制。

简单演示一下。

public interface Animal {
      
  void race();
}

public class Horse implements Animal {
      

  @Override
  public void race() {
      
    System.out.println("Horse.race()");
  }
}

.....
// 利用接口抽象方法
  public static void main(String[] args) throws Exception {
      
    Animal animal = new Horse();
    animal.race();
  }
// 反射调用
  public static void main(String[] args) throws Exception {
      
    Horse object = new Horse();
    Method method = object.getClass().getMethod("race");
    method.invoke(object);
  }

上述两个方案,转换为字节码文件,可以发现还是与那四条指令相关,需要类名、方法名和方法描述符,最后才能完成方法的调用,那么还有其他的方式可以实现吗?

Java 7 引入了一条新的指令 invokedynamic。该指令的调用机制抽象出调用点这一个概念,并允许应用程序将调用点链接至任意符合条件的方法上。为此引入了一个新的概念:方法句柄(MethodHandle)

声明:如果没有特殊说明,本文代码皆是基于 JDK8 执行的。

 

方法句柄的概念

方法句柄是一个强类型的,能够被直接执行的引用。该引用可以指向常规的静态方法或者实例方法,也可以指向构造器或者字段。当指向字段时,方法句柄实则指向包含字段访问字节码的虚构方法,语义上等价于目标字段的 getter 或者 setter 方法。

方法句柄的类型(MethodType)是由所指向方法的参数类型以及返回类型组成的。它是用来确认方法句柄是否适配的唯一关键。当使用方法句柄时,我们其实并不关心方法句柄所指向方法的类名或者方法名

创建

方法句柄的创建是通过 MethodHandles.Lookup 类来完成的。它提供了多个 API,既可以使用反射 API 中的 Method 来查找,也可以根据类、方法名以及方法句柄类型来查找。

MethodHandle 的创建方式:

方式一、通过反射创建 MethodHandle

public class Cobra {
      
  public static void race() {
      
  }

  public static Lookup lookup() {
      
    return MethodHandles.lookup();
  }
}

MethodHandles.Lookup lookup = Cobra.lookup();
Method method = Cobra.class.getDeclaredMethod("race");
MethodHandle methodHandle = lookup.unreflect(method);

方式二、根据 MethodType 创建 MethodHandle

public class Cobra {
      
  public static void race() {
      
  }

  public void say(){
      
    System.out.println("say");
  }

  public static Lookup lookup() {
      
    return MethodHandles.lookup();
  }
}

public class RaceTest {
      

  public static void main(String[] args) throws Throwable {
      
    MethodHandles.Lookup lookup = Cobra.lookup();
    MethodType methodType = MethodType.methodType(void.class);
    MethodHandle methodHandle = lookup.findStatic(Cobra.class, "race", methodType);

    MethodHandle methodHandle2 = lookup.findVirtual(Cobra.class, "say", methodType);
  }
}

通过上述演示,我们发现当使用方式二时,用户需要区分具体的调用类型,比如说对于用 invokestatic 调用的静态方法,我们需要使用 Lookup.findStatic 方法;对于用 invokevirtual 调用的实例方法,以及用 invokeinterface 调用的接口方法,我们需要使用 findVirtual 方法;对于用 invokespecial 调用的实例方法,我们则需要使用 findSpecial 方法。

几个 MethodHandle 方法与字节码的对应:

调用方法句柄,和原本对应的调用指令是一致的。也就是说,对于原本用 invokevirtual 调用的方法句柄,它也会采用动态绑定;而对于原本用 invokespecial 调用的方法句柄,它会采用静态绑定。

权限

方法句柄同样也有权限问题。但它与反射 API 不同,其权限检查是在句柄的创建阶段完成的。

在实际调用过程中,Java 虚拟机并不会检查方法句柄的权限。如果该句柄被多次调用的话,那么与反射调用相比,它将省下重复权限检查的开销。需要注意的是,方法句柄的访问权限不取决于方法句柄的创建位置,而是取决于 Lookup 对象的创建位置。

如下案例所示,我们在 RaceTest 类中想要访问 Cobra 类的私有方法,提示权限错误。

public class Cobra {
      
  private void eat(){
      
    System.out.println("eat");
  }
}

public class RaceTest {
      
  public static void main(String[] args) throws Throwable {
      
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    MethodType methodType = MethodType.methodType(void.class);
    MethodHandle methodHandle = lookup.findSpecial(Cobra.class, "eat", methodType,Cobra.class);
  }
}

执行上述代码报错,错误信息如下:

Exception in thread "main" java.lang.IllegalAccessException: no private access for invokespecial: class com.msdn.java.hotspot.invokedynamic.Cobra, from com.msdn.java.hotspot.invokedynamic.RaceTest

上述代码是针对私有方法,接下来我们试着对私有字段操作一番。对于一个私有字段,如果 Lookup 对象是在私有字段所在类中获取的,那么这个 Lookup 对象便拥有对该私有字段的访问权限。但如果在其他类中,按理说也能够通过该 Lookup 对象创建该私有字段的 getter 或者 setter,具体来说是通过 MethodHandles.Lookup 的 findGetter 和 findSetter 方法来实现。

首先我们测试一下在其他类中创建 Lookup 对象,看看是否可以操控私有字段。

public class Cobra {
      

  private String name;
}

public class RaceTest {
      
  public static void main(String[] args) throws Throwable {
      
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    MethodHandle methodHandle = lookup.findSetter(Cobra.class, "name", String.class);
    Cobra cobra = new Cobra();
    methodHandle.invokeExact(cobra,"111");
  }
}

上述代码执行报错:

Exception in thread "main" java.lang.IllegalAccessException: member is private: com.msdn.java.hotspot.invokedynamic.Cobra.name/java.lang.String/putField, from com.msdn.java.hotspot.invokedynamic.RaceTest

如果 Lookup 对象在 Cobra 类中创建,是否就可以操控私有字段了呢?

public class Cobra {
      

  private String name;

  public static Lookup lookup() {
      
    return MethodHandles.lookup();
  }
}

public class RaceTest {
      
  public static void main(String[] args) throws Throwable {
      
    MethodHandles.Lookup lookup = Cobra.lookup();
    MethodHandle methodHandle = lookup.findSetter(Cobra.class, "name", String.class);
    Cobra cobra = new Cobra();
    methodHandle.invokeExact(cobra,"111");
    MethodHandle getterMethodHandle = lookup.findGetter(Cobra.class, "name", String.class);
    System.out.println(getterMethodHandle.invoke(cobra));
  }
}

外类中操控私有字段

因为权限问题,外类中创建的 Lookup 对象无法操控私有字段,那么有什么替代方案呢?

方案一:既然无法通过 findGetter 和 findSetter 方法来操控 Cobra 类中的 name 字段,只能先手动在 Cobra 类中构建 getter 和 setter 方法,我们看下面的案例:

public class Cobra {
      

  private String name;

  public String getName() {
      
    return name;
  }

  public void setName(String name) {
      
    this.name = name;
  }
}


public class RaceTest {
      
  public static void main(String[] args) throws Throwable {
      
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    Cobra cobra = new Cobra();
    MethodHandle getNameMethodHandle = lookup
      .findVirtual(Cobra.class, "getName", MethodType.methodType(String.class));
    MethodHandle setNameMethodHandle = lookup
      .findVirtual(Cobra.class, "setName", MethodType.methodType(void.class, String.class));
    setNameMethodHandle.invokeExact(cobra,"123");
    System.out.println((String)getNameMethodHandle.invokeExact(cobra));
  }
}

虽然上述方案可以解决操控其他类的私有字段,但是我们构建了 getter 和 setter 方法,如果没有它们还可以怎么做?是否可以通过反射来实现呢,说干就干。

方案二:

public class Cobra {
      

  private String name;
}

public class RaceTest {
      
  public static void main(String[] args) throws Throwable {
      
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    Cobra cobra = new Cobra();
    Field field = Cobra.class.getDeclaredField("name");
    field.setAccessible(true);
    MethodHandle methodHandle = lookup.unreflectSetter(field);
    methodHandle.invokeExact(cobra, "123");
    methodHandle = lookup.unreflectGetter(field);
    System.out.println((String) methodHandle.invokeExact(cobra));
  }
}

如果注释 setAccessible 方法,仍然会报错,但是如果加上这行代码,那就与反射的权限有关了,这不是我们想要得到的结果。

JDK9的权限变化

偶然情况下,发现了 JDK9 环境下 MethodHandles 类实现有所不同,其中就包括针对私有属性的处理手段。

public class Cobra {
      

  private String name;
}


public class RaceTest {
      
  public static void main(String[] args) throws Throwable {
      
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    Cobra cobra = new Cobra();
    Lookup privateLookupIn = MethodHandles.privateLookupIn(Cobra.class, lookup);
    MethodHandle methodHandle = privateLookupIn.findSetter(Cobra.class, "name", String.class);
    methodHandle.invoke(cobra,"124");
    MethodHandle getterMethodHandle = privateLookupIn.findGetter(Cobra.class, "name", String.class);
    System.out.println(getterMethodHandle.invoke(cobra));
  }
}

根据官方文档可知,JDK9 中 MethodHandles 引入了一个新的方法 privateLookupIn(Class<?> targetClass, MethodHandles.Lookup lookup),该方法具有在目标类上lookup object模拟所有支持的字节码行为(包括 私有访问)的完整功能。

小结一下:

1、方法句柄的访问权限不取决于方法句柄的创建位置,而是取决于 Lookup 对象的创建位置。

2、如果 JDK 版本大于8,那么可以在其他类中,也能够通过该 Lookup 对象对类私有属性进行赋值、取值操作。

需要注意的是,JDK9 中权限更大了,也存在一些隐患。由于方法句柄没有运行时权限检查,因此,应用程序需要负责方法句柄的管理。一旦它发布了某些指向私有方法的方法句柄,那么这些私有方法便被暴露出去了。如下述案例所示:

public class Cobra {
      

  private void money(){
      
    System.out.println("money");
  }
}

public class RaceTest {
      
  public static void main(String[] args) throws Throwable {
      
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    Cobra cobra = new Cobra();
    Lookup privateLookupIn = MethodHandles.privateLookupIn(Cobra.class, lookup);

    MethodHandle methodHandle = privateLookupIn
      .findSpecial(Cobra.class, "money", MethodType.methodType(void.class),
                   Cobra.class);
    methodHandle.invoke(cobra);
  }
}

 

方法句柄的操作

方法句柄的调用可分为两种,一是需要严格匹配参数类型的 invokeExact。它有多严格呢?假设一个方法句柄将接收一个 Object 类型的参数,如果你直接传入 String 作为实际参数,那么方法句柄的调用会在运行时抛出方法类型不匹配的异常。正确的调用方式是将该 String 显式转化为 Object 类型。

invokeExact 位于 MethodHandle 文件中

public final native @PolymorphicSignature Object invokeExact(Object... args) throws Throwable;

方法句柄 API 有一个特殊的注解类 @PolymorphicSignature。在碰到被它注解的方法调用时,Java 编译器会根据所传入参数的声明类型来生成方法描述符,而不是采用目标方法所声明的描述符。

如下示例:

public class Cobra {
      

  public void read(Object object){
      
  }
}


public class RaceTest {
      
  public static void main(String[] args) throws Throwable {
      
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    Cobra cobra = new Cobra();
    MethodHandle methodHandle = lookup
        .findVirtual(Cobra.class, "read", MethodType.methodType(void.class, Object.class));
    methodHandle.invokeExact(cobra,"123");
    methodHandle.invokeExact(cobra,(Object) "456");
  }
}

//编译RaceTest,查看其字节码
        13: ldc           #3                  // class com/msdn/java/hotspot/invokedynamic/Cobra
        15: ldc           #5                  // String read
        17: getstatic     #6                  // Field java/lang/Void.TYPE:Ljava/lang/Class;
        20: ldc           #7                  // class java/lang/Object
        22: invokestatic  #8                  // Method java/lang/invoke/MethodType.methodType:(Ljava/lang/Class;Ljava/lang/Class;)Ljava/lang/invoke/MethodType;
        25: invokevirtual #9                  // Method java/lang/invoke/MethodHandles$Lookup.findVirtual:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;
        28: astore_3
        29: aload_3
        30: ldc           #10                 // String 123
        32: invokevirtual #11                 // Method java/lang/invoke/MethodHandle.invokeExact:(Ljava/lang/String;)V
        35: aload_3
        36: ldc           #12                 // String 456
        38: invokevirtual #13                 // Method java/lang/invoke/MethodHandle.invokeExact:(Ljava/lang/Object;)V
        41: return

当然,上述代码执行会报错,错误提示如下:

Exception in thread "main" java.lang.invoke.WrongMethodTypeException: expected (Cobra,Object)void but found (String)void

如果你需要自动适配参数类型,那么你可以选取方法句柄的第二种调用方式 invoke。它同样是一个签名多态性的方法。invoke 会调用 MethodHandle.asType 方法,生成一个适配器方法句柄,对传入的参数进行适配,再调用原方法句柄。调用原方法句柄的返回值同样也会先进行适配,然后再返回给调用者。

MethodHandle methodHandle = lookup
.findVirtual(Cobra.class, "read", MethodType.methodType(void.class, Object.class));
methodHandle.invoke(cobra,"123");

MethodHandle.asType

方法句柄还支持增删改参数的操作,这些操作都是通过生成另一个方法句柄来实现的。这其中,改操作就是刚刚介绍的 MethodHandle.asType 方法。

MethodHandle methodHandle = lookup
  .findVirtual(Cobra.class, "read", MethodType.methodType(void.class, Object.class));
MethodHandle methodHandle1 = methodHandle
  .asType(methodHandle.type().changeParameterType(1,String.class));
methodHandle1.invokeExact(cobra,"123");

MethodHandles.dropArguments

dropArguments可以在一个方法句柄的参数中添加一些无用的参数。这些参数虽然在实际调用时不会被使用,但是它们可以使变换之后的方法句柄的参数类型格式符合某些所需的特定模式。

    MethodHandle methodHandle = lookup
        .findVirtual(Cobra.class, "read", MethodType.methodType(void.class, Object.class));
    MethodHandle methodHandle2 = MethodHandles.dropArguments(methodHandle, 1, String.class);
    methodHandle2.invokeExact(cobra,"123",(Object)"456");

read 方法本来只有一个 Object 类型的参数,我们使用 dropArguments 增加了一个 String 类型的参数,如何查看方法的最新参数列表呢?

methodHandle2.invokeExact(cobra);

执行上述语句会报错,错误提示为:

Exception in thread "main" java.lang.invoke.WrongMethodTypeException: expected (Cobra,String,Object)void but found (Cobra)void

MethodHandles.insertArguments

这个方法可以同时为方法句柄中的多个参数预先绑定具体的值。在得到的新方法句柄中,已经绑定了具体值的参数不再需要提供,也不会出现在参数列表中。

public void read(Object object) {
      
  System.out.println("read" + object);
}

MethodHandle methodHandle = lookup
  .findVirtual(Cobra.class, "read", MethodType.methodType(void.class, Object.class));
MethodHandle methodHandle1 = MethodHandles.insertArguments(methodHandle, 1, "999");
methodHandle1.invokeExact(cobra);//输出结果为 read999

MethodHandle.bindTo

通过 MethodHandle 的 bindTo 方法可以预先绑定底层方法的调用接收者,在实际调用的时候,只需要传入实际参数即可,不需要再指定方法的接收者。

public void bindTo()throws Throwable{
      
  MethodHandles.Lookup lookup=MethodHandles.lookup();
   //MethodHandle.bindTo使用
  MethodHandle methodHandle = lookup
    .findVirtual(String.class, "length", MethodType.methodType(int.class));
  int len = (int) methodHandle.invoke("java");
  System.out.println(len);
  methodHandle = methodHandle.bindTo("hello java");
  len = (int) methodHandle.invoke();
  System.out.println(len);
}

可以看出

  • 第一种没有进行绑定,调用时需要传入length方法的接收者;
  • 第二种方法预先绑定了一个String类的对象,因此调用时不需要再指定。

关于方法句柄的操作还有很多内容,这里不做过多介绍了,推荐阅读java方法句柄-----1.方法句柄类型、调用

 

方法句柄的实现

前面提到,调用方法句柄所使用的 invokeExact 或者 invoke 方法具备签名多态性的特性。它们会根据具体的传入参数来生成方法描述符。那么,拥有这个描述符的方法实际存在吗?对 invokeExact 或者 invoke 的调用具体会进入哪个方法呢?

//JDK9
public class CallPathTest {

  public static void bar(Object o) {
    new Exception().printStackTrace();
  }

  public static void main(String[] args) throws Throwable {
    MethodHandles.Lookup l = MethodHandles.lookup();
    MethodType t = MethodType
        .methodType(void.class, Object.class);
    MethodHandle mh = l.findStatic(CallPathTest.class, "bar", t);
    mh.invokeExact(new Object());
  }

}

和查阅反射调用的方式一样,我们可以通过新建异常实例来查看栈轨迹。打印出来的占轨迹如下所示:

java.lang.Exception
	at com.msdn.java.hotspot.invokedynamic.CallPathTest.bar(CallPathTest.java:15)
	at com.msdn.java.hotspot.invokedynamic.CallPathTest.main(CallPathTest.java:23)

也就是说,invokeExact 的目标方法竟然就是方法句柄指向的方法。

执行目标方法前需要校验方法句柄的类型,我们可以设置 JVM 参数来输出更多的信息。参数为:

-XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames

输出结果如下:(JDK8和JDK9输出内容不同)

//JDK9
java.lang.Exception
	at com.msdn.java.hotspot.invokedynamic.CallPathTest.bar(CallPathTest.java:15)
	at java.base/java.lang.invoke.DirectMethodHandle$Holder.invokeStatic(DirectMethodHandle$Holder:1000010)
	at java.base/java.lang.invoke.LambdaForm$MH000/1146848448.invokeExact_000_MT(LambdaForm$MH000:1000019)
	at com.msdn.java.hotspot.invokedynamic.CallPathTest.main(CallPathTest.java:23)
	
//JDK8
java.lang.Exception
	at com.msdn.java.hotspot.invokedynamic.CallPathTest.bar(CallPathTest.java:15)
	at java.lang.invoke.LambdaForm$DMH001/1950409828.invokeStatic_001_L_V(LambdaForm$DMH001:1000010)
	at java.lang.invoke.LambdaForm$MH012/1067040082.invokeExact_000_MT(LambdaForm$MH012:1000016)
	at com.msdn.java.hotspot.invokedynamic.CallPathTest.main(CallPathTest.java:26)

环境不同,最终的执行链路有所不同,下文我们将在 JDK9 环境下进行代码演示,最后会额外讲述 JDK8 的调用链路。

接下来我们就要想办法来获取对应的 class 文件,首先是 LambdaForm(后续我们称之为适配器) 相关的 class 文件,可以通过添加虚拟机参数将之导出成 class 文件(-Djava.lang.invoke.MethodHandle.DUMP_CLASS_FILES=true)。

得到的 class 文件内容如下:

final class LambdaForm$MH000 {
      
  @Hidden
  @Compiled
  @ForceInline
  static void invokeExact_000_MT(Object var0, Object var1, Object var2) {
      
    MethodHandle var3;
    Invokers.checkExactType(var3 = (MethodHandle)var0, (MethodType)var2);
    Invokers.checkCustomized(var3);
    var3.invokeBasic(var1);
  }

  static void dummy() {
      
    String var10000 = "MH.invokeExact_000_MT=Lambda(a0:L,a1:L,a2:L)=>{\n t3:V=Invokers.checkExactType(a0:L,a2:L);\n t4:V=Invokers.checkCustomized(a0:L);\n t5:V=MethodHandle.invokeBasic(a0:L,a1:L);void}";
  }
}

可以看到,在这个适配器中,它会调用 Invokers.checkExactType 方法来检查参数类型,然后调用 Invokers.checkCustomized 方法。深入查看其源码可知,在方法句柄的执行次数超过一个阈值时进行优化(对应参数 -Djava.lang.invoke.MethodHandle.CUSTOMIZE_THRESHOLD,默认值为 127)。

    static final int CUSTOMIZE_THRESHOLD;
    CUSTOMIZE_THRESHOLD = Integer.parseInt(
                props.getProperty("java.lang.invoke.MethodHandle.CUSTOMIZE_THRESHOLD", "127"));
    
    
    @DontInline
    void maybeCustomize(MethodHandle mh) {
      
        byte count = mh.customizationCount;
        if (count >= CUSTOMIZE_THRESHOLD) {
      
            mh.customize();
        } else {
      
            mh.customizationCount = (byte)(count+1);
        }
    }

最后,它会调用方法句柄的 invokeBasic 方法。

如果循环调用 invokeExact 超过 127次,则会发现其调用链路会发生变化。

最后调用的 invokeBasic 方法也是一个本地方法,我们尝试通过 HSDB 来查找 DirectMethodHandle$Holder 文件。

在反射一节中,我们也遇到过 inflationThreshold 阈值,如果超过该阈值,则会生成一个 Java 字节码文件,相较于 native 版本,反射调用效率会有所提升。那么关于 CUSTOMIZE_THRESHOLD 阈值的优化是怎么回事?接下来通过调试来感受这快程序的精妙之处。

1、默认情况下,CUSTOMIZE_THRESHOLD=127,执行 checkCustomized 方法可以看到 mh.form——适配器的入口就是 DirectMethodHandle$Holder。

2、设置 CUSTOMIZE_THRESHOLD=0,执行 checkCustomized 方法时 mh.form 和上面截图内容一致,不过因为 count >= CUSTOMIZE_THRESHOLD 条件为 true,进而执行 customize 方法。

    void customize() {
      
        if (form.customized == null) {
      
            LambdaForm newForm = form.customize(this);
            updateForm(newForm);
        } else {
      
            assert(form.customized == this);
        }
    }

可以看到,在该方法中会创建一个新的 LambdaForm 对象,接着执行 updateForm 方法,将新的 LambdaForm 对象更换到内存中。

    void updateForm(LambdaForm newForm) {
        assert(newForm.customized == null || newForm.customized == this);
        if (form == newForm)  return;
        newForm.prepare();  // as in MethodHandle.<init>
        UNSAFE.putObject(this, FORM_OFFSET, newForm);
        UNSAFE.fullFence();
    }

下次再执行 invokeExact 方法时,进入到 checkCustomized 方法时因为 mh.form.customized 不为 null 会直接退出。

上述只介绍了 LambdaForm 适配器有所不同,那么优化到底在哪体现的呢?基于我们上述的测试代码,分别查看默认的 DirectMethodHandle H o l d e r . c l a s s 和 达 到 阈 值 后 生 成 的 L a m b d a F o r m Holder.class 和达到阈值后生成的 LambdaForm Holder.classLambdaFormDMH000.class(该文件通过 DUMP_CLASS_FILES参数可以生成),比较 invokeStatic 方法有何不同。

//DirectMethodHandle$Holder
  @Hidden
  @Compiled
  @ForceInline
  static Object invokeStatic(Object var0, Object var1) {
      
    Object var2 = DirectMethodHandle.internalMemberName(var0);
    return MethodHandle.linkToStatic(var1, (MemberName)var2);
  }
  
//LambdaForm$DMH000
  @Hidden
  @Compiled
  @ForceInline
  static void invokeStatic_001_L_V(Object var0, Object var1) {
      
    MethodHandle var3 = (MethodHandle)"CONSTANT_PLACEHOLDER_1 <<com.msdn.java.hotspot.invokedynamic.CallPathTest.bar(Object)void/invokeStatic>>";
    Object var2 = DirectMethodHandle.internalMemberName(var3);
    MethodHandle.linkToStatic(var1, (MemberName)var2);
  }

可以看出,DirectMethodHandle 会调用 internalMemberName 方法获取 MemberName 类型的字段,并继续后面的 linkToStatic 调用。

差异之处:LambdaForm$DMH000 文件的 invokeStatic 方法中,DirectMethodHandle 会将方法句柄作为常量,最终得到 MemberName 类型的字段。

综上所述,当方法句柄 MethodHandle 被多次调用之后,Invokers.checkCustomized 方法会为该方法句柄生成一个特有的适配器。这个特有的适配器会将方法句柄作为常量,直接获取其 MemberName 类型的字段,并继续后面的 linkToStatic 调用。

 

扩展

讲完上述优化之后,我们回头看一下 JDK8 的调用链路。

// JDK8,JVM参数为:-XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames -Djava.lang.invoke.MethodHandle.DUMP_CLASS_FILES=true

java.lang.Exception
	at com.msdn.java.hotspot.invokedynamic.CallPathTest.bar(CallPathTest.java:15)
	at java.lang.invoke.LambdaForm$DMH001/1950409828.invokeStatic_001_L_V(LambdaForm$DMH001:1000010)
	at java.lang.invoke.LambdaForm$MH012/1067040082.invokeExact_000_MT(LambdaForm$MH012:1000016)
	at com.msdn.java.hotspot.invokedynamic.CallPathTest.main(CallPathTest.java:26)

最终我们得到了很多份 LambdaForm 前缀的 class 文件。

根据上文的分析,优化前应该是使用共享的适配器,可是这里为什么没有呢?

我们先来看看使用到的 class 文件内容:

final class LambdaForm$DMH001 {
      
  @Hidden
  @Compiled
  @ForceInline
  static void invokeStatic_001_L_V(Object var0, Object var1) {
      
    Object var2 = DirectMethodHandle.internalMemberName(var0);
    MethodHandle.linkToStatic(var1, (MemberName)var2);
  }

  static void dummy() {
      
    String var10000 = "DMH.invokeStatic_001_L_V=Lambda(a0:L,a1:L)=>{\n t2:L=DirectMethodHandle.internalMemberName(a0:L);\n t3:V=MethodHandle.linkToStatic(a1:L,t2:L);void}";
  }
}

final class LambdaForm$MH012 {
      
  @Hidden
  @Compiled
  @ForceInline
  static void invokeExact_000_MT(Object var0, Object var1, Object var2) {
      
    Invokers.checkExactType(var0, var2);
    Invokers.checkCustomized(var0);
    MethodHandle var3;
    (var3 = (MethodHandle)var0).invokeBasic(var1);
  }

  static void dummy() {
      
    String var10000 = "invokeExact_000_MT=Lambda(a0:L,a1:L,a2:L)=>{\n t3:V=Invokers.checkExactType(a0:L,a2:L);\n t4:V=Invokers.checkCustomized(a0:L);\n t5:V=MethodHandle.invokeBasic(a0:L,a1:L);void}";
  }
}

通过调试可知,优化前使用的适配器不是 DirectMethodHandle$Holder,但是查看 invokeStatic_001_L_V 方法发现也并没有什么特殊之处,我们尝试改一下 CUSTOMIZE_THRESHOLD 的值为0,然后看一下优化后生成的适配器。

查看该文件内容如下:

final class LambdaForm$DMH009 {
      
  @Hidden
  @Compiled
  @ForceInline
  static void invokeStatic_007_L_V(Object var0, Object var1) {
      
    MethodHandle var3 = (MethodHandle)"CONSTANT_PLACEHOLDER_0 <<com.msdn.java.hotspot.invokedynamic.CallPathTest.bar(Object)void/invokeStatic>>";
    Object var2 = DirectMethodHandle.internalMemberName(var3);
    MethodHandle.linkToStatic(var1, (MemberName)var2);
  }

  static void dummy() {
      
    String var10000 = "DMH.invokeStatic_007_L_V=Lambda(a0:L,a1:L)=>{\n t2:L=DirectMethodHandle.internalMemberName(a0:L);\n t3:V=MethodHandle.linkToStatic(a1:L,t2:L);void}";
  }
}

内容和 JDK9 版本下优化后的内容一样,都会将方法句柄变为常量。这样看来 JDK8和 JDK9 最终的执行效果是一致的,至于为何调用链路有所不同,这点就不可知了。

 

JVM系列阅读

JVM系列之:你真的了解垃圾回收吗

JVM系列之:JVM是如何创建对象的

JVM系列之:JVM是怎么实现invokedynamic的?

JVM系列之:关于方法句柄的那些事

JVM系列之:关于HSDB的一点心得

JVM系列之:JVM是如何实现反射的

JVM系列之:关于JVM类加载的那些事

JVM系列之:聊一聊Java异常

JVM系列之:JVM如何执行方法调用

JVM系列之:聊聊Java的数据类型

JVM系列之:宏观分析Java代码是如何执行的

参考文献

JAVA基础知识之反射

java方法句柄-----2.方法句柄的获取、变换、特殊方法句柄

极客时间《深入拆解Java虚拟机》 郑雨迪

请先登录,再评论

👍

1月前

为你推荐

字符串字面量长度是有限制的
前言 偶然在一次单元测试中写了一个非常长的字符串字面量。 正文 在一次单元测试中,我写了一个很长的字符串字面量,大概10万个字符左右,编译时,编译器给出了异常告警 `java: constant
多次字符串相加一定要用StringBuilder而不用-吗?
今天在写一个读取Java class File并进行分析的Demo时,偶然发现了下面这个场景(基于oracle jdk 1.8.0_144): ``` package test; public c
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得
高吞吐、低延迟 Java 应用的 GC 优化实践
本篇原文作者是 LinkedIn 的 Swapnil Ghike,这篇文章讲述了 LinkedIn 的 Feed 产品的 GC 优化过程,虽然文章写作于 April 8, 2014,但其中的很多内容和
「每日五分钟,玩转 JVM」:久识你名,初居我心
聊聊 JVMJVM,一个熟悉又陌生的名词,从认识Java的第一天起,我们就会听到这个名字,在参加工作的前一两年,面试的时候还会经常被问到JDK,JRE,JVM这三者的区别。JVM可以说和我们是老朋友了
据说99.99%的人都会答错的类加载的问题
概述首先还是把问题抛给大家,这个问题也是我厂同学在做一个性能分析产品的时候碰到的一个问题。 同一个类加载器对象是否可以加载同一个类文件多次并且得到多个Class对象而都可以被java层使用吗请仔细注意
Java多线程——并发测试
编写并发程序时候,可以采取和串行程序相同的编程方式。唯一的难点在于,并发程序存在不确定性,这种不确定性会令程序出错的地方远比串行程序多,出现的方式也没有固定规则。那么如何在测试中,尽可能的暴露出这些问
Java多线程知识小抄集(一)
本文主要整理笔者遇到的Java多线程的相关知识点,适合速记,故命名为“小抄集”。本文没有特别重点,每一项针对一个多线程知识做一个概要性总结,也有一些会带一点例子,习题方便理解和记忆。 1.interr