我知道,使用反射API,我们可以通过存储在字符串中的方法名称调用方法。
但是,反射API不能用于高性能应用程序。在我的应用程序中,方法将以非常高的速率被调用。所以,我不能使用反射API。
那么,反射API的替代方案是什么?
我做了研究,发现可以使用cglib和其他代码生成库。
但是,我没有找到任何例子来调用方法的名称存储在一个字符串。
一个反射选项的例子也会很好。
更新:实际上,我正在实现一些主从通信API。其中,从服务器将远程调用主方法。而且,方法调用将以非常高的速率进行(大约每秒50次方法调用)。因为,主服务器不断地轮询从服务器以获取任何响应。那么,我应该在如此高的调用率下尝试反射吗?
这就是的反射。在排除它之前,我建议尝试一下,看看在过去几年的任何JVM上,您是否真的看到了与它相关的任何性能问题。我怀疑你不会。
你唯一的其他实物期权
(实际上,有cglib
;是一个让人们调用的方法,传入要调用的方法的名称,然后分派给该方法(例如,使用一个大的switch
,或分派表,或类似的)。例如:
public Object callMethod(String methodName, Object[] args) {
switch (methodName) { // Using strings in `switch` requires a recent version of Java
case "foo":
return this.foo(args[0]);
case "bar":
this.bar(args[0], args[1]);
return null;
// ...and so on...
default:
throw new AppropriateException();
}
}
Cglib附带一个名为FastMethod
的类。该类的目的是通过使用非反射接口调用给定的方法。为此,FastMethod
实现接口并生成用于调用指定方法的字节码,从而避免了被认为代价高昂的反射调用。
然而,这里有两个理由说明为什么你不应该使用这个类。Cglib是很久以前写的。在那些日子里,反射仍然比今天更昂贵。然而,现代jvm知道一个叫做膨胀的概念。默认情况下,JVM将在第15次反射调用之后为调用方法生成字节代码。这正是cglib为您提供的显式功能。
此外,最昂贵的不是反射调用,而是查找。您仍然需要将方法命名为FastMethod
。因此,即使使用cglib,也无法避免这些成本。
从Java 1.7开始,有一种通过MethodHandle
从方法名调用方法的新方法,它比反射快得多。
我在一台蹩脚的笔记本电脑上做了一些基准测试:
- 反射每秒可以执行大约400万次调用
- MethodHandle每秒可以做大约1.4亿次调用
都是可以接受的性能,但是平均调用时间只有7纳秒,使用MethodHandle是非常好的。
如果您根据名称在HashMap中存储了对MethodHandle的引用,则可以在后续调用中重用它。
认为Reflection很慢是一个常见的误解。那是在Java 1.3左右的时代。
但是在现代Java中,Reflection确实得到了很好的优化。它在底层使用动态字节码生成。此外,JVM可以将这样的调用直接内联到调用者中,因此使用Reflection调用方法几乎和直接调用一样快。
这里的其他注释建议使用cglib的FastMethod。事实上,它并不比Reflection快多少。下面是一个用著名的JMH框架编写的基准测试:
@State(Scope.Benchmark)
public class MethodInvoke {
Method method;
FastMethod fastMethod;
@Setup
public void init() throws NoSuchMethodException {
method = getClass().getMethod("foo", int.class, long.class);
method.setAccessible(true);
fastMethod = FastClass.create(getClass()).getMethod("foo", new Class[] { int.class, long.class });
}
@GenerateMicroBenchmark
public long fastMethod() throws Exception {
return (Long) fastMethod.invoke(this, new Object[] {2, 3L});
}
@GenerateMicroBenchmark
public long reflection() throws Exception {
return (Long) method.invoke(this, 2, 3L);
}
public long foo(int a, long b) {
return a + b;
}
}
Java 7u51(64位)的结果:
Benchmark Mode Samples Mean Mean error Units
b.MethodInvoke.fastMethod thrpt 5 79248,583 3978,941 ops/ms
b.MethodInvoke.reflection thrpt 5 76975,414 2844,730 ops/ms
你看,FastMethod.invoke
只比Method.invoke
快3%,但与反射相反,FastMethod没有执行适当的参数验证等。
通常,方法的内省调用分为两个阶段:首先需要找到要调用的目标方法,然后在目标实例上调用该方法,为其提供参数。
在这个过程中,最昂贵的操作是在类中定位目标方法(例如Method method = TargetClass.getMethod(Class[] signature ...)
),一旦你获得了method对象,调用对象上的方法,如:method.invoke(targetObj, param,...)
是一个轻量级操作,只是比直接方法调用稍微昂贵一些。
为了演示这一点,我只是做了一个快速的&将三个方法与以下结果进行比较(您需要相对比较它们):
- 每次反射方法+调用:167ms
- 类+调用的第一个缓存方法:36 ms
- 直接调用:17ms
请注意,自省具有固定的性能成本,因此该方法执行的计算越多,这些数字就越接近。
我在以前的项目中使用了这种方法缓存方法,其中性能非常重要。在实践中,您观察到实际的方法执行时间使得内省的成本可以忽略不计。(病死率。阿姆达哈尔定律)
测试代码(承受快速&污秽)
import java.lang.reflect.Method;
import java.util.Random;
/**
* Created by maasg on 5/10/14.
*/
public class Instrospection {
public static void main(String [] params) throws Exception {
Random ran = new Random();
String[] methods = new String[] {"method1", "method2", "method3"};
Target target = new Target();
// Warmup
for (int i=0; i<1000; i++) {
String methodName = methods[ran.nextInt(3)];
String param = new Integer(ran.nextInt()).toString();
Method method = Target.class.getMethod(methodName, String.class);
}
StringBuilder builder = new StringBuilder();
long t0 = System.currentTimeMillis();
for (int i=0; i<100000; i++) {
String methodName = methods[ran.nextInt(3)];
String param = new Integer(ran.nextInt()).toString();
Method method = Target.class.getMethod(methodName, String.class);
Object result = method.invoke(target, "param");
builder.append(result.toString());
}
System.out.println("Elapsed 1: "+(System.currentTimeMillis()-t0));
Method[] invokeMethods = new Method[] {
Target.class.getMethod(methods[0], String.class),
Target.class.getMethod(methods[1], String.class),
Target.class.getMethod(methods[2], String.class),
};
builder = new StringBuilder();
long t1 = System.currentTimeMillis();
for (int i=0; i<100000; i++) {
String param = new Integer(ran.nextInt()).toString();
Method method = invokeMethods[ran.nextInt(3)];
Object result = method.invoke(target, "param");
builder.append(result.toString());
}
System.out.println("Elapsed 2: "+(System.currentTimeMillis()-t1));
builder = new StringBuilder();
long t2 = System.currentTimeMillis();
for (int i=0; i<100000; i++) {
Object result = null;
String param = new Integer(ran.nextInt()).toString();
switch (ran.nextInt(3)) {
case 0: result = target.method1(param);
case 1: result = target.method2(param);
case 2: result = target.method3(param);
}
builder.append(result.toString());
}
System.out.println("Elapsed 3: "+(System.currentTimeMillis()-t2));
}
}