当前位置:Java -> Finalizer存在的问题
在Java中,finalize
方法从早期就成为了语言的一部分,提供了在对象被垃圾回收之前执行清理活动的机制。然而,由于几个与性能相关的问题,使用finalizers受到了质疑。从Java 9开始,finalize方法已被弃用,并且强烈不建议使用。
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 {
}
我们将评估这两个类的创建性能。该代码只是在一个无限循环中创建这些对象:
@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
方法也可能会导致性能显著下降。任何额外的逻辑都会使情况变得更糟。我们可以从性能测试中看到这一点。现在,我们不打算使用垃圾回收日志来分析这段代码:
Benchmark | Mode | Cnt | Score | Error | Units |
---|---|---|---|---|---|
OverheadBenchmark.finalizeableBigObjectCreationBenchmark | thrpt | 6 | 23221.308 | ± 226.856 | ops/s |
OverheadBenchmark.nonFinalizeableBigObjectCreationBenchmark | thrpt | 6 | 23807.144 | ± 117.467 | ops/s |
该测试分别在两个独立的分支中进行了三次迭代,每次迭代二十分钟,其中热身阶段为十秒。这意味着总体上每次测量都需要两小时,应足够估算每项测试的相对性能。本文中其他测试都采用了相同的配置。
将`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`方法也可能会显著降低性能:
基准测试 | 模式 | 计数 | 得分 | 错误 | 单位 |
---|---|---|---|---|---|
OverheadBenchmark.delayedFinalizeableBigObjectCreationBenchmark | thrpt | 6 | 142.630 | ± 1.282 | ops/s |
OverheadBenchmark.throwingFinalizeableBigObjectCreationBenchmark | thrpt | 6 | 23100.262 | ± 632.131 | ops/s |
如先前提到的,对于这类问题最好的情况是出现`OutOfMemoryError`的应用程序失败。这明显显示了内存使用问题。然而,让我们集中精力于更微妙的问题,这些问题会降低性能,但不会明确表达自己。
第一步是分析垃圾回收日志,并检查是否有一些异常数量的回收周期。市场上有很好的工具可以分析垃圾回收日志。我们使用GCeasy来分析捕获的垃圾回收日志。这里我们正在比较从垃圾回收日志中获取的前述示例的指标。请注意,比较是从一个二十分钟迭代中获取的:
NonFinalizeable BigObject |
Finalizeable BigObject |
ThrowingFinalizeable BigObject |
DelayedFinalizea bleBigObject |
|
---|---|---|---|---|
GC 次数 | ≈60000 | ≈93000 | ≈94000 | ≈550000 |
不可终结的 大对象 |
可终结的 大对象 |
抛出终结的 大对象 |
延迟终结的 大对象 |
|
---|---|---|---|---|
吞吐量 | 约98% | 约95% | 约95% | 约5% |
平均暂停GC时间 | 约0.4毫秒 | 约0.5毫秒 | 约0.5毫秒 | 约2毫秒 |
最大暂停GC时间 | 约14毫秒 | 约47毫秒 | 约22毫秒 | 约50毫秒 |
延迟终结的大对象
中,我们只花了5%的时间在工作上,其余时间都被垃圾收集器占用。这意味着在运行十分钟时间的应用程序中,我们只有30秒用于实际工作。
GCeasy报告中包含以上信息的一部分:
使用GCeasy API,可以将吞吐量设定为要求,并对其运行测试,或者实施运行系统的实时监控,以通知任何值的下降。
"gcKPI": {
"throughputPercentage": 99.952,
"averagePauseTime": 750.232,
"maxPauseTime": 57880
},
我们永远不应该低估性能问题。几毫秒的浪费可能在一年内累积成重要的时间。 性能问题不仅会导致花费更多的金钱,还可能影响SLA,并造成更严重的后果。
推荐阅读: 1.什么是MySQL?
本文链接: Finalizer存在的问题