<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Java性能優(yōu)化指南(一)

          共 22005字,需瀏覽 45分鐘

           ·

          2021-01-15 20:38

          2015年在大物流項(xiàng)目中,給項(xiàng)目團(tuán)隊(duì)做了幾次Java性能優(yōu)化和問題排查的分享,不過效果都不是很好。一直覺得偏向技術(shù)實(shí)踐類的東西,單純的聽和單純的講收獲都很有限,最好的做法是閱讀學(xué)習(xí)-理解-實(shí)踐-總結(jié),這樣的方式。這一份原來是我在閱讀《Java性能優(yōu)化權(quán)威指南》時(shí)候的閱讀筆記,最近整理后在這邊做一下分享。

          想要讓程序運(yùn)行得飛快,需要深入了解程序的工作原理,在Java世界里面,這既包括了特定的業(yè)務(wù)代碼,也包括JVM和Java API的工作原理,只有在理解它們是如何工作的原理之后,才能理解為什么應(yīng)用的某些行為這么糟糕,已經(jīng)怎么排除那些導(dǎo)致性能低下的問題。

          性能調(diào)優(yōu)是科學(xué)和藝術(shù)的結(jié)合,即需要嚴(yán)格的數(shù)據(jù)收集和分析,有時(shí)候也需要給出一些直覺的判斷,特別是在一些需要進(jìn)行取舍權(quán)衡的地方。分析可以定位為題,解決問題,有時(shí)候需要一些經(jīng)驗(yàn)和直覺的判斷。

          1. 針對(duì)業(yè)務(wù)模型進(jìn)行設(shè)計(jì),保持業(yè)務(wù)邏輯的清晰,DDD

          2. 編寫更好的算法——程序設(shè)計(jì)=算法+數(shù)據(jù)結(jié)構(gòu),合理的算法邏輯可以改變成功程序的運(yùn)行過程

          3. 奧卡姆剃刀原理——如無必要,勿增實(shí)體

            1. 借助性能分析來優(yōu)化代碼,重點(diǎn)關(guān)注最耗時(shí)的方法

            2. 簡(jiǎn)單即有效,性能問題最可能最容易理解的,新代碼>機(jī)器配置>JVM和操作系統(tǒng)Bug

            3. 歷史的遺留分析,處理最為復(fù)雜,如果業(yè)務(wù)已經(jīng)遺失,不要輕易進(jìn)行重構(gòu)

          4. 良好的編碼習(xí)慣,良好的編碼習(xí)慣能規(guī)避必然會(huì)影響性能的代碼和設(shè)計(jì)

          5. 留意數(shù)據(jù)庫的性能,在多數(shù)情況下,數(shù)據(jù)庫的性能總有提升的空間,對(duì)于業(yè)務(wù)系統(tǒng),大量時(shí)間會(huì)消耗在對(duì)數(shù)據(jù)庫的操作上,對(duì)這部分的優(yōu)化可以達(dá)到立竿見影的效果。

          測(cè)試和衡量

          測(cè)試

          1. 微基準(zhǔn)測(cè)試,局部的代碼測(cè)試,實(shí)現(xiàn)上多為針對(duì)特定方法的單元測(cè)試,可以測(cè)試一些特定方法或者代碼邏輯的性能表現(xiàn)

            1. 微基準(zhǔn)測(cè)試難以編寫

            2. 很難模擬實(shí)際的程序運(yùn)行場(chǎng)景

          2. 宏基準(zhǔn)測(cè)試,衡量應(yīng)用性能最好的就是應(yīng)用本身,以及其用到的各種資源,實(shí)現(xiàn)上多為整體集成測(cè)試,可以反應(yīng)系統(tǒng)各個(gè)模塊之間的效率差

            1. 隨著應(yīng)用規(guī)模的增長,難以達(dá)到。一般需要一整套工具配套包括不僅限于:流量重放工具,調(diào)用鏈跟蹤工具

            2. 資源的分配,進(jìn)行一次測(cè)試,需要大量的資源和不同系統(tǒng)的協(xié)調(diào),無法快速完成。

          3. 介基準(zhǔn)測(cè)試,規(guī)模介于上面兩者之間的測(cè)試,表現(xiàn)為一個(gè)特定模塊或者獨(dú)立功能的測(cè)試。具體可以表現(xiàn)為包含獨(dú)立業(yè)務(wù)邏輯的單元測(cè)試,可以測(cè)試獨(dú)立模塊的性能表現(xiàn)

            1. 只是一個(gè)折中選擇,無法準(zhǔn)確反映性能表現(xiàn)

          總結(jié):

          • 微基準(zhǔn)測(cè)試,有助于了解局部的性能特性,進(jìn)行局部得代碼優(yōu)化,難以編寫,價(jià)值有限,但可以快速了解代碼的特性。

          • 介基準(zhǔn)測(cè)試,在系統(tǒng)進(jìn)行模塊化隔離之后,用于測(cè)試局部的系統(tǒng)功能,對(duì)于開發(fā)團(tuán)隊(duì)而言是一個(gè)很好的輸出檢測(cè)。

          • 宏基準(zhǔn)測(cè)試,是了解整體系統(tǒng)性能的唯一途徑,是系統(tǒng)上線前的必備前提。

          如果要對(duì)一個(gè)產(chǎn)品進(jìn)行性能評(píng)估,找到問題點(diǎn)的時(shí)候,也可以分別從三面的三種測(cè)試。

          系統(tǒng)整體性能瓶頸=>最核心的耗時(shí)模塊=>模塊分解為獨(dú)立的子模塊獨(dú)立進(jìn)行性能測(cè)試=>耗時(shí)代碼=>微基準(zhǔn)測(cè)試進(jìn)行問題分析

          不同的開發(fā)領(lǐng)域?qū)ι厦嫒叩亩x也是不一樣的,一般參考比較的是所對(duì)應(yīng)系統(tǒng)或者產(chǎn)品的自身特性來進(jìn)行劃分

          時(shí)間衡量

          首先Java世界中,性能測(cè)試需要先進(jìn)行系統(tǒng)的熱身。一方面是因?yàn)榧磿r(shí)編譯(JIT)的存在,另一方面也需要對(duì)一些緩存數(shù)據(jù)等進(jìn)行預(yù)熱

          關(guān)注的指標(biāo):TPS、RPS、OPS、響應(yīng)時(shí)間、90響應(yīng)時(shí)間、95響應(yīng)時(shí)間

          統(tǒng)計(jì)方法

          基準(zhǔn)測(cè)試會(huì)通過多次測(cè)試、模擬真實(shí)的數(shù)據(jù),衡量測(cè)試數(shù)據(jù)的變化,需要基線(baseline)和試樣(specimen)

          準(zhǔn)確衡量測(cè)試的結(jié)果,需要利用統(tǒng)計(jì)學(xué)只是進(jìn)行衡量,此處不進(jìn)行拓展。

          盡早頻繁測(cè)試

          盡量頻繁的測(cè)試能夠讓我們今早發(fā)現(xiàn)問題和定位問題,但是測(cè)試的是有成本的,好的性能測(cè)試,需要反復(fù)多次運(yùn)行,特別是在代碼修改之后。

          1. 自動(dòng)化:自動(dòng)化的測(cè)試可以讓測(cè)試過程更加規(guī)范,讓每輪測(cè)試運(yùn)行時(shí)保持相同的環(huán)境,使得測(cè)試可重復(fù)。

          2. 測(cè)試一切:理想的測(cè)試需要收集系統(tǒng)各方面的信息,包括不限于CPU使用率、磁盤使用率、網(wǎng)絡(luò)使用率、內(nèi)存使用率、堆棧信息、gc日志、數(shù)據(jù)庫的AWR報(bào)告等等

          3. 在真實(shí)系統(tǒng)上運(yùn)行:如果測(cè)試要如實(shí)反映真實(shí)系統(tǒng)的運(yùn)行表現(xiàn),需要基于相同的硬件條件進(jìn)行測(cè)試

          工具箱

          為了了解程序的性能表現(xiàn),需要獲取大量的統(tǒng)計(jì)指標(biāo),通過操作系統(tǒng)和JDK自帶的工具,可以使我們獲取系統(tǒng)運(yùn)行時(shí)各項(xiàng)指標(biāo)

          操作系統(tǒng)工具箱

          性能分析可以先分析CPU,性能調(diào)優(yōu)的目標(biāo)是讓CPU的使用率盡量提高,并且CPU的計(jì)算時(shí)間都運(yùn)行在業(yè)務(wù)計(jì)算之上,在試圖進(jìn)行調(diào)優(yōu)之前,應(yīng)該先弄清楚為什么CPU的使用率較低

          vmstat?1

          CPU的空閑一般由于下面的原因

          1. 應(yīng)用被同步原語阻塞、直至鎖釋放才能繼續(xù)執(zhí)行

          2. 應(yīng)用在等待某些東西,例如數(shù)據(jù)庫返回響應(yīng)

          3. 應(yīng)用確實(shí)沒什么事情可以做

          前兩個(gè)可以通過降低競(jìng)爭(zhēng)、提高外部系統(tǒng)性能來提高CPU的利用率。

          CPU的利用率是基于時(shí)間來統(tǒng)計(jì)的,如果在1秒內(nèi),CPU被100%占用450ms,550ms沒有被占用,那么這段時(shí)間的利用率就是45%

          磁盤使用率

          iostat?-xm?5

          如果沒有進(jìn)行有效的IO,磁盤寫入數(shù)據(jù)的時(shí)候,統(tǒng)計(jì)數(shù)據(jù)會(huì)很低、 或者應(yīng)用系統(tǒng)的IO過高達(dá)到了硬件的瓶頸。 此外虛擬內(nèi)存swap區(qū)域也可能會(huì)導(dǎo)致磁盤的IO出現(xiàn)問題

          網(wǎng)絡(luò)使用率

          nicstat?5

          監(jiān)控網(wǎng)絡(luò)的吞吐,必要的時(shí)候進(jìn)行抓包分析網(wǎng)絡(luò)通信狀況

          JDK監(jiān)控工具

          獲取JVM調(diào)優(yōu)標(biāo)志

          jcmd?process_id?VM.flags?[-all]

          在程序的執(zhí)行過程中改變標(biāo)志[需要時(shí)manageable級(jí)別的標(biāo)志]

          jinfo?-flag?-PrintGCDetails?process_id?#?turns?off?PrintGCDetails
          jstack?-l?process_id
          jcmd?process_id?Thread.print

          實(shí)時(shí)GC分析:jconsole 事后堆轉(zhuǎn)儲(chǔ):jmap -dump 然后使用jvisualvm、jhat、MAT等工具分析

          性能采樣

          一種方式是進(jìn)行數(shù)據(jù)采樣,這里面存在一個(gè)矛盾,就是采樣的過程本身會(huì)影響程序的性能表現(xiàn)。為了盡量得到更加接近現(xiàn)實(shí)的數(shù)據(jù),我們隊(duì)采樣率的選擇應(yīng)該謹(jǐn)慎對(duì)待。

          避免采樣不均衡,如果系統(tǒng)有周期性的任務(wù),而采樣周期和系統(tǒng)運(yùn)行周期錯(cuò)開那么就有可能遺漏很多重要的信息,為了防止采樣失真,可以采用動(dòng)態(tài)調(diào)整的采樣率。

          另一種對(duì)性能進(jìn)行分析的方式是探測(cè)分析器,類似jvirtualvm這樣的工具具有探測(cè)功能,相比采樣分析,探測(cè)工具可以得到更加精確的系統(tǒng)運(yùn)行信息,特別是對(duì)調(diào)用次數(shù)等。但是同樣的,探測(cè)分析器會(huì)在類加載的時(shí)候,更改類的字節(jié)碼,更加可能引入性能偏差

          因此探測(cè)分析器應(yīng)該僅在小代碼區(qū)域,一些類和包中設(shè)置使用,以減少對(duì)應(yīng)用整體性能的影響

          同時(shí)Java的采樣分析器只能在線程位于安全點(diǎn)時(shí)采集線程樣本,也就是在JVM分配內(nèi)存的時(shí)候。有一些方法不需要執(zhí)行此步驟,可能無法采樣

          阻塞方法,在進(jìn)行線程執(zhí)行時(shí)間分析的時(shí)候,如果線程處于等待狀態(tài),那么線程對(duì)于CPU資源是沒有消耗的,必須分析出阻塞的原因才能判斷是不是性能問題、

          使用JMC等工具,因?yàn)榇嬖谝欢ǖ纳虡I(yè)授權(quán)限制,雖然JMC非常強(qiáng)大而方便,但是一般應(yīng)用在測(cè)試環(huán)境中,常見的功能可以監(jiān)控?zé)狳c(diǎn)代碼、獲取GC事件,特別是GC事件,在JFR之外很難獲得。

          jcmd?process_id?JFR.start

          各種工具非常重要,但是也各有擅長,聰明的調(diào)優(yōu)應(yīng)該結(jié)合不同的工具,爭(zhēng)取能夠洞悉系統(tǒng)的各個(gè)運(yùn)行環(huán)節(jié),有的放矢才能實(shí)現(xiàn)目標(biāo)。

          JIT 即時(shí)編譯

          在回答Java是編譯型語言和解釋型語言的時(shí)候,總是不免討論JIT的問題。JIT也是對(duì)于Java執(zhí)行性能影響最大的技術(shù)、也是Java能夠應(yīng)用在一些高性能核心領(lǐng)域和C/CPP競(jìng)爭(zhēng)的武器之一。

          編譯型語言,程序以編譯后的二進(jìn)制形式交付,這個(gè)二進(jìn)制文件中匯編碼是針對(duì)特定的CPU指令集的。解釋型語言:在運(yùn)行的是時(shí)候使用解析器,解釋器將對(duì)應(yīng)的程序代碼轉(zhuǎn)換成為二進(jìn)制代碼。

          解釋型語言強(qiáng)在可移植性,但是因?yàn)槊看螆?zhí)行都需要對(duì)代碼進(jìn)行翻譯,因此執(zhí)行效率較差。并且由于編譯型語言可以獲取更多的程序信息,主要是程序的上下文關(guān)聯(lián)(解釋型語言一般是逐行解析的),因此可以編譯出效率更高的二進(jìn)制執(zhí)行文件。

          Java采用的是中間線路,Java應(yīng)用會(huì)被編譯,但是不是編譯成特定CPU所專用的二進(jìn)制碼,而是一種理想化的匯編語言。然后該匯編語言也就是Java字節(jié)碼,可以用Java運(yùn)行。這使得Java是一種平臺(tái)獨(dú)立的解釋型語言。同時(shí),在Java程序運(yùn)行的是字節(jié)碼,JVM能夠在代碼執(zhí)行的時(shí)候?qū)⑵渚幾g成為平臺(tái)特定的二進(jìn)制代碼,這個(gè)過程就成為JIT。

          衡量一段代碼會(huì)不會(huì)被即時(shí)編譯的標(biāo)準(zhǔn)就是,編譯的代碼執(zhí)行更快,多次執(zhí)行累計(jì)節(jié)約的時(shí)間超過了編譯所花費(fèi)的時(shí)間。這也是一種利弊的權(quán)衡。編譯需要時(shí)間、如果編譯了只少量執(zhí)行的代碼,那么這個(gè)時(shí)間花費(fèi)顯然是不劃算的。

          同時(shí)字節(jié)碼在編譯成為匯編語言的過程中會(huì)有大量的優(yōu)化,如果字節(jié)碼在解釋運(yùn)行一段時(shí)間之后,JVM會(huì)回去足夠多的優(yōu)化信息,通過對(duì)代碼特性的辨別,可以使用諸如查找優(yōu)化、寄存器等技術(shù),可以極大地提高性能

          Client 和Server

          client和server是兩種模式的編譯器,主要的區(qū)別在于編譯的時(shí)機(jī)不同,client編譯器會(huì)早于server編譯器開始編譯。這意味著代碼執(zhí)行的開始階段,client編譯器比server編譯器要快,因?yàn)樗木幾g代碼相比server編譯器而言要多得多,在server編譯器還在使用解釋型運(yùn)行的時(shí)候,client編譯器已經(jīng)開始使用二進(jìn)制匯編執(zhí)行了。

          但是server的長處在于,server在等待編譯的時(shí)候,收集了更多的程序運(yùn)行信息,因此在編譯的時(shí)候可以更好地進(jìn)行優(yōu)化,由server編譯器生成的代碼要比client編譯器生成的代碼運(yùn)行更快。

          綜合兩者的優(yōu)點(diǎn),我們會(huì)選擇在程序啟動(dòng)的時(shí)候使用client編譯器,在程序運(yùn)行過程中,隨著代碼逐漸變熱使用server編譯器,這種技術(shù)成為分層編譯。

          -server
          -client
          -XX:+TieredCompilation

          在JDK8中,分層編譯默認(rèn)開啟,在此之前需要手動(dòng)開啟

          一般而言,開始分層編譯都是一個(gè)比較好的默認(rèn)選擇

          32位和64位的區(qū)別,目前的服務(wù)器大多數(shù)都是64位,并且在引入了壓縮的普通對(duì)象指針之后,64位JVM在小規(guī)模內(nèi)存堆中的表現(xiàn)也有所提升,可以使用超過4G的內(nèi)存,也給未來提供了更多的優(yōu)化可能性,因此大部分情況下采用64服務(wù)器和64位JVM、除非有明顯的決策因素證明32位JVM更加適合

          JVM編譯代碼的時(shí)候,會(huì)在代碼緩存中保留編譯之后的匯編語言指令集,如果緩存大小固定,那么一旦填滿后就不能編譯更多的代碼了。

          通過

          -XX:ReservedCodeCacheSize=N

          可以設(shè)置最大的緩存保留內(nèi)存,這個(gè)值的大小一般要根據(jù)具體的硬件資源來設(shè)置,當(dāng)設(shè)置這個(gè)最大值后,會(huì)作為保留內(nèi)存預(yù)留出來。等待有需要的時(shí)候才進(jìn)行分配

          JVM判斷是否進(jìn)行編譯的標(biāo)準(zhǔn)一般是運(yùn)行的次數(shù),當(dāng)運(yùn)行次數(shù)達(dá)到編譯閾值的時(shí)候,編譯器就認(rèn)為獲取了足夠的信息可以對(duì)代碼進(jìn)行編譯了

          -XX:CompileThreshold=N

          可以修改編譯閾值,默認(rèn)client N為1500 server N為10,000

          這個(gè)次數(shù)統(tǒng)計(jì)基于兩種JVM技術(shù):方法調(diào)用計(jì)數(shù)器和方法中的循環(huán)回邊計(jì)數(shù)器,回邊實(shí)際上可以看作是循環(huán)完成執(zhí)行的次數(shù),包括continute這樣的分支。

          如果一個(gè)方法中的循環(huán),執(zhí)行了非常多次,其回邊計(jì)數(shù)器就會(huì)達(dá)到編譯閾值,但是方法的棧還沒有退出。JVM可以在棧上對(duì)代碼進(jìn)行替換。這種技術(shù)成為棧上替換(OSR On-Stack Replacement)

          較低的閾值,降低了編譯優(yōu)化的門檻。適當(dāng)?shù)亟档烷撝?,?yōu)化的原理是可以提前對(duì)代碼進(jìn)行編譯優(yōu)化。又可以盡量保證優(yōu)化出來的代碼質(zhì)量

          并且每個(gè)計(jì)數(shù)器,也會(huì)周期性地減少,特別是JVM到達(dá)安全點(diǎn)之后,因此計(jì)數(shù)器并非累加,而只是最新熱度的衡量。也是這個(gè)原因,使用分層編譯可能更能照顧到一些不太頻繁執(zhí)行的代碼的優(yōu)化。

          -XX:+PrintCompilation

          檢測(cè)編譯的過程,每次編譯一個(gè)方法會(huì)被輸出

          jstat?-compiler?process_id
          jstat?-printcompilation?process_id?1000

          可以了解編譯的信息

          在編碼的過程中,我們也可以讓代碼結(jié)構(gòu)更加簡(jiǎn)單,可以使得編譯器能夠更好地處理。

          單核使用多線程,只會(huì)增加線程切換(寄存器、高速緩存的切換)

          運(yùn)行進(jìn)程的信息被存儲(chǔ)在處理器的寄存器和高速緩存(cache)中,執(zhí)行的進(jìn)程被加載到寄存器的數(shù)據(jù)集被稱為上下文(context)

          如果兩個(gè)線程做同樣的事情,那么切換這兩個(gè)線程只會(huì)并不會(huì)帶來性能的提升

          內(nèi)聯(lián):是編譯器所能做的最有利的優(yōu)化,特別是對(duì)屬性封裝良好的面向?qū)ο蟮拇a,內(nèi)聯(lián)會(huì)將getter和setter方法簡(jiǎn)化為直接對(duì)屬性的操作,也就是代碼的合并,減少一個(gè)棧的調(diào)用深度。不過內(nèi)聯(lián)的細(xì)節(jié)信息很難查看。

          逃逸分析:如果開啟逃逸分析(-XX:+DoEscapeAnalysis?默認(rèn)開啟),編譯器會(huì)執(zhí)行一些非常激進(jìn)的優(yōu)化,這是編譯器能夠做得最復(fù)雜的優(yōu)化,通常會(huì)簡(jiǎn)化鎖的處理、減少中間對(duì)象的分配而只是追蹤個(gè)別字段。這樣的優(yōu)化可能會(huì)給不正確的同步代碼帶來一些問題。通過簡(jiǎn)化相關(guān)的代碼,可以讓逃逸分析優(yōu)化有更好的表現(xiàn)

          逆優(yōu)化

          在一些情況下,編譯器不得不撤銷之前的某些編譯

          1. 代碼被丟棄,例如一個(gè)接口有兩個(gè)實(shí)現(xiàn)類,但是已經(jīng)編譯優(yōu)化后使用了其中的一個(gè),當(dāng)需要調(diào)用第二個(gè)的時(shí)候,就會(huì)使之前的優(yōu)化失效。

          2. 代碼先由cleint編譯器編譯,然后使用server編譯器編譯。中間也會(huì)存在一個(gè)丟棄和替換的過程。

          對(duì)于final關(guān)鍵字,以前認(rèn)為final會(huì)影響編譯器的內(nèi)聯(lián)和其他的優(yōu)化,讓JIT編譯器作出更好的選擇。但是準(zhǔn)確地說,只要有必要的時(shí)候,就應(yīng)該使用final,比如你不打算改變的不可變對(duì)象或者原生值,內(nèi)部類應(yīng)用的外部參數(shù)(本質(zhì)上也是一個(gè)不可改變的引用,為了保證語義上的一致,內(nèi)部類不包括外部參數(shù)的引用,因此內(nèi)部類實(shí)現(xiàn)對(duì)外部參數(shù)引用的時(shí)候選擇創(chuàng)建一個(gè)變量引用外部變量但是這個(gè)變量修改之后無法同步到外部變量值中,因此干脆將外部變量設(shè)計(jì)為final)等。但是final關(guān)鍵字并不會(huì)影響應(yīng)用的性能。

          JVM GC 算法

          Java相比CPP而言最大的優(yōu)勢(shì)就是不需要顯示地管理對(duì)象的生命周期,我們?cè)谛枰臅r(shí)候創(chuàng)建對(duì)象,對(duì)象不再被使用的時(shí)候,JVM會(huì)負(fù)責(zé)進(jìn)行GC。

          GC可以簡(jiǎn)單地分成三個(gè)步驟:查找不再使用的對(duì)象,以及釋放這些對(duì)象所管理的內(nèi)存、對(duì)堆的內(nèi)存布局進(jìn)行壓縮整理。完成這些操作的時(shí)候,根據(jù)不同的GC算法會(huì)采用不同的方法。

          C/CPP這類自定義構(gòu)造和析構(gòu)函數(shù)的編程語言,更加利于做內(nèi)存管理,但是也存在更大的風(fēng)險(xiǎn)。性能、安全性、便利性之間總是要作出取舍,這種取舍在生活中比比皆是。

          在GC過程中存在競(jìng)爭(zhēng),應(yīng)用程序的線程和處理gc的線程,特別是在內(nèi)存整理的時(shí)候,因?yàn)樾枰苿?dòng)內(nèi)存的位置,此時(shí)應(yīng)用程序線程不應(yīng)該訪問到這些內(nèi)存對(duì)象。所有應(yīng)用線程停止運(yùn)行被稱為時(shí)空停頓(Stop The World)這是對(duì)應(yīng)用的性能影響最大的一種操作。在進(jìn)行GC的時(shí)候,減少這種停頓是最為關(guān)鍵的考量要素。

          GC roots

          一個(gè)對(duì)象被使用的判斷依據(jù)是從GC Roots 出發(fā),分析是否可以達(dá)到該對(duì)象,可以作為GC roots的引用點(diǎn)有

          1. 方法區(qū)中靜態(tài)變量和常量引用的對(duì)象

          2. 活動(dòng)線程

          3. 本地方法棧中的引用

          當(dāng)一個(gè)對(duì)象到GC roots沒有任何引用鏈的時(shí)候(或者說GC roots到這個(gè)對(duì)象不可達(dá))則說明這個(gè)對(duì)象是不可用的。

          GC 分代

          Java的GC對(duì)象主要是針對(duì)堆進(jìn)行,大部分情況下JVM在堆上為應(yīng)用分配內(nèi)存對(duì)象,大部分的對(duì)象存活的時(shí)間非常短,少數(shù)對(duì)象會(huì)長期使用,針對(duì)這種情況,所有的GC收集器都采用了同一種方式,將堆分成不同的代(Generation),分別為老年代(Old Generation)和新生代(Yound Generation),新生代又進(jìn)一步分為Eden空間和Survivor空間。

          對(duì)象首先在新生代中分配,當(dāng)新生代填滿的時(shí)候,垃圾收集器會(huì)暫停所有的應(yīng)用線程,回收新生代空間。不再使用的對(duì)象會(huì)被回收,仍然使用的對(duì)象會(huì)被移動(dòng)到其他地方。這種操作成為Minor GC,在清理新生代的時(shí)候,都會(huì) stop the world 不過這個(gè)時(shí)間通常都很短。

          這種設(shè)計(jì)意味著,新生代只是堆的一部分,先比處理整個(gè)堆處理新生代的速度回更快,應(yīng)用停頓的事件會(huì)更短,但是這也會(huì)導(dǎo)致更加頻繁地發(fā)生停頓。同時(shí),對(duì)象分配在Eden空間中,GC的時(shí)候,新生代空間會(huì)被清空,Eden空間中的對(duì)象要么被移走,或者被回收,所有的存活對(duì)象要么被移動(dòng)到另一個(gè)Survivor空間要么被移動(dòng)到老年代,相當(dāng)于對(duì)新生代空間自動(dòng)進(jìn)行了一次壓縮整理。

          對(duì)象不斷移動(dòng)到老年代,當(dāng)老年代也填滿的時(shí)候,就需要對(duì)老年代進(jìn)行GC,這是不同算法最大的不同。簡(jiǎn)單的算法會(huì)暫停所有的應(yīng)用線程,對(duì)堆空間進(jìn)行整理,成為Full GC,這會(huì)導(dǎo)致長時(shí)間的停頓。CMS和G1等算法,則可以在線程運(yùn)行的同時(shí)找到不再使用的對(duì)象,將stop the world的可能性降低,這些算法也稱為Concurrent GC算法或者低停頓GC算法

          在一些定義中還存著Major GC,表示對(duì)老年代的GC,而Full GC則表示對(duì)永久代/元空間、老年代、新生代的全局性GC。但是在我看來Minor GC和Major GC、Full GC的定義其實(shí)不是很重要,更加應(yīng)該關(guān)注的是這些GC會(huì)帶來的停頓以及回收的內(nèi)容,在這個(gè)定義中Full GC和Major GC并沒有很大的區(qū)別,都會(huì)帶來較長時(shí)間的停頓,并對(duì)老年代進(jìn)行GC。通過GC是在JVM需要的時(shí)候觸發(fā):新生代用盡的時(shí)候出發(fā)Minor GC、老年代用盡的時(shí)候出發(fā)Full GC、或者堆空間即將填滿的時(shí)候出發(fā)Concurrent 垃圾收集(如果選擇CMS或者G1收集器)。使用System.gc()會(huì)觸發(fā)Full GC,但是行為不定,只是增加了JVM Full GC的概率。

          GC算法

          1. Serial垃圾收集器

          無論是Minor GC還是Full GC都會(huì)暫停應(yīng)用線程,進(jìn)行Full GC的時(shí)候還會(huì)對(duì)老年代的對(duì)象進(jìn)行壓縮整理。通過-XX:+UseSerialGC標(biāo)志啟動(dòng)。也是client模式的JVM的默認(rèn)GC算法

          1. Throughout垃圾收集器

          使用對(duì)縣城回收新生代空間,MinorGC的速度要比Serial收集器快。對(duì)于老年代也采用多線程GC,可以通過-XX:+UseParallelOldGC顯示開啟,JDK7u4之后已經(jīng)顯示開啟。由于采用多線程GC,因此也被稱為Parallel收集器,是server模式的JVM的默認(rèn)GC算法。可以通過-XX:+UseParallelGC來顯示開啟。

          1. CMS收集器

          設(shè)計(jì)的初衷是為了消除上面兩種收集器在Full GC周期中的長時(shí)間停頓,CMS收集器在Minor GC的時(shí)候回暫停所有的應(yīng)用線程,并以多線程的方式進(jìn)行GC,但使用的算法和Throughout算法不同,使用-XX:+UseParNewGC來啟用。CMS收集器在GC的時(shí)候不再暫停應(yīng)用線程,而是分成幾個(gè)不同的周期使用若干后臺(tái)線程定期對(duì)老年代進(jìn)行掃描,及時(shí)回收其中不再使用的對(duì)象,應(yīng)用線程只是在Minor GC以及后臺(tái)線程掃描老年代的時(shí)候發(fā)生極短的停頓。

          CMS收集付出的代價(jià)就是更高的CPU使用,必須有足夠的CPU資源英語運(yùn)行后臺(tái)的GC線程以及引用線程。此外后臺(tái)線程不會(huì)再進(jìn)行任何壓縮整理的工作,這是的堆會(huì)組件碎片化。如果CMS的后臺(tái)無法獲得足夠的CPU資源、或者碎片化過于嚴(yán)重而無法獲取連續(xù)的空間分配對(duì)象,CMS會(huì)退化到Serial收集器模式,之后恢復(fù)。通過-XX:+UseConcMarkSweepGC-XX:+UseParNewGC來啟用CMS收集器

          1. G1垃圾收集器

          設(shè)計(jì)的初衷是為了盡量縮短處理大堆產(chǎn)生的停頓,它將堆劃分成若干個(gè)區(qū)域Region,G1收集器也屬于分代收集器,新生代的GC仍然采用暫停所有應(yīng)用線程,使用多線程,將存活對(duì)象移動(dòng)到老年代或者Survivor空間的做法。

          G1收集器也是Concurrent收集器,和CMS收集器的最大不同,在于老年代被劃分到不同的區(qū)域,G1收集器通過將對(duì)象從一個(gè)區(qū)域復(fù)制到另一個(gè)區(qū)域,完成對(duì)象的清理工作,也就是在正常的處理過程中,G1收集器實(shí)現(xiàn)了堆的壓縮整理,因此G1更加不容易出現(xiàn)碎片化的情況。

          同樣的G1收集器也需要消耗額外的CPU資源,通過-XX:+UseG1GC來啟動(dòng)

          選擇GC算法

          1. Serial收集器適用于應(yīng)用程序的內(nèi)存使用少于100MB的場(chǎng)景,但是大多數(shù)的情況下,只能作為其他GC收集器的替補(bǔ)選項(xiàng)。

          2. Throughput收集器處理批量任務(wù)的時(shí)候,能夠最大限度利用CPU的處理能力(全力計(jì)算、全力GC)通常能夠獲得更好的性能一般指吞吐量。但是如果批量任務(wù)并沒有使用機(jī)器上所有的可用CPU資源,那么使用Concurrent收集器往往能夠取得更好的性能。

          3. 如果CPU計(jì)算資源不足,或者無法獲取連續(xù)空間容納對(duì)象的時(shí)候,采用CMS收集器的時(shí)候,可能會(huì)出現(xiàn)并發(fā)模式失效(Concurrent Mode Failure),這會(huì)使JVM退化成為單線程的Full GC模式,從而使得CPU利用率下降,導(dǎo)致長時(shí)間的Full GC停頓

          4. 相比Throughout收集器,CMS在99%響應(yīng)時(shí)間上有巨大的優(yōu)勢(shì),顯然CMS介紹了Full GC的次數(shù),從而減少了由于Full GC導(dǎo)致長時(shí)間停頓的次數(shù)。同時(shí)在90%響應(yīng)時(shí)間上,Throughput則可能優(yōu)于CMS收集器,一般而言在CPU資源充足的情況下,CMS的響應(yīng)時(shí)間要由于CMS收集器。

          5. 如果選擇Concurrent收集器,一般情況下堆空間小于4G選擇CMS收集器的性能會(huì)比G1更好,因?yàn)镃MS的算法相比G1更加簡(jiǎn)單,一般而言在堆較少的情況下運(yùn)行速度回更快,而當(dāng)堆較大的時(shí)候,G1收集可以分割工作區(qū),不必像CMS一樣掃描完整個(gè)老年代空間,通常比CMS收集器表現(xiàn)更好。同時(shí)G1收集器可以并行對(duì)老年代進(jìn)行壓縮整理更加不容易出現(xiàn)碎片化空間,從而降低Full GC出現(xiàn)的幾率

          6. 無論是CMS還是G1都仍然可能出現(xiàn)并發(fā)模式失效的問題

          7. Throughout和CMS算法存在的事件比較差,經(jīng)過了大量的優(yōu)化,G1收集器存在的事件較短,實(shí)際上大部分Concurrent選擇的時(shí)候還是選擇CMS較多。未來G1應(yīng)該會(huì)作為一個(gè)選項(xiàng)

          GC調(diào)優(yōu)

          堆空間調(diào)優(yōu)

          對(duì)JVM的GC進(jìn)行調(diào)優(yōu),首先考慮的就是調(diào)整堆的大小。堆的大小不是越大越好,如果超過了機(jī)器的物理內(nèi)存,操作系統(tǒng)會(huì)使用swap空間存儲(chǔ)原本應(yīng)該存儲(chǔ)在內(nèi)存中的對(duì)象,操作磁盤會(huì)導(dǎo)致嚴(yán)重的性能問題,特別是在進(jìn)行Full GC或者Concurrent收集器后臺(tái)回收堆的時(shí)候。較大的堆可以降低GC的頻率,但是同時(shí)每次GC所需要的時(shí)間也會(huì)相應(yīng)增加。

          堆的調(diào)整是JVM調(diào)優(yōu)的核心參數(shù),通過堆的大小調(diào)整可以影響GC算法的行為。堆大小由兩個(gè)參數(shù)值控制:分別是初始值(-Xms N)和最大值(-Xmx N),JVM的目標(biāo)是找到一個(gè)合理的堆值,因此會(huì)自動(dòng)在這兩個(gè)值之間進(jìn)行調(diào)整,一般都是根據(jù)GC消耗的時(shí)間來決定的。

          對(duì)于Xmx的一個(gè)經(jīng)驗(yàn)值是,在完成一次Full GC之后,應(yīng)該釋放出70%的空間,可以使用jconsole連接程序進(jìn)行強(qiáng)制Full GC,觀察對(duì)應(yīng)的值。

          另一個(gè)經(jīng)驗(yàn)做法是,如果確切了解應(yīng)用程序需要多大的堆,那么可以將堆的初始值和最大值直接設(shè)置成為對(duì)應(yīng)的值,可以稍微提高GC的運(yùn)行效率。-Xms4096m -Xmx4096m

          XmsXmx對(duì)應(yīng)為堆的堆空間的committed memorymax memory,同時(shí)存在used memory。used memory表示已經(jīng)使用的內(nèi)存,也就是實(shí)際堆中占有的內(nèi)存空間;committed memory代表操作系統(tǒng)承諾可以被JVM使用的內(nèi)存大小;max memeory最大可以被JVM使用的內(nèi)存,如果這個(gè)值超過了可用內(nèi)存的最大值,那么分配可能失敗。

          代空間調(diào)整

          確認(rèn)了堆的大小之后,就需要決定多少空間分配給新生代多少分配給永久代。這里也存在權(quán)衡,如果新生代比較大,Minor GC發(fā)生的頻率就比較低,但是同時(shí)老年代就會(huì)比較小,容易填滿而導(dǎo)致觸發(fā)Full GC。

          -XX:NewRatio=N?設(shè)置新生代和老年代的空間占用比率,默認(rèn)為2,新生代空間大小則等于?堆空間大小 / (1 + NewRatio)也就是默認(rèn) 1/3 的堆大小為新生代空間

          -XX:NewSize=N?設(shè)置新生代空間的初始大小,優(yōu)先級(jí)高于NewRatio

          -XX:MaxNewSize=N?設(shè)置新生代空間的最大大小

          -XmnN?將NewSizeMaxNewSize設(shè)置為同一個(gè)值,如果使用固定的堆大小,使用這個(gè)值設(shè)置即可。如果動(dòng)態(tài)增長則建議使用NewRatio比例設(shè)置的方式

          永久代和元空間的調(diào)整

          JVM載入類的時(shí)候,需要記錄這些類的元數(shù)據(jù),在Java7里,這部分空間成為永久代Permgen,在Java8中,成為元空間Metaspace。這些空間和Heap對(duì)應(yīng)也成為no-heap。

          元數(shù)據(jù),存儲(chǔ)的是Java的字節(jié)碼加載到JVM之后,運(yùn)行過程中的數(shù)據(jù),包含符號(hào)、字面變量、連接符等等一些類似“書簽”的信息,這些信息只對(duì)編譯器或者JVM的運(yùn)行時(shí)有用。而XXX.class也是一個(gè)對(duì)象,作為一個(gè)實(shí)例存儲(chǔ)在heap中。在JDK6以及之前,永久代還保持著字符常量池,JDK7將String的分配和常量池移到Heap中,因?yàn)镻ermgen很難被拓展,存儲(chǔ)其中的數(shù)據(jù)需要根據(jù)類的數(shù)量、常量池池String.intern,方法大小等進(jìn)行評(píng)估,而這些很難進(jìn)行,并且對(duì)Permgen的GC回收比較難奏效,字符池 String Pool 移到Heap中,可以統(tǒng)一GC回收的模型。JDK8的時(shí)候徹底移除了永久代,而引入了Metaspace,在JDK7中還保留的和類數(shù)據(jù)無關(guān)的雜項(xiàng)對(duì)象(miscellaneous object),也被移到了普通的堆空間中。這么做的核心原因還是Permgen的優(yōu)化困難導(dǎo)致的,每一種GC算法都需要特定的代碼處理Permgen中的元數(shù)據(jù)。從Permgen中分離元數(shù)據(jù),不進(jìn)可以對(duì)元數(shù)據(jù)進(jìn)行統(tǒng)一管理,也可以簡(jiǎn)化GC代碼對(duì)這部分空間的GC操作。

          評(píng)估永久代/元空間的大小,和程序使用的類的數(shù)量成比例關(guān)系,如果應(yīng)用程序越復(fù)雜,使用對(duì)象越多,則所需要的空間越大。JDK8中,元空間默認(rèn)使用盡可能大的空間,因?yàn)槭褂玫氖遣皇嵌褍?nèi)存,而是直接使用原生內(nèi)存,因此大小不受限制。JDK7以及之前則需要考慮Permgen的大小。

          -XX:PermSize=N
          --XX:MaxPermSize=N
          --XX:MetaspaceSize=N
          --XX:MaxMetaspaceSize=N

          可以調(diào)整對(duì)應(yīng)的永久代或者元空間的初始大小和最大值

          調(diào)整這些空間會(huì)導(dǎo)致Full GC,如果系統(tǒng)啟動(dòng)的時(shí)候頻繁Full GC導(dǎo)致啟動(dòng)過慢,可以考慮使用一個(gè)較大的初始值。如果使用JDK7還需要針對(duì)程序規(guī)模設(shè)置一個(gè)合適的MaxPermSize值。

          這篇區(qū)域中依然存在垃圾回收操作,特別是在應(yīng)用服務(wù)器中,使用不同的類加載器加載類,當(dāng)一個(gè)類加載器不再被引用后,則它定義的任何類也不會(huì)再被引用,可以進(jìn)行GC回收。在服務(wù)器運(yùn)行的周期內(nèi),永久代或者元空間被新的類充滿填滿,老的類的元數(shù)據(jù)等待被回收。因此如果出現(xiàn)類加載器泄漏的情況可能導(dǎo)致此片區(qū)域被耗盡的情況。

          jcmd?pid?GC.class_stats

          可以輸出類加載器相關(guān)的信息

          并發(fā)控制

          并發(fā)控制,本質(zhì)上是要在盡量利用多核CPU計(jì)算力的基礎(chǔ)上,盡量減少線程上下文的切換,從而將計(jì)算資源集中在真正有意義的計(jì)算上。

          -XX:ParallelGCThreads=N參數(shù)用于控制GC在并發(fā)啟動(dòng)的線程數(shù),包括

          1. -XX:+UseParallelGC收集新生代空間

          2. -XX:+UseParallelOldGC收集老年代空間

          3. -XX:+UseParallelNewGC收集新生代空間

          4. -XX:+UseG1GC收集新生代空間

          5. CMS收集器的”空間停頓”階段(并非Full GC,初始標(biāo)記和重新標(biāo)記階段)

          6. G1收集器的”空間停頓”階段(并非Full GC)

          這些GC操作會(huì)暫停應(yīng)用線程,JVM為了加快執(zhí)行速度,會(huì)使用盡量多的CPU資源,默認(rèn)情況下JVM會(huì)在每個(gè)CPU上運(yùn)行一個(gè)線程,最多8個(gè),達(dá)到上限后每超出8/5個(gè)CPU個(gè)CPU啟動(dòng)一個(gè)新的線程。

          優(yōu)化的原則是,盡量不讓GC線程發(fā)生爭(zhēng)搶Throughput特別是在核心比較多的機(jī)器上運(yùn)行多個(gè)實(shí)例的JVM,比較容易出現(xiàn)啟動(dòng)的GC線程過多的情況這時(shí)候應(yīng)該適當(dāng)減少GC的線程數(shù)量。

          自適應(yīng)調(diào)整

          默認(rèn)JVM會(huì)根據(jù)我們?cè)O(shè)置的參數(shù)和運(yùn)行時(shí)的性能歷史記錄,自行調(diào)整參數(shù),尋找優(yōu)化性能的機(jī)會(huì)。

          使用-XX:-UseAdaptiveSizePolicy標(biāo)志可以在全局范圍內(nèi)關(guān)閉自適應(yīng)調(diào)整功能(默認(rèn)是開啟的)。如果堆容量初始值和最大值設(shè)置成相同,同時(shí)新生代的初始值和最大值都設(shè)置成相同大小,自適應(yīng)調(diào)整功能會(huì)自動(dòng)關(guān)閉。

          如果想知道JVM的空間是如何進(jìn)行調(diào)整的,可以使用-XX:+PrintAdaptiveSizePolicy標(biāo)志,打印出詳細(xì)的調(diào)整信息

          監(jiān)控工具

          1. -verbose:gc或者-XX:+PrintGC能創(chuàng)建基本的GC日志

          2. -XX:+PrintGCDetails會(huì)創(chuàng)建詳細(xì)的GC日志

          3. -XX:+PrintGCTimeStamps打印出GC發(fā)生的時(shí)間

          4. -Xloggc:filename修改日志輸出到某個(gè)文件

          5. -XX:+UseGCLogfileRotation?-XX:NumberOfGCLogfiles=N?-XX:GCLogfileSize=N,-XX:+UseGCLogfileRotation控制日志文件的循環(huán),-XX:NumberOfGCLogfiles=N控制數(shù)量,-XX:GCLogfileSize=N控制文件的大小,默認(rèn)不做限制

          jstat?-gcutil?process_id?1000

          可以統(tǒng)計(jì)GC的一些歷史信息

          GC日志的開銷很小,一般情況下都建議在生產(chǎn)環(huán)境中添加,JVM調(diào)優(yōu)一般都需要根據(jù)這些GC日志來進(jìn)行。

          GC算法

          Throughput收集器

          Throughput收集器,會(huì)進(jìn)行兩種操作,Minor GC和Full GC。所有的調(diào)優(yōu)都圍繞著停頓時(shí)間進(jìn)行,主要就是堆的大小、新生代和老年代的大小之間的平衡。

          1. 時(shí)間和空間,也就是GC時(shí)間和內(nèi)存空間的取舍

          2. GC時(shí)間的分布,增大堆減少Full GC的頻率,但是增加了其時(shí)間,影響平均響應(yīng)時(shí)間。同樣新生代分配更多的空間,可以縮短Full GC的停頓時(shí)間,但是增加老年代GC的頻率。

          如果使用JVM自動(dòng)調(diào)整的策略,我們可以設(shè)置一些性能指標(biāo)-XX:MaxGCPauseMillis=N?和-XX:GCTimeRatio=N,XX:MaxGCPauseMillis=N標(biāo)志用于設(shè)定應(yīng)用可承受的最大停頓時(shí)間,如果這個(gè)標(biāo)志設(shè)置得非常小,那么應(yīng)用的老年代最終會(huì)非常小,從而頻繁觸發(fā)Full GC,默認(rèn)情況下我們不對(duì)這個(gè)值進(jìn)行設(shè)置,但是要注意這個(gè)參數(shù)優(yōu)先級(jí)最高,一旦設(shè)置了這個(gè)值,JVM就會(huì)對(duì)新生代和老年代的大小進(jìn)行不斷調(diào)整直到滿足停頓目標(biāo)。-XX:GCTimeRatio=N標(biāo)志設(shè)置你喜歡程序在GC上花費(fèi)的時(shí)間,默認(rèn)值為99,其數(shù)值計(jì)算的方式throughputGoal = 1 - 1 / ( 1+ GCTimeRatio )?也就是默認(rèn)程序運(yùn)行時(shí)間占用99%,1%的時(shí)間用在GC上。對(duì)于GC占用時(shí)間,一般在3%到6%之間,表現(xiàn)就非常好,對(duì)應(yīng)的-XX:GCTimeRatio=19,程序也會(huì)盡量?jī)?yōu)化達(dá)到這個(gè)參數(shù),如果采用自動(dòng)調(diào)整,那么設(shè)置這個(gè)標(biāo)識(shí)是一個(gè)很好的選擇。

          CMS收集器

          CMS收集過程

          1. 對(duì)新生代進(jìn)行Minor GC

          2. 在應(yīng)用運(yùn)行過程中,對(duì)老年代進(jìn)行并發(fā)GC

          3. 出現(xiàn)并發(fā)模式失效的時(shí)候,觸發(fā)Full GC

          JVM會(huì)在堆的使用達(dá)到某個(gè)程度的時(shí)候,啟動(dòng)并發(fā)回收

          1. 初始標(biāo)記[CMS-initial-mark](stw)

           [GC [1 CMS-initial-mark: 2905437K(4096000K)] 3134625K(5916480K), 0.2551680 secs] [Times: user=0.26 sys=0.00, real=0.25 secs]

          主要任務(wù)是找到堆中所有的垃圾回收根節(jié)點(diǎn)對(duì)象

          1. 并發(fā)標(biāo)記[CMS-concurrent-mark]

          [CMS-concurrent-mark-start]
          [CMS-concurrent-mark: 2.787/3.329 secs] [Times: user=12.12 sys=0.64, real=3.33 secs]

          初始標(biāo)記的對(duì)象,并發(fā)標(biāo)記對(duì)象,不會(huì)對(duì)堆的使用情況產(chǎn)生實(shí)質(zhì)性的改變

          1. 并發(fā)預(yù)刪除[CMS-concurrent-preclean]

          [CMS-concurrent-preclean-start]
          [CMS-concurrent-preclean: 0.342/0.477 secs] [Times: user=1.79 sys=0.10, real=0.48 secs]

          并發(fā)預(yù)清理完成的工作和重新標(biāo)記類似,主要是在上一個(gè)并發(fā)標(biāo)記和應(yīng)用線程是并發(fā)執(zhí)行的,因此有些對(duì)象狀態(tài)在標(biāo)記后會(huì)發(fā)生變化,這個(gè)階段主要發(fā)現(xiàn)從新生代晉升的對(duì)象、分配到老年代的對(duì)象和在并發(fā)標(biāo)記階段被修改的對(duì)象。

          1. 重新標(biāo)記(stw)

          [CMS-concurrent-abortable-preclean-start]
          [CMS-concurrent-abortable-preclean: 0.920/1.083 secs] [Times: user=4.06 sys=0.20, real=1.08 secs]
          [GC[YG occupancy: 777901 K (1820480 K)]
          [Rescan (parallel) , 0.1361120 secs]
          [weak refs processing, 0.0005370 secs] [scrub string table, 0.0044130 secs]
          [1 CMS-remark: 3034451K(4096000K)] 3812352K(5916480K), 0.1412750 secs] [Times: user=0.54 sys=0.00, real=0.14 secs]

          重新標(biāo)記覆蓋多個(gè)階段,是CMS中比較復(fù)雜的一個(gè)步驟,

          • 并發(fā)可中斷的重標(biāo)記[CMS-concurrent-abortable-preclean]

          在進(jìn)行重新標(biāo)記的時(shí)候,需要掃描所有的新生代和老年代,這個(gè)階段會(huì)很慢,為了能夠加速這個(gè)階段,能想到的就是在之前進(jìn)行一次 minor gc,從而減少新生代的掃描數(shù)量。但是minor gc 是會(huì)stw,為了避免minor gc 一次暫停,重新標(biāo)記又一次暫停這樣的連續(xù)暫停。這個(gè)minor gc 是可以被提前放棄的,也就是如果預(yù)計(jì)這個(gè)minor gc 需要 4 s 那么可能在 2s 之后就放棄了這次回收。這個(gè)數(shù)量可以通過CMS 有兩個(gè)參數(shù):CMSScheduleRemarkEdenSizeThreshold、CMSScheduleRemarkEdenPenetration,默認(rèn)值分別是2M、50%。兩個(gè)參數(shù)組合起來的意思是預(yù)清理后,eden空間使用超過2M時(shí)啟動(dòng)可中斷的并發(fā)預(yù)清理(CMS-concurrent-abortable-preclean),直到eden空間使用率達(dá)到50%時(shí)中斷,進(jìn)入remark階段。

          • Rescan

          Rescan 要重新掃描 新生代和老年代(因?yàn)樾枰_定對(duì)象是真的存活,這個(gè)階段可能會(huì)非常慢,為了改進(jìn)這個(gè)問題,產(chǎn)生了前面的可中斷預(yù)清除,在這個(gè)階段才是完成重標(biāo)記,重標(biāo)記是要暫停應(yīng)用程序的,由于前面的并發(fā)標(biāo)記,并且使用可中斷的預(yù)清理最大化減少了需要掃描的數(shù)量,在此處消耗的時(shí)間相對(duì)比比較少。

          1. 并發(fā)清除

          [CMS-concurrent-sweep-start]
          [CMS-concurrent-sweep: 5.656/6.900 secs] [Times: user=25.88 sys=1.28, real=6.90 secs]

          重新喚醒應(yīng)用線程,并發(fā)將標(biāo)記為需要清除的對(duì)象清除

          1. 并發(fā)重置

            [CMS-concurrent-reset-start]
          [CMS-concurrent-reset: 0.010/0.010 secs] [Times: user=0.04 sys=0.00, real=0.01 secs]

          完成CMS垃圾回收的周期,老年代空間中沒有被應(yīng)用的對(duì)象被回收,同時(shí)清理CMS內(nèi)部狀態(tài)

          從這邊可以看到CMS收集的的并發(fā)GC過程并不完全等于Full GC,只有包含了Stop the world的階段才包含在Full GC中,一次CMS GC過程會(huì)包括兩次Full GC統(tǒng)計(jì)次數(shù),因此如果采用CMS并發(fā)收集,除了關(guān)注Full GC的統(tǒng)計(jì)次數(shù),更應(yīng)該關(guān)注Full GC的運(yùn)行時(shí)間更加有參考意義

          在這邊額外關(guān)注一下 concurrent mode failure

          1. 老年代沒有足夠的大小容納新生代提升上來的數(shù)據(jù)時(shí),就會(huì)觸發(fā)并發(fā)模式失效,從而退化成單線程的Serial GC進(jìn)行Full GC,這就是一些情況下出現(xiàn)CPU 100%的原因

           [GC 267.006: [ParNew: 629120K->629120K(629120K), 0.0000200 secs] 
          267.006: [CMS267.350: [CMS-concurrent-mark: 2.683/2.804 secs]
          [Times: user=4.81 sys=0.02, real=2.80 secs]
          (concurrent mode failure):
          1378132K->1366755K(1398144K), 5.6213320 secs]
          2007252K->1366755K(2027264K),
          [CMS Perm : 57231K->57222K(95548K)], 5.6215150 secs]
          [Times: user=5.63 sys=0.00, real=5.62 secs]
          1. 老年代中有足夠的空間,但是由于空閑空間不連續(xù),而導(dǎo)致晉升失敗,也同樣會(huì)引起一次Full GC 進(jìn)行壓縮整理操作

           [GC 6043.903: 
          [ParNew (promotion failed): 614254K->629120K(629120K), 0.1619839 secs]
          6044.217: [CMS: 1342523K->1336533K(2027264K), 30.7884210 secs]
          2004251K->1336533K(1398144K),
          [CMS Perm : 57231K->57231K(95548K)], 28.1361340 secs]
          [Times: user=28.13 sys=0.38, real=28.13 secs]
          [Full GC 279.803:
          [CMS: 88569K->68870K(1398144K), 0.6714090 secs]
          558070K->68870K(2027264K),
          [CMS Perm : 81919K->77654K(81920K)],
          0.6716570 secs]

          默認(rèn)情況下CMS是不對(duì)Metaspace或永久代進(jìn)行資源回收的,只有Full gc的時(shí)候會(huì)進(jìn)行這個(gè)操作,因此當(dāng)出現(xiàn)并發(fā)模式失敗的時(shí)候,也會(huì)對(duì)該區(qū)域進(jìn)行GC,所有沒有被引用的類都會(huì)被回收

          CMS調(diào)優(yōu)

          從上面的GC日志中可以看到,當(dāng)并發(fā)模式失敗的時(shí)候,付出的GC時(shí)間代價(jià)是最大的,而并發(fā)模式失效往往是因?yàn)镃MS不能以足夠快的速度清理老年代,默認(rèn)情況下當(dāng)老年代空間占用達(dá)到70%的時(shí)候,并發(fā)回收就開始,在剩下的30%空間用盡之前,如果CMS無法及時(shí)回收空間,那么就會(huì)出現(xiàn)并發(fā)失效。

          CMS優(yōu)化的核心目標(biāo)就是減少并發(fā)失效,主要有三種做法

          1. 增大老年代空間

          2. 以更高的頻率運(yùn)行g(shù)c

          3. 使用更多的后臺(tái)回收線程

          CMS收集器只有在Full GC的時(shí)候,才會(huì)調(diào)整堆和代的大小

          第一個(gè)選項(xiàng)的平衡之前已經(jīng)談到,這邊重點(diǎn)描述后兩個(gè)做法。

          1. 提高頻率 如果在老年代占用達(dá)到60%的時(shí)候就啟動(dòng)并發(fā)回收,無疑完成GC的幾率更大,通過-XX:CMSInitiatingOccupancyFraction=N-XX:+UseCMSInitiatingOccupancyOnly,前者確定比例,后者確定是否使用這個(gè)比例,如果-XX:+UseCMSInitiatingOccupancyOnly開啟,默認(rèn)的比例為70%,如果不啟動(dòng),那么CMS會(huì)根據(jù)更加復(fù)雜的算法來判斷何時(shí)啟動(dòng)并發(fā)回收。一般經(jīng)驗(yàn)來說,要設(shè)置這個(gè)比例,需要觀察上一次并發(fā)失效的時(shí)候,老年代的占用情況,將比例設(shè)置得比這個(gè)失敗值小。同時(shí)要要考慮到頻繁進(jìn)行CMS并發(fā)GC帶來的CPU壓力,以及評(píng)分的CMS并發(fā)GC帶來的停頓影響

          2. 調(diào)整CMS后臺(tái)線程數(shù)量?-XX:ConGCThreads=N?增加后臺(tái)線程的數(shù)目,同樣調(diào)整這個(gè)參數(shù)要考慮可用的CPU,如果設(shè)置偏大,可能會(huì)占用原來應(yīng)用線程的CPU,造成應(yīng)用程序的停頓,如果沒有頻繁早于并發(fā)模式失敗,在CPU數(shù)量較大的機(jī)器上可以適當(dāng)減少這個(gè)值,以騰出更多的CPU給應(yīng)用線程使用。默認(rèn)值為 (3 + ParallelGCThreads) / 4

          3. 提高效率 前面提到在重新標(biāo)記中會(huì)觸發(fā)一次可中斷的Minor GC目的是減少重新標(biāo)記過程中要掃描的內(nèi)存數(shù)量。因?yàn)檫@個(gè)步驟要暫停應(yīng)用線程,所以如果觀察到重新標(biāo)記的GC停頓過長,可以通過-XX:+CMSScavengeBeforeRemark,強(qiáng)制在重標(biāo)記之前進(jìn)行一次Minor GC,來減少GC的停頓時(shí)長。

          4. 提高remark的執(zhí)行速度 如果發(fā)現(xiàn)remakr的執(zhí)行速度較慢,那么可以開啟-XX:+CMSScavengeBeforeRemark在remark開始之前,強(qiáng)制執(zhí)行一次minor gc

          默認(rèn)情況下JDK7的CMS收集器不會(huì)對(duì)永久代進(jìn)行回收,如果永久代空間用盡,會(huì)發(fā)起一次Full GC來進(jìn)行回收??梢酝ㄟ^-XX:+CMSPermGenSweepingEnabled標(biāo)志來開啟,開啟之后,會(huì)通過一組后臺(tái)線程并發(fā)地回收永久代。-XX:CMSinitiatingPermOccupancyFraction=N參數(shù)可以設(shè)置永久代占用比例達(dá)到閾值后啟動(dòng)回收線程,默認(rèn)值為80%。如果要回收不被使用的類,還需要-XX:+CMSClassUnloadingEnabled開啟。在JDK8中,這個(gè)參數(shù)是默認(rèn)開啟的。

          CMS還有一個(gè)增量式的CMS收集器,通過-XX:+CMSIncreamentalMode開啟,其最大的好處是后臺(tái)線程會(huì)間歇性停頓,讓出一部分CPU給應(yīng)用程序線程運(yùn)行,使得在有限CPU資源的機(jī)器上也可以運(yùn)行低延遲的GC收集器。但是現(xiàn)在普遍是多核機(jī)器為主流,如果CPU資源確實(shí)非常有限,可以考慮使用G1收集器。

          G1收集器

          G1收集器的特點(diǎn)是堆進(jìn)行分區(qū)(Region),分區(qū)既可以屬于老年代,又可以屬于新生代,在需要的時(shí)候,G1收集器會(huì)強(qiáng)制指定空分區(qū)用于任何需要的代,默認(rèn)一個(gè)堆分成2048個(gè)分區(qū)。同一個(gè)代的分區(qū)不需要保持物理連續(xù),只需要保留邏輯關(guān)聯(lián)。這樣設(shè)計(jì)的初衷是可以讓G1收集器在回收老年代的時(shí)候,優(yōu)先回收垃圾較多的分區(qū),也是其名稱的由來。但是在回收新生代的時(shí)候,還是和其他算法一樣,整個(gè)新生代要么被回收或者被晉升。新生代也采用分區(qū)的原因是因?yàn)榭梢酝ㄟ^預(yù)定義的分區(qū)來調(diào)整代的大小。

          G1收集器的收集活動(dòng)包括4種操作

          1. 新生代GC

          [GC?pause?(young), 0.23094400 secs] ...?[Eden: 1286M(1286M)->0B(1212M)?Survivors: 78M->152M Heap: 1454M(4096M)->242M(4096M)]?[Times:?user=0.85 sys=0.05, real=0.23 secs]

          Eden空間耗盡會(huì)觸發(fā)G1收集器進(jìn)行新生代進(jìn)行GC,Eden空間被清空,一部分晉升到Survivor空間,一部分移到老年代。

          1. 后臺(tái)收集,并發(fā)周期[concurrent G1 cycle]

            在并發(fā)周期中,可以發(fā)生一次或者多次新生代的GC,過程中Eden空間的分區(qū),可以填充新分配的對(duì)象。其次,一些老年代的分歧會(huì)被標(biāo)記,這就是標(biāo)記周期(marking cycle)找出的包含最多垃圾的分區(qū)。標(biāo)記周期不會(huì)實(shí)際是否老年代中的對(duì)象,而僅僅是鎖定了那些垃圾最多的分區(qū),這些分區(qū)中的垃圾數(shù)據(jù)會(huì)在后續(xù)被釋放。下面簡(jiǎn)述整個(gè)標(biāo)記的過程 * 初始標(biāo)記,這個(gè)階段會(huì)暫停所有的應(yīng)用線程

             [GC pause (young) (initial-mark), 0.27767100 secs] 
            [Eden: 1220M(1220M)->0B(1220M)
            Survivors: 144M->144M Heap: 3242M(4096M)->2093M(4096M)]
            [Times: user=1.02 sys=0.04, real=0.28 secs]

            可以從日志中看到G1收集器重用了新生代GC周期,來完成暫停應(yīng)用線程這個(gè)操作,這會(huì)稍微增加新生代周期的CPU開銷,但是總體還是有利的。* 掃描根分區(qū)(root region)

            [GC concurrent-root-region-scan-start] 
            [GC concurrent-root-region-scan-end, 0.5890230]

            掃描過程,可以并發(fā)執(zhí)行,不需要暫停應(yīng)用線程。不過這個(gè)過程為了避免新生代晉升到老年代,不允許進(jìn)行新生代GC,如果新生代剛好用盡,那么新生代GC會(huì)暫停所有的應(yīng)用線程,等待根掃描結(jié)束。?[GC pause (young) 351.093: [GC concurrent-root-region-scan-end, 0.6100090] 351.093: [GC concurrent-mark-start], 0.37559600 secs]?出現(xiàn)這種情況會(huì)使新生代停頓時(shí)間更長,可能需要進(jìn)行優(yōu)化。* 并發(fā)標(biāo)記,這個(gè)階段在后臺(tái)并發(fā)完成,并且是可中斷的,因此可以在這個(gè)過程中發(fā)生新生代GC。

            [GC concurrent-mark-start] ....?[GC?concurrent-mark-end,?9.5225160?sec]
               * 重新標(biāo)記(remarking),會(huì)暫停應(yīng)用線程,但是時(shí)間一般都很短 ````shell [GC remark 120.959: 
            [GC ref-PRC, 0.0000890 secs], 0.0718990 secs]
            [Times: user=0.23 sys=0.01, real=0.08 secs] [GC cleanup 3510M->3434M(4096M), 0.0111040 secs]
            [Times: user=0.04 sys=0.00, real=0.01 secs] ````
            * 并發(fā)清理,該階段回收的內(nèi)存數(shù)量很少,主要是完成待收集分區(qū)的定位,到這邊整個(gè)標(biāo)記過程就已經(jīng)完成了。````shell [GC concurrent-cleanup-start] [GC concurrent-cleanup-end, 0.0004520] ````
          2. 混合式GC (mixed GC) 這個(gè)階段被稱為混合式GC,即同時(shí)回收新生代以及前面后臺(tái)掃描標(biāo)記的分區(qū)中的一部分分區(qū)。在這個(gè)階段,被回收的活躍數(shù)據(jù)會(huì)被移動(dòng)到另一個(gè)分區(qū),相當(dāng)于是進(jìn)行了一次額壓縮和碎片整理。這也是相比CMS收集器較少出現(xiàn)碎片化的原因。

             [GC?pause?(mixed), 0.26161600 secs] ....?[Eden: 1222M(1222M)->0B(1220M)?Survivors: 142M->144M Heap: 3200M(4096M)->1964M(4096M)]?[Times:?user=1.01 sys=0.00, real=0.26 secs]

            混合式GC會(huì)持續(xù)運(yùn)行到幾乎所有被標(biāo)記的分區(qū)都被回收,然后開啟下一次并發(fā)周期

          3. 必要時(shí)的Full GC 和CMS收集器一樣,G1收集器也可能出現(xiàn)并發(fā)模式失效的情況

            • 老年代在標(biāo)記周期完成之前被填滿,G1收集器放棄標(biāo)記周期

              [GC concurrent-mark-start] 
              [Full GC 4095M->1395M(4096M), 6.1963770 secs]
              [Times: user=7.87 sys=0.00, real=6.20 secs]
              [GC concurrent-mark-abort]

              這種失敗意味著需要增大堆空間,或者讓G1收集器更早開始,或者增加后臺(tái)處理的線程數(shù)

          • 晉升失敗,完成了標(biāo)記階段,啟動(dòng)混合回收,但是老年代空間在釋放出足夠的內(nèi)存之前被耗盡,一般表現(xiàn)為混合收集后開始一次Full GC

             2226.224: [GC?pause?(mixed)?2226.440:?[SoftReference, 0 refs, 0.0000060 secs] 2226.441:?[WeakReference, 0 refs, 0.0000020 secs] 2226.441:?[FinalReference, 0 refs, 0.0000010 secs] 2226.441:?[PhantomReference, 0 refs, 0.0000010 secs] 2226.441:?[JNI Weak Reference, 0.0000030 secs]?(to-space exhausted), 0.2390040 secs] ....?[Eden: 0.0B(400.0M)->0.0B(400.0M)?Survivors: 0.0B->0.0B Heap: 2006.4M(2048.0M)->2006.4M(2048.0M)]?[Times:?user=1.70 sys=0.04, real=0.26 secs] 
            2226.510: [Full?GC?(Allocation Failure)?2227.519:?[SoftReference, 4329 refs, 0.0005520 secs] 2227.520:?[WeakReference, 12646 refs, 0.0010510 secs] 2227.521:?[FinalReference, 7538 refs, 0.0005660 secs] 2227.521:?[PhantomReference, 168 refs, 0.0000120 secs] 2227.521:?[JNI Weak Reference, 0.0000020 secs] 2006M->907M(2048M), 4.1615450 secs]?[Times:?user=6.76 sys=0.01, real=4.16 secs]

            此時(shí)需要更快完成GC,或者提前開始GC

          • 疏散失敗,在進(jìn)行新生代GC的時(shí)候,Survivor空間和老年代中沒有足夠的空間容納幸存對(duì)象,一般這種情況是堆已經(jīng)被用盡、或者碎片化過于嚴(yán)重。G1會(huì)使用Full GC來處理,處理方法可以增加堆的大小

          • 大對(duì)象分配失敗,在分配巨大對(duì)象的時(shí)候,會(huì)出現(xiàn)和上面類似的問題

          G1收集器調(diào)優(yōu)

          同樣的G1收集器的主要調(diào)優(yōu)目標(biāo)也是避免并發(fā)模式失敗和疏散失敗等導(dǎo)致的Full GC,并減少整個(gè)回收周期中的停頓時(shí)間??刹扇〉牟呗灾饕缦?/span>

          • 增大堆空間、調(diào)整老年代和新生代的比例關(guān)系

          • 增加后臺(tái)線程的數(shù)量,加入CPU資源充足

          • 提高G1收集器的GC頻率

          • 在混合式GC周期中完成更多的GC工作(處理更多的分區(qū))

          最常見的調(diào)優(yōu)策略是通過設(shè)置停頓時(shí)間-XX:MaxGCPauseMillis=N,默認(rèn)值為200ms,讓JVM自己進(jìn)行調(diào)優(yōu)。

          手工調(diào)整則可

          1. 調(diào)整后臺(tái)線程數(shù)量?ParallelGCThreads?設(shè)置在應(yīng)用線程暫停時(shí)候的線程數(shù)量,ConcGCThreads?設(shè)置并發(fā)運(yùn)行階段的線程數(shù)量。默認(rèn)的比例為ConcGCThreads = (ParallelGCThreads + 2)/ 4

          2. 調(diào)整運(yùn)行頻率?-XX:InitiatingHeapOccupancyPercent=N設(shè)置啟動(dòng)閾值,默認(rèn)情況下為45%,這個(gè)閾值和CMS收集器不太一樣,根據(jù)的是整個(gè)堆的內(nèi)存使用情況,而不單是老年代

          3. 調(diào)整混合GC周期 在混合GC周期完成前,無法啟動(dòng)新的并發(fā)周期,因此可以在混合式GC周期內(nèi)處理更多的分區(qū),來減少混合式GC的次數(shù)?;旌鲜紾C的工作量主要取決于,有多少分區(qū)被認(rèn)為是可以回收的,也就是分區(qū)中垃圾對(duì)象的比例;另一個(gè)是最大混合式GC周期數(shù)-XX:G1MixedGCCountTarget=N,默認(rèn)為8,減少這個(gè)數(shù)值,可以幫助解決晉升失敗的問題,代價(jià)是單次停頓的變短更長。如果增加這個(gè)值,則每停頓的時(shí)間會(huì)變短,但是可能會(huì)延遲下一次G1并發(fā)周期的到來,引發(fā)并發(fā)模式失?。籑axGCPauseMillis也會(huì)影響混合式周期的執(zhí)行,增大這個(gè)時(shí)間,可以使每次混合式GC回收更多的老年代分區(qū)。減少這個(gè)值,可以更早地啟動(dòng)并發(fā)周期。

          G1分區(qū)的大小

          G1收集器將堆分成一定數(shù)量的分區(qū),每個(gè)分區(qū)的大小是固定的,并且不是動(dòng)態(tài)變化。最小值為1MB,如果堆的大小超過2GB,則為1 << log(heap_size / 2048),最大不超過64MB。G1分區(qū)的大小可以通過-XX:G1HeapRegionSize=N來設(shè)置,默認(rèn)為0,也就是根據(jù)上面的公式計(jì)算。這個(gè)參數(shù)應(yīng)該是2的冪,否則會(huì)去小于并且最接近這個(gè)數(shù)的2的冪。在調(diào)優(yōu)這個(gè)參數(shù)的時(shí)候,我們應(yīng)該盡量讓分區(qū)的數(shù)量接近2048,這個(gè)是G1算法設(shè)計(jì)時(shí)候期望的分區(qū)數(shù)量。此外增大G1分區(qū)的大小,能夠讓G1收集器更好地處理直接分配到老年代的超大對(duì)象,避免超大對(duì)象無法獲取連續(xù)分區(qū)導(dǎo)致的Full GC??梢酝ㄟ^GC日志觀察這些現(xiàn)象進(jìn)行調(diào)優(yōu)。

          其他調(diào)優(yōu)

          Survivor

          為了讓對(duì)象在新生代中有足夠的機(jī)會(huì)被回收,而不是等到Full GC的時(shí)候回收,因此將新生代分成三個(gè)區(qū)域,一個(gè)Eden空間和兩個(gè)Survivor空間。一般情況下,在Eden分配的對(duì)象都會(huì)很快被回收,如果在一次Minor GC過程中,對(duì)象還存活,那么會(huì)從Eden區(qū)或者Survivor區(qū)移到另一個(gè)Survivor區(qū)。對(duì)象如果在Survivor中經(jīng)歷了GC周期達(dá)到上限,則會(huì)被移動(dòng)到老年代中;如果Survivor區(qū)域無法直接容納Eden中的活躍對(duì)象,那么也會(huì)分配到老年代中。

          Survivor空間的初始大小通過-XX:InitialSurvivorRatio=N標(biāo)志來決定,其具體的大小=新生代大小 / (initial_survivor_ratio + 2)默認(rèn)情況下其值為8,也就是每個(gè)Survivor空間占新生代空間的10%。

          Survivor空間的最大值通過-XX:MinSurvivorRatio=N來控制,這個(gè)參數(shù)默認(rèn)為3,計(jì)算公式和上面一直,也就是最大允許占用20%。

          JVM自動(dòng)調(diào)整Survivor空間大小的時(shí)候,以GC之后,有-XX:TargetSurvivorRatio=N大小的空間是空閑為目標(biāo),默認(rèn)這個(gè)值是50%

          如果要設(shè)置為固定值,則通過-XX:SurvivorRatio=N來設(shè)置,并且同時(shí)關(guān)閉-XX:-UseAdaptiveSizePolicy。

          晉升到老年代的閾值設(shè)置,初始值為-XX:InitialTenuringThreshold=N(Throughout和G1默認(rèn)為7,CMS為6)最大值為-XX:MaxTenuringThreshold=N(Throughout和G1默認(rèn)為15,CMS為6)。實(shí)際上晉升閾值為1~MaxTenuringThreshold中的一個(gè)值,因此只要設(shè)置后者即可。

          對(duì)于Survivor空間的調(diào)優(yōu),一般也是通過調(diào)整大小來進(jìn)行,通過-XX:+PrintTenuringDistribution可以手機(jī)到晉升的統(tǒng)計(jì)信息,如果Survivor空間過小,對(duì)象會(huì)直接晉升到老年代,導(dǎo)致更多的老年代GC,如果堆的大小無法增加,那么Minor GC和老年代GC之間存在一個(gè)取舍。有點(diǎn)情況下,需要避免對(duì)象晉升到老年代,可以提升晉升閾值或者增加Survivor的大小。

          分配大對(duì)象

          在Eden空間中,對(duì)象能夠快速分配對(duì)象是因?yàn)槊恳粋€(gè)線程都有一個(gè)固定的內(nèi)存區(qū)域用于分配對(duì)象,這個(gè)區(qū)域成為TLAB(線程本地分配緩沖區(qū) Thread Local Allocation Buffer)。線程獨(dú)占避免了線程在同一片區(qū)域中分配空間帶來的競(jìng)爭(zhēng)和同步開銷,和Java中的ThreadLocal變量起到相同的作用。默認(rèn)情況下TLAB是開啟的,如果一個(gè)對(duì)象無法在TLAB中分配,我們稱之為大對(duì)象,大對(duì)象需要直接在堆上分配,因此需要額外的同步開銷。如果一個(gè)對(duì)象沒有辦法在TLAB上分配,由于TLAB是Eden空間上的一個(gè)區(qū)段,因此我們可以丟棄這個(gè)TLAB,重新分配一個(gè)TLAB,將數(shù)據(jù)復(fù)制過去,舊的TLAB會(huì)被回收,這種做法可能帶來一些空間浪費(fèi)。另一種做法是直接在堆中分配這個(gè)對(duì)象,保留原來的TLAB。這些主要取決于TLAB的大小,而TLAB的大小,在默認(rèn)情況下取決于:應(yīng)用程序的線程數(shù)、Eden空間的代銷和線程的分配率。我們很難準(zhǔn)確預(yù)測(cè)TLAB的大小,一般的經(jīng)驗(yàn)是通過監(jiān)控TLAB的分配情況,如果大量的對(duì)象分配發(fā)生在TLAB之外,那么可以調(diào)整對(duì)象的大小,或者調(diào)整TLAB的大小。使用JFR可以很好地獲取TLAB分配信息 或者通過-XX:+PrintTLAB,這樣每次Minor GC的時(shí)候,會(huì)輸出包含該線程TLAB使用情況以及整體的TLAB使用情況。

          TLAB的大小是基于Eden空間的,如果增大Eden空間就會(huì)自動(dòng)增大TLAB的大小,更常用的優(yōu)化方式是,顯示指定TLAB值,并關(guān)閉每次GC時(shí)候的自動(dòng)調(diào)整。-XX:TLABSize=N設(shè)置初始值默認(rèn)這個(gè)值是0,以及-XX:-ResizeTLAB,默認(rèn)情況下這個(gè)標(biāo)識(shí)是開啟的,一個(gè)可用的參考值是256KB

          當(dāng)一個(gè)對(duì)象無法直接在當(dāng)前的TLAB上分配的時(shí)候,JVM通過refill waste閾值,來決定是丟棄當(dāng)前的TLAB重新分配一個(gè)新的TLAB還是直接在堆上分配。如果超過這個(gè)閾值,則會(huì)在堆上分配,否則新分配TLAB,回收老的TLAB空間。默認(rèn)這個(gè)值是TLAB大小的1%,或者--XX:TLABWasteTargetPercent=N來設(shè)置特定的值。每當(dāng)在堆上分配一個(gè)對(duì)象的時(shí)候,這個(gè)值就會(huì)增大一點(diǎn),增量由-XX:TLABWasteIncrement=N?控制,默認(rèn)為4。這樣可以避免達(dá)到了閾值之后,連續(xù)在堆上分配對(duì)象。隨著TargetPercentage的增加,TLAB空間被回收的幾率也在增加。

          TLAB空間的最小值為-XX:MinTLABSize=N默認(rèn)2KB,最大容量略小于1G。一般情況下,更加建議使用較小的對(duì)象,來避免使用大TLAB值。根據(jù)GC日志來進(jìn)行調(diào)整是比較明智的選擇。

          而對(duì)于特大對(duì)象的分配,Eden區(qū)無法容納,只能在老年代中分配,特別是這個(gè)對(duì)象還是一個(gè)短期對(duì)象,這會(huì)對(duì)GC產(chǎn)生非常負(fù)面的影響。應(yīng)該從程序上避免這種情況的出現(xiàn)。

          一個(gè)配置說明

          -Xms4096M -Xmx4096M -Xmn512M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -Xss256K -XX:+TieredCompilation -XX:+CMSParallelRemarkEnabled -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+UseFastEmptyMethods -XX:+UseFastAccessorMethods -XX:+ExplicitGCInvokesConcurrent -XX:ParallelCMSThreads=4 -XX:+CMSScavengeBeforeRemark -XX:+CMSClassUnloadingEnabled -XX:CMSInitiatingOccupancyFraction=80 -XX:-ResizeTLAB -XX:TLABSize=256K -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M -Xloggc:/home/data/logs/app/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/data/logs/app/oom.hporf

          JVM的參數(shù)配置,一般沒有固定,也不存在標(biāo)準(zhǔn)的參數(shù),需要根據(jù)不同的應(yīng)用調(diào)整測(cè)試,上面是我比較經(jīng)常使用的JVM配置,采用CMS收集器,主要的調(diào)整項(xiàng)是第一部分各個(gè)分代和分區(qū)的大小,這是對(duì)應(yīng)用性能影響最大的因素。其他參數(shù)中比較值得說一下的是CMSInitiatingOccupancyFraction=80,這個(gè)參數(shù)會(huì)在老年代空間達(dá)到80%的時(shí)候,才開始并發(fā)回收周期,比默認(rèn)的70%稍大,這樣設(shè)置的主要考慮是,這份設(shè)置中當(dāng)老年代使用達(dá)到80%的時(shí)候,剩余空間為(4096-512)* 0.2 = 716M,這個(gè)大小也大于整個(gè)新生代的值,相對(duì)還是比較不容易出現(xiàn)并發(fā)失效的,這樣設(shè)置可以降低并發(fā)回收的周期。-XX:+ExplicitGCInvokesConcurrent則是代替了一般-XX:+DisableExplicitGC將顯式的System.gc()調(diào)用從失效,變成調(diào)用一次CMS并發(fā)回收周期,算是一種折中選擇。其他的優(yōu)化則是一些常規(guī)的優(yōu)化,部分參數(shù)和JVM的默認(rèn)設(shè)置是重復(fù)的,如果需要參考,可以用jcmd process_id VM.flags -all?讀取對(duì)應(yīng)的參數(shù)設(shè)置,將重復(fù)的取出。

          source: https://fzsens.github.io/java/2018/06/01/java-performance-guide-01/


          喜歡,在看


          瀏覽 72
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产精品免费人成网站酒店 | 国产亚洲欧美精品久久久久久 | 久操视频免费看 | 日比视频在线观看 | 韩国一区二区三区免费视频 |