paint-brush
软件蓬勃发展,除非你先杀死它:过早优化和 Java GC 的故事经过@wasteofserver
553 讀數
553 讀數

软件蓬勃发展,除非你先杀死它:过早优化和 Java GC 的故事

经过 Frankie6m2024/04/06
Read on Terminal Reader

太長; 讀書

不要过度优化,让语言为您服务。故事讲得通。Java 升级可以免费提高性能。始终进行基准测试。
featured image - 软件蓬勃发展,除非你先杀死它:过早优化和 Java GC 的故事
Frankie HackerNoon profile picture
0-item

LinkedList 会更快吗?我应该用 `iterator` 替换 `for each` 吗?这个 `ArrayList` 应该是 `Array` 吗?这篇文章是对一个恶意优化的回应,它已经永远铭刻在我的记忆中了。


在深入研究 Java 和解决干扰(无论是来自垃圾收集器还是上下文切换)的方法之前,让我们先来了解一下为您未来的自己编写代码的基础知识。


过早的优化是一切罪恶的根源。


您之前可能听说过,过早优化是万恶之源。嗯,有时确实如此。在编写软件时,我坚信:


  1. 尽可能详细地描述;您应该尝试像写故事一样叙述意图。


  2. 尽可能优化;这意味着您应该了解该语言的基础知识并相应地应用它们。

尽可能描述

您的代码应该表达意图,其中很多与您命名方法和变量的方式有关。


 int[10] array1; // bad int[10] numItems; // better int[10] backPackItems; // great

仅通过变量名,您就可以推断出功能。


虽然numItems是抽象的,但是backPackItems可以告诉您很多有关预期行为的信息。


或者说你有这个方法:


 List<Countries> visitedCountries() { if(noCountryVisitedYet) return new ArrayList<>(0); } // (...) return listOfVisitedCountries; }

就代码而言,这看起来或多或少还不错。


我们可以做得更好吗?我们一定可以!


 List<Countries> visitedCountries() { if(noCountryVisitedYet) return Collections.emptyList(); } // (...) return listOfVisitedCountries; }

阅读Collections.emptyList()比阅读new ArrayList<>(0);


想象一下,您是第一次阅读上述代码,并偶然发现了用于检查用户是否确实访问过各个国家的保护条款。此外,想象一下,这被埋在一个冗长的类中,阅读Collections.emptyList()肯定比new ArrayList<>(0)更具描述性,您还要确保它是不可变的,确保客户端代码无法修改它。

尽可能优化

了解你的语言,并相应地使用它。如果你需要double ,则无需将其包装在Double对象中。如果你实际上需要的只是一个Array ,那么使用List也是如此。


知道如果在线程之间共享状态,则应该使用StringBuilderStringBuffer连接字符串:


 // don't do this String votesByCounty = ""; for (County county : counties) { votesByCounty += county.toString(); } // do this instead StringBuilder votesByCounty = new StringBuilder(); for (County county : counties) { votesByCounty.append(county.toString()); }


知道如何索引数据库。预测瓶颈并相应地进行缓存。以上都是优化。它们是您作为第一批公民应该了解和实施的优化。

怎样先杀死它?

我永远不会忘记几年前读过的一篇拙劣之作。说实话,作者很快就改口了,但这表明许多邪恶行为都是由善意引发的。


 // do not do this, ever! int i = 0; while (i<10000000) { // business logic if (i % 3000 == 0) { //prevent long gc try { Thread.sleep(0); } catch (Ignored e) { } } }

来自地狱的垃圾收集器黑客!


您可以在原始文章中阅读有关上述代码的工作原因和方式的更多信息,虽然该漏洞确实很有趣,但这是您永远应该做的事情之一。


  • 通过副作用起作用, Thread.sleep(0)在此块中没有任何用途
  • 通过利用下游代码的缺陷来工作
  • 对于任何继承此代码的人来说,它都是晦涩而神奇的


只有在使用语言提供的所有默认优化进行编写后遇到瓶颈时,才开始构建更复杂的东西。但要避免使用上述混合物。


Microsoft Copilot “想象” 的 Java 未来垃圾收集器解读


如何处理垃圾收集器?

如果在完成所有操作之后,垃圾收集器仍然是提供阻力的部分,您可以尝试以下一些方法:


  • 如果您的服务对延迟非常敏感以致于无法使用 GC,请运行“Epsilon GC”并完全避免使用 GC
    -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC


    这显然会增加你的内存,直到你得到一个 OOM 异常,所以这要么是一个短暂的情况,要么你的程序被优化为不创建对象


  • 如果您的服务对延迟敏感,但允许的容忍度允许一些余地,请运行 GC1 并为其提供类似-XX:MaxGCPauseTimeMillis=100内容(默认值为 250 毫秒)

  • 如果问题是由外部库引起的,比如其中一个库调用System.gc()Runtime.getRuntime().gc()它们是 Stop-the-world 垃圾收集器,您可以通过运行-XX:+DisableExplicitGC来覆盖有问题的行为


  • 如果你在 11 以上的 JVM 上运行,请尝试Z 垃圾收集器 (ZGC) ,性能提升非常显著! -XX:+UnlockExperimentalVMOptions -XX:+UseZGC 。你可能还想查看这个JDK 21 GC 基准


版本开始

版本结束

默认 GC

Java 1

Java 4

串行垃圾收集器

Java 5

Java 8

并行垃圾收集器

Java 9

正在进行

G1 垃圾收集器


注 1:自 Java 15 起, ZGC可用于生产环境,但您仍然必须使用-XX:+UseZGC明确激活它。


注 2:如果 VM 检测到两个以上处理器且堆大小大于或等于 1792 MB,则 VM 会将机器视为服务器级。如果不是服务器级,则默认为串行 GC


本质上,当应用程序的性能限制显然与垃圾收集行为直接相关,并且您具备做出明智调整所需的专业知识时,请选择 GC 调优。否则,请信任 JVM 的默认设置并专注于优化应用程序级代码。

u/shiphe - 你会想阅读完整的评论


您可能想要探索的其他相关库:

Java 微基准测试工具 (JMH)

如果您只是凭感觉进行优化,而没有进行任何实际的基准测试,那么您就是在自讨苦吃。JMH 是测试算法性能的事实上的Java 库。使用它吧。

Java 线程亲和性

将进程固定到特定核心可能会提高缓存命中率。这取决于底层硬件以及您的例程如何处理数据。尽管如此,这个库使实现变得如此简单,以至于如果 CPU 密集型方法拖累了您,您将需要测试它。

LMAX 破坏者

这是那些即使你不需要也想研究的库之一。它的想法是允许超低延迟并发。但它的实现方式,从机械同情环形缓冲区,带来了很多新概念。我还记得七年前我第一次发现它时,熬了一个通宵来消化它。

Netflix jvmquake

jvmquake的前提是,当 JVM 出现问题时,您希望它停止运行而不是挂起。几年前,我在 HTCondor 集群上运行模拟,该集群的内存限制很严格,有时,作业会因“内存不足”错误而停滞。


此库强制 JVM 停止运行,让您能够处理实际错误。在这种特定情况下,HTCondor 会自动重新安排作业。

最后的想法

是什么代码让我写了这篇文章?我写过更糟糕的代码。我现在还在写。我们能期望的最好结果就是不断减少混乱。


我预计几年后当我看到自己的代码时会感到不满。


这是一个好兆头。



编辑并致谢:


也发表在wasteofserver.com