Tomcat 高并發(fā)之道原理拆解與性能調(diào)優(yōu)
高并發(fā)拆解核心準備
這回,再次拆解,專注 Tomcat 高并發(fā)設(shè)計之道與性能調(diào)優(yōu),讓大家對整個架構(gòu)有更高層次的了解與感悟。其中設(shè)計的每個組件思路都是將 Java 面向?qū)ο?、面向接口、如何封裝變與不變,如何根據(jù)實際需求抽象不同組件分工合作,如何設(shè)計類實現(xiàn)單一職責(zé),怎么做到將相似功能高內(nèi)聚低耦合,設(shè)計模式運用到極致的學(xué)習(xí)借鑒。
這次主要涉及到的是 I/O 模型,以及線程池的基礎(chǔ)內(nèi)容。
一起來看 Tomcat 如何實現(xiàn)并發(fā)連接處理以及任務(wù)處理,性能的優(yōu)化是每一個組件都起到對應(yīng)的作用,如何使用最少的內(nèi)存,最快的速度執(zhí)行是我們的目標。
設(shè)計模式
?模板方法模式:抽象算法流程在抽象類中,封裝流程中的變化與不變點。將變化點延遲到子類實現(xiàn),達到代碼復(fù)用,開閉原則。
?觀察者模式:針對事件不同組件有不同響應(yīng)機制的需求場景,達到解耦靈活通知下游。
?責(zé)任鏈模式:將對象連接成一條鏈,將沿著這條鏈傳遞請求。在 Tomcat 中的 Valve 就是該設(shè)計模式的運用。
I/O 模型
Tomcat 實現(xiàn)高并發(fā)接收連接,必然涉及到 I/O 模型的運用,了解同步阻塞、異步阻塞、I/O 多路復(fù)用,異步非阻塞相關(guān)概念以及 Java NIO 包的運用很有必要。本文也會帶大家著重說明 I/O 是如何在 Tomcat 運用實現(xiàn)高并發(fā)連接。大家通過本文我相信對 I/O 模型也會有一個深刻認識。
Java 并發(fā)編程
實現(xiàn)高并發(fā),除了整體每個組件的優(yōu)雅設(shè)計、設(shè)計模式的合理、I/O 的運用,還需要線程模型,如何高效的并發(fā)編程技巧。在高并發(fā)過程中,不可避免的會出現(xiàn)多個線程對共享變量的訪問,需要加鎖實現(xiàn),如何高效的降低鎖沖突。因此作為程序員,要有意識的盡量避免鎖的使用,比如可以使用原子類 CAS 或者并發(fā)集合來代替。如果萬不得已需要用到鎖,也要盡量縮小鎖的范圍和鎖的強度。
對于并發(fā)相關(guān)的基礎(chǔ)知識,如果讀者感興趣后面也給大家安排上,目前也寫了部分并發(fā)專輯,大家可移步到歷史文章或者專輯翻閱,主要講解了并發(fā)實現(xiàn)的原理、什么是內(nèi)存可見性,JMM 內(nèi)存模模型、讀寫鎖等并發(fā)知識點。
Tomcat 總體架構(gòu)
再次回顧下 Tomcat 整體架構(gòu)設(shè)計,主要設(shè)計了?connector 連接器處理 TCP/IP 連接,container 容器作為 Servlet 容器,處理具體的業(yè)務(wù)請求。對外對內(nèi)分別抽象兩個組件實現(xiàn)拓展。
一個 Tomcat 實例默認會有一個 Service,而一個 Service 可以包含多個連接器。連接器主要有 ProtocalHandler 和 Adapter 兩個組件共同完成連接器核心功能。 ProtocolHandler?主要由?Acceptor?以及?SocketProcessor?構(gòu)成,實現(xiàn)了 TCP/IP 層 的 Socket 讀取并轉(zhuǎn)換成?TomcatRequest?和?TomcatResponse,最后根據(jù) http 或者 ajp 協(xié)議獲取合適的?Processor?解析為應(yīng)用層協(xié)議,并通過 Adapter 將 TomcatRequest、TomcatResponse 轉(zhuǎn)化成 標準的 ServletRequest、ServletResponse。通過?getAdapter().service(request, response);將請求傳遞到 Container 容器。adapter.service()實現(xiàn)將請求轉(zhuǎn)發(fā)到容器? org.apache.catalina.connector.CoyoteAdapter
//?Calling?the?container
connector.getService().getContainer().getPipeline().getFirst().invoke(
????????????????????????request,?response);
這個調(diào)用會觸發(fā) getPipeline 構(gòu)成的責(zé)任鏈模式將請求一步步走入容器內(nèi)部,每個容器都有一條 Pipeline,通過 First 開始到 Basic 結(jié)束并進入容器內(nèi)部持有的子類容器,最后到 Servlet,這里就是責(zé)任鏈模式的經(jīng)典運用。具體的源碼組件是 Pipeline 構(gòu)成一條請求鏈。如下圖所示,整個 Tomcat 的架構(gòu)設(shè)計重要組件清晰可見,希望大家將這個全局架構(gòu)圖深深印在腦海里,掌握全局思路才能更好地分析細節(jié)之美。
啟動流程:startup.sh 腳本到底發(fā)生了什么
Tomcat 本生就是一個 Java 程序,所以 startup.sh 腳本就是啟動一個 JVM 來運行 Tomcat 的啟動類 Bootstrap。 Bootstrap 主要就是實例化 Catalina 和初始化 Tomcat 自定義的類加載器。熱加載與熱部署就是靠他實現(xiàn)。 Catalina: 解析 server.xml 創(chuàng)建 Server 組件,并且調(diào)用 Server.start() 方法。 Server:管理 Service 組件,調(diào)用 Server 的 start() 方法。 Service:主要職責(zé)就是管理連接器和頂層容器 Engine,分別調(diào)用? Connector?和?Engine?的?start?方法。
Engine 容器主要就是組合模式將各個容器根據(jù)父子關(guān)系關(guān)聯(lián),并且 Container 容器繼承了 Lifecycle 實現(xiàn)各個容器的初始化與啟動。Lifecycle 定義了?init()、start()、stop()?控制整個容器組件的生命周期實現(xiàn)一鍵啟停。
這里就是一個面向接口、單一職責(zé)的設(shè)計思想?,Container 利用組合模式管理容器,LifecycleBase 抽象類繼承 Lifecycle 將各大容器生命周期統(tǒng)一管理這里便是,而實現(xiàn)初始化與啟動的過程又 LifecycleBase 運用了?模板方法設(shè)計模式抽象出組件變化與不變的點,將不同組件的初始化延遲到具體子類實現(xiàn)。并且利用觀察者模式發(fā)布啟動事件解耦。
具體的 init 與 start 流程如下泳道圖所示:這是我在閱讀源碼 debug 所做的筆記,讀者朋友們不要怕筆記花費時間長,自己跟著 debug 慢慢記錄,相信會有更深的感悟。
init 流程

start 流程

讀者朋友根據(jù)我的兩篇內(nèi)容,抓住主線組件去 debug,然后跟著該泳道圖閱讀源碼,我相信都會有所收獲,并且事半功倍。在讀源碼的過程中,切勿進入某個細節(jié),一定要先把各個組件抽象出來,了解每個組件的職責(zé)即可。最后在了解每個組件的職責(zé)與設(shè)計哲學(xué)之后再深入理解每個組件的實現(xiàn)細節(jié),千萬不要一開始就想著深入理解具體一篇葉子。
每個核心類我在架構(gòu)設(shè)計圖以及泳道圖都標識出來了,給大家分享下如何高效閱讀源碼,以及保持學(xué)習(xí)興趣的心得體會。
如何正確閱讀源碼
切勿陷入細節(jié),不看全局:我還沒弄清楚森林長啥樣,就盯著葉子看?,看不到全貌和整體設(shè)計思路。所以閱讀源碼學(xué)習(xí)的時候不要一開始就進入細節(jié),而是宏觀看待整體架構(gòu)設(shè)計思想,模塊之間的關(guān)系。
1.閱讀源碼之前,需要有一定的技術(shù)儲備
比如常用的設(shè)計模式,這個必須掌握,尤其是:模板方法、策略模式、單例、工廠、觀察者、動態(tài)代理、適配器、責(zé)任鏈、裝飾器。大家可以看 關(guān)于設(shè)計模式的歷史文章,打造好的基礎(chǔ)。
2.必須會使用這個框架/類庫,精通各種變通用法
魔鬼都在細節(jié)中,如果有些用法根本不知道,可能你能看明白代碼是什么意思,但是不知道它為什么這些寫。
3.先去找書,找資料,了解這個軟件的整體設(shè)計。
從全局的視角去看待,上帝視角理出主要核心架構(gòu)設(shè)計,先森林后樹葉。都有哪些模塊?模塊之間是怎么關(guān)聯(lián)的?怎么關(guān)聯(lián)的?
可能一下子理解不了,但是要建立一個整體的概念,就像一個地圖,防止你迷航。
在讀源碼的時候可以時不時看看自己在什么地方。就像「碼哥字節(jié)」給大家梳理好了 Tomcat 相關(guān)架構(gòu)設(shè)計,然后自己再嘗試跟著 debug,這樣的效率如虎添翼。
4. 搭建系統(tǒng),把源代碼跑起來!
Debug 是非常非常重要的手段, 你想通過只看而不運行就把系統(tǒng)搞清楚,那是根本不可能的!合理運用調(diào)用棧(觀察調(diào)用過程上下文)。
5.筆記
一個非常重要的工作就是記筆記(又是寫作!),畫出系統(tǒng)的類圖(不要依靠 IDE 給你生成的), 記錄下主要的函數(shù)調(diào)用, 方便后續(xù)查看。
文檔工作極為重要,因為代碼太復(fù)雜,人的大腦容量也有限,記不住所有的細節(jié)。文檔可以幫助你記住關(guān)鍵點, 到時候可以回想起來,迅速地接著往下看。
要不然,你今天看的,可能到明天就忘個差不多了。所以朋友們記得收藏后多翻來看看,嘗試把源碼下載下來反復(fù)調(diào)試。
錯誤方式
陷入細節(jié),不看全局:我還沒弄清楚森林長啥樣,就盯著葉子看?,看不到全貌和整體設(shè)計思路。所以閱讀源碼學(xué)習(xí)的時候不要一開始就進入細節(jié),而是宏觀看待整體架構(gòu)設(shè)計思想,模塊之間的關(guān)系。 還沒學(xué)會用就研究如何設(shè)計:首先基本上框架都運用了設(shè)計模式,我們最起碼也要了解常用的設(shè)計模式,即使是“背”,也得了然于胸。在學(xué)習(xí)一門技術(shù),我推薦先看官方文檔,看看有哪些模塊、整體設(shè)計思想。然后下載示例跑一遍,最后才是看源碼。 看源碼深究細節(jié):到了看具體某個模塊源碼的時候也要下意識的不要去深入細節(jié),重要的是學(xué)習(xí)設(shè)計思路,而不是具體一個方法實現(xiàn)邏輯。除非自己要基于源碼做二次開發(fā),而且二次開發(fā)也是基于在了解整個架構(gòu)的情況下才能深入細節(jié)。
組件設(shè)計-落實單一職責(zé)、面向接口思想
當我們接到一個功能需求的時候,最重要的就是抽象設(shè)計,將功能拆解主要核心組件,然后找到需求的變化與不變點,將相似功能內(nèi)聚,功能之間若耦合,同時對外支持可拓展,對內(nèi)關(guān)閉修改。努力做到一個需求下來的時候我們需要合理的抽象能力抽象出不同組件,而不是一鍋端將所有功能糅合在一個類甚至一個方法之中,這樣的代碼牽一發(fā)而動全身,無法拓展,難以維護和閱讀。
帶著問題我們來分析 Tomcat 如何設(shè)計組件完成連接與容器管理。
看看 Tomcat 如何實現(xiàn)將 Tomcat 啟動,并且又是如何接受請求,將請求轉(zhuǎn)發(fā)到我們的 Servlet 中。
Catalina
主要任務(wù)就是創(chuàng)建 Server,并不是簡單創(chuàng)建,而是解析 server.xml 文件把文件配置的各個組件意義創(chuàng)建出來,接著調(diào)用 Server 的 init() 和 start() 方法,啟動之旅從這里開始…,同時還要兼顧異常,比如關(guān)閉 Tomcat 還需要做到優(yōu)雅關(guān)閉啟動過程創(chuàng)建的資源需要釋放,Tomcat 則是在 JVM 注冊一個「關(guān)閉鉤子」,源碼我都加了注釋,省略了部分無關(guān)代碼。同時通過?await()?監(jiān)聽停止指令關(guān)閉 Tomcat。
????/**
?????*?Start?a?new?server?instance.
?????*/
????public?void?start()?{
????//?若?server?為空,則解析?server.xml?創(chuàng)建
????????if?(getServer()?==?null)?{
????????????load();
????????}
????//?創(chuàng)建失敗則報錯并退出啟動
????????if?(getServer()?==?null)?{
????????????log.fatal("Cannot?start?server.?Server?instance?is?not?configured.");
????????????return;
????????}
????????//?開始啟動?server
????????try?{
????????????getServer().start();
????????}?catch?(LifecycleException?e)?{
????????????log.fatal(sm.getString("catalina.serverStartFail"),?e);
????????????try?{
????????????????//?異常則執(zhí)行?destroy?銷毀資源
????????????????getServer().destroy();
????????????}?catch?(LifecycleException?e1)?{
????????????????log.debug("destroy()?failed?for?failed?Server?",?e1);
????????????}
????????????return;
????????}
????????//?創(chuàng)建并注冊?JVM?關(guān)閉鉤子
????????if?(useShutdownHook)?{
????????????if?(shutdownHook?==?null)?{
????????????????shutdownHook?=?new?CatalinaShutdownHook();
????????????}
????????????Runtime.getRuntime().addShutdownHook(shutdownHook);
????????}
????//?通過?await?方法監(jiān)聽停止請求
????????if?(await)?{
????????????await();
????????????stop();
????????}
????}
通過「關(guān)閉鉤子」,就是當 JVM 關(guān)閉的時候做一些清理工作,比如說釋放線程池,清理一些零時文件,刷新內(nèi)存數(shù)據(jù)到磁盤中…...
「關(guān)閉鉤子」本質(zhì)就是一個線程,JVM 在停止之前會嘗試執(zhí)行這個線程。我們來看下 CatalinaShutdownHook 這個鉤子到底做了什么。
????/**
?????*?Shutdown?hook?which?will?perform?a?clean?shutdown?of?Catalina?if?needed.
?????*/
????protected?class?CatalinaShutdownHook?extends?Thread?{
????????@Override
????????public?void?run()?{
????????????try?{
????????????????if?(getServer()?!=?null)?{
????????????????????Catalina.this.stop();
????????????????}
????????????}?catch?(Throwable?ex)?{
???????????????...
????????}
????}
????/**
?????*?關(guān)閉已經(jīng)創(chuàng)建的?Server?實例
?????*/
????public?void?stop()?{
????????try?{
????????????//?Remove?the?ShutdownHook?first?so?that?server.stop()
????????????//?doesn't?get?invoked?twice
????????????if?(useShutdownHook)?{
????????????????Runtime.getRuntime().removeShutdownHook(shutdownHook);
????????????}
????????}?catch?(Throwable?t)?{
????????????......
????????}
????????//?關(guān)閉?Server
????????try?{
????????????Server?s?=?getServer();
????????????LifecycleState?state?=?s.getState();
???????????//?判斷是否已經(jīng)關(guān)閉,若是在關(guān)閉中,則不執(zhí)行任何操作
????????????if?(LifecycleState.STOPPING_PREP.compareTo(state)?<=?0
????????????????????&&?LifecycleState.DESTROYED.compareTo(state)?>=?0)?{
????????????????//?Nothing?to?do.?stop()?was?already?called
????????????}?else?{
????????????????s.stop();
????????????????s.destroy();
????????????}
????????}?catch?(LifecycleException?e)?{
????????????log.error("Catalina.stop",?e);
????????}
????}
實際上就是執(zhí)行了 Server 的 stop 方法,Server 的 stop 方法會釋放和清理所有的資源。
Server 組件
來體會下面向接口設(shè)計美,看 Tomcat 如何設(shè)計組件與接口,抽象 Server 組件,Server 組件需要生命周期管理,所以繼承 Lifecycle 實現(xiàn)一鍵啟停。
它的具體實現(xiàn)類是 StandardServer,如下圖所示,我們知道 Lifecycle 主要的方法是組件的 初始化、啟動、停止、銷毀,和 監(jiān)聽器的管理維護,其實就是觀察者模式的設(shè)計,當觸發(fā)不同事件的時候發(fā)布事件給監(jiān)聽器執(zhí)行不同業(yè)務(wù)處理,這里就是如何解耦的設(shè)計哲學(xué)體現(xiàn)。
而 Server 自生則是負責(zé)管理 Service 組件。

接著,我們再看 Server 組件的具體實現(xiàn)類是 StandardServer 有哪些功能,又跟哪些類關(guān)聯(lián)?

在閱讀源碼的過程中,我們一定要多關(guān)注接口與抽象類,接口是組件全局設(shè)計的抽象;而抽象類基本上是模板方法模式的運用,主要目的就是抽象整個算法流程,將變化點交給子類,將不變點實現(xiàn)代碼復(fù)用。
StandardServer 繼承了 LifeCycleBase,它的生命周期被統(tǒng)一管理,并且它的子組件是 Service,因此它還需要管理 Service 的生命周期,也就是說在啟動時調(diào)用 Service 組件的啟動方法,在停止時調(diào)用它們的停止方法。Server 在內(nèi)部維護了若干 Service 組件,它是以數(shù)組來保存的,那 Server 是如何添加一個 Service 到數(shù)組中的呢?
????/**
?????*?添加?Service?到定義的數(shù)組中
?????*
?????*?@param?service?The?Service?to?be?added
?????*/
????@Override
????public?void?addService(Service?service)?{
????????service.setServer(this);
????????synchronized?(servicesLock)?{
???????????//?創(chuàng)建一個?services.length?+?1?長度的?results?數(shù)組
????????????Service?results[]?=?new?Service[services.length?+?1];
???????????//?將老的數(shù)據(jù)復(fù)制到?results?數(shù)組
????????????System.arraycopy(services,?0,?results,?0,?services.length);
????????????results[services.length]?=?service;
????????????services?=?results;
??????//?啟動?Service?組件
????????????if?(getState().isAvailable())?{
????????????????try?{
????????????????????service.start();
????????????????}?catch?(LifecycleException?e)?{
????????????????????//?Ignore
????????????????}
????????????}
????????????//?觀察者模式運用,觸發(fā)監(jiān)聽事件
????????????support.firePropertyChange("service",?null,?service);
????????}
????}
從上面的代碼可以知道,并不是一開始就分配一個很長的數(shù)組,而是在新增過程中動態(tài)拓展長度,這里就是為了節(jié)省空間,對于我們平時開發(fā)是不是也要主要空間復(fù)雜度帶來的內(nèi)存損耗,追求的就是極致的美。
除此之外,還有一個重要功能,上面 Caralina 的啟動方法的最后一行代碼就是調(diào)用了 Server 的 await 方法。
這個方法主要就是監(jiān)聽停止端口,在?await?方法里會創(chuàng)建一個 Socket 監(jiān)聽 8005 端口,并在一個死循環(huán)里接收 Socket 上的連接請求,如果有新的連接到來就建立連接,然后從 Socket 中讀取數(shù)據(jù);如果讀到的數(shù)據(jù)是停止命令“SHUTDOWN”,就退出循環(huán),進入 stop 流程。
Service
同樣是面向接口設(shè)計,Service 組件的具體實現(xiàn)類是 StandardService,Service 組件依然是繼承 Lifecycle 管理生命周期,這里不再累贅展示圖片關(guān)系圖。我們先來看看 Service 接口主要定義的方法以及成員變量。通過接口我們才能知道核心功能,在閱讀源碼的時候一定要多關(guān)注每個接口之間的關(guān)系,不要急著進入實現(xiàn)類。
public?interface?Service?extends?Lifecycle?{
??//?----------主要成員變量
????//Service?組件包含的頂層容器?Engine
????public?Engine?getContainer();
????//?設(shè)置?Service?的?Engine?容器
????public?void?setContainer(Engine?engine);
????//?該?Service?所屬的?Server?組件
????public?Server?getServer();
????//?---------------------------------------------------------?Public?Methods
???//?添加?Service?關(guān)聯(lián)的連接器
????public?void?addConnector(Connector?connector);
????public?Connector[]?findConnectors();
???//?自定義線程池
????public?void?addExecutor(Executor?ex);
???//?主要作用就是根據(jù)?url?定位到?Service,Mapper?的主要作用就是用于定位一個請求所在的組件處理
????Mapper?getMapper();
}
接著再來細看 Service 的實現(xiàn)類:
public?class?StandardService?extends?LifecycleBase?implements?Service?{
????//?名字
????private?String?name?=?null;
????//Server?實例
????private?Server?server?=?null;
????//?連接器數(shù)組
????protected?Connector?connectors[]?=?new?Connector[0];
????private?final?Object?connectorsLock?=?new?Object();
????//?對應(yīng)的?Engine?容器
????private?Engine?engine?=?null;
????//?映射器及其監(jiān)聽器,又是觀察者模式的運用
????protected?final?Mapper?mapper?=?new?Mapper();
????protected?final?MapperListener?mapperListener?=?new?MapperListener(this);
}
StandardService 繼承了 LifecycleBase 抽象類,抽象類定義了 三個 final 模板方法定義生命周期,每個方法將變化點定義抽象方法讓不同組件實現(xiàn)自己的流程。這里也是我們學(xué)習(xí)的地方,利用模板方法抽象變與不變。
此外 StandardService 中還有一些我們熟悉的組件,比如 Server、Connector、Engine 和 Mapper。
那為什么還有一個 MapperListener?這是因為 Tomcat 支持熱部署,當 Web 應(yīng)用的部署發(fā)生變化時,Mapper 中的映射信息也要跟著變化,MapperListener 就是一個監(jiān)聽器,它監(jiān)聽容器的變化,并把信息更新到 Mapper 中,這是典型的觀察者模式。下游服務(wù)根據(jù)多上游服務(wù)的動作做出不同處理,這就是?觀察者模式的運用場景,實現(xiàn)一個事件多個監(jiān)聽器觸發(fā),事件發(fā)布者不用調(diào)用所有下游,而是通過觀察者模式觸發(fā)達到解耦。
Service 管理了 連接器以及 Engine 頂層容器,所以繼續(xù)進入它的 startInternal 方法,其實就是 LifecycleBase 模板定義的 抽象方法??纯此窃趺磫用總€組件順序。
protected?void?startInternal()?throws?LifecycleException?{
????//1.?觸發(fā)啟動監(jiān)聽器
????setState(LifecycleState.STARTING);
????//2. 先啟動 Engine,Engine 會啟動它子容器,因為運用了組合模式,所以每一層容器在會先啟動自己的子容器。
????if?(engine?!=?null)?{
????????synchronized?(engine)?{
????????????engine.start();
????????}
????}
????//3.?再啟動?Mapper?監(jiān)聽器
????mapperListener.start();
????//4.?最后啟動連接器,連接器會啟動它子組件,比如?Endpoint
????synchronized?(connectorsLock)?{
????????for?(Connector?connector:?connectors)?{
????????????if?(connector.getState()?!=?LifecycleState.FAILED)?{
????????????????connector.start();
????????????}
????????}
????}
}
Service 先啟動了 Engine 組件,再啟動 Mapper 監(jiān)聽器,最后才是啟動連接器。這很好理解,因為內(nèi)層組件啟動好了才能對外提供服務(wù),才能啟動外層的連接器組件。而 Mapper 也依賴容器組件,容器組件啟動好了才能監(jiān)聽它們的變化,因此 Mapper 和 MapperListener 在容器組件之后啟動。組件停止的順序跟啟動順序正好相反的,也是基于它們的依賴關(guān)系。
Engine
作為 Container 的頂層組件,所以 Engine 本質(zhì)就是一個容器,繼承了 ContainerBase ,看到抽象類再次運用了模板方法設(shè)計模式。ContainerBase 使用一個?HashMap?成員變量保存每個組件的子容器。同時使用?protected final Pipeline pipeline = new StandardPipeline(this);?Pipeline 組成一個管道用于處理連接器傳過來的請求,責(zé)任鏈模式構(gòu)建管道。
?public?class?StandardEngine?extends?ContainerBase?implements?Engine?{
?}
Engine 的子容器是 Host,所以 children 保存的就是 Host。
我們來看看 ContainerBase 做了什么...
initInternal 定義了容器初始化,同時創(chuàng)建了專門用于啟動停止容器的線程池。 startInternal:容器啟動默認實現(xiàn),通過組合模式構(gòu)建容器父子關(guān)系,首先獲取自己的子容器,使用 startStopExecutor 啟動子容器。
public?abstract?class?ContainerBase?extends?LifecycleMBeanBase
????????implements?Container?{
???//?提供了默認初始化邏輯
????@Override
????protected?void?initInternal()?throws?LifecycleException?{
????????BlockingQueue?startStopQueue?=?new?LinkedBlockingQueue<>();
???????//?創(chuàng)建線程池用于啟動或者停止容器
????????startStopExecutor?=?new?ThreadPoolExecutor(
????????????????getStartStopThreadsInternal(),
????????????????getStartStopThreadsInternal(),?10,?TimeUnit.SECONDS,
????????????????startStopQueue,
????????????????new?StartStopThreadFactory(getName()?+?"-startStop-"));
????????startStopExecutor.allowCoreThreadTimeOut(true);
????????super.initInternal();
????}
??//?容器啟動
????@Override
????protected?synchronized?void?startInternal()?throws?LifecycleException?{
????????//?獲取子容器并提交到線程池啟動
????????Container?children[]?=?findChildren();
????????List>?results?=?new?ArrayList<>();
????????for?(Container?child?:?children)?{
????????????results.add(startStopExecutor.submit(new?StartChild(child)));
????????}
????????MultiThrowable?multiThrowable?=?null;
????????//?獲取啟動結(jié)果
????????for?(Future?result?:?results)?{
????????????try?{
????????????????result.get();
????????????}?catch?(Throwable?e)?{
????????????????log.error(sm.getString("containerBase.threadedStartFailed"),?e);
????????????????if?(multiThrowable?==?null)?{
????????????????????multiThrowable?=?new?MultiThrowable();
????????????????}
????????????????multiThrowable.add(e);
????????????}
????????}
???????......
????????//?啟動?pipeline?管道,用于處理連接器傳遞過來的請求
????????if?(pipeline?instanceof?Lifecycle)?{
????????????((Lifecycle)?pipeline).start();
????????}
?????//?發(fā)布啟動事件
????????setState(LifecycleState.STARTING);
????????//?Start?our?thread
????????threadStart();
????}
}
繼承了 LifecycleMBeanBase 也就是還實現(xiàn)了生命周期的管理,提供了子容器默認的啟動方式,同時提供了對子容器的 CRUD 功能。
Engine 在啟動 Host 容器就是 使用了 ContainerBase 的 startInternal 方法。Engine 自己還做了什么呢?
我們看下 構(gòu)造方法,pipeline 設(shè)置了 setBasic,創(chuàng)建了 StandardEngineValve。
/**
?????*?Create?a?new?StandardEngine?component?with?the?default?basic?Valve.
?????*/
????public?StandardEngine()?{
????????super();
????????pipeline.setBasic(new?StandardEngineValve());
????????.....
????}
容器主要的功能就是處理請求,把請求轉(zhuǎn)發(fā)給某一個 Host 子容器來處理,具體是通過 Valve 來實現(xiàn)的。每個容器組件都有一個 Pipeline 用于組成一個責(zé)任鏈傳遞請求。而 Pipeline 中有一個基礎(chǔ)閥(Basic Valve),而 Engine 容器的基礎(chǔ)閥定義如下:
final?class?StandardEngineValve?extends?ValveBase?{
????@Override
????public?final?void?invoke(Request?request,?Response?response)
????????throws?IOException,?ServletException?{
????????//?選擇一個合適的?Host?處理請求,通過?Mapper?組件獲取到合適的?Host
????????Host?host?=?request.getHost();
????????if?(host?==?null)?{
????????????response.sendError
????????????????(HttpServletResponse.SC_BAD_REQUEST,
?????????????????sm.getString("standardEngine.noHost",
??????????????????????????????request.getServerName()));
????????????return;
????????}
????????if?(request.isAsyncSupported())?{
????????????request.setAsyncSupported(host.getPipeline().isAsyncSupported());
????????}
????????//?獲取?Host?容器的?Pipeline?first?Valve?,將請求轉(zhuǎn)發(fā)到?Host
????????host.getPipeline().getFirst().invoke(request,?response);
}
這個基礎(chǔ)閥實現(xiàn)非常簡單,就是把請求轉(zhuǎn)發(fā)到 Host 容器。處理請求的 Host 容器對象是從請求中拿到的,請求對象中怎么會有 Host 容器呢?這是因為請求到達 Engine 容器中之前,Mapper 組件已經(jīng)對請求進行了路由處理,Mapper 組件通過請求的 URL 定位了相應(yīng)的容器,并且把容器對象保存到了請求對象中。
組件設(shè)計總結(jié)
大家有沒有發(fā)現(xiàn),Tomcat 的設(shè)計幾乎都是面向接口設(shè)計,也就是通過接口隔離功能設(shè)計其實就是單一職責(zé)的體現(xiàn),每個接口抽象對象不同的組件,通過抽象類定義組件的共同執(zhí)行流程。單一職責(zé)四個字的含義其實就是在這里體現(xiàn)出來了。在分析過程中,我們看到了觀察者模式、模板方法模式、組合模式、責(zé)任鏈模式以及如何抽象組件面向接口設(shè)計的設(shè)計哲學(xué)。
連接器之 I/O 模型與線程池設(shè)計
連接器主要功能就是接受 TCP/IP 連接,限制連接數(shù)然后讀取數(shù)據(jù),最后將請求轉(zhuǎn)發(fā)到?Container?容器。所以這里必然涉及到 I/O 編程,今天帶大家一起分析 Tomcat 如何運用 I/O 模型實現(xiàn)高并發(fā)的,一起進入 I/O 的世界。
I/O 模型主要有 5 種:同步阻塞、同步非阻塞、I/O 多路復(fù)用、信號驅(qū)動、異步 I/O。是不是很熟悉但是又傻傻分不清他們有何區(qū)別?
所謂的I/O 就是計算機內(nèi)存與外部設(shè)備之間拷貝數(shù)據(jù)的過程。
CPU 是先把外部設(shè)備的數(shù)據(jù)讀到內(nèi)存里,然后再進行處理。請考慮一下這個場景,當程序通過 CPU 向外部設(shè)備發(fā)出一個讀指令時,數(shù)據(jù)從外部設(shè)備拷貝到內(nèi)存往往需要一段時間,這個時候 CPU 沒事干了,程序是主動把 CPU 讓給別人?還是讓 CPU 不停地查:數(shù)據(jù)到了嗎,數(shù)據(jù)到了嗎……
這就是 I/O 模型要解決的問題。今天我會先說說各種 I/O 模型的區(qū)別,然后重點分析 Tomcat 的 NioEndpoint 組件是如何實現(xiàn)非阻塞 I/O 模型的。
I/O 模型
一個網(wǎng)絡(luò) I/O 通信過程,比如網(wǎng)絡(luò)數(shù)據(jù)讀取,會涉及到兩個對象,分別是調(diào)用這個 I/O 操作的用戶線程和操作系統(tǒng)內(nèi)核。一個進程的地址空間分為用戶空間和內(nèi)核空間,用戶線程不能直接訪問內(nèi)核空間。
網(wǎng)絡(luò)讀取主要有兩個步驟:
用戶線程等待內(nèi)核將數(shù)據(jù)從網(wǎng)卡復(fù)制到內(nèi)核空間。 內(nèi)核將數(shù)據(jù)從內(nèi)核空間復(fù)制到用戶空間。
同理,將數(shù)據(jù)發(fā)送到網(wǎng)絡(luò)也是一樣的流程,將數(shù)據(jù)從用戶線程復(fù)制到內(nèi)核空間,內(nèi)核空間將數(shù)據(jù)復(fù)制到網(wǎng)卡發(fā)送。
不同 I/O 模型的區(qū)別:實現(xiàn)這兩個步驟的方式不一樣。
對于同步,則指的應(yīng)用程序調(diào)用一個方法是否立馬返回,而不需要等待。 對于阻塞與非阻塞:主要就是數(shù)據(jù)從內(nèi)核復(fù)制到用戶空間的讀寫操作是否是阻塞等待的。
同步阻塞 I/O
用戶線程發(fā)起read調(diào)用的時候,線程就阻塞了,只能讓出 CPU,而內(nèi)核則等待網(wǎng)卡數(shù)據(jù)到來,并把數(shù)據(jù)從網(wǎng)卡拷貝到內(nèi)核空間,當內(nèi)核把數(shù)據(jù)拷貝到用戶空間,再把剛剛阻塞的讀取用戶線程喚醒,兩個步驟的線程都是阻塞的。
同步非阻塞
用戶線程一直不停的調(diào)用read方法,如果數(shù)據(jù)還沒有復(fù)制到內(nèi)核空間則返回失敗,直到數(shù)據(jù)到達內(nèi)核空間。用戶線程在等待數(shù)據(jù)從內(nèi)核空間復(fù)制到用戶空間的時間里一直是阻塞的,等數(shù)據(jù)到達用戶空間才被喚醒。循環(huán)調(diào)用read方法的時候不阻塞。
I/O 多路復(fù)用
用戶線程的讀取操作被劃分為兩步:
用戶線程先發(fā)起? select?調(diào)用,主要就是詢問內(nèi)核數(shù)據(jù)準備好了沒?當內(nèi)核把數(shù)據(jù)準備好了就執(zhí)行第二步。用戶線程再發(fā)起? read?調(diào)用,在等待內(nèi)核把數(shù)據(jù)從內(nèi)核空間復(fù)制到用戶空間的時間里,發(fā)起 read 線程是阻塞的。
為何叫 I/O 多路復(fù)用,核心主要就是:一次?select?調(diào)用可以向內(nèi)核查詢多個**數(shù)據(jù)通道(Channel)**的狀態(tài),因此叫多路復(fù)用。
異步 I/O
用戶線程執(zhí)行 read 調(diào)用的時候會注冊一個回調(diào)函數(shù), read 調(diào)用立即返回,不會阻塞線程,在等待內(nèi)核將數(shù)據(jù)準備好以后,再調(diào)用剛剛注冊的回調(diào)函數(shù)處理數(shù)據(jù),在整個過程中用戶線程一直沒有阻塞。
Tomcat NioEndpoint
Tomcat 的 NioEndpoit 組件實際上就是實現(xiàn)了 I/O 多路復(fù)用模型,正是因為這個并發(fā)能力才足夠優(yōu)秀。讓我們一起窺探下 Tomcat NioEndpoint 的設(shè)計原理。
對于 Java 的多路復(fù)用器的使用,無非是兩步:
創(chuàng)建一個 Seletor,在它身上注冊各種感興趣的事件,然后調(diào)用 select 方法,等待感興趣的事情發(fā)生。
感興趣的事情發(fā)生了,比如可以讀了,這時便創(chuàng)建一個新的線程從 Channel 中讀數(shù)據(jù)。
Tomcat 的 NioEndpoint 組件雖然實現(xiàn)比較復(fù)雜,但基本原理就是上面兩步。我們先來看看它有哪些組件,它一共包含 LimitLatch、Acceptor、Poller、SocketProcessor 和 Executor 共 5 個組件,它們的工作過程如下圖所示:

正是由于使用了 I/O 多路復(fù)用,Poller 內(nèi)部本質(zhì)就是持有 Java Selector 檢測 channel 的 I/O 時間,當數(shù)據(jù)可讀寫的時候創(chuàng)建 SocketProcessor 任務(wù)丟到線程池執(zhí)行,也就是少量線程監(jiān)聽讀寫事件,接著專屬的線程池執(zhí)行讀寫,提高性能。
自定義線程池模型
為了提高處理能力和并發(fā)度, Web 容器通常會把處理請求的工作放在線程池來處理, Tomcat 拓展了 Java 原生的線程池來提升并發(fā)需求,在進入 Tomcat 線程池原理之前,我們先回顧下 Java 線程池原理。
Java 線程池
簡單的說,Java 線程池里內(nèi)部維護一個線程數(shù)組和一個任務(wù)隊列,當任務(wù)處理不過來的時,就把任務(wù)放到隊列里慢慢處理。
ThreadPoolExecutor
來窺探線程池核心類的構(gòu)造函數(shù),我們需要理解每一個參數(shù)的作用,才能理解線程池的工作原理。
????public?ThreadPoolExecutor(int?corePoolSize,
??????????????????????????????int?maximumPoolSize,
??????????????????????????????long?keepAliveTime,
??????????????????????????????TimeUnit?unit,
??????????????????????????????BlockingQueue?workQueue,
??????????????????????????????ThreadFactory?threadFactory,
??????????????????????????????RejectedExecutionHandler?handler) ?{
????????......
????}
corePoolSize:保留在池中的線程數(shù),即使它們空閑,除非設(shè)置了 allowCoreThreadTimeOut,不然不會關(guān)閉。 maximumPoolSize:隊列滿后池中允許的最大線程數(shù)。 keepAliveTime、TimeUnit:如果線程數(shù)大于核心數(shù),多余的空閑線程的保持的最長時間會被銷毀。unit 是 keepAliveTime 參數(shù)的時間單位。當設(shè)置? allowCoreThreadTimeOut(true)?時,線程池中 corePoolSize 范圍內(nèi)的線程空閑時間達到 keepAliveTime 也將回收。workQueue:當線程數(shù)達到?corePoolSize?后,新增的任務(wù)就放到工作隊列?workQueue?里,而線程池中的線程則努力地從?workQueue?里拉活來干,也就是調(diào)用 poll 方法來獲取任務(wù)。 ThreadFactory:創(chuàng)建線程的工廠,比如設(shè)置是否是后臺線程、線程名等。 RejectedExecutionHandler:拒絕策略,處理程序因為達到了線程界限和隊列容量執(zhí)行拒絕策略。也可以自定義拒絕策略,只要實現(xiàn)? RejectedExecutionHandler?即可。默認的拒絕策略:AbortPolicy?拒絕任務(wù)并拋出?RejectedExecutionException?異常;CallerRunsPolicy?提交該任務(wù)的線程執(zhí)行;``
來分析下每個參數(shù)之間的關(guān)系:
提交新任務(wù)的時候,如果線程池數(shù) < corePoolSize,則創(chuàng)建新的線程池執(zhí)行任務(wù),當線程數(shù) = corePoolSize 時,新的任務(wù)就會被放到工作隊列 workQueue 中,線程池中的線程盡量從隊列里取任務(wù)來執(zhí)行。
如果任務(wù)很多,workQueue 滿了,且 當前線程數(shù) < maximumPoolSize 時則臨時創(chuàng)建線程執(zhí)行任務(wù),如果總線程數(shù)量超過 maximumPoolSize,則不再創(chuàng)建線程,而是執(zhí)行拒絕策略。DiscardPolicy?什么都不做直接丟棄任務(wù);DiscardOldestPolicy?丟棄最舊的未處理程序;
具體執(zhí)行流程如下圖所示:

Tomcat 線程池
定制版的 ThreadPoolExecutor,繼承了 java.util.concurrent.ThreadPoolExecutor。對于線程池有兩個很關(guān)鍵的參數(shù):
線程個數(shù)。 隊列長度。
Tomcat 必然需要限定想著兩個參數(shù)不然在高并發(fā)場景下可能導(dǎo)致 CPU 和內(nèi)存有資源耗盡的風(fēng)險。繼承了 與 java.util.concurrent.ThreadPoolExecutor 相同,但實現(xiàn)的效率更高。
其構(gòu)造方法如下,跟 Java 官方的如出一轍
public?ThreadPoolExecutor(int?corePoolSize,?int?maximumPoolSize,?long?keepAliveTime,?TimeUnit?unit,?BlockingQueue?workQueue,?RejectedExecutionHandler?handler) ?{
????????super(corePoolSize,?maximumPoolSize,?keepAliveTime,?unit,?workQueue,?handler);
????????prestartAllCoreThreads();
????}
在 Tomcat 中控制線程池的組件是?StandardThreadExecutor?, 也是實現(xiàn)了生命周期接口,下面是啟動線程池的代碼
????@Override
????protected?void?startInternal()?throws?LifecycleException?{
????????//?自定義任務(wù)隊列
????????taskqueue?=?new?TaskQueue(maxQueueSize);
????????//?自定義線程工廠
????????TaskThreadFactory?tf?=?new?TaskThreadFactory(namePrefix,daemon,getThreadPriority());
???????//?創(chuàng)建定制版線程池
????????executor?=?new?ThreadPoolExecutor(getMinSpareThreads(),?getMaxThreads(),?maxIdleTime,?TimeUnit.MILLISECONDS,taskqueue,?tf);
????????executor.setThreadRenewalDelay(threadRenewalDelay);
????????if?(prestartminSpareThreads)?{
????????????executor.prestartAllCoreThreads();
????????}
????????taskqueue.setParent(executor);
????????//?觀察者模式,發(fā)布啟動事件
????????setState(LifecycleState.STARTING);
????}
其中的關(guān)鍵點在于:
Tomcat 有自己的定制版任務(wù)隊列和線程工廠,并且可以限制任務(wù)隊列的長度,它的最大長度是 maxQueueSize。 Tomcat 對線程數(shù)也有限制,設(shè)置了核心線程數(shù)(minSpareThreads)和最大線程池數(shù)(maxThreads)。
除此之外, Tomcat 在官方原有基礎(chǔ)上重新定義了自己的線程池處理流程,原生的處理流程上文已經(jīng)說過。
前 corePoolSize 個任務(wù)時,來一個任務(wù)就創(chuàng)建一個新線程。 還有任務(wù)提交,直接放到隊列,隊列滿了,但是沒有達到最大線程池數(shù)則創(chuàng)建臨時線程救火。 線程總線數(shù)達到 maximumPoolSize ,直接執(zhí)行拒絕策略。
Tomcat 線程池擴展了原生的 ThreadPoolExecutor,通過重寫 execute 方法實現(xiàn)了自己的任務(wù)處理邏輯:
前 corePoolSize 個任務(wù)時,來一個任務(wù)就創(chuàng)建一個新線程。 還有任務(wù)提交,直接放到隊列,隊列滿了,但是沒有達到最大線程池數(shù)則創(chuàng)建臨時線程救火。 線程總線數(shù)達到 maximumPoolSize ,繼續(xù)嘗試把任務(wù)放到隊列中。如果隊列也滿了,插入任務(wù)失敗,才執(zhí)行拒絕策略。
最大的差別在于 Tomcat 在線程總數(shù)達到最大數(shù)時,不是立即執(zhí)行拒絕策略,而是再嘗試向任務(wù)隊列添加任務(wù),添加失敗后再執(zhí)行拒絕策略。
代碼如下所示:
????public?void?execute(Runnable?command,?long?timeout,?TimeUnit?unit)?{
???????//?記錄提交任務(wù)數(shù)?+1
????????submittedCount.incrementAndGet();
????????try?{
????????????//?調(diào)用?java?原生線程池來執(zhí)行任務(wù),當原生拋出拒絕策略
????????????super.execute(command);
????????}?catch?(RejectedExecutionException?rx)?{
??????????//總線程數(shù)達到?maximumPoolSize,Java?原生會執(zhí)行拒絕策略
????????????if?(super.getQueue()?instanceof?TaskQueue)?{
????????????????final?TaskQueue?queue?=?(TaskQueue)super.getQueue();
????????????????try?{
????????????????????//?嘗試把任務(wù)放入隊列中
????????????????????if?(!queue.force(command,?timeout,?unit))?{
????????????????????????submittedCount.decrementAndGet();
??????????????????????//?隊列還是滿的,插入失敗則執(zhí)行拒絕策略
????????????????????????throw?new?RejectedExecutionException("Queue?capacity?is?full.");
????????????????????}
????????????????}?catch?(InterruptedException?x)?{
????????????????????submittedCount.decrementAndGet();
????????????????????throw?new?RejectedExecutionException(x);
????????????????}
????????????}?else?{
??????????????//?提交任務(wù)書?-1
????????????????submittedCount.decrementAndGet();
????????????????throw?rx;
????????????}
????????}
????}
Tomcat 線程池是用 submittedCount 來維護已經(jīng)提交到了線程池,這跟 Tomcat 的定制版的任務(wù)隊列有關(guān)。Tomcat 的任務(wù)隊列 TaskQueue 擴展了 Java 中的 LinkedBlockingQueue,我們知道 LinkedBlockingQueue 默認情況下長度是沒有限制的,除非給它一個 capacity。因此 Tomcat 給了它一個 capacity,TaskQueue 的構(gòu)造函數(shù)中有個整型的參數(shù) capacity,TaskQueue 將 capacity 傳給父類 LinkedBlockingQueue 的構(gòu)造函數(shù),防止無限添加任務(wù)導(dǎo)致內(nèi)存溢出。而且默認是無限制,就會導(dǎo)致當前線程數(shù)達到核心線程數(shù)之后,再來任務(wù)的話線程池會把任務(wù)添加到任務(wù)隊列,并且總是會成功,這樣永遠不會有機會創(chuàng)建新線程了。
為了解決這個問題,TaskQueue 重寫了 LinkedBlockingQueue 的 offer 方法,在合適的時機返回 false,返回 false 表示任務(wù)添加失敗,這時線程池會創(chuàng)建新的線程。
public?class?TaskQueue?extends?LinkedBlockingQueue<Runnable>?{
??...
???@Override
??//?線程池調(diào)用任務(wù)隊列的方法時,當前線程數(shù)肯定已經(jīng)大于核心線程數(shù)了
??public?boolean?offer(Runnable?o)?{
??????//?如果線程數(shù)已經(jīng)到了最大值,不能創(chuàng)建新線程了,只能把任務(wù)添加到任務(wù)隊列。
??????if?(parent.getPoolSize()?==?parent.getMaximumPoolSize())
??????????return?super.offer(o);
??????//?執(zhí)行到這里,表明當前線程數(shù)大于核心線程數(shù),并且小于最大線程數(shù)。
??????//?表明是可以創(chuàng)建新線程的,那到底要不要創(chuàng)建呢?分兩種情況:
??????//1.?如果已提交的任務(wù)數(shù)小于當前線程數(shù),表示還有空閑線程,無需創(chuàng)建新線程
??????if?(parent.getSubmittedCount()<=(parent.getPoolSize()))
??????????return?super.offer(o);
??????//2.?如果已提交的任務(wù)數(shù)大于當前線程數(shù),線程不夠用了,返回?false?去創(chuàng)建新線程
??????if?(parent.getPoolSize()??????????return?false;
??????//?默認情況下總是把任務(wù)添加到任務(wù)隊列
??????return?super.offer(o);
??}
}
只有當前線程數(shù)大于核心線程數(shù)、小于最大線程數(shù),并且已提交的任務(wù)個數(shù)大于當前線程數(shù)時,也就是說線程不夠用了,但是線程數(shù)又沒達到極限,才會去創(chuàng)建新的線程。這就是為什么 Tomcat 需要維護已提交任務(wù)數(shù)這個變量,它的目的就是在任務(wù)隊列的長度無限制的情況下,讓線程池有機會創(chuàng)建新的線程。可以通過設(shè)置 maxQueueSize 參數(shù)來限制任務(wù)隊列的長度。
性能優(yōu)化
線程池調(diào)優(yōu)
跟 I/O 模型緊密相關(guān)的是線程池,線程池的調(diào)優(yōu)就是設(shè)置合理的線程池參數(shù)。我們先來看看 Tomcat 線程池中有哪些關(guān)鍵參數(shù):
| 參數(shù) | 詳情 |
|---|---|
| threadPriority | 線程優(yōu)先級,默認是 5 |
| daemon | 是否是 后臺線程,默認 true |
| namePrefix | 線程名前綴 |
| maxThreads | 最大線程數(shù),默認 200 |
| minSpareThreads | 最小線程數(shù)(空閑超過一定時間會被回收),默認 25 |
| maxIdleTime | 線程最大空閑時間,超過該時間的會被回收,直到只有 minSpareThreads 個。默認是 1 分鐘 |
| maxQueueSize | 任務(wù)隊列最大長度 |
| prestartAllCoreThreads | 是否在線程池啟動的時候就創(chuàng)建 minSpareThreads 個線程,默認是 fasle |
這里面最核心的就是如何確定 maxThreads 的值,如果這個參數(shù)設(shè)置小了,Tomcat 會發(fā)生線程饑餓,并且請求的處理會在隊列中排隊等待,導(dǎo)致響應(yīng)時間變長;如果 maxThreads 參數(shù)值過大,同樣也會有問題,因為服務(wù)器的 CPU 的核數(shù)有限,線程數(shù)太多會導(dǎo)致線程在 CPU 上來回切換,耗費大量的切換開銷。
線程 I/O 時間與 CPU 時間
至此我們又得到一個線程池個數(shù)的計算公式,假設(shè)服務(wù)器是單核的:
線程池大小 = (線程 I/O 阻塞時間 + 線程 CPU 時間 )/ 線程 CPU 時間
其中:線程 I/O 阻塞時間 + 線程 CPU 時間 = 平均請求處理時間。
Tomcat 內(nèi)存溢出的原因分析及調(diào)優(yōu)
JVM 在拋出 java.lang.OutOfMemoryError 時,除了會打印出一行描述信息,還會打印堆棧跟蹤,因此我們可以通過這些信息來找到導(dǎo)致異常的原因。在尋找原因前,我們先來看看有哪些因素會導(dǎo)致 OutOfMemoryError,其中內(nèi)存泄漏是導(dǎo)致 OutOfMemoryError 的一個比較常見的原因。
其實調(diào)優(yōu)很多時候都是在找系統(tǒng)瓶頸,假如有個狀況:系統(tǒng)響應(yīng)比較慢,但 CPU 的用率不高,內(nèi)存有所增加,通過分析 Heap Dump 發(fā)現(xiàn)大量請求堆積在線程池的隊列中,請問這種情況下應(yīng)該怎么辦呢?可能是請求處理時間太長,去排查是不是訪問數(shù)據(jù)庫或者外部應(yīng)用遇到了延遲。
java.lang.OutOfMemoryError: Java heap space
當 JVM 無法在堆中分配對象的會拋出此異常,一般有以下原因:
內(nèi)存泄漏:本該回收的對象唄程序一直持有引用導(dǎo)致對象無法被回收,比如在線程池中使用 ThreadLocal、對象池、內(nèi)存池。為了找到內(nèi)存泄漏點,我們通過 jmap 工具生成 Heap Dump,再利用 MAT 分析找到內(nèi)存泄漏點。
jmap -dump:live,format=b,file=filename.bin pid內(nèi)存不足:我們設(shè)置的堆大小對于應(yīng)用程序來說不夠,修改 JVM 參數(shù)調(diào)整堆大小,比如 -Xms256m -Xmx2048m。
finalize 方法的過度使用。如果我們想在 Java 類實例被 GC 之前執(zhí)行一些邏輯,比如清理對象持有的資源,可以在 Java 類中定義 finalize 方法,這樣 JVM GC 不會立即回收這些對象實例,而是將對象實例添加到一個叫“java.lang.ref.Finalizer.ReferenceQueue”的隊列中,執(zhí)行對象的 finalize 方法,之后才會回收這些對象。Finalizer 線程會和主線程競爭 CPU 資源,但由于優(yōu)先級低,所以處理速度跟不上主線程創(chuàng)建對象的速度,因此 ReferenceQueue 隊列中的對象就越來越多,最終會拋出 OutOfMemoryError。解決辦法是盡量不要給 Java 類定義 finalize 方法。
java.lang.OutOfMemoryError: GC overhead limit exceeded
垃圾收集器持續(xù)運行,但是效率很低幾乎沒有回收內(nèi)存。比如 Java 進程花費超過 96%的 CPU 時間來進行一次 GC,但是回收的內(nèi)存少于 3%的 JVM 堆,并且連續(xù) 5 次 GC 都是這種情況,就會拋出 OutOfMemoryError。
這個問題 IDE 解決方法就是查看 GC 日志或者生成 Heap Dump,先確認是否是內(nèi)存溢出,不是的話可以嘗試增加堆大小??梢酝ㄟ^如下 JVM 啟動參數(shù)打印 GC 日志:
-verbose:gc?//在控制臺輸出GC情況
-XX:+PrintGCDetails??//在控制臺輸出詳細的GC情況
-Xloggc:?filepath??//將GC日志輸出到指定文件中
比如 可以使用?java -verbose:gc -Xloggc:gc.log -XX:+PrintGCDetails -jar xxx.jar?記錄 GC 日志,通過 GCViewer 工具查看 GC 日志,用 GCViewer 打開產(chǎn)生的 gc.log 分析垃圾回收情況。
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
拋出這種異常的原因是“請求的數(shù)組大小超過 JVM 限制”,應(yīng)用程序嘗試分配一個超大的數(shù)組。比如程序嘗試分配 128M 的數(shù)組,但是堆最大 100M,一般這個也是配置問題,有可能 JVM 堆設(shè)置太小,也有可能是程序的 bug,是不是創(chuàng)建了超大數(shù)組。
java.lang.OutOfMemoryError: MetaSpace
JVM 元空間的內(nèi)存在本地內(nèi)存中分配,但是它的大小受參數(shù) MaxMetaSpaceSize 的限制。當元空間大小超過 MaxMetaSpaceSize 時,JVM 將拋出帶有 MetaSpace 字樣的 OutOfMemoryError。解決辦法是加大 MaxMetaSpaceSize 參數(shù)的值。
java.lang.OutOfMemoryError: Request size bytes for reason. Out of swap space
當本地堆內(nèi)存分配失敗或者本地內(nèi)存快要耗盡時,Java HotSpot VM 代碼會拋出這個異常,VM 會觸發(fā)“致命錯誤處理機制”,它會生成“致命錯誤”日志文件,其中包含崩潰時線程、進程和操作系統(tǒng)的有用信息。如果碰到此類型的 OutOfMemoryError,你需要根據(jù) JVM 拋出的錯誤信息來進行診斷;或者使用操作系統(tǒng)提供的 DTrace 工具來跟蹤系統(tǒng)調(diào)用,看看是什么樣的程序代碼在不斷地分配本地內(nèi)存。
java.lang.OutOfMemoryError: Unable to create native threads
Java 程序向 JVM 請求創(chuàng)建一個新的 Java 線程。 JVM 本地代碼(Native Code)代理該請求,通過調(diào)用操作系統(tǒng) API 去創(chuàng)建一個操作系統(tǒng)級別的線程 Native Thread。 操作系統(tǒng)嘗試創(chuàng)建一個新的 Native Thread,需要同時分配一些內(nèi)存給該線程,每一個 Native Thread 都有一個線程棧,線程棧的大小由 JVM 參數(shù) -Xss決定。由于各種原因,操作系統(tǒng)創(chuàng)建新的線程可能會失敗,下面會詳細談到。 JVM 拋出“java.lang.OutOfMemoryError: Unable to create new native thread”錯誤。
總結(jié)
回顧 Tomcat 總結(jié)架構(gòu)設(shè)計,詳細拆解 Tomcat 如何處理高并發(fā)連接設(shè)計。并且分享了如何高效閱讀開源框架源碼思路,設(shè)計模式、并發(fā)編程基礎(chǔ)是重中之重.

