2024年3月

关于 Java GC 里面的PLAB

之前有篇文章提到过 TALB (Thread-Local Allocation Buffers), 与之类似的概念在年轻代 promote 到 survivor 和 old generation 的时候, 也有 PLAB.

什么是 PLAB

PLAB 是 Promotion Local Allocation Buffers 的缩写, 是 Java 垃圾回收相关的概念. heap 里面通常把heap 分为年轻代和老年代,年轻代又细分为 Eden 区域和 2个 survivor 区域. 当把 Eden 区域的对象 promote 到老年代的时候, 我们称之为 Promotion. 但是在 promote 到老年代之前, 根据设置参数的不同, 可能会现在2个 survivor 区域来回复制几次.

从 Eden 区域复制到 survivor 区域或者 promote 到老年代都是 GC 线程做的事情, 这个时候是 CPU 密集型操作. 并且复制到的目的区域是各个GC 线程同样的目的区域. 这个时候就会出现竞争, 竞争就会出现锁.

为了避免锁竞争, 就要采用分块的概念, 也就是给每个做GC 的线程独自分配一块区域, 是它自己独享的目的区域, 这样就能避免竞争. 于是就有了 PLAB 的概念.

与 PLAB 相关的参数

这是基于 JDK 17 的与 PLAB 相关的参数:

java -XX:+PrintFlagsFinal -version | grep PLAB
   size_t OldPLABSize                              = 1024                                      {product} {default}
    uintx PLABWeight                               = 75                                        {product} {default}
     bool ResizePLAB                               = true                                      {product} {default}
    uintx TargetPLABWastePct                       = 10                                        {product} {default}
   size_t YoungPLABSize                            = 4096                                      {product} {default}

PLAB 与 NUMA

这篇文章讨论了 G1 在 NUMA 情况下如何 promote 对象到老年代的时候, 处理 NUMA 内存相关的问题.
https://sangheon.github.io/2020/11/03/g1-numa.html

PLAB 在 CMS 和 G1 GC 的使用

PLAB 参数的相关概念

0.1 - 诊断之前

在开始诊断之前, 我们通常要确定几件事情, 以帮助我们诊断过程更有效和有针对性.

确认对应的 Java 程序在运行.

有时有人报告说某个程序运行不正常, 我们去查看各种指标和日志, 却找不到相关信息, 各种查找以后发现找错了机器,或者对应的程序其实已经退出了. 所以在线诊断一定要先确定要诊断的程序是否正在运行.

有时候我们登录了某台机器, 却无法查看到对应的Java 程序在运行, 可能是我们权限不够, 又或者是在不同的 pod/container 里面. 所以在开始诊断之前, 这些信息最好确认好, 否则会浪费时间.

比如下面的例子中, 我们使用 jps 命令只能看到当时正在运行的 jps 命令本身, 看不到其它 Java 在运行程序. 但是但我们使用 pgrep 命令, 或者 ps 命令的时候, 却能看到这些在 Docker container 里面运行的 Java 程序.

supra@suprabox:~$ jps
3769076 Jps

supra@suprabox:~$ pgrep java
3567
3780
4535

supra@suprabox:~$ ps aux | grep java
7474        3567  0.8  1.8 10073680 308672 ?     Sl   Jan27 511:09 /opt/java/openjdk/bin/java -cp /var/lib/neo4j/plugins/*:/var/lib/neo4j/conf/*:/var/lib/neo4j/lib/* -XX:+UseG1GC -XX:-OmitStackTraceInFastThrow -XX:+AlwaysPreTouch -XX:+UnlockExperimentalVMOptions -XX:+TrustFinalNonStaticFields --console-mode
supra       3780  9.1  9.6 7020176 1561912 ?     Sl   Jan27 5667:41 /usr/share/elasticsearch/jdk/bin/java -Xshare:auto -Des.networkaddress.cache.ttl=60 -Des.networkaddress.cache.negative.ttl=10 -Dio.netty.noUnsafe=true
999         4535  0.1 24.0 8232288 3913624 ?     Ssl  Jan27 104:44 /usr/local/openjdk-8/bin/java -Dlog4j.configuration=file:/opt/janusgraph/conf/log4j-server.properties -Xms4096m -Xmx4096m -javaagent:/opt/janusgraph/lib/jamm-0.3.0.jar

确认 Java 的版本

尽管 Java 会向前兼容, 但不同的 Java 发行版本, 会有不同的默认行为和不同的参数, 确认版本, 帮助我们在后面的诊断中判断某种症状是不是合理.

比如, 对于上面结果中的3个 Java 程序, 它们都运行在 Docker container 中, 我们查看他们的 Java 版本, 发现它们分别都是使用了不同版本的 Java.

supra@suprabox:~$ sudo nsenter -t 3567 -a /opt/java/openjdk/bin/java -version
[sudo] password for supra:
openjdk version "17.0.5" 2022-10-18
OpenJDK Runtime Environment Temurin-17.0.5+8 (build 17.0.5+8)
OpenJDK 64-Bit Server VM Temurin-17.0.5+8 (build 17.0.5+8, mixed mode, sharing)

supra@suprabox:~$ sudo nsenter -t 3780 -a /usr/share/elasticsearch/jdk/bin/java  -version
openjdk version "17.0.1" 2021-10-19
OpenJDK Runtime Environment Temurin-17.0.1+12 (build 17.0.1+12)
OpenJDK 64-Bit Server VM Temurin-17.0.1+12 (build 17.0.1+12, mixed mode, sharing)

supra@suprabox:~$ sudo nsenter -t 4535 -a /usr/local/openjdk-8/bin/java -version
openjdk version "1.8.0_332"
OpenJDK Runtime Environment (build 1.8.0_332-b09)
OpenJDK 64-Bit Server VM (build 25.332-b09, mixed mode)

不同版本的Java 可能对应不同的默认参数, 我们可以通过下面的命令来查看这些默认参数的名字,及默认值:

supra@suprabox:~$ java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal -version
[Global flags]
      int AVX3Threshold                            = 4096                              {ARCH diagnostic} {default}
      int ActiveProcessorCount                     = -1                                        {product} {default}
     intx AliasLevel                               = 3                                      {C2 product} {default}
   size_t TLABSize                                 = 0                                         {product} {default}
                                            ... 省略更多 ... 

这个列表很长, 大多数你可能都用不到, 但是某些值对于以后诊断某些问题的细节很有用. 第一列是类型, 第二列是参数的名字, 第三列显示默认值, 第四列里面的不同值表示不同的意思, 比如是否跟某些平台架构想管, 是不是诊断用的参数, 是不是某个特定的编译器才有的参数等, 最后的 {default} 表示是不是一定有默认值.

确认启动命令及参数

有些参数的不设置是有默认值的, 比如初始堆大小, 有些值是根据不同的平台及硬件特性在启动时根据算法选择的, 有些是用户在启动命令参数设置的, 有些虽然可能有默认值或启动设置值, 但是运行时是可以改的. 所以我们要了解这些启动参数是不是有默认值, 默认值是如何设置或计算的, 是不是用户设置了它的值, 它的值在启动后是不是被改动了.