VisualVM分析與HelloWorld、springBoot項目

VisualVM分析與HelloWorld、springBoot項目

自從1995年第一個JDK版本JDKBeta發(fā)布,至今已經(jīng)快25年,這些年來Java的框架日新月異,從最開始的Servlet階段,到SSH,SSI,SSM,springboot等,還有一些其他方向的框架微服務(wù)SpringCloud、響應(yīng)式編程Spring Reactor。零零總總 的框架,我們都需要去熟悉,那么怎么去快速熟悉呢,我覺得可以看源碼,可以看博客,也可以根據(jù)內(nèi)存分配去完善理解。

那么問題來了,一個Java項目在咱們平時啟動項目的時候,究竟發(fā)生了什么,創(chuàng)建幾個簡單的項目,用VisualVM來分析一下~

Main

簡單的項目,應(yīng)該沒有比HelloWorld更簡單的了吧,按照老規(guī)矩,咱們就從HelloWorld開始分析!那么簡單的項目大家都能閉著眼睛敲出來,是不是沒分析的必要啊,別著急,寫好HelloWorld咱們開始分析:

System.out.println("HelloWorld start");
// 這里讓線程睡一會,方便分析
Thread.sleep(100000);
System.out.println("HelloWorld end");

運行main方法,打開VisualVM,發(fā)現(xiàn)事情并不簡單哦,這個簡單的項目有十六個線程維護,其中守護線程有十五個。

其中幾大線程的內(nèi)存分配情況如下:

這些線程都是干什么用的?寫了那么多年HelloWorld沒想到還有這種知識盲區(qū):

  1. RMI TCP Connection(2)-10.128.227.33

    10.128.227.33是我本地的ip地址。正確而愚蠢的原因是因為開了VisualVM(JMX客戶端),JVM需要把他的數(shù)據(jù)傳遞給這個客戶端,就是使用的TCP傳遞,相同作用的線程還有JMX server connection timeout:MAIN方法跑完了,JMX連接的心跳斷開。RMI TCP Connection(idle):用來在RMI連接池中創(chuàng)建線程。*** Profiler Agent Communication Thread:Profiler代理通信線程。RMI TCP Accept-0:進行JMX進行JMX監(jiān)測。

  2. Attach Listener

    Attach Listener線程是負責接收到外部的命令,對該命令進行執(zhí)行并把結(jié)果返回給發(fā)送者。通常我們會用一些命令去要求jvm給我們一些反饋信息,如:java -version、jmap、jstack等等。如果該線程在jvm啟動的時候沒有初始化,那么,則會在用戶第一次執(zhí)行jvm命令時,得到啟動。

  3. main

    main線程,就是我們代碼所寫得代碼對應(yīng)線程

  4. Monitor Ctr-Break

    這應(yīng)該是 IDEA 通過反射的方式,伴隨你的程序一起啟動的對你程序的監(jiān)控線程。這也是一個默認全局線程

  5. Signal Dispatcher

    前面提到的Attach Listener線程職責是接收外部jvm命令,當命令接收成功后,就會交給signal dispather線程分發(fā)到各個不同的模塊處理,并且返回處理結(jié)果。signal dispather線程是在第一次接收外部jvm命令時,才進行初始化工作。

  6. Finalizer

    這個線程是在main線程之后創(chuàng)建的,其優(yōu)先級為10,主要用于在垃圾收集前,調(diào)用對象的finalize()方法;關(guān)于Finalizer線程的幾點:

    1. 只有當開始一輪垃圾收集時,才會開始調(diào)用finalize()方法;因此并不是所有對象的finalize()方法都會被執(zhí)行;

    2. 該線程是守護線程,因此如果虛擬機中沒有其他非守護線程的線程,不管該線程有沒有執(zhí)行完finalize()方法,JVM也會退出;

    3. JVM在垃圾收集時會將失去引用的對象包裝成Finalizer對象(Reference的實現(xiàn)),并放入ReferenceQueue,由Finalizer線程來處理;最后將該Finalizer對象的引用置為null,由垃圾收集器來回收;

    4. JVM為什么要單獨用一個線程來執(zhí)行finalize()方法呢?如果JVM的垃圾收集線程自己來做,很有可能由于在finalize()方法中誤操作導(dǎo)致GC線程停止或不可控,這對GC線程來說是一種災(zāi)難,所以單獨創(chuàng)建了一個守護線程。

  7. Reference Handler

    VM在創(chuàng)建main線程后就創(chuàng)建Reference Handler線程,其優(yōu)先級最高,為10,它主要用于處理引用對象本身(軟引用、弱引用、虛引用)的垃圾回收問題。

經(jīng)過上面的分析可以看出來main本身程序的線程有:main線程,Reference Handler線程,F(xiàn)inalizer線程,Attach Listener線程,Signal Dispatcher線程。

java代碼想要實現(xiàn)也很簡單,如下即可:

// 獲取java線程管理器MXBean,dumpAllThreads參數(shù):
//                                  lockedMonitors參數(shù)表示是否獲取同步的monitor信息
//                                  lockedSynchronizers表示是否獲取同步的synchronizer
ThreadInfo[] threadInfos = ManagementFactory.getThreadMXBean().dumpAllThreads(true, false);
for (ThreadInfo threadInfo : threadInfos) {
    System.out.println(threadInfo.getThreadId() + " : " + threadInfo.getThreadName());
}

得到的打印結(jié)果為:

也就是說,寫了那么多年的HelloWorld居然有五個線程來支撐,而我卻一直被蒙在鼓里??誰能隨時去關(guān)注項目有多少個線程啊,VIsualVM可以= =,雖然我覺得他一直起線程進行通信很蠢,但是項目結(jié)構(gòu)大了就有必要了。

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個。

不同的顏色對應(yīng)著不同的狀態(tài),詳情看右下角。這些線程很多都是熟悉的,Main方法分析過的,通過VisualVM工具進行JMX監(jiān)視(RMI TCP...)開了些線程;IDEA(Monitor Ctrl-Break)開了些線程;垃圾回收(Finalizer,Reference Handler)開了些線程。著重講一下沒見過的線程。

  1. DestroyJavaVM

    所有POJO應(yīng)用程序都通過調(diào)用該main方法開始。正常情況下,main完成后,將告知JVM的DestroyJavaVM`線程來關(guān)閉JVM,該線程等待所有非守護進程線程完成后再進行工作。這是為了確保創(chuàng)建的所有非守護程序線程都可以在JVM拆除之前運行完畢。

    但是,帶有GUI的應(yīng)用程序通常以多個線程運行。用于監(jiān)視系統(tǒng)事件,例如鍵盤或鼠標事件。JVM仍然會創(chuàng)建DestroyJavaVM線程,且需要等待所有創(chuàng)建的線程完成,然后再拆除VM,然而應(yīng)用并不會停止,所以DestoryJavaVM線程就會一直處于等待,直到應(yīng)用運行完成。

    任何創(chuàng)建線程并僅依賴其功能的應(yīng)用程序都會有一個DestroyJavaVM線程,等待應(yīng)用程序完成并關(guān)閉JVM。由于它等待所有其他線程執(zhí)行完畢(join),因此它不會消耗任何資源。

  2. Http-nio-8080-Acceptor、Http-nio-8080-ClientPoller、Http-nio-8080-BlockPoller、http-nio-8080-exec-1...10

    這些線程都有個特點,http-nio-8080開頭。8080就是這個應(yīng)用的端口,顯然這是給容器使用的。項目引入的是spring-boot-starter-web依賴,也就是默認使用springBoot的內(nèi)置tomcat容器啟動,我們的maven下面也會有這樣的幾個包:tomcat-embed-core、tomcat-embed-el、tomcat-embed-websocket,我們所看到的線程都是由這幾個包產(chǎn)生的。那么這些線程是干什么用的?

    解決這個問題之前,先看一下tomcat的總體架構(gòu):

    Tomcat由Connector和Container兩個核心組件構(gòu)成,Connector組件負責網(wǎng)絡(luò)請求接入,目前支持BIO、NIO、APR三種模式,Tomcat5之后就支持了NIO,看我們的線程名也就是用的NIO;Container組件負責管理servlet容器。service服務(wù)將Container和Connector又包裝了一層,使得外部可以直接獲取。多個service服務(wù)運行在tomcat的Server服務(wù)器上,Server上有所有的service實例,并實現(xiàn)了LifeCycle接口來控制所有service的生命周期。

    而NIO對應(yīng)線程主要是實現(xiàn)在Connector組件中,他負責接受瀏覽器發(fā)過來的tcp請求,創(chuàng)建一個Reuqest和Response對象用來請求和響應(yīng),然后產(chǎn)生一個線程,將Request和Response分發(fā)給他們對應(yīng)處理的線程。

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

    ?
    1. Acceptor:接受socket線程,接受的方法比較傳統(tǒng):serverSocket.accept(),得到SocketChannel對象并封裝到NioChannel對象中。然后NioChannel對象封裝在PollerEvent對象中,并放到events queue中。使用隊列(生產(chǎn)者-消費者)和Poller組件交互,Acceptor是生產(chǎn)者,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 {
                            // 這句會調(diào)用NioEndPoint類,底層是serverSock.accept()
                              socket = this.endpoint.serverSocketAccept();
                          } catch (Exception var6) {
                              ...
                          }
                                            ...
                      }
                  } catch (Throwable var7) {
                      ...
                  }
              }
      
              this.state = Acceptor.AcceptorState.ENDED;
          }
      }
      
    2. Poller:NIO選擇器Selector用于檢查一個或多個NIO Channel(通道)的狀態(tài)是否可讀、可寫。如此可以實現(xiàn)單線程管理多個channels也就是可以管理多個網(wǎng)絡(luò)線程。Poller是NIO實現(xiàn)的主要線程,首先從events queue隊列中消費得到PollerEvent對象,再將此對象中的Channel以O(shè)P_READ事件注冊到主Selector中,Selector執(zhí)行select操作,遍歷出可以讀數(shù)據(jù)的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();
                            // 然后調(diào)用processKey方法,將socket傳給worker線程進行處理
                              this.processKey(sk, socketWrapper);
                          }
                      }
      
                      this.timeout(this.keyCount, hasEvents);
                  }
              }
          }
      
    3. Worker:Worker線程從Poller傳過來的socket后,將socket封裝在SocketProcessor對象中,然后從Http11ConnectionHandler獲取Http11NioProcessor對象,從Http11NioProcessor中調(diào)用CoyoteAdapter的邏輯(這就出了Http11NioProtocol組件,可以看上上圖)。在Worker線程中,會完成從socket中讀取http request,解析成HttpervletRequest對象,分派到相應(yīng)的servlet并完成邏輯,然而將response通過socket發(fā)回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邏輯)。

總結(jié)

平時看起來很熟悉的代碼,HelloWorld和SpringBoot初始化的項目。沒想到背地里有那么多線程來支撐。裝了VisualVM插件并不是讓你蹭的就變強,但是可以給你提供一些進步的思路,引導(dǎo)你去思考去進步。下面還會繼續(xù)帶著分析更復(fù)雜的項目,不知道會不會有更多常見又未知的知識點等待我們?nèi)グl(fā)現(xiàn)~

歡迎訪問我的個人博客

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 注:不能免俗,本文大量借鑒綜合其他文章及圖片,結(jié)尾有備注,在此先行感謝,僅作為自己學(xué)習(xí)總結(jié)用。那為何還要搞一篇這樣...
    01010100閱讀 2,169評論 0 1
  • 前言 在使用tomcat時,經(jīng)常會遇到連接數(shù)、線程數(shù)之類的配置問題,要真正理解這些概念,必須先了解Tomcat的連...
    Java大生閱讀 1,289評論 0 2
  • 前言 在使用tomcat時,經(jīng)常會遇到連接數(shù)、線程數(shù)之類的配置問題,要真正理解這些概念,必須先了解Tomcat的連...
    程序員BUG閱讀 528評論 0 0
  • 前言 在使用tomcat時,經(jīng)常會遇到連接數(shù)、線程數(shù)之類的配置問題,要真正理解這些概念,必須先了解Tomcat的連...
    shaolin79閱讀 820評論 0 0
  • 白駒過隙 歲月如歌 往事不堪回首 有的 猶如在昨 有的 在記憶里永遠消失 有的 卻成為生命中的永恒 你—— ...
    人生若只如初見_5b1a閱讀 409評論 6 9

友情鏈接更多精彩內(nèi)容