深入理解Java虚拟机:(六)JVM是如何实现反射的?

一、前言

反射应该是 Java 基础最重要的特性之一吧。反射在 Java 应用中无处不在,像 Web 开发中,我们经常接触到各种可配置的框架,为了保证框架的可扩展性,往往会借助 Java 的反射机制,根据配置文件来加载不同的类。比如说,Spring 框架的最重要特性之一依赖反转(IoC),就是用的反射机制来实现的。我们这一篇就来了解一下反射的实现机制以及它性能糟糕的原因。

二、反射调用的实现

首先,我们来看看方法的反射调用,也就是 Method.invoke,是怎么实现的。先看个例子:

package com.jvm;

import java.lang.reflect.Method;

public class MethodInvokeTest {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("com.jvm.MethodInvokeTest");
        Method method = clazz.getMethod("target", int.class);
        for (int i = 0; i < 16; i++) {
            method.invoke(null, i);
        }
    }

    public static void target(int i) {
        System.out.println("test: " + i);
    }
}

然后我们从源码层面一步一步来剖析。

1、入口 java.lang.reflect.Method#invoke

public final class Method extends Executable {
	...
	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();// 获取MethodAccessor
	   }
	   return ma.invoke(obj, args);
	}
	static final ReflectionFactory reflectionFactory =
        AccessController.doPrivileged(
            new sun.reflect.ReflectionFactory.GetReflectionFactoryAction());
	
	// 获取MethodAccessor
	private MethodAccessor acquireMethodAccessor() {
        // First check to see if one has been created yet, and take it
        // if so
        MethodAccessor tmp = null;
        if (root != null) tmp = root.getMethodAccessor();
        if (tmp != null) {
            methodAccessor = tmp;
        } else {
            // Otherwise fabricate one and propagate it up to the root
            tmp = reflectionFactory.newMethodAccessor(this);
            setMethodAccessor(tmp);
        }

        return tmp;
    }
	
	private Method  root;
	private volatile MethodAccessor methodAccessor;
	// 把新创建methodAccessor对象通过root包装起来
	void setMethodAccessor(MethodAccessor accessor) {
        methodAccessor = accessor;
        // Propagate up
        if (root != null) {
            root.setMethodAccessor(accessor);
        }
    }
}

从源码中不难看出:Method.invoke() 实际上并不是自己实现的反射调用逻辑,而是委派给 sun.reflect.MethodAccessor来处理。每个 Java 方法只有一个对应的Method对象作为root。这个 root 不会暴露给用户,而是每次通过反射调用获取 Method 时,把新创建的 methodAccessor 对象通过 root 包装起来。在第一次调用一个实际Java 方法对应的Method 对象的 invoke() 方法之前,实现调用逻辑的 MethodAccessor 对象还没创建;等第一次调用时才新创建 MethodAccessor 并更新给 root,然后调用MethodAccessor.invoke() 真正完成反射调用。

2、MethodAccessor 是什么?

跟着源码链接到了 sun.reflect.MethodAccessor 包里。

public interface MethodAccessor {
    Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException;
}

这个类只是一个接口,而且只有一个方法,其 invoke() 方法与 Method.invoke() 的对应。

创建 MethodAccessor 实例的是 ReflectionFactory

public class ReflectionFactory {
    ...
    private static boolean noInflation = false;
    // 调用超过15次就采用java版本
    private static int inflationThreshold = 15;
    
    public MethodAccessor newMethodAccessor(Method var1) {
        checkInitted();
        if (noInflation && !ReflectUtil.isVMAnonymousClass(var1.getDeclaringClass())) {
            return (new MethodAccessorGenerator()).generateMethod(var1.getDeclaringClass(), var1.getName(), var1.getParameterTypes(), var1.getReturnType(), var1.getExceptionTypes(), var1.getModifiers());
        } else {
            NativeMethodAccessorImpl var2 = new NativeMethodAccessorImpl(var1);
            DelegatingMethodAccessorImpl var3 = new DelegatingMethodAccessorImpl(var2);
            var2.setParent(var3);
            return var3;
        }
    }
	
    private static void checkInitted() {
        if (!initted) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    if (System.out == null) {
                        return null;
                    } else {
                        String var1 = System.getProperty("sun.reflect.noInflation");
                        if (var1 != null && var1.equals("true")) {
                            ReflectionFactory.noInflation = true;
                        }

                        var1 = System.getProperty("sun.reflect.inflationThreshold");
                        if (var1 != null) {
                            try {
                                ReflectionFactory.inflationThreshold = Integer.parseInt(var1);
                            } catch (NumberFormatException var3) {
                                throw new RuntimeException("Unable to parse property sun.reflect.inflationThreshold", var3);
                            }
                        }

                        ReflectionFactory.initted = true;
                        return null;
                    }
                }
            });
        }
    }

}

从上面源码不难看出:MethodAccessor 有两个版本的实现。

  • 一个是Java实现的。Java实现的版本在初始化时需要较多时间,但长久来说性能较好;
  • 另一个是 native code 实现的。native 版本正好相反,启动时相对较快,但运行时间长了之后速度就比不过 Java 版了。这是 HotSpot 的优化方式带来的性能特性,同时也是许多虚拟机的共同点:跨越 native 边界会对优化有阻碍作用,它就像个黑箱一样让虚拟机难以分析也将其内联,于是运行时间长了之后反而是托管版本的代码更快些。

为了权衡两个版本的性能,Sun 的 JDK 使用了 inflation 的技巧:让 Java 方法在被反射调用时,开头若干次使用 native 版,等反射调用次数超过阈值(15次)时则生成一个专用的 MethodAccessor 实现类,生成其中的 invoke() 方法的字节码,以后对该 Java 方法的反射调用就会使用 Java 版。

验证一波:

javac.exe MethodInvokeTest.java
java -XX:+TraceClassLoading com.jvm.MethodInvokeTest

截取其中的重要信息出来:

test: 0
test: 1
test: 2
test: 3
test: 4
test: 5
test: 6
test: 7
test: 8
test: 9
test: 10
test: 11
test: 12
test: 13
test: 14
[Loaded sun.reflect.ClassFileConstants from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]
[Loaded sun.reflect.AccessorGenerator from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]
[Loaded sun.reflect.MethodAccessorGenerator from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]
[Loaded sun.reflect.ByteVectorFactory from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]
[Loaded sun.reflect.ByteVector from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]
[Loaded sun.reflect.ByteVectorImpl from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]
[Loaded sun.reflect.ClassFileAssembler from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]
[Loaded sun.reflect.UTF8 from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]
[Loaded sun.reflect.Label from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]
[Loaded sun.reflect.Label$PatchInfo from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]
[Loaded java.util.ArrayList$Itr from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]
[Loaded sun.reflect.MethodAccessorGenerator$1 from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]
[Loaded sun.reflect.ClassDefiner from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]
[Loaded sun.reflect.ClassDefiner$1 from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]
[Loaded sun.reflect.GeneratedMethodAccessor1 from __JVM_DefineClass__]
test: 15
[Loaded java.lang.Shutdown from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from D:\Tools\jdk\jdk-8u91\jre\lib\rt.jar]

可以看到在执行 16 次也就是 test: 15 时被触发了,导致 JVM 新加载了一堆类,其中就包括 [Loaded sun.reflect.GeneratedMethodAccessor1 from __JVM_DefineClass__] 这么一行。

具体实现如下:

(1)、MethodAccessor 实现版本:开头若干次使用 native 版

通过 DelegatingMethodAccessorImpl 实现的,代码如下:

class DelegatingMethodAccessorImpl extends MethodAccessorImpl {
    private MethodAccessorImpl delegate;

    DelegatingMethodAccessorImpl(MethodAccessorImpl var1) {
        this.setDelegate(var1);
    }

    public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
        return this.delegate.invoke(var1, var2);
    }
	
	// 传的参数var1就是这里的var2 NativeMethodAccessorImpl var2 = new NativeMethodAccessorImpl(var1);
    void setDelegate(MethodAccessorImpl var1) {
        this.delegate = var1;
    }
}

Method.invoke() 调用时,同时调用 sun.reflect.DelegatingMethodAccessorImpl#invoke 方法,即调用 sun.reflect.NativeMethodAccessorImpl#invoke 方法

代码如下:

class NativeMethodAccessorImpl extends MethodAccessorImpl {
    private final Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;

    NativeMethodAccessorImpl(Method var1) {
        this.method = var1;
    }

    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);
    }

    void setParent(DelegatingMethodAccessorImpl var1) {
        this.parent = var1;
    }

    private static native Object invoke0(Method var0, Object var1, Object[] var2);
}

从上面源码可以看出:每次 NativeMethodAccessorImpl.invoke() 方法被调用时,都会增加一个调用次数计数器 numInvocations,看超过阈值没有;一旦超过,则调用MethodAccessorGenerator.generateMethod() 来生成 Java 版的 MethodAccessor 的实现类,并且改变 DelegatingMethodAccessorImpl 所引用的 MethodAccessorJava 版。后续经由DelegatingMethodAccessorImpl.invoke() 调用到的就是 Java 版的实现了。

注意到关键的 invoke0() 方法是个 native 方法。它在 HotSpot VM 里是由JVM_InvokeMethod() 函数所支持的:

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_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  

其中的关键又是 Reflection::invoke_method()

// This would be nicer if, say, java.lang.reflect.Method was a subclass  
// of java.lang.reflect.Constructor  
  
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_klassOop(mirror));  
  methodOop 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);  
}  

(2)、MethodAccessor 实现版本:java 版本 MethodAccessorGenerator

class MethodAccessorGenerator extends AccessorGenerator {
	...
	private MagicAccessorImpl generate(final Class<?> declaringClass,
                                       String name,
                                       Class<?>[] parameterTypes,
                                       Class<?>   returnType,
                                       Class<?>[] checkedExceptions,
                                       int modifiers,
                                       boolean isConstructor,
                                       boolean forSerialization,
                                       Class<?> serializationTargetClass)
    {
        ByteVector vec = ByteVectorFactory.create();
        asm = new ClassFileAssembler(vec);
        this.declaringClass = declaringClass;
        this.parameterTypes = parameterTypes;
        this.returnType = returnType;
        this.modifiers = modifiers;
        this.isConstructor = isConstructor;
        this.forSerialization = forSerialization;

        asm.emitMagicAndVersion();

        short numCPEntries = NUM_BASE_CPOOL_ENTRIES + NUM_COMMON_CPOOL_ENTRIES;
        boolean usesPrimitives = usesPrimitiveTypes();
        if (usesPrimitives) {
            numCPEntries += NUM_BOXING_CPOOL_ENTRIES;
        }
        if (forSerialization) {
            numCPEntries += NUM_SERIALIZATION_CPOOL_ENTRIES;
        }

        // Add in variable-length number of entries to be able to describe
        // non-primitive parameter types and checked exceptions.
        numCPEntries += (short) (2 * numNonPrimitiveParameterTypes());

        asm.emitShort(add(numCPEntries, S1));
		
		// 这里生成[Loaded sun.reflect.GeneratedMethodAccessor1 from __JVM_DefineClass__]
        final String generatedName = generateName(isConstructor, forSerialization);
        asm.emitConstantPoolUTF8(generatedName);
        asm.emitConstantPoolClass(asm.cpi());
        thisClass = asm.cpi();
        if (isConstructor) {
            if (forSerialization) {
                asm.emitConstantPoolUTF8
                    ("sun/reflect/SerializationConstructorAccessorImpl");
            } else {
                asm.emitConstantPoolUTF8("sun/reflect/ConstructorAccessorImpl");
            }
        } else {
            asm.emitConstantPoolUTF8("sun/reflect/MethodAccessorImpl");
        }
        asm.emitConstantPoolClass(asm.cpi());
        superClass = asm.cpi();
        asm.emitConstantPoolUTF8(getClassName(declaringClass, false));
        asm.emitConstantPoolClass(asm.cpi());
        targetClass = asm.cpi();
        short serializationTargetClassIdx = (short) 0;
        if (forSerialization) {
            asm.emitConstantPoolUTF8(getClassName(serializationTargetClass, false));
            asm.emitConstantPoolClass(asm.cpi());
            serializationTargetClassIdx = asm.cpi();
        }
        asm.emitConstantPoolUTF8(name);
        asm.emitConstantPoolUTF8(buildInternalSignature());
        asm.emitConstantPoolNameAndType(sub(asm.cpi(), S1), asm.cpi());
        if (isInterface()) {
            asm.emitConstantPoolInterfaceMethodref(targetClass, asm.cpi());
        } else {
            if (forSerialization) {
                asm.emitConstantPoolMethodref(serializationTargetClassIdx, asm.cpi());
            } else {
                asm.emitConstantPoolMethodref(targetClass, asm.cpi());
            }
        }
        targetMethodRef = asm.cpi();
        if (isConstructor) {
        	// 构建newInstance
            asm.emitConstantPoolUTF8("newInstance");
        } else {
        	// 构建invoke
            asm.emitConstantPoolUTF8("invoke");
        }
        invokeIdx = asm.cpi();
        if (isConstructor) {
            asm.emitConstantPoolUTF8("([Ljava/lang/Object;)Ljava/lang/Object;");
        } else {
            asm.emitConstantPoolUTF8
                ("(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;");
        }
        invokeDescriptorIdx = asm.cpi();

        // Output class information for non-primitive parameter types
        nonPrimitiveParametersBaseIdx = add(asm.cpi(), S2);
        for (int i = 0; i < parameterTypes.length; i++) {
            Class<?> c = parameterTypes[i];
            if (!isPrimitive(c)) {
                asm.emitConstantPoolUTF8(getClassName(c, false));
                asm.emitConstantPoolClass(asm.cpi());
            }
        }

        // Entries common to FieldAccessor, MethodAccessor and ConstructorAccessor
        emitCommonConstantPoolEntries();

        // Boxing entries
        if (usesPrimitives) {
            emitBoxingContantPoolEntries();
        }

        if (asm.cpi() != numCPEntries) {
            throw new InternalError("Adjust this code (cpi = " + asm.cpi() +
                                    ", numCPEntries = " + numCPEntries + ")");
        }

        // Access flags
        asm.emitShort(ACC_PUBLIC);

        // This class
        asm.emitShort(thisClass);

        // Superclass
        asm.emitShort(superClass);

        // Interfaces count and interfaces
        asm.emitShort(S0);

        // Fields count and fields
        asm.emitShort(S0);

        // Methods count and methods
        asm.emitShort(NUM_METHODS);

        emitConstructor();
        emitInvoke();

        // Additional attributes (none)
        asm.emitShort(S0);

        // Load class
        vec.trim();
        final byte[] bytes = vec.getData();
        // Note: the class loader is the only thing that really matters
        // here -- it's important to get the generated code into the
        // same namespace as the target class. Since the generated code
        // is privileged anyway, the protection domain probably doesn't
        // matter.
        return AccessController.doPrivileged(
            new PrivilegedAction<MagicAccessorImpl>() {
                public MagicAccessorImpl run() {
                        try {
                        return (MagicAccessorImpl)
                        ClassDefiner.defineClass
                                (generatedName,
                                 bytes,
                                 0,
                                 bytes.length,
                                 declaringClass.getClassLoader()).newInstance();
                        } catch (InstantiationException | IllegalAccessException e) {
                            throw new InternalError(e);
                        }
                    }
                });
    }
}

最后生成的 JavaMethodAccessor 大致如下:

abstract class MethodAccessorImpl extends MagicAccessorImpl
    implements MethodAccessor {
    /** Matches specification in {@link java.lang.reflect.Method} */
    public abstract Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException;
}

3、小结

在默认情况下,方法的反射调用为委派实现,委派给本地实现来进行方法调用。在调用超过 15 次之后,委派实现便会将委派对象切换至动态实现。这个动态实现的字节码是自动生成的,它将直接使用 invoke 指令来调用目标方法。

三、反射调用的开销

在刚才的例子中,我们先后进行了 Class.forName,Class.getMethod 以及 Method.invoke 三个操作。其中,Class.forName 会调用本地方法,Class.getMethod 则会遍历该类的公有方法。如果没有匹配到,它还将遍历父类的公有方法。可想而知,这两个操作都非常费时。

在实践中,我们往往会在应用程序中缓存 Class.forName 和 Class.getMethod 的结果。因此,下面我就只关注反射调用本身的性能开销。

为了比较直接调用和反射调用的性能差距,我将前面的例子改为下面的 v2 版本。它会将反射调用循环二十亿次。此外,它还将记录下每跑一亿次的时间。

在我这个老笔记本上,一亿次直接调用耗费的时间大约在 195ms。这和不调用的时间是一致的。其原因在于这段代码属于热循环,同样会触发即时编译。并且,即时编译会将对 MethodInvokeTest2.target 的调用内联进来,从而消除了调用的开销。

package com.jvm;

import java.lang.reflect.Method;

public class MethodInvokeTest2 {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("com.jvm.MethodInvokeTest2");
        Method method = clazz.getMethod("target", int.class);

        long current = System.currentTimeMillis();
        for (int i = 0; i < 2_000_000_000; i++) {
            if (i % 100_000_000 == 0) {
                long temp = System.currentTimeMillis();
                System.out.println(temp - current);
                current = temp;
            }
            method.invoke(null, 128);
        }
    }

    public static void target(int i) {
        // 空方法
    }
}

由于目标方法 Test.target 接收一个 int 类型的参数,因此我传入 128 作为反射调用的参数,测得的结果均值为 645ms ,约为基准的 3.1 倍。我们暂且不管这个数字是高是低,先来看看在反射调用之前字节码都做了什么。

 public static void main(java.lang.String[]) throws java.lang.Exception;
   descriptor: ([Ljava/lang/String;)V
   flags: ACC_PUBLIC, ACC_STATIC
   Code:
     stack=6, locals=8, args_size=1
        0: ldc           #2                  // String com.jvm.MethodInvokeTest2
        2: invokestatic  #3                  // Method java/lang/Class.forName:(Ljava/lang/String;)Ljava/lang/Class;
        5: astore_1
        6: aload_1
        7: ldc           #4                  // String target
        9: iconst_1
       10: anewarray     #5                  // class java/lang/Class
       13: dup
       14: iconst_0
       15: getstatic     #6                  // Field java/lang/Integer.TYPE:Ljava/lang/Class;
       18: aastore
       19: invokevirtual #7                  // Method java/lang/Class.getMethod:(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
       22: astore_2
       23: invokestatic  #8                  // Method java/lang/System.currentTimeMillis:()J
       26: lstore_3
       27: iconst_0
       28: istore        5
       30: iload         5
       32: ldc           #9                  // int 2000000000
       34: if_icmpge     88
       37: iload         5
       39: ldc           #10                 // int 100000000
       41: irem
       42: ifne          63
       45: invokestatic  #8                  // Method java/lang/System.currentTimeMillis:()J
       48: lstore        6
       50: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
       53: lload         6
       55: lload_3
       56: lsub
       57: invokevirtual #12                 // Method java/io/PrintStream.println:(J)V
       60: lload         6
       62: lstore_3
       63: aload_2                           // 加载Method对象
       64: aconst_null 						 // 反射调用的第一个参数null
       65: iconst_1
       66: anewarray     #13                 // 生成一个长度为1的Object数组
       69: dup
       70: iconst_0
       71: sipush        128
       74: invokestatic  #14                // 将128自动装箱成Integer
       77: aastore							// 存入Object数组中
       78: invokevirtual #15                // 反射调用
       81: pop
       82: iinc          5, 1
       85: goto          30
       88: return

从上面字节码可以看到,除了反射调用外,还额外做了两个操作。

第一,由于 Method.invoke 是一个变长参数方法,在字节码层面它的最后一个参数会是 Object 数组。Java 编译器会在方法调用处生成一个长度为传入参数数量的 Object 数组,并将传入参数一一存储进该数组中。

第二,由于 Object 数组不能存储基本类型,Java 编译器会对传入的基本类型参数进行自动装箱。

关于第二个自动装箱,Java 缓存了 [-128, 127] 中所有整数所对应的 Integer 对象。当需要自动装箱的整数在这个范围之内时,便返回缓存的 Integer,否则需要新建一个 Integer 对象。

因此,我们可以将这个缓存的范围扩大至覆盖 128(对应参数-Djava.lang.Integer.IntegerCache.high=128),便可以避免需要新建 Integer 对象的场景。

或者,我们可以在循环外缓存 128 自动装箱得到的 Integer 对象,并且直接传入反射调用中。这两种方法测得的结果差不多,约为基准的 2.2 倍。

现在我们再回来看看第一个因变长参数而自动生成的 Object 数组。既然每个反射调用对应的参数个数是固定的,那么我们可以选择在循环外新建一个 Object 数组,设置好参数,并直接交给反射调用。改进代码成 v3 版本。

package com.jvm;

import java.lang.reflect.Method;

public class MethodInvokeTest3 {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("com.jvm.MethodInvokeTest3");
        Method method = clazz.getMethod("target", int.class);

        Object[] arg = new Object[1]; // 在循环外构造参数数组
        arg[0] = 128;

        long current = System.currentTimeMillis();
        for (int i = 0; i < 2_000_000_000; i++) {
            if (i % 100_000_000 == 0) {
                long temp = System.currentTimeMillis();
                System.out.println(temp - current);
                current = temp;
            }
            method.invoke(null, arg);
        }
    }

    public static void target(int i) {
        // empty method
    }
}

测得的结果反而更糟糕了,为基准的 3.22 倍。这是为什么呢?

如果你在上一步解决了自动装箱之后查看运行时的 GC 状况,你会发现这段程序并不会触发 GC。其原因在于,原本的反射调用被内联了,从而使得即时编译器中的逃逸分析将原本新建的 Object 数组判定为不逃逸的对象。

如果一个对象不逃逸,那么即时编译器可以选择栈分配甚至是虚拟分配,也就是不占用堆空间。

如果在循环外新建数组,即时编译器无法确定这个数组会不会中途被更改,因此无法优化掉访问数组的操作,可谓是得不偿失。

到目前为止,我们的最好记录是 2.2 倍。那能不能再进一步提升呢?

刚才我们提到,可以关闭反射调用的 Inflation 机制,从而取消委派实现,并且直接使用动态实现。此外,每次反射调用都会检查目标方法的权限,而这个检查同样可以在 Java 代码里关闭,在关闭了这两项机制之后,也就得到了我们的 v4 版本,它测得的结果约为基准的 1.4 倍。

package com.jvm;

import java.lang.reflect.Method;

// 在运行指令中添加如下两个虚拟机参数:
// -Djava.lang.Integer.IntegerCache.high=128
// -Dsun.reflect.noInflation=true
public class MethodInvokeTest4 {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("com.jvm.MethodInvokeTest4");
        Method method = clazz.getMethod("target", int.class);

        method.setAccessible(true);  // 关闭权限检查

        long current = System.currentTimeMillis();
        for (int i = 0; i < 2_000_000_000; i++) {
            if (i % 100_000_000 == 0) {
                long temp = System.currentTimeMillis();
                System.out.println(temp - current);
                current = temp;
            }
            method.invoke(null, 128);
        }
    }

    public static void target(int i) {
        // empty method
    }
}

在这个例子中,之所以反射调用能够变得这么快,主要是因为即时编译器中的方法内联。在关闭了 Inflation 的情况下,内联的瓶颈在于 Method.invoke 方法中对 MethodAccessor.invoke 方法的调用。

在这里插入图片描述

由于 Java 虚拟机的关于上述调用点的类型 profile(注:对于 invokevirtual 或者 invokeinterface,Java 虚拟机会记录下调用者的具体类型,我们称之为类型 profile)无法同时记录这么多个类,因此可能造成所测试的反射调用没有被内联的情况。

package com.jvm;

import java.lang.reflect.Method;

public class MethodInvokeTest5 {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("com.jvm.MethodInvokeTest5");
        Method method = clazz.getMethod("target", int.class);

        method.setAccessible(true);  // 关闭权限检查
        polluteProfile();

        long current = System.currentTimeMillis();
        for (int i = 0; i < 2_000_000_000; i++) {
            if (i % 100_000_000 == 0) {
                long temp = System.currentTimeMillis();
                System.out.println(temp - current);
                current = temp;
            }
            method.invoke(null, 128);
        }
    }

    public static void polluteProfile() throws Exception {
        Method method1 = MethodInvokeTest5.class.getMethod("target1", int.class);
        Method method2 = MethodInvokeTest5.class.getMethod("target2", int.class);
        for (int i = 0; i < 2000; i++) {
            method1.invoke(null, 0);
            method2.invoke(null, 0); }
    }
    public static void target1(int i) { }
    public static void target2(int i) { }

    public static void target(int i) {
        // empty method
    }
}

而测试循环则保持不变。测得的结果约为基准的 9.6 倍。也就是说,只要误扰了 Method.invoke 方法的类型 profile,性能开销便会从 1.4 倍上升至 9.6 倍。

之所以这么慢,除了没有内联之外,另外一个原因是逃逸分析不再起效。这时候,我们便可以采用刚才 v3 版本中的解决方案,在循环外构造参数数组,并直接传递给反射调用。这样子测得的结果约为基准的 7.3 倍。

除此之外,我们还可以提高 Java 虚拟机关于每个调用能够记录的类型数目(对应虚拟机参数 -XX:TypeProfileWidth,默认值为 2,这里设置为 3)。最终测得的结果约为基准的 7 倍,尽管它和原本的 1.4 倍还有一定的差距,但总算是比 9.6 倍好多了。

package com.jvm;

import java.lang.reflect.Method;

// -XX:TypeProfileWidth 默认值为 2,这里设置为 3
public class MethodInvokeTest6 {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("com.jvm.MethodInvokeTest6");
        Method method = clazz.getMethod("target", int.class);

        method.setAccessible(true);  // 关闭权限检查
        Object[] arg = new Object[1]; // 在循环外构造参数数组
        arg[0] = 128;
        polluteProfile();

        long current = System.currentTimeMillis();
        for (int i = 0; i < 2_000_000_000; i++) {
            if (i % 100_000_000 == 0) {
                long temp = System.currentTimeMillis();
                System.out.println(temp - current);
                current = temp;
            }
            method.invoke(null, arg);
        }
    }

    public static void polluteProfile() throws Exception {
        Method method1 = MethodInvokeTest6.class.getMethod("target1", int.class);
        Method method2 = MethodInvokeTest6.class.getMethod("target2", int.class);
        for (int i = 0; i < 2000; i++) {
            method1.invoke(null, 0);
            method2.invoke(null, 0); }
    }
    public static void target1(int i) { }
    public static void target2(int i) { }

    public static void target(int i) {
        // empty method
    }
}

小结:

方法的反射调用会带来不少性能开销,原因主要有三个:变长参数方法导致的 Object 数组,基本类型的自动装箱、拆箱,还有最重要的方法内联。

发布了332 篇原创文章 · 获赞 198 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/riemann_/article/details/104033656