在优化垃圾回收器之前先调优代码
摘要
基准测试表明,优化Java代码(例如减少SLF4J日志记录)对延迟的影响远大于选择垃圾回收器,尤其是在高百分位下。
暂无内容
查看缓存全文
缓存时间: 2026/07/13 19:55
# 为什么你应该在调优垃圾收集器之前先调优代码
来源:http://blog.vanillajava.blog/2026/06/why-you-should-tun-code-before-your.html
### 为什么你应该在调优垃圾收集器之前先调优代码
优化 Java 中的内存分配所带来的效果可能远超你对垃圾收集器的选择,甚至可能改变哪个垃圾收集器是最佳的。
在这篇文章中,我通过一个简单的事件响应延迟基准测试来观察——使用 JLBH 测试 Chronicle-FIX,以每秒 50K 的速率运行 MarketDataSnapshot 到 NewOrderSingle,持续 30 分钟。目标是比较一个执行冗余工作的系统(本例中使用 SLF4J 记录每条消息)与不记录日志的情况(Chronicle-FIX 内部使用 Chronicle Queue 记录每条消息),并观察这如何改变垃圾收集器的选择。
对于 p99(最差的 1/100),垃圾收集器的选择所带来的差异与优化日志记录方式相当。
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhz2R-QJ2T4etyhbtJhchUdCqFPAibBnBnJDlFyRR-wgyzN_9Zu_AnqS6QY60xVhuaFdorkvT9smrXJ0z6p2l3FDXG6zmE-gmUCxRD0HOaRKEZQ3kgJsovRsVDwG8LIvqimrUSCCFhEdJMluuRcA28VHEl1ZLhKuHLeVcp4zzIcyqTXbzsHZXX_64lZbxbd/s1600/Screenshot%20from%202026-06-08%2016-26-30.png)
然而,对于 p99.99(最差的 1/10,000),优化日志记录方式所带来的效果比垃圾收集器的选择要显著多个数量级。
[](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiSuq8kQhCQq9B6npcB29I11UIgJoR93QMOEE8LJp85vyVUZreUlTSoXdChyphenhyphenGSuGDT0tu24cVeiph8PV5DdFjmFt_QR-z5IvfSc92XL9Q7Q2xYvUqw-X9iFyz41k8t_MMqvh_Gb0D9vlDwkHvBXBU_KZJ9IrLsw1XQzvyzadIPPO184qKCksFOmOH68aOmv/s1600/Screenshot%20from%202026-06-08%2016-26-03.png)
## 未优化的基准测试
该测试在优化后的基准测试基础上,为每条待发送消息添加了一条 SLF4J 日志记录。一条日志记录听起来可能不多,但在每条消息上执行此操作会产生巨大差异,尤其是当其余代码已为低延迟编写时。
表 1. 使用 slf4j 日志记录的 RTT 延迟分布
| GC 选项 | p99 | p99.99 |
|------|------|--------|
| Parallel 大 Eden,无大页面 | **16.86** | 20,480 |
| ZGC,无大页面 | 12.21 | 19,694 |
| G1 + COH,无大页面 | 12.02 | 20,349 |
| G1,2 MiB 大页面 | 13.94 | 20,021 |
| Shenandoah 分代,无大页面 | 12.30 | 19,235 |
注意:
p99.99 达到数千微秒,即 19 到 20 毫秒。
p99.99(最差的 1/10,000)听起来可能很罕见,但在每秒 50K 速率下,相当于每秒发生 5 次,每分钟 300 次。
基于这些结果,你可能会得出结论:Shenandoah 是一个不错的选择,并避免使用 Parallel GC。
## 磁盘 IO 造成多大差异?
IO 通常是延迟的一个重要组成部分,我们可以看到仅改变日志写入位置所带来的效果。本例中,改为写入 tmpfs 文件系统。
表 2. 使用 slf4j 日志写入 /dev/shm 的 RTT 延迟分布
| GC 选项 | p99 | p99.99 |
|------|------|--------|
| Parallel 大 Eden,无大页面 | **34.50** | 10,994 |
| ZGC,无大页面 | 10.86 | 9,617 |
| G1 + COH,无大页面 | 10.90 | 10,043 |
| G1,2 MiB 大页面 | 10.64 | 11,682 |
| Shenandoah 分代,无大页面 | 10.32 | 9,552 |
虽然 Parallel GC 的 p99 可能是一个异常值,但你仍然可能倾向于 Shenandoah,但仅仅改变日志写入位置所带来的效果要大得多。
注意:
p99.99 达到数千微秒,即 9 到 10 毫秒。
## 移除冗余日志记录
Chronicle-FIX 已经通过 Chronicle-FIX 记录了每条消息,因此 slf4j 日志是冗余的。对于低延迟编码,我们使用 Chronicle Queue 进行所有记录,几乎不使用日志记录,否则它可能是最大的延迟来源。作为一项策略,我们在启动后没有 info 级别的日志记录。即,要么是错误/警告(默认开启),要么是调试日志(默认关闭)。即,决定是否真正需要它。
表 3. 仅使用 Chronicle Queue 记录的 RTT 延迟分布
| GC 选项 | p99 | p99.99 |
|------|------|--------|
| Parallel 大 Eden,无大页面 | **7.14** | **8.94** |
| ZGC,无大页面 | 7.82 | 9.36 |
| G1 + COH,无大页面 | 8.30 | 12.4 |
| G1,2 MiB 大页面 | 8.59 | 154.4 |
| Shenandoah 分代,无大页面 | 7.83 | 233.7 |
正如预期,p99 下降了大约 2 微秒,但重要的是 p99.99 下降了三个数量级。此外,由于工作负载的变化,首选垃圾收集器也发生了反转——这一切仅仅是因为一条日志记录。在本例中,Parallel GC 表现最佳,而 Shenandoah 在 p99.99 上表现最差。
注意:
p99.99 单位是微秒,比写入 /dev/shm 的日志记录快了 1000 倍以上。
## 结论
最佳垃圾收集器的选择及其调优方式取决于你的工作负载。确保先优化你的工作负载是有意义的,因为:a) 在某些情况下它能产生更大差异,b) 它也能改变你的偏好。
### 此博客的热门文章
### 让多个 AI 优化同一段代码 (http://blog.vanillajava.blog/2025/07/asking-multiple-ai-to-optimise-same-code.html)
由于不同 AI 的实现方式不同,它们并不总是提供相同的答案,也不会始终如一地超越彼此。最佳方法是使用多个 AI,然后选择你最喜欢的一个。我的目标不是根据一个例子来宣布胜者,而是展示使用不同 AI 所能获得的各种答案。我让每个 AI 建议如何更优化地实现以下代码:
```java
private static String formatOffset(int millis) {
String sign = millis < 0 ? "-" : "+";
int saveSecs = Math.abs(millis) / 1000;
int hours = saveSecs / 3600;
int mins = ((saveSecs / 60) % 60);
int secs = (saveSecs % 60);
if (secs == 0) {
if (mins == 0) {
return sign + twoDigitString(hours);
}
return sign + twoDigitString(hours) + twoDigitString(mins);
}
return sign + twoDigitString(hours) + twoDigitString(mins) + twoDigitString(secs);
}
private static String twoDigitString(int value) {
...
```
### 解密 Java 对象大小:紧凑头、压缩普通对象指针等 (http://blog.vanillajava.blog/2024/12/demystifying-java-object-sizes-compact.html)
引言
在 Java 中测量对象的大小并不直观。该平台鼓励你考虑引用和抽象,而不是原始内存使用。然而,理解对象如何装入内存可以带来显著的好处,特别是对于高性能、低延迟的系统。随着时间的推移,JVM 引入了诸如压缩普通对象指针(Compressed Oops)以及最近的紧凑对象头(Compact Object Headers)等优化。这些都会影响对象的大小。理解这些因素有助于你更具体地推理内存使用。
测量对象大小
原则上,你可以通过创建实例并观察 JVM 空闲内存的变化来估算对象的大小。然而,你需要消除某些因素以获得一致的结果。例如,关闭 TLAB 分配(`-XX:-UseTLAB`)可以使内存使用更直接地可观察。重复测量和中位数计算可以减少影响...
### 更新简介 (http://blog.vanillajava.blog/2025/08/updated-biography.html)
Peter Lawrey 是一位澳大利亚/英国软件工程师和企业家,以超低延迟 Java 系统的工作以及领导开源 OpenHFT 库而闻名。他是 Chronicle Software 的创始人兼首席执行官,这是一家总部位于伦敦的公司,其技术用于交易和市场基础设施工作负载。Lawrey 也是公认的 Java 社区人物:他于 2015 年被授予 Java Champion 称号,被会议组织者描述为在 Stack Overflow 上为 Java 和 JVM 标签提供最多答案的人,并撰写了长期运行的 Vanilla Java 博客。(Chronicle Software, javachampions.org, qconnewyork.com, blog.vanillajava.blog)
职业生涯
Lawrey 创立并领导 Chronicle Software,该公司构建用于事件驱动交易和市场数据平台的使能技术。该公司称其软件支撑着多家一级银行的系统;2024 年的一则新闻公告同样将 Chronicle 描述为“为 8 家...
相似文章
加速Plush垃圾收集器
作者详细介绍了为其玩具编程语言Plush的垃圾收集器进行的性能改进,专注于优化复制算法以减少收集时间。
为何低延迟Java仍需严谨的编码纪律?
讨论为何在现代JVM优化下,低延迟Java仍需严谨的编码实践。
垃圾回收的实际成本
一篇技术文章解释了垃圾回收的真实性能成本,对比了 Go、Java、Rust、Swift 和 Python 等语言中的跟踪式 GC、引用计数和编译期内存管理。
JDK 27 G1/Parallel/Serial GC 变更
JDK 27 引入了对 HotSpot 的 stop-the-world 垃圾收集器的显著更改,最重要的是 JEP 523 使 G1 在所有环境中成为默认 GC,同时还包含各种改进、重构和错误修复。
观察 Go 的新垃圾回收器在堆中的移动
Go 1.26 将 Green Tea 设为默认垃圾回收器,提升了缓存友好性。本文通过 Go 和 C# 可视化堆分配,并讨论了非移动回收器和稀疏页面带来的挑战。