什么是反射
反射是Java编程语言的一个特性。它允许正在执行的Java程序对自身进行检查、操纵程序的内部属性。例如,Java类可以获取其所有成员方法的名称并输出。
反射本质上也是一种元编程,“Program as data”。
示例代码
import java.lang.reflect.*;
public class DumpMethods {
public static void main(String args[])
{
try {
Class c = Class.forName(args[0]);
Method m[] = c.getDeclaredMethods();
for (int i = 0; i < m.length; i++)
System.out.println(m[i].toString());
}
catch (Throwable e) {
System.err.println(e);
}
}
}
实际使用的时候,我们一般都会调用方法。这种单纯看字段的场景很少,再来个例子:
public class A {
public void foo(String name) {
System.out.println("Hello, " + name);
}
}
// 另一个文件
public class TestClassLoad {
public static void main(String[] args) throws Exception {
Class<?> clz = Class.forName("A");
Object o = clz.newInstance();
Method foo = clz.getMethod("foo", String.class);
for (int i=0; i<16; i++) {
foo.invoke(o, Integer.toString(i));
}
}
}
反射的性能
做个简单的测试吧。
import java.lang.reflect.Method;
public class TestClassLoad {
public static void main(String[] args) throws Exception {
Class<?> clz = Class.forName("A");
Object o = clz.newInstance();
Method foo = clz.getMethod("foo", String.class);
int callTimes = 1000000;
A a = new A();
long startTime = System.nanoTime();
for (int i=0; i<callTimes; i++) {
foo.invoke(o, Integer.toString(i));
}
long endTime = System.nanoTime();
String str1 = "reflect: " + (endTime - startTime) / callTimes + "per call";
startTime = System.nanoTime();
for (int i=0; i<callTimes; i++) {
a.foo(Integer.toString(i));
}
endTime = System.nanoTime();
String str2 = "direct: " + (endTime - startTime) / callTimes + "per call";
System.out.println(str1);
System.out.println(str2);
}
}
最后的结果是:
reflect: 1524 per call
direct: 1199 per call
反射调用消耗的时间大概是直接调用的1.27倍。其实并没有很慢。
性能分析
那么慢的这一点到底慢在哪呢?以OpenJDK 17 为例,我们可以看一下invoke方法:
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz,
Modifier.isStatic(modifiers) ? null : obj.getClass(),
modifiers);
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
invoke方法的逻辑大概分为两部分,第一部分是进行检查(checkAccess),第二部分是通过MethodAccessor调用invoke方法。
检查部分自不必说,做检查一定比不做检查慢。
其实还进行了参数检查,看看传入参数的类型是否合法等。这部分实现在MethodAccessor中,感兴趣的朋友可以去查一查
而第二部分的MethodAccessor是一个接口,有两种实现。第一种是native实现,第二种是Java实现。native方法启动快,但是无法做任何运行时优化。Java实现一开始会慢(因为是字节码),但是变成热点代码以后会被JIT编译器优化。
变成热点代码需要被访问多次,以前是被访问16次。
证明方法:
上文中的结论是两个方法都被调用1000000次,如果我们每个方法调用10次呢?
reflect: 292124 per call
direct: 28570 per call
100次呢?
reflect: 85765 per call
direct: 23214 per call
1000次
reflect: 26987 per call
direct: 3685 per call
10000次
reflect: 6027 per call
direct: 2422 per call
二者的差距越来越小,说明二者都会被JIT编译器很好的优化,当运行次数趋于无穷大的时候,二者速度将会无穷接近。(甚至可能反超)
比如,运行一千万次次
reflect: 1285 per call
direct: 1412 per call
运行一亿次
reflect: 1474 per call
direct: 1839 per call
结论
反射是一种很有用的工具,可以提供良好的扩展性。
反射会带来一定的性能问题,但对于长期运行的程序(如web服务),反射的性能不会成为瓶颈。这是JIT带来的能力。
彩蛋
MethodHanlde类有一个反反射方法,可以对反射方法进行超级优化
刚才的方法跑10000次的情况下:
reflect: 5385 per call
direct: 1913 per call
unreflect: 1818 per call
反反射真是太可怕了。
最后给出代码供大家测试
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
public class TestClassLoad {
public static void main(String[] args) throws Throwable {
Class<?> clz = Class.forName("A");
Object o = clz.newInstance();
Method foo = clz.getMethod("foo", String.class);
int callTimes = 10000;
A a = new A();
long startTime = System.nanoTime();
for (int i=0; i<callTimes; i++) {
foo.invoke(o, Integer.toString(i));
}
long endTime = System.nanoTime();
String str1 = "reflect: " + (endTime - startTime) / callTimes + " per call";
startTime = System.nanoTime();
for (int i=0; i<callTimes; i++) {
a.foo(Integer.toString(i));
}
endTime = System.nanoTime();
String str2 = "direct: " + (endTime - startTime) / callTimes + " per call";
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle unreflectFoo = lookup.unreflect(foo);
startTime = System.nanoTime();
for (int i=0; i<callTimes; i++) {
unreflectFoo.invoke(o, Integer.toString(i));
}
endTime = System.nanoTime();
String str3 = "unreflect: " + (endTime - startTime) / callTimes + " per call";
System.out.println(str1);
System.out.println(str2);
System.out.println(str3);
}
}