分类 Java 相关 下的文章

JVM 安全点 Safepoint

最近在看 ZGC 的某些具体的实现, 有篇文章对了从 Serial GC, 到 parallel GC, 再到CMS, 然后到G1, 最后到如今的ZGC, 其中一个重要的差别就是把很多GC 时间(STOP the world)要做的事情移到并发去做的过程. 其实这是一个从简单到复杂的过程, 也是一个从粗放到逐步精细控制的过程. 最终的结果就是在GC的时间点上, 做的事情越来越少.

如果讨论到GC 的时间点, 其中一个重要的事情, 就是安全点(Safepoint), 它是一个让所有业务线程在某个点全部停下来的过程, 由于有很多业务线程, 让它们同时停下来, 就涉及到一个协调机制, 如何让这些线程在不影响业务线程的情况下, 以最快的速度停下来, 就显得非常重要.

什么是JVM Safepoint

JVM(Java虚拟机)中的Safepoint是一种机制,用于确保所有线程在执行某些特定的系统级操作之前达到一个已知且一致的状态。这些系统级操作通常包括垃圾收集(GC)、线程栈的展开、代码重优化以及一些运行时系统的更新等。

在Safepoint期间,JVM会暂停所有的Java线程执行(也就是所谓的“Stop-The-World”暂停),直到所有线程都到达Safepoint。这样可以确保在进行这些操作时,不会有任何线程在执行Java字节码,从而避免了潜在的数据不一致和竞争条件。

JVM 可以在那些代码区域达到安全点?

  1. 方法调用边界:当一个方法被调用时,可能会在调用前后插入Safepoint检查。这是因为方法调用是程序执行中的自然中断点,且通常是执行时间较长的操作。
  2. 循环回边:在循环结构中,循环的末尾(即循环要重新开始的地方)是达到Safepoint的一个常见位置。这样做是为了防止长时间运行的循环阻止系统达到Safepoint。
  3. 显式的Safepoint检查点:JVM的即时编译器(JIT)可能会在生成的机器代码中的特定位置插入显式的Safepoint检查。这些检查通常会在执行时间较长的代码段中进行。
  4. 同步操作:当线程尝试进入或退出同步块(synchronized block)或方法时,也可能会进行Safepoint检查,因为这些操作涉及到锁的获取和释放。
  5. 异常抛出点:当程序抛出异常时,可能会在异常处理之前达到Safepoint,因为异常处理涉及到栈的展开和控制流的改变。
  6. 线程状态变化:当线程状态发生变化时(例如,从运行状态转为等待或休眠状态),也可能会进行Safepoint检查。

其他:JVM实现还可能在其他不那么明显的地方插入Safepoint检查,这些通常是由于特定的实现细节和优化策略。

其它

  1. Safepoint 在 Java 语言规范里没有涉及, 但是每个 JVM 实现都有 Safepoint;
  2. 什么时候需要安全点 Safepoint?

    1. GC 某些阶段的时候;
    2. JVM TI 捕获 stacktrace 的时候;
    3. 类重新定义的时候(Class redefinition), 比如 BCI 代码 Instrument 的时候;
    4. 捕获 heap dump 的时候;
    5. 锁膨胀的时候 (monitor deflation);
    6. 锁从偏向锁取消的时候(Lock unbiasing);
    7. 方法逆优化的时候(Method deoptimization);
    8. 其它...
  3. 对于 Zing JVM 实现, 分为全局安全点( global Safepoint) 和 线程安全点 (Thread Safepoint), 对于 Hotspot (Oracle/OpenJDK)系列只有全局安全点;
  4. 所有的 JVM 实现都在某些地方需要全局安全点( global Safepoint);

参考:

http://psy-lob-saw.blogspot.com/2016/02/why-most-sampling-java-profilers-are.html
http://psy-lob-saw.blogspot.com/2015/12/safepoints.html
https://psy-lob-saw.blogspot.com/2014/03/where-is-my-safepoint.html

google search: with-gc-solved-what-else-makes-jvm-pause

Java 内存分析工具 MAT 使用技巧

本文列出作者在日常使用 Java 内存分析工具 MAT 的过程中用到的一些技巧

导出长字符串

有时候我们经常要从 heap 中复制出某些很长的字符串, 来观察它到底有哪些数据. 通常我们通过: 在某个字符串上点击右键 - Copy -> Value. 但是通常这个复制出的内容都有长度限制.

比如下面的例子, 我想从 HTTP request 的 HeapByteBuffer 复制出它已经读取请求的内容, 但是通过上面的方法只能复制出几千的字符:
copy.png

但是如果其内容远超这个数量, 拿到的就是部分数据, 根据部分数据可能得出错误的结论.
如何完全导出其内容?
在上面的菜单中选择 -> Save Value To File. 就能导出全部内容.

不过对于上面例子中的 HeapByteBuffer 要特别注意, 它是通过当前的位置(pos)来标记那里是有效数据的, pos 位置之后可能还有数据, 只不过是无效数据.

根据字段值分组统计

SQL 里面有 select * from table_0 group by column_0. 可是 OQL 里面却没有这个语句. 但是MAT 却提供了这样的功能.
根据下面的菜单栏, 就能找到 Group By Value 选项.
groupBy.png

然后填入你想分组的类名字和要分组的字段. 下面以 java.util.regex.PatternnormalizedPattern 来分组:
pattern.png

最终看到每个 normalizedPattern 的统计个数:

p_result.png

为什么一个简单的 Java 进程的 core dump 那么大

写了一个非常简单的 java main 函数, 然后运行, 然后做 core dump, 竟然发现 core dump 竟然有 6.6G.

Java 程序:

public class ErrorExample {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(600000);
    }
}

做 core dump 和 2种不同的 heap dump.

gcore <pid>

$ ls -lah 
-rw-rw-r-- 1 supra supra 6.6G Apr 30 00:26 core.276268
-rw-rw-r-- 1 supra supra  67M Apr 30 00:26 core.276268.gz
-rw-rw-r-- 1 supra supra 1.3M Apr 30 01:02 heap.bin
-rw-rw-r-- 1 supra supra 334K Apr 30 01:02 heap.bin.gz
-rw------- 1 supra supra 2.8M Apr 30 01:01 heap.hprof
-rw------- 1 supra supra 855K Apr 30 01:01 heap.hprof.gz

上面分别是使用不同工具得到的不同 dump 及进一步压缩过后的:

  1. gcore 276268 得到 core.276268
  2. jhsdb jmap --binaryheap --pid 280038 得到 heap.bin
  3. jcmd 280038 GC.heap_dump heap.hprof 得到 heap.hprof.

可以看到 jhsdb 做出的最小, core dump 最大. 这很有可能是 JVM 保留了很多想用但是还没用到的内存.

查看JVM 当前使用的内存, 发现其实使用的很小:

$ jcmd 281746 GC.heap_info
281746:
 garbage-first heap   total 258048K, used 1744K [0x0000000707e00000, 0x0000000800000000)
  region size 1024K, 2 young (2048K), 0 survivors (0K)
 Metaspace       used 155K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 6K, capacity 386K, committed 512K, reserved 1048576K

然后查看使用的启动参数, 可以看到保留的最大的堆是将近4G(MaxHeapSize=4162846720), 保留的代码缓存区是240M(ReservedCodeCacheSize=251658240), 再加上其它, 比如 JDK 库, 元数据区等, 可能达到6G多.

$ jcmd 281746 VM.flags
281746:
-XX:CICompilerCount=4 -XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=8 -XX:G1HeapRegionSize=1048576 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=262144000 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=4162846720 -XX:MaxNewSize=2497708032 -XX:MinHeapDeltaBytes=1048576 -XX:NonNMethodCodeHeapSize=5836300 -XX:NonProfiledCodeHeapSize=122910970 -XX:ProfiledCodeHeapSize=122910970 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseG1GC

如果使用 pmap 查看这个进程的虚拟内存使用情况, 可以发现确实使用了6.6G(最后一行):

$ pmap -x 280038
280038:   java ErrorExample
Address           Kbytes     RSS   Dirty Mode  Mapping
0000000707e00000  256000     328     328 rw---   [ anon ]
0000000717800000 2758656       0       0 -----   [ anon ]
00000007c0000000 1048576       0       0 -----   [ anon ]
............ 省略 ....................
00007f39c513e000       8       8       0 r---- ld-linux-x86-64.so.2
00007f39c5178000       8       8       8 rw--- ld-linux-x86-64.so.2
00007ffc854ed000     132      32      32 rw---   [ stack ]
ffffffffff600000       4       0       0 --x--   [ anon ]
---------------- ------- ------- -------
total kB         6821276   37464   10660

Java 应用产生 core dump

什么是core dump

在 Linux 系统中,当一个进程崩溃(例如,由于段错误或其他严重错误),它通常会产生一个称为 "核心转储"(core dump)的文件。核心转储文件包含了进程崩溃时的内存映像和一些执行上下文信息,如寄存器状态、程序计数器位置、内存管理信息等。这些信息对于开发者来说非常有价值,因为它们可以用来调试程序,了解程序崩溃的原因。

Java 进程如何产生 core dump

对于正在运行的 Java 进程, 它就是一个标准的linux 进程, 可以使用 linux 上的各种工具来产生 core dump, 比如 gcore 或者 kill :

$ gcore <pid>
$ kill -11 <pid>

对于 Java 应用程序自身, 当它奔溃的时候, 默认会产生 core dump, 因为它有如下默认参数-XX:+CreateCoredumpOnCrash:

$ java -XX:+PrintFlagsFinal -version | grep Core
     bool CreateCoredumpOnCrash                    = true                                      {product} {default}

虽然上面的方式都能产生 core dump, 但是很有可能你并不能看到 core dump, 因为有各种其它条件会阻碍 core dump的产生:

  1. 系统设置了 core size 的大小太小, 查看 ulimit -c
  2. core dump 要写入的文件夹没有权限
  3. core dump 被系统设置拦截, 比如 apport, 它产生了 crash report, 却拦截了 core dump的产生.

core dump 有什么用?

core dump 里有进程当时时间点上的全部内存信息, 寄存器信息, 栈信息, 栈上变量值, 打开文件句柄, 打开的socket 等各种非常有用的信息, 对于诊断应用为什么崩溃具有很大的意义.

gdb 可以直接打开core dump 文件, 并读取里面的信息.

$ gdb core core.720444` 

使用 gdb 去debug Java 程序比较麻烦.

但是可以使用 JDK 自带的 jdb:

# java 8 版本
$ jdb -listconnectors
$ jdb -connect sun.jvm.hotspot.jdi.SACoreAttachingConnector:javaExecutable=$JAVA_HOME/bin/java,core=core.720444

java 11 版本
$ jhsdb clhsdb --exe ErrorExample --core core.720444

Core Dump vs Java Heap Dump

Core Dump

  • 定义
    Core dump 是操作系统在进程异常终止时生成的一个文件,它包含了进程终止时内存中的内容。
  • 内容
    Core dump 包含了进程的整个内存映像,包括程序计数器、寄存器、堆栈、堆内存、全局变量、打开的文件描述符、环境变量、程序代码、加载的共享库等。
  • 用途
    主要用于程序崩溃后的调试和故障排查。可以使用调试工具(如 gdb)来分析 core dump 文件,确定程序崩溃的原因。
  • 大小
    Core dump 文件通常很大,因为它包含了整个进程的内存映像。
  • 生成方式
    Core dump 通常由操作系统在进程崩溃时自动生成,或者可以使用 gcore 命令手动生成。

Java Heap Dump

  • 定义
    Java heap dump 是 JVM 在某一时刻的堆内存的快照,包含了所有的 Java 对象和类信息。
  • 内容
    Heap dump 专注于 Java 堆内存,包括对象实例、数组、类实例、垃圾收集器信息等。
  • 用途
    主要用于分析 Java 应用程序的内存使用情况,如检测内存泄漏、查看对象的分配和引用情况等。
  • 大小
    Heap dump 文件的大小取决于 Java 堆的大小,通常比完整的 core dump 小。
  • 生成方式
    Heap dump 可以通过 JVM 提供的工具(如 jmap)、管理接口(如 JMX)生成,或在发生 OutOfMemoryError 时自动生成(如果配置了 -XX:+HeapDumpOnOutOfMemoryError JVM 参数)。

heap dump vs crash log file

这就类似
core dump -> apport crash report
heap dump -> crash log file (err_log_pid.log)

-XX:OnError 选项

如果启动参数配置了-XX:OnError选项, 当 fatal error 产生的时候, JVM 就会执行该选项配置的命令. 多个执行命令可以用 ; 分割开. 可以用 %p 指代当前进程号, 因为 % 用作了特殊符, 所以遇到真的 %, 就要用两个 %% 代替. 官方给的例子:

java -XX:OnError="pmap %p" MyApp #使用 pmap 查看内存空间
java -XX:OnError="gcore %p; dbx - %p" MyApp # 使用 gcore 产生core dump 并用 dbx 进行debug
java -XX:OnError="gdb - %p" MyApp # 使用 gdb debug

-XX:ErrorFile 选项

设置当 fatal error 产生的时候, 把log写到哪个文件去, 需要全路径名(%p指代当前进程号, %%=%).
如果该文件存在, 并且可以写, 那么就覆盖.
如果不设置, 它的默认行为是: 产生一个 java_error_%p.log 文件, %p是进程号, 默认放在当前进程的当前工作目录(CWD), 如果当前工作目录不可用(比如写权限,空间不够等), 会写到临时文件夹目录(/tmp).
参考: https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/felog001.html

试验 -XX:OnError 和 -XX:ErrorFile

更多细节在:
https://www.tianxiaohui.com/index.php/Troubleshooting/Java-%E5%A6%82%E4%BD%95%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA-fatal-error.html

jhsdb

jhsdb(Java HotSpot Debugger)是在JDK 9中引入的命令行实用程序,它是服务性代理(Serviceability Agent)工具的一部分。服务性代理是一个框架,允许对运行中的Java虚拟机(JVM)或崩溃后的核心转储进行深入分析。引入jhsdb的目的有几个:

  1. 统一的调试工具:在JDK 9之前,开发者需要使用不同的工具,如jmapjstackjinfo等来执行各种调试任务。jhsdb将这些工具统一到一个命令行界面下,使得在不切换多个工具的情况下更容易执行广泛的调试和诊断任务。
  2. 增强的事后诊断jhsdb提供了分析核心转储和实时进程的能力,这对事后诊断至关重要。当JVM崩溃时,开发者需要了解崩溃时JVM的状态,这一点尤其有用。
  3. 高级分析能力:使用jhsdb,开发者可以执行高级分析,如检查堆、分析内存使用情况和获取线程堆栈跟踪。它还允许使用内置的命令行调试器(CLD)来调试Java进程,这是一个类似于gdb的基于文本的界面。
  4. 跨平台一致性jhsdb旨在在不同平台上保持一致的工作,为在各种操作系统上进行诊断提供了标准化的方法。
  5. 与HotSpot JVM集成:由于jhsdb是为HotSpot JVM量身定制的,它可以利用HotSpot的内部特性来提供有关JVM内部的详细信息,这对于性能调优和解决复杂问题非常宝贵。
  6. 可访问性:通过包含在JDK中,jhsdb可以立即供所有Java开发者使用,无需额外下载或安装。
  7. 可脚本化:作为命令行工具,jhsdb可以轻松地编写脚本并集成到自动化的调试和诊断工作流中,这对于持续集成和部署管道非常有益。

总的来说,JDK 9中引入jhsdb的目的是通过提供一个强大而多功能的JVM诊断和调试工具来改善开发者体验,从而增强分析和解决Java应用程序中复杂问题的能力。

实践

linux 系统现在有一个正在运行的 Java 程序, 进程号是 256487, 却遇到了下面的错误

$ jhsdb jmap  --pid 256487
Attaching to process ID 256487, please wait...
ERROR: ptrace(PTRACE_ATTACH, ..) failed for 256487: Operation not permitted
Error attaching to process: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 256487: Operation not permitted
sun.jvm.hotspot.debugger.DebuggerException: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 256487: Operation not permitted

这是因为 ptrace 权限的问题. 查看 /proc/sys/kernel/yama/ptrace_scope 可以看到里面的值是 1, 改成 0 就好了

$ cat /proc/sys/kernel/yama/ptrace_scope
1
$ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
0

$ jhsdb jmap  --pid 256487 # as pmap output
Attaching to process ID 256487, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 11.0.22+7-post-Ubuntu-0ubuntu222.04.1
0x0000563f4efe6000    14K    /usr/lib/jvm/java-11-openjdk-amd64/bin/java
0x00007fe2671ba000    44K    /usr/lib/jvm/java-11-openjdk-amd64/lib/libzip.so
0x00007fe2671c5000    66K    /usr/lib/x86_64-linux-gnu/libresolv.so.2
0x00007fe2671d9000    22K    /usr/lib/x86_64-linux-gnu/libkeyutils.so.1.9
0x00007fe2671e0000    50K    /usr/lib/x86_64-linux-gnu/libkrb5support.so.0.1
0x00007fe2671ee000    18K    /usr/lib/x86_64-linux-gnu/libcom_err.so.2.1
0x00007fe2671f4000    178K    /usr/lib/x86_64-linux-gnu/libk5crypto.so.3.1
0x00007fe267223000    808K    /usr/lib/x86_64-linux-gnu/libkrb5.so.3.3
0x00007fe2672ee000    330K    /usr/lib/x86_64-linux-gnu/libgssapi_krb5.so.2.2
0x00007fe267342000    178K    /usr/lib/x86_64-linux-gnu/libtirpc.so.3.0.0
0x00007fe267370000    91K    /usr/lib/x86_64-linux-gnu/libnsl.so.2.0.1
0x00007fe26738a000    54K    /usr/lib/x86_64-linux-gnu/libnss_nis.so.2.0.0
0x00007fe267399000    42K    /usr/lib/x86_64-linux-gnu/libnss_compat.so.2
0x00007fe2673b8000    39K    /usr/lib/jvm/java-11-openjdk-amd64/lib/libjimage.so
0x00007fe2673c1000    213K    /usr/lib/jvm/java-11-openjdk-amd64/lib/libjava.so
0x00007fe2673ef000    14K    /usr/lib/x86_64-linux-gnu/librt.so.1
0x00007fe2674f4000    122K    /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
0x00007fe267514000    918K    /usr/lib/x86_64-linux-gnu/libm.so.6
0x00007fe2675fb000    2207K    /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30
0x00007fe267827000    22708K    /usr/lib/jvm/java-11-openjdk-amd64/lib/server/libjvm.so
0x00007fe268afc000    2168K    /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007fe268d25000    80K    /usr/lib/jvm/java-11-openjdk-amd64/lib/jli/libjli.so
0x00007fe268d38000    106K    /usr/lib/x86_64-linux-gnu/libz.so.1.2.11
0x00007fe268d54000    67K    /usr/lib/jvm/java-11-openjdk-amd64/lib/libverify.so
0x00007fe268d67000    235K    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2

$ jhsdb -h
    clhsdb           command line debugger
    hsdb             ui debugger
    debugd --help    to get more information
    jstack --help    to get more information
    jmap   --help    to get more information
    jinfo  --help    to get more information
    jsnap  --help    to get more information

$ jhsdb jmap --help

$ jhsdb jmap --binaryheap --pid 274471
Attaching to process ID 274471, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 11.0.22+7-post-Ubuntu-0ubuntu222.04.1
heap written to heap.bin