これは、なにをしたくて書いたもの?
コンテナ環境などでJavaアプリケーションを実行している時でかつJDKをインストールしていない場合、jcmd等のJDK付属ツールがなくて
困る場合などがあると思います。
こういう時にはjattachというツールを使うと便利そうなので試してみました。
jattach
GitHub - jattach/jattach: JVM Dynamic Attach utility
jattachは動的アタッチメカニズムを使用してJVMプロセスにコマンドを送信するユーティリティ、とされています。
The utility to send commands to a JVM process via Dynamic Attach mechanism.
jmap、jstack、jcmd、jjinfoを使えるシングルバイナリなプログラムとされていて、JDKは必要なくJREのみで動作します。
All-in-one jmap + jstack + jcmd + jinfo functionality in a single tiny program.
No installed JDK required, works with just JRE. Supports Linux containers.
Attach APIの軽量なネイティブバージョンだとされています(Attach APIを使っているわけではありません)。
現在のバージョンは2.2で、サポートしているコマンドは以下となっています。
- load : load agent library
- properties : print system properties
- agentProperties : print agent properties
- datadump : show heap and thread summary
- threaddump : dump all stack traces (like jstack)
- dumpheap : dump heap (like jmap)
- inspectheap : heap histogram (like jmap -histo)
- setflag : modify manageable VM flag
- printflag : print VM flag
- jcmd : execute jcmd command
ここでMercurialリポジトリーへのリンクが出典的に貼られているのですが、すでになくなっているので代わりにGitHubリポジトリーへの
リンクを載せておきます。
Mercurialの時のリビジョン、行番号とは完全に一致してはいませんが、内容的にここでしょう…。
ダウンロードできるバイナリーを見ると、Linux、macOS、Windowsのいずれでも使えそうです。
Release Concatenate jcmd arguments · jattach/jattach · GitHub
使い方としては
$ jattach [pid] [command]
という形式になります。
それでは試してみましょう。
参考)
JRE しか入ってない Pod で Java の heap dump を取りたい / Get heap dump on JRE container - Speaker Deck
環境
今回の環境はこちら。
$ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 22.04.4 LTS Release: 22.04 Codename: jammy $ uname -srvmpio Linux 5.15.0-94-generic #104-Ubuntu SMP Tue Jan 9 15:25:40 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux
Ubuntu Linux 22.04 LTSです。
お題
簡単なJavaプログラムを書き、JREのみをインストールした複数のJavaバージョン(8〜21までのLTS)の組み合わせでjattachが
動作するか見ていきたいと思います。
お題は以下のプログラムにします。
Server.java
import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.concurrent.Executors; import com.sun.net.httpserver.HttpServer; public class Server { public static void main(String... args) throws IOException { int port = Integer.parseInt(System.getProperty("server.port", "8000")); HttpServer server = HttpServer.create(new InetSocketAddress(port), 0); server.createContext("/", exchange -> { String message = "Hello World"; byte[] bytes = message.getBytes(StandardCharsets.UTF_8); exchange.sendResponseHeaders(200, bytes.length); try (OutputStream os = exchange.getResponseBody()) { os.write(bytes); } }); server.setExecutor(Executors.newCachedThreadPool()); server.start(); System.out.printf("[%s] start lightweight http server.%n", LocalDateTime.now()); } }
JDKに付属しているHTTPサーバーを使ったプログラムです。少しわざとらしいですが、システムプロパティも使うようにしています。
こちらは先にコンパイルしておく必要があるので、別にJDKをインストールした環境で作成して今回の下限バージョンである
Java 8向けにコンパイルします。
$ javac --release 8 Server.java
作成はJava 21で行っています。
$ java --version openjdk 21.0.1 2023-10-17 OpenJDK Runtime Environment (build 21.0.1+12-Ubuntu-222.04) OpenJDK 64-Bit Server VM (build 21.0.1+12-Ubuntu-222.04, mixed mode, sharing)
このJavaを使うのはここまでです。
JREのみのJavaをインストールする
$ sudo apt install openjdk-8-jre-headless openjdk-11-jre-headless openjdk-17-jre-headless openjdk-21-jre-headless
ちなみに、JDKをインストールする場合はopenjdk-21-jdk-headlessやopenjdk-21-jdk-headlessといった感じです。
インストールされたバージョン。
$ /usr/lib/jvm/java-8-openjdk-amd64/bin/java -version openjdk version "1.8.0_392" OpenJDK Runtime Environment (build 1.8.0_392-8u392-ga-1~22.04-b08) OpenJDK 64-Bit Server VM (build 25.392-b08, mixed mode) $ /usr/lib/jvm/java-11-openjdk-amd64/bin/java --version openjdk 11.0.21 2023-10-17 OpenJDK Runtime Environment (build 11.0.21+9-post-Ubuntu-0ubuntu122.04) OpenJDK 64-Bit Server VM (build 11.0.21+9-post-Ubuntu-0ubuntu122.04, mixed mode, sharing) $ /usr/lib/jvm/java-17-openjdk-amd64/bin/java --version openjdk 17.0.9 2023-10-17 OpenJDK Runtime Environment (build 17.0.9+9-Ubuntu-122.04) OpenJDK 64-Bit Server VM (build 17.0.9+9-Ubuntu-122.04, mixed mode, sharing) $ /usr/lib/jvm/java-21-openjdk-amd64/bin/java --version openjdk 21.0.1 2023-10-17 OpenJDK Runtime Environment (build 21.0.1+12-Ubuntu-222.04) OpenJDK 64-Bit Server VM (build 21.0.1+12-Ubuntu-222.04, mixed mode, sharing)
JREのみなので、jcmd等は入っていません。
$ /usr/lib/jvm/java-21-openjdk-amd64/bin/javac -bash: /usr/lib/jvm/java-21-openjdk-amd64/bin/javac: そのようなファイルやディレクトリはありません $ /usr/lib/jvm/java-21-openjdk-amd64/bin/jcmd -bash: /usr/lib/jvm/java-21-openjdk-amd64/bin/jcmd: そのようなファイルやディレクトリはありません
先ほど作成したプログラムが各バージョンで動作することも確認しておきます。
## Java 8 $ /usr/lib/jvm/java-8-openjdk-amd64/bin/java -Dserver.port=8080 Server [2024-02-18T15:26:46.535] start lightweight http server. $ curl localhost:8080 Hello World ## Java 11 $ /usr/lib/jvm/java-11-openjdk-amd64/bin/java -Dserver.port=8080 Server [2024-02-18T15:27:56.106774] start lightweight http server. $ curl localhost:8080 Hello World ## Java 17 $ /usr/lib/jvm/java-17-openjdk-amd64/bin/java -Dserver.port=8080 Server [2024-02-18T15:28:22.205653282] start lightweight http server. $ curl localhost:8080 Hello World ## Java 21 $ /usr/lib/jvm/java-21-openjdk-amd64/bin/java -Dserver.port=8080 Server [2024-02-18T15:28:43.328222880] start lightweight http server. $ curl localhost:8080 Hello World
これで準備はできました。
jattachをインストールする
jattachをインストールしましょう。 Ubuntu Linuxの場合はaptでインストールすることもできるのですが
$ sudo apt install jattach
GitHubのReleasesから取得することが多そうな気がするので、そちらにしておきます。
apt showで見るとこんな感じになっています。バージョンは最新版ではないですね。
$ apt show jattach Package: jattach Version: 2.0-1 Priority: optional Section: universe/java Origin: Ubuntu Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com> Original-Maintainer: Sven Hoexter <hoexter@debian.org> Bugs: https://bugs.launchpad.net/ubuntu/+filebug Installed-Size: 47.1 kB Depends: libc6 (>= 2.34) Homepage: https://github.com/apangin/jattach Download-Size: 12.4 kB APT-Sources: https://mirrors.edge.kernel.org/ubuntu jammy/universe amd64 Packages Description: JVM Dynamic Attach utility all in one jmap jstack jcmd jinfo jattach is a utility implementing commands for the JVM Dynamic Attach mechanism. Instead of installing a complete JDK you can use this small utility to query information from your running JVM.
では、Releasesよりダウンロードして展開。
$ curl -LO https://github.com/jattach/jattach/releases/download/v2.2/jattach-linux-x64.tgz $ tar xf jattach-linux-x64.tgz
jattachというシングルバイナリのファイルが現れます。
引数なしで実行すると、バージョンとヘルプが表示されます。
$ ./jattach
jattach 2.2 built on Jan 10 2024
Usage: jattach <pid> <cmd> [args ...]
Commands:
load threaddump dumpheap setflag properties
jcmd inspectheap datadump printflag agentProperties
とりあえず、Java 21をターゲットにして試してみましょう。
$ /usr/lib/jvm/java-21-openjdk-amd64/bin/java -Xmx512M -Dtarget.port=8080 Server [2024-02-18T15:41:31.273379805] start lightweight http server.
まずはpidを取得するところからですが、これ自体をjattachのjcmdに頼ることはできません。先にpidを指定する必要があるからですね。
$ ./jattach jcmd
jattach 2.2 built on Jan 10 2024
Usage: jattach <pid> <cmd> [args ...]
Commands:
load threaddump dumpheap setflag properties
jcmd inspectheap datadump printflag agentProperties
$ ./jattach jcmd -l
jcmd is not a valid process ID
使えるコマンドは以下ということでした。
- load : load agent library
- properties : print system properties
- agentProperties : print agent properties
- datadump : show heap and thread summary
- threaddump : dump all stack traces (like jstack)
- dumpheap : dump heap (like jmap)
- inspectheap : heap histogram (like jmap -histo)
- setflag : modify manageable VM flag
- printflag : print VM flag
- jcmd : execute jcmd command
ヘルプで表示されているコマンドと見比べると、jcmdは使えますがjmap、jstack、jinfoは似た形態で使えるということに気づきます。
実際、たとえばjstackを指定しても使えません。
$ ./jattach 3745 jstack Connected to remote JVM JVM response code = -1 Operation jstack not recognized!
なので、jmap、jstack、jinfoを使いたい場合はjcmdで代替するかヘルプに従って別のコマンドを使うことになります。
いくつか試してみましょう。jattach [pid] propertiesでシステムプロパティの表示。
$ ./jattach 3745 properties Connected to remote JVM JVM response code = 0 #Sun Feb 18 15:47:26 JST 2024 file.encoding=UTF-8 file.separator=/ java.class.path=. java.class.version=65.0 java.home=/usr/lib/jvm/java-21-openjdk-amd64 java.io.tmpdir=/tmp java.library.path=/usr/java/packages/lib\:/usr/lib/x86_64-linux-gnu/jni\:/lib/x86_64-linux-gnu\:/usr/lib/x86_64-linux-gnu\:/usr/lib/jni\:/lib\:/usr/lib java.runtime.name=OpenJDK Runtime Environment java.runtime.version=21.0.1+12-Ubuntu-222.04 java.specification.name=Java Platform API Specification java.specification.vendor=Oracle Corporation java.specification.version=21 java.vendor=Private Build java.vendor.url=Unknown java.vendor.url.bug=Unknown java.version=21.0.1 java.version.date=2023-10-17 java.vm.compressedOopsMode=32-bit java.vm.info=mixed mode, sharing java.vm.name=OpenJDK 64-Bit Server VM java.vm.specification.name=Java Virtual Machine Specification java.vm.specification.vendor=Oracle Corporation java.vm.specification.version=21 java.vm.vendor=Private Build java.vm.version=21.0.1+12-Ubuntu-222.04 jdk.debug=release line.separator=\n native.encoding=UTF-8 os.arch=amd64 os.name=Linux os.version=5.15.0-94-generic path.separator=\: stderr.encoding=UTF-8 stdout.encoding=UTF-8 sun.arch.data.model=64 sun.boot.library.path=/usr/lib/jvm/java-21-openjdk-amd64/lib sun.cpu.endian=little sun.io.unicode.encoding=UnicodeLittle sun.java.command=Server sun.java.launcher=SUN_STANDARD sun.jnu.encoding=UTF-8 sun.management.compiler=HotSpot 64-Bit Tiered Compilers target.port=8080 〜省略〜 user.timezone=Asia/Tokyo
ここはjattach固有の部分です。
Connected to remote JVM JVM response code = 0
jattach [pid] agentProperties。
$ ./jattach 3745 agentProperties Connected to remote JVM JVM response code = 0 #Sun Feb 18 15:53:30 JST 2024 sun.java.command=Server sun.jvm.args=-Xmx512M -Dtarget.port\=8080 sun.jvm.flags=
jattach [pid] threaddumpでスレッドダンプ。
$ ./jattach 3745 threaddump
Connected to remote JVM
JVM response code = 0
2024-02-18 15:50:41
Full thread dump OpenJDK 64-Bit Server VM (21.0.1+12-Ubuntu-222.04 mixed mode, sharing):
Threads class SMR info:
_java_thread_list=0x00007fe7a40024f0, length=13, elements={
0x00007fe8340ae000, 0x00007fe8340af680, 0x00007fe8340b1110, 0x00007fe8340b2750,
0x00007fe8340b3cf0, 0x00007fe8340b5830, 0x00007fe8340b6ef0, 0x00007fe8340c5100,
0x00007fe8340c8a50, 0x00007fe8340fa8f0, 0x00007fe83410aa70, 0x00007fe8340162d0,
0x00007fe7a4000fe0
}
"Reference Handler" #9 [3754] daemon prio=10 os_prio=0 cpu=0.49ms elapsed=549.96s tid=0x00007fe8340ae000 nid=3754 waiting on condition [0x00007fe80db0e000]
java.lang.Thread.State: RUNNABLE
at java.lang.ref.Reference.waitForReferencePendingList(java.base@21.0.1/Native Method)
at java.lang.ref.Reference.processPendingReferences(java.base@21.0.1/Reference.java:246)
at java.lang.ref.Reference$ReferenceHandler.run(java.base@21.0.1/Reference.java:208)
"Finalizer" #10 [3755] daemon prio=8 os_prio=0 cpu=0.32ms elapsed=549.96s tid=0x00007fe8340af680 nid=3755 in Object.wait() [0x00007fe80da0e000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait0(java.base@21.0.1/Native Method)
- waiting on <0x00000000e1f01670> (a java.lang.ref.NativeReferenceQueue$Lock)
at java.lang.Object.wait(java.base@21.0.1/Object.java:366)
at java.lang.Object.wait(java.base@21.0.1/Object.java:339)
at java.lang.ref.NativeReferenceQueue.await(java.base@21.0.1/NativeReferenceQueue.java:48)
at java.lang.ref.ReferenceQueue.remove0(java.base@21.0.1/ReferenceQueue.java:158)
at java.lang.ref.NativeReferenceQueue.remove(java.base@21.0.1/NativeReferenceQueue.java:89)
- locked <0x00000000e1f01670> (a java.lang.ref.NativeReferenceQueue$Lock)
at java.lang.ref.Finalizer$FinalizerThread.run(java.base@21.0.1/Finalizer.java:173)
"Signal Dispatcher" #11 [3756] daemon prio=9 os_prio=0 cpu=0.70ms elapsed=549.96s tid=0x00007fe8340b1110 nid=3756 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Service Thread" #12 [3757] daemon prio=9 os_prio=0 cpu=0.18ms elapsed=549.96s tid=0x00007fe8340b2750 nid=3757 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Monitor Deflation Thread" #13 [3758] daemon prio=9 os_prio=0 cpu=161.02ms elapsed=549.96s tid=0x00007fe8340b3cf0 nid=3758 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" #14 [3759] daemon prio=9 os_prio=0 cpu=81.61ms elapsed=549.96s tid=0x00007fe8340b5830 nid=3759 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
No compile task
"C1 CompilerThread0" #15 [3760] daemon prio=9 os_prio=0 cpu=126.57ms elapsed=549.96s tid=0x00007fe8340b6ef0 nid=3760 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
No compile task
"Notification Thread" #16 [3761] daemon prio=9 os_prio=0 cpu=0.21ms elapsed=549.95s tid=0x00007fe8340c5100 nid=3761 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Common-Cleaner" #17 [3762] daemon prio=8 os_prio=0 cpu=2.68ms elapsed=549.94s tid=0x00007fe8340c8a50 nid=3762 waiting on condition [0x00007fe80d30e000]
java.lang.Thread.State: TIMED_WAITING (parking)
at jdk.internal.misc.Unsafe.park(java.base@21.0.1/Native Method)
- parking to wait for <0x00000000e1f10390> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.parkNanos(java.base@21.0.1/LockSupport.java:269)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@21.0.1/AbstractQueuedSynchronizer.java:1847)
at java.lang.ref.ReferenceQueue.await(java.base@21.0.1/ReferenceQueue.java:71)
at java.lang.ref.ReferenceQueue.remove0(java.base@21.0.1/ReferenceQueue.java:143)
at java.lang.ref.ReferenceQueue.remove(java.base@21.0.1/ReferenceQueue.java:218)
at jdk.internal.ref.CleanerImpl.run(java.base@21.0.1/CleanerImpl.java:140)
at java.lang.Thread.runWith(java.base@21.0.1/Thread.java:1596)
at java.lang.Thread.run(java.base@21.0.1/Thread.java:1583)
at jdk.internal.misc.InnocuousThread.run(java.base@21.0.1/InnocuousThread.java:186)
"idle-timeout-task" #18 [3763] daemon prio=5 os_prio=0 cpu=9.68ms elapsed=549.87s tid=0x00007fe8340fa8f0 nid=3763 in Object.wait() [0x00007fe80d20e000]
java.lang.Thread.State: TIMED_WAITING (on object monitor)
at java.lang.Object.wait0(java.base@21.0.1/Native Method)
- waiting on <0x00000000e1fa5fa0> (a java.util.TaskQueue)
at java.lang.Object.wait(java.base@21.0.1/Object.java:366)
at java.util.TimerThread.mainLoop(java.base@21.0.1/Timer.java:563)
- locked <0x00000000e1fa5fa0> (a java.util.TaskQueue)
at java.util.TimerThread.run(java.base@21.0.1/Timer.java:516)
"HTTP-Dispatcher" #19 [3764] prio=5 os_prio=0 cpu=89.43ms elapsed=549.84s tid=0x00007fe83410aa70 nid=3764 runnable [0x00007fe80d10e000]
java.lang.Thread.State: RUNNABLE
at sun.nio.ch.EPoll.wait(java.base@21.0.1/Native Method)
at sun.nio.ch.EPollSelectorImpl.doSelect(java.base@21.0.1/EPollSelectorImpl.java:121)
at sun.nio.ch.SelectorImpl.lockAndDoSelect(java.base@21.0.1/SelectorImpl.java:130)
- locked <0x00000000e1fa3308> (a sun.nio.ch.Util$2)
- locked <0x00000000e1fa2f80> (a sun.nio.ch.EPollSelectorImpl)
at sun.nio.ch.SelectorImpl.select(java.base@21.0.1/SelectorImpl.java:142)
at sun.net.httpserver.ServerImpl$Dispatcher.run(jdk.httpserver@21.0.1/ServerImpl.java:474)
at java.lang.Thread.runWith(java.base@21.0.1/Thread.java:1596)
at java.lang.Thread.run(java.base@21.0.1/Thread.java:1583)
"DestroyJavaVM" #20 [3746] prio=5 os_prio=0 cpu=172.46ms elapsed=549.80s tid=0x00007fe8340162d0 nid=3746 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Attach Listener" #21 [3775] daemon prio=9 os_prio=0 cpu=39.40ms elapsed=260.89s tid=0x00007fe7a4000fe0 nid=3775 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"VM Thread" os_prio=0 cpu=43.51ms elapsed=549.97s tid=0x00007fe8340a0e10 nid=3753 runnable
"GC Thread#0" os_prio=0 cpu=0.34ms elapsed=550.01s tid=0x00007fe834042490 nid=3747 runnable
"G1 Main Marker" os_prio=0 cpu=0.30ms elapsed=550.01s tid=0x00007fe834047a50 nid=3748 runnable
"G1 Conc#0" os_prio=0 cpu=0.19ms elapsed=550.01s tid=0x00007fe8340489f0 nid=3749 runnable
"G1 Refine#0" os_prio=0 cpu=0.24ms elapsed=550.01s tid=0x00007fe83406ccc0 nid=3750 runnable
"G1 Service" os_prio=0 cpu=42.37ms elapsed=550.01s tid=0x00007fe83406dc70 nid=3751 runnable
"VM Periodic Task Thread" os_prio=0 cpu=865.36ms elapsed=549.98s tid=0x00007fe834086b50 nid=3752 waiting on condition
JNI global refs: 12, weak refs: 0
pidの位置は変わりますが、jattach [pid] jcmd Thread.printでもOKです。
$ ./jattach 3745 jcmd Thread.print
Connected to remote JVM
JVM response code = 0
2024-02-18 15:51:35
Full thread dump OpenJDK 64-Bit Server VM (21.0.1+12-Ubuntu-222.04 mixed mode, sharing):
Threads class SMR info:
_java_thread_list=0x00007fe7a40024f0, length=13, elements={
0x00007fe8340ae000, 0x00007fe8340af680, 0x00007fe8340b1110, 0x00007fe8340b2750,
0x00007fe8340b3cf0, 0x00007fe8340b5830, 0x00007fe8340b6ef0, 0x00007fe8340c5100,
0x00007fe8340c8a50, 0x00007fe8340fa8f0, 0x00007fe83410aa70, 0x00007fe8340162d0,
0x00007fe7a4000fe0
}
"Reference Handler" #9 [3754] daemon prio=10 os_prio=0 cpu=0.49ms elapsed=604.39s tid=0x00007fe8340ae000 nid=3754 waiting on condition [0x00007fe80db0e000]
java.lang.Thread.State: RUNNABLE
at java.lang.ref.Reference.waitForReferencePendingList(java.base@21.0.1/Native Method)
at java.lang.ref.Reference.processPendingReferences(java.base@21.0.1/Reference.java:246)
at java.lang.ref.Reference$ReferenceHandler.run(java.base@21.0.1/Reference.java:208)
〜省略〜
JNI global refs: 12, weak refs: 0
jcmdのThread.dump_to_fileは使えるんでしょうか?
$ ./jattach 3745 jcmd Thread.dump_to_file -format=json thread_dump.json Connected to remote JVM JVM response code = 0 Created $HOME/thread_dump.json
使えました…。
$ head -n 10 thread_dump.json
{
"threadDump": {
"processId": "3745",
"time": "2024-02-18T06:55:01.331346937Z",
"runtimeVersion": "21.0.1+12-Ubuntu-222.04",
"threadContainers": [
{
"container": "<root>",
"parent": null,
"owner": null,
他のバージョンのJREに同じコマンドを使っても当然ですが受け付けてもらえません。
## 3805はJava 17で動作させているプログラムのpid $ ./jattach 3805 jcmd Thread.dump_to_file -format=json thread_dump.json Connected to remote JVM JVM response code = -1 java.lang.IllegalArgumentException: Unknown diagnostic command
また、通常のjcmdだとpidのみ指定して実行すると使用できるコマンドが表示されますが、このjcmdだとその機能はないようです。
$ ./jattach 3840 jcmd Connected to remote JVM JVM response code = 0
ここまではJava 21で確認してきましたが、あとのバージョンもざっくり確認しておきましょう。
## Java 8
$ /usr/lib/jvm/java-8-openjdk-amd64/bin/java -Xmx512M -Dtarget.port=8080 Server
[2024-02-18T16:02:21.863] start lightweight http server.
$ ./jattach 3866 properties | grep '^java.*version'
java.vm.version=25.392-b08
java.runtime.version=1.8.0_392-8u392-ga-1~22.04-b08
java.class.version=52.0
java.specification.version=1.8
java.vm.specification.version=1.8
java.version=1.8.0_392
java.specification.maintenance.version=5
$ ./jattach 3866 threaddump | head
Connected to remote JVM
JVM response code = 0
2024-02-18 16:03:27
Full thread dump OpenJDK 64-Bit Server VM (25.392-b08 mixed mode):
"Attach Listener" #12 daemon prio=9 os_prio=0 tid=0x00007f0e48001000 nid=0xf2b waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"DestroyJavaVM" #11 prio=5 os_prio=0 tid=0x00007f0e7400a000 nid=0xf1b waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
## Java 11
$ /usr/lib/jvm/java-11-openjdk-amd64/bin/java -Xmx512M -Dtarget.port=8080 Server
[2024-02-18T16:03:49.186818] start lightweight http server.
$ ./jattach 3893 properties | grep '^java.*version'
java.specification.version=11
java.vm.specification.version=11
java.version.date=2023-10-17
java.runtime.version=11.0.21+9-post-Ubuntu-0ubuntu122.04
java.version=11.0.21
java.vm.version=11.0.21+9-post-Ubuntu-0ubuntu122.04
java.specification.maintenance.version=2
java.class.version=55.0
$ ./jattach 3893 threaddump | head
Connected to remote JVM
JVM response code = 0
2024-02-18 16:04:07
Full thread dump OpenJDK 64-Bit Server VM (11.0.21+9-post-Ubuntu-0ubuntu122.04 mixed mode, sharing):
Threads class SMR info:
_java_thread_list=0x00007fab1c000c40, length=12, elements={
0x00007fab60092000, 0x00007fab60094000, 0x00007fab60099800, 0x00007fab6009b800,
0x00007fab6009d800, 0x00007fab6009f800, 0x00007fab600a1800, 0x00007fab600d5000,
0x00007fab600fb000, 0x00007fab6016b800, 0x00007fab60015000, 0x00007fab1c001000
## Java 17
$ /usr/lib/jvm/java-17-openjdk-amd64/bin/java -Xmx512M -Dtarget.port=8080 Server
[2024-02-18T16:04:35.788345626] start lightweight http server.
$ ./jattach 3920 properties | grep '^java.*version'
java.specification.version=17
java.vm.specification.version=17
java.version.date=2023-10-17
java.runtime.version=17.0.9+9-Ubuntu-122.04
java.version=17.0.9
java.vm.version=17.0.9+9-Ubuntu-122.04
java.class.version=61.0
$ ./jattach 3920 threaddump | head
Connected to remote JVM
JVM response code = 0
2024-02-18 16:04:55
Full thread dump OpenJDK 64-Bit Server VM (17.0.9+9-Ubuntu-122.04 mixed mode, sharing):
Threads class SMR info:
_java_thread_list=0x00007f7a58001e40, length=14, elements={
0x00007f7ae408fe30, 0x00007f7ae4091220, 0x00007f7ae40967c0, 0x00007f7ae4097b80,
0x00007f7ae4098fa0, 0x00007f7ae409a960, 0x00007f7ae409bea0, 0x00007f7ae409d320,
0x00007f7ae40aca40, 0x00007f7ae40b0080, 0x00007f7ae40d6a60, 0x00007f7ae40e3ed0,
よさそうですね。
どうなっているのか?
ところでJREのみで実行できるというjattachですが、どういう仕組みで実現しているのでしょうか?
ソースコードを見ると、posixとwindowsに分かれています。
https://github.com/jattach/jattach/tree/v2.2/src
posixは処理の主体はHotSpotとOpenJ9に分かれています。
https://github.com/jattach/jattach/blob/v2.2/src/posix/jattach_hotspot.c
https://github.com/jattach/jattach/blob/v2.2/src/posix/jattach_openj9.c
今回はHotSpotの方を見てみます。
処理を見ると、ソケット作成 → コマンド送信 → 結果の受信、といった流れのようです。
int jattach_hotspot(int pid, int nspid, int argc, char** argv, int print_output) { if (check_socket(nspid) != 0 && start_attach_mechanism(pid, nspid) != 0) { perror("Could not start attach mechanism"); return 1; } int fd = connect_socket(nspid); if (fd == -1) { perror("Could not connect to socket"); return 1; } if (print_output) { printf("Connected to remote JVM\n"); } if (write_command(fd, argc, argv) != 0) { perror("Error writing to socket"); close(fd); return 1; } int result = read_response(fd, argc, argv, print_output); close(fd); return result; }
https://github.com/jattach/jattach/blob/v2.2/src/posix/jattach_hotspot.c#L178-L204
// Connect to UNIX domain socket created by JVM for Dynamic Attach static int connect_socket(int pid) { int fd = socket(PF_UNIX, SOCK_STREAM, 0); if (fd == -1) { return -1; } struct sockaddr_un addr; addr.sun_family = AF_UNIX; int bytes = snprintf(addr.sun_path, sizeof(addr.sun_path), "%s/.java_pid%d", tmp_path, pid); if (bytes >= sizeof(addr.sun_path)) { addr.sun_path[sizeof(addr.sun_path) - 1] = 0; } if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) { close(fd); return -1; } return fd; }
https://github.com/jattach/jattach/blob/v2.2/src/posix/jattach_hotspot.c#L83-L103
このソケットを使い、コマンドを送信して
// Send command with arguments to socket static int write_command(int fd, int argc, char** argv) { char buf[8192]; const char* const limit = buf + sizeof(buf); // jcmd has 2 arguments maximum; merge excessive arguments into one int cmd_args = argc >= 2 && strcmp(argv[0], "jcmd") == 0 ? 2 : argc >= 4 ? 4 : argc; // Protocol version char* p = stpncpy(buf, "1", sizeof(buf)) + 1; int i; for (i = 0; i < argc && p < limit; i++) { if (i >= cmd_args) p[-1] = ' '; p = stpncpy(p, argv[i], limit - p) + 1; } for (i = cmd_args; i < 4 && p < limit; i++) { *p++ = 0; } const char* q = p < limit ? p : limit; for (p = buf; p < q; ) { ssize_t bytes = write(fd, p, q - p); if (bytes <= 0) { return -1; } p += (size_t)bytes; } return 0; }
https://github.com/jattach/jattach/blob/v2.2/src/posix/jattach_hotspot.c#L105-L134
結果を受信します。
// Mirror response from remote JVM to stdout static int read_response(int fd, int argc, char** argv, int print_output) { char buf[8192]; ssize_t bytes = read(fd, buf, sizeof(buf) - 1); if (bytes == 0) { fprintf(stderr, "Unexpected EOF reading response\n"); return 1; } else if (bytes < 0) { perror("Error reading response"); return 1; } // First line of response is the command result code buf[bytes] = 0; int result = atoi(buf); // Special treatment of 'load' command if (result == 0 && argc > 0 && strcmp(argv[0], "load") == 0) { size_t total = bytes; while (total < sizeof(buf) - 1 && (bytes = read(fd, buf + total, sizeof(buf) - 1 - total)) > 0) { total += (size_t)bytes; } bytes = total; // The second line is the result of 'load' command; since JDK 9 it starts from "return code: " buf[bytes] = 0; result = atoi(strncmp(buf + 2, "return code: ", 13) == 0 ? buf + 15 : buf + 2); } if (print_output) { // Mirror JVM response to stdout printf("JVM response code = "); do { fwrite(buf, 1, bytes, stdout); bytes = read(fd, buf, sizeof(buf)); } while (bytes > 0); printf("\n"); } return result; }
https://github.com/jattach/jattach/blob/v2.2/src/posix/jattach_hotspot.c#L136-L176
つまり、Unixドメインソケットを使ったRPC的な形で実現されていて、jattachはjcmd等の代わりの機能を実装しているというよりは
JavaVMへのコマンド送信を橋渡しをするようになっていることがわかります。
なので、対象のJavaプロセスが動作していればOK、jattach自身はJavaに依存せずに動作する、ということになっているようです。
これを見ると、たとえばjcmdのThread.dump_to_fileがJava 21でのみ動作した理由(というかJava 21でちゃんと動いた理由)が
わかりますね。
おわりに
JDKなしでjcmd等の各種診断ツールを動かせるjattachを試してみました。
導入すればあっさりと使え、またJavaのバージョンにそれほど依存していなさそうなこともわかりました。便利ですね。
デバッグ用途等に押さえておくとよいのかなと思います。
実現方法を見て、こういうやり方もあるのかと参考になりました。