在JDK 9/JDK 8和JMH中,newInstance与new的区别

39

我在这里看到了很多关于比较并试图回答哪个更快的线程:newInstance还是new operator

从源代码来看,newInstance 应该会慢得多,因为它要进行很多安全检查,并使用反射。我决定先测量一下 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();
}

这里是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    

有人可以解释一下为什么会有如此大的差异吗?


1
你是否正在使用带有jigsaw的JDK9版本? - Jorn Vernee
2
这很重要,因为模块系统可能会有许多额外的访问检查,而JIT可能还不知道如何处理得好。 - Jorn Vernee
4
Class.newInstance()在Java 9中已被弃用。推荐使用的替代方法是clazz.getDeclaredConstructor().newInstance(),其性能值得关注... - Holger
1
@Holger 说得好,已添加。差异仍然是10倍,比起2倍还有很大的差距... - Eugene
1
你能再做一个测试吗?这次使用Something中的非public(默认访问)构造函数。 - Holger
显示剩余6条评论
2个回答

56

首先,这个问题与模块系统没有直接关系。

我注意到即使在使用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,基准测试也会很快。

使用-XX:+UseParallelGC重新运行JDK 9基准测试:

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未能内联Class.newInstance,因为已超过InlineSmallCode限制。 编译版本的Class.newInstance由于JDK 9中反射代码的更改以及默认GC已更改为G1而变得更大。


1
像Spring这样反射密集的框架,这难道不是一个大问题吗?也许值得写一份报告,以便能够将方法变得更小(例如,通过将一些代码提取到独立的方法中)。 - Kirill Rakhman
2
@KirillRakhman 这不应该是一个问题,因为在实际情况中,newInstance 不太可能被内联。我无法想象出一个合理的情况,在同一位置通过反射调用 相同 构造函数 多次。在原始问题中,性能提升仅因 JIT 适应于调用特定方法而实现。 - apangin
很棒的解释,甚至还有一个tldr! - TecHunter

4
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;
    }
}

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 无法识别触发优化的某些代码模式

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接