前言
參考資料:
《深入理解 Java 虛擬機(jī) - JVM 高級特性與最佳實(shí)踐》
第1部分主題為自動內(nèi)存管理,以此延伸出 Java 內(nèi)存區(qū)域與內(nèi)存溢出、垃圾收集器與內(nèi)存分配策略已日、參數(shù)配置與性能調(diào)優(yōu)等相關(guān)內(nèi)容;
第2部分主題為虛擬機(jī)執(zhí)行子系統(tǒng),以此延伸出 class 類文件結(jié)構(gòu)、虛擬機(jī)類加載機(jī)制纺酸、虛擬機(jī)字節(jié)碼執(zhí)行引擎等相關(guān)內(nèi)容;
第3部分主題為程序編譯與代碼優(yōu)化址否,以此延伸出程序前后端編譯優(yōu)化餐蔬、前端易用性優(yōu)化、后端性能優(yōu)化等相關(guān)內(nèi)容佑附;
第4部分主題為高效并發(fā)樊诺,以此延伸出 Java 內(nèi)存模型、線程與協(xié)程音同、線程安全與鎖優(yōu)化等相關(guān)內(nèi)容词爬;
本系列學(xué)習(xí)筆記可看做《深入理解 Java 虛擬機(jī) - JVM 高級特性與最佳實(shí)踐》書籍的縮減版與總結(jié)版,想要了解細(xì)節(jié)請見紙質(zhì)版書籍权均;
1. 自動內(nèi)存管理
1.1 JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)
-
線程共享數(shù)據(jù)區(qū):
-
方法區(qū)(Non-Heap 非堆):存儲已被 Java 虛擬機(jī)加載的
類信息
顿膨、常量
、靜態(tài)變量
叽赊,即時(shí)編譯器編譯后的代碼
等數(shù)據(jù)虽惭。當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時(shí),拋出 OutOfMemoryError 異常蛇尚;- 運(yùn)行時(shí)常量池:存放編譯期生成的各種字面量和符號引用;
-
Java 堆(Java Heap):內(nèi)存中最大的一塊顾画。存放
對象實(shí)例和數(shù)組
取劫。是垃圾收集器管理的主要區(qū)域匆笤。可能劃分出多個(gè)線程私有的分配緩沖區(qū)谱邪。目的是為了更好的回收內(nèi)存炮捧,或者更快的分配內(nèi)存〉胍可以處于物理上不連續(xù)的內(nèi)存空間中咆课。沒有內(nèi)存可以完成實(shí)例分配,并且堆也無法再擴(kuò)展時(shí)扯俱,將會拋出 OutOfMemoryError 異常书蚪;
-
方法區(qū)(Non-Heap 非堆):存儲已被 Java 虛擬機(jī)加載的
-
線程獨(dú)立數(shù)據(jù)區(qū):
-
程序計(jì)數(shù)器(Program Counter Register 線程計(jì)數(shù)器):線程正在執(zhí)行 Java 方法時(shí),保存
虛擬機(jī)字節(jié)碼指令的地址
迅栅,否則 Undefined殊校。Java 虛擬機(jī)的多線程是通過線程輪流切換并分配處理器執(zhí)行時(shí)間的方式來實(shí)現(xiàn)的。每條線程都有一個(gè)獨(dú)立的程序計(jì)數(shù)器读存,各個(gè)線程之間計(jì)數(shù)器互不影響为流,獨(dú)立存儲。是虛擬機(jī)中唯一沒有規(guī)定 OutOfMemoryError 情況的區(qū)域让簿; - 本地方法棧(Native Method Stack):為虛擬機(jī)使用到的 Native 方法服務(wù)敬察。源碼是 C 和 C++;
-
Java 虛擬機(jī)棧(Java Virtual Machine Stacks):生命周期和線程一致尔当。存儲 Java 方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行的同時(shí)都會創(chuàng)建一個(gè)
棧幀
(Stack Frame)用于存儲局部變量表
莲祸、操作數(shù)棧
、動態(tài)鏈接
居凶、方法出口
等信息虫给;-
局部變量表:存放
方法參數(shù)
和方法內(nèi)定義的局部變量
。存放編譯期可知的各種基本類型(boolean侠碧、byte抹估、char、short弄兜、int药蜻、float、long替饿、double)语泽、對象引用(reference 類型,能找到對象在 Java 堆中的數(shù)據(jù)存放的起始地址索引视卢,與對象所屬數(shù)據(jù)類型在方法區(qū)中存儲的類型信息)踱卵、returnAddress類型(指向了一條字節(jié)碼指令的地址)。線程安全。通過索引定位
方式使用局部變量表惋砂,容量以變量槽(slot)為最小單位妒挎;- slot 可以復(fù)用,但可能會導(dǎo)致 GC 問題:大對象復(fù)用時(shí)會作為 GC Roots的一部分西饵,當(dāng)它的其中一個(gè)局部變量超過作用域時(shí)酝掩,理應(yīng)回收大對象。但由于 slot 復(fù)用保持著大對象的引用眷柔,導(dǎo)致 GC 無法回收期虾;
- 操作數(shù)棧:操作數(shù)棧是用來操作的。棧中的數(shù)據(jù)元素必須與字節(jié)碼指令的序列嚴(yán)格匹配驯嘱。在概念模型中镶苞,兩個(gè)棧幀是相互獨(dú)立的;在實(shí)際實(shí)現(xiàn)中宙拉,上下兩個(gè)棧幀可能會出現(xiàn)一部分重疊宾尚,以實(shí)現(xiàn)數(shù)據(jù)共享;
- 動態(tài)鏈接:鏈接到別的方法中去谢澈,用來存儲鏈接的地方煌贴。動態(tài)體現(xiàn)在:在每一次運(yùn)行期間轉(zhuǎn)化為直接引用,而不是第一次類加載階段(靜態(tài)解析)锥忿;
-
方法出口:有兩種出口:
- 正常 return:方法調(diào)用者的程序計(jì)數(shù)器的值可以作為返回地址牛郑;
- 不正常拋出異常:需要通過異常處理表來確定出口;
- 附加信息:虛擬機(jī)規(guī)范允許具體的虛擬機(jī)實(shí)現(xiàn)增加一些規(guī)范里沒有描述的信息到棧幀中敬鬓,例如與調(diào)試相關(guān)的信息淹朋,由虛擬機(jī)自行實(shí)現(xiàn);
-
局部變量表:存放
-
程序計(jì)數(shù)器(Program Counter Register 線程計(jì)數(shù)器):線程正在執(zhí)行 Java 方法時(shí),保存
1.2 Java 內(nèi)存結(jié)構(gòu)
-
直接內(nèi)存(Direct Memory):非虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的部分钉答。不是 Java 虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域础芍;
- 應(yīng)用:JDK1.4 中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的 I/O 方式数尿,可以使用 Native 函數(shù)庫直接分配堆外內(nèi)存仑性,然后使用一個(gè)存儲在 Java 堆中的 DirectByteBuffer 對象作為這塊內(nèi)存的引用進(jìn)行操作。避免了在 Java 堆和 Native(本地)堆中來回復(fù)制數(shù)據(jù)右蹦;
- 內(nèi)存區(qū)域總和大于物理內(nèi)存限制從而導(dǎo)致動態(tài)擴(kuò)展時(shí)出現(xiàn) OutOfMemoryError 異常诊杆;
-
直接內(nèi)存與堆內(nèi)存的區(qū)別:
- 直接內(nèi)存:ByteBuffer.allocateDirect();
- 非直接內(nèi)存:ByteBuffer.allocate()何陆;
- 直接內(nèi)存申請空間
耗費(fèi)高性能
晨汹,堆內(nèi)存申請空間耗費(fèi)比較低; - 直接內(nèi)存的
IO 讀寫的性能優(yōu)
于堆內(nèi)存贷盲,在多次讀寫操作的情況相差非常明顯淘这;
- JVM 字節(jié)碼執(zhí)行引擎:核心組件。負(fù)責(zé)執(zhí)行虛擬機(jī)的字節(jié)碼;
- 垃圾收集系統(tǒng):垃圾收集系統(tǒng)是 Java 的核心慨灭。垃圾指沒有引用指向的內(nèi)存對象朦乏;
1.3 HotSpot 虛擬機(jī)創(chuàng)建對象
- 1. 判斷是否加載:遇到 new 指令時(shí),首先檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號引用氧骤,并且檢查這個(gè)符號引用代表的類是否已經(jīng)被加載、解析和初始化過吃引。如果沒有筹陵,執(zhí)行相應(yīng)的類加載;
- 2. 分配內(nèi)存:類加載檢查通過之后镊尺,為新對象分配內(nèi)存(在堆里朦佩,內(nèi)存大小在類加載完成后便可確認(rèn))。在堆的空閑內(nèi)存中劃分一塊區(qū)域(有兩種:‘指針碰撞’——serial庐氮、ParNew 算法语稠;‘空閑列表’——CMS 算法);
- 這里可能會有并發(fā)線程安全問題弄砍,多個(gè)個(gè)線程同時(shí)分配同一塊內(nèi)存仙畦,兩種解決方法:對分配內(nèi)存的動作進(jìn)行同步處理(采用 CAS 配上失敗重試保證原子操作)∫羯簦或者采用:根據(jù)線程不同劃分不同的內(nèi)存緩沖區(qū)執(zhí)行內(nèi)存分配操作慨畸;
- 3. 初始化值:內(nèi)存空間分配完成后會初始化為 0(不包括對象頭),然后填充對象頭(哪個(gè)類的實(shí)例衣式、何找到類的元數(shù)據(jù)信息寸士、哈希碼、GC 分代年齡等)碴卧;
- 4. 執(zhí)行 init 方法:賦實(shí)際值弱卡,程序員可控;
1.4 HotSpot 虛擬機(jī)的對象內(nèi)存布局
-
對象頭(Header):包含兩部分:
- 用于存儲對象自身的運(yùn)行時(shí)數(shù)據(jù):哈希碼住册、GC 分代年齡婶博、鎖狀態(tài)標(biāo)志、線程持有的鎖界弧、偏向線程 ID凡蜻、偏向時(shí)間戳等;
- 類型指針:對象指向它的類的元數(shù)據(jù)指針垢箕,確定是哪個(gè)類的實(shí)例划栓;
- 數(shù)組在對象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù)(普通對象可以通過元數(shù)據(jù)確定大小)条获;
- 實(shí)例數(shù)據(jù)(Instance Data):程序代碼中所定義的各種類型的字段內(nèi)容(包含父類繼承下來的和子類中定義的)
- 對齊填充(Padding):不是必然需要忠荞,主要是占位,保證對象大小是某個(gè)字節(jié)的整數(shù)倍;
1.5 訪問對象
- 通過棧上的 reference 數(shù)據(jù)(在 Java 堆中)來操作堆上的具體對象:
- reference 存儲的是句柄地址:好處是在對象移動(GC)時(shí)只改變實(shí)例數(shù)據(jù)指針地址委煤;
- reference 中直接存儲對象地址:好處是速度快(只需一次指針尋址)堂油;
2. 垃圾回收與內(nèi)存分配
垃圾回收機(jī)制的缺點(diǎn):是否執(zhí)行,什么時(shí)候執(zhí)行卻是不可知的碧绞;
2.1 判斷對象是否存活
-
引用計(jì)數(shù)法:
- 如果一個(gè)對象沒有被任何引用指向府框,則可視之為垃圾;
- 主流的Java虛擬機(jī)里面都沒有選用引用計(jì)數(shù)算法來管理內(nèi)存讥邻;
- 缺點(diǎn):不能解決循環(huán)引用問題迫靖;
-
可達(dá)性分析法:(主流)
- 從 GC Roots 開始向下搜索,搜索所走過的路徑為引用鏈兴使。當(dāng)一個(gè)對象到 GC Roots 沒用任何引用鏈時(shí)系宜,則證明此對象是不可用的,表示可以回收发魄。實(shí)際上一個(gè)對象的真正死亡至少要經(jīng)歷兩次標(biāo)記過程盹牧;
- GC Roots 的對象:
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象;
- 方法區(qū)中
類靜態(tài)屬于引用的對象
励幼; - 方法區(qū)中
常量引用的對象
汰寓; - 本地方法棧中 JNI(即一般說的 Native方法)引用的對象;
- 目前主流的虛擬機(jī)都是采用的算法赏淌;
- 對象的四種引用:(JDK 1.2 之后踩寇,引用概念進(jìn)行了擴(kuò)充)
- 強(qiáng)引用:類似 new 關(guān)鍵字創(chuàng)建的引用,只要強(qiáng)引用在就不回收六水;
- 軟引用:SoftReference 類實(shí)現(xiàn)俺孙,發(fā)生內(nèi)存溢出異常之前,會把這些對象列進(jìn)回收范圍掷贾;
- 弱引用:WeakReference 類實(shí)現(xiàn)睛榄,在垃圾收集器工作時(shí),無論內(nèi)存是否足夠都會回收想帅;
- 虛引用:PhantomReference 類實(shí)現(xiàn)场靴,無法訪問實(shí)例,唯一目的是在這個(gè)對象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知港准;
2.2 分代與內(nèi)存分配旨剥、回收策略
- 相關(guān)代碼:
- 手動回收垃圾:
System.gc()
- 執(zhí)行 GC 操作調(diào)用:
Object.finalize()
- 手動回收垃圾:
-
分代:
-
方法區(qū):永久代。不容易回收浅缸。主要回收
廢棄的常量
(沒有該常量的引用)和無用的類
(所有實(shí)例已回收轨帜、該類的 ClassLoader 已回收、無法通過反射訪問)衩椒; -
Java 堆:新生代 + 老年代蚌父。默認(rèn)新生代與老年代的比例的值為 1:2哮兰;
-
老年代(2/3):對象存活率高、沒有額外空間對它進(jìn)行分配擔(dān)保苟弛。
“標(biāo)記-清理”算法
或者“標(biāo)記-整理”算法
喝滞。大對象、長期存活對象分配在老年代膏秫; -
新生代(1/3):Eden + From Survivor + To Survivor右遭。默認(rèn)的 Edem : From Survivor : To Survivor = 8 : 1 : 1。JVM 每次只會使用 Eden 和其中的一塊 Survivor 區(qū)域來為對象服務(wù)缤削,剩余的存放回收后存活的對象(與
復(fù)制算法
有關(guān))狸演;- Eden(4/15):數(shù)據(jù)會首先分配到 Eden 區(qū)。Eden沒有足夠空間的時(shí)候就會觸發(fā) JVM 發(fā)起一次 Minor GC僻他,存活則進(jìn)入 Survivor;
- From Survivor(1/30) 和 To Survivor(1/30):對象每熬過一次 Minor GC 還存活則年齡加1腊尚,當(dāng)年齡達(dá)到(默認(rèn)為15)時(shí)晉升到老年代吨拗;
-
老年代(2/3):對象存活率高、沒有額外空間對它進(jìn)行分配擔(dān)保苟弛。
-
方法區(qū):永久代。不容易回收浅缸。主要回收
- 幾種分代 GC:
-
Minor GC:新生代 GC。執(zhí)行頻繁婿斥,回收速度快劝篷;
- 觸發(fā)條件:Eden 區(qū)滿。
-
Major GC:老年代 GC民宿。通常會連著 Minor GC 一起執(zhí)行娇妓。速度慢;
- 觸發(fā)條件:晉升老年代對象大小 > 老年代剩余空間活鹰。Minor GC 后存活對象大小 > 老年代剩余空間哈恰。永久代空間不足。執(zhí)行 System.gc()志群。CMS GC異常着绷。堆內(nèi)存分配很大對象。
-
Full GC:清理整個(gè)堆空間锌云,包括新生代和老年代荠医。Full GC 相對于Minor GC來說,停止用戶線程的 STW(stop the world)時(shí)間過長桑涎,應(yīng)盡量避免彬向;
- 觸發(fā)條件:System.gc() 方法調(diào)用。晉升老年代對象大小 > 老年代剩余空間攻冷。Metaspace區(qū)內(nèi)存達(dá)到閾值(JDK8 引入娃胆,使用本地內(nèi)存)。堆中產(chǎn)生大對象超過閾值讲衫。老年代連續(xù)空間不足缕棵。
-
Minor GC:新生代 GC。執(zhí)行頻繁婿斥,回收速度快劝篷;
- 動態(tài)對象年齡判定:在 Survivor 空間中相同年齡 x 的對象總大小 > Survivor 空間的一半時(shí)孵班,年齡大于等于 x 的對象將直接進(jìn)入老年代;(HotSpot 虛擬機(jī))
-
空間分配擔(dān)保:
- JDK 6 Update 24 之前:
老年代可用連續(xù)空間大小 < 新生代對象總大小
時(shí)招驴,查看相關(guān)參數(shù)判斷是否允許擔(dān)保失敗篙程。允許則判斷是否:年代可用連續(xù)空間大小 > 歷次晉升老年代對象平均大小
,成立則進(jìn)行 Major GC(有風(fēng)險(xiǎn))别厘;不成立說明老年代可用連續(xù)空間很少虱饿,進(jìn)行 Full GC〈ヅ浚或者不允許擔(dān)保失敗也會進(jìn)行 Full GC氮发; - JDK 6 Update 24 之后:
老年代可用連續(xù)空間大小 > 新生代對象總大小
或老年代可用連續(xù)空間大小 > 次晉升老年代對象平均大小
時(shí),進(jìn)行 Major GC冗懦。反之進(jìn)行 Full GC爽冕;
- JDK 6 Update 24 之前:
2.3 垃圾回收算法(GC 的算法)
-
引用計(jì)數(shù)算法(Reference counting):
- 每個(gè)對象在創(chuàng)建的時(shí)候,就給這個(gè)對象綁定一個(gè)計(jì)數(shù)器披蕉。每當(dāng)有一個(gè)引用指向該對象時(shí)颈畸,計(jì)數(shù)器加一。每當(dāng)有一個(gè)指向它的引用被刪除時(shí)没讲,計(jì)數(shù)器減一眯娱。計(jì)數(shù)器為0就代表該對象死亡,這時(shí)就應(yīng)該對這個(gè)對象進(jìn)行垃圾回收操作爬凑;
- 主流的 Java 虛擬機(jī)里面都沒有選用引用計(jì)數(shù)算法來回收垃圾徙缴;
-
標(biāo)記–清除算法(Mark-Sweep):
- 分為兩個(gè)階段,一個(gè)是標(biāo)記階段嘁信,這個(gè)階段內(nèi)于样,為每個(gè)對象更新標(biāo)記位,檢查對象是否死亡吱抚。第二個(gè)階段是清除階段百宇,該階段對死亡的對象進(jìn)行清除,執(zhí)行 GC 操作秘豹;
- 優(yōu)點(diǎn):必要時(shí)才回收携御。解決循環(huán)引用的問題;
- 缺點(diǎn):回收時(shí)既绕,應(yīng)用需要掛起啄刹。效率不高。會造成內(nèi)存碎片凄贩;
- 應(yīng)用:老年代(生命周期比較長)誓军;
-
標(biāo)記–整理算法:
- 在第二個(gè)清除階段,該算法并沒有直接對死亡的對象進(jìn)行清理疲扎,而是將所有存活的對象整理一下昵时,放到另一處空間捷雕,然后把剩下的所有對象全部清除;
- 優(yōu)點(diǎn):解決內(nèi)存碎片問題壹甥;
- 缺點(diǎn):由于移動了可用對象救巷,需要去更新引用;
- 應(yīng)用:老年代(生命周期比較長)句柠;
-
復(fù)制算法:
- 把空間分成兩塊浦译,每次只對其中一塊進(jìn)行 GC。當(dāng)這塊內(nèi)存使用完時(shí)溯职,就將還存活的對象復(fù)制到另一塊上面精盅,循環(huán)下去。實(shí)際分為一塊 Eden 和兩塊 Survivor谜酒;
- 優(yōu)點(diǎn):存活對象不多時(shí)性能高叹俏。解決內(nèi)存碎片和引用更新問題;
- 缺點(diǎn):內(nèi)存浪費(fèi)僻族。存活對象數(shù)量大時(shí)性能差她肯;
- 應(yīng)用:新生代(當(dāng)回收時(shí),將 Eden 和 Survivor 中還存活的對象一次性復(fù)制到另一塊 Survivor 上鹰贵,最后清理 Eden 和 Survivor 空間);
-
分代算法:(次要)
- 針對不同代使用不同的 GC 算法康嘉;
2.4 HotSpot 的算法實(shí)現(xiàn)
2.5 垃圾收集器
垃圾回收算法是內(nèi)存回收的理論碉输,垃圾回收器是內(nèi)存回收的實(shí)踐;
- 上圖說明:如果兩個(gè)收集器之間存在連線說明他們之間可以搭配使用亭珍;
-
垃圾收集器:
- 是垃圾回收算法的具體實(shí)現(xiàn)敷钾,不同版本的 JVM 所提供的垃圾收集器可能會有很在差別;
- JDK8 的垃圾收集器:
-
Serial:Client 模式下默認(rèn)肄梨。一個(gè)單線程收集器阻荒,只會使用一個(gè) CPU 或者線程去完成垃圾收集工作,而且在它進(jìn)行垃圾收集時(shí)众羡,必須暫停其他所有的工作線程侨赡,直到它收集結(jié)束。單線程收集高效粱侣;
- 工作區(qū)域:新生帶羊壹;
- 回收算法:復(fù)制算法;
- 工作線程:單線程齐婴;
- 線程并行:不支持油猫;
-
ParNew:可看做 Serial 的多線程版本,Server 模式下首選柠偶, 可搭配 CMS 的新生代收集器情妖;
- 工作區(qū)域:新生帶睬关;
- 回收算法:復(fù)制算法;
- 工作線程:多線程毡证;
- 線程并行:不支持电爹;
-
Parallel Scavenge:目標(biāo)是達(dá)到可控制的吞吐量(即:減少垃圾收集時(shí)間)。吞吐量 Throughput = 運(yùn)行用戶代碼時(shí)間 / (運(yùn)行用戶代碼時(shí)間 + 垃圾收集時(shí)間)情竹;
- 工作區(qū)域:新生帶藐不;
- 回收算法:復(fù)制算法;
- 工作線程:多線程秦效;
- 線程并行:不支持雏蛮;
-
Serial Old:Serial 老年代版本,Client 模式下的虛擬機(jī)使用阱州;
- 工作區(qū)域:老年帶挑秉;
- 回收算法:標(biāo)記-整理算法承边;
- 工作線程:單線程疏遏;
- 線程并行:不支持;
-
Parallnel old:Parallel Scavenge 老年代版本挂滓,吞吐量優(yōu)先夜惭;
- 工作區(qū)域:老年帶姻灶;
- 回收算法:標(biāo)記-整理算法;
- 工作線程:多線程诈茧;
- 線程并行:不支持产喉;
-
CMS:一種以獲取
最短回收停頓時(shí)間
為目標(biāo)的收集器,適用于互聯(lián)網(wǎng)站或者 B/S 系統(tǒng)的服務(wù)端上敢会。并發(fā)收集曾沈、低停頓。與用戶線程可以同時(shí)工作鸥昏;- 工作區(qū)域:老年帶塞俱;
- 回收算法:標(biāo)記-清除算法(內(nèi)存碎片);
- 工作線程:多線程吏垮;
- 線程并行:支持障涯;
- 缺點(diǎn):對 CPU 資源敏感。無法收集浮動垃圾(Concurrent Mode Failure)膳汪。內(nèi)存碎片像樊;
- 運(yùn)作步驟:初始標(biāo)記(標(biāo)記 GC Roots 能直接關(guān)聯(lián)到的對象)、并發(fā)標(biāo)記(進(jìn)行 GC Roots Tracing)旅敷、重新標(biāo)記生棍;
-
G1:最前沿成果之一。面向服務(wù)端應(yīng)用的垃圾收集器媳谁⊥康危可看做 CM的終極改進(jìn)版友酱。JDK1.9 默認(rèn)垃圾收集器。能充分利用多CPU柔纵、多核環(huán)境下的硬件優(yōu)勢缔杉。可以并行來縮短(Stop The World)停頓時(shí)間搁料。能獨(dú)立管理整個(gè) GC 堆或详。采用不同方式處理不同時(shí)期的對象。
- 工作區(qū)域:新生帶 + 老年帶郭计;
- 回收算法:標(biāo)記-整理 + 復(fù)制算法霸琴;
- 工作線程:多線程;
- 線程并行:支持昭伸;
- 運(yùn)作步驟:初始標(biāo)記(標(biāo)記 GC Roots 能直接關(guān)聯(lián)到的對象)梧乘、并發(fā)標(biāo)記(進(jìn)行 GC Roots Tracing)、重新標(biāo)記庐杨;
-
Serial:Client 模式下默認(rèn)肄梨。一個(gè)單線程收集器阻荒,只會使用一個(gè) CPU 或者線程去完成垃圾收集工作,而且在它進(jìn)行垃圾收集時(shí)众羡,必須暫停其他所有的工作線程侨赡,直到它收集結(jié)束。單線程收集高效粱侣;
3. JVM 參數(shù)配置
3.1 JVM 內(nèi)存參數(shù)簡述
- 常用:
- -Xms:初始堆大小选调,JVM 啟動的時(shí)候,給定堆空間大辛榉荨仁堪;
- -Xmx:最大堆大小,JVM 運(yùn)行過程中填渠,如果初始堆空間不足的時(shí)候枝笨,最大可以擴(kuò)展到多少;
- -Xmn:設(shè)置堆中年輕代大小揭蜒。整個(gè)堆大小=年輕代大小+年老代大小+持久代大小剔桨;
- -XX:NewSize=n 設(shè)置年輕代初始化大小大刑敫;
- -XX:MaxNewSize=n 設(shè)置年輕代最大值洒缀;
- -XX:NewRatio=n 設(shè)置年輕代和年老代的比值瑰谜。如: -XX:NewRatio=3,表示年輕代與年老代比值為 1:3树绩,年輕代占整個(gè)年輕代+年老代和的 1/4 萨脑;
- -XX:SurvivorRatio=n 年輕代中 Eden 區(qū)與兩個(gè) Survivor 區(qū)的比值。注意 Survivor 區(qū)有兩個(gè)饺饭。8表示兩個(gè)Survivor :eden=2:8 ,即一個(gè)Survivor占年輕代的1/10渤早,默認(rèn)就為8;
- -Xss:設(shè)置每個(gè)線程的堆棧大小瘫俊。JDK5后每個(gè)線程 Java 棧大小為 1M鹊杖,以前每個(gè)線程堆棧大小為 256K悴灵;
- -XX:ThreadStackSize=n 線程堆棧大小骂蓖;
- -XX:PermSize=n 設(shè)置持久代初始值积瞒;
- -XX:MaxPermSize=n 設(shè)置持久代大小登下;
- -XX:MaxTenuringThreshold=n 設(shè)置年輕帶垃圾對象最大年齡茫孔。如果設(shè)置為 0 的話,則年輕代對象不經(jīng)過 Survivor 區(qū)被芳,直接進(jìn)入年老代缰贝;
- 不常用:
- -XX:LargePageSizeInBytes=n 設(shè)置堆內(nèi)存的內(nèi)存頁大小筐钟;
- -XX:+UseFastAccessorMethods 優(yōu)化原始類型的 getter 方法性能揩瞪;
- -XX:+DisableExplicitGC 禁止在運(yùn)行期顯式地調(diào)用 System.gc(),默認(rèn)啟用篓冲;
- -XX:+AggressiveOpts 是否啟用JVM開發(fā)團(tuán)隊(duì)最新的調(diào)優(yōu)成果李破。例如編譯優(yōu)化,偏向鎖壹将,并行年老代收集等嗤攻,jdk6 之后默認(rèn)啟動;
- -XX:+UseBiasedLocking 是否啟用偏向鎖诽俯,JDK6 默認(rèn)啟用妇菱;
- -Xnoclassgc 是否禁用垃圾回收;
- -XX:+UseThreadPriorities 使用本地線程的優(yōu)先級暴区,默認(rèn)啟用闯团;
3.2 JVM 的 GC 收集器設(shè)置
- -XX:+UseSerialGC:設(shè)置串行收集器,年輕帶收集器仙粱;
- -XX:+UseParNewGC:設(shè)置年輕代為并行收集房交。可與 CMS 收集同時(shí)使用伐割。JDK5.0 以上候味,JVM 會根據(jù)系統(tǒng)配置自行設(shè)置,所以無需再設(shè)置此值隔心;
- -XX:+UseParallelGC:設(shè)置并行收集器白群,目標(biāo)是目標(biāo)是達(dá)到可控制的吞吐量;
- -XX:+UseParallelOldGC:設(shè)置并行年老代收集器硬霍,JDK6.0 支持對年老代并行收集帜慢;
- -XX:+UseConcMarkSweepGC:設(shè)置年老代并發(fā)收集器;
- -XX:+UseG1GC:設(shè)置 G1 收集器,JDK1.9 默認(rèn)垃圾收集器崖堤;
4. JVM 性能調(diào)優(yōu)案例分析
調(diào)優(yōu)目的:GC 的時(shí)間足夠的小侍咱、GC 的次數(shù)足夠的少、發(fā)生 Full GC 的周期足夠的長密幔;
問題原因:Full GC 的停止用戶線程的 STW 時(shí)間過長楔脯,應(yīng)盡量避免;
Full GC 觸發(fā)條件:主要是兩個(gè):老年代內(nèi)存過小胯甩、老年代連續(xù)內(nèi)存過忻镣ⅰ;
控制 Full GC 頻率的關(guān)鍵:保障老年代空間的穩(wěn)定偎箫,大多數(shù)對象的生存時(shí)間不應(yīng)當(dāng)太長木柬,尤其是不能有成批量的、長生存時(shí)間的大對象產(chǎn)生淹办;
4.1 大內(nèi)存硬件上的應(yīng)用程序部署策略
- 場景簡述:原來有 16GB 物理內(nèi)存(堆內(nèi)存有 4GB)眉枕,升級硬件配置后控制堆內(nèi)存為 12GB。結(jié)構(gòu)出現(xiàn)不定期長時(shí)間失去響應(yīng)的問題怜森;
- 場景特點(diǎn):用戶交互性強(qiáng)速挑、對停頓時(shí)間敏感、內(nèi)存較大副硅、Java堆較大姥宝;
- 問題原因:內(nèi)存出現(xiàn)很多由文檔序列化產(chǎn)生的大對象,大對象大多在分配時(shí)就直接進(jìn)入了老年代恐疲,Minor GC 清理不掉腊满。最終導(dǎo)致導(dǎo)致老年代內(nèi)存過小,經(jīng)常發(fā)生 Full GC培己;
- 解決思路:通過減少單個(gè)進(jìn)程的內(nèi)存碳蛋,減低老年代內(nèi)存,使文檔序列化對象不易進(jìn)入老年代省咨,在 Minor GC 時(shí)就被清理肃弟;
-
實(shí)際方案:目前單體應(yīng)用在較大內(nèi)存的硬件上主要的部署方式有兩種:
-
方案一:通過一個(gè)單獨(dú)的 Java 虛擬機(jī)實(shí)例來管理大量的 Java 堆內(nèi)存。具體來說:
- 1. 使用 Shenandoah茸炒、ZGC 這些明確以控制延遲為目標(biāo)的垃圾收集器;
- 2. 在把 Full GC 頻率控制得足夠低的情況下(老年代的相對穩(wěn)定)阵苇,使用 Parallel Scavenge/Old 收集器壁公,并且給 Java 虛擬機(jī)分配較大的堆內(nèi)存;
-
方案二:使用多個(gè) Java 虛擬機(jī)绅项,建立邏輯集群來利用硬件資源紊册。具體來說:
- 1. 在一臺物理機(jī)器上啟動多個(gè)應(yīng)用服務(wù)器進(jìn)程,為每個(gè)服務(wù)器進(jìn)程分配不同端口,然后在前端搭建一個(gè)負(fù)載均衡器囊陡,以反向代理的方式來分配訪問請求芳绩;
- 2. 使用無 Session 復(fù)制的親合式集群,即:均衡器按一定的規(guī)則算法(譬如根據(jù) Session ID 分配)將一個(gè)固定的用戶請求永遠(yuǎn)分配到一個(gè)固定的集群節(jié)點(diǎn)進(jìn)行處理撞反;(一致 hash 算法的思想)妥色;
-
方案一:通過一個(gè)單獨(dú)的 Java 虛擬機(jī)實(shí)例來管理大量的 Java 堆內(nèi)存。具體來說:
-
調(diào)優(yōu)過程:
- 1. 發(fā)現(xiàn)問題:監(jiān)控服務(wù)器運(yùn)行狀況 -> 發(fā)現(xiàn)網(wǎng)站失去響應(yīng)是由垃圾收集停頓所導(dǎo)致的;
- 2. 分析解決遏片;
-
經(jīng)驗(yàn)之談:
- 1. 計(jì)劃使用單個(gè) Java 虛擬機(jī)實(shí)例來管理大內(nèi)存嘹害,可能遇到的問題:
- 回收大塊堆內(nèi)存而導(dǎo)致的長時(shí)間停頓(G1 收集器緩解問題,ZGC 和 Shenandoah 收集器徹底解決)吮便;
- 大內(nèi)存必須有 64 位 Java 虛擬機(jī)的支持笔呀,但由于壓縮指針、處理器緩存行容量(Cache Line)等因素髓需,64 位虛擬機(jī)的性能測試結(jié)果普遍略低于相同版本的 32 位虛擬機(jī)许师;
- 必須保證應(yīng)用程序足夠穩(wěn)定,因?yàn)檫@種大型單體應(yīng)用要是發(fā)生了堆內(nèi)存溢出僚匆,幾乎無法產(chǎn)生堆轉(zhuǎn)儲快照(要產(chǎn)生十幾GB乃至更大的快照文件)微渠。出了問題可能必須應(yīng)用 JMC 這種能夠在生產(chǎn)環(huán)境中進(jìn)行的運(yùn)維工具;
- 相同的程序在 64 位虛擬機(jī)中消耗的內(nèi)存一般比 32 位虛擬機(jī)要大白热,這是由于指針膨脹敛助,以及數(shù)據(jù)類型對齊補(bǔ)白等因素導(dǎo)致的,可以開啟(默認(rèn)即開啟)壓縮指針功能來緩解屋确;
- 2. 使用邏輯集群的方式來部署程序纳击,可能遇到的問題:
- 節(jié)點(diǎn)競爭全局的資源,最典型的就是磁盤競爭攻臀;
- 很難最高效率地利用某些資源池焕数,譬如連接池,一般都是在各個(gè)節(jié)點(diǎn)建立自己獨(dú)立的連接池刨啸,這樣有可能導(dǎo)致一些節(jié)點(diǎn)的連接池已經(jīng)滿了堡赔,而另外一些節(jié)點(diǎn)仍有較多空余。盡管可以使用集中式的 JNDI 來解決设联,但這個(gè)方案有一定復(fù)雜性并且可能帶來額外的性能代價(jià)善已;
- 如果使用 32 位 Java 虛擬機(jī)作為集群節(jié)點(diǎn)的話,各個(gè)節(jié)點(diǎn)仍然不可避免地受到 32 位的內(nèi)存限制离例,在 32 位 Windows 平臺中每個(gè)進(jìn)程只能使用 2GB 的內(nèi)存换团,考慮到堆以外的內(nèi)存開銷,堆最多一般只能開到 1.5GB宫蛆。在某些 Linux 或 UNIX 系統(tǒng)(如 Solaris)中艘包,可以提升到 3GB 乃至接近 4GB 的內(nèi)存,但 32 位中仍然受最高 4GB(2 的 32 次冪)內(nèi)存的限制;
- 大量使用本地緩存(如大量使用 HashMap 作為 K/V 緩存)的應(yīng)用想虎,在邏輯集群中會造成較大的內(nèi)存浪費(fèi)卦尊,因?yàn)槊總€(gè)邏輯節(jié)點(diǎn)上都有一份緩存,這時(shí)候可以考慮把本地緩存改為集中式緩存(如 4.6)舌厨;
- 1. 計(jì)劃使用單個(gè) Java 虛擬機(jī)實(shí)例來管理大內(nèi)存嘹害,可能遇到的問題:
4.2 集群間同步導(dǎo)致的內(nèi)存溢出
- 場景簡述:采用親合式集群的 MIS 系統(tǒng)岂却,為了實(shí)現(xiàn)部分?jǐn)?shù)據(jù)在各個(gè)節(jié)點(diǎn)中共享,使用 JBossCache 構(gòu)建了一個(gè)全局緩存邓线。結(jié)果不定期出現(xiàn)多次的內(nèi)存溢出問題淌友;
- 場景特點(diǎn):親合式集群、JBossCache 全局緩存骇陈;
- 問題原因:JBossCache 基于 JGroups 進(jìn)行集群間的數(shù)據(jù)通信震庭,JGroups 在收發(fā)數(shù)據(jù)包時(shí)會在內(nèi)存構(gòu)建 NAKACK 棧保證順序與重發(fā)。當(dāng)網(wǎng)絡(luò)不好時(shí)重發(fā)數(shù)據(jù)在內(nèi)存中不斷堆積你雌;
- 解決思路:改進(jìn) JBossCache 的缺陷器联,改進(jìn) MIS 系統(tǒng);
- 實(shí)際方案:可以允許讀操作頻繁婿崭,不允許寫操作頻繁拨拓,避免大的網(wǎng)絡(luò)同步開銷;
-
調(diào)優(yōu)過程:
- 1. 發(fā)現(xiàn)問題:添加
-XX:+HeapDumpOnOutOfMemoryError
參數(shù) -> 運(yùn)行一段時(shí)間發(fā)現(xiàn)存在大量 t.NAKACK 對象氓栈; - 2. 分析解決渣磷;
- 1. 發(fā)現(xiàn)問題:添加
4.3 堆外內(nèi)存導(dǎo)致的溢出錯(cuò)誤
-
場景簡述:使用 CometD 1.1.1 作為服務(wù)端推送框架,服務(wù)器為 4GB 內(nèi)存授瘦,運(yùn)行 32 位
Windows 操作系統(tǒng)醋界,堆內(nèi)存設(shè)置為 1.6GB。結(jié)果不定時(shí)拋出內(nèi)存溢出異常提完; - 場景特點(diǎn):32 位系統(tǒng)形纺、小內(nèi)存、大量的 NIO 操作
- 問題原因:32 位 Windows 平臺中每個(gè)進(jìn)程只能使用 2GB 的內(nèi)存徒欣,其中 1.6GB 分配給了堆內(nèi)存逐样,0.4 GB 分配給了直接內(nèi)存。CometD 1.1.1 框架打肝,有大量的 NIO 操作脂新,NIO 會使用 Native 函數(shù)庫直接分配堆外內(nèi)存,最終導(dǎo)致直接內(nèi)存溢出粗梭;
-
解決思路:注意占用較多內(nèi)存的區(qū)域:調(diào)整直接內(nèi)存争便、線程堆棧、Socket 緩沖區(qū)大小楼吃,注意 JNI 代碼始花,選擇合適的虛擬機(jī)與垃圾收集器;
- 1. 直接內(nèi)存:通過
-XX:MaxDirectMemorySize
調(diào)整直接內(nèi)存大泻⑽酷宵; - 2. 線程堆棧:通過
-Xss
調(diào)整線程堆大小躬窜; - 3. Socket緩存:每個(gè) Socket 連接都 Receive 和 Send 兩個(gè)緩存區(qū)浇垦,控制 Socket 連接數(shù);
- 4. JNI代碼:JNI調(diào)用本地庫會使用 Native 函數(shù)庫直接分配堆外內(nèi)存荣挨;
- 5. 虛擬機(jī)和垃圾收集器:虛擬機(jī)男韧、垃圾收集器的工作也是要消耗一定數(shù)量的內(nèi)存的;
- 1. 直接內(nèi)存:通過
-
調(diào)優(yōu)過程:
- 1. 發(fā)現(xiàn)問題:首先查看日志 -> 在內(nèi)存溢出后的系統(tǒng)日志中找到異常堆棧(OutOfMemoryError)默垄;
- 2. 分析解決此虑;
4.4 外部命令導(dǎo)致系統(tǒng)緩慢
- 場景簡述:在一臺四路處理器的 Solaris 10 操作系統(tǒng)上,處理每次用戶請求時(shí)都會執(zhí)行一個(gè)外部 Shell 腳本獲取系統(tǒng)信息口锭。最后發(fā)現(xiàn)請求響應(yīng)時(shí)間比較慢朦前,并且系統(tǒng)中占用絕大多數(shù)處理器資源的程序并不是該應(yīng)用本身;
- 場景特點(diǎn):Shell 腳本鹃操、創(chuàng)建進(jìn)程耗費(fèi)大量資源韭寸、“fork”系統(tǒng);
- 問題原因:執(zhí)行 Shell 腳本是通過 Java 的 Runtime.getRuntime().exec() 方法來調(diào)用的荆隘,它首先復(fù)制一個(gè)和當(dāng)前虛擬機(jī)擁有一樣環(huán)境變量的進(jìn)程恩伺,再用這個(gè)新的進(jìn)程去執(zhí)行外部命令,最后再退出這個(gè)進(jìn)程椰拒;
- 解決思路:盡量減少創(chuàng)建進(jìn)程的開銷晶渠;
- 實(shí)際方案:去掉這個(gè) Shell 腳本執(zhí)行的語句,改為使用 Java 的 API 去獲取信息耸三;
-
調(diào)優(yōu)過程:
- 1. 發(fā)現(xiàn)問題:通過 Solaris 10 的 dtrace 腳本 -> 查看當(dāng)前情況下哪些系統(tǒng)調(diào)用花費(fèi)了最多的處理器資源乱陡;
- 2. 定位問題:發(fā)現(xiàn)最消耗處理器資源的竟然是“fork”系統(tǒng)調(diào)用(用來創(chuàng)建進(jìn)程);
- 3. 分析問題:Shell腳本是通過 Java 的
Runtime.getRuntime().exec()
方法創(chuàng)建大量進(jìn)程仪壮; - 4. 分析解決憨颠;
4.5 服務(wù)器虛擬機(jī)進(jìn)程崩潰
- 場景簡述:MIS 系統(tǒng)在與一個(gè) OA 門戶做了集成后,服務(wù)器運(yùn)行期間頻繁出現(xiàn)集群節(jié)點(diǎn)的虛擬機(jī)進(jìn)程自動關(guān)閉的現(xiàn)象积锅;
- 場景特點(diǎn):遠(yuǎn)程斷開連接異常爽彤、OA 門戶集成、異步調(diào)用缚陷;
- 問題原因:MIS 系統(tǒng)工作流待辦事項(xiàng)變化時(shí)适篙,使用異步調(diào)用 Web 服務(wù),通知 OA 門戶箫爷。兩邊服務(wù)速度不對等嚷节,時(shí)間越長越多 Web 服務(wù)沒有調(diào)用聂儒,等待線程和 Socket 連接越多;
- 解決思路:問題根源是異步調(diào)用導(dǎo)致線程過多硫痰,處理時(shí)間超過了設(shè)置的超時(shí)等待時(shí)間衩婚;可以從服務(wù)通信和超時(shí)等待兩方面優(yōu)化;
- 實(shí)際方案:將異步調(diào)用改為生產(chǎn)者/消費(fèi)者模式的消息隊(duì)列效斑;
-
調(diào)優(yōu)過程:
- 1. 發(fā)現(xiàn)問題:首先查看日志 -> 發(fā)現(xiàn)報(bào)大量相同的 Socket 重連異常(java.net.SocketException: Connection reset)非春;
- 2. 分析解決;
4.6 不恰當(dāng)數(shù)據(jù)結(jié)構(gòu)導(dǎo)致內(nèi)存占用過大
- 場景簡述:一個(gè)后臺 RPC 服務(wù)器缓屠,需要每 10 min 加載一個(gè)約 800MB 的 HashMap<Long奇昙,Long>Entry 類型的數(shù)據(jù)結(jié)構(gòu),在這段時(shí)間內(nèi)執(zhí)行 Minor GC 停頓較長時(shí)間敌完;
- 場景特點(diǎn):Map數(shù)據(jù)結(jié)構(gòu)储耐、長停頓 Minor GC;
-
問題原因:有兩方面滨溉。一來 800MB 的數(shù)據(jù)很快把 Eden 填滿引發(fā)垃圾收集弧岳,垃圾收集時(shí)這 800MB 數(shù)據(jù)重復(fù)復(fù)制到 Survivor 導(dǎo)致 Minor GC 時(shí)間長。二來
HashMap<Long业踏,Long>
類型 key 和 value 共占 2*8=16 字節(jié)禽炬,封裝成 Map.Entry 后多了 16 字節(jié)對象頭、8 字節(jié) next 字段和 4 字節(jié) int 類型的 hash 字段勤家,為了對其追加 4 字節(jié)空白對象頭腹尖,還有 8 字節(jié)對這個(gè) Map.Entry 的引用。最后實(shí)際耗費(fèi)的內(nèi)存為(Long(24byte)×2)+Entry(32byte)+HashMap Ref(8byte) = 88byte
伐脖,空間效率為:16 字節(jié) / 88 字節(jié) = 18% 太低热幔; - 解決思路:有兩方面的思路。一來可以將大對象盡早劃入老年代讼庇,二來可以優(yōu)化數(shù)據(jù)結(jié)構(gòu)绎巨;
-
實(shí)際方案:將新生代空間減少或使用親合式集群將大內(nèi)存劃進(jìn)老年代(類似 4.1)。除此之外還可以將 Survivor 空間去掉蠕啄,讓新生代中存活的對象在第一次 Minor GC 后立即進(jìn)入老年代场勤,等到 Major GC 的時(shí)候再去清理它們。最根本的方法是優(yōu)化數(shù)據(jù)結(jié)構(gòu)歼跟;
-
方案一:去掉 Survivor 空間和媳。具體來說:
- 1. 加入
參數(shù)-XX:SurvivorRatio=65536
、-XX:MaxTenuringThreshold=0
哈街; - 2. 或者
-XX:+Always-Tenure
留瞳;
- 1. 加入
- 方案二:優(yōu)化數(shù)據(jù)結(jié)構(gòu),需要具體的業(yè)務(wù)背景骚秦;
-
方案一:去掉 Survivor 空間和媳。具體來說:
-
調(diào)優(yōu)過程:
- 1. 發(fā)現(xiàn)問題:首先查看日志 -> 發(fā)現(xiàn)在每 10min 里她倘,Minor GC 會造成 500ms 停頓璧微;
- 2. 分析解決;
4.7 由 Windows 虛擬內(nèi)存導(dǎo)致的長時(shí)間停頓
- 場景簡述:GUI 程序使用內(nèi)存較小硬梁。在最小化時(shí)往毡,偶爾會出現(xiàn)長時(shí)間完全無日志輸出,程序處于停頓狀態(tài)靶溜。查看內(nèi)存發(fā)現(xiàn)在最小化時(shí)占用內(nèi)存大幅減小,但虛擬了留下來沒有變化懒震;
- 場景特點(diǎn):GUI 程序罩息、虛擬內(nèi)存、應(yīng)用最小化个扰;
- 問題原因:GUI 程序在應(yīng)用最小化時(shí)瓷炮,會將工作內(nèi)存交換到磁盤頁面文件中(修剪),在進(jìn)行垃圾回收前需要恢復(fù)工作頁面文件導(dǎo)致停頓递宅,進(jìn)而導(dǎo)致從準(zhǔn)備開始垃圾收集娘香,到真正開始之間所消耗的時(shí)間較長;
- 解決思路:由于 GUI 程序使用內(nèi)存較小办龄,不對其修剪烘绽。修剪的好處是內(nèi)存可用于其他應(yīng)用程序,缺點(diǎn)是在恢復(fù)工作集內(nèi)存時(shí)會有延遲俐填;
-
實(shí)際方案:在應(yīng)用程序最小化后阻止 JVM 對其進(jìn)行修剪安接。具體來說:
- 1.
-Dsun.awt.keepWorkingSetOnMinimize=true
;
- 1.
-
調(diào)優(yōu)過程:
- 1. 定位停頓問題:加入?yún)?shù)
-XX:+PrintGCApplicationStoppedTime-XX:+PrintGCDate-Stamps-Xloggc:gclog.log
-> 確認(rèn)了停頓確實(shí)是由垃圾收集導(dǎo)致英融; - 2. 定位停頓日志:添加
-XX:+PrintReferenceGC
參數(shù)盏檐,找到長時(shí)間停頓的具體日志信息 -> 發(fā)現(xiàn)從準(zhǔn)備開始收集,到真正開始收集之間所消耗的時(shí)間卻占了絕大部分驶悟; - 3. 分析解決胡野;
- 1. 定位停頓問題:加入?yún)?shù)
4.8 由安全點(diǎn)導(dǎo)致長時(shí)間停頓
- 場景簡述:一個(gè)使用 G1 收集器的離線 HBase 集群,有大量的 MapReduce 或 Spark 離線分析任務(wù)對其進(jìn)行訪問痕鳍,集群讀寫壓力較大硫豆。結(jié)果發(fā)現(xiàn)垃圾收集的停頓時(shí)間較長;
- 場景特點(diǎn):MapReduce 與 Spark 任務(wù)笼呆、垃圾收集時(shí)間短但空轉(zhuǎn)等待時(shí)間長够庙、可數(shù)循環(huán);
-
問題原因:HotSpot 虛擬機(jī)在認(rèn)為循環(huán)次數(shù)較少時(shí)抄邀,使用 int 類型或范圍更小
的數(shù)據(jù)類型作為索引值耘眨,不進(jìn)入安全點(diǎn)(具有讓程序長時(shí)間執(zhí)行的特征)。在 HBase 連接中有很多個(gè)Mapper / Reducer / Executer 線程境肾。清理這些線程靠一個(gè)連接超時(shí)清理的循環(huán)函數(shù)剔难, HotSpot 判斷這個(gè)循環(huán)函數(shù)為可數(shù)循環(huán)胆屿,等待循環(huán)全部跑完才能進(jìn)入安全點(diǎn),此時(shí)其他線程也必須一起等著偶宫,宏觀來看就是長時(shí)間停頓非迹; - 解決思路:連接超時(shí)清理的循環(huán)函數(shù)使用 int 索引因此被判斷為可數(shù)循環(huán),修改索引將其變?yōu)椴豢蓴?shù)循環(huán)即可纯趋;
- 實(shí)際方案:把循環(huán)索引的數(shù)據(jù)類型從int改為long即可憎兽;
-
調(diào)優(yōu)過程:
- 1. 發(fā)現(xiàn)問題:首先查看日志 -> 發(fā)現(xiàn)垃圾收集停頓時(shí)間長,但實(shí)際垃圾回收時(shí)間短吵冒;
- 2. 查看安全點(diǎn)日志:加入?yún)?shù)
-XX:+PrintSafepointStatistics
和-XX:PrintSafepointStatisticsCount=1
查看安全點(diǎn)日志 -> 發(fā)現(xiàn)虛擬機(jī)在等待所有用戶線程進(jìn)入安全點(diǎn)時(shí)有線程很慢纯命; - 3. 找到超時(shí)線程:添加
-XX: +SafepointTimeout
和-XX:SafepointTimeoutDelay=2000
兩個(gè)參數(shù),使虛擬機(jī)在等到線程進(jìn)入安全點(diǎn)的時(shí)間超過 2000 毫秒時(shí)就認(rèn)定為超時(shí) -> 輸出導(dǎo)致問題的線程名稱痹栖; - 4. 分析解決亿汞;
4.9 調(diào)優(yōu)總結(jié)
- 在實(shí)際工作中,我們可以直接將初始的堆大小與最大堆大小相等揪阿,這樣的好處是可以減少程序運(yùn)行時(shí)垃圾回收次數(shù)疗我,從而提高效率;
- 初始堆值和最大堆內(nèi)存內(nèi)存越大南捂,吞吐量就越高吴裤,但是也要根據(jù)自己電腦(服務(wù)器)的實(shí)際內(nèi)存來比較;
- 最好使用并行收集器溺健,因?yàn)椴⑿惺占魉俣缺却型掏铝扛呓滥Γ俣瓤臁.?dāng)然矿瘦,服務(wù)器一定要是多線程的枕面;
- 設(shè)置堆內(nèi)存新生代的比例和老年代的比例最好為 1:2 或者 1:3 。默認(rèn)的就是 1:2缚去;
- 減少 GC 對老年代的回收(老年代 GC 慢)潮秘。設(shè)置新生代垃圾對象最大年齡,盡量不要有大量連續(xù)內(nèi)存空間的 Java 對象易结,因?yàn)闀苯拥嚼夏甏碥瘢瑑?nèi)存不夠就會執(zhí)行 GC;
- 默認(rèn)的 JVM 堆大小是電腦實(shí)際內(nèi)存的四分之一左右搞动;