jdk-9/jdk-8 和 jmh 中的 newInstance 与 new
newInstance vs new in jdk-9/jdk-8 and jmh
我在这里看到很多线程比较并尝试回答哪个更快:newInstance
或 new operator
。
查看源代码,似乎 newInstance
应该 慢得多 ,我的意思是它做了很多安全检查并使用了反射。我决定先测量 运行ning 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
热代码的差异约为 2x,单次发射时间更差。
现在我切换到 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).
在测试中再添加一种方法后:
@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
谁能解释一下为什么会有这么大的差异?
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 可能会优化具有可预测类型的反射实例化,并且 Something.class.newInstance()
指的是完全可预测的类型。这种优化可能变得不太有效。有几个可能的原因:
- 新模块访问规则使流程复杂化
- 由于
Class.newInstance()
已被弃用,一些支持已被故意删除(对我来说似乎不太可能)
- 由于上面显示的实施代码已更改,HotSpot 无法识别触发优化的某些代码模式
首先问题与模块系统无关(直接)
我注意到即使使用 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
"already compiled into a big method" 消息表示编译器未能内联 Class.newInstance
调用,因为被调用者的编译大小大于 InlineSmallCode
值(默认为 2000)。
当我用 -XX:InlineSmallCode=2500
重新运行基准测试时,它又变快了。
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
.
,基准测试也会很快
重新运行 JDK 9 基准测试 -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 需要在对象存储发生时设置一些障碍,这就是编译代码变得有点大的原因,因此 Class.newInstance
超过了默认的 InlineSmallCode
限制。编译的Class.newInstance
变大的另一个原因是反射代码在JDK 9.
中被稍微重写了
TL;DR JIT has failed to inline Class.newInstance
, because InlineSmallCode
limit has been exceeded. The compiled version of Class.newInstance
has become larger due to changes in reflection code in JDK 9 and because the default GC has been changed to G1.
我在这里看到很多线程比较并尝试回答哪个更快:newInstance
或 new operator
。
查看源代码,似乎 newInstance
应该 慢得多 ,我的意思是它做了很多安全检查并使用了反射。我决定先测量 运行ning 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
热代码的差异约为 2x,单次发射时间更差。
现在我切换到 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).
在测试中再添加一种方法后:
@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
谁能解释一下为什么会有这么大的差异?
Class.newInstance()
的实现大部分相同,除了以下部分:
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 可能会优化具有可预测类型的反射实例化,并且 Something.class.newInstance()
指的是完全可预测的类型。这种优化可能变得不太有效。有几个可能的原因:
- 新模块访问规则使流程复杂化
- 由于
Class.newInstance()
已被弃用,一些支持已被故意删除(对我来说似乎不太可能) - 由于上面显示的实施代码已更改,HotSpot 无法识别触发优化的某些代码模式
首先问题与模块系统无关(直接)
我注意到即使使用 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
"already compiled into a big method" 消息表示编译器未能内联 Class.newInstance
调用,因为被调用者的编译大小大于 InlineSmallCode
值(默认为 2000)。
当我用 -XX:InlineSmallCode=2500
重新运行基准测试时,它又变快了。
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
.
重新运行 JDK 9 基准测试 -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 需要在对象存储发生时设置一些障碍,这就是编译代码变得有点大的原因,因此 Class.newInstance
超过了默认的 InlineSmallCode
限制。编译的Class.newInstance
变大的另一个原因是反射代码在JDK 9.
TL;DR JIT has failed to inline
Class.newInstance
, becauseInlineSmallCode
limit has been exceeded. The compiled version ofClass.newInstance
has become larger due to changes in reflection code in JDK 9 and because the default GC has been changed to G1.