Java

深入研究 BeanFactoryPostProcessor

深入研究 BeanFactoryPostProcessor

D瓜哥
D瓜哥在 Spring 扩展点概览及实践 中概要性地介绍了一下 Spring 的核心扩展点。里面也提到了 BeanFactoryPostProcessor 和 BeanDefinitionRegistryPostProcessor,但仅仅提了一句,没有深入研究。在 Spring 扩展点实践:整合 MyBATIS 中,由于 MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor 接口,也只是简单介绍了一些作用,又一次没有深入研究。 最近,在开发一个插件时,遇到了一个问题:利用 BeanFactoryPostProcessor 对一些特定 BeanDefinition 设置属性,但生成的 Bean 却没有相关的属性值。由此,对 BeanFactoryPostProcessor 做了一些研究。记录一下,以备不时之需。 Spring 启动流程简介 在 Spring 启动流程概述 中,D瓜哥对 Spring 的启动流程做了比较详细的介绍。同时画了一张启动流程图,如下: 图 1. AbstractApplicationContext.refresh — 重塑容器 从该图中可以明显看到,如果需要对 Spring 的 BeanDefinition 做些修改,那么,就需要通过实现 BeanFactoryPostProcessor 接口,来对 Spring 做些扩展。坦白讲,为了上述流程图只展示了一个非常概要性的流程。如果深入一下 invokeBeanFactoryPostProcessors 方法的细节,会发现这又是一番天地。 BeanFactoryPostProcessor 调用详解 D瓜哥把 invokeBeanFactoryPostProcessors 方法的流程图也画了出来,细节如下: 图 2. BeanDefinitionRegistryPostProcessor & BeanFactoryPostProcessor 调用过程 从这张流程图上可以看出 BeanFactoryPostProcessor 的调用过程,比在 Spring 启动流程概述 中介绍的要复杂很多: 首先,执行 BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry 方法,顺序如下: 关于 BeanDefinitionRegistryPostProcessor 的处理流程,D瓜哥在 Spring 扩展点概览及实践:BeanDefinitionRegistryPostProcessor 中有更详细的描述,不了解的朋友请参考那篇文章的介绍。
生产环境中 Java 21 启动参数

生产环境中 Java 21 启动参数

D瓜哥
在 OpenJDK 21 升级指南 中,给大家分享了一下升级到 OpenJDK 21 中遇到的一些问题。文末留了一个小问题:生产环境的 Java 21 启动参数怎么配置?这篇文章将给出 D瓜哥的答案。 先说明一下生产环境的机器配置:4C8G,四个内核,8G 内存。 启动参数 鉴于 JVM GC 性能测试(二):递增流量 和 JVM GC 性能测试(三):真实流量 中,G1 GC 的惊艳表现,这里分别提供 Gen ZGC 和 G1 GC 两个配置。 两个配置差距级小,为了方便复制粘贴,还是分两个来展示。 Gen ZGC 配置 追求极致低延迟,就上 GenZGC,它通过牺牲大约 10% 的吞吐量,换来无与伦比的低延时。 注意:使用时,请修改日志目录! ## 变量配置 ####################################################################### # java -XshowSettings:all --展示所有配置项(测试发现也不全) -Dfile.encoding=UTF-8 # https://zhuanlan.zhihu.com/p/455313866 # https://zhuanlan.zhihu.com/p/455746995 # https://blog.csdn.net/u014149685/article/details/83002405 # 随机数来源 -Djava.security.egd=file:/dev/./urandom -Djava.security=file:/dev/./urandom # https://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html # https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/net/doc-files/net-properties.html # DNS 过期时间 -Dnetworkaddress.cache.ttl=10 # -Dsun.net.client.defaultConnectTimeout=60000 -Dsun.net.client.defaultReadTimeout=60000 #-Dsun.net.inetaddr.ttl=300 # https://mdnice.com/writing/47e729bbf8e44431a396a481ed173dae -Djava.awt.headless=true # https://blog.csdn.net/maverick0/article/details/8282472 -Djmagick.systemclassloader=no # From Cassandra # On Java >= 9 Netty requires the io.netty.tryReflectionSetAccessible system property # to be set to true to enable creation of direct buffers using Unsafe. Without it, # this falls back to ByteBuffer.allocateDirect which has inferior performance and # risks exceeding MaxDirectMemory # https://blog.csdn.net/jdcdev_/article/details/132843927 -Dio.netty.tryReflectionSetAccessible=true # 内部中间件 # 注意:一些中间件会内嵌 Netty,这里建议同步修改其相关参数配置。 -Dump.profiler.shade.io.netty.tryReflectionSetAccessible=true -Dtitan.profiler.shade.io.netty.tryReflectionSetAccessible=true # Revert changes in defaults introduced in https://netty.io/news/2022/03/10/4-1-75-Final.html -Dio.netty.allocator.useCacheForAllThreads=true -Dio.netty.allocator.maxOrder=11 # 内部中间件 # 理由上面已讲 -Dump.profiler.shade.io.netty.allocator.useCacheForAllThreads=true -Dump.profiler.shade.io.netty.allocator.maxOrder=11 # Byte Buddy 支持21 -Dnet.bytebuddy.experimental=true -Dpfinder.shade.net.bytebuddy.experimental=true ## 参数配置 ##################################################################### # https://jacoline.dev/inspect -- JVM 参数诊断 # https://chriswhocodes.com/corretto_jdk21_options.html # https://docs.oracle.com/en/java/javase/21/docs/specs/man/java.html # https://blog.csdn.net/wxb880114/article/details/119888587 # https://www.cnblogs.com/three-fighter/p/14644152.html #- https://www.skjava.com/article/2134434173 # 解锁诊断参数 -XX:+UnlockDiagnosticVMOptions # 解锁试验参数 -XX:+UnlockExperimentalVMOptions # 启用 ZGC -XX:+UseZGC # 启用分代ZGC -XX:+ZGenerational # https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html # 加快 GC 的时间和能力 -XX:ZAllocationSpikeTolerance=5 -XX:ConcGCThreads=2 -XX:ParallelGCThreads=4 # G1 GC #-XX:+UseG1GC #-XX:MaxGCPauseMillis=50 # 初始堆大小,等价于 -XX:InitialHeapSize -Xms4608m # 弱最大堆,尽量保持,但是可以突破 #-XX:SoftMaxHeapSize=3g # 最大堆大小,等价于 -XX:MaxHeapSize -Xmx4608m # 归还未使用的内存 #-XX:+ZUncommit # 设置每个线程的堆栈大小,等价于 -XX:ThreadStackSize=512k -Xss512k # https://cloud.tencent.com/developer/article/1408384 # 本地内存大小 -XX:MaxDirectMemorySize=512m # https://cloud.tencent.com/developer/article/2277327 # https://cloud.tencent.com/developer/article/2277328 # https://cloud.tencent.com/developer/article/2277329 # 元空间 # 设置为 256m 时,发生过一次频繁 GC 导致应用无法相应的问题 -XX:MetaspaceSize=512m # 最大元空间 -XX:MaxMetaspaceSize=512m # https://cloud.tencent.com/developer/article/1408773 # https://blog.csdn.net/lidf1992/article/details/75050219 # 编译代码缓存空间 -XX:ReservedCodeCacheSize=256m # https://cloud.tencent.com/developer/article/1408827 # https://malloc.se/blog/zgc-jdk15 # https://tinyzzh.github.io/java/jvm/2022/04/24/JVM_CompressedOops.html # https://www.cnblogs.com/star95/p/17512212.html -- 由于从 JDK15 开始, # -XX:+UseCompressedClassPointers 与 -XX:-UseCompressedOops 之间的强 # 关联被打破,文章里关于上述这种搭配是不正确的。 TODO 可以从新测试验证一线。 # TODO 如果开启 -XX:+UseCompressedClassPointers,不确定 32M 是否够用? # https://www.zhihu.com/question/268392125 -XX:+UseCompressedClassPointers -XX:CompressedClassSpaceSize=48M # 关闭热度衰减 -XX:-UseCounterDecay # 内存占座 -XX:+AlwaysPreTouch # 禁止代码中显示调用GC -XX:+DisableExplicitGC # 关闭安全点间隔 -XX:GuaranteedSafepointInterval=0 # 避免循环无法进入安全点的问题 -XX:+UseCountedLoopSafepoints # https://blog.csdn.net/m0_46596655/article/details/123606813 -XX:LoopStripMiningIter=1000 # 打印命令行参数 -XX:+PrintCommandLineFlags # 显式地并发处理 GC 调用 -XX:+ExplicitGCInvokesConcurrent # https://panlw.github.io/15320998566522.html -XX:AutoBoxCacheMax=20000 # https://blog.csdn.net/zshake/article/details/88796414 # 省略异常栈信息从而快速抛出 -XX:-OmitStackTraceInFastThrow # https://www.jianshu.com/p/c9259953ca38 # 致命错误日志文件 -XX:ErrorFile=/path/to/log/jvm/hs_err_%p.log # https://blog.csdn.net/lusa1314/article/details/84134458 # https://juejin.cn/post/7127557371932442632 # 当JVM发生OOM时,自动生成DUMP文件。 -XX:+HeapDumpOnOutOfMemoryError # 设置上述DUMP文件路径 -XX:HeapDumpPath=/path/to/log/jvm/ # https://juejin.cn/post/6959405798556434440 # 设置 JFR 相关参数 # TODO 感觉这里不全乎,似乎需要 -XX:+FlightRecorder 来启用 # TODO 似乎可以设置文件,例如: -XX:StartFlightRecording=duration=200s,filename=flight.jfr # 不确定文件名是否可以这样配置,测试一下_%p-%t # Amazon Corretto JDK OK;Eclipse Temurin 不识别,并且监控报错 #-XX:StartFlightRecording=delay=5s,disk=true,dumponexit=true,duration=24h,maxage=5d,maxsize=2g,filename=/path/to/log/jvm/jfr_%p-%t.jfr.log #-XX:FlightRecorderOptions=maxchunksize=128m #-XX:StringDeduplicationAgeThreshold=threshold? TODO 测试之后才可以定 # https://zhuanlan.zhihu.com/p/111886882 # https://github.com/apache/cassandra/tree/trunk/conf # https://github.com/elastic/elasticsearch/blob/main/distribution/src/config/jvm.options # java -Xlog:help # 日志配置 -Xlog:gc*=debug,stringdedup*=debug,heap*=trace,age*=debug,promotion*=trace,jit*=info,safepoint*=debug:file=/path/to/log/jvm/gc_%p-%t.log:time,pid,tid,level,tags:filecount=10,filesize=500M # 分开设置可用,使用分开的配置 #-Xlog:gc*=debug,stringdedup*=debug,heap*=trace,age*=debug,promotion*=trace:file=/path/to/log/jvm/gc-%t.log:utctime,level,tags:filecount=10,filesize=200M #-Xlog:jit*=info:file=/path/to/log/jvm/jit_compile-%t.log:utctime,level,tags:filecount=10,filesize=50M #-Xlog:safepoint*=debug:file=/path/to/log/jvm/safepoint-%t.log:utctime,level,tags:filecount=10,filesize=50M # https://stackoverflow.com/a/44059335 # https://openjdk.org/jeps/261 # https://www.diguage.com/post/upgrade-to-openjdk21/ -- 内有详细介绍 # 开启模块权限:下面是D瓜哥需要的模块,请根据自己实际需求来调整。 --add-exports java.base/sun.security.action=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.security=ALL-UNNAMED --add-opens java.base/java.time=ALL-UNNAMED --add-opens java.base/java.util.concurrent.locks=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/jdk.internal.loader=ALL-UNNAMED # Netty 内部需要 https://stackoverflow.com/a/57892679 # https://github.com/netty/netty/issues/7769 # https://blog.csdn.net/thewindkee/article/details/123618476 --add-opens java.base/jdk.internal.misc=ALL-UNNAMED --add-opens java.base/sun.net.util=ALL-UNNAMED # 设置 -Dio.netty.tryReflectionSetAccessible=true 后,不设置该值也会报错 --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/sun.reflect.generics.reflectiveObjects=ALL-UNNAMED --add-opens java.base/sun.util.calendar=ALL-UNNAMED --add-opens java.management/com.sun.jmx.mbeanserver=ALL-UNNAMED --add-opens java.management/java.lang.management=ALL-UNNAMED --add-opens java.management/sun.management=ALL-UNNAMED --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED
使用 OpenRewrite 优化代码

使用 OpenRewrite 优化代码

D瓜哥
在 OpenJDK 21 升级指南 中提到, OpenRewrite 可以帮忙解决一些升级 OpenJDK 中发现的问题。随着不断的探索,D瓜哥发现,OpenRewrite 的功能远远不止这些。下面就挑选一些重要的功能来给大家做一些讲解。 为了方便查看改动点,建议将代码交给版本管理工具,比如 Git,来管理。 快速入门 OpenRewrite 是一套对源码做重构的大型生态系统,可以帮助开发人员减少技术债。所以,它提供了一套的相关工具。对于大多数开发人员来说,最方便的也许就是基于 Maven 插件的相关工具。这里以对 Java 的 import 语句排序来为示例展示一下 OpenRewrite 的使用方法。 在项目的 pom.xml 中增加如下配置: <!-- @author: D瓜哥 · https://www.diguage.com --> <plugin> <groupId>org.openrewrite.maven</groupId> <artifactId>rewrite-maven-plugin</artifactId> <version>5.30.0</version> <configuration> <activeRecipes> <!-- import 排序 --> <!-- https://docs.openrewrite.org/recipes/java/orderimports --> <recipe>org.openrewrite.java.OrderImports</recipe> </activeRecipes> </configuration> </plugin> 然后执行如下命令: mvn rewrite:run 执行会输出一大堆东西,这里就不再展示,执行完成后,使用 Git 查看一下改动点。如下图: 图 1. 使用 OpenRewrite 排序 import 的改动点 将这些修改点提交,就完成了一次优化, OpenRewrite 的基本使用,你学废了吗? 这里再多说一句: 由于 OpenRewrite 精巧的设计,可以通过使用不同的处方,进行各种各样的优化。所以,最重要的一点就是了解 OpenRewrite 各种不同的处方及使用办法。下面就介绍一下常用的处方及使用办法。 常用处方 升级到 Java 21 在 OpenJDK 21 升级指南 中提到,可以使用“科技与狠活”来解决很多升级中遇到的问题。这里就来实操一把。
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 ..... 5.3.33 -> 6.1.6 [INFO] org.springframework:spring-beans ....... 5.3.33 -> 6.1.6 [INFO] org.springframework:spring-context ..... 5.3.33 -> 6.1.6 [INFO] org.springframework:spring-core ........ 5.3.33 -> 6.1.6 [INFO] org.springframework:spring-jdbc ........ 5.3.33 -> 6.1.6 [INFO] org.springframework:spring-web ......... 5.3.33 -> 6.1.6 [INFO] org.mybatis:mybatis-2-spring ............ 1.1.0 -> 1.2.0 [INFO] org.mybatis:mybatis-spring .............. 2.1.1 -> 2.1.2 [INFO] org.junit.jupiter:junit-jupiter ........ 5.9.3 -> 5.10.2 [INFO] org.junit.jupiter:junit-jupiter-api .... 5.9.3 -> 5.10.2
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 的一半。 J21-Gen-ZGC 和 J21-G1 还是一如既往的稳。 这次测试中,J17-ZGC 也很稳,有些出乎意料。但是,结合下面 JVM CPU 使用率 和 系统 CPU 使用率 来看,J17-ZGC 和 J21-ZGC 的 CPU 使用率早早就达到 90%+,再结合上面两个测试,从稳定性来看,J17-ZGC 和 J21-ZGC 只能被排除掉。
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 1050,00:09:30 ~ 00:19:30 1100,00:19:30 ~ 00:29: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; CPU 消耗:J21-G1 < J8-G1 < J17-ZGC < J21-Gen-ZGC < 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 来做测试,但是,这样面临好几个问题:
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}
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 } } 理论上,没有什么能阻止我们这样做。我们甚至可以把这种优化看作是 循环判断外提,只不过这里是针对锁而已。然而,这样做的缺点是有可能使锁变得过于粗糙,从而导致特定线程在执行大循环时占用锁。 实验 要回答这个问题,最简单的方法就是找到当前 Hotspot 优化的正面证据。幸运的是,有了 JMH,这一切都变得非常简单。它不仅有助于建立基准,还有助于工程中最重要的部分—​基准分析。让我们从一个简单的基准检查程序开始: @Fork(..., jvmArgsPrepend = {"-XX:-UseBiasedLocking"}) @State(Scope.Benchmark) public class LockRoach { (1) int x; @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE) public void test() { for (int c = 0; c < 1000; c++) { synchronized (this) { x += 0x42; } } } } 1 完整代码在 这里。 这里有几个重要的技巧: 使用 -XX:-UseBiasedLocking 禁用偏向锁可以避免更长的预热时间,因为偏向锁不会立即启动,而是会在初始化阶段等待 5 秒(参见 BiasedLockingStartupDelay 选项)。 禁用 @Benchmark 的方法内联有助于在反汇编时将其分离。 增加一个神奇的数字 0x42,有助于在反汇编中快速找到增量。