OpenJDK 21 升级指南

OpenJDK 21 升级指南

D瓜哥
OpenJDK 21 已经发布半年有余,在这个版本中, Generational ZGC 也一起发布了。在 ZGC | What’s new in JDK 16 中, Per Lidén 宣称,将 ZGC 的最大停顿时间从 10ms 降低到了 1ms。再加上 JVM GC 性能测试(二):递增流量 和 JVM GC 性能测试(三):真实流量 文中,GenZGC 的惊艳表现,这些种种先进技术,着实充满诱惑,忍不住想吃口螃蟹 🦀。这篇文章,D瓜哥就来分享一下,自己在升级 OpenJDK 21 中的一些经验。 本文仅介绍升级 OpenJDK 的相关内容,ZGC 原理等会专门撰文介绍。 升级依赖 依赖升级不是 KPI,也不涉及需求交付。所以,大多数项目的依赖自从项目创建后,就很少升级。如果想比较顺利地将项目升级到 OpenJDK 21,那么,先将项目所用依赖做一个整体升级是一个事半功倍的操作。可以直接使用 Maven 命令来检查依赖可以升级的情况: mvn versions:display-dependency-updates 执行该命令后,会有如下类似输出: # 检查依赖升级情况 $ mvn versions:display-dependency-updates # 此处省略一万个字 # @author: D瓜哥 · https://www.diguage.com [INFO] org.springframework:spring-aop ......... 5.3.33 -> 6.1.6 [INFO] org.springframework:spring-aspects .
JVM GC 性能测试(三):真实流量

JVM GC 性能测试(三):真实流量

D瓜哥
JVM GC 性能测试系列: JVM GC 性能对比方法 JVM GC 性能测试(一):相同流量 JVM GC 性能测试(二):递增流量 JVM GC 性能测试(三):真实流量 书接上文,在 JVM GC 性能测试(二):递增流量 的最后,D瓜哥提到了一个问题,对于在 JVM GC 性能测试(一):相同流量 和 JVM GC 性能测试(二):递增流量 中存在的巨大 QPS 差异疑惑不解。所以,D瓜哥决定将测试机器接入到线上环境,在真实访问中,观察各个 GC 的表现。 一言以蔽之 J21-Gen-ZGC 和 J21-G1 无论在稳定性,吞吐量以及响应时效性上都非常优秀。 再极端峰值情况,J21-G1 是更好的选择,更加稳定,不容易出凸点。 日常使用,J21-Gen-ZGC 响应性更好,接口耗时更低。 1. 服务调用监控数据 监控服务调用的相关数据,这是对于用户来说,感知最强烈的相关数据,也是直接关系到服务质量的数据。 1.1. 服务调用次数 从调用次数上来看,五个分组没有大的变化,可以说根本没有达到系统的极限峰值。当然,这才是正常现象,如果日常运行都爆峰值,那说明系统早该扩容了。 图 1. 服务调用次数(秒级) 图 2. 服务调用次数(分钟级) 1.2. 服务调用耗时 整体上讲,J21-Gen-ZGC 的耗时更短,从数据上来看,TP999 能比 J21-G1 的少 10~20ms;TP99 更加夸张,J21-Gen-ZGC 的耗时只有 J21-G1 的一半。 J21-Gen-ZGC 和 J21-G1 还是一如既往的稳。
JVM GC 性能测试(二):递增流量

JVM GC 性能测试(二):递增流量

D瓜哥
JVM GC 性能测试系列: JVM GC 性能对比方法 JVM GC 性能测试(一):相同流量 JVM GC 性能测试(二):递增流量 JVM GC 性能测试(三):真实流量 在上一篇文章 JVM GC 性能测试(一):相同流量 中,D瓜哥使用一个总量请求对所有分组的所有机器进行性能测试。但是,经过测试发现了一个问题,同时产生了另外一个问题,有两个问题没有得到很好的解答: 由于服务响应时长直接关系到服务调用次数,当某一台机器出现问题时,整体调用次数就会急剧下降,调用次数加不上去。一个机器出问题,所有机器的访问量就上不去了。这是测试中发现的一个问题。当然,这属于测试工具的问题,别不是 GC 的问题。但是,也影响到我们的压测,也需要解决。 上次测试,这是针对某一个指定服务调用量进行性能测试,那么,无法确定每个 GC 能支撑的极限调用峰值。另外,在极限峰值和超极限峰值的情况下,各个 GC 的表现如何?这个也有待验证。 针对上述两个问题,设计了本次测试。测试方法如下: 各个分组使用一套相同的流量策略: 各个分组几乎同时开始执行测试任务; 调用量从低到高,以此同时使用相关的调用量进行测试; 除最开始预热阶段的调用量外,后续每个调用量都持续进行十分钟的测试。 针对每个 GC 分组单独设定一套调用发量程序,这个保证各个 GC 分组直接不相互影响。 最后,再分析调用量相同时段的各个 GC 表现,就可以看到各个 GC 的极限峰值。 为了保留更多细节,本文所有截图都是在 34 吋带鱼屏下,使用全屏模式展示并截图的。如果看不清楚,可以右击在新页面打开图片来查看。 具体流量及时间段: 750, 23:14:30 ~ 23:19:30 800, 23:19:30 ~ 23:29:30 850, 23:29:30 ~ 23:39:30 900, 23:39:30 ~ 23:49:30 950, 23:49:30 ~ 23:59:30 1000,23:59:30 ~ 00:09:30
JVM GC 性能测试(一):相同流量

JVM GC 性能测试(一):相同流量

D瓜哥
JVM GC 性能测试系列: JVM GC 性能对比方法 JVM GC 性能测试(一):相同流量 JVM GC 性能测试(二):递增流量 JVM GC 性能测试(三):真实流量 在上一篇文章 JVM GC 性能对比方法 介绍了性能对比的方法,这篇文章就根据该方法对上述提到的5种 JVM GC 进行性能测试。 在正式测试之前,D瓜哥进行了多次小流量试探性测试,来探索一个合适的量。找到一个比较平稳的量后,乘以机器数量,获得一个每秒总计请求量,最后使用该总量数据去做压测。 根据多次测试的数据来看,最后选择的是每台每秒 500 QPS,5 个分组,每个分组 5 台机器,所以,每秒的请求总量是: 500 * 5 * 5 = 12500 QPS;每个分组每分钟的总量是:500 * 5 * 60 = 150000 QPS。使用每台机器以此使用 100 QPS,200 QPS,300 QPS,400 QPS 各运行一分钟来对系统进行预热。最后以每台每秒 500 QPS 的访问量来对测试机器进行持续十分钟的性能测试,最后分析这十分钟的相关数据。 一言以蔽之 服务稳定性:J21-Gen-ZGC、J21-G1、J8-G1 稳定性最好;J17-ZGC 有轻微波动;J21-ZGC 有剧烈波动; 服务耗时 TP999:J21-Gen-ZGC < J17-ZGC < J21-G1 < J8-G1 < J21-ZGC;
JVM GC 性能对比方法

JVM GC 性能对比方法

D瓜哥
JVM GC 性能测试系列: JVM GC 性能对比方法 JVM GC 性能测试(一):相同流量 JVM GC 性能测试(二):递增流量 JVM GC 性能测试(三):真实流量 现在部门内部绝大部分应用都还在使用 OpenJDK 8,计划推进部门升级 JDK 到 OpenJDK21。本着实事求是,用数据说话的原则,准备对如下 GC 做性能测试: OpenJDK 8 G1 GC(以下称 J8-G1。具体版本号:1.8.0_321-b07。) OpenJDK 17 ZGC(以下称 J17-ZGC。具体版本号:17.0.9+9。) OpenJDK 21 G1(以下称 J21-G1。具体版本号:21.0.2+13-LTS。) OpenJDK 21 ZGC(以下称 J21-ZGC。具体版本号:21.0.2+13-LTS。) OpenJDK 21 Gen ZGC(以下称 J21-Gen-ZGC。具体版本号:21.0.2+13-LTS。) 所有 OpenJDK 版本都是选用相同大版本号里的最高的版本。所有的机器都是 4C8G 的配置,JVM 堆栈内存设置为 4608M 。 为了减少不必要的干扰,JVM 相关参数也尽可能做到了一致或者接近。(等测试完,D瓜哥会把相关参数也分享出来。) 测试对象 由于D瓜哥所处的部门是一个直接面向用户的线上业务部门,所以,大部分系统是直接面对用户,接受用户访问的在线业务系统。所以,为了服务线上业务系统的需求,测试对象的选择就限定在了类似的场景中。测试对象是线上接受用户访问的一个服务。结构如下: 图 1. 压测接口依赖关系图 该接口有外部依赖服务,也有数据库查询,是一个微服务架构下典型的在线服务接口。 测试方法 原本计划是想直接通过上线,将线上不同分组的机器使用不同的 GC 来做测试,但是,这样面临好几个问题: 由于正是环境上线需要审批,每次上线是针对一个构建包做上线,由于基于三个不同版本的 OpenJDK,所以,至少需要上线三次。 线上环境,不同分组的机器数量是不一样的,所以,不方便对比。比如,对比不同分组的响应请求数量。 性能测试势必会影响到线上的业务处理。如果引发客诉,鱼有没有迟到不确定,但是绝对惹一身腥。
JVM 剖析花园:2 - 透明大页

JVM 剖析花园:2 - 透明大页

问题 什么是大页(Large Page)?什么是透明大页(Transparent Huge Page)?它对我有什么帮助? 理论 虚拟内存现在已被视为理所当然。现在只有少数人还记得,更不用说做一些“真实模式”编程了,在这种情况下,你会接触到实际的物理内存。相反,每个进程都有自己的虚拟内存空间,该空间被映射到实际内存上。例如,两个进程在相同的虚拟地址 0x42424242 上拥有不同的数据,而这些数据将由不同的物理内存支持。现在,当程序访问该地址时,应将虚拟地址转换为物理地址。 图 1. 虚拟内存地址与物理内存地址之间的关系 这通常由操作系统维护 “页表”,硬件通过“页表遍历”来实现地址转换。如果在页面粒度上维护翻译,整个过程就会变得简单。但这样做的成本并不低,而且每次内存访问都需要这样做!因此,还需要对最新的翻译进行小型缓存,即 转译后备缓冲区(Translation Lookaside Buffer (TLB))。TLB 通常很小,只有不到 100 个条目,因为它的速度至少要与 L1 缓存相当,甚至更快。对于许多工作负载来说,TLB 未命中和相关的页表遍历需要大量时间。 既然我们无法将 TLB 做得更大,那么我们可以做其他事情:制作更大的页面!大多数硬件有 4K 基本页和 2M/4M/1G “大页”。用更大的页来覆盖相同的区域,还能使页表本身更小,从而降低页表遍历的成本。 在 Linux 世界中,至少有两种不同的方法可以在应用程序中实现这一点: hugetlbfs。切出系统内存的一部分,将其作为虚拟文件系统公开,让应用程序通过 mmap(2) 从其中获取。这是一个特殊的接口,需要操作系统配置和应用程序更改才能使用。这也是一种“要么全有,要么全无”的交易:分配给 hugetlbfs(持久部分)的空间不能被普通进程使用。 透明大页(Transparent Huge Pages (THP))。让应用程序像往常一样分配内存,但尽量以透明方式为应用程序提供大容量页面支持的存储空间。理想情况下,不需要更改应用程序,但我们会看到应用程序如何从了解 THP 的可用性中获益。但在实际应用中,会产生内存开销(因为会为小文件分配整个大页面)或时间开销(因为 THP 有时需要对内存进行碎片整理以分配页面)。好在有一个中间方案:通过 madvise(2) 可以让应用程序告诉 Linux 在哪里使用 THP。 不明白为什么术语中会交替使用 "large "和 "huge"。总之,OpenJDK 支持这两种模式: $ java -XX:+PrintFlagsFinal 2>&1 | grep Huge bool UseHugeTLBFS = false {product} {default} bool UseTransparentHugePages = false {product} {default} $ java -XX:+PrintFlagsFinal 2>&1 | grep LargePage bool UseLargePages = false {pd product} {default} -XX:+UseHugeTLBFS 将 Java 堆映射到 hugetlbfs 中,后者应单独准备。
JVM 剖析花园:1 - 锁粗化及循环

JVM 剖析花园:1 - 锁粗化及循环

D瓜哥
“JVM 剖析花园”是由 JVM 研发专家及性能极客 Aleksey Shipilëv 撰写的一个系列文章,专门介绍一些有关 JVM 的基本知识。笔者也是前几年无意间发现的一片宝藏文章。早就有翻译过来,介绍给大家的想法,可惜一直未能付诸实践。最近在查资料时,无意间又翻到了这个系列,遂下定决心,完成这个萌发已久的小想法。 为了便于理解,对该系列的名字做了微调,原文是“JVM Anatomy Quarks”,将原文的“Quarks”(夸克)翻译为了“花园”。 “JVM 解剖花园”是一个正在进行中的小型系列文章,每篇文章都会介绍一些有关 JVM 的基本知识。这个名字强调了一个事实,即单篇文章不能孤立地看待,这里描述的大部分内容都会很容易地相互影响。 阅读这篇文章大约需要 5-10 分钟。因此,它只针对单一主题、单一测试、单一基准和单一观察进行深入探讨。这里的证据和讨论可能是轶事,并没有对错误、一致性、写作风格、语法和语义错误、重复或一致性进行实际审查。请自行承担使用和/或信任的风险。 以上是该系列介绍。这里介绍一次,后续文章不再赘述。 问题 众所周知,Hotspot 可以进行 锁粗化优化,有效合并多个相邻的锁定块,从而减少锁定开销。它能有效地对如下代码做优化: synchronized (obj) { // statements 1 } synchronized (obj) { // statements 2 } 优化后: synchronized (obj) { // statements 1 // statements 2 } 现在,今天提出的一个有趣问题是:Hotspot 是否会对循环进行这种优化?例如: for (...) { synchronized (obj) { // something } } 是否会被优化成如下这样: synchronized (this) { for (...) { // something } } 理论上,没有什么能阻止我们这样做。我们甚至可以把这种优化看作是 循环判断外提,只不过这里是针对锁而已。然而,这样做的缺点是有可能使锁变得过于粗糙,从而导致特定线程在执行大循环时占用锁。
Spring Boot 应用程序浪费的内存

Spring Boot 应用程序浪费的内存

D瓜哥
当今世界被广泛浪费的资源之一是:内存。由于编程效率低下,内存浪费量惊人(有时 "令人震惊")。我们在多个企业应用程序中都看到了这种情况。为了证明这一点,我们进行了一项小型研究。我们分析了著名的 Spring Boot Pet Clinic 应用程序,看看它浪费了多少内存。该应用程序由社区设计,旨在展示如何使用 Spring 应用程序框架构建简单但功能强大的面向数据库的应用程序。 环境 Spring Boot 2.1.4.RELEASE Java SDK 1.8 Tomcat 8.5.20 MySQL 5.7.26 with MySQL Connector/J 8.0.15 压力测试 我们使用流行的开源压力测试工具 Apache JMeter 进行压力测试。我们使用以下设置执行了 30 分钟的压力测试: 线程数(用户)- 1000(连接到目标的用户数量) 上升周期(秒) - 10。所有请求开始的时间范围。根据我们的配置,每 0.01 秒将启动 1 个新线程,即 100 个线程/秒。 循环次数 - 永久。这 1000 个线程将背靠背执行测试迭代。 持续时间(秒) - 1800。启动后,1000 个线程持续运行 1800 秒。 图 1. JMeter 设置 我们在负载测试中使用了以下场景: 在系统中添加新的宠物主人。 查看宠物主人的相关信息。 向系统中添加一只新宠物。 查看宠物相关信息。 在宠物探视历史中添加探视信息。 更新宠物相关信息。 更新宠物主人的相关信息。 通过搜索主人姓名查看主人信息。 查看所有主人的信息。 如何测量内存浪费? 业界有数百种工具可以显示内存使用量。但是,我们很少遇到能测量因低效编程而浪费的内存量的工具。 HeapHero 是一款简单的工具,它可以分析堆转储,并告诉我们由于编程效率低下而浪费了多少内存。
GC 调优成功案例:减少新生代的大小

GC 调优成功案例:减少新生代的大小

D瓜哥
当对垃圾回收性能做调优时,不仅能改善垃圾回收暂停时间,还能改善整个应用程序的响应时间并降低云计算成本。最近,我们帮助调整了一个流行应用程序的垃圾回收行为。仅仅是一个微小的改动,就带来了巨大的改善。让我们在这篇文章中讨论一下这个垃圾回收调整的成功案例。 垃圾收集关键绩效指标 有句名言叫“无法衡量的东西就无法优化”。说到垃圾回收的调整,您只需关注 3 个主要关键绩效指标 (KPI): GC 暂停时间 GC 吞吐量 CPU 消耗量 垃圾回收运行时,会暂停应用程序。“GC 停顿时间”表示应用程序在垃圾回收事件中停顿的时间。该指标以秒或毫秒为单位。 “GC 吞吐量”表示应用程序处理客户事务的总时间与处理垃圾回收活动的总时间之比。该指标以百分比为单位。例如,如果有人说他的应用程序的 GC 吞吐量是 98%,这表明该应用程序有 98% 的时间用于处理客户活动,其余 2% 的时间用于处理垃圾回收活动。 即使是处理一个简单的请求,现代应用程序也会创建成千上万个对象。因此,垃圾收集器必须在后台不断运行,以释放为每个请求创建的成千上万个对象。因此,垃圾回收往往会消耗大量的 CPU。因此,在调整垃圾回收性能时,还应研究 CPU 消耗。要了解有关这些 KPI 的更多信息,请参阅: 内存调整: 关键性能指标。 如何获取这些 KPI? 在调优垃圾回收性能时,垃圾回收日志是您最好的朋友。您可以通过 这篇文章 给出的 JVM 参数在应用程序中启用垃圾回收日志。建议始终开启垃圾回收日志,因为它能提供丰富的信息,有助于预测中断、排除生产问题并帮助进行容量规划。此外,启用垃圾收集不会给应用程序增加任何明显的开销。 启用垃圾收集日志后,您可以使用免费的垃圾收集日志分析工具,如 GCeasy、 IBM GC & Memory visualizer 和 Google Garbage cat 等,查看上述关键绩效指标。 在下面这篇文章,教你 如何进行 GC 日志分析。 垃圾回收行为基线 介绍到此为止。让我们回到本文最初的主题。我们在这个流行的应用程序上启用了垃圾回收日志。我们让应用程序运行了 24 小时。然后,我们将生成的 GC 日志文件上传到 GCeasy 工具。该工具提供了具有洞察力的图表和 GC KPI。该应用程序的 GC 吞吐量为 96.176%,平均暂停时间为 12.429 秒。
应用程序的内存是大还是小?

应用程序的内存是大还是小?

D瓜哥
应该在内存容量大的少量实例(即机器)上运行应用程序,还是在内存容量小的大量实例上运行应用程序?哪种策略是最佳的?这个问题可能会经常遇到。在开发应用程序长达 20 年,且构建了 JVM 性能工程/故障排除工具( GCeasy、 FastThread,、 HeapHero)之后,我仍然不知道这个问题的正确答案。同时,我相信这个问题也没有非黑即白的答案。在本文中,我想与大家分享一下我对这个问题的看法和经验。 两个数十亿美元企业的故事 由于我们的 JVM 性能工程/故障排除工具已广泛应用于各大企业,因此我有机会看到世界级企业应用的实际实施情况。最近,我有机会参观了两家高速成长的科技公司(如果我说出他们的名字,读这篇文章的人都会知道)。这两家公司的总部都在硅谷。它们的业务是技术,因此在工程设计方面很有一套。它们是华尔街的宠儿,享有极高的估值。它们的市值高达数十亿美元。它们是现代企业蓬勃发展的典型代表。在我们的对话中,让我们称这两家企业为公司 A 和公司 B。 在内存大小方面,两家企业都采用了两个极端,这让我感到非常惊讶。公司 A 将堆大小(即 -Xmx)设置为 250GB,而公司 B 则将堆大小设置为 2GB:公司 A 的堆大小是公司 B 的 125 倍。两家公司都对自己的内存大小设置很自信。俗话说:"事实胜于雄辩",两家企业都在扩大规模,处理数十亿的关键业务交易。 两家公司都从事相同的业务,收入/市值大致相同,位于同一地理区域,在同一时间点采用两种极端的内存大小,这真是一次绝佳的体验。鉴于这种现实生活中的真实经验,正确的答案是什么?大内存还是小内存?我的结论是:如果你有一支优秀的团队,采用这两种策略都能取得成功。 大内存容量可能很昂贵 与内存容量小、实例数量多的情况相比,内存容量大、实例(即机器)数量少的情况往往成本较高。以下是基于美国东部(弗吉尼亚州北部)地区 AWS EC2 实例成本的简单计算: m4.16xlarge - 256GB 内存 - Linux 按实例收费:3.2 美元/小时 T3a small - 2GB 内存 - Linux 按实例收费:0.0188 美元/小时 因此,要获得 256GB 内存容量,我们必须获得 128 个 “T3a small” 实例(即 128 个实例 x 2GB = 256GB)。 128 x T3a small - 2GB 内存 - Linux 按实例收费:2.