CLOVER🍀

That was when it all began.

JDKなしでjcmd等のJavaの各種診断ツールを動かせるjattachを試す

これは、なにをしたくて書いたもの?

コンテナ環境などでJavaアプリケーションを実行している時でかつJDKをインストールしていない場合、jcmd等のJDK付属ツールがなくて
困る場合などがあると思います。

こういう時にはjattachというツールを使うと便利そうなので試してみました。

jattach

jattachのGitHubリポジトリーはこちら。

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を使っているわけではありません)。

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リポジトリーへの
リンクを載せておきます。

https://github.com/openjdk/jdk8u/blob/jdk8u392-ga/hotspot/src/share/vm/services/attachListener.cpp#L394-L407

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をインストールする

まずは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

ここでのソケットは、Unixドメインソケットのようです。

// 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のバージョンにそれほど依存していなさそうなこともわかりました。便利ですね。

デバッグ用途等に押さえておくとよいのかなと思います。

実現方法を見て、こういうやり方もあるのかと参考になりました。