Finalize的性能问题

本文为译文,原文地址:Problems With Finalizer

1. 前言

​ 在Java中,finalize方法自语言早期以来就是其组成部分,提供了一种机制,在对象被垃圾回收之前执行清理活动。然而,由于几个与性能相关的问题,使用finalizers已经受到了质疑。从Java 9开始,finalize方法已经被弃用,强烈不推荐使用它。

2. 延迟垃圾回收

​ Finalizers 可以显著减慢垃圾回收过程。当一个对象准备好被收集,但它有 finalize 方法时,垃圾回收器必须调用这个方法,然后在下一个垃圾回收周期中重新检查该对象。这个两步过程延迟了内存回收,导致内存使用量增加和潜在的内存泄漏。

​ 这个问题会引起两种形式的 CPU 使用率上升。第一个也是最明显的问题是,实现了 finalize 方法的对象必须经历两个垃圾回收周期。另一个不太明显的问题是由于对象在内存中停留的时间更长,会触发更多由于内存不足而产生的垃圾回收周期。

​ 如果垃圾回收器无法长时间回收一个对象,应用程序可能会因 OutOfMemoryError 而崩溃,因为对象的创建速率明显高于回收速率。

​ 然而,上述情况很容易发现,因为应用程序会崩溃报错。让我们考虑一个更隐蔽的情况,即应用程序可以运行,但在垃圾回收上占用更多的资源和时间。

public class BigObject {
    private int[] data = new int[1000000];

}

public class FinalizeableBigObject extends BigObject {
    @Deprecated
    protected void finalize() throws Throwable {
        super.finalize();
    }
}

让我们引入另一个继承自 BigObject 的类,但是它不会实现 finalize 方法:

public class NonFinalizeableBigObject extends BigObject {

}

我们将评估这两个类的创建性能。代码只是在无限循环中创建这些对象:

此处使用JMH微基准性能测试工具

@Benchmark
@BenchmarkMode(Mode.Throughput)
@Fork(value = 1, jvmArgs = {"-Xlog:gc:file=gc-with-finalizable-object-%t.txt -Xmx6gb -Xms6gb"})
public void finalizeableBigObjectCreationBenchmark(Blackhole blackhole) {
    final FinalizeableBigObject finalizeableBigObject = new FinalizeableBigObject();
    blackhole.consume(finalizeableBigObject);
}

@Benchmark
@BenchmarkMode(Mode.Throughput)
@Fork(value = 1, jvmArgs = {"-Xlog:gc:file=gc-with-non-finalizable-object-%t.txt -Xmx6gb -Xms6gb"})
public void nonFinalizeableBigObjectCreationBenchmark(Blackhole blackhole) {
    NonFinalizeableBigObject nonFinalizeableBigObject = new NonFinalizeableBigObject();
    blackhole.consume(nonFinalizeableBigObject);
}

即使是一个空的 finalize 方法也可能导致性能显著下降。任何额外的逻辑都会使情况变得更糟。我们可以从性能测试中看到这一点。现在,我们不会使用垃圾收集日志来分析这段代码。

BenchmarkModeCntScoreErrorUnits
OverheadBenchmark.finalizeableBigObjectCreationBenchmarkthrpt623221.308± 226.856ops/s
OverheadBenchmark.nonFinalizeableBigObjectCreationBenchmarkthrpt623807.144± 117.467ops/s

这个测试在两个独立的分支中分别进行了三次迭代,每次迭代持续二十分钟,其中包含一次十秒钟的迭代用于预热。这意味着总体上每次测量需要两个小时,这应该足够估计每个测试的相对性能。本文中使用的其余测试都具有相同的配置。

3. Finalizers 作为安全网

​ 将 finalizers 作为安全网是一个合理的想法。然而,在这样做之前,我们应该知道所有的优缺点。通常这种安全网场景涉及实现 AutoCloseable 接口的资源。在这种情况下,finalizers 会调用 close 方法,我们可以确信资源将在某个时刻关闭。

从 finalize 方法中关闭资源应该是罕见的。管理资源的主要方式应该涉及 try-with-resources。在这种情况下,即使我们一直遵循良好的实践,也会受到惩罚,如前一个示例所示。拥有一个实现的 finalize 方法将需要两步内存回收。

如果我们有清理逻辑,它包含昂贵的操作,或者在我们尝试两次关闭资源时抛出异常,我们可能会遇到严重的性能问题。这甚至可能导致 OutOfMemoryError。让我们检查一下,如果我们暂停线程一毫秒,之前的例子会发生什么:

public class DelayedFinalizableBigObject extends BigObject {
    @Override
    protected void finalize() throws Throwable {
        Thread.sleep(1);
    }
}

@Benchmark
@BenchmarkMode(Mode.Throughput)
@Fork(value = 1, jvmArgs = {"-Xlog:gc:file=gc-with-delayed-finalizable-object-%t.txt -Xmx6gb -Xms6gb"})
public void delayedFinalizeableBigObjectCreationBenchmark(Blackhole blackhole) {
    DelayedFinalizableBigObject delayedFinalizeableBigObject = new DelayedFinalizableBigObject();
    blackhole.consume(delayedFinalizeableBigObject);
}

同时,让我们检查抛出异常的 finalize 方法的相同指标:

public class ThrowingFinalizableBigObject extends BigObject {
    @Override
    protected void finalize() throws Throwable {
        throw new Exception();
    }
}

@Benchmark
@BenchmarkMode(Mode.Throughput)
@Fork(value = 1, jvmArgs = {"-Xlog:gc:file=gc-with-throwing-finalizable-object-%t.txt -Xmx6gb -Xms6gb"})
public void throwingFinalizeableBigObjectCreationBenchmark(Blackhole blackhole) {
    ThrowingFinalizableBigObject throwingFinalizeableBigObject = new ThrowingFinalizableBigObject();
    blackhole.consume(throwingFinalizeableBigObject);
}

从性能测试的角度来看,我们可以看到,finalize 方法可能会显著降低性能:

BenchmarkModeCntScoreErrorUnits
OverheadBenchmark.delayedFinalizeableBigObjectCreationBenchmarkthrpt6142.630± 1.282ops/s
OverheadBenchmark.throwingFinalizeableBigObjectCreationBenchmarkthrpt623100.262± 632.131ops/s

4. 识别问题

​ 如前所述,这类问题的最佳情况是应用程序失败并出现 OutOfMemoryError。这明确显示了内存使用问题。然而,让我们关注更微妙的问题,这些问题会降低性能,但不会明确表现出来。

​ 第一步是分析垃圾回收日志,并检查是否有一些不寻常的回收周期数量。市场上有一些很好的工具可以分析垃圾回收日志。我们使用 GCeasy 分析捕获的垃圾回收日志。这里我们比较了前面示例中从垃圾回收日志中获取的指标。请注意,这些比较是从一个二十分钟的迭代中获取的:

NonFinalizeableBigObjectFinalizeableBigObjectThrowingFinalizeableBigObjectDelayedFinalizeableBigObject
Throughput≈98%≈95%≈95%≈5%
Avg Pause GC Time≈0.4 ms≈0.5 ms≈0.5 ms≈2 ms
Max Pause GC Time≈14 ms≈47 ms≈22 ms≈50 ms

吞吐量显示了应用程序在有用工作上花费的时间。在 DelayedFinalizeableBigObject 的情况下,我们只花费了 5% 的时间来完成工作。其余时间都用于垃圾回收器。这意味着在十分钟的运行时间中,我们只花了三十秒在实际工作上。

GCeasy 报告中有一部分包含了上述信息:
在这里插入图片描述

5. 结论

​ 我们永远不应低估性能问题。浪费的几毫秒可能在一年内累积成相当可观的时间。性能问题不仅可能导致花费更多本可以节省的钱,而且可能影响服务水平协议(SLA),并导致更严重的后果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值