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のバヌゞョンにそれほど䟝存しおいなさそうなこずもわかりたした。䟿利ですね。

デバッグ甚途等に抌さえおおくずよいのかなず思いたす。

実珟方法を芋お、こういうやり方もあるのかず参考になりたした。