1. 程式人生 > >VisualVM分析與HelloWorld、springBoot專案

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沒想到還有這種知識盲區:

  1. 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監測。

  2. Attach Listener

    Attach Listener執行緒是負責接收到外部的命令,對該命令進行執行並把結果返回給傳送者。通常我們會用一些命令去要求jvm給我們一些反饋資訊,如:java -version、jmap、jstack等等。如果該執行緒在jvm啟動的時候沒有初始化,那麼,則會在使用者第一次執行jvm命令時,得到啟動。

  3. main

    main執行緒,就是我們程式碼所寫得程式碼對應執行緒

  4. Monitor Ctr-Break

    這應該是 IDEA 通過反射的方式,伴隨你的程式一起啟動的對你程式的監控執行緒。這也是一個預設全域性執行緒

  5. Signal Dispatcher

    前面提到的Attach Listener執行緒職責是接收外部jvm命令,當命令接收成功後,就會交給signal dispather執行緒分發到各個不同的模組處理,並且返回處理結果。signal dispather執行緒是在第一次接收外部jvm命令時,才進行初始化工作。

  6. Finalizer

    這個執行緒是在main執行緒之後建立的,其優先順序為10,主要用於在垃圾收集前,呼叫物件的finalize()方法;關於Finalizer執行緒的幾點:

    1. 只有當開始一輪垃圾收集時,才會開始呼叫finalize()方法;因此並不是所有物件的finalize()方法都會被執行;

    2. 該執行緒是守護執行緒,因此如果虛擬機器中沒有其他非守護執行緒的執行緒,不管該執行緒有沒有執行完finalize()方法,JVM也會退出;
    3. JVM在垃圾收集時會將失去引用的物件包裝成Finalizer物件(Reference的實現),並放入ReferenceQueue,由Finalizer執行緒來處理;最後將該Finalizer物件的引用置為null,由垃圾收集器來回收;
    4. JVM為什麼要單獨用一個執行緒來執行finalize()方法呢?如果JVM的垃圾收集執行緒自己來做,很有可能由於在finalize()方法中誤操作導致GC執行緒停止或不可控,這對GC執行緒來說是一種災難,所以單獨建立了一個守護執行緒。

  7. 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)開了些執行緒。著重講一下沒見過的執行緒。

  1. DestroyJavaVM

    所有POJO應用程式都通過呼叫該main方法開始。正常情況下,main完成後,將告知JVM的DestroyJavaVM`執行緒來關閉JVM,該執行緒等待所有非守護程序執行緒完成後再進行工作。這是為了確保建立的所有非守護程式執行緒都可以在JVM拆除之前執行完畢。

    但是,帶有GUI的應用程式通常以多個執行緒執行。用於監視系統事件,例如鍵盤或滑鼠事件。JVM仍然會建立DestroyJavaVM執行緒,且需要等待所有建立的執行緒完成,然後再拆除VM,然而應用並不會停止,所以DestoryJavaVM執行緒就會一直處於等待,直到應用執行完成。

    任何建立執行緒並僅依賴其功能的應用程式都會有一個DestroyJavaVM執行緒,等待應用程式完成並關閉JVM。由於它等待所有其他執行緒執行完畢(join),因此它不會消耗任何資源。

  2. 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-coretomcat-embed-eltomcat-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下面的幾個元件

    1. 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;
          }
      }
    2. 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);
                  }
              }
          }
    3. 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 {
                          ...
      
                      }
      
                  }
              }
          }
    4. NioSelectorPool:NioEndPoint物件維護了一個NioSelectorPool物件,這個NioSelectorPool中又維護了一個BlockPoller執行緒(基於Selector進行NIO邏輯)。

總結

平時看起來很熟悉的程式碼,HelloWorld和SpringBoot初始化的專案。沒想到背地裡有那麼多執行緒來支撐。裝了VisualVM外掛並不是讓你蹭的就變強,但是可以給你提供一些進步的思路,引導你去思考去進步。下面還會繼續帶著分析更復雜的專案,不知道會不會有更多常見又未知的知識點等待我們去發現~

歡迎訪問我的個人部落格