JVM的內(nèi)存布局?
Java虛擬機(jī)主要包含幾個(gè)區(qū)域:
堆:Java 堆是所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建柴信。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例以及數(shù)組都在這里分配內(nèi)存蚯舱。堆區(qū)細(xì)分為Yound區(qū)年輕代和Old區(qū)老年代,其中年輕代又分為Eden掩蛤、S0枉昏、S1 3個(gè)部分,他們默認(rèn)的比例是8:1:1的大小揍鸟。
虛擬機(jī)棧:虛擬機(jī)棧是線程私有的內(nèi)存區(qū)域兄裂,每個(gè)方法執(zhí)行的時(shí)候都會(huì)在棧創(chuàng)建一個(gè)棧幀,方法的調(diào)用過(guò)程就對(duì)應(yīng)著棧的入棧和出棧的過(guò)程阳藻。每個(gè)棧幀的結(jié)構(gòu)又包含局部變量表晰奖、操作數(shù)棧、動(dòng)態(tài)連接腥泥、方法返回地址匾南。
局部變量表用于存儲(chǔ)方法參數(shù)和局部變量。
操作數(shù)棧用于一些字節(jié)碼指令從局部變量表中傳遞至操作數(shù)棧蛔外,也用來(lái)準(zhǔn)備方法調(diào)用的參數(shù)以及接收方法返回結(jié)果蛆楞。
動(dòng)態(tài)連接用于將符號(hào)引用表示的方法轉(zhuǎn)換為實(shí)際方法的直接引用溯乒。
方法區(qū):方法區(qū)與 Java 堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域豹爹,它用于存儲(chǔ)已被虛擬機(jī)加載的類信息裆悄、常量、靜態(tài)變量臂聋、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)光稼。在Java1.7之前,永久代包含方法區(qū)孩等,常量池就存在于方法區(qū)(永久代)中艾君,而方法區(qū)本身是一個(gè)邏輯上的概念,在1.7之后則是把常量池移到了堆內(nèi)瞎访,為了更容易管理方法區(qū)腻贰,從 JDK 1.8 開始,移除永久代扒秸,并把方法區(qū)移至元空間,它位于直接內(nèi)存中冀瓦,而不是虛擬機(jī)內(nèi)存中伴奥。方法區(qū)是一個(gè) JVM 規(guī)范,永久代與元空間都是其一種實(shí)現(xiàn)方式翼闽。在 JDK 1.8 之后拾徙,原來(lái)永久代的數(shù)據(jù)被分到了堆和元空間中。元空間存儲(chǔ)類的元信息感局,靜態(tài)變量和常量池等放入堆中尼啡。
本地方法棧:類似于虛擬機(jī)棧,主要用于執(zhí)行本地native方法的區(qū)域
程序計(jì)數(shù)器:也是線程私有的區(qū)域询微,用于記錄當(dāng)前線程下虛擬機(jī)正在執(zhí)行的字節(jié)碼的指令地址崖瞭。
類加載過(guò)程?
加載:通過(guò)全類名獲取定義此類的二進(jìn)制字節(jié)流撑毛,并將字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)书聚,在內(nèi)存中生成一個(gè)代表該類的 Class 對(duì)象,作為方法區(qū)這些數(shù)據(jù)的訪問(wèn)入口
驗(yàn)證:校驗(yàn)Class文件是否符合虛擬機(jī)規(guī)范
準(zhǔn)備:為類變量分配內(nèi)存并設(shè)置類變量初始值
解析:虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程,也就是得到類或者字段藻雌、方法在內(nèi)存中的指針或者偏移量雌续。
初始化:執(zhí)行初始化方法 <clinit> ()方法進(jìn)行初始化,如果存在父類胯杭,先對(duì)父類進(jìn)行初始化
一個(gè)對(duì)象的創(chuàng)建過(guò)程(new)驯杜?
類加載檢查:虛擬機(jī)遇到一條 new 指令時(shí),首先將去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到這個(gè)類的符號(hào)引用做个,并且檢查這個(gè)符號(hào)引用代表的類是否已被加載過(guò)鸽心。如果沒(méi)有腔呜,那必須先執(zhí)行相應(yīng)的類加載過(guò)程。
分配內(nèi)存:在類加載檢查通過(guò)后再悼,接下來(lái)虛擬機(jī)將為新生對(duì)象分配內(nèi)存核畴。對(duì)象所需的內(nèi)存大小在類加載完成后便可確定,為對(duì)象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從 Java 堆中劃分出來(lái)(分配方式有 “指針碰撞” 和 “空閑列表” 兩種)冲九。
初始化零值:將對(duì)象的實(shí)例字段初始化為0
設(shè)置對(duì)象頭:虛擬機(jī)要對(duì)對(duì)象進(jìn)行必要的設(shè)置谤草,哈希碼、 GC 分代年齡莺奸、元數(shù)據(jù)信息等丑孩。
執(zhí)行init方法:執(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)用程序類加載器:當(dāng)前應(yīng)用classpathClassPath下的類
User ClassLoader用戶自定義類加載器:由用戶自己定義
雙親委派模型:
1温学、先檢查類是否已經(jīng)被加載過(guò)
2、若沒(méi)有加載則調(diào)用父加載器的loadClass()方法進(jìn)行加載
3甚疟、若父加載器為空則默認(rèn)使用啟動(dòng)類加載器作為父加載器仗岖。
4、如果父類加載失敗览妖,拋出ClassNotFoundException異常后轧拄,再調(diào)用自己的findClass()方法進(jìn)行加載。
好處:雙親委派模型保證了Java程序的穩(wěn)定運(yùn)行讽膏,可以避免類的重復(fù)加載(JVM 區(qū)分不同類的方式不僅僅根據(jù)類名檩电,相同的類文件被不同的類加載器加載產(chǎn)生的是兩個(gè)不同的類)。如果沒(méi)有使用雙親委派模型府树,而是每個(gè)類加載器加載自己的話就會(huì)出現(xiàn)一些問(wèn)題俐末,比如我們編寫一個(gè)稱為 java.lang.Object 類的話,那么程序運(yùn)行的時(shí)候奄侠,系統(tǒng)就會(huì)出現(xiàn)多個(gè)不同的 Object 類卓箫。另外,通過(guò)雙親委派的方式遭铺,還保證了安全性丽柿。因?yàn)锽ootstrap ClassLoader在加載的時(shí)候,只會(huì)加載JAVA_HOME/lib中的jar包里面的類魂挂,如java.lang.Integer甫题,那么這個(gè)類是不會(huì)被隨意替換的,除非有人跑到你的機(jī)器上涂召, 破壞你的JDK坠非。那么,就可以避免有人自定義一個(gè)有破壞功能的java.lang.Integer被加載果正。這樣可以有效的防止核心Java API被篡改炎码。
如何破壞雙親委派模型盟迟?
因?yàn)殡p親委派過(guò)程都是在loadClass方法中實(shí)現(xiàn)的,那么想要破壞這種機(jī)制潦闲,那么就自定義一個(gè)類加載器攒菠,重寫其中的loadClass方法,使其不進(jìn)行雙親委派即可歉闰。如果你想定義一個(gè)自己的類加載器辖众,并且要遵守雙親委派模型,那么可以繼承ClassLoader和敬,并且在findClass中實(shí)現(xiàn)你自己的加載邏輯即可凹炸。
破壞雙親委派機(jī)制的場(chǎng)景:
第一種被破壞的情況是在雙親委派出現(xiàn)之前。由于雙親委派模型是在JDK1.2之后才被引入的昼弟,而在這之前已經(jīng)有用戶自定義類加載器在用了啤它。所以,這些是沒(méi)有遵守雙親委派原則的舱痘。
第二種变骡,是JNDI、JDBC等需要加載SPI接口實(shí)現(xiàn)類的情況:DriverManager.getConnection
若按照雙親委任模型的話衰粹, DriverManager被Bootstrap類加載器加載锣光,其內(nèi)部引用到的connection也理應(yīng)由Bootstrap類加載器加載,但Driver.class的實(shí)現(xiàn)類不在Bootstrap類加載器所能掃描到的范圍里铝耻,所以交由Bootstrap類加載器是加載不了的,而雙親委任模型規(guī)定是先交由父類去加載蹬刷,加載不了再由自己加載瓢捉,而Bootstrap類加載器是最頂層的類加載器了,沒(méi)有父類了办成,所以交由自己加載泡态,但是自己也加載不了,所以為了能夠加載到Driver.class的實(shí)現(xiàn)類迂卢,只能打破雙親委任模型了【使用子類加載器去加載Driver.class的實(shí)現(xiàn)類】某弦,通過(guò)Thread.currentThread().getContextClassLoader()的方式,得到ApplicationClassLoader而克,而ApplicationClassLoader就能夠掃描到添加進(jìn)來(lái)的jar包【依賴的jar包都是在ClassPath下】靶壮,所以Driver.class的實(shí)現(xiàn)類就能夠被加載到。
第三種是為了實(shí)現(xiàn)熱插拔熱部署工具员萍。為了讓代碼動(dòng)態(tài)生效而無(wú)需重啟腾降,實(shí)現(xiàn)方式時(shí)把模塊連同類加載器一起換掉就實(shí)現(xiàn)了代碼的熱替換。
第四種是tomcat等web容器的出現(xiàn):
一個(gè)web容器可能需要部署多個(gè)應(yīng)用程序碎绎。不同的應(yīng)用程序可能會(huì)依賴同一個(gè)第三方類庫(kù)的不同版本螃壤,但是不同版本的類庫(kù)中某一個(gè)類的全路徑名可能是一樣的抗果。如多個(gè)應(yīng)用都要依賴xxx.jar,但是A應(yīng)用需要依賴1.0.0版本奸晴,但是B應(yīng)用需要依賴1.0.1版本冤馏。這兩個(gè)版本中都有一個(gè)類是com.xxx.Test.class。如果采用默認(rèn)的雙親委派類加載機(jī)制寄啼,那么是無(wú)法加載多個(gè)相同的類逮光。所以,Tomcat破壞雙親委派原則辕录,提供隔離的機(jī)制睦霎,為每個(gè)web容器單獨(dú)提供一個(gè)WebAppClassLoader加載器。Tomcat的類加載機(jī)制:為了實(shí)現(xiàn)隔離性走诞,優(yōu)先加載 Web 應(yīng)用自己定義的類副女,所以沒(méi)有遵照雙親委派的約定,每一個(gè)應(yīng)用自己的類加載器——WebAppClassLoader負(fù)責(zé)加載本身的目錄下的class文件蚣旱,加載不到時(shí)再交給CommonClassLoader加載碑幅,這和雙親委派剛好相反。
第五種是OSGI塞绿、Jigsaw等模塊化技術(shù)的應(yīng)用
如何判斷對(duì)象是否死亡(可回收)沟涨?
-
引用計(jì)數(shù)法
給對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它异吻,計(jì)數(shù)器就加 1裹赴;當(dāng)引用失效,計(jì)數(shù)器就減 1诀浪;任何時(shí)候計(jì)數(shù)器為 0 的對(duì)象就是不可能再被使用的棋返。這個(gè)方法實(shí)現(xiàn)簡(jiǎn)單,效率高雷猪,但是它很難解決對(duì)象之間相互循環(huán)引用的問(wèn)題睛竣。
-
可達(dá)性分析算法
Java通過(guò)可達(dá)性分析算法來(lái)達(dá)到標(biāo)記存活對(duì)象的目的,定義一系列的GC ROOT為起點(diǎn)求摇,從起點(diǎn)開始向下開始搜索射沟,搜索走過(guò)的路徑稱為引用鏈,當(dāng)一個(gè)對(duì)象到GC ROOT沒(méi)有任何引用鏈相連的話与境,則對(duì)象可以判定是可以被回收的验夯。
可作為 GC Roots 的對(duì)象包括下面幾種:
虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象
本地方法棧(Native 方法)中引用的對(duì)象
方法區(qū)中靜態(tài)變量引用的對(duì)象
方法區(qū)中常量引用的對(duì)象
所有被同步鎖持有的對(duì)象
-
回收對(duì)象的兩次標(biāo)記過(guò)程
一次標(biāo)記:當(dāng)可達(dá)性分析確認(rèn)該對(duì)象沒(méi)有引用鏈與GC Roots相連,則對(duì)其進(jìn)行第一次標(biāo)記和篩選嚷辅,篩選的條件是重寫了finalize()方法并沒(méi)有執(zhí)行過(guò)簿姨,對(duì)于重寫了且并沒(méi)有執(zhí)行finalize()方法的對(duì)象這將其放置在一個(gè)F-Queue隊(duì)列中,并在稍后由一個(gè)由虛擬機(jī)自動(dòng)建立的低優(yōu)先級(jí)的Finalizer線程去執(zhí)行它。此處執(zhí)行只保證執(zhí)行該方法扁位,但是不保證等待該方法執(zhí)行結(jié)束准潭,之所以這樣子設(shè)計(jì)是為了系統(tǒng)的穩(wěn)定性和健壯性考慮,以免該方法執(zhí)行時(shí)間較長(zhǎng)或者死循環(huán)導(dǎo)致系統(tǒng)崩潰域仇。在此之后刑然,系統(tǒng)會(huì)對(duì)對(duì)象進(jìn)行第二次標(biāo)記,如果在第一次標(biāo)記之后的對(duì)象在執(zhí)行finalize()方法時(shí)沒(méi)有被引用到一個(gè)新的變量暇务,這該對(duì)象將被回收掉泼掠。
垃圾回收算法?
-
標(biāo)記-清除
先根據(jù)可達(dá)性算法統(tǒng)一標(biāo)記出需要回收的對(duì)象垦细,標(biāo)記完成之后統(tǒng)一回收所有被標(biāo)記的對(duì)象择镇,而由于標(biāo)記的過(guò)程需要遍歷所有的GC ROOT,清除的過(guò)程也要遍歷堆中所有的對(duì)象括改,所以標(biāo)記-清除算法的效率低下腻豌,同時(shí)也帶來(lái)了內(nèi)存碎片的問(wèn)題
-
復(fù)制算法
為了解決上面的問(wèn)題,復(fù)制算法應(yīng)運(yùn)而生嘱能,它將內(nèi)存分為大小相等的兩塊區(qū)域吝梅,每次使用其中的一塊,當(dāng)一塊內(nèi)存使用完之后惹骂,將還存活的對(duì)象拷貝到另外一塊內(nèi)存區(qū)域中苏携,然后把當(dāng)前內(nèi)存清空,這樣性能和內(nèi)存碎片的問(wèn)題得以解決对粪。但是同時(shí)帶來(lái)了另外一個(gè)問(wèn)題右冻,可使用的內(nèi)存空間縮小了一半。
-
標(biāo)記整理
前面兩步和標(biāo)記清除法一樣著拭,不同的是它在標(biāo)記清除法的基礎(chǔ)上添加了一個(gè)整理的過(guò)程 国旷,即將所有的存活對(duì)象都往一端移動(dòng),緊鄰排列,再清理掉另一端的所有區(qū)域茫死,這樣的話就解決了內(nèi)存碎片的問(wèn)題。
-
分代收集算法
當(dāng)前虛擬機(jī)的垃圾收集都采用分代收集算法履羞,這種算法沒(méi)有什么新的思想峦萎,只是根據(jù)對(duì)象存活周期的不同將內(nèi)存分為幾塊。一般將 java 堆分為新生代和老年代忆首,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)選擇合適的垃圾收集算法爱榔。比如在新生代中,每次收集都會(huì)有大量對(duì)象死去糙及,所以可以選擇”標(biāo)記-復(fù)制“算法详幽,只需要付出少量對(duì)象的復(fù)制成本就可以完成每次垃圾收集。而老年代的對(duì)象存活幾率是比較高的,而且沒(méi)有額外的空間對(duì)它進(jìn)行分配擔(dān)保唇聘,所以必須選擇“標(biāo)記-清除”或“標(biāo)記-整理”算法進(jìn)行垃圾收集版姑。
垃圾收集器
- Serial 收集器
單線程版本收集器,進(jìn)行垃圾回收的時(shí)候會(huì)STW(Stop The World)迟郎,也就是進(jìn)行垃圾回收的時(shí)候其他的工作線程都必須暫停剥险,采用復(fù)制算法。 - ParNew 收集器
Serial的多線程版本宪肖,用于和CMS配合使用表制。 - Parallel Scavenge
Parallel Scavenge 收集器也是一個(gè)使用復(fù)制算法,多線程控乾,工作于新生代的垃圾收集器么介。Parallel Scavenge 收集器關(guān)注點(diǎn)是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的關(guān)注點(diǎn)更多的是用戶線程的停頓時(shí)間(提高用戶體驗(yàn))蜕衡。所謂吞吐量就是 CPU 中用于運(yùn)行用戶代碼的時(shí)間與 CPU 總消耗時(shí)間的比值壤短,也就是說(shuō) CMS 等垃圾收集器更適合用到與用戶交互的程序,因?yàn)橥nD時(shí)間越短衷咽,用戶體驗(yàn)越好鸽扁,而 Parallel Scavenge 收集器關(guān)注的是吞吐量,所以更適合做后臺(tái)運(yùn)算等不需要太多用戶交互的任務(wù)镶骗。JDK1.8 默認(rèn)使用的是 Parallel Scavenge + Parallel Old桶现。
- Serial Old 收集器
Serial 收集器的老年代版本,它同樣是一個(gè)單線程收集器鼎姊,使用標(biāo)記-整理算法骡和。它主要有兩大用途:一種用途是在 JDK1.5 以及以前的版本中與 Parallel Scavenge 收集器搭配使用,另一種用途是作為 CMS 收集器的后備方案相寇。 - Parallel Old收集器
Parallel Scavenge 收集器的老年代版本慰于。使用多線程和“標(biāo)記-整理”算法。在注重吞吐量以及 CPU 資源的場(chǎng)合唤衫,都可以優(yōu)先考慮 Parallel Scavenge 收集器和 Parallel Old 收集器婆赠。 - CMS收集器
CMS 收集器是以實(shí)現(xiàn)最短 STW 時(shí)間為目標(biāo)的收集器,如果應(yīng)用很重視服務(wù)的響應(yīng)速度佳励,希望給用戶最好的體驗(yàn)休里,則 CMS 收集器是個(gè)很不錯(cuò)的選擇!CMS 雖然工作于老年代赃承,但采用的是標(biāo)記清除法妙黍,主要有以下四個(gè)步驟:
初始標(biāo)記:標(biāo)記GC ROOT能關(guān)聯(lián)到的對(duì)象,需要STW
并發(fā)標(biāo)記:從GCRoots的直接關(guān)聯(lián)對(duì)象開始遍歷整個(gè)對(duì)象圖的過(guò)程瞧剖,不需要STW
重新標(biāo)記:為了修正并發(fā)標(biāo)記期間拭嫁,因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生改變的標(biāo)記可免,需要STW
并發(fā)清除:清理刪除掉標(biāo)記階段判斷的已經(jīng)死亡的對(duì)象,不需要STW
從整個(gè)過(guò)程來(lái)看做粤,并發(fā)標(biāo)記和并發(fā)清除的耗時(shí)最長(zhǎng)浇借,但是不需要停止用戶線程,而初始標(biāo)記和重新標(biāo)記的耗時(shí)較短驮宴,但是需要停止用戶線程逮刨,總體而言,整個(gè)過(guò)程造成的停頓時(shí)間較短堵泽,大部分時(shí)候是可以和用戶線程一起工作的修己。三個(gè)缺點(diǎn):
對(duì) CPU 資源敏感;
無(wú)法處理浮動(dòng)垃圾迎罗;
它使用的回收算法-“標(biāo)記-清除”算法會(huì)導(dǎo)致收集結(jié)束時(shí)會(huì)有大量空間碎片產(chǎn)生睬愤。 - G1收集器
G1作為JDK9之后的服務(wù)端默認(rèn)收集器,且不再區(qū)分年輕代和老年代進(jìn)行垃圾回收纹安,他把內(nèi)存劃分為多個(gè)Region尤辱。對(duì)于大對(duì)象的存儲(chǔ)則衍生出Humongous的概念,超過(guò)Region大小一半的對(duì)象會(huì)被認(rèn)為是大對(duì)象厢岂,而超過(guò)整個(gè)Region大小的對(duì)象被認(rèn)為是超級(jí)大對(duì)象光督,將會(huì)被存儲(chǔ)在連續(xù)的N個(gè)Humongous Region中,G1在進(jìn)行回收的時(shí)候會(huì)在后臺(tái)維護(hù)一個(gè)優(yōu)先級(jí)列表塔粒,每次根據(jù)用戶設(shè)定允許的收集停頓時(shí)間優(yōu)先回收收益最大的Region结借。
G1的回收過(guò)程分為以下四個(gè)步驟:
初始標(biāo)記:標(biāo)記GC ROOT能關(guān)聯(lián)到的對(duì)象,需要STW
并發(fā)標(biāo)記:從GCRoots的直接關(guān)聯(lián)對(duì)象開始遍歷整個(gè)對(duì)象圖的過(guò)程卒茬,掃描完成后還會(huì)重新處理并發(fā)標(biāo)記過(guò)程中產(chǎn)生變動(dòng)的對(duì)象
最終標(biāo)記:短暫暫停用戶線程船老,再處理一次,需要STW
篩選回收:更新Region的統(tǒng)計(jì)數(shù)據(jù)圃酵,對(duì)每個(gè)Region的回收價(jià)值和成本排序柳畔,根據(jù)用戶設(shè)置的停頓時(shí)間制定回收計(jì)劃。再把需要回收的Region中存活對(duì)象復(fù)制到空的Region郭赐,同時(shí)清理舊的Region薪韩。需要STW
總的來(lái)說(shuō)除了并發(fā)標(biāo)記之外,其他幾個(gè)過(guò)程也還是需要短暫的STW捌锭,G1的目標(biāo)是在停頓和延遲可控的情況下盡可能提高吞吐量躬存。
G1 優(yōu)點(diǎn):
停頓時(shí)間短;
用戶可以指定最大停頓時(shí)間舀锨;
不會(huì)產(chǎn)生內(nèi)存碎片:G1 的內(nèi)存布局并不是固定大小以及固定數(shù)量的分代區(qū)域劃分,而是把連續(xù)的Java堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域 (Region)宛逗,G1 從整體來(lái)看是基于“標(biāo)記-整理”算法實(shí)現(xiàn)的收集器坎匿,但從局部 (兩個(gè)Region 之間)上看又是基于“標(biāo)記-復(fù)制”算法實(shí)現(xiàn),不會(huì)像 CMS (“標(biāo)記-清除”算法) 那樣產(chǎn)生內(nèi)存碎片。
G1 缺點(diǎn):
G1 需要記憶集 (具體來(lái)說(shuō)是卡表)來(lái)記錄新生代和老年代之間的引用關(guān)系替蔬,這種數(shù)據(jù)結(jié)構(gòu)在 G1 中需要占用大量的內(nèi)存告私,可能達(dá)到整個(gè)堆內(nèi)存容量的 20% 甚至更多。而且 G1 中維護(hù)記憶集的成本較高承桥,帶來(lái)了更高的執(zhí)行負(fù)載驻粟,影響效率。
內(nèi)存分配策略
- 多數(shù)情況凶异,對(duì)象都在新生代 Eden 區(qū)分配蜀撑。當(dāng) Eden 區(qū)分配沒(méi)有足夠的空間進(jìn)行分配時(shí),虛擬機(jī)將會(huì)發(fā)起一次 Minor GC剩彬。如果本次 GC 后還是沒(méi)有足夠的空間酷麦,則將啟用分配擔(dān)保機(jī)制在老年代中分配內(nèi)存(這里是把已經(jīng)在的對(duì)象轉(zhuǎn)移到老年代,要分配的新對(duì)象還是在會(huì)存在Eden)喉恋。
- Minor GC 是指發(fā)生在新生代的 GC沃饶,因?yàn)?Java 對(duì)象大多都是朝生夕死,所有 Minor GC 非常頻繁轻黑,一般回收速度也非澈簦快;
- Major GC/Full GC 是指發(fā)生在老年代的 GC氓鄙,出現(xiàn)了 Major GC 通常會(huì)伴隨至少一次 Minor GC馆揉。Major GC 的速度通常會(huì)比 Minor GC 慢 10 倍以上。
大對(duì)象直接進(jìn)入老年代
所謂大對(duì)象是指需要大量連續(xù)內(nèi)存空間的對(duì)象(虛擬機(jī)提供參數(shù)-XX:PretenureSizeThreshold參數(shù)(這個(gè)參數(shù)只在serial和ParNew起作用)玖详,當(dāng)對(duì)象比這個(gè)值大把介,就直接存入老年代),頻繁出現(xiàn)大對(duì)象是致命的蟋座,會(huì)導(dǎo)致在內(nèi)存還有不少空間的情況下提前觸發(fā) GC 以獲取足夠的連續(xù)空間來(lái)安置新對(duì)象拗踢。新生代使用的是復(fù)制算法來(lái)處理垃圾回收的,如果大對(duì)象直接在新生代分配就會(huì)導(dǎo)致 Eden 區(qū)和兩個(gè) Survivor 區(qū)之間發(fā)生大量的內(nèi)存復(fù)制向臀。因此對(duì)于大對(duì)象都會(huì)直接在老年代進(jìn)行分配巢墅。長(zhǎng)期存活對(duì)象將進(jìn)入老年代
虛擬機(jī)采用分代收集的思想來(lái)管理內(nèi)存,那么內(nèi)存回收時(shí)就必須判斷哪些對(duì)象應(yīng)該放在新生代券膀,哪些對(duì)象應(yīng)該放在老年代君纫。因此虛擬機(jī)給每個(gè)對(duì)象定義了一個(gè)對(duì)象年齡的計(jì)數(shù)器,如果對(duì)象在 Eden 區(qū)出生芹彬,并且能夠被 Survivor 容納蓄髓,將被移動(dòng)到 Survivor 空間中,這時(shí)設(shè)置對(duì)象年齡為 1舒帮。對(duì)象在 Survivor 區(qū)中每「熬過(guò)」一次 Minor GC 年齡就加 1会喝,當(dāng)年齡達(dá)到一定程度(默認(rèn) 15陡叠,可以通過(guò)參數(shù) -XX:MaxTenuringThreshold 來(lái)設(shè)置) 就會(huì)被晉升到老年代。
動(dòng)態(tài)對(duì)象年齡判定
虛擬機(jī)并不是永遠(yuǎn)要求對(duì)象的年齡必須達(dá)到 MaxTenuringThreshold 才能晉升老年代肢执,如果在 Survivor 中相同年齡所有對(duì)象大小的總和大于 Survivor 空間的一半枉阵,則年齡大于或等于該年齡的對(duì)象可以直接進(jìn)入老年代,無(wú)需等到 MaxTenuringThreshold 中要求的年齡预茄。空間分配擔(dān)保
在發(fā)生 Minor GC 之前兴溜,虛擬機(jī)先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對(duì)象總空間,如果條件成立的話耻陕,那么 Minor GC 可以確認(rèn)是安全的拙徽。如果不成立的話虛擬機(jī)會(huì)查看 HandlePromotionFailure 的值是否允許擔(dān)保失敗,如果允許那么就會(huì)繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對(duì)象的平均大小淮蜈,如果大于斋攀,將嘗試著進(jìn)行一次 Minor GC;如果小于梧田,或者 HandlePromotionFailure 的值不允許冒險(xiǎn)淳蔼,那么就要進(jìn)行一次 Full GC。
Full GC 的觸發(fā)條件
對(duì)于 Minor GC裁眯,其觸發(fā)條件非常簡(jiǎn)單鹉梨,當(dāng) Eden 空間滿時(shí),就將觸發(fā)一次 Minor GC穿稳。而 Full GC 則相對(duì)復(fù)雜存皂,有以下條件:
調(diào)用 System.gc()
只是建議虛擬機(jī)執(zhí)行 Full GC,但是虛擬機(jī)不一定真正去執(zhí)行逢艘。不建議使用這種方式旦袋,而是讓虛擬機(jī)管理內(nèi)存。老年代空間不足
老年代空間不足的常見場(chǎng)景為大對(duì)象直接進(jìn)入老年代它改、長(zhǎng)期存活的對(duì)象進(jìn)入老年代等疤孕。為了避免以上原因引起的 Full GC,應(yīng)當(dāng)盡量不要?jiǎng)?chuàng)建過(guò)大的對(duì)象以及數(shù)組央拖。除此之外祭阀,可以通過(guò) -Xmn 虛擬機(jī)參數(shù)調(diào)大新生代的大小,讓對(duì)象盡量在新生代被回收掉鲜戒,不進(jìn)入老年代专控。還可以通過(guò) -XX:MaxTenuringThreshold 調(diào)大對(duì)象進(jìn)入老年代的年齡,讓對(duì)象在新生代多存活一段時(shí)間遏餐。空間分配擔(dān)保失敗
使用復(fù)制算法的 Minor GC 需要老年代的內(nèi)存空間作擔(dān)保伦腐,如果擔(dān)保失敗會(huì)執(zhí)行一次 Full GC。JDK 1.7 及以前的永久代空間不足
在 JDK 1.7 及以前失都,HotSpot 虛擬機(jī)中的方法區(qū)是用永久代實(shí)現(xiàn)的蔗牡,永久代中存放的為一些 Class 的信息颖系、常量、靜態(tài)變量等數(shù)據(jù)辩越。當(dāng)系統(tǒng)中要加載的類、反射的類和調(diào)用的方法較多時(shí)信粮,永久代可能會(huì)被占滿黔攒,在未配置為采用 CMS GC 的情況下也會(huì)執(zhí)行 Full GC。如果經(jīng)過(guò) Full GC 仍然回收不了强缘,那么虛擬機(jī)會(huì)拋出 java.lang.OutOfMemoryError督惰。為避免以上原因引起的 Full GC,可采用的方法為增大永久代空間或轉(zhuǎn)為使用 CMS GC旅掂。Concurrent Mode Failure
執(zhí)行 CMS GC 的過(guò)程中同時(shí)有對(duì)象要放入老年代赏胚,而此時(shí)老年代空間不足(可能是 GC 過(guò)程中浮動(dòng)垃圾過(guò)多導(dǎo)致暫時(shí)性的空間不足),便會(huì)報(bào) Concurrent Mode Failure 錯(cuò)誤商虐,并觸發(fā) Full GC觉阅。
JVM調(diào)優(yōu)
JDK 監(jiān)控和故障處理工具總結(jié)
[JDK 命令行工具]
這些命令在 JDK 安裝目錄下的 bin 目錄下:
-
jps
(JVM Process Status): 類似 UNIX 的ps
命令。用戶查看所有 Java 進(jìn)程的啟動(dòng)類秘车、傳入?yún)?shù)和 Java 虛擬機(jī)參數(shù)等信息典勇; -
jstat
( JVM Statistics Monitoring Tool): 用于收集 HotSpot 虛擬機(jī)各方面的運(yùn)行數(shù)據(jù); -
jinfo
(Configuration Info for Java) : Configuration Info forJava,顯示虛擬機(jī)配置信息; -
jmap
(Memory Map for Java) :生成堆轉(zhuǎn)儲(chǔ)快照; -
jhat
(JVM Heap Dump Browser ) : 用于分析 heapdump 文件,它會(huì)建立一個(gè) HTTP/HTML 服務(wù)器叮趴,讓用戶可以在瀏覽器上查看分析結(jié)果; -
jstack
(Stack Trace for Java):生成虛擬機(jī)當(dāng)前時(shí)刻的線程快照割笙,線程快照就是當(dāng)前虛擬機(jī)內(nèi)每一條線程正在執(zhí)行的方法堆棧的集合。
JDK可視化分析工具
JConsole 是基于 JMX 的可視化監(jiān)視眯亦、管理工具伤溉。可以很方便的監(jiān)視本地及遠(yuǎn)程服務(wù)器的 java 進(jìn)程的內(nèi)存使用情況妻率÷夜耍可以在控制臺(tái)輸出入console命令啟動(dòng)或者在 JDK 目錄下的 bin 目錄找到j(luò)console.exe然后雙擊啟動(dòng)。JConsole 可以顯示當(dāng)前內(nèi)存的詳細(xì)信息舌涨。不僅包括堆內(nèi)存/非堆內(nèi)存的整體信息糯耍,還可以細(xì)化到 eden 區(qū)、survivor 區(qū)等的使用情況
VisualVM是到目前為止隨 JDK 發(fā)布的功能最強(qiáng)大的運(yùn)行監(jiān)視和故障處理程序囊嘉,官方在 VisualVM 的軟件說(shuō)明中寫上了“All-in-One”的描述字樣温技,預(yù)示著他除了運(yùn)行監(jiān)視、故障處理外扭粱,還提供了很多其他方面的功能舵鳞,如性能分析(Profiling)。VisualVM 的性能分析功能甚至比起 JProfiler琢蛤、YourKit 等專業(yè)且收費(fèi)的 Profiling 工具都不會(huì)遜色多少蜓堕,而且 VisualVM 還有一個(gè)很大的優(yōu)點(diǎn):不需要被監(jiān)視的程序基于特殊 Agent 運(yùn)行抛虏,因此他對(duì)應(yīng)用程序的實(shí)際性能的影響很小,使得他可以直接應(yīng)用在生產(chǎn)環(huán)境中套才。這個(gè)優(yōu)點(diǎn)是 JProfiler迂猴、YourKit 等工具無(wú)法與之媲美的。
JVM參數(shù)
標(biāo)準(zhǔn)參數(shù)(-)背伴,所有的 JVM 實(shí)現(xiàn)都必須實(shí)現(xiàn)這些參數(shù)的功能沸毁,而且向后兼容;例如 -verbose:gc(輸出每次GC的相關(guān)情況)
非標(biāo)準(zhǔn)參數(shù)(-X)傻寂,默認(rèn) JVM 實(shí)現(xiàn)這些參數(shù)的功能息尺,但是并不保證所有 JVM 實(shí)現(xiàn)都滿足,且不保證向后兼容疾掰,棧搂誉,堆大小的設(shè)置都是通過(guò)這個(gè)參數(shù)來(lái)配置的,用得最多的如下
參數(shù)示例 表示意義
-Xms512m JVM 啟動(dòng)時(shí)設(shè)置的初始堆大小為 512M
-Xmx512m JVM 可分配的最大堆大小為 512M
-Xmn200m 設(shè)置的年輕代大小為 200M
-Xss128k 設(shè)置每個(gè)線程的棧大小為 128k非Stable參數(shù)(-XX),此類參數(shù)各個(gè) jvm 實(shí)現(xiàn)會(huì)有所不同静檬,將來(lái)可能會(huì)隨時(shí)取消炭懊,需要慎重使用, -XX:-option 代表關(guān)閉 option 參數(shù),-XX:+option 代表要打開 option 參數(shù),例如要啟用串行 GC巴柿,對(duì)應(yīng)的 JVM 參數(shù)即為 -XX:+UseSerialGC凛虽。非 Stable 參數(shù)主要有三大類行為參數(shù)(Behavioral Options):用于改變 JVM 的一些基礎(chǔ)行為,如啟用串行/并行 GC
參數(shù)示例 表示意義
-XX:+DisableExplicitGC 禁止調(diào)用System.gc()广恢;但jvm的gc仍然有效
-XX:+UseConcMarkSweepGC 對(duì)老生代采用并發(fā)標(biāo)記交換算法進(jìn)行GC
-XX:+UseParallelGC 啟用并行GC
-XX:+UseParallelOldGC 對(duì)Full GC啟用并行凯旋,當(dāng)-XX:-UseParallelGC啟用時(shí)該項(xiàng)自動(dòng)啟用
-XX:+UseSerialGC 啟用串行GC
性能調(diào)優(yōu)(Performance Tuning):用于 jvm 的性能調(diào)優(yōu),如設(shè)置新老生代內(nèi)存容量比例
參數(shù)示例 表示意義
-XX:MaxHeapFreeRatio=70 GC后java堆中空閑量占的最大比例
-XX:NewRatio=2 新生代內(nèi)存容量與老生代內(nèi)存容量的比例
-XX:NewSize=2.125m 新生代對(duì)象生成時(shí)占用內(nèi)存的默認(rèn)值
-XX:ReservedCodeCacheSize=32m 保留代碼占用的內(nèi)存容量
-XX:ThreadStackSize=512 設(shè)置線程棧大小钉迷,若為0則使用系統(tǒng)默認(rèn)值
調(diào)試參數(shù)(Debugging Options):一般用于打開跟蹤至非、打印、輸出等 JVM 參數(shù)糠聪,用于顯示 JVM 更加詳細(xì)的信息
參數(shù)示例 表示意義
-XX:HeapDumpPath=./java_pid.hprof 指定導(dǎo)出堆信息時(shí)的路徑或文件名
-XX:-HeapDumpOnOutOfMemoryError 當(dāng)首次遭遇OOM時(shí)導(dǎo)出此時(shí)堆中相關(guān)信息
-XX:-PrintGC 每次GC時(shí)打印相關(guān)信息
-XX:-PrintGC Details 每次GC時(shí)打印詳細(xì)信息
參數(shù)設(shè)置
1荒椭、首先 Oracle 官方推薦堆的初始化大小與堆可設(shè)置的最大值一般是相等的,即 Xms = Xmx舰蟆,因?yàn)槠鹗级褍?nèi)存太腥せ荨(Xms),會(huì)導(dǎo)致啟動(dòng)初期頻繁 GC身害,起始堆內(nèi)存較大(Xmx)有助于減少 GC 次數(shù)
2味悄、調(diào)試的時(shí)候設(shè)置一些打印參數(shù),如-XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:log/gc.log塌鸯,這樣可以從gc.log里看出一些端倪出來(lái)
3侍瑟、系統(tǒng)停頓時(shí)間過(guò)長(zhǎng)可能是 GC 的問(wèn)題也可能是程序的問(wèn)題,多用 jmap 和 jstack 查看,或者killall -3 Java涨颜,然后查看 Java 控制臺(tái)日志费韭,能看出很多問(wèn)題
4、 采用并發(fā)回收時(shí)庭瑰,年輕代小一點(diǎn)星持,老年代要大,因?yàn)槔夏甏玫氖遣l(fā)回收弹灭,即使時(shí)間長(zhǎng)點(diǎn)也不會(huì)影響其他程序繼續(xù)運(yùn)行钉汗,網(wǎng)站不會(huì)停頓
5、仔細(xì)了解自己的應(yīng)用鲤屡,如果用了緩存,那么年老代應(yīng)該大一些福侈,緩存的HashMap不應(yīng)該無(wú)限制長(zhǎng)酒来,建議采用LRU算法的Map做緩存,LRUMap的最大長(zhǎng)度也要根據(jù)實(shí)際情況設(shè)定