# 3.2 实现原理

在上一节介绍了Attach API的基本使用,本节将结合JDK源码分析其中的原理。Attach机制本质上是进程间的通信,外部进程通过JVM提供的socket连接到目标JVM上并发送指令,目标JVM接受并处理指令然后返回处理结果。

# 3.2.1 Attach客户端源码解析

有了前面一节的API的使用基础,我们将分析Attach API的实现原理并对相应的源码做解析,从而挖掘更多可用的功能。VirtualMachine是抽象类,不同厂商的虚拟机可以实现不同VirtualMachine子类,HotSpotVirtualMachine是HotSpot官方提供的VirtualMachine实现,它也是一个抽象类,在不同操作系统上还有各自实现,如Linux系统上,JDK11版本的实现类的名称为VirtualMachineImpl(JDK8上实现类名称为LinuxVirtualMachine)。JDK8上VirtualMachine实现类的的继承关系如下图3-2所示:

图3-2 VirtualMachine实现类的继承关系

图3-2 VirtualMachineImpl继承关系.png

先来看下HotSpotVirtualMachine类的loadAgentLibrary方法

代码位置:src/jdk.attach/share/classes/sun/tools/attach/HotSpotVirtualMachine.java

private void loadAgentLibrary(String agentLibrary, boolean isAbsolute, String options)
    throws AgentLoadException, AgentInitializationException, IOException
{
    if (agentLibrary == null) {
        throw new NullPointerException("agentLibrary cannot be null");
    }
    
    // jdk11返回字符串"return code: 0"
    String msgPrefix = "return code: ";
    // 执行load指令,给目标 jvm 传输 agent jar路径和参数
    InputStream in = execute("load",
                             agentLibrary,
                             isAbsolute ? "true" : "false",
                             options);
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
        String result = reader.readLine();
        // 返回结果
        if (result == null) {
            throw new AgentLoadException("Target VM did not respond");
        } else if (result.startsWith(msgPrefix)) {
            int retCode = Integer.parseInt(result.substring(msgPrefix.length()));
            if (retCode != 0) {
                throw new AgentInitializationException("Agent_OnAttach failed", retCode);
            }
        } else {
            throw new AgentLoadException(result);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

上面的代码是加载一个Java Agent,核心实现在 execute 方法中,来看下execute方法的源码:

// 在目标JVM上执行给定的命令,需要由子类来实现
abstract InputStream execute(String cmd, Object ... args)
    throws AgentLoadException, IOException;
1
2
3

execute是一个抽象方法,需要在子类中实现,HotSpotVirtualMachine类中的其他方法大多数最终都会调用这个execute方法。

再来看下Linux系统上的实现类LinuxVirtualMachine代码。

代码位置:src/jdk.attach/linux/classes/sun/tools/attach/VirtualMachineImpl.java

VirtualMachineImpl(AttachProvider provider, String vmid)
    throws AttachNotSupportedException, IOException
{
    super(provider, vmid);

    int pid;
    try {
        pid = Integer.parseInt(vmid);
    } catch (NumberFormatException x) {
        throw new AttachNotSupportedException("Invalid process identifier");
    }
    // 在/tmp目录下寻找socket文件是否存在                    
    File socket_file = new File(tmpdir, ".java_pid" + pid);
    socket_path = socket_file.getPath();
    if (!socket_file.exists()) {
        // 创建 attach_pid 文件
        File f = createAttachFile(pid);
        try {
            // 向目标JVM 发送 kill -3 信号
            sendQuitTo(pid);

            // 等待目标JVM创建socket文件
            final int delay_step = 100;
            final long timeout = attachTimeout();
            long time_spend = 0;
            long delay = 0;
            do {
                // 计算等待时间
                delay += delay_step;
                try {
                    Thread.sleep(delay);
                } catch (InterruptedException x) { }

                time_spend += delay;
                if (time_spend > timeout/2 && !socket_file.exists()) {
                    sendQuitTo(pid); // 发送kill -3 信号
                }
            } while (time_spend <= timeout && !socket_file.exists());
            
            // 等待时间结束后,确认socket文件是否存在
            if (!socket_file.exists()) {
                throw new AttachNotSupportedException(
                    String.format("Unable to open socket file %s: " +
                                  "target process %d doesn't respond within %dms " +
                                  "or HotSpot VM not loaded", socket_path,
                                  pid, time_spend));
            }
        } finally {
            // 最后删除 attach_pid 文件
            f.delete();
        }
    }

    // 确认socket文件权限
    checkPermissions(socket_path);

    // 尝试连接socket,确认可以连接到目标JVM
    int s = socket();
    try {
        connect(s, socket_path);
    } finally {
        close(s);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

再次梳理下attach通信的过程:

第一步: 发起attach的进程在/tmp目录下查找目标JVM是否已经创建了.java_pid$pid ,如果已经创建,直接跳到第六步;

第二步: attach进程创建socket通信的握手文件.attach_pid$pid;

第三步: attach进程给目标JVM发送SIGQUIT(kill -3)信号,提示目标JVM外部进程发起了attach请求;

第四步: attach进程循环等待目标JVM创建.java_pid$pid文件;

第五步: 删除握手文件.attach_pid$pid文件;

第六步: attach进程校验socket文件权限;

第七步: attach进程测试socket连接可用性;

上面详细说明了socket连接的建立过程,下面将介绍发送命令的协议。

代码位置:src/jdk.attach/linux/classes/sun/tools/attach/VirtualMachineImpl.java

InputStream execute(String cmd, Object ... args) throws AgentLoadException, IOException {
    // 参数、socket_path校验
        
    // create UNIX socket
    int s = socket();

    // connect to target VM
    try {
        connect(s, socket_path);
    } catch (IOException x) {
        // 错误处理
    }

    IOException ioe = null;

    // 发送attach请求信息
    try {
        // 发送协议
        writeString(s, PROTOCOL_VERSION);
        // 发送命令
        writeString(s, cmd);
        // 发送参数,最多三个参数
        for (int i=0; i<3; i++) {
            if (i < args.length && args[i] != null) {
                writeString(s, (String)args[i]);
            } else {
                // 没有参数,发送空字符串代替
                writeString(s, "");
            }
        }
    } catch (IOException x) {
        ioe = x;
    }

    // 读取执行结果
    SocketInputStream sis = new SocketInputStream(s);

    // 读取命令的执行状态
    int completionStatus;
    try {
        completionStatus = readInt(sis);
    } catch (IOException x) {
        // 错误处理
    }

    if (completionStatus != 0) {
        // 错误处理
    }
    
    return sis;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

从上面的代码可以知道一次命令发送,先发送版本协议,然后是命令,最后是参数,并且参数的个数最多为3个。

为了更加清晰的看到通信协议的内容,在Linux上使用strace命令能够跟踪attach的系统调用过程。

strace -f java Main 2> main.out
1

在 main.out 文件中找到attach通信过程,从开始写入部分可以看出依次先写入协议号、命令、命令参数, 然后读取返回结果。

// 建立UDS链接
[pid 31412] socket(AF_LOCAL, SOCK_STREAM, 0) = 6
[pid 31412] connect(6, {sa_family=AF_LOCAL, sun_path="/tmp/.java_pid27730"}, 110) = 0
// 开始写入
[pid 31412] write(6, "1", 1)            = 1   // 协议号
[pid 31412] write(6, "\0", 1)           = 1   // 分割符号
[pid 31412] write(6, "properties", 10)  = 10  // 命令
[pid 31412] write(6, "\0", 1)           = 1   // 分割符号
[pid 31412] write(6, "\0", 1 &lt;unfinished ...> // 参数1
[pid 31412] write(6, "\0", 1)           = 1   // 参数2
[pid 31412] write(6, "\0", 1)           = 1   // 参数3
// 读取返回结果
[pid 31412] read(6, "0", 1)             = 1
[pid 31412] read(6, "\n", 1)            = 1
[pid 31412] read(6, "#Thu Jul 27 17:52:11 CST 2023\nja"..., 128) = 128
[pid 31412] read(6, "oot.loader\nsun.boot.library.path"..., 128) = 128
[pid 31412] read(6, "poration\njava.vendor.url=http\\:/"..., 128) = 128
[pid 31412] read(6, ".pkg=sun.io\nuser.country=CN\nsun."..., 128) = 128
[pid 31412] read(6, "e=Java Virtual Machine Specifica"..., 128) = 128
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

因此Attach客户端的发送协议可以总结为下面的字符串序列。

1 byte PROTOCOL_VERSION
1 byte '\0'
n byte command
1 byte '\0'
n byte arg1
1 byte '\0'
n byte arg2
1 byte '\0'
n byte arg3
1 byte '\0'
1
2
3
4
5
6
7
8
9
10

# 3.2.2 Attach服务端源码解析

我们再来看下接收Attach命令的服务端是如何实现的,这部分代码是c/c++语言,但是也是不难理解的。 以Linux系统为例子,说明目标JVM如何处理Attach请求和执行指定的命令。

Linux系统下Attach机制信号与线程的创建流程可以描述为下图3-3。

图3-3 Attach机制信号与线程的处理流程

图3-3 Attach机制信号与线程的处理流程

先来看下目标JVM如何处理kill -3信号。JVM初始化过程中会创建2个线程,线程名称分别为Signal DispatcherAttach Listener,Signal Dispatcher线程用来处理信号量,Attach Listener线程用来响应Attach请求。

JVM线程的的初始化都在Threads::create_vm中,当然与Attach有关的线程也在这个方法中初始化。

代码位置:src/hotspot/share/runtime/thread.cpp

// 代码位置 src/hotspot/share/runtime/thread.cpp
jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain) {

  // 参数和系统初始化,省略....

  // 初始化Signal Dispatcher线程支持信号量处理
  os::initialize_jdk_signal_support(CHECK_JNI_ERR);

  // 目标JVM没有禁用Attach机制
  if (!DisableAttachMechanism) {
  	// 在JVM启动时删除已经存在的通信文件.java_pid$pid
    AttachListener::vm_start();
    
    // 如果JVM启动参数设置-XX:+StartAttachListener或者
    // 减少了信号量的使用而不能延迟启动,则在JVM启动时初始化Attach Listener
    // 默认情况下AttachListener是延迟启动模式,即在JVM启动时不会立即创建Attach Listener线程
    if (StartAttachListener || AttachListener::init_at_startup()) {
      // 初始化Attach Listener线程
      AttachListener::init();
    }
  }
  
  // 参数和系统初始化,省略....
}  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

上面的代码中分别初始化Signal Dispatcher和Attach Listener线程,Signal Dispatcher在JVM 启动时初始化,Attach Listener则延迟初始化。下面分别详细说下各自的初始化流程。

# 3.2.2.1 Signal Dispatcher线程

initialize_jdk_signal_support的实现代码如下所示:

代码位置:src/hotspot/share/runtime/os.cpp

// 代码位置 src/hotspot/share/runtime/os.cpp
// 初始化JDK的信号支持系统
void os::initialize_jdk_signal_support(TRAPS) {
  // 没有禁止信号量的使用
  if (!ReduceSignalUsage) {
  
    // 线程名称 Signal Dispatcher
    const char thread_name[] = "Signal Dispatcher";
    
    // ... 线程初始化过程

    // 设置线程入口 signal_thread_entry
    JavaThread* signal_thread = new JavaThread(&signal_thread_entry);
    
    // ...
    
    // 注册SIGBREAK信号处理handler
    os::signal(SIGBREAK, os::user_handler());
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

JVM创建了一个单独的线程来实现信号处理,这个线程名称为Signal Dispatcher。该线程的入口是signal_thread_entry函数。入口函数代码:

代码位置 src/hotspot/share/runtime/os.cpp

#ifndef SIGBREAK
#define SIGBREAK SIGQUIT  // SIGBREAK就是SIGQUIT
#endif

// Signal Dispatcher线程的入口
static void signal_thread_entry(JavaThread* thread, TRAPS) {
  os::set_priority(thread, NearMaxPriority);
  // 处理信号
  while (true) {
    int sig;
    {
      sig = os::signal_wait(); //阻塞等待信号
    }
    if (sig == os::sigexitnum_pd()) {
       // 停止Signal Dispatcher信号处理线程
       return;
    }
    
    // 循环处理各种信号
    switch (sig) {
    	// 当接收到SIGBREAK信号,就执行接下来的代码
      case SIGBREAK: {
        
        // 如果没有禁用attach机制并且是attach请求则初始化AttachListener
        // 如果AttachListener没有初始化,则进行初始化并返回true
        if (!DisableAttachMechanism && AttachListener::is_init_trigger()) {
          continue;
        }
        
        // 如果上面条件不满足,则打印线程栈等信息
        VM_PrintThreads op;
        VMThread::execute(&op);    // 线程栈信息
        VM_PrintJNI jni_op;
        VMThread::execute(&jni_op);// JNI global references数量
        VM_FindDeadlocks op1(tty);
        VMThread::execute(&op1);   // 死锁信息
        Universe::print_heap_at_SIGBREAK(); // 堆、元空间的使用占比
        
        // 启用-XX:+PrintClassHistogram,则强制执行一次full GC
        if (PrintClassHistogram) {
          // 下面的true表示force full GC before heap inspection
          VM_GC_HeapInspection op1(tty, true);
          VMThread::execute(&op1);
        }
        if (JvmtiExport::should_post_data_dump()) {
          JvmtiExport::post_data_dump();
        }
        break;
      }
      default: {
        // Dispatch the signal to java
        // ...其他信号处理
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

代码行号1~3定义了宏SIGBREAK,可以看出,SIGBREAK信号就是SIGQUIT。代码26行的DisableAttachMechanism参数可以禁止attach,默认为false,即允许attach。

再来看下AttachListener::is_init_trigger的实现。

代码位置:src/hotspot/os/linux/attachListener_linux.cpp

// 如果在JVM工作目录或者/tmp目录下存在文件.attach_pid$pid
// 表示是启动attach机制
bool AttachListener::is_init_trigger() {
  // 记录AttachListener的初始状态
  // JVM 用一个全局变量_is_initialized记录AttachListener 的状态
  if (init_at_startup() || is_initialized()) {
    // AttachListener在JVM启动时已经初始化或者已经是初始化的状态
    return false;               
  }
  
  // 检查.attach_pid是否存在
  char fn[PATH_MAX + 1];
  int ret;
  struct stat64 st;
  sprintf(fn, ".attach_pid%d", os::current_process_id());
  RESTARTABLE(::stat64(fn, &st), ret);
  if (ret == -1) {
    // .attach_pid文件不存在,打印日志,代码省略...
  }
  
  // 当前进程的.attach_pid文件存在,创建AttachListener线程
  if (ret == 0) {
    // attach文件权限校验(root权限或者权限相同)
    if (os::Posix::matches_effective_uid_or_root(st.st_uid)) {
      // 创建AttachListener线程
      init();
      return true;
    } 
  }
  return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

在Signal Dispatcher线程接收到SIGBREAK信号后,有两种处理方法,第一种是初始化AttachListener线程;第二种打印线程栈等快照信息。处理方式取决于.attach_pid握手文件是否存在,如果存在则调用AttachListener的init方法,初始化成功后返回true。

# 3.2.2.2 Attach Listener

Attach机制通过Attach Listener线程来进行相关命令的处理,下面来看一下Attach Listener线程是如何初始化的。从上面的代码分析可以看出,AttachListener可以在JVM启动时(立即初始化),也可以在首次收到SIGBREAK信号后,由Signal Dispatcher线程完成初始化(延迟初始化)。

来看下Attach Listener初始化过程。

代码位置:src/hotspot/os/linux/attachListener_linux.cpp

void AttachListener::init() {
  
  // 线程名称Attach Listener
  const char thread_name[] = "Attach Listener";
  
  // ... 线程初始化过程
  
  // 设置AttachListener线程的入口函数attach_listener_thread_entry
  JavaThread* listener_thread = new JavaThread(&attach_listener_thread_entry);
  
  // ... 设置线程状态
}
1
2
3
4
5
6
7
8
9
10
11
12

上面的代码初始化了一个线程,并设置线程的入口函数。重点分析下attach_listener_thread_entry函数。

代码位置:src/hotspot/share/services/attachListener.cpp

// Attach Listener线程从队列中获取操作命令,并执行命令对应的函数
static void attach_listener_thread_entry(JavaThread* thread, TRAPS) {
  // STEP1:AttachListener初始化
  if (AttachListener::pd_init() != 0) {
    return;
  }
  // STEP2:设置AttachListener的全局状态
  AttachListener::set_initialized();

  for (;;) {
    // STEP3:从队列中取AttachOperation
    AttachOperation* op = AttachListener::dequeue();
    // find the function to dispatch too
    AttachOperationFunctionInfo* info = NULL;
    for (int i=0; funcs[i].name != NULL; i++) {
      const char* name = funcs[i].name;
      if (strcmp(op->name(), name) == 0) {
        info = &(funcs[i]); break;
      }}
      // dispatch to the function that implements this operation
      // ... 执行具体的操作
      res = (info->func)(op, &st);
    //...
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

第一步:先执行AttachListener socket的初始化操作;

第二步:初始化完成后设置,AttachListener的状态为initialized;

第三步:从队列中取AttachOperation,并且调用对应的处理函数处理并返回结果。

下面分别对这个过程详细分析。

# AttachListener::pd_init

代码位置:src/hotspot/os/linux/attachListener_linux.cpp

int AttachListener::pd_init() {
  
  // linux 系统下的初始化操作  
  int ret_code = LinuxAttachListener::init();
  
  // ...
  
  return ret_code;
}
1
2
3
4
5
6
7
8
9

实际执行的是LinuxAttachListener::init,不同操作系统执行初始化逻辑不同。在Linux系统中实际执行LinuxAttachListener::init。

代码位置:src/hotspot/os/linux/attachListener_linux.cpp

// 创建了一个socket并监听socket文件
int LinuxAttachListener::init() {
  char path[UNIX_PATH_MAX];          // socket file
  char initial_path[UNIX_PATH_MAX];  // socket file during setup
  int listener;                      // listener socket (file descriptor)

  // register function to cleanup
  ::atexit(listener_cleanup);

  int n = snprintf(path, UNIX_PATH_MAX, "%s/.java_pid%d",
                   os::get_temp_directory(), os::current_process_id());
  if (n < (int)UNIX_PATH_MAX) {
    n = snprintf(initial_path, UNIX_PATH_MAX, "%s.tmp", path);
  }
  if (n >= (int)UNIX_PATH_MAX) {
    return -1;
  }

  // create the listener socket
  listener = ::socket(PF_UNIX, SOCK_STREAM, 0);
  if (listener == -1) {
    return -1;
  }

  // 绑定socket
  struct sockaddr_un addr;
  memset((void *)&addr, 0, sizeof(addr));
  addr.sun_family = AF_UNIX;
  strcpy(addr.sun_path, initial_path);
  ::unlink(initial_path);
  int res = ::bind(listener, (struct sockaddr*)&addr, sizeof(addr));
  if (res == -1) {
    ::close(listener);
    return -1;
  }

  // 开启监听
  res = ::listen(listener, 5);
  if (res == 0) {
    RESTARTABLE(::chmod(initial_path, S_IREAD|S_IWRITE), res);
    if (res == 0) {
      // make sure the file is owned by the effective user and effective group
      // e.g. the group could be inherited from the directory in case the s bit is set
      RESTARTABLE(::chown(initial_path, geteuid(), getegid()), res);
      if (res == 0) {
        res = ::rename(initial_path, path);
      }
    }
  }n'n'n'h
  if (res == -1) {
    ::close(listener);
    ::unlink(initial_path);
    return -1;
  }
  set_path(path);
  set_listener(listener);

  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

AttachListener::pd_init()方法调用了LinuxAttachListener::init()方法,完成了套接字的创建和监听。

# LinuxAttachListener::dequeue

for循环的执行逻辑,流程简略的概括为下面的步骤:

  • 从dequeue拉取一个需要执行的AttachOperation对象;
  • 查询匹配的命令处理函数;
  • 执行匹配到的命令执行函数并返回结果;

AttachOperation的全部操作函数表如下:

代码位置:src/hotspot/share/services/attachListener.cpp

static AttachOperationFunctionInfo funcs[] = {
  { "agentProperties",  get_agent_properties },
  { "datadump",         data_dump },
  { "dumpheap",         dump_heap },
  { "load",             load_agent },
  { "properties",       get_system_properties },
  { "threaddump",       thread_dump },
  { "inspectheap",      heap_inspection },
  { "setflag",          set_flag },
  { "printflag",        print_flag },
  { "jcmd",             jcmd },
  { NULL,               NULL }
};
1
2
3
4
5
6
7
8
9
10
11
12
13

对于加载Agent来说,对应的命令就是上面的load。现在,我们知道了Attach Listener大概的工作模式,但是还是不太清楚任务从哪来,这个秘密就藏在AttachListener::dequeue这行代码里面,接下来我们来分析一下dequeue这个函数:

代码位置:src/hotspot/os/linux/attachListener_linux.cpp

LinuxAttachOperation* LinuxAttachListener::dequeue() {
  for (;;) {
    // 等待attach进程连接socket
    struct sockaddr addr;
    socklen_t len = sizeof(addr);
    RESTARTABLE(::accept(listener(), &addr, &len), s);
    // 校验attach进程的权限
    struct ucred cred_info;
    socklen_t optlen = sizeof(cred_info);
    if (::getsockopt(s, SOL_SOCKET, SO_PEERCRED, (void*)&cred_info, &optlen) == -1) {
      ::close(s);
      continue;
    }
    // 读取socket获取操作的对象
    LinuxAttachOperation* op = read_request(s);
    return op;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

dequeue方法是一个for循环,会循环使用accept方法,接受socket中传过来的数据,并且在验证通信的另一端的uid与gid与自身的euid与egid相同后,执行read_request方法,从socket读取内容,并且把内容包装成AttachOperation类的一个实例。接下来看看read_request是如何解析socket数据流的。

代码位置:src/hotspot/os/linux/attachListener_linux.cpp

LinuxAttachOperation* LinuxAttachListener::read_request(int s) {
  // 缓存区最大长度计算,省略...
  
  char buf[max_len];
  int str_count = 0;
  
  // 数据流写入buf
  // 包括版本去掉命令数据的分割符号代码"\0"
  // 版本协议校验等,省略...

  // 参数遍历
  ArgumentIterator args(buf, (max_len)-left);

  // 协议版本
  char* v = args.next();
  // 命令名称  
  char* name = args.next();
  if (name == NULL || strlen(name) > AttachOperation::name_length_max) {
    return NULL;
  }
  
  // 创建AttachOperation对象
  LinuxAttachOperation* op = new LinuxAttachOperation(name);
  
  // 从buf中读取AttachOperation参数  
  for (int i=0; i<AttachOperation::arg_count_max; i++) {
    char* arg = args.next();
    if (arg == NULL) {
      op->set_arg(i, NULL);
    } else {
      if (strlen(arg) > AttachOperation::arg_length_max) {
        delete op;
        return NULL;
      }
      op->set_arg(i, arg);
    }
  }
  
  // 将socket引用设置到op对象中
  op->set_socket(s);
  return op;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

这是Linux上的实现,不同的操作系统实现方式不一样。Attach Listener线程监听.java_pid文件,等待Attach 客户端发起连接,解析Attach 客户端的Attach request 请求信息,将请求的字节流包装成一个AttachOperation类型的对象,之后就会从表里查询对应的处理函数,然后进行处理并返回处理结果。

Attach 机制详细的交互流程可以用下面的图3-4描述。

图3-4 Attach交互处理流程

图3-4 Attach交互处理流程

# 3.2.3 Attach机制涉及到的JVM参数

这里重新总结下Attach机制涉及到JVM参数。如下表3-1所示。

表3-1 Attach机制相关的JVM参数

名称 含义 默认值
ReduceSignalUsage 禁止信号量使用 false
DisableAttachMechanism 禁止attach到当前JVM false
StartAttachListener JVM 启动时初始化AttachListener false
EnableDynamicAgentLoading 允许运行时加载Agent true

JVM 参数都在src/hotspot/share/runtime/globals.hpp 中定义