newInstance与新的jdk-9 / jdk-8和jmh
我在这里看到很multithreading比较并试图回答哪个更快: newInstance
或new operator
。
看源代码,似乎newInstance
应该慢得多 ,我的意思是它做了这么多的安全检查并使用reflection。 我决定先测量一下,首先运行jdk-8。 这里是使用jmh
的代码。
@BenchmarkMode(value = { Mode.AverageTime, Mode.SingleShotTime }) @Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) @State(Scope.Benchmark) public class TestNewObject { public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder().include(TestNewObject.class.getSimpleName()).build(); new Runner(opt).run(); } @Fork(1) @Benchmark public Something newOperator() { return new Something(); } @SuppressWarnings("deprecation") @Fork(1) @Benchmark public Something newInstance() throws InstantiationException, IllegalAccessException { return Something.class.newInstance(); } static class Something { } }
我不认为这里有很大的惊喜(JIT做了很多优化,使这个差异不那么大 ):
Benchmark Mode Cnt Score Error Units TestNewObject.newInstance avgt 5 7.762 ± 0.745 ns/op TestNewObject.newOperator avgt 5 4.714 ± 1.480 ns/op TestNewObject.newInstance ss 5 10666.200 ± 4261.855 ns/op TestNewObject.newOperator ss 5 1522.800 ± 2558.524 ns/op
热码的差别大概是2倍左右,单拍时间差很多。
现在我切换到jdk-9(如果它重要,内部版本157),并运行相同的代码。 结果是:
Benchmark Mode Cnt Score Error Units TestNewObject.newInstance avgt 5 314.307 ± 55.054 ns/op TestNewObject.newOperator avgt 5 4.602 ± 1.084 ns/op TestNewObject.newInstance ss 5 10798.400 ± 5090.458 ns/op TestNewObject.newOperator ss 5 3269.800 ± 4545.827 ns/op
这是一个热门的代码50倍的差异。 我正在使用最新的jmh版本(1.19.SNAPSHOT)。
在testing中增加一个方法之后:
@Fork(1) @Benchmark public Something newInstanceJDK9() throws Exception { return Something.class.getDeclaredConstructor().newInstance(); }
这里是整体结果n jdk-9:
TestNewObject.newInstance avgt 5 308.342 ± 107.563 ns/op TestNewObject.newInstanceJDK9 avgt 5 50.659 ± 7.964 ns/op TestNewObject.newOperator avgt 5 4.554 ± 0.616 ns/op
有人可以解释为什么有这么大的差异 ?
首先,这个问题与模块系统(直接)无关。
我注意到,即使在JDK 9中, newInstance
的第一次热身迭代与JDK 8一样快。
# Fork: 1 of 1 # Warmup Iteration 1: 10,578 ns/op <-- Fast! # Warmup Iteration 2: 246,426 ns/op # Warmup Iteration 3: 242,347 ns/op
这意味着JIT编译中已经有了一些内容。
-XX:+PrintCompilation
确认在第一次迭代后重新编译了基准:
10,762 ns/op # Warmup Iteration 2: 1541 689 ! 3 java.lang.Class::newInstance (160 bytes) made not entrant 1548 692 % 4 bench.generated.NewInstance_newInstance_jmhTest::newInstance_avgt_jmhStub @ 13 (56 bytes) 1552 693 4 bench.generated.NewInstance_newInstance_jmhTest::newInstance_avgt_jmhStub (56 bytes) 1555 662 3 bench.generated.NewInstance_newInstance_jmhTest::newInstance_avgt_jmhStub (56 bytes) made not entrant 248,023 ns/op
然后-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
指向内联问题:
1577 667 % 4 bench.generated.NewInstance_newInstance_jmhTest::newInstance_avgt_jmhStub @ 13 (56 bytes) @ 17 bench.NewInstance::newInstance (6 bytes) inline (hot) ! @ 2 java.lang.Class::newInstance (160 bytes) already compiled into a big method
“已编译成大方法”消息意味着编译器无法内联Class.newInstance
调用,因为调用方的编译大小大于InlineSmallCode
值(默认为2000)。
当我用-XX:InlineSmallCode=2500
重新testing基准时,它又变得很快了。
Benchmark Mode Cnt Score Error Units NewInstance.newInstance avgt 5 8,847 ± 0,080 ns/op NewInstance.operatorNew avgt 5 5,042 ± 0,177 ns/op
你知道,JDK 9现在有G1作为默认的GC 。 如果我回到并行GC,即使使用默认的InlineSmallCode
,基准testing也会很快。
用-XX:+UseParallelGC
重新运行JDK 9基准testing-XX:+UseParallelGC
:
Benchmark Mode Cnt Score Error Units NewInstance.newInstance avgt 5 8,728 ± 0,143 ns/op NewInstance.operatorNew avgt 5 4,822 ± 0,096 ns/op
G1需要在发生对象存储时join一些障碍,这就是为什么编译后的代码变得更大一些,所以Class.newInstance
超出了默认的InlineSmallCode
限制。 为什么编译后的Class.newInstance
变得更大的另一个原因是reflection代码在JDK 9中被稍微改写了。
TL; DR JIT未能内联
Class.newInstance
,因为InlineSmallCode
限制已被超过。 由于JDK 9中的reflection代码的改变,并且因为默认的GC已经改变为G1,所以Class.newInstance
的编译版本变得更大。
Class.newInstance()
的实现大致相同,除了以下部分:
Java 8:
Constructor<T> tmpConstructor = cachedConstructor; // Security check (same as in java.lang.reflect.Constructor) int modifiers = tmpConstructor.getModifiers(); if (!Reflection.quickCheckMemberAccess(this, modifiers)) { Class<?> caller = Reflection.getCallerClass(); if (newInstanceCallerCache != caller) { Reflection.ensureMemberAccess(caller, this, null, modifiers); newInstanceCallerCache = caller; } }
Java 9
Constructor<T> tmpConstructor = cachedConstructor; // Security check (same as in java.lang.reflect.Constructor) Class<?> caller = Reflection.getCallerClass(); if (newInstanceCallerCache != caller) { int modifiers = tmpConstructor.getModifiers(); Reflection.ensureMemberAccess(caller, this, null, modifiers); newInstanceCallerCache = caller; }
正如你所看到的,Java 8有一个quickCheckMemberAccess
允许绕过昂贵的操作,比如Reflection.getCallerClass()
。 这个快速检查已被删除,我猜,因为它不符合新的模块访问规则。
但还有更多。 JVM可能会使用可预测的types优化reflection实例,而Something.class.newInstance()
指的是完美可预测的types。 这种优化可能会变得不那么有效。 有几个可能的原因:
- 新的模块访问规则使该过程变得复杂
- 因为
Class.newInstance()
已经被弃用,一些支持被故意删除(似乎不太可能) - 由于上面显示的更改的实现代码,HotSpot无法识别某些触发优化的代码模式