看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
public static void target(int i) {}

for (int i = 1; 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);
}

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

由于Object数组不能存储基本类型, Java编译器会对传入的基本类型参数进行自动装箱, 比如会将int参数转换成为integer传入进去。

这两个操作会带来一定的性能开销, 还会占用堆内存, 让GC变得十分的频繁,

但是Java也会对自动装箱进行缓存, 比如Java缓存了[-128, 127]中整数所对应的 Integer 对象, 当需要自动装箱的整数在这个范围之内的时候, 返回缓存的Integer, 否则需要新建一个 Integer 对象。所以关于创建装箱对象的性能也就比较不错了

我们可以使用下面这个命令来扩大装箱的范围, 这样就减轻了装箱压力

1
-Djava.lang.Integer.IntegerCache.high=128

那我们如何解决关于Object转换的问题呢, 我们可以在循环外部创建一个Object[]数组, 但是这样真的会提升性能么:

1
2
3
4
5
6
7
8
9
10
11
12
Object[] args = new Object[1];
args[0] = 128;

for (int i = 1; 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, args);
}

但是这段代码经过测试之后, 发现性能并不是很好,这是因为原来的反射调用被编译器给内联了, 从而使得即时编译器中的逃逸分析将原本的Object数组判定为不逃逸的对象。

但是如果在循环外新建数组, 即时编译器无法确定这个数组会不会中途被修改, 因此无法优化掉访问数组的操作, 所以完全没有必要多此一举在外部创建一个Object[]数组

那么我们就不能提高这段代码的性能了吗, 答案当然是可以的, 我们可以取消我们的委派实现, 直接使用动态实现, 此外每次反射调用都会检查目标方法的权限, 而这个检查同样也可以在Java中进行关闭。

运行下面两个API

1
2
3
-Djava.lang.Integer.IntegerCache.high=128 //设置范围
-Dsun.reflect.noInflation=true //设置直接动态生成
method.setAccessible(true); //关闭权限检查

对于invokeinterfaceinvokevirtual, Java虚拟机会记录下调用者的具体类型, 我们称之为类型profile,但是在生产环境中我们无法同时记录这么多类, 因此可能会造成所测试的反射调用没有被内联的情况。这样会导致性能下降的速度更快.

1
2
3
4
5
6
7
8
9
public static void polluteProfile() throws Exception {
Method method1 = Test.class.getMethod("target1", int.class);
Method method2 = Test.class.getMethod("target2", int.class);

for (int i = 0; i < 2000; i++) {
method1.invoke(null, 0);
method2.invoke(null, 0);
}
}