內(nèi)存泄露分析總結(jié)和tomcat調(diào)優(yōu)

? ? 寫在最前面,運(yùn)行環(huán)境:tomcat8,jdk1.8,windows? server 2008內(nèi)存16G,軟件LoadRunner11,MAT和JProfile9.1。

? ? 問題描述:前段時間遇到一個很奇怪的問題,開發(fā)的WEB應(yīng)用,經(jīng)常會毫無癥狀的宕掉,然后抓了線程??聪?,發(fā)現(xiàn)之前寫的數(shù)據(jù)庫鏈接池出現(xiàn)了阻塞的問題,后面分析代碼發(fā)現(xiàn)同步鎖那個地方有一些問題,出現(xiàn)異??赡軐?dǎo)致鎖不釋放,造成堵塞,然后其他線程全block住了,然后應(yīng)用卡住了,最后就掛了。后面換成了開源的DBPC連接池,獲取數(shù)據(jù)庫鏈接卡住的問題就解決了。但是又發(fā)現(xiàn)了一個新問題,用LoadRunner做壓力測試時發(fā)現(xiàn)tomcat占用的內(nèi)存持續(xù)上升,壓了一段時間停了再繼續(xù)壓,tomcat占用內(nèi)存不會釋放,繼續(xù)往上漲,第一反應(yīng)就是懷疑存在內(nèi)存泄露,于是繼續(xù)往下研究。

? ? 由于之前毫無分析內(nèi)存泄露的經(jīng)驗(yàn),對JVM的內(nèi)存分配和回收機(jī)制也不算了解,純小白一個,所以只能看《深入理解Java虛擬機(jī)》和從網(wǎng)上查各種資料。

Part1.JVM內(nèi)存組成介紹

這里先介紹一下JVM的內(nèi)存組成,如下圖所示:

JVM 將內(nèi)存區(qū)域劃分為 MethodArea(Non-Heap)(方法區(qū)),Heap(堆),Program Counter Register(程序計數(shù)器), VM

Stack(虛擬機(jī)棧,也有翻譯成JAVA 方法棧的),Native Method Stack (本地方法棧),其中Method Area和Heap是線程共享的,VMStack,Native

Method Stack 和Program Counter Register是非線程共享的。

那我們的程序是怎么在這些內(nèi)存上運(yùn)行的呢,概括地說來,JVM初始運(yùn)行的時候都會分配好Method Area(方法區(qū))Heap(堆),而JVM 每遇到一個線程(當(dāng)前情景下WEB應(yīng)用前臺的一個數(shù)據(jù)請求發(fā)送到后臺對應(yīng)就是啟動了一個線程),就為其分配一個Program Counter Register(程序計數(shù)器),VM

Stack(虛擬機(jī)棧)和Native Method Stack (本地方法棧),當(dāng)線程終止時,三者(虛擬機(jī)棧,本地方法棧和程序計數(shù)器)所占用的內(nèi)存空間也會被釋放掉。非線程共享的那三個區(qū)域的生命周期與所屬線程相同,而線程共享的區(qū)域與JAVA程序運(yùn)行的生命周期相同,所以這也是系統(tǒng)垃圾回收的場所只發(fā)生在線程共享的區(qū)域(實(shí)際上對大部分虛擬機(jī)來說知發(fā)生在Heap上)的原因。

1.程序計數(shù)器

程序計數(shù)器是一塊較小的內(nèi)存區(qū)域,作用可以看做是當(dāng)前線程執(zhí)行的字節(jié)碼的位置指示器。分支、循環(huán)、跳轉(zhuǎn)、異常處理和線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計算器來完成。

2.VM Strack

  先來了解下JAVA指令的構(gòu)成:

  JAVA指令由 操作碼 (方法本身)和 操作數(shù) (方法內(nèi)部變量) 組成,其實(shí)底層都是體系結(jié)構(gòu)和組成原理里面學(xué)的東西。

  1)方法本身是指令的操作碼部分,保存在Stack中;

  2)方法內(nèi)部變量(局部變量)作為指令的操作數(shù)部分,跟在指令的操作碼之后,保存在Stack中(實(shí)際上是簡單類型(int,byte,short 等)保存在Stack中,對象類型在Stack中保存地址(相當(dāng)于指針里面的地址),在Heap 中保存值);

  虛擬機(jī)棧也叫棧內(nèi)存,是在線程創(chuàng)建時創(chuàng)建,它的生命期是跟隨線程的生命期,線程結(jié)束棧內(nèi)存也就釋放,對于棧來說不存在垃圾回收問題,只要線程一結(jié)束,該棧就 Over,所以不存在垃圾回收。也有一些資料翻譯成JAVA方法棧,大概是因?yàn)樗枋龅氖莏ava方法執(zhí)行的內(nèi)存模型,每個方法執(zhí)行的同時創(chuàng)建幀棧(Strack Frame)用于存儲局部變量表(包含了對應(yīng)的方法參數(shù)和局部變量),操作棧(Operand Stack,記錄出棧、入棧的操作),動態(tài)鏈接、方法出口等信息,每個方法被調(diào)用直到執(zhí)行完畢的過程,對應(yīng)這幀棧在虛擬機(jī)棧的入棧和出棧的過程。

  局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對象的引用(reference類型,不等同于對象本身,根據(jù)不同的虛擬機(jī)實(shí)現(xiàn),可能是一個指向?qū)ο笃鹗嫉刂返囊弥羔?,也可能是一個代表對象的句柄或者其他與對象相關(guān)的位置)和 returnAdress類型(指向下一條字節(jié)碼指令的地址)。局部變量表所需的內(nèi)存空間在編譯期間完成分配,在方法在運(yùn)行之前,該局部變量表所需要的內(nèi)存空間是固定的,運(yùn)行期間也不會改變。

  棧幀是一個內(nèi)存區(qū)塊,是一個數(shù)據(jù)集,是一個有關(guān)方法(Method)和運(yùn)行期數(shù)據(jù)的數(shù)據(jù)集,當(dāng)一個方法 A 被調(diào)用時就產(chǎn)生了一個棧幀 F1,并被壓入到棧中,A 方法又調(diào)用了 B 方法,于是產(chǎn)生棧幀 F2 也被壓入棧,執(zhí)行完畢后,先彈出 F2棧幀,再彈出 F1 棧幀,遵循“先進(jìn)后出”原則。如下圖所示:

3.Heap

  Heap(堆)是JVM的內(nèi)存數(shù)據(jù)區(qū)。Heap 的管理很復(fù)雜,是被所有線程共享的內(nèi)存區(qū)域,在JVM啟動時候創(chuàng)建,專門用來保存對象的實(shí)例。在Heap 中分配一定的內(nèi)存來保存對象實(shí)例,實(shí)際上也只是保存對象實(shí)例的屬性值,屬性的類型和對象本身的類型標(biāo)記等,并不保存對象的方法(以幀棧的形式保存在Stack中),在Heap 中分配一定的內(nèi)存保存對象實(shí)例。而對象實(shí)例在Heap 中分配好以后,需要在Stack中保存一個4字節(jié)的Heap 內(nèi)存地址,用來定位該對象實(shí)例在Heap 中的位置,便于找到該對象實(shí)例,是垃圾回收的主要場所。java堆處于物理不連續(xù)的內(nèi)存空間中,只要邏輯上連續(xù)即可。下面我們還會著重介紹一下這塊區(qū)域。

4.Method Area

  Object Class Data(加載類的類定義數(shù)據(jù)) 是存儲在方法區(qū)的。除此之外,常量、靜態(tài)變量、JIT(即時編譯器)編譯后的代碼也都在方法區(qū)。正因?yàn)榉椒▍^(qū)所存儲的數(shù)據(jù)與堆有一種類比關(guān)系,所以它還被稱為 Non-Heap。方法區(qū)也可以是內(nèi)存不連續(xù)的區(qū)域組成的,并且可設(shè)置為固定大小,也可以設(shè)置為可擴(kuò)展的,這點(diǎn)與堆一樣。

  垃圾回收在這個區(qū)域會比較少出現(xiàn),這個區(qū)域內(nèi)存回收的目的主要針對常量池的回收和類的卸載。

5.運(yùn)行時常量池(Runtime Constant Pool

  方法區(qū)內(nèi)部有一個非常重要的區(qū)域,叫做運(yùn)行時常量池(Runtime Constant Pool,簡稱 RCP)。在字節(jié)碼文件(Class文件)中,除了有類的版本、字段、方法、接口等先關(guān)信息描述外,還有常量池(Constant Pool Table)信息,用于存儲編譯器產(chǎn)生的字面量和符號引用。這部分內(nèi)容在類被加載后,都會存儲到方法區(qū)中的RCP。值得注意的是,運(yùn)行時產(chǎn)生的新常量也可以被放入常量池中,比如 String 類中的 intern() 方法產(chǎn)生的常量。

  常量池就是這個類型用到的常量的一個有序集合。包括直接常量(基本類型,String)和對其他類型、方法、字段的符號引用.例如:

類和接口的全限定名;

字段的名稱和描述符;

方法和名稱和描述符。

  池中的數(shù)據(jù)和數(shù)組一樣通過索引訪問。由于常量池包含了一個類型所有的對其他類型、方法、字段的符號引用,所以常量池在Java的動態(tài)鏈接中起了核心作用。

6.NativeMethod Stack

與VM Strack相似,VM Strack為JVM提供執(zhí)行JAVA方法的服務(wù),Native

Method Stack則為JVM提供使用native 方法的服務(wù)。

7.直接內(nèi)存區(qū)

直接內(nèi)存區(qū)并不是 JVM 管理的內(nèi)存區(qū)域的一部分,而是其之外的。該區(qū)域也會在 Java 開發(fā)中使用到,并且存在導(dǎo)致內(nèi)存溢出的隱患。如果你對 NIO 有所了解,可能會知道 NIO 是可以使用 Native Methods 來使用直接內(nèi)存區(qū)的。

Part2.Heap(堆)和CMS垃圾回收算法

下面我們要詳細(xì)分析一下Heap,Heap(堆)又可以細(xì)分成三部分,Old Gen(老年堆),Eden Space(年輕堆也叫伊甸園),Survivor Space(S0+S1)。我們可以通過配置參數(shù)控制Heap的大小,具體設(shè)置在后面調(diào)優(yōu)會講。當(dāng)程序運(yùn)行時,大多數(shù)情況new的一些對象,最開始都會存放Par Eden Space,然后多次回收(Young GC)之后仍然存活的對象就會挪到CMS Old Gen(老年堆)。需要注意的是除此之外,大的數(shù)組對象且對象中無外部引用的對象,和通過啟動參數(shù)設(shè)置的-XX:PretenureSizeThreshold=1024(字節(jié)),超過這個大小的對象都會直接分配到CMS Old Gen(老年堆)。下面我們要講的垃圾回收算法就是發(fā)生在這個地方。在我們應(yīng)用環(huán)境中,由于我們配置了CMS GC(并發(fā)GC)的回收方法,所以對Eden Space使用的GC算法默認(rèn)就是ParNew(并行GC)。這里供Par Eden Space和Old Gen選擇的GC算法有很多種,可以根據(jù)自己的環(huán)境選擇,一般多核CPU都會選擇CMS(并發(fā)GC),這樣更高效。

CMS執(zhí)行過程可以分成:初始標(biāo)記,并發(fā)標(biāo)記,并發(fā)預(yù)處理,重標(biāo)記,并發(fā)清理,重置六個階段,這里需要注意的是初始標(biāo)記和重標(biāo)記兩個階段是需要Stop-the-world,其他階段都是和程序其他進(jìn)程并發(fā)執(zhí)行的,System.gc()調(diào)用的Full GC的整個過程都是Stop-the-world,這也是為什么說CMS是對系統(tǒng)影響最小的垃圾回收方法。

初始標(biāo)記:該階段進(jìn)行可達(dá)性分析,標(biāo)記GC ROOT可以直接關(guān)聯(lián)的對象。注意這里是直接關(guān)聯(lián),間接關(guān)聯(lián)的將在第二階段進(jìn)行標(biāo)記。那么什么可以作為GC ROOT呢,一般是:①虛擬機(jī)棧中的引用對象。②方法區(qū)中類靜態(tài)屬性引用的對象③方法區(qū)中常量引用對象④本地方法棧中JNI引用對象。

并發(fā)標(biāo)記階段:該階段進(jìn)行GC ROOT

Tracing(大家可以把這個想象成由一個Root構(gòu)成的樹,樹上除了Root節(jié)點(diǎn),存在引用關(guān)系的其他節(jié)點(diǎn)到Root都有可達(dá)路徑。),在第一階段被暫停的線程全部恢復(fù)執(zhí)行,然后從上一階段mark的對象出發(fā),對所有可達(dá)的對象進(jìn)行標(biāo)記。

并發(fā)預(yù)處理:這一步就是CMS算法的精髓所在,因?yàn)镃MS是以獲取最短的停頓時間為目的的GC算法。在mark和remark兩個階段都需要Stop-the-world,所以并發(fā)預(yù)處理的目的就是提前做一些remark做的事情,減短remark階段的耗時。這一階段,將標(biāo)記從Eden Space晉升的對象、從Eden Space分配到Old Gen的對象,以及在并發(fā)標(biāo)記階段被修改的對象。怎么確定一個對象是否存活,即通過追蹤GC ROOT Tracing有可達(dá)路徑的對象就是活著的。舉個例子吧,就比如說一個在Old Gen中存在對象B,在并發(fā)標(biāo)記階段沒被標(biāo)記成alive,眼看就要小命不保了,就在這個時候程序進(jìn)程又New了一個對象A,此時A對象又引用了Old Gen中的B對象(因?yàn)椴l(fā)標(biāo)記階段并不是Stop-the-world,所以程序進(jìn)程和標(biāo)記進(jìn)程是并發(fā)執(zhí)行的)。那么這個對象B就不應(yīng)該被回收掉,因?yàn)楸籄撈了一把,手牽手進(jìn)入了GC ROOT Trace。這個B在并發(fā)預(yù)處理階段就會被標(biāo)記成alive。

重標(biāo)記:這個階段也是要Stop-the-world的,重新掃描堆中的對象,再次進(jìn)行可達(dá)性分析,標(biāo)記alive的對象。

并發(fā)清理:重新激活用戶線程,然后清理哪些dead Objects(不存在引用的對象)。

重置:CMS清楚內(nèi)部狀態(tài),準(zhǔn)備下一次回收。

為了更好地說明CMS回收的過程,這里貼一段實(shí)際場景中的GC日志:

-----------------------------------------初始標(biāo)記(Stop-the-world)---------------------------------------------

135140.215: [GC (CMS Initial Mark) [1CMS-initial-mark: 195002K(3375104K)]207465K(3989504K), 0.0053961 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

-----------------------------------------并發(fā)標(biāo)記---------------------------------------------

135140.221: [CMS-concurrent-mark-start]135140.287: [CMS-concurrent-mark:0.066/0.066 secs] [Times: user=0.50 sys=0.00, real=0.07 secs]

-----------------------------------------并發(fā)預(yù)處理---------------------------------------------

135140.287: [CMS-concurrent-preclean-start]135140.295: [CMS-concurrent-preclean:0.009/0.009 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]

-----------------------------------------重標(biāo)記(Stop-the-world)---------------------------------------------

135140.298: [GC (CMS Final Remark) [YG occupancy: 13058 K (614400 K)]//這里最前面的135140.298是JVM運(yùn)行時間,單位是S。YG就是Young Gen(Eden Space),前面的數(shù)字是占用大小,括號里是總大小

135140.298: [Rescan (parallel) , 0.0071866

secs]//這里要對Young Gen重新掃描

135140.305: [weak refs processing,0.1143667 secs]135140.420: [class unloading, 0.1829570secs]135140.603: [scrub symbol table, 0.0194112secs]135140.622: [scrub string table, 0.0019222secs][1 CMS-remark: 195002K(3375104K)] 208060K(3989504K), 0.4727087 secs][Times: user=0.47 sys=0.00, real=0.47 secs] //這里195002K(3375104K)表示的是Old Gen的使用情況,208060K(3989504K)是整個Heap的使用情況

-----------------------------------------并發(fā)清理---------------------------------------------

135140.771: [CMS-concurrent-sweep-start]135140.845: [CMS-concurrent-sweep:0.073/0.073 secs] [Times: user=0.09 sys=0.03, real=0.07 secs]

-----------------------------------------重置---------------------------------------------

135140.850: [CMS-concurrent-reset-start]135140.856: [CMS-concurrent-reset:0.006/0.006 secs] [Times: user=0.01 sys=0.02, real=0.01 secs] 。

關(guān)于CMS算法的優(yōu)缺點(diǎn),還有具體實(shí)現(xiàn)的的一些細(xì)節(jié),這里就不做過多敘述了,有興趣的可以自行查閱資料。

Part3.MAT分析工具和Jprofile分析工具

一頓操作猛如虎,基本了解了JVM的內(nèi)存的組成和垃圾回收相關(guān)的基礎(chǔ)信息。然后就要來分析一下WEB應(yīng)用到底問題出在哪了。工欲善其事,必先利其器,然后我就下載了MAT和Jprofile。

MAT是專門用來分析內(nèi)存dump文件的工具,需要有Eclipse才能跑。先用jmap指令可以抓到dump文件,具體指令格式如下:

jmap-dump:format=b,file=output pid

pid就是你的java進(jìn)程id,需要注意的是使用這個方法抓dump時,tomcat要用startup.bat去啟動,如果以服務(wù)的方式啟動,這個指令會報錯,可能是權(quán)限問題。這里有個要注意的,抓Heap dump時會先進(jìn)行垃圾回收,再生成dump文件。

直接用MAT打開Dump文件,然后工具還會幫你生成一份分析報告,告訴你可能存在內(nèi)存泄露的地方。廢話不多說,上圖,下圖是對內(nèi)存泄露的分析報告,點(diǎn)進(jìn)去可以看到詳情信息

MAT最重要的功能就是可以分析Heap里的Object、類和引用關(guān)系。

下圖列出了Heap中所有的對象,這里是以Class的方式展示的,然后這里末尾兩列需要關(guān)注一下,Shallow Heap是自身在Heap里面占用的大小,Retained Heap是引用的對象總工占用的大小,單位都是字節(jié)。因?yàn)橐婚_始就是懷疑代碼存在內(nèi)存泄露,擔(dān)心代碼里面定義的靜態(tài)變量占用內(nèi)存太多。剛開始看到String類型占用了大部分堆內(nèi)存,然后進(jìn)去可以看到String類里面由哪些對象引用占用了。

下圖是查看String類的所有引用對象,然后點(diǎn)最后一個Retained Heap可以按從大到小排列,可以看到,最大的那幾個果然是我自己代碼里面定義的靜態(tài)變量,圖中圈紅的地方就是類的堆棧地址,因?yàn)檫@個靜態(tài)緩存的類是單例實(shí)現(xiàn)的,所以在這里出現(xiàn)這個類名的地方后面跟的堆棧地址都是一樣的。

MAT還支持查詢,根據(jù)類名,查詢某個地址上的對象,下圖就是通過地址查找某個對象。

通過一些分析我發(fā)現(xiàn)之前擔(dān)心靜態(tài)變量太多導(dǎo)致內(nèi)存占用太多其實(shí)是多慮了,因?yàn)榫彺娴哪菐讉€靜態(tài)變量都只存了一份,并且他們的Retained Heap也不大,對整個內(nèi)存沒多大影響。雖然對內(nèi)存泄露分析沒什么進(jìn)展但是還是有一些發(fā)現(xiàn),我看到了很多在代碼中手動調(diào)用logger.info()打印的日志信息都存在堆中。這些logger.info當(dāng)初都是為了調(diào)試或者分析問題時添加的,后面也就放著沒刪,有一些打印輸出的內(nèi)容還挺大,這些后面都會占用Heap。

MAT還有個很重要的騷操作就是可以添加兩個Dump來對比,現(xiàn)在回想我覺得這個其實(shí)就可以確定是否存在內(nèi)存泄露。抓Dump時要注意的是抓兩個Dump中間最好要有一定的時間間隔,這個時間間隔中最好應(yīng)用要經(jīng)過一定的壓力測試。下圖就是兩個Dump的對比圖,第二個Dump就是我用LoadRunner壓了一晚上之后再抓的。這里#0就是第二個Dump,#1就是第一個Dump??梢园l(fā)現(xiàn),一個晚上的壓力測試之后,在GC之后生成的Heap Dump文件里面Shallow Heap的大小變化不是很大,Object的數(shù)量變化也不是很大,說明了GC對Heap里面對象回收狀況差不多,如果存在內(nèi)存泄露,存在不能被回收的對象,那第二個Dump文件里面應(yīng)該會出現(xiàn)比第一個Dump文件大很多的類和Object,可是經(jīng)過這么長時間的壓力測試,并沒有出現(xiàn)這種扎眼球的對象和類,所以基本可以斷定應(yīng)用不存在內(nèi)存泄露的狀況。

用MAT分析后感覺還是有點(diǎn)不確定,然后又用Jprofile實(shí)時監(jiān)控了一下JVM狀態(tài),不得不說這個工具真的很強(qiáng)大,可以監(jiān)控本地的JVM進(jìn)程,也可以監(jiān)控遠(yuǎn)程的JVM進(jìn)程,監(jiān)控內(nèi)容從內(nèi)存,對象,GC到線程,CPU,數(shù)據(jù)庫連接狀態(tài)覆蓋面很廣。

具體的使用有興趣的可以去下載下來玩玩,下圖是內(nèi)存監(jiān)控的視圖,這里支持對整個Heap的監(jiān)控,也可以分開監(jiān)控Eden Space和Old Gen。從這個回收視圖可以看出對Eden Space的回收基本每次都可以很徹底(主要看波谷有多低),如果存在內(nèi)存泄露的情況,不會每次回收都能觸及波谷,而且波谷會慢慢升高,因?yàn)閮?nèi)存泄露會導(dǎo)致一些對象無法被回收,而且隨著軟件運(yùn)行時間和壓力增大,泄露的對象會慢慢積累,所以GC完之后藍(lán)色顯示Used size是不可能達(dá)到最低點(diǎn)幾乎為0的大小。

下面這張圖是對GC狀態(tài)的監(jiān)控

分析到這里基本可以確定應(yīng)用不存在內(nèi)存泄露的情況。然后用這個軟件也有一些其他發(fā)現(xiàn)。它有個線程分析視圖可以抓取到線程的狀態(tài),主要是查看壓力測試下線程阻塞的狀態(tài),我發(fā)現(xiàn)很多線程都block在寫日志文件的地方,各進(jìn)程間對日志文件的寫操作肯定是互斥的,一次只允許一個進(jìn)程對日志文件進(jìn)行寫操作,同一時間如果后臺有幾百個進(jìn)程同時需要對日志文件進(jìn)行寫操作,這時就進(jìn)入了阻塞狀態(tài),如下圖所示:

從之前的MAT分析Heap中對象中也發(fā)現(xiàn)很多打印的日志數(shù)據(jù)都存在Heap中,到現(xiàn)在看到這么多線程阻塞在log4j的地方,控制好日志的輸出對高并發(fā)的WEB應(yīng)用影響還是挺大的。

除了視圖還可以監(jiān)控數(shù)據(jù)庫連接的情況,事務(wù)的完成時間,連接池的狀況,連接串的狀況,還可以根據(jù)一些篩選條件進(jìn)行篩選,功能十分強(qiáng)大。

最后再放一張總的監(jiān)控視圖:

由于這個兩個軟件都是臨時下載初次使用,可能還有很多強(qiáng)大的功能沒嘗試,以后還可以繼續(xù)研究研究。

雖然對內(nèi)存泄露的研究有了明確的結(jié)果,可是tomcat占用內(nèi)存持續(xù)升高得不到釋放的問題還是沒有答案?,F(xiàn)在通過監(jiān)控軟件可以明確看到Heap的內(nèi)存使用回收很正常,可是通過任務(wù)管理器監(jiān)控看到的Tomcat占用內(nèi)存卻只升不減。

Tomcat是Java寫的,運(yùn)行在JVM之上,所以tomcat的使用的堆內(nèi)存大小是不可能超過JVM定義的堆大小。所以Jprofile監(jiān)控的Heap使用情況和從任務(wù)管理器看到Tomcat使用內(nèi)存肯定不完全一樣,除了堆內(nèi)存肯定還有之外的內(nèi)存。那么還有什么內(nèi)存呢?上面介紹內(nèi)存組成時有個直接內(nèi)存,當(dāng)發(fā)現(xiàn)還有直接內(nèi)存這個東西時,感覺發(fā)生了新大陸一樣,隱隱約約感覺問題的關(guān)鍵就在這里。

Part4.直接內(nèi)存和NIO

又是一頓查資料了解直接內(nèi)存和NIO相關(guān)的內(nèi)容。根據(jù)官方文檔的描述:

A byte buffer is either direct ornon-direct. Given a direct byte buffer, the Java virtual machine will make abest effort to perform native I/O operations directly upon it. That is, it willattempt to avoid copying the buffer's content to (or from) an intermediatebuffer before (or after) each invocation of one of the underlying operatingsystem's native I/O operations.

byte byffer可以是兩種類型,一種是基于直接內(nèi)存(也就是非堆內(nèi)存);另一種是非直接內(nèi)存(也就是堆內(nèi)存)。

對于直接內(nèi)存來說,JVM將會在IO操作上具有更高的性能,因?yàn)樗苯幼饔糜诒镜叵到y(tǒng)的IO操作。而非直接內(nèi)存,也就是堆內(nèi)存中的數(shù)據(jù),如果要作IO操作,會先復(fù)制到直接內(nèi)存,再利用本地IO處理。

從數(shù)據(jù)流的角度,非直接內(nèi)存是下面這樣的數(shù)據(jù)鏈:

本地IO-->直接內(nèi)存-->非直接內(nèi)存-->直接內(nèi)存-->本地IO

而直接內(nèi)存是:

本地IO-->直接內(nèi)存-->本地IO

很明顯,再做IO處理時,比如網(wǎng)絡(luò)發(fā)送大量數(shù)據(jù)時,直接內(nèi)存會具有更高的效率。

NIO(New IO)是基于基于通道(Channel)和緩沖區(qū)(Buffer)進(jìn)行操作,數(shù)據(jù)總是從通道讀取到緩沖區(qū),或者從緩沖區(qū)寫到通道中。具體信息有興趣可以自行上網(wǎng)查。

這個時候突然看到一個帖子里的回復(fù)說Tomcat8默認(rèn)通信方式就是采用NIO方式,這個時候感覺看到希望之光了,立馬就去看哪里用到了了NIO。后來在tomcat-coyote.jar中找到了,這個coyote是用來處理Tomcat底層的socket,并將http請求、響應(yīng)等字節(jié)流層面的東西,包裝成Request和Response兩個類,供容器使用。所以意味著每一個前端請求都會經(jīng)過這個處理了。下圖源碼中被圈中的代碼就是分配直接內(nèi)存的代碼。

我們再點(diǎn)進(jìn)去可以看到在jdk的源碼中分配直接內(nèi)存有個reserveMemory的函數(shù),在每次分配直接內(nèi)存的時候都會執(zhí)行這個清理函數(shù),然后再點(diǎn)進(jìn)去有個大發(fā)現(xiàn),在這個清理內(nèi)存函數(shù)里面居然有手動調(diào)用gc的代碼。

這個時候靈光一閃突然回想起我們的tomcat的JVM配置參數(shù)里面好像有一個是忽略代碼里面調(diào)用system.gc()的配置參數(shù)。因?yàn)檫@個tomcat容器是被其他單位下發(fā)的已經(jīng)做過優(yōu)化的,所以里面有一堆的配置參數(shù)。其中被我標(biāo)綠的這個-XX:DisableExplicitGC的作用就是不響應(yīng)代碼里面手動調(diào)用system.gc(),看下圖:

上面我們可以看到一堆的配置,里面配置含義我們后面再講。這個時候我以為我已經(jīng)找到了問題的本質(zhì),由于direct Memory在堆外,所以對young gen 的gc過程中是不會回收的。JVM只會在old gen GC(full GC/major GC或者concurrent GC都算)的時候才會對old gen中的對象做reference processing,而在young GC時只會對young gen里的對象做reference processing。也就是說,做full GC的話會對old gen做reference processing,進(jìn)而能觸發(fā)Cleaner對已死的DirectByteBuffer對象做清理工作。而如果很長一段時間里沒做過GC或者只做了young GC的話則不會在old gen觸發(fā)Cleaner的工作,那么就可能讓本來已經(jīng)死了的、但已經(jīng)晉升到old gen的DirectByteBuffer關(guān)聯(lián)的direct Memory得不到及時釋放,這么分析看來這里就是問題的根本了。。。

那我們直接把那個參數(shù)刪掉不就好了嗎,我想了想自己的代碼里面也沒寫過system.gc所以應(yīng)該影響不大,然后就果斷去掉了那個參數(shù)順手也設(shè)置了個直接內(nèi)存大小,配置參數(shù)是-XX:MaxDirectMemorySize。這個直接內(nèi)存不設(shè)置時,默認(rèn)大小是最大堆大小,看下圖源碼。

這么修改完之后又開始做壓力測試,這次看到內(nèi)存很穩(wěn)定,測試二十多個小時,內(nèi)存基本增長到兩個多G就沒再漲了,心情相當(dāng)開心。然后我再去檢查GC日志發(fā)現(xiàn)果然出現(xiàn)了很多system.gc的日志,這個之前都是沒見過的,看下圖:

本以為這次分析到這就結(jié)束了,可是沒想到后面還有新發(fā)現(xiàn),把我本以為下了定論的答案又推翻了。。。有點(diǎn)自己打自己臉的感覺???♂?

Part5.Tomca配置和調(diào)優(yōu)

在分析GC日志的時候,我看到壓力測試中GC日志中Full gc的次數(shù)有點(diǎn)頻繁,而且這種Full gc是Stop-the-world的,很影響應(yīng)用的響應(yīng)時間,從GC日志中可以看到基本一次Full GC耗時要一秒多,頻率高的話很影響性能。然后我又開始搜資料,搜資料的過程中發(fā)現(xiàn)Tomcat8默認(rèn)用NIO是指在linux服務(wù)器下,而我是在Windows的服務(wù)器上跑的。。。然后我發(fā)現(xiàn)Tomcat支持三種接收請求的模式,分別是:BIO,NIO,APR,其中NIO就是我們上面提到的在linux服務(wù)器上默認(rèn)的模式。網(wǎng)上有人對這三種模式分別作了性能測試,發(fā)現(xiàn)APR模式是三種模式里面性能最好的,這種方式是從操作系統(tǒng)級別解決異步IO問題,也是Tomcat運(yùn)行高并發(fā)應(yīng)用的首選。但是開啟比較麻煩,需要一些額外的jar包,有興趣的也可以自行查資料了解一下。我上面也提到了我這里用的Tomcat是經(jīng)過優(yōu)化的,然后我打開server.xml看了一眼,結(jié)果兩眼一黑,我用的Tomcat疑似采用的就是APR這種模式,因?yàn)槲铱吹絊erver.xml中包含這么一句配置:

然后為了確定我這個猜測我又去看了啟動日志,在啟動日志中看到了:

這不赤裸裸的告訴我們開啟了APR的模式嘛。。。這就意味著上面根據(jù)Tomcat8默認(rèn)NIO模式用到了直接內(nèi)存,得出的關(guān)于我的應(yīng)用部署的Tomcat為什么占用內(nèi)存持續(xù)上升的結(jié)論是不成立的!雖然Tomcat確實(shí)有NIO的模式,NIO也確實(shí)會用到直接內(nèi)存,分配直接內(nèi)存時確實(shí)會手動調(diào)用system.gc(),然后tomcat里面配置-XX:DisableExplicitGC確實(shí)會影響內(nèi)存分配導(dǎo)致直接內(nèi)存堆積,可是和我這并沒什么關(guān)系啊。。此時心里萬馬奔騰,但我冷靜一想當(dāng)我去掉-XX:DisableExplicitGC時,GC日志里面出現(xiàn)了很多Full GC的日志,那不是因?yàn)榉峙渲苯觾?nèi)存引起的還有誰再調(diào)用呢。沒辦法,只能一直手動抓線程棧來分析,用下面的指令就可以把當(dāng)前線程棧輸出到一個txt文檔中。

jstack-l pid>C:\Users\Administrator\Desktop\log\ThreadStack.txt

? ? ? ? 這個pid就是你的java線程id。果然抓了幾次就被我抓到了現(xiàn)場,線程棧中果然有個線程在執(zhí)行g(shù)c操作,看下圖:

這次感覺自己應(yīng)該是找到了問題本質(zhì)了,光看這個也看不出什么然后上網(wǎng)搜了下,發(fā)現(xiàn)也是Tomcat配置引起的,真相只有一個就是下面這個配置:

這個看名字就知道是用來檢測是否存在內(nèi)存泄露的,后面看到Tomcat管理頁面自帶一個Find Leaks的功能,不知道是不是和這個有關(guān)系。然后我進(jìn)這個類看了下就有了新發(fā)現(xiàn),在這個類里面我看到GC日志里面執(zhí)行system.gc的那個類名!看下圖:

這里這個gcDaemonProtection的參數(shù)在這個類的上面已經(jīng)定義了,默認(rèn)是true。這就意味著如果不手動修改配置文件,肯定會進(jìn)這個判斷。里面用反射調(diào)用了sun.misc.GC的requestLatency方法。我點(diǎn)進(jìn)這個sun.misc.GC類里面,看到了GC日志里面那個run的地方,看下圖:

看名字這是一個守護(hù)進(jìn)程,里面調(diào)用了system.gc(),這下可以肯定的是GC日志里頻繁出現(xiàn)的Full GC操作就是這里引起的(后面我又抓了很多次線程棧發(fā)現(xiàn)調(diào)用GC的只有這一個類)。到這里我終于可以確定為什么GC日志里面那么多Full GC了,都是因?yàn)門omcat配置里面加載這個內(nèi)存檢測的Class導(dǎo)致的。那有什么辦法可以避免這個呢,后來網(wǎng)上查了下,有這么幾種方法:

① 直接去掉這個配置

②? 將上面那個默認(rèn)配置true的參數(shù)改成false,將Server.xml里面的對應(yīng)那條配置中增加下面的一段:

gcDaemonProtection=”false”

③ 增加-XX:+ExplicitGCInvokesConcurrent配置,這個參數(shù)不會像DisableExplicitGC一樣強(qiáng)行忽略手動調(diào)用system.gc,而是在遇到調(diào)用system.gc時調(diào)用CMS垃圾回收方法。因?yàn)樯厦嫣徇^CMS是停頓最短的GC方法,這樣就可以避免由full

GC帶來的長GC pause引起的性能問題。

經(jīng)過測試我是采用的第三個方法,到最后內(nèi)存增長的問題得到了解決。最后我們看一下GC日志中CMS的耗時,看下圖:

這里被我用紅線劃得就是mark和remark的兩個階段,因?yàn)檫@有這兩個階段是Stop-the-world的,可以看到耗時和Full GC比起來要短很多。

最后Tomcat的JVM配置參數(shù)被我修改為下圖:

這里面有幾個比較重要好用的我大概說一下:

-Xloggc:gc.log -XX:+PrintGCDetails,這個參數(shù)會設(shè)置打印GC日志,這次問題,靠GC日志分析出了很多有用的東西。

-XX:+UseConcMarkSweepGC ,選用CMS作為垃圾回收方法

-XX:+ ExplicitGCInvokesConcurrent,用這個替換了DisableExplicitGC,每次遇到system.gc時調(diào)用一次CMS回收,并不是直接Stop-the-world。

-XX:+UseCMSCompactAtFullCollection,在每次CMS收集器在完成垃圾回收之后做一次內(nèi)存碎片整理。

Tomcat的線程池也是可以自己配置的,包括可接受的連接數(shù)之類的,這里我就不展開說了,碼字也不輕松…

其他還有很多有興趣的可以自行了解。

總結(jié):這次遇到這個奇怪的問題到解決查了很多資料也收獲了很多新知識,特別是發(fā)現(xiàn)Full GC并不是直接內(nèi)存引起而是因?yàn)榱硪粋€配置導(dǎo)致的時候,有種柳暗花明又一村的感覺。其實(shí)真正需要做的改動只是增加了兩個配置參數(shù),刪除了一個配置參數(shù),但需要了解的東西卻十分龐雜,性能調(diào)優(yōu)涉及的東西太多了,這次很多東西這次只是淺淺的接觸了解了一下,還需要繼續(xù)努力。這次解決完問題感覺很有必要寫下來,一個是擔(dān)心以后忘了,還有就是順便鍛煉一下自己的總結(jié)能力。有時候你會發(fā)現(xiàn),你很抗拒的事情在等你完成之后回頭看也不過如此。最后,與君共勉。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內(nèi)容