Java反射 与 Lambda 函数映射

一、用「 Lambda 生成函数映射」代替「高频的反射操作」

我们来看一段最简单的反射执行代码:

public class Bean {  
  int id;    
  public int getId() {     
    return id;  
  }
}

Method methodGetId = Bean.class.getMethod("getId");
Bean bean = createInstance();
int value = (Integer) methodGetId.invoke(bean);

上面的反射执行代码可以被改写成这样:

// 将getId()映射为function函数,这里需要对操作目标已知
ToIntFunction<Bean> function = Bean::getId; 
int i = function.applyAsInt(bean);

fastjson2 中的具体实现的要复杂一点,但本质上跟上面一样,其本质也是生成了一个 function。

//function,这种相对好一些,虽然复杂,看看源码也能明白一些
/**
    Facilitates the creation of simple "function objects" that implement one or more interfaces by delegation to a provided MethodHandle, after appropriate type adaptation and partial evaluation of arguments. Typically used as a bootstrap method for invokedynamic call sites, to support the lambda expression and method reference expression features of the Java Programming Language.
    This is the standard, streamlined metafactory; additional flexibility is provided by altMetafactory(MethodHandles.Lookup, String, MethodType, Object...). A general description of the behavior of this method is provided above.
    When the target of the CallSite returned from this method is invoked, the resulting function objects are instances of a class which implements the interface named by the return type of factoryType, declares a method with the name given by interfaceMethodName and the signature given by interfaceMethodType. It may also override additional methods from Object.

Params:
    caller – Represents a lookup context with the accessibility privileges of the caller. Specifically, the lookup context must have full privilege access. When used with invokedynamic, this is stacked automatically by the VM. 
    interfaceMethodName – The name of the method to implement. When used with invokedynamic, this is provided by the NameAndType of the InvokeDynamic structure and is stacked automatically by the VM. 
    factoryType – The expected signature of the CallSite. The parameter types represent the types of capture variables; the return type is the interface to implement. When used with invokedynamic, this is provided by the NameAndType of the InvokeDynamic structure and is stacked automatically by the VM. 
    interfaceMethodType – Signature and return type of method to be implemented by the function object. 
    implementation – A direct method handle describing the implementation method which should be called (with suitable adaptation of argument types and return types, and with captured arguments prepended to the invocation arguments) at invocation time. 
    dynamicMethodType – The signature and return type that should be enforced dynamically at invocation time. In simple use cases this is the same as interfaceMethodType.

Returns:
    a CallSite whose target can be used to perform capture, generating instances of the interface named by factoryType

Throws:
    LambdaConversionException – If caller does not have full privilege access, or if interfaceMethodName is not a valid JVM method name, or if the return type of factoryType is not an interface, or if implementation is not a direct method handle referencing a method or constructor, or if the linkage invariants are violated, as defined above.
    NullPointerException – If any argument is null.
    SecurityException – If a security manager is present, and it refuses access from caller to the package of implementation.
  */

CallSite callSite = LambdaMetafactory.metafactory(
              lookup,
              "applyAsInt",
 	      methodType(ToIntFunction.class),
              methodType(int.class, Object.class),
              lookup.findVirtual(Bean.class, "getId", methodType(int.class)),
              methodType(int.class, Bean.class));
//var invoke = callSite.getTarget().invoke(); // error
//var invokeExact = callSite.getTarget().invokeExact(); /error
ToIntFunction<Bean> function = (ToIntFunction<Bean>) callSite.getTarget().invokeExact();
int i = function.applyAsInt(bean);


// 使用findVirtual也能实现,还简单,不清楚与LambdaMetafactory相交的适用场景,看了下面的原理后,感觉findVirtual本质上还像反射,而LambdaMetafactory采用动态生成函数的方式,底层是不一样的。但测试下来,findVirtual比LambdaMetafactory快多了,但当函数复用之后,两者的耗时都大幅减少,且LambdaMetafactory更快一些。复用对后者的影响特别大4s -> 28ms,而前者相交就没这么离谱 290ms -> 38ms
@FunctionalInterface
public interface MultiFunction {
    Integer runFun(Function<String, Integer> function, String str);
}

    @Test
    void testLambdaParams() throws Throwable {

        MultiFunction fun = Function::apply;
       // 直接调用的方式
        var i = fun.runFun(Integer::valueOf, "2");
       
       // 使用findVirtual
        var lookup = MethodHandles.lookup();
        var runFun = lookup.findVirtual(MultiFunction.class, "runFun", MethodType.methodType(Integer.class, Function.class, String.class));
       // runFun.invoke(fun, Integer::valueOf, "3"); // 编译error: Object is not a functional interface
        Function<String, Integer> innerFun = Integer::valueOf; // 虽然直接不行,但可以封装成Function,然后就可以了
        var i2 = runFun.invoke(fun, innerFun, "3"); // 所以findVirtual也可以应对参数包含lambda的method

       // 使用LambdaMetafactory
        var multiFunction = (MultiFunction) LambdaMetafactory.metafactory(
                lookup,
                "runFun", // interfaceMethodName
                MethodType.methodType(MultiFunction.class), // factoryType
                MethodType.methodType(Integer.class, Function.class, String.class), // interfaceMethodType
                lookup.findVirtual(Function.class, "apply", MethodType.methodType(Object.class, Object.class)), // implementation
                MethodType.methodType(Integer.class, Function.class, String.class) // dynamicMethodType
        ).getTarget().invokeExact();
        var i4 = multiFunction.runFun(Integer::valueOf, "4");

    }

关于MethodHandle: A method handle is a typed, directly executable reference to an underlying method, constructor, field, or similar low-level operation, with optional transformations of arguments or return values. These transformations are quite general, and include such patterns as conversion, insertion, deletion, and substitution.

关于VarHandle: A VarHandle is a dynamically strongly typed reference to a variable, or to a parametrically-defined family of variables, including static fields, non-static fields, array elements, or components of an off-heap data structure. Access to such variables is supported under various access modes, including plain read/write access, volatile read/write access, and compare-and-set.
VarHandles are immutable and have no visible state. VarHandles cannot be subclassed by the user.

关于CallSite: A CallSite is a holder for a variable MethodHandle, which is called its target. An invokedynamic instruction linked to a CallSite delegates all calls to the site's current target. A CallSite may be associated with several invokedynamic instructions, or it may be "free floating", associated with none. In any case, it may be invoked through an associated method handle called its dynamic invoker.

关于LambdaMetaFactory: Methods to facilitate the creation of simple "function objects" that implement one or more interfaces by delegation to a provided MethodHandle, possibly after type adaptation and partial evaluation of arguments. These methods are typically used as bootstrap methods for invokedynamic call sites, to support the lambda expression and method reference expression features of the Java Programming Language.

我们使用反射获取到的 Method 和 Lambda 函数分别执行 10000 次来看下处理速度差异:

Method invoke elapsed: 25ms
Bean::getId elapsed: 1ms

处理速度相差居然达到 25 倍,使用 Java8 Lambda 为什么能提升这多呢?

答案就是:Lambda 利用 LambdaMetafactory 生成了函数映射代替反射。

下面我们详细分析下 Java反射 与 Lambda 函数映射 的底层区别。

1、反射执行的底层原理

注:以下只是想表达出反射调用本身的繁杂性,大可不必深究这些代码细节

如果要一句话解释的话,那就是通过元数据找到对应的方法,检查系统状态和反射参数没问题后就执行那个方法。

也就是说,正常执行一个方法的路径:

java代码 => class字节码 => JVM解析字节码执行类加载过程 => 类加载获得元数据,JVM执行到对应的方法时根据元数据找到对应的方法开始执行

而反射执行的路径:

java代码 => class字节码 => JVM解析字节码执行类加载过程 => JVM执行到反射部分的代码 => 通过反射系统拿到对应方法的元数据 => 检查系统状态和参数可以执行这个方法 => 和正常JVM执行方法一样执行这个方法。

从代码角度,我们从 Java 方法反射 Method.invoke 的源码入口来深入:

public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException,InvocationTargetException{   
  if (!override) {     
    if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {       
      Class<?> caller = Reflection.getCallerClass();       
      checkAccess(caller, clazz, obj, modifiers);      
    }  
  }  
  MethodAccessor ma = methodAccessor;// read volatile   
  if (ma == null) ma = acquireMethodAccessor();  
  return ma.invoke(obj, args);
}

可见,经过简单的检查后(检查是一个优化点,因为这个是每次调用都检查的),调用的是MethodAccessor.invoke(),这部分的实际实现:

public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {   
  if (++this.numInvocations > ReflectionFactory.inflationThreshold() && 
      !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {        
    MethodAccessorImpl var3 = 
      (MethodAccessorImpl)(new MethodAccessorGenerator())
      .generateMethod(this.method.getDeclaringClass(), 
                      this.method.getName(), 
                      this.method.getParameterTypes(), 
                      this.method.getReturnType(), 
                      this.method.getExceptionTypes(), 
                      this.method.getModifiers());    
    this.parent.setDelegate(var3);   
  }   
  return invoke0(this.method, var1, var2);}
private static native Object invoke0(Method var0, Object var1, Object[] var2);

可见,最终调用的是 native 本地方法(本地方法栈)的 invoke0(),这部分的实现:

JNIEXPORT jobject JNICALL Java_sun_reflect_NativeMethodAccessorImpl_invoke0
  (JNIEnv *env, jclass unused, jobject m, jobject obj, jobjectArray args)
{   
  return JVM_InvokeMethod(env, m, obj, args);
}

可见,调用的是 jvm.h 模块的 JVM_InvokeMethod 方法,这部分的实现:

JVM_ENTRY(jobject, JVM_InvokeMethod(JNIEnv *env, jobject method, jobject obj, jobjectArray args0))
  JVMWrapper("JVM_InvokeMethod");
  Handle method_handle;
  if (thread->stack_available((address) &method_handle) >= JVMInvokeMethodSlack) {
    method_handle = Handle(THREAD, JNIHandles::resolve(method));
    Handle receiver(THREAD, JNIHandles::resolve(obj));
    objArrayHandle args(THREAD, objArrayOop(JNIHandles::resolve(args0)));

    oop result = Reflection::invoke_method(method_handle(), receiver, args, CHECK_NULL);

    jobject res = JNIHandles::make_local(env, result);
    if (JvmtiExport::should_post_vm_object_alloc()) {
      oop ret_type = java_lang_reflect_Method::return_type(method_handle());
      assert(ret_type != NULL, "sanity check: ret_type oop must not be NULL!");
      if (java_lang_Class::is_primitive(ret_type)) {
        // Only for primitive type vm allocates memory for java object.
        // See box() method.
        JvmtiExport::post_vm_object_alloc(JavaThread::current(), result);
      }
    }
    return res;
  } else {
    THROW_0(vmSymbols::java_lang_StackOverflowError());
  }
JVM_END

可见,经过一些检查和JVM的元数据的获取后,实际执行的是Reflection::invoke_method()方法,返回值转换为Java的对象返回。这部分的实现是:

oop Reflection::invoke_method(oop method_mirror, Handle receiver, objArrayHandle args, TRAPS) {
  oop mirror             = java_lang_reflect_Method::clazz(method_mirror);
  int slot               = java_lang_reflect_Method::slot(method_mirror);
  bool override          = java_lang_reflect_Method::override(method_mirror) != 0;
  objArrayHandle ptypes(THREAD, objArrayOop(java_lang_reflect_Method::parameter_types(method_mirror)));

  oop return_type_mirror = java_lang_reflect_Method::return_type(method_mirror);
  BasicType rtype;
  if (java_lang_Class::is_primitive(return_type_mirror)) {
    rtype = basic_type_mirror_to_basic_type(return_type_mirror, CHECK_NULL);
  } else {
    rtype = T_OBJECT;
  }

  instanceKlassHandle klass(THREAD, java_lang_Class::as_Klass(mirror));
  Method* m = klass->method_with_idnum(slot);
  if (m == NULL) {
    THROW_MSG_0(vmSymbols::java_lang_InternalError(), "invoke");
  }
  methodHandle method(THREAD, m);

  return invoke(klass, method, receiver, override, ptypes, rtype, args, true, THREAD);
}

可见,这只是一个包装的函数,做了一些调用前的类型检查和转换,实际实现在invoke()方法,这部分的实现是

oop Reflection::invoke(instanceKlassHandle klass, methodHandle reflected_method,
                       Handle receiver, bool override, objArrayHandle ptypes,
                       BasicType rtype, objArrayHandle args, bool is_method_invoke, TRAPS) {
  ResourceMark rm(THREAD);

  methodHandle method;      // actual method to invoke
  KlassHandle target_klass; // target klass, receiver's klass for non-static

  // Ensure klass is initialized
  klass->initialize(CHECK_NULL);

  bool is_static = reflected_method->is_static();
  if (is_static) {
    // ignore receiver argument
    method = reflected_method;
    target_klass = klass;
  } else {
    // check for null receiver
    if (receiver.is_null()) {
      THROW_0(vmSymbols::java_lang_NullPointerException());
    }
    // Check class of receiver against class declaring method
    if (!receiver->is_a(klass())) {
      THROW_MSG_0(vmSymbols::java_lang_IllegalArgumentException(), "object is not an instance of declaring class");
    }
    // target klass is receiver's klass
    target_klass = KlassHandle(THREAD, receiver->klass());
    // no need to resolve if method is private or <init>
    if (reflected_method->is_private() || reflected_method->name() == vmSymbols::object_initializer_name()) {
      method = reflected_method;
    } else {
      // resolve based on the receiver
      if (reflected_method->method_holder()->is_interface()) {
        // resolve interface call
        if (ReflectionWrapResolutionErrors) {
          // new default: 6531596
          // Match resolution errors with those thrown due to reflection inlining
          // Linktime resolution & IllegalAccessCheck already done by Class.getMethod()
          method = resolve_interface_call(klass, reflected_method, target_klass, receiver, THREAD);
          if (HAS_PENDING_EXCEPTION) {
          // Method resolution threw an exception; wrap it in an InvocationTargetException
            oop resolution_exception = PENDING_EXCEPTION;
            CLEAR_PENDING_EXCEPTION;
            // JVMTI has already reported the pending exception
            // JVMTI internal flag reset is needed in order to report InvocationTargetException
            if (THREAD->is_Java_thread()) {
              JvmtiExport::clear_detected_exception((JavaThread*) THREAD);
            }
            JavaCallArguments args(Handle(THREAD, resolution_exception));
            THROW_ARG_0(vmSymbols::java_lang_reflect_InvocationTargetException(),
                vmSymbols::throwable_void_signature(),
                &args);
          }
        } else {
          method = resolve_interface_call(klass, reflected_method, target_klass, receiver, CHECK_(NULL));
        }
      }  else {
        // if the method can be overridden, we resolve using the vtable index.
        assert(!reflected_method->has_itable_index(), "");
        int index = reflected_method->vtable_index();
        method = reflected_method;
        if (index != Method::nonvirtual_vtable_index) {
          // target_klass might be an arrayKlassOop but all vtables start at
          // the same place. The cast is to avoid virtual call and assertion.
          InstanceKlass* inst = (InstanceKlass*)target_klass();
          method = methodHandle(THREAD, inst->method_at_vtable(index));
        }
        if (!method.is_null()) {
          // Check for abstract methods as well
          if (method->is_abstract()) {
            // new default: 6531596
            if (ReflectionWrapResolutionErrors) {
              ResourceMark rm(THREAD);
              Handle h_origexception = Exceptions::new_exception(THREAD,
                     vmSymbols::java_lang_AbstractMethodError(),
                     Method::name_and_sig_as_C_string(target_klass(),
                     method->name(),
                     method->signature()));
              JavaCallArguments args(h_origexception);
              THROW_ARG_0(vmSymbols::java_lang_reflect_InvocationTargetException(),
                vmSymbols::throwable_void_signature(),
                &args);
            } else {
              ResourceMark rm(THREAD);
              THROW_MSG_0(vmSymbols::java_lang_AbstractMethodError(),
                        Method::name_and_sig_as_C_string(target_klass(),
                                                                method->name(),
                                                                method->signature()));
            }
          }
        }
      }
    }
  }

  // I believe this is a ShouldNotGetHere case which requires
  // an internal vtable bug. If you ever get this please let Karen know.
  if (method.is_null()) {
    ResourceMark rm(THREAD);
    THROW_MSG_0(vmSymbols::java_lang_NoSuchMethodError(),
                Method::name_and_sig_as_C_string(klass(),
                                                        reflected_method->name(),
                                                        reflected_method->signature()));
  }

  // In the JDK 1.4 reflection implementation, the security check is
  // done at the Java level
  if (!(JDK_Version::is_gte_jdk14x_version() && UseNewReflection)) {

  // Access checking (unless overridden by Method)
  if (!override) {
    if (!(klass->is_public() && reflected_method->is_public())) {
      bool access = Reflection::reflect_check_access(klass(), reflected_method->access_flags(), target_klass(), is_method_invoke, CHECK_NULL);
      if (!access) {
        return NULL; // exception
      }
    }
  }

  } // !(Universe::is_gte_jdk14x_version() && UseNewReflection)

  assert(ptypes->is_objArray(), "just checking");
  int args_len = args.is_null() ? 0 : args->length();
  // Check number of arguments
  if (ptypes->length() != args_len) {
    THROW_MSG_0(vmSymbols::java_lang_IllegalArgumentException(), "wrong number of arguments");
  }

  // Create object to contain parameters for the JavaCall
  JavaCallArguments java_args(method->size_of_parameters());

  if (!is_static) {
    java_args.push_oop(receiver);
  }

  for (int i = 0; i < args_len; i++) {
    oop type_mirror = ptypes->obj_at(i);
    oop arg = args->obj_at(i);
    if (java_lang_Class::is_primitive(type_mirror)) {
      jvalue value;
      BasicType ptype = basic_type_mirror_to_basic_type(type_mirror, CHECK_NULL);
      BasicType atype = unbox_for_primitive(arg, &value, CHECK_NULL);
      if (ptype != atype) {
        widen(&value, atype, ptype, CHECK_NULL);
      }
      switch (ptype) {
        case T_BOOLEAN:     java_args.push_int(value.z);    break;
        case T_CHAR:        java_args.push_int(value.c);    break;
        case T_BYTE:        java_args.push_int(value.b);    break;
        case T_SHORT:       java_args.push_int(value.s);    break;
        case T_INT:         java_args.push_int(value.i);    break;
        case T_LONG:        java_args.push_long(value.j);   break;
        case T_FLOAT:       java_args.push_float(value.f);  break;
        case T_DOUBLE:      java_args.push_double(value.d); break;
        default:
          THROW_MSG_0(vmSymbols::java_lang_IllegalArgumentException(), "argument type mismatch");
      }
    } else {
      if (arg != NULL) {
        Klass* k = java_lang_Class::as_Klass(type_mirror);
        if (!arg->is_a(k)) {
          THROW_MSG_0(vmSymbols::java_lang_IllegalArgumentException(), "argument type mismatch");
        }
      }
      Handle arg_handle(THREAD, arg);         // Create handle for argument
      java_args.push_oop(arg_handle); // Push handle
    }
  }

  assert(java_args.size_of_parameters() == method->size_of_parameters(), "just checking");

  // All oops (including receiver) is passed in as Handles. An potential oop is returned as an
  // oop (i.e., NOT as an handle)
  JavaValue result(rtype);
  JavaCalls::call(&result, method, &java_args, THREAD);

  if (HAS_PENDING_EXCEPTION) {
    // Method threw an exception; wrap it in an InvocationTargetException
    oop target_exception = PENDING_EXCEPTION;
    CLEAR_PENDING_EXCEPTION;
    // JVMTI has already reported the pending exception
    // JVMTI internal flag reset is needed in order to report InvocationTargetException
    if (THREAD->is_Java_thread()) {
      JvmtiExport::clear_detected_exception((JavaThread*) THREAD);
    }

    JavaCallArguments args(Handle(THREAD, target_exception));
    THROW_ARG_0(vmSymbols::java_lang_reflect_InvocationTargetException(),
                vmSymbols::throwable_void_signature(),
                &args);
  } else {
    if (rtype == T_BOOLEAN || rtype == T_BYTE || rtype == T_CHAR || rtype == T_SHORT)
      narrow((jvalue*) result.get_value_addr(), rtype, CHECK_NULL);
    return box((jvalue*) result.get_value_addr(), rtype, CHECK_NULL);
  }
}

可见,这里做了大量的检查以及区分了多态的调用以及对基本类型的参数进行转换等工作,实际执行方法的逻辑在JavaCalls::call()方法,而这里也是正常解析字节码调用方法实际会执行的位置,这里的实现是:

void JavaCalls::call(JavaValue* result, methodHandle method, JavaCallArguments* args, TRAPS) {
  // Check if we need to wrap a potential OS exception handler around thread
  // This is used for e.g. Win32 structured exception handlers
  assert(THREAD->is_Java_thread(), "only JavaThreads can make JavaCalls");
  // Need to wrap each and everytime, since there might be native code down the
  // stack that has installed its own exception handlers
  os::os_exception_wrapper(call_helper, result, &method, args, THREAD);
}

最终在os::os_exception_wrapper()落地,不同操作系统将调用不同的实现来执行方法,而这里的call_helper则是JVM在解析字节码时针对方法生成的一个函数指针,最终指向的地址是一段可执行的native code,内容是建立方法栈逻辑,并执行由解释器或JIT生成的代码。

更详细的细节:https://www.zhihu.com/question/464985077/answer/1940021614

2、Lambda生成函数映射的底层原理

具体来讲,Bean::getId 这种 Lambda 写法进过编译后,会通过 java.lang.invoke.LambdaMetafactory

调用到

java.lang.invoke.InnerClassLambdaMetafactory#spinInnerClass

最终实现是调用 JDK 自带的字节码库 jdk.internal.org.objectweb.asm 动态生成一个内部类,上层 call 内部类的方法执行调用。

所以 Lambda 生成函数映射的方式,核心消耗就在于生成函数映射,那生成函数映射的效率究竟如何呢?

我们和反射获取 Method 做个对比,Benchmark 结论:

BenchmarkModeCntScoreErrorUnits
genMethod(反射获取方法)avgt(平均耗时)50.1250.015us/op
genLambda(生成方法的函数映射)avgt551.88040.040us/op

从数据来看,生成函数映射的耗时远高于反射获取 Method。那为我们不禁要问,既然生成函数映射的性能远低于反射获取方法,那为什么最终用生成函数的方式的执行速度比反射要快?

答案就在于——函数复用,将一个固定签名的函数缓存起来,下次调用就可以省去函数创建的过程。

比如 fastjson2 直接将常用函数的初始化缓存放在 static 代码块,这就将函数创建的消耗就被前置到类加载阶段,在数据处理阶段的耗时进一步降低。

3、对比分析 & 结论

从原理上来说,反射方式,在获取 Method 阶段消耗较少,但 invoke 阶段则是每次都用都调用本地方法执行,先是在 jvm 层面多出一些检查,而后转到 JNI 本地库,除了有额外的 jvm 堆栈与本地方法栈的 context 交换 ,还多出一系列 C 体系额外操作,在性能上自然是不如 Lambda 函数映射;

Lambda 生成函数映射的方式,在生成代理类的过程中有部分开销,这部分开销可以通过缓存机制大量减少,而后的调用则全部属于 Java 范畴内的堆栈调用(即拿到代理类后,调用效率和原生方法调用几乎一致)。

相关的可以再看看MethodHandle系列,关于InvokeDynamic以及bootstrap,这些也是lambda的基础,强烈推荐

Leave a Reply

Your email address will not be published. Required fields are marked *

lWoHvYe 无悔,专一