JHSDB:基于服务性代理的调试工具

JHSDB:基于服务性代理的调试工具

JDK中提供了JCMD和JHSDB两个集成式的多功能工具箱,它们不仅整合了上一节介绍到的所有基础工具所能提供的专项功能,而且由于有着“后发优势”,能够做得往往比之前的老工具们更好、更强大。

JHSDB是一款基于服务性代理(Serviceability Agent,SA)实现的进程外调试工具。服务性代理是HotSpot虚拟机中一组用于映射Java虚拟机运行信息的、主要基于Java语言(含少量JNI代码)实现的API集合。服务性代理以HotSpot内部的数据结构为参照物进行设计,把这些C++的数据抽象出Java模型对象,相当于HotSpot的C++代码的一个镜像。通过服务性代理的API,可以在一个独立的Java虚拟机的进程里分析其他HotSpot虚拟机的内部数据,或者从HotSpot虚拟机进程内存中dump出来的转储快照里还原出它的运行状态细节。服务性代理的工作原理跟Linux上的GDB或者Windows上的Windbg是相似的。

JHSDB测试代码

/
 * -Xmx20m
 */
public class TestHsdb {

    public static class Test {
        /
         * 随着Test的类型信息存放在方法区
         */
        public static HsDb staticHsDb = new HsDb();

        /
         * 随着Test的类型信息存放在方法区
         */
        public HsDb instanceHsDb = new HsDb();

        void foo() {
            //存放在foo()方法栈帧的局部变量表中
            HsDb stackHsDb = new HsDb();
            //断点处
            System.out.println("finish");
        }
    }

    public static class HsDb {
        public int i;
        public String y;
        public final static String staticStr = "123";

        public static String hs = "321";

    }

    public static void main(String[] args) {
        Test test = new Test();
        test.foo();
    }
}

限制堆大小:-Xmx20m

程序执行后通过jps查询到测试程序的进程ID

D:\pro\jdk-17.0.10\bin>jps -l
22560 org.jetbrains.idea.maven.server.RemoteMavenServer36
17428
20596 com.mystic.ycc.blog.test.TestHsdb
23764 org.jetbrains.idea.maven.server.RemoteMavenServer36
24068
22440 jdk.jcmd/sun.tools.jps.Jps
21612 org.jetbrains.jps.cmdline.Launcher
604 org.jetbrains.jps.cmdline.Launcher
9964 org.jetbrains.idea.maven.server.RemoteMavenServer36

使用以下命令进入JHSDB的图形化模式

jhsdb.exe hsdb --pid 20596

命令打开的JHSDB的界面如图

image-20240520093112542

运行至断点位置一共会创建三个HsDb对象的实例,只要是对象实例必然会在Java堆中分配,既然我们要查找引用这三个对象的指针存放在哪里,不妨从这三个对象开始着手,先把它们从Java堆中找出来。首先点击菜单中的Tools->Heap Parameters,结果如图所示,未指定垃圾收集器的时候JDK17 [本人使用的JDK] 使用的是G1.

image-20240520093424774

打开Windows->Console窗口,使用scanoops命令在Java堆的新生代(起始地址到结束地址)范围内查找HsDb[这里不要使用工具复制引用,内部类使用$表示,如果使用的不是内部类可以忽略]的实例,结果如下所示:

scanoops 0x00000000fec00000 0x0000000100000000 com.mystic.ycc.blog.test.TestHsdb$HsDb

image-20240520093637008

这里找出了代码创建的三个实例的地址。再使用Tools->Inspector功能确认一下这三个地址中存放的对象,结果如图所示。

image-20240520094100133

Inspector为我们展示了对象头和指向对象元数据的指针,里面包括了Java类型的名字、继承关系、实现接口关系,字段信息、方法信息、运行时常量池的指针、内嵌的虚方法表(vtable)以及接口方法表(itable)等。

这里创建了4个属性,通过它来观察内存布局如下图[这里地址有变化,因为上面测试的程序被关闭重新启动导致,请忽略]

       public int i;
       public String y;
       //静态常量
       public final static String staticStr = "123";
       //静态常量
       public static String hs = "321";

image-20240520094532840

属性总共有4个,对于创建的对象布局中只存在2个属性,另外两个是静态常量【相关内容】,对象大小是24个字节。

接下来要根据堆中对象实例地址找出引用它们的指针。[revptrs],这里我们会找到第一个引用来自staticHsDb的静态属性,以此类推可以找到所有不在栈上的引用。

image-20240520102854467

revptrs命令并不支持查找栈上的指针引用,可以在Java Thread窗口选中main线程后点击Stack Memory按钮查看该线程的栈内存。

image-20240520103128310

从这个页面可以在右边的地方看到具体的解释,能够发现TestHsdb&HsDb在栈中创建。

这便是hsdb的一些可视化功能,对于学习java内存布局是很有帮助的。下面讲述一下命令行功能。

JCMD、JHSDB与原基础工具实现相同功能的简要对比。

1716168039457

sa-jdi.jar

export JAVA_HOME="/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home"
chmod +x $JAVA_HOME/lib/sa-jdi.jar
java -cp $JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.HSDB
java -cp $JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.CLHSDB
  • 在java9之前,JAVA_HOME/lib目录下有个sa-jdi.jar,可以通过如上命令启动HSDB(图形界面)及CLHSDB(命令行)
  • sa-jdi.jar中的sa的全称为Serviceability Agent,它之前是sun公司提供的一个用于协助调试HotSpot的组件,而HSDB便是使用Serviceability Agent来实现的
  • HSDB就是HotSpot Debugger的简称,由于Serviceability Agent在使用的时候会先attach进程,然后暂停进程进行snapshot,最后deattach进程(进程恢复运行),所以在使用HSDB时要注意

jhsdb

/ # jhsdb
    clhsdb          command line debugger
    debugd          debug server
    hsdb            ui debugger
    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是java9引入的,可以在JAVA_HOME/bin目录下找到jhsdb;它取代了jdk9之前的JAVA_HOME/lib/sa-jdi.jar
  • jhsdb有clhsdb、debugd、hsdb、jstack、jmap、jinfo、jsnap这些mode可以使用
  • 其中hsdb为ui debugger,就是jdk9之前的sun.jvm.hotspot.HSDB;而clhsdb即为jdk9之前的sun.jvm.hotspot.CLHSDB

jhsdb jstack

/ # jhsdb jstack --help
    --locks to print java.util.concurrent locks
    --mixed to print both java and native frames (mixed mode)
    --exe   executable image name
    --core  path to coredump
    --pid   pid of process to attach

–pid用于指定JVM的进程ID;–exe用于指定可执行文件;–core用于指定core dump文件

异常

jhsdb jstack --mixed --pid 1
//......
Caused by: sun.jvm.hotspot.debugger.DebuggerException: get_thread_regs failed for a lwp
    at jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal.getThreadIntegerRegisterSet0(Native Method)
    at jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$1GetThreadIntegerRegisterSetTask.doit(LinuxDebuggerLocal.java:534)
    at jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$LinuxDebuggerLocalWorkerThread.run(LinuxDebuggerLocal.java:151)

如果出现这个异常表示是采用jdk版本的问题,可以尝试一下其他jdk编译版本

debugger

/ # jhsdb jstack --locks --pid 1
Attaching to process ID 1, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 12+33
Deadlock Detection:

No deadlocks found.

"DestroyJavaVM" #32 prio=5 tid=0x000055c3b5be0800 nid=0x6 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   JavaThread state: _thread_blocked

Locked ownable synchronizers:
    - None

"http-nio-8080-Acceptor-0" #30 daemon prio=5 tid=0x000055c3b5d71800 nid=0x2f runnable [0x00007fa0d13de000]
   java.lang.Thread.State: RUNNABLE
   JavaThread state: _thread_in_native
 - sun.nio.ch.ServerSocketChannelImpl.accept0(java.io.FileDescriptor, java.io.FileDescriptor, java.net.InetSocketAddress[]) @bci=0 (Interpreted frame)
 - sun.nio.ch.ServerSocketChannelImpl.accept(java.io.FileDescriptor, java.io.FileDescriptor, java.net.InetSocketAddress[]) @bci=4, line=525 (Interpreted frame)
 - sun.nio.ch.ServerSocketChannelImpl.accept() @bci=41, line=277 (Interpreted frame)
 - org.apache.tomcat.util.net.NioEndpoint.serverSocketAccept() @bci=4, line=448 (Interpreted frame)
 - org.apache.tomcat.util.net.NioEndpoint.serverSocketAccept() @bci=1, line=70 (Interpreted frame)
 - org.apache.tomcat.util.net.Acceptor.run() @bci=98, line=95 (Interpreted frame)
 - java.lang.Thread.run() @bci=11, line=835 (Interpreted frame)

Locked ownable synchronizers:
    - <0x00000000e3aab6e0>, (a java/util/concurrent/locks/ReentrantLock$NonfairSync)

"http-nio-8080-ClientPoller-0" #29 daemon prio=5 tid=0x000055c3b5c20000 nid=0x2e runnable [0x00007fa0d14df000]
   java.lang.Thread.State: RUNNABLE
   JavaThread state: _thread_in_native
 - sun.nio.ch.EPoll.wait(int, long, int, int) @bci=0 (Interpreted frame)
 - sun.nio.ch.EPollSelectorImpl.doSelect(java.util.function.Consumer, long) @bci=96, line=120 (Interpreted frame)
 - sun.nio.ch.SelectorImpl.lockAndDoSelect(java.util.function.Consumer, long) @bci=42, line=124 (Interpreted frame)
    - locked <0x00000000e392ece8> (a sun.nio.ch.EPollSelectorImpl)
    - locked <0x00000000e392ee38> (a sun.nio.ch.Util$2)
 - sun.nio.ch.SelectorImpl.select(long) @bci=31, line=136 (Interpreted frame)
 - org.apache.tomcat.util.net.NioEndpoint$Poller.run() @bci=55, line=743 (Interpreted frame)
 - java.lang.Thread.run() @bci=11, line=835 (Interpreted frame)

Locked ownable synchronizers:
    - None

"http-nio-8080-exec-10" #28 daemon prio=5 tid=0x000055c3b48d6000 nid=0x2d waiting on condition [0x00007fa0d15e0000]
   java.lang.Thread.State: WAITING (parking)
   JavaThread state: _thread_blocked
 - jdk.internal.misc.Unsafe.park(boolean, long) @bci=0 (Interpreted frame)
    - parking to wait for <0x00000000e3901670> (a java/util/concurrent/locks/AbstractQueuedSynchronizer$ConditionObject)
 - java.util.concurrent.locks.LockSupport.park(java.lang.Object) @bci=14, line=194 (Interpreted frame)
 - java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await() @bci=42, line=2081 (Interpreted frame)
 - java.util.concurrent.LinkedBlockingQueue.take() @bci=27, line=433 (Interpreted frame)
 - org.apache.tomcat.util.threads.TaskQueue.take() @bci=36, line=107 (Interpreted frame)
 - org.apache.tomcat.util.threads.TaskQueue.take() @bci=1, line=33 (Interpreted frame)
 - java.util.concurrent.ThreadPoolExecutor.getTask() @bci=147, line=1054 (Interpreted frame)
 - java.util.concurrent.ThreadPoolExecutor.runWorker(java.util.concurrent.ThreadPoolExecutor$Worker) @bci=26, line=1114 (Interpreted frame)
 - java.util.concurrent.ThreadPoolExecutor$Worker.run() @bci=5, line=628 (Interpreted frame)
 - org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run() @bci=4, line=61 (Interpreted frame)
 - java.lang.Thread.run() @bci=11, line=835 (Interpreted frame)
 //......

/ # jhsdb jstack --mixed --pid 1
Attaching to process ID 1, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 12+33
Deadlock Detection:

No deadlocks found.

----------------- 47 -----------------
"http-nio-8080-Acceptor-0" #30 daemon prio=5 tid=0x000055c3b5d71800 nid=0x2f runnable [0x00007fa0d13de000]
   java.lang.Thread.State: RUNNABLE
   JavaThread state: _thread_in_native
0x00007fa0ee0923ad      ????????
----------------- 46 -----------------
"http-nio-8080-ClientPoller-0" #29 daemon prio=5 tid=0x000055c3b5c20000 nid=0x2e runnable [0x00007fa0d14df000]
   java.lang.Thread.State: RUNNABLE
   JavaThread state: _thread_in_native
0x00007fa0ee05f3d0  epoll_pwait + 0x1d
0x00007fa0daa97810  * sun.nio.ch.EPoll.wait(int, long, int, int) bci:0 (Interpreted frame)
0x00007fa0daa91680  * sun.nio.ch.EPollSelectorImpl.doSelect(java.util.function.Consumer, long) bci:96 line:120 (Interpreted frame)
0x00007fa0db85f57c  * sun.nio.ch.SelectorImpl.lockAndDoSelect(java.util.function.Consumer, long) bci:42 line:124 (Compiled frame)
* sun.nio.ch.SelectorImpl.select(long) bci:31 line:136 (Compiled frame)
* org.apache.tomcat.util.net.NioEndpoint$Poller.run() bci:55 line:743 (Interpreted frame)
0x00007fa0daa91c88  * java.lang.Thread.run() bci:11 line:835 (Interpreted frame)
0x00007fa0daa88849  <StubRoutines>
0x00007fa0ed122952  _ZN9JavaCalls11call_helperEP9JavaValueRK12methodHandleP17JavaCallArgumentsP6Thread + 0x3c2
0x00007fa0ed1208d0  _ZN9JavaCalls12call_virtualEP9JavaValue6HandleP5KlassP6SymbolS6_P6Thread + 0x200
0x00007fa0ed1ccfc5  _ZL12thread_entryP10JavaThreadP6Thread + 0x75
0x00007fa0ed74f3a3  _ZN10JavaThread17thread_main_innerEv + 0x103
0x00007fa0ed74c3f5  _ZN6Thread8call_runEv + 0x75
0x00007fa0ed4a477e  _ZL19thread_native_entryP6Thread + 0xee
//......

–locks或者–mixed花费的时间可能比较长(几分钟,可能要将近6分钟),因而进程暂停的时间也可能比较长,在使用这两个选项时要注意

jhsdb jmap

jmap -heap pid

/ # jmap -heap 1
Error: -heap option used
Cannot connect to core dump or remote debug server. Use jhsdb jmap instead

jdk9及以上版本使用jmap -heap pid命令查看当前heap使用情况时,发现报错,提示需要使用jhsdb jmap来替代

jhsdb jmap pid

/ # jhsdb jmap 1
sh: jhsdb: not found

发现jlink的时候没有添加jdk.hotspot.agent这个module,添加了这个module之后可以发现JAVA_HOME/bin目录下就有了jhsdb

PTRACE_ATTACH failed

/ # jhsdb jmap 1
You have to set --pid or --exe.
    <no option> to print same info as Solaris pmap
    --heap  to print java heap summary
    --binaryheap    to dump java heap in hprof binary format
    --dumpfile  name of the dump file
    --histo to print histogram of java object heap
    --clstats   to print class loader statistics
    --finalizerinfo to print information on objects awaiting finalization
    --exe   executable image name
    --core  path to coredump
    --pid   pid of process to attach
/ # jhsdb jmap --heap --pid 1
Attaching to process ID 1, please wait...
ERROR: ptrace(PTRACE_ATTACH, ..) failed for 1: Operation not permitted
Error attaching to process: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 1: Operation not permitted
sun.jvm.hotspot.debugger.DebuggerException: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 1: Operation not permitted
    at jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$LinuxDebuggerLocalWorkerThread.execute(LinuxDebuggerLocal.java:176)
    at jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal.attach(LinuxDebuggerLocal.java:336)
    at jdk.hotspot.agent/sun.jvm.hotspot.HotSpotAgent.attachDebugger(HotSpotAgent.java:672)
    at jdk.hotspot.agent/sun.jvm.hotspot.HotSpotAgent.setupDebuggerLinux(HotSpotAgent.java:612)
    at jdk.hotspot.agent/sun.jvm.hotspot.HotSpotAgent.setupDebugger(HotSpotAgent.java:338)
    at jdk.hotspot.agent/sun.jvm.hotspot.HotSpotAgent.go(HotSpotAgent.java:305)
    at jdk.hotspot.agent/sun.jvm.hotspot.HotSpotAgent.attach(HotSpotAgent.java:141)
    at jdk.hotspot.agent/sun.jvm.hotspot.tools.Tool.start(Tool.java:185)
    at jdk.hotspot.agent/sun.jvm.hotspot.tools.Tool.execute(Tool.java:118)
    at jdk.hotspot.agent/sun.jvm.hotspot.tools.JMap.main(JMap.java:176)
    at jdk.hotspot.agent/sun.jvm.hotspot.SALauncher.runJMAP(SALauncher.java:326)
    at jdk.hotspot.agent/sun.jvm.hotspot.SALauncher.main(SALauncher.java:455)
Caused by: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 1: Operation not permitted
    at jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal.attach0(Native Method)
    at jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$1AttachTask.doit(LinuxDebuggerLocal.java:326)
    at jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$LinuxDebuggerLocalWorkerThread.run(LinuxDebuggerLocal.java:151)

发现PTRACE_ATTACH被docker禁用了,需要在运行容器时启用PTRACE_ATTACH

docker启用SYS_PTRACE

docker run --cap-add=SYS_PTRACE

之后就可以正常使用jhsdb如下:

/ # jhsdb jmap --heap --pid 1
Attaching to process ID 1, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 12+33

using thread-local object allocation.
Shenandoah GC with 4 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 523763712 (499.5MB)
   NewSize                  = 1363144 (1.2999954223632812MB)
   MaxNewSize               = 17592186044415 MB
   OldSize                  = 5452592 (5.1999969482421875MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   ShenandoahRegionSize     = 262144 (0.25MB)

Heap Usage:
Shenandoah Heap:
   regions   = 1997
   capacity  = 523501568 (499.25MB)
   used      = 70470552 (67.2059555053711MB)
   committed = 144441344 (137.75MB)

jhsdb jinfo

/ # jhsdb jinfo --help
    --flags to print VM flags
    --sysprops  to print Java System properties
    <no option> to print both of the above
    --exe   executable image name
    --core  path to coredump
    --pid   pid of process to attach

使用jhsdb显示jinfo的sysprops如下:

/ # jhsdb jinfo --sysprops --pid 1
Attaching to process ID 1, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 12+33
awt.toolkit = sun.awt.X11.XToolkit
java.specification.version = 12
sun.jnu.encoding = UTF-8
//......

这个命令其实跟jinfo -sysprops 1是等价的

jhsdb jsnap

/ # jhsdb jsnap --pid 1
Attaching to process ID 1, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 12+33
java.threads.started=27 event(s)
java.threads.live=24
java.threads.livePeak=24
java.threads.daemon=20
java.cls.loadedClasses=8250 event(s)
java.cls.unloadedClasses=1 event(s)
java.cls.sharedLoadedClasses=0 event(s)
java.cls.sharedUnloadedClasses=0 event(s)
java.ci.totalTime=18236958158 tick(s)
java.property.java.vm.specification.version=12
java.property.java.vm.specification.name=Java Virtual Machine Specification
java.property.java.vm.specification.vendor=Oracle Corporation
java.property.java.vm.version=12+33
java.property.java.vm.name=OpenJDK 64-Bit Server VM
java.property.java.vm.vendor=Azul Systems, Inc.
java.property.java.vm.info=mixed mode
java.property.jdk.debug=release
//......

jhsdb jsnap的功能主要是由jdk.hotspot.agent模块中的sun.jvm.hotspot.tools.JSnap.java来提供的,它可以用于查看threads及class loading/unloading相关的event、JVM属性参数等,其中–all可以显示更多的JVM属性参数

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片