VisualVM分析與HelloWorld、springBoot專案
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邏輯)。
總結
平時看起來很熟悉的程式碼,HelloWorld和SpringBoot初始化的專案。沒想到背地裡有那麼多執行緒來支撐。裝了VisualVM外掛並不是讓你蹭的就變強,但是可以給你提供一些進步的思路,引導你去思考去進步。下面還會繼續帶著分析更復雜的專案,不知道會不會有更多常見又未知的知識點等待我們去發現~
歡迎訪問我的個人部落格