前言

呵呵 最近有一些 需要远程调试 flink 代码的需求 

然后 太久了不用, 有些 生疏, 然后 碰到了一些问题  

如下配置添加到了 flink taskManager 上面之后, 发现 taskManager 一直没有启动起来 

java -Xrunjdwp:transport=dt_socket,suspend=y,server=y,address=3317 com.hx.test12.Test13RemoteDebug

然后 使用 jps 查看进程, taskmanage 对应的进程一直查询不出来, 最终得到的显示是 "-- main class information unavailable" 

然后 才在网上搜索了一下 这个 suspend=y 的意思

呵呵 当然网上搜索到的那是一个 理解, 但是缺少一些 实质性的一些判断 来让你确信这个理解的东西 

另外 我们还可以看一下 jps 在这里 为什么显示的是 "-- main class information unavailable", 另外就是 jps 明显停顿了几秒, 为什么会停顿几秒 ? 

以下调试 vm 部分基于 jdk9, 其他基于 jdk8 

测试用例 

/**
 * Test13RemoteDebug
 *
 * @author Jerry.X.He 
 * @version 1.0
 * @date 2021-11-01 18:53
 */
public class Test13RemoteDebug {

    // Test13RemoteDebug
    // java -Xrunjdwp:transport=dt_socket,suspend=y,server=y,address=3317 com.hx.test12.Test13RemoteDebug
    // 新建远程连接 配置 ip, port 进行远程调试
    public static void main(String[] args) throws Exception {

        int i = 0;
        while (true) {
            System.out.println(i++);
            Thread.sleep(3000);
        }

    }

}

jps 信息如下 

master:jdk jerry$ jps
3266 -- main class information unavailable

jstack 查看 main 的堆栈信息如下 

看不到任何堆栈信息, 因为阻塞的时候还没有开始执行任何 java 代码 

"main" #1 prio=5 os_prio=31 tid=0x00007fd877003800 nid=0x1a03 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   JavaThread state: _thread_blocked
Thread: 0x00007fd877003800  [0x1a03] State: _at_safepoint _has_called_back 0 _at_poll_safepoint 0
   JavaThread state: _thread_blocked

HotspotVM 阻塞在了那里?

vm 是处于 createVM 函数中, 因为我们配置了 suspend=y, 因此 这里需要等待 debugMonitor 被唤醒[gdata->jvmti] 

46 调试启动 suspend=y 的情况下, jps 得到 -- main class information unavailable_jvm

从整理 createVM 的流程上来看, 已经创建了 vm, 已经初始化过了 

注意这张图, 和待会儿 jps 的 "-- main class information unavailable" 有关系 

46 调试启动 suspend=y 的情况下, jps 得到 -- main class information unavailable_debug_02

HotspotVM 怎么被唤醒 ?

新建一个 java 远程连接, 并连接 

46 调试启动 suspend=y 的情况下, jps 得到 -- main class information unavailable_HotspotVM_03

正常流程的唤醒是在 有 debugger 连接上了之后, 这里调用的是 debugMonitorNotifyAll, 唤醒的就是上面 wait 的 main 线程 

这里看到的上面 (*t) -> Accept 里面除了 正常的 tcp 握手之外, 包含的是一个 JDWP 逻辑意义上的一个 握手, 客户端需要发送 "JDWP-Handshake" 这十四个字节序列到 服务端 

是由 JDWP 规范约束的  

46 调试启动 suspend=y 的情况下, jps 得到 -- main class information unavailable_jpda_04

main 线程被唤醒之后, 继续走 createVM 之后的流程, 程序 正常启动 

46 调试启动 suspend=y 的情况下, jps 得到 -- main class information unavailable_HotspotVM_05

程序正常执行 

46 调试启动 suspend=y 的情况下, jps 得到 -- main class information unavailable_jvm_06

jps 为什么显示 "-- main class information unavailable" ?

获取对应的进程的启动命令的时候, 需要 attach 到给定的 vm, 获取 PerfDataPrologue 的信息, 判断 vm 是否准备好了, 这里等待 了 syncWaitMs[默认是5s], 可是一直没有等到 PerfDataPrologue.accessable, 最终抛出了异常 “Could not synchronize with target”   

46 调试启动 suspend=y 的情况下, jps 得到 -- main class information unavailable_HotspotVM_07

异常来到 jps 外层, 我们看到的 “-- main class information unavailable” 是获取 mainClass 阶段的一个默认的错误信息

意思是只要是 获取 mainClass 阶段发生了任意异常, 我们都会得到 "-- main class information unavailable"

46 调试启动 suspend=y 的情况下, jps 得到 -- main class information unavailable_jpda_08

我们来看一下 PerfDataPrologue.access 是哪里被设置为 true 的? 

可以看到的是在 createVM 里面, 然后你可以回顾一下 上面的 wait 的哪一张图片, create_vm_timer.end() 是 createVM 里面的倒数第几行代码 

46 调试启动 suspend=y 的情况下, jps 得到 -- main class information unavailable_jvm_09

jps 是如何获取所有的进程信息的? 

读取的是 hsperfdata_* 文件夹下面的信息, 里面的是各个进程号, 然后根据 进程号获取 jps 本身需要的相关信息 

master:jdk jerry$ ll /var/folders/pw/lb8dvl7d6474r5plrnwtcp180000gn/T/hsperfdata_jerry/ 
total 576
-rw-------  1 jerry  staff  32768 Nov  7 16:01 1770
-rw-------  1 jerry  staff  32768 Nov  7 19:55 3338
-rw-------  1 jerry  staff  32768 Nov  7 20:18 3520
-rw-------  1 jerry  staff  32768 Nov  7 20:20 3526
-rw-------  1 jerry  staff  32768 Nov  7 20:20 3527
-rw-------  1 jerry  staff  32768 Nov  7 20:24 3639
-rw-------  1 jerry  staff  32768 Nov  7 10:51 661
-rw-------  1 jerry  staff  32768 Nov  7 10:53 760
-rw-------  1 jerry  staff  32768 Nov  7 11:29 948

基于 jdwp协议 和 HotSpotVM 进行交互

呵呵 演示版本, 以后有机会 放出来 

import com.alibaba.fastjson.JSON;
import com.hx.codec.utils.IoUtils;
import com.hx.net.client.ClientChannelHandler;
import com.hx.net.client.jdwp.JdwpClient;
import com.hx.net.common.BaseProtocolTests;
import com.hx.net.config.ClientConfig;
import com.hx.net.protocol.jdwp.JdwpClientProtocol;
import com.hx.net.protocol.jdwp.common.JdwpMessage;
import com.hx.net.protocol.jdwp.msg.JdwpHandShake;
import com.hx.net.protocol.jdwp.msg.JdwpRequest;
import com.hx.net.protocol.jdwp.msg.JdwpResponse;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;

/**
 * Test08JdwpClientProtocol
 *
 * @author Jerry.X.He
 * @version 1.0
 * @date 2021-11-07 16:30
 */
@FixMethodOrder(value = MethodSorters.NAME_ASCENDING)
public class Test08JdwpClientProtocol extends BaseProtocolTests {

    @Test
    public void test02Server() throws Exception {

        JdwpClientProtocol clientProtocol = new JdwpClientProtocol();

        ClientConfig clientConfig = new ClientConfig();
        clientConfig.setHost("localhost");
        clientConfig.setPort(3317);
        JdwpClient client = new JdwpClient(clientConfig, clientProtocol, new JdwpClientHandler(), new JdwpSocketChannelHandler());

        client.start();

        // sleep for biz, then stop
        IoUtils.sleep(15000 * 1000);

        client.stop();

    }

    // ------------------------------------------ assist methods ------------------------------------------

    /**
     * JdwpClientHandler
     *
     * @author Jerry.X.He
     * @version 1.0
     * @date 2021-11-07 16:32
     */
    @ChannelHandler.Sharable
    static class JdwpClientHandler extends SimpleChannelInboundHandler<JdwpMessage> {
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, JdwpMessage msg) throws Exception {
            LOGGER.info(" client received : {} ", JSON.toJSONString(msg));
            if (msg instanceof JdwpHandShake) {
                JdwpRequest entity = new JdwpRequest();
//                entity.setLen(11);
                entity.setId(1171);
                entity.setFlags(0);
                entity.setCmdSet(1);
                entity.setCmd(7);
                entity.setData(new Integer[]{});
                ctx.writeAndFlush(entity);
            } else if (msg instanceof JdwpResponse) {

            }
        }
    }

    /**
     * JdwpSocketChannelHandler
     *
     * @author Jerry.X.He
     * @version 1.0
     * @date 2021-11-07 16:33
     */
    static class JdwpSocketChannelHandler implements ClientChannelHandler {
        @Override
        public void doBiz(Channel channel) throws Exception {
            JdwpHandShake handShakeEntity = new JdwpHandShake();
            channel.writeAndFlush(handShakeEntity);
            System.in.read();
        }
    }

}

日式输出如下 

可以看到的是 客户端发送了 JDWP握手 信息之后, HotspotVM 回复了 JDWP握手回复 信息 

客户端拿到 HotspotVM 的 JDWP握手回复 之后, 发送了一个 cmdSet 为 1, cmd 为 7 的一个请求过去, 此请求对应的 handler 是 VirtualMachine_Cmds.idSizes 

然后 HotspotVM 响应给了我的客户端 5 个 8, 也就是对应的 idsSize 里面输出的 5 个 8

[20:43:00.458] INFO  com.hx.net.interceptor.common.ChannelStateInterceptor 23 channelActive -  channelActive : [id: 0x27ca94e3, L:/127.0.0.1:54489 - R:localhost/127.0.0.1:3317]
[20:43:00.662] INFO  com.hx.net.protocol.Test08JdwpClientProtocol$JdwpClientHandler 151 channelRead0 -  client received : {"echo":"JDWP-Handshake"} 
[20:43:00.732] INFO  com.hx.net.protocol.Test08JdwpClientProtocol$JdwpClientHandler 151 channelRead0 -  client received : {"data":[0,0,0,8,0,0,0,8,0,0,0,8,0,0,0,8,0,0,0,8],"errorCode":0,"flags":-128,"id":1171,"len":31} 
Disconnected from the target VM, address: '127.0.0.1:54486', transport: 'socket'
[20:43:14.224] INFO  com.hx.net.interceptor.common.ChannelStateInterceptor 29 channelInactive -  channelInactive : [id: 0x27ca94e3, L:/127.0.0.1:54489 ! R:localhost/127.0.0.1:3317]

VirtualMachine_Cmds.idSizes 如下 

46 调试启动 suspend=y 的情况下, jps 得到 -- main class information unavailable_jvm_10

jpda 的各个角色 

在我们通常调试的场景中 

HotspotVM 提供了 jvmti 的实现, jwdp 服务端的实现, 实现的是 调试相关操作 落地到 vm 的相关处理 

idea 提供了 jdwp 客户端的实现, 和 jdi的调用 最终就是我们看到的这个前端这一套 

奉上 jpda, jdwp 相关规范文档  

Java™ Platform Debugger Architecture (JPDA)

JavaTM Debug Wire Protocol

提供一个完整的 usage, 让你不再迷茫 

ERROR: JDWP option syntax error: -agentlib:jdwp=trhelp
master:classes jerry$ java -Xrunjdwp:help com.hx.test12.Test13RemoteDebug
               Java Debugger JDWP Agent Library
               --------------------------------

  (see http://java.sun.com/products/jpda for more information)

jdwp usage: java -agentlib:jdwp=[help]|[<option>=<value>, ...]

Option Name and Value            Description                       Default
---------------------            -----------                       -------
suspend=y|n                      wait on startup?                  y
transport=<name>                 transport spec                    none
address=<listen/attach address>  transport spec                    ""
server=y|n                       listen for debugger?              n
launch=<command line>            run debugger on event             none
onthrow=<exception name>         debug on throw                    none
onuncaught=y|n                   debug on any uncaught?            n
timeout=<timeout value>          for listen/attach in milliseconds n
mutf8=y|n                        output modified utf-8             n
quiet=y|n                        control over terminal messages    n

Obsolete Options
----------------
strict=y|n
stdalloc=y|n

Examples
--------
  - Using sockets connect to a debugger at a specific address:
    java -agentlib:jdwp=transport=dt_socket,address=localhost:8000 ...
  - Using sockets listen for a debugger to attach:
    java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y ...

Notes
-----
  - A timeout value of 0 (the default) is no timeout.

Warnings
--------
  - The older -Xrunjdwp interface can still be used, but will be removed in
    a future release, for example:
        java -Xdebug -Xrunjdwp:[help]|[<option>=<value>, ...]

完