JVM奪命連環(huán)10問
說說JVM的內(nèi)存布局?

Java虛擬機(jī)主要包含幾個(gè)區(qū)域:
堆:堆Java虛擬機(jī)中最大的一塊內(nèi)存,是線程共享的內(nèi)存區(qū)域,基本上所有的對象實(shí)例數(shù)組都是在堆上分配空間。堆區(qū)細(xì)分為Yound區(qū)年輕代和Old區(qū)老年代,其中年輕代又分為Eden、S0、S1 3個(gè)部分,他們默認(rèn)的比例是8:1:1的大小。
棧:棧是線程私有的內(nèi)存區(qū)域,每個(gè)方法執(zhí)行的時(shí)候都會(huì)在棧創(chuàng)建一個(gè)棧幀,方法的調(diào)用過程就對應(yīng)著棧的入棧和出棧的過程。每個(gè)棧幀的結(jié)構(gòu)又包含局部變量表、操作數(shù)棧、動(dòng)態(tài)連接、方法返回地址。
局部變量表用于存儲(chǔ)方法參數(shù)和局部變量。當(dāng)?shù)谝粋€(gè)方法被調(diào)用的時(shí)候,他的參數(shù)會(huì)被傳遞至從0開始的連續(xù)的局部變量表中。
操作數(shù)棧用于一些字節(jié)碼指令從局部變量表中傳遞至操作數(shù)棧,也用來準(zhǔn)備方法調(diào)用的參數(shù)以及接收方法返回結(jié)果。
動(dòng)態(tài)連接用于將符號(hào)引用表示的方法轉(zhuǎn)換為實(shí)際方法的直接引用。
元數(shù)據(jù):在Java1.7之前,包含方法區(qū)的概念,常量池就存在于方法區(qū)(永久代)中,而方法區(qū)本身是一個(gè)邏輯上的概念,在1.7之后則是把常量池移到了堆內(nèi),1.8之后移出了永久代的概念(方法區(qū)的概念仍然保留),實(shí)現(xiàn)方式則是現(xiàn)在的元數(shù)據(jù)。它包含類的元信息和運(yùn)行時(shí)常量池。
Class文件就是類和接口的定義信息。
運(yùn)行時(shí)常量池就是類和接口的常量池運(yùn)行時(shí)的表現(xiàn)形式。
本地方法棧:主要用于執(zhí)行本地native方法的區(qū)域
程序計(jì)數(shù)器:也是線程私有的區(qū)域,用于記錄當(dāng)前線程下虛擬機(jī)正在執(zhí)行的字節(jié)碼的指令地址
知道new一個(gè)對象的過程嗎?

當(dāng)虛擬機(jī)遇見new關(guān)鍵字時(shí)候,實(shí)現(xiàn)判斷當(dāng)前類是否已經(jīng)加載,如果類沒有加載,首先執(zhí)行類的加載機(jī)制,加載完成后再為對象分配空間、初始化等。
首先校驗(yàn)當(dāng)前類是否被加載,如果沒有加載,執(zhí)行類加載機(jī)制
加載:就是從字節(jié)碼加載成二進(jìn)制流的過程
驗(yàn)證:當(dāng)然加載完成之后,當(dāng)然需要校驗(yàn)Class文件是否符合虛擬機(jī)規(guī)范,跟我們接口請求一樣,第一件事情當(dāng)然是先做個(gè)參數(shù)校驗(yàn)了
準(zhǔn)備:為靜態(tài)變量、常量賦默認(rèn)值
解析:把常量池中符號(hào)引用(以符號(hào)描述引用的目標(biāo))替換為直接引用(指向目標(biāo)的指針或者句柄等)的過程
初始化:執(zhí)行static代碼塊(cinit)進(jìn)行初始化,如果存在父類,先對父類進(jìn)行初始化
Ps:靜態(tài)代碼塊是絕對線程安全的,只能隱式被java虛擬機(jī)在類加載過程中初始化調(diào)用!(此處該有問題static代碼塊線程安全嗎?)
當(dāng)類加載完成之后,緊接著就是對象分配內(nèi)存空間和初始化的過程
首先為對象分配合適大小的內(nèi)存空間
接著為實(shí)例變量賦默認(rèn)值
設(shè)置對象的頭信息,對象hash碼、GC分代年齡、元數(shù)據(jù)信息等
執(zhí)行構(gòu)造函數(shù)(init)初始化
?
知道雙親委派模型嗎?
類加載器自頂向下分為:
Bootstrap ClassLoader啟動(dòng)類加載器:默認(rèn)會(huì)去加載JAVA_HOME/lib目錄下的jar
Extention ClassLoader擴(kuò)展類加載器:默認(rèn)去加載JAVA_HOME/lib/ext目錄下的jar
Application ClassLoader應(yīng)用程序類加載器:比如我們的web應(yīng)用,會(huì)加載web程序中ClassPath下的類
User ClassLoader用戶自定義類加載器:由用戶自己定義
當(dāng)我們在加載類的時(shí)候,首先都會(huì)向上詢問自己的父加載器是否已經(jīng)加載,如果沒有則依次向上詢問,如果沒有加載,則從上到下依次嘗試是否能加載當(dāng)前類,直到加載成功。

?
說說有哪些垃圾回收算法?
標(biāo)記-清除
統(tǒng)一標(biāo)記出需要回收的對象,標(biāo)記完成之后統(tǒng)一回收所有被標(biāo)記的對象,而由于標(biāo)記的過程需要遍歷所有的GC ROOT,清除的過程也要遍歷堆中所有的對象,所以標(biāo)記-清除算法的效率低下,同時(shí)也帶來了內(nèi)存碎片的問題。
復(fù)制算法
為了解決性能的問題,復(fù)制算法應(yīng)運(yùn)而生,它將內(nèi)存分為大小相等的兩塊區(qū)域,每次使用其中的一塊,當(dāng)一塊內(nèi)存使用完之后,將還存活的對象拷貝到另外一塊內(nèi)存區(qū)域中,然后把當(dāng)前內(nèi)存清空,這樣性能和內(nèi)存碎片的問題得以解決。但是同時(shí)帶來了另外一個(gè)問題,可使用的內(nèi)存空間縮小了一半!
因此,誕生了我們現(xiàn)在的常見的年輕代+老年代的內(nèi)存結(jié)構(gòu):Eden+S0+S1組成,因?yàn)楦鶕?jù)IBM的研究顯示,98%的對象都是朝生夕死,所以實(shí)際上存活的對象并不是很多,完全不需要用到一半內(nèi)存浪費(fèi),所以默認(rèn)的比例是8:1:1。
這樣,在使用的時(shí)候只使用Eden區(qū)和S0S1中的一個(gè),每次都把存活的對象拷貝另外一個(gè)未使用的Survivor區(qū),同時(shí)清空Eden和使用的Survivor,這樣下來內(nèi)存的浪費(fèi)就只有10%了。
如果最后未使用的Survivor放不下存活的對象,這些對象就進(jìn)入Old老年代了。
PS:所以有一些初級(jí)點(diǎn)的問題會(huì)問你為什么要分為Eden區(qū)和2個(gè)Survior區(qū)?有什么作用?就是為了節(jié)省內(nèi)存和解決內(nèi)存碎片的問題,這些算法都是為了解決問題而產(chǎn)生的,如果理解原因你就不需要死記硬背了
標(biāo)記-整理
針對老年代再用復(fù)制算法顯然不合適,因?yàn)檫M(jìn)入老年代的對象都存活率比較高了,這時(shí)候再頻繁的復(fù)制對性能影響就比較大,而且也不會(huì)再有另外的空間進(jìn)行兜底。所以針對老年代的特點(diǎn),通過標(biāo)記-整理算法,標(biāo)記出所有的存活對象,讓所有存活的對象都向一端移動(dòng),然后清理掉邊界以外的內(nèi)存空間。
?
那么什么是GC ROOT?有哪些GC ROOT?
上面提到的標(biāo)記的算法,怎么標(biāo)記一個(gè)對象是否存活?簡單的通過引用計(jì)數(shù)法,給對象設(shè)置一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用他,就給計(jì)數(shù)器+1,反之則計(jì)數(shù)器-1,但是這個(gè)簡單的算法無法解決循環(huán)引用的問題。
Java通過可達(dá)性分析算法來達(dá)到標(biāo)記存活對象的目的,定義一系列的GC ROOT為起點(diǎn),從起點(diǎn)開始向下開始搜索,搜索走過的路徑稱為引用鏈,當(dāng)一個(gè)對象到GC ROOT沒有任何引用鏈相連的話,則對象可以判定是可以被回收的。
而可以作為GC ROOT的對象包括:
棧中引用的對象 靜態(tài)變量、常量引用的對象 本地方法棧native方法引用的對象
?
垃圾回收器了解嗎?年輕代和老年代都有哪些垃圾回收器?

初始標(biāo)記:標(biāo)記GC ROOT能關(guān)聯(lián)到的對象,需要STW 并發(fā)標(biāo)記:從GCRoots的直接關(guān)聯(lián)對象開始遍歷整個(gè)對象圖的過程,不需要STW 重新標(biāo)記:為了修正并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生改變的標(biāo)記,需要STW 并發(fā)清除:清理刪除掉標(biāo)記階段判斷的已經(jīng)死亡的對象,不需要STW

初始標(biāo)記:標(biāo)記GC ROOT能關(guān)聯(lián)到的對象,需要STW 并發(fā)標(biāo)記:從GCRoots的直接關(guān)聯(lián)對象開始遍歷整個(gè)對象圖的過程,掃描完成后還會(huì)重新處理并發(fā)標(biāo)記過程中產(chǎn)生變動(dòng)的對象 最終標(biāo)記:短暫暫停用戶線程,再處理一次,需要STW 篩選回收:更新Region的統(tǒng)計(jì)數(shù)據(jù),對每個(gè)Region的回收價(jià)值和成本排序,根據(jù)用戶設(shè)置的停頓時(shí)間制定回收計(jì)劃。再把需要回收的Region中存活對象復(fù)制到空的Region,同時(shí)清理舊的Region。需要STW

jstat -gcutil或者查看gc.log日志,查看內(nèi)存回收情況


dump出內(nèi)存文件在具體分析,比如通過jmap命令jmap -dump:format=b,file=dumpfile pid,導(dǎo)出之后再通過Eclipse Memory Analyzer等工具進(jìn)行分析,定位到代碼,修復(fù)
找到當(dāng)前進(jìn)程的pid,top -p pid -H 查看資源占用,找到線程 printf “%x\n” pid,把線程pid轉(zhuǎn)為16進(jìn)制,比如0x32d jstack pid|grep -A 10 0x32d查看線程的堆棧日志,還找不到問題繼續(xù) dump出內(nèi)存文件用MAT等工具進(jìn)行分析,定位到代碼,修復(fù)
JVM調(diào)優(yōu)有什么經(jīng)驗(yàn)嗎?
簡單的參數(shù)含義

-Xms設(shè)置初始堆的大小,-Xmx設(shè)置最大堆的大小 -XX:NewSize年輕代大小,-XX:MaxNewSize年輕代最大值,-Xmn則是相當(dāng)于同時(shí)配置-XX:NewSize和-XX:MaxNewSize為一樣的值 -XX:NewRatio設(shè)置年輕代和年老代的比值,如果為3,表示年輕代與老年代比值為1:3,默認(rèn)值為2 -XX:SurvivorRatio年輕代和兩個(gè)Survivor的比值,默認(rèn)8,代表比值為8:1:1 -XX:PretenureSizeThreshold 當(dāng)創(chuàng)建的對象超過指定大小時(shí),直接把對象分配在老年代。 -XX:MaxTenuringThreshold設(shè)定對象在Survivor復(fù)制的最大年齡閾值,超過閾值轉(zhuǎn)移到老年代 -XX:MaxDirectMemorySize當(dāng)Direct ByteBuffer分配的堆外內(nèi)存到達(dá)指定大小后,即觸發(fā)Full GC
調(diào)優(yōu)
為了打印日志方便排查問題最好開啟GC日志,開啟GC日志對性能影響微乎其微,但是能幫助我們快速排查定位問題。-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:gc.log 一般設(shè)置-Xms=-Xmx,這樣可以獲得固定大小的堆內(nèi)存,減少GC的次數(shù)和耗時(shí),可以使得堆相對穩(wěn)定 -XX:+HeapDumpOnOutOfMemoryError讓JVM在發(fā)生內(nèi)存溢出的時(shí)候自動(dòng)生成內(nèi)存快照,方便排查問題 -Xmn設(shè)置新生代的大小,太小會(huì)增加YGC,太大會(huì)減小老年代大小,一般設(shè)置為整個(gè)堆的1/4到1/3 設(shè)置-XX:+DisableExplicitGC禁止系統(tǒng)System.gc(),防止手動(dòng)誤觸發(fā)FGC造成問題
有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號(hào)
好文章,我在看??
