JVM中的監(jiān)聽信號的線程以及Unix域套接字通信的線程
【實驗】
package com.infuq.tmp;public class Main {public static void main(String args[]) {for (;;) {try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}}}}
以上代碼中,讓JVM不退出,我們對它做點手腳,看一下JVM中的兩個線程.
編譯之后運行它.
通過jps查看進程號=6617

查看進程6617的線程
ps -Lf 6617

共計20個輕量級進程(LWP),即線程.
也可以通過/proc/6617/task查看進程6617下有多少個任務(wù)(即線程), 也是20個線程,如下.

我們再看一下這個進程6617打開的文件描述符,如下
ls -l /proc/6617/fd

共計6個文件描述符, 0,1,2分別是標(biāo)準(zhǔn)輸入,標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯誤輸出. 3,4,5描述符表示打開的3個jar.
總結(jié)一下,此時的JVM里面,共計20個線程,進程打開了6個文件描述符.
面試題: 如何知道JVM中的線程個數(shù),有哪些方法?
接下來,我們在/tmp目錄下創(chuàng)建一個.attach_pid6617文件,如下

接下來,我們使用kill命令向進程發(fā)送退出信號.

說明: 信號機制是進程間通信的一種方式
再觀察下線程的信息


多了一個6666的線程.
再看下進程6617打開的文件描述符

會發(fā)現(xiàn)多了一個文件描述6,而且還是個socket文件描述符.
總結(jié)一下,使用kill命令向JVM進程發(fā)送一個退出信號, 結(jié)果JVM多了1個線程,還多了1個sokcet文件描述符.
進程間通信的方式有很多,其中信號就是其中一種方式. 關(guān)于進程間的通信可以閱讀它[ https://www.yuque.com/infuq/language/rvdvcu ] . 向JVM發(fā)送一個信號之后,那么JVM必然有一個線程來處理信號,而這個線程就是Signal Dispatcher線程.
我相信,讀者朋友,通過jstack命令查看線程棧的時候,一定能看到這個線程.
Signal Dispatcher線程在JVM啟動的時候就創(chuàng)建了. 關(guān)于JVM的啟動,我們先在這里簡單說一下.
在jdk/src/share/bin/main.c文件中,有個main方法,它是一切的源頭,JVM就是從這里開始它的人生之旅的,經(jīng)過一路小跑,會創(chuàng)建main線程,也會創(chuàng)建JVM. 還會創(chuàng)建Signal Dispatcher線程,Signal Dispatcher線程會阻塞等待接收外部的信號. 比如上文中,我們使用kill向指定的進程6617發(fā)送的3號退出信號,就是由進程6617中的Signal Dispatcher線程來處理的. Signal Dispatcher線程在收到并處理3號退出信號的時候,它會創(chuàng)建Attach Listener線程,也會創(chuàng)建一個socket文件描述符,這個socket文件描述符就是上文中看到的那個6號文件描述符,那么這個socket文件描述符能干啥用呢?
除了信號可以用于進程間通信, Unix Domain Socket也可以用于進程間通信. 這種socket有別于網(wǎng)絡(luò)socket. Unix Domain Socket僅用于本地進程間通信, 而網(wǎng)絡(luò)socket用于網(wǎng)絡(luò)間的進程間通信. 而通過Unix Domain Socket創(chuàng)建出來的6號文件描述符,它就是由Attach Listener這個線程來使用的. 這個Attach Listener線程作為服務(wù)端,監(jiān)聽客戶端的請求. 比如像jstack命令,阿里的Arthas(阿爾薩斯)等工具,它們底層都是通過這個socket文件描述符連接到目標(biāo)JVM,從而實現(xiàn)通信.

我們通過JDK自帶的bin目錄下的工具jvisualvm,通過圖形化的方式,再次查看下進程6617中的線程.

看看你公司的服務(wù)器是否有這兩個線程呢?
接下來我們通過3種方式獲取進程6617的線程棧信息.
面試題: 如何得到一個進程的線程棧信息?
第一種方式就是通過jstack命令,或者JDK體系的其他命令.

第二種方式,通過Java代碼的方式
import com.sun.tools.attach.VirtualMachine;import sun.tools.attach.HotSpotVirtualMachine;import java.io.InputStream;public class Attach {public static void main(String[] args)throws Exception {// attach底層就是發(fā)送了一個kill -3 6617的命令給目標(biāo)JVMVirtualMachine virtualMachine = VirtualMachine.attach("6617");HotSpotVirtualMachine hotSpotVirtualMachine = (HotSpotVirtualMachine)virtualMachine;// 發(fā)送threaddump命令給目標(biāo)JVMInputStream inputStream = hotSpotVirtualMachine.remoteDataDump(new String[]{});byte[] buff = new byte[256];int len;do {// 接收目標(biāo)JVM返回的數(shù)據(jù)len = inputStream.read(buff);if (len > 0) {String respone = new String(buff, 0, len, "UTF-8");System.out.print(respone);}} while(len > 0);inputStream.close();virtualMachine.detach();}}
編譯并運行這個Java程序,依然可以得到進程6617的線程棧信息

第三種方式,通過C語言的方式, 之所以通過C語言的方式,旨在說明一點,不管我們使用的是jstack命令,還是上面的Java程序,或者阿里開源的Arthas(阿爾薩斯)工具,在它們的底層,都是通過同一種方式與目標(biāo)JVM進行通信的, 而通過C語言,能更好的把它展現(xiàn)給我們看.
個人理解: 如果真想把JVM或者JDK學(xué)透了,C語言是要熟悉的. JVM的底層都是C語言,包括與操作系統(tǒng)的一些交互,都是C語言. 包括進程間的通信等, 如果不懂C語言,不懂一些操作系統(tǒng)的知識,那么很難學(xué)透JVM或者JDK. 之所以要學(xué)習(xí)JVM等底層知識, 個人理解,主要是讓我們的知識體系健全,不至于知識碎片化.
代碼如下
// threaddump.cconst char *filename = "/tmp/.java_pid6617";int main(int argc, char **argv){struct sockaddr_un un;int fd;char buffer[BUFFER_SIZE];char *cmd = "1\0threaddump\0\0\0\0"; // 長度16un.sun_family = AF_UNIX;strcpy(un.sun_path, filename);fd = socket(PF_UNIX, SOCK_STREAM, 0);connect(fd, (struct sockaddr *) &un, sizeof(un));// 方式一send(fd, cmd, 16, 0);recv(fd, buffer, BUFFER_SIZE, 0);// 方式二//write(fd, cmd, 16);//read(fd, buffer, BUFFER_SIZE);printf("\n%s\n", buffer);close(fd);return 0;}
編譯

運行

上面我們可以看到,線程棧信息正常打印出來了. 那么它是如何做到的呢?
首先,在代碼中定義了一個 const char *filename = "/tmp/.java_pid6617"; 文件名, 我們看下這個文件.

6617就是進程ID. 當(dāng)我們通過kill命令向JVM發(fā)送3號退出信號的時候, Signal Dispatcher線程就會把Attach Listener線程創(chuàng)建出來, Attach Listener線程就會根據(jù)進程ID創(chuàng)建一個/tmp/.java_pid<PID>的文件. 如果是網(wǎng)絡(luò)socket通信,是基于IP和端口,而如果是Unix Domain Socket通信,就是基于文件的,而此時創(chuàng)建了一個/tmp/.java_pid<PID>的文件, Attach Listener線程就會創(chuàng)建一個服務(wù)端的socket, 那么客戶端就可以根據(jù)這個/tmp/.java_pid<PID>的文件創(chuàng)建一個客戶端,然后與服務(wù)端進行通信了. 那么如何創(chuàng)建客戶端的socket呢?
在我們的C語言代碼里
// 創(chuàng)建Unix Domain Socket用于本機進程間通信fd = socket(PF_UNIX, SOCK_STREAM, 0);// 連接服務(wù)器. 服務(wù)器也是通過Unix Domain Socket創(chuàng)建的.connect(fd, (struct sockaddr *) &un, sizeof(un));
通過以上兩句,創(chuàng)建了客戶端的socket, 并與服務(wù)端(也就是目標(biāo)JVM)建立了連接, 然后就是發(fā)送命令了.
代碼中我們發(fā)送了一個threaddump的命令,如下
char *cmd = "1\0threaddump\0\0\0\0"; // 長度16一切皆協(xié)議, 客戶端和服務(wù)端約定好了, 服務(wù)端接收什么樣子的命令格式才表示需要dump線程棧, 于是乎,客戶端就構(gòu)造這樣的命令, 然后把它發(fā)送給目標(biāo)JVM. 目標(biāo)JVM的Attach Listener線程收到命令之后,進行處理,然后把處理結(jié)果返回給客戶端, 于是乎客戶端就拿到了目標(biāo)JVM的線程棧.

本篇啰嗦這么多,主要就是在表達,如何與目標(biāo)JVM進行通信,以及涉及的一些線程和知識點.
