VisualVM分析與HelloWorld、springBoot項目
自從1995年第一個JDK版本JDKBeta發布,至今已經快25年,這些年來Java的框架日新月異,從最開始的Servlet階段,到SSH,SSI,SSM,springboot等,還有一些其他方向的框架微服務SpringCloud、響應式編程Spring Reactor。零零總總 的框架,我們都需要去熟悉,那么怎么去快速熟悉呢,我覺得可以看源碼,可以看博客,也可以根據內存分配去完善理解。
那么問題來了,一個Java項目在咱們平時啟動項目的時候,究竟發生了什么,創建幾個簡單的項目,用VisualVM來分析一下~
Main
簡單的項目,應該沒有比HelloWorld更簡單的了吧,按照老規矩,咱們就從HelloWorld開始分析!那么簡單的項目大家都能閉着眼睛敲出來,是不是沒分析的必要啊,別着急,寫好HelloWorld咱們開始分析:
System.out.println("HelloWorld start");
// 這里讓線程睡一會,方便分析
Thread.sleep(100000);
System.out.println("HelloWorld end");
運行main方法,打開VisualVM,發現事情並不簡單哦,這個簡單的項目有十六個線程維護,其中守護線程有十五個。

其中幾大線程的內存分配情況如下:

這些線程都是干什么用的?寫了那么多年HelloWorld沒想到還有這種知識盲區:
-
RMI TCP Connection(2)-10.128.227.33
10.128.227.33是我本地的ip地址。正確而愚蠢的原因是因為開了VisualVM(JMX客戶端),JVM需要把他的數據傳遞給這個客戶端,就是使用的TCP傳遞,相同作用的線程還有JMX server connection timeout:MAIN方法跑完了,JMX連接的心跳斷開。RMI TCP Connection(idle):用來在RMI連接池中創建線程。*** Profiler Agent Communication Thread:Profiler代理通信線程。RMI TCP Accept-0:進行JMX進行JMX監測。
-
Attach Listener
Attach Listener線程是負責接收到外部的命令,對該命令進行執行並把結果返回給發送者。通常我們會用一些命令去要求jvm給我們一些反饋信息,如:java -version、jmap、jstack等等。如果該線程在jvm啟動的時候沒有初始化,那么,則會在用戶第一次執行jvm命令時,得到啟動。
-
main
main線程,就是我們代碼所寫得代碼對應線程
-
Monitor Ctr-Break
這應該是 IDEA 通過反射的方式,伴隨你的程序一起啟動的對你程序的監控線程。這也是一個默認全局線程
-
Signal Dispatcher
前面提到的Attach Listener線程職責是接收外部jvm命令,當命令接收成功后,就會交給signal dispather線程分發到各個不同的模塊處理,並且返回處理結果。signal dispather線程是在第一次接收外部jvm命令時,才進行初始化工作。
-
Finalizer
這個線程是在main線程之后創建的,其優先級為10,主要用於在垃圾收集前,調用對象的finalize()方法;關於Finalizer線程的幾點:
-
只有當開始一輪垃圾收集時,才會開始調用finalize()方法;因此並不是所有對象的finalize()方法都會被執行;
-
該線程是守護線程,因此如果虛擬機中沒有其他非守護線程的線程,不管該線程有沒有執行完finalize()方法,JVM也會退出;
-
JVM在垃圾收集時會將失去引用的對象包裝成Finalizer對象(Reference的實現),並放入ReferenceQueue,由Finalizer線程來處理;最后將該Finalizer對象的引用置為null,由垃圾收集器來回收;
-
JVM為什么要單獨用一個線程來執行finalize()方法呢?如果JVM的垃圾收集線程自己來做,很有可能由於在finalize()方法中誤操作導致GC線程停止或不可控,這對GC線程來說是一種災難,所以單獨創建了一個守護線程。
-
-
Reference Handler
VM在創建main線程后就創建Reference Handler線程,其優先級最高,為10,它主要用於處理引用對象本身(軟引用、弱引用、虛引用)的垃圾回收問題。
經過上面的分析可以看出來main本身程序的線程有:main線程,Reference Handler線程,Finalizer線程,Attach Listener線程,Signal Dispatcher線程。
java代碼想要實現也很簡單,如下即可:
// 獲取java線程管理器MXBean,dumpAllThreads參數:
// lockedMonitors參數表示是否獲取同步的monitor信息
// lockedSynchronizers表示是否獲取同步的synchronizer
ThreadInfo[] threadInfos = ManagementFactory.getThreadMXBean().dumpAllThreads(true, false);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println(threadInfo.getThreadId() + " : " + threadInfo.getThreadName());
}
得到的打印結果為:

也就是說,寫了那么多年的HelloWorld居然有五個線程來支撐,而我卻一直被蒙在鼓里??誰能隨時去關注項目有多少個線程啊,VIsualVM可以= =,雖然我覺得他一直起線程進行通信很蠢,但是項目結構大了就有必要了。
Spring-Boot
那么一個啥都沒有的springBoot項目啟動了之后,會有哪些線程呢?先看看他的pom文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.visual.vm.performance</groupId>
<artifactId>mock</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mock</name>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
只引入了spring-boot-starter-web的依賴,其他的什么都沒有,啟動着試一下。共有27個線程,守護線程有23個。

不同的顏色對應着不同的狀態,詳情看右下角。這些線程很多都是熟悉的,Main方法分析過的,通過VisualVM工具進行JMX監視(RMI TCP...)開了些線程;IDEA(Monitor Ctrl-Break)開了些線程;垃圾回收(Finalizer,Reference Handler)開了些線程。着重講一下沒見過的線程。
-
DestroyJavaVM
所有POJO應用程序都通過調用該
main方法開始。正常情況下,main完成后,將告知JVM的DestroyJavaVM`線程來關閉JVM,該線程等待所有非守護進程線程完成后再進行工作。這是為了確保創建的所有非守護程序線程都可以在JVM拆除之前運行完畢。但是,帶有GUI的應用程序通常以多個線程運行。用於監視系統事件,例如鍵盤或鼠標事件。JVM仍然會創建
DestroyJavaVM線程,且需要等待所有創建的線程完成,然后再拆除VM,然而應用並不會停止,所以DestoryJavaVM線程就會一直處於等待,直到應用運行完成。任何創建線程並僅依賴其功能的應用程序都會有一個
DestroyJavaVM線程,等待應用程序完成並關閉JVM。由於它等待所有其他線程執行完畢(join),因此它不會消耗任何資源。 -
Http-nio-8080-Acceptor、Http-nio-8080-ClientPoller、Http-nio-8080-BlockPoller、http-nio-8080-exec-1...10
這些線程都有個特點,http-nio-8080開頭。8080就是這個應用的端口,顯然這是給容器使用的。項目引入的是spring-boot-starter-web依賴,也就是默認使用springBoot的內置tomcat容器啟動,我們的maven下面也會有這樣的幾個包:
tomcat-embed-core、tomcat-embed-el、tomcat-embed-websocket,我們所看到的線程都是由這幾個包產生的。那么這些線程是干什么用的?解決這個問題之前,先看一下tomcat的總體架構:

Tomcat由Connector和Container兩個核心組件構成,Connector組件負責網絡請求接入,目前支持BIO、NIO、APR三種模式,Tomcat5之后就支持了NIO,看我們的線程名也就是用的NIO;Container組件負責管理servlet容器。service服務將Container和Connector又包裝了一層,使得外部可以直接獲取。多個service服務運行在tomcat的Server服務器上,Server上有所有的service實例,並實現了LifeCycle接口來控制所有service的生命周期。
而NIO對應線程主要是實現在Connector組件中,他負責接受瀏覽器發過來的tcp請求,創建一個Reuqest和Response對象用來請求和響應,然后產生一個線程,將Request和Response分發給他們對應處理的線程。

終於看到了線程名中包含的Acceptor、Poller。他們都在Connector組件下的Http11NioProtocol下。着重介紹一下Http11NioProtocol下面的幾個組件

-
Acceptor:接受socket線程,接受的方法比較傳統:serverSocket.accept(),得到SocketChannel對象並封裝到NioChannel對象中。然后NioChannel對象封裝在PollerEvent對象中,並放到events queue中。使用隊列(生產者-消費者)和Poller組件交互,Acceptor是生產者,Poller是消費者,通過events queue通信。
package org.apache.tomcat.util.net; public class Acceptor<U> implements Runnable { ... public void run() { byte errorDelay = 0; while(this.endpoint.isRunning()) { .... try { this.endpoint.countUpOrAwaitConnection(); if (!this.endpoint.isPaused()) { Object socket = null; try { // 這句會調用NioEndPoint類,底層是serverSock.accept() socket = this.endpoint.serverSocketAccept(); } catch (Exception var6) { ... } ... } } catch (Throwable var7) { ... } } this.state = Acceptor.AcceptorState.ENDED; } } -
Poller:NIO選擇器Selector用於檢查一個或多個NIO Channel(通道)的狀態是否可讀、可寫。如此可以實現單線程管理多個channels也就是可以管理多個網絡線程。Poller是NIO實現的主要線程,首先從events queue隊列中消費得到PollerEvent對象,再將此對象中的Channel以OP_READ事件注冊到主Selector中,Selector執行select操作,遍歷出可以讀數據的socket,並從Worker線程池中拿到可用的Workrer線程,將可用的socket傳遞給Worker線程。
package org.apache.tomcat.util.net; public class Poller implements Runnable { ... public void run() { while(true) { boolean hasEvents = false; label59: { try { if (!this.close) { hasEvents = this.events(); if (this.wakeupCounter.getAndSet(-1L) > 0L) { this.keyCount = this.selector.selectNow(); } else { // selector.select方法,接受acceptor的socket this.keyCount = this.selector.select(NioEndpoint.this.selectorTimeout); } this.wakeupCounter.set(0L); } if (!this.close) { break label59; } this.events(); this.timeout(0, false); try { this.selector.close(); } catch (IOException var5) { NioEndpoint.log.error(AbstractEndpoint.sm.getString("endpoint.nio.selectorCloseFail"), var5); } } catch (Throwable var6) { ExceptionUtils.handleThrowable(var6); NioEndpoint.log.error(AbstractEndpoint.sm.getString("endpoint.nio.selectorLoopError"), var6); continue; } NioEndpoint.this.getStopLatch().countDown(); return; } if (this.keyCount == 0) { hasEvents |= this.events(); } Iterator iterator = this.keyCount > 0 ? this.selector.selectedKeys().iterator() : null; while(iterator != null && iterator.hasNext()) { SelectionKey sk = (SelectionKey)iterator.next(); NioEndpoint.NioSocketWrapper socketWrapper = (NioEndpoint.NioSocketWrapper)sk.attachment(); if (socketWrapper == null) { iterator.remove(); } else { iterator.remove(); // 然后調用processKey方法,將socket傳給worker線程進行處理 this.processKey(sk, socketWrapper); } } this.timeout(this.keyCount, hasEvents); } } } -
Worker:Worker線程從Poller傳過來的socket后,將socket封裝在SocketProcessor對象中,然后從Http11ConnectionHandler獲取Http11NioProcessor對象,從Http11NioProcessor中調用CoyoteAdapter的邏輯(這就出了Http11NioProtocol組件,可以看上上圖)。在Worker線程中,會完成從socket中讀取http request,解析成HttpervletRequest對象,分派到相應的servlet並完成邏輯,然而將response通過socket發回client。
package org.apache.tomcat.util.net; protected class SocketProcessor extends SocketProcessorBase<NioChannel> { public SocketProcessor(SocketWrapperBase<NioChannel> socketWrapper, SocketEvent event) { super(socketWrapper, event); } protected void doRun() { // 這一句從Poller拿到socket,然后進行tomcat主線程處理流程 NioChannel socket = (NioChannel)this.socketWrapper.getSocket(); SelectionKey key = socket.getIOChannel().keyFor(socket.getSocketWrapper().getPoller().getSelector()); NioEndpoint.Poller poller = NioEndpoint.this.poller; if (poller == null) { this.socketWrapper.close(); } else { try { int handshake = -1; try { if (key != null) { if (socket.isHandshakeComplete()) { handshake = 0; } else if (this.event != SocketEvent.STOP && this.event != SocketEvent.DISCONNECT && this.event != SocketEvent.ERROR) { handshake = socket.handshake(key.isReadable(), key.isWritable()); this.event = SocketEvent.OPEN_READ; } else { handshake = -1; } } } catch (IOException var13) { handshake = -1; if (NioEndpoint.log.isDebugEnabled()) { NioEndpoint.log.debug("Error during SSL handshake", var13); } } catch (CancelledKeyException var14) { handshake = -1; } if (handshake == 0) { SocketState state = SocketState.OPEN; if (this.event == null) { state = NioEndpoint.this.getHandler().process(this.socketWrapper, SocketEvent.OPEN_READ); } else { state = NioEndpoint.this.getHandler().process(this.socketWrapper, this.event); } if (state == SocketState.CLOSED) { poller.cancelledKey(key, this.socketWrapper); } } else if (handshake == -1) { NioEndpoint.this.getHandler().process(this.socketWrapper, SocketEvent.CONNECT_FAIL); poller.cancelledKey(key, this.socketWrapper); } else if (handshake == 1) { this.socketWrapper.registerReadInterest(); } else if (handshake == 4) { this.socketWrapper.registerWriteInterest(); } } catch (CancelledKeyException var15) { ... } finally { ... } } } } -
NioSelectorPool:NioEndPoint對象維護了一個NioSelectorPool對象,這個NioSelectorPool中又維護了一個BlockPoller線程(基於Selector進行NIO邏輯)。
-
