初步探究java中程序退出、GC垃圾回收時,socket tcp連接的行為
今天在項目開發中需要用到socket tcp連接相關(作為tcp客戶端),在思考中發覺需要理清socket主動、被動關閉時發生了什么,所以做了一番實驗,驗證socket tcp連接在調用close、被GC回收、程序運行完畢退出、程序進程被殺掉時,tcp會產生什么行為。得出了一些結論,記錄於此同時分享給大家。
先寫出得到的結論:
- java程序運行完畢退出和被殺進程時,socket tcp連接會被關閉。而且是通過發送RST方式關閉tcp,不是四次揮手方式關閉tcp,不會進入TIME_WAIT狀態。(一般在關閉異常連接時,使用發出RST復位標志的方式)
- 在socket對象被GC回收時,socket的close()方法會被調用,此時將使用四次揮手方式關閉主動tcp連接,隨后進入一段時間的TIME_WAIT狀態。主動調用socket的close()方法效果相同。
- 在使用new Socket(host,port)創建socket對象后,便會建立起tcp連接。
以上結論是個人測試得到的,如果有出入歡迎指正。
PS:下方是new Socket(host,port)構造方法的代碼,結合上面結論第3條,可見在構造方法中第40行進行了一些業務邏輯。聯想到前段時間廣泛傳播的《阿里巴巴JAVA開發手冊.pdf》文檔,其中有這樣一條:
11.【強制】構造方法里面禁止加入任何業務邏輯,如果有初始化邏輯,請放在 init 方法中。
可見這個規則也不是處處適用的(至少此處jdk中Socket的構造方法就與這條規則相悖),在使用時考慮到團隊協作、符合習慣等時考慮遵守這條規則。
1 /** 2 * Creates a stream socket and connects it to the specified port 3 * number on the named host. 4 * <p> 5 * If the specified host is {@code null} it is the equivalent of 6 * specifying the address as 7 * {@link java.net.InetAddress#getByName InetAddress.getByName}{@code (null)}. 8 * In other words, it is equivalent to specifying an address of the 9 * loopback interface. </p> 10 * <p> 11 * If the application has specified a server socket factory, that 12 * factory's {@code createSocketImpl} method is called to create 13 * the actual socket implementation. Otherwise a "plain" socket is created. 14 * <p> 15 * If there is a security manager, its 16 * {@code checkConnect} method is called 17 * with the host address and {@code port} 18 * as its arguments. This could result in a SecurityException. 19 * 20 * @param host the host name, or {@code null} for the loopback address. 21 * @param port the port number. 22 * 23 * @exception UnknownHostException if the IP address of 24 * the host could not be determined. 25 * 26 * @exception IOException if an I/O error occurs when creating the socket. 27 * @exception SecurityException if a security manager exists and its 28 * {@code checkConnect} method doesn't allow the operation. 29 * @exception IllegalArgumentException if the port parameter is outside 30 * the specified range of valid port values, which is between 31 * 0 and 65535, inclusive. 32 * @see java.net.Socket#setSocketImplFactory(java.net.SocketImplFactory) 33 * @see java.net.SocketImpl 34 * @see java.net.SocketImplFactory#createSocketImpl() 35 * @see SecurityManager#checkConnect 36 */ 37 public Socket(String host, int port) 38 throws UnknownHostException, IOException 39 { 40 this(host != null ? new InetSocketAddress(host, port) : 41 new InetSocketAddress(InetAddress.getByName(null), port), 42 (SocketAddress) null, true); 43 }
驗證時,使用的工具:
- windows cmd命令行,“netstat -ano|findstr 9911”命令(9911是我測試時連接的對方服務器端口),用以查看tcp連接情況,包括tcp是否存在,以及處於TIME_WAIT、ESTABLISHED或者別的狀態;
- wireshark,抓包詳細查看tcp中傳輸的數據包;
- JProfiler,java性能分析工具,我在使用時和IDEA做了集成,這里用它的主要目的是手動觸發GC,以及驗證是否進行了GC。
具體驗證過程:
使用以下測試代碼
1 public static void main(String[] args) throws Exception { 2 main2(); 3 System.out.println("調用結束"); 4 Thread.sleep(15000); 5 System.out.println("退出程序"); 6 7 } 8 9 public static void main2() throws Exception { 10 11 System.out.println("啟動"); 12 Thread.sleep(5000); 13 System.out.println("創建socket對象"); 14 Socket socket = new Socket("123.56.113.123", 9911); 15 System.out.println("socket.isConnected() = " + socket.isConnected()); 16 System.out.println("socket.isClosed() = " + socket.isClosed()); 17 Thread.sleep(5000); 18 System.out.println("獲取輸出流"); 19 OutputStream outputStream = socket.getOutputStream(); 20 System.out.println("socket.isConnected() = " + socket.isConnected()); 21 System.out.println("socket.isClosed() = " + socket.isClosed()); 22 System.out.println("即將退出"); 23 // socket.shutdownOutput(); 24 Thread.sleep(5000); 25 System.out.println("退出"); 26 27 }
為了方便在執行不同的操作之間,進行手動的tcp連接情況查詢,代碼中加了一些sleep()。
測試方案1,程序正常執行,不手動GC,不殺進程:
- 在14行之前,沒有創建tcp連接;
- 14行創建了Socket對象之后,tcp連接就已經建立了,此時socket.isConnected()為true,socket.isClosed()為false;
- 19行執行時,沒有發現tcp中有任何數據傳輸,說明獲取輸出流是不需要網絡層的動作的;
- 19行之后,程序退出之前,tcp一直是ESTABLISHED狀態(前提是這期間沒有發生過GC);
- 程序退出后,tcp連接馬上消失(沒有進入TIME_WAIT狀態),通過抓包查看是主動發出了RST(發出RST reset復位標志,一般是關閉異常tcp連接的方法)。
以下wireshark抓包的截圖中序號為1-4的包,是上述過程產生的。

測試方案2,tcp連接建立之后,程序退出前,殺掉進程:
和方案1中的效果相同,上圖中5-8號包是方案2產生的。殺掉進程時,會主動發出RST關閉tcp連接。
測試方案3,tcp連接建立后,GC回收socket對象:
觸發GC的方法,是使用JProfiler的Run GC按鈕,觸發GC,如下圖,內存變化說明確實進行了GC。

具體測試的設計,socket對象是main2方法中的局部變量,在main2執行結束后便可以被回收。
在測試中,第2、3行代碼執行完之后,我點擊Run GC按鈕,預計socket對象會被回收。
此時用“netstat -ano|findstr 9911”命令查看,如下圖,tcp已經由ESTABLISHED狀態變為TIME_WAIT狀態。
直到程序結束退出,仍然是TIME_WAIT狀態。(正常情況下,TIME_WAIT狀態會持續MSL時間的2倍,即報文最大生存時間的2倍,Windows下默認為4分鍾)

實際上,在GC時會調用socket的close()方法,導致主動關閉tcp,進入TIME_WAIT狀態。參考
java - If the jvm gc an unclosed socket instance what would happen to the underlying tcp connection? - Stack Overflow
https://stackoverflow.com/questions/25543149/if-the-jvm-gc-an-unclosed-socket-instance-what-would-happen-to-the-underlying-tc
在Socket的成員變量中有SocketImpl類型的變量impl,impl實例化的實現類都是直接或間接繼承java.net.AbstractPlainSocketImpl的,AbstractPlainSocketImpl中有如下方法:
1 /** 2 * Cleans up if the user forgets to close it. 3 */ 4 protected void finalize() throws IOException { 5 close(); 6 }
在GC時,impl的finalize方法會被調用,這時候就相當於調用了socket.close(),所以tcp被正常地主動關閉(socket.close()內的代碼也是調用impl.close(),只是額外加了線程同步控制代碼)。
測試方案4,主動調用socket.close()關閉tcp連接:
即將第23行代碼取消注釋,替換為socket.close(),或socket.shutdownOutput() ,測試結果均和方案3的一致,close或shutdownOutput之后tcp關閉,端口進入TIME_WAIT狀態。
以上是本次測試驗證的過程,對java程序退出、GC回收socket對象、被殺進程、主動close時對tcp影響的初步探究,如有錯誤、疏漏還請斧正。
