虚拟机

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 响应性更好,接口耗时更低。 鉴于 OpenJDK 21 G1 GC 一如既往的惊艳表现,D瓜哥准备整理一下 G1 GC 的主要优化,敬请关注: Java G1 垃圾收集器主要优化。 1. 服务调用监控数据 监控服务调用的相关数据,这是对于用户来说,感知最强烈的相关数据,也是直接关系到服务质量的数据。 1.1. 服务调用次数 从调用次数上来看,五个分组没有大的变化,可以说根本没有达到系统的极限峰值。当然,这才是正常现象,如果日常运行都爆峰值,那说明系统早该扩容了。 图 1. 服务调用次数(秒级) 图 2. 服务调用次数(分钟级) 1.2. 服务调用耗时 整体上讲,J21-Gen-ZGC 的耗时更短,从数据上来看,TP999 能比 J21-G1 的少 10~20ms;TP99 更加夸张,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 } } 理论上,没有什么能阻止我们这样做。我们甚至可以把这种优化看作是 循环判断外提,只不过这里是针对锁而已。然而,这样做的缺点是有可能使锁变得过于粗糙,从而导致特定线程在执行大循环时占用锁。
深入理解 Java 代码块

深入理解 Java 代码块

D瓜哥
在 Java 虚拟机操作码探秘:常量指令 中对 Java 虚拟机操作码中关于常量操作的指令(操作码)做了初步介绍。估计会有人疑问:文中的“栈”、“栈顶”等是什么?接下来就准备解答这些疑问。 在答疑解惑之前,先来了解一下 Java 编译器对 Java 代码中的代码块是如何处理的?常见的代码块有普通代码块和静态代码块,下面对其做分别介绍。由于涉及到构造函数,所以,先对构造函数做一个介绍。 构造函数 无构造函数 先来看看当没有声明构造函数时,编译结果是什么样的: /** * 无构造函数示例 * * @author D瓜哥 · https://www.diguage.com */ public class Example { } 编译后,使用 javap -c 查看一下编译结果: $ javap -c Example Compiled from "Example.java" public class Example { public Example(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return } 从结果上来看:编译器自动给没有声明构造函数的类,生成了一个无参构造函数,并且在其中调用了父类(这里是 Object)的无参构造函数。这是大家都熟知的基础知识。 有参构造函数 再来看看当有声明参数的构造函数时,编译结果是什么样的: /** * 有参构造函数示例 * * @author D瓜哥 · https://www.
Java 虚拟机操作码探秘:常量指令

Java 虚拟机操作码探秘:常量指令

D瓜哥
在 Java 虚拟机指令(操作码)集 中给出了一个操作码的列表。针对所有的指令,仅仅给出了一个大概介绍,对理解来说可以说毫无助力。为了弥补这个短板,这里也学习 “Hessian 协议解释与实战”系列 那样,来一个详细解释和实战,配合实例来做个深入分析和讲解。这是这个系列的第一篇文章,就以列表中第一部分“常量”指令开始。 从 Java 虚拟机指令(操作码)集 列表上来看,一共 21 个指令;按照处理数据的类型,合并同类项后,剩下有 nop、 aconst_null、 iconst_<i>、 lconst_<l>、 fconst_<f>、 dconst_<d>、 bipush、 sipush、 ldc 和 ldc2_w 等几个指令。下面,按照顺序,对其进行一一讲解。 操作码助记符的首字母一般是有特殊含义的,表示操作码所作用的数据类型: i 代表对 int 类型的数据操作; l 代表 long; s 代表 short; b 代表 byte;c 代表 char;f 代表 float, d 代表 double; a 代表 reference。 尖括号之间的字母指定了指令隐含操作数的数据类型,<n> 代表非负的整数; <i> 代表是 int 类型数据; <l> 代表 long 类型; <f> 代表 float 类型; <d> 代表 double 类型。 另外还需要指出一点:这种指令表示法在整个 Java 虚拟机规范之中都是通用的。
Java 虚拟机指令(操作码)集

Java 虚拟机指令(操作码)集

D瓜哥
最近在研究 Java 虚拟机字节码。在 《Java虚拟机规范》 看到一个整理完整的 Java 虚拟机指令集(也叫操作码)列表。转载过来,方便查阅。 关于 Java 虚拟机指令(操作码),准备写一个“探秘”系列: Java 虚拟机操作码探秘:常量指令 — 重点介绍一下关于“常量”指令。 分类 操作码 助记符 指令含义 常量 0 0x00 nop 什么都不做 1 0x01 aconst_null 将 null 推送至栈顶 2 0x02 iconst_m1 将 int 类型 -1 推送至栈顶 3 0x03 iconst_0 将 int 类型 0 推送至栈顶 4 0x04 iconst_1 将 int 类型 1 推送至栈顶 5 0x05 iconst_2 将 int 类型 2 推送至栈顶 6 0x06 iconst_3 将 int 类型 3 推送至栈顶