一、內(nèi)存模型

- 程序計數(shù)器
指向當(dāng)前線程所執(zhí)行的字節(jié)碼指令的(地址)行號。
程序計數(shù)器是唯一不會出現(xiàn) OutOfMemoryError 的內(nèi)存區(qū)域,它的生命周期隨著線程的創(chuàng)建而創(chuàng)建,隨著線程的結(jié)束而死亡 - 虛擬機棧
棧數(shù)據(jù)結(jié)構(gòu)為先進(jìn)后出
局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口信息。
局部變量表主要存放了編譯器可知的各種基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)和對象引用(reference類型,它不同于對象本身,可能是一個指向?qū)ο笃鹗嫉刂返囊弥羔?,也可能是指向一個代表對象的句柄或其他與此對象相關(guān)的位置)
StackOverFlowError:若Java虛擬機棧的內(nèi)存大小不允許動態(tài)擴展,那么當(dāng)線程請求棧的深度超過當(dāng)前Java虛擬機棧的最大深度的時候,就拋出StackOverFlowError異常。(遞歸死循環(huán))
OutOfMemoryError:若 Java 虛擬機棧的內(nèi)存大小允許動態(tài)擴展,且當(dāng)線程請求棧時內(nèi)存用完了,無法再動態(tài)擴展了,此時拋出OutOfMemoryError異常。(方法太多)
本地方法棧
調(diào)用Native 方法服務(wù),其他和棧差不多,也可能會有StackOverFlowError和OutOfMemoryError方法區(qū)
用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。-
堆
主要用來存放對象實例和數(shù)組
運行時常量池也屬于堆中的一部分,用于存放編譯期生成的各種字面量和符號引用
JDK1.7及之后版本的 JVM 已經(jīng)將運行時常量池從方法區(qū)中移了出來,在 Java 堆(Heap)中開辟了一塊區(qū)域存放運行時常量池。
image.png
新生代:老年代 1:2
Eden : from : to = 8:1:1
二、垃圾回收算法
程序計數(shù)器、虛擬機棧、本地方法棧3個區(qū)域隨線程而生、隨線程而滅,因此這幾個區(qū)域的內(nèi)存分配和回收都具備確定性,就不需要過多考慮回收的問題,因為方法結(jié)束或者線程結(jié)束時,內(nèi)存自然就跟隨著回收了。而Java堆區(qū)和方法區(qū)則不一樣,這部分內(nèi)存的分配和回收是動態(tài)的,正是垃圾收集器所需關(guān)注的部分。
垃圾收集器在對堆區(qū)和方法區(qū)進(jìn)行回收前,首先要確定這些區(qū)域的對象哪些可以被回收,哪些暫時還不能回收,這就要用到判斷對象是否存活的算法!
1.引用計數(shù)法
堆中每個對象實例都有一個引用計數(shù)。當(dāng)一個對象被創(chuàng)建時,就將該對象實例分配給一個變量,該變量計數(shù)設(shè)置為1。當(dāng)任何其它變量被賦值為這個對象的引用時,計數(shù)加1(a = b,則b引用的對象實例的計數(shù)器+1),但當(dāng)一個對象實例的某個引用超過了生命周期或者被設(shè)置為一個新值時,對象實例的引用計數(shù)器減1。任何引用計數(shù)器為0的對象實例可以被當(dāng)作垃圾收集。當(dāng)一個對象實例被垃圾收集時,它引用的任何對象實例的引用計數(shù)器減1
缺點:循環(huán)引用無法檢測
2.可達(dá)性算法
從一個節(jié)點GC ROOT開始,尋找對應(yīng)的引用節(jié)點,找到這個節(jié)點以后,繼續(xù)尋找這個節(jié)點的引用節(jié)點,當(dāng)所有的引用節(jié)點尋找完畢之后,剩余的節(jié)點則被認(rèn)為是沒有被引用到的節(jié)點,即無用的節(jié)點,無用的節(jié)點將會被判定為是可回收的對象。
在Java語言中,可作為GC Roots的對象包括下面幾種:(京東)
??a) 虛擬機棧中引用的對象(棧幀中的本地變量表);
??b) 方法區(qū)中類靜態(tài)屬性引用的對象;
??c) 方法區(qū)中常量引用的對象;
??d) 本地方法棧中JNI(Native方法)引用的對象。
即使在可達(dá)性分析算法中不可達(dá)的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段,要真正宣告一個對象死亡,至少要經(jīng)歷兩次標(biāo)記過程:如果對象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈,那它將會被第一次標(biāo)記并且進(jìn)行一次篩選,篩選的條件是此對象是否有必要執(zhí)行finapze()方法。當(dāng)對象沒有覆蓋finapze()方法,或者finapze()方法已經(jīng)被虛擬機調(diào)用過,虛擬機將這兩種情況都視為“沒有必要執(zhí)行”。程序中可以通過覆蓋finapze()來一場"驚心動魄"的自我拯救過程,但是,這只有一次機會呦。
3.標(biāo)記-清除算法(Mark-Sweep)
標(biāo)記-清除算法分為兩個階段:標(biāo)記階段和清除階段。標(biāo)記階段的任務(wù)是標(biāo)記出所有需要被回收的對象,清除階段就是回收被標(biāo)記的對象所占用的空間
會造成不連續(xù)的內(nèi)存碎片
4.復(fù)制算法(Copying)
將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對象復(fù)制到另外一塊上面,然后再把已使用的內(nèi)存空間一次清理掉,這樣一來就不容易出現(xiàn)內(nèi)存碎片的問題。
能夠使用的內(nèi)存縮減到原來的一半
如果存活對象很多,那么Copying算法的效率將會大大降低
5.標(biāo)記-整理算法(Mark-compact)
該算法標(biāo)記階段和Mark-Sweep一樣,但是在完成標(biāo)記之后,它不是直接清理可回收對象,而是將存活對象都向一端移動(記住是完成標(biāo)記之后,先不清理,先移動再清理回收對象),然后清理掉端邊界以外的內(nèi)存。
6分代收集
一般把 Java 堆分為新生代和老年代,這樣就可以根據(jù)各個年代的特點采用最適當(dāng)?shù)氖占惴?br> 在新生代中,每次垃圾收集時都發(fā)現(xiàn)有大批對象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量存活對象的復(fù)制成本就可以完成收集。而老年代中因為對象存活率高、沒有額外空間對它進(jìn)行分配擔(dān)保,就必須使用“標(biāo)記-清理”或者“標(biāo)記一整理”算法來進(jìn)行回收
新生代采用復(fù)制算法
新生代中的對象 98%是“朝生夕死”的,所以并不需要按照 1:1 的比例來劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。 Survivor from 和Survivor to ,內(nèi)存比例 8:1:1
當(dāng)回收時,將 Eden 和 Survivor 中還存活著的對象一次性地復(fù)制到另外一塊 Survivor 空間上,最后清理掉 Eden 和剛才用過的 Survivor 空間。HotSpot 虛擬機默認(rèn) Eden 和 Survivor 的大小比例是 8:1, 也就是每次新生代中可用內(nèi)存空間為整個新生代容量的 90% (80%+10%),只有 10% 的內(nèi)存會被“浪費”。當(dāng)然,98%的對象可回收只是一般場景下的數(shù)據(jù),我們沒有辦法保證每次回收都只有不多于 10%的對象存活,當(dāng) Survivor 空間不夠用時,需要依賴其他內(nèi)存(這里指老年代)進(jìn)行分配擔(dān)保(Handle Promotion)。
三、垃圾回收器
jdk8環(huán)境下,默認(rèn)使用 Parallel Scavenge(新生代)+ Serial Old(老年代)
serial收集器
單線程的回收器,串行 復(fù)制算法
“Stop The World”,它進(jìn)行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結(jié)束。
ParNew收集器
ParNew 收集器其實就是 Serial 收集器的多線程版本
Parallel Scavenge收集器
使用復(fù)制算法的收集器,又是并行的多線程收集器
由于與吞吐量關(guān)系密切,Parallel Scavenge 收集器也經(jīng)常稱為“吞吐量優(yōu)先”收集器
吞吐量是什么?CPU用于運行用戶代碼的時間與CPU總時間的比值,99%時間執(zhí)行用戶線程,1%時間回收垃圾 ,這時候吞吐量就是99%
Serial Old收集器
同Serial,單線程,算法采用 標(biāo)記整理
cms收集器
CMS (Concurrent Mark Sweep)收集器是-種以獲取最短回收停頓時間為目標(biāo)的收集器
CMS 收集器是基于“標(biāo)記-清除”算法實現(xiàn)的
步驟流程:
初始標(biāo)記(CMS initial mark) -----標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)到的對象,速度很快
并發(fā)標(biāo)記(CMS concurrent mark --------并發(fā)標(biāo)記階段就是進(jìn)行 GC RootsTracing 的過程
重新標(biāo)記(CMS remark) -----------為了修正并發(fā)標(biāo)記期間因用戶程序?qū)е聵?biāo)記產(chǎn)生變動的標(biāo)記記錄
并發(fā)清除(CMS concurrent sweep)
CMS垃圾收集器缺點
對CPU資源非常敏感
無法處理浮動垃圾,程序在進(jìn)行并發(fā)清除階段用戶線程所產(chǎn)生的新垃圾
標(biāo)記-清除存在空間碎片
G1收集器
G1是一款面向服務(wù)端應(yīng)用的垃圾收集器
G1 中每個 Region 都有一個與之對應(yīng)的 Remembered Set,當(dāng)進(jìn)行內(nèi)存回收時,在 GC 根節(jié)點的枚舉范圍中加入 Remembered Set 即可保證不對全堆掃描也不會有遺漏 檢查Reference引用的對象是否處于不同的Region
初始標(biāo)記(Initial Marking) --標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)到的對象
并發(fā)標(biāo)記(Concurrent Marking)---從GC Root 開始對堆中對象進(jìn)行可達(dá)性分析,找出存活的對象,這階段耗時較長,但可與用戶程序并發(fā)執(zhí)行
最終標(biāo)記(Final Marking) ---為了修正在并發(fā)標(biāo)記期間因用戶程序繼續(xù)運作而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分標(biāo)記記錄。虛擬機將這段時間對象變化記錄在線程 Remembered Set Logs 里面,最終標(biāo)記階段需要把 Remembered Set Logs的數(shù)據(jù)合并到 Remembered Set 中
篩選回收(Live Data Counting and Evacuation)
對各個Region的回收價值和成本進(jìn)行排序,根據(jù)用戶所期望的GC停頓時間來制定回收計劃
G1的優(yōu)勢有哪些
空間整合:基于“標(biāo)記一整理”算法實現(xiàn)為主和Region之間采用復(fù)制算法實現(xiàn)的垃圾收集
可預(yù)測的停頓:這是 G1 相對于 CMS 的另一大優(yōu)勢,降低停頓時間是 G1 和 CMS 共同的關(guān)注點,但 G1 除了追求低停頓外,還能建立可預(yù)測的停頓時間模型
在 G1 之前的其他收集器進(jìn)行收集的范圍都是整個新生代或者老年代,而 G1 不再是這樣。使用 G1 收集器時,Java 堆的內(nèi)存布局就與其他收集器有很大差別,它將整個 Java 堆劃分為多個大小相等的獨立區(qū)域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔髙的了,它們都是一部分 Region(不需要連續(xù))的集合。
G1 收集器之所以能建立可預(yù)測的停頓時間模型,是因為它可以有計劃地避免在整個 Java 堆中進(jìn)行全區(qū)域的垃圾收集。G1 跟蹤各個 Regions 里面的垃圾堆積的價值大?。ɑ厥账@得的空間大小以及回收所需時間的經(jīng)驗值),在后臺維護(hù)一個優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先回收價值最大的 Region(這也就是 Garbage- Firsti 名稱的來由)。這種使用 Region 劃分內(nèi)存空間以及有優(yōu)先級的區(qū)域回收方式,保證了 G1 收集器在有限的時間內(nèi)可以獲取盡可能高
GC是什么時候觸發(fā)的
Scavenge GC
??一般情況下,當(dāng)新對象生成,并且在Eden申請空間失敗時,就會觸發(fā)Scavenge GC,對Eden區(qū)域進(jìn)行GC,清除非存活對象,并且把尚且存活的對象移動到Survivor區(qū)。然后整理Survivor的兩個區(qū)。這種方式的GC是對年輕代的Eden區(qū)進(jìn)行,不會影響到年老代。因為大部分對象都是從Eden區(qū)開始的,同時Eden區(qū)不會分配的很大,所以Eden區(qū)的GC會頻繁進(jìn)行。因而,一般在這里需要使用速度快、效率高的算法,使Eden去能盡快空閑出來。
Full GC
??對整個堆進(jìn)行整理,包括Young、Tenured和Perm。Full GC因為需要對整個堆進(jìn)行回收,所以比Scavenge GC要慢,因此應(yīng)該盡可能減少Full GC的次數(shù)。在對JVM調(diào)優(yōu)的過程中,很大一部分工作就是對于Full GC的調(diào)節(jié)。有如下原因可能導(dǎo)致Full GC:
a) 年老代(Tenured)被寫滿;
b) 持久代(Perm)被寫滿;
c) System.gc()被顯示調(diào)用;
d) 上一次GC之后Heap的各域分配策略動態(tài)變化;
逃逸分析
逃逸分析的基本行為就是分析對象動態(tài)作用域:當(dāng)一個對象在方法中被定義后,它可能被外部方法所引用,稱為方法逃逸。甚至還有可能被外部線程訪問到,譬如賦值給類變量或可以在其他線程中訪問的實例變量,稱為線程逃逸
棧上分配
棧上分配就是把方法中的變量和對象分配到棧上,方法執(zhí)行完后自動銷毀,而不需要垃圾回收的介入,從而提高系統(tǒng)性能
-XX:+DoEscapeAnalysis開啟逃逸分析(jdk1.8默認(rèn)開啟,其它版本未測試)
-XX:-DoEscapeAnalysis 關(guān)閉逃逸分析
GC 優(yōu)化需要考慮的 JVM 參數(shù)
類型 參數(shù) 描述
堆內(nèi)存大小 -Xms 啟動 JVM 時堆內(nèi)存的大小
-Xmx 堆內(nèi)存最大限制
新生代空間大小 -XX:NewRatio 新生代和老年代的內(nèi)存比
-XX:NewSize 新生代內(nèi)存大小
-XX:SurvivorRatio Eden 區(qū)和 Survivor 區(qū)的內(nèi)存比
