概述
一、前言
反射应该是 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:Toolsjdkjdk-8u91jrelibrt.jar]
[Loaded sun.reflect.AccessorGenerator from D:Toolsjdkjdk-8u91jrelibrt.jar]
[Loaded sun.reflect.MethodAccessorGenerator from D:Toolsjdkjdk-8u91jrelibrt.jar]
[Loaded sun.reflect.ByteVectorFactory from D:Toolsjdkjdk-8u91jrelibrt.jar]
[Loaded sun.reflect.ByteVector from D:Toolsjdkjdk-8u91jrelibrt.jar]
[Loaded sun.reflect.ByteVectorImpl from D:Toolsjdkjdk-8u91jrelibrt.jar]
[Loaded sun.reflect.ClassFileAssembler from D:Toolsjdkjdk-8u91jrelibrt.jar]
[Loaded sun.reflect.UTF8 from D:Toolsjdkjdk-8u91jrelibrt.jar]
[Loaded sun.reflect.Label from D:Toolsjdkjdk-8u91jrelibrt.jar]
[Loaded sun.reflect.Label$PatchInfo from D:Toolsjdkjdk-8u91jrelibrt.jar]
[Loaded java.util.ArrayList$Itr from D:Toolsjdkjdk-8u91jrelibrt.jar]
[Loaded sun.reflect.MethodAccessorGenerator$1 from D:Toolsjdkjdk-8u91jrelibrt.jar]
[Loaded sun.reflect.ClassDefiner from D:Toolsjdkjdk-8u91jrelibrt.jar]
[Loaded sun.reflect.ClassDefiner$1 from D:Toolsjdkjdk-8u91jrelibrt.jar]
[Loaded sun.reflect.GeneratedMethodAccessor1 from __JVM_DefineClass__]
test: 15
[Loaded java.lang.Shutdown from D:Toolsjdkjdk-8u91jrelibrt.jar]
[Loaded java.lang.Shutdown$Lock from D:Toolsjdkjdk-8u91jrelibrt.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
所引用的 MethodAccessor
为 Java
版。后续经由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);
}
}
});
}
}
最后生成的 Java
版 MethodAccessor
大致如下:
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 数组,基本类型的自动装箱、拆箱,还有最重要的方法内联。
最后
以上就是伶俐战斗机为你收集整理的深入理解Java虚拟机:(六)JVM是如何实现反射的?的全部内容,希望文章能够帮你解决深入理解Java虚拟机:(六)JVM是如何实现反射的?所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复