背景
以前看了周志明的《深入理解Java虛擬機(jī)》,今天突然想談起它來(lái)爸吮。
所以今天這篇文章會(huì)說(shuō)說(shuō) java的內(nèi)存分配策略和垃圾收集器囤采。
提出問(wèn)題
java 中內(nèi)存如何申請(qǐng)的,什么樣的數(shù)據(jù)存放在新生代吗坚,什么樣的數(shù)據(jù)存放在老生代,什么樣的數(shù)據(jù)存放在永久代呆万。
數(shù)據(jù)的回收:這3個(gè)區(qū)域的數(shù)據(jù)是如何回收的呢商源?各使用什么樣的垃圾收集器。
今天的文章圍繞他們解決谋减。
對(duì)象如何被轉(zhuǎn)移
這里有兩個(gè)概念:MinGc:新生代GC, Major GC:老生代GC牡彻。
新生代往老生代轉(zhuǎn)移的原則是:
1).在進(jìn)行15次 MinGC還存活下來(lái)的就轉(zhuǎn)移到 Old GC。
2).如果對(duì)象太大,直接放到 Old區(qū)庄吼。
3).如果MinGC 過(guò)程中缎除,Survivor放不下會(huì)被擔(dān)保到Old區(qū)。
當(dāng)Eden區(qū)滿了就會(huì)觸發(fā)MinGC总寻,當(dāng)Old區(qū)滿了就會(huì)觸發(fā)Major GC器罐。
為什么會(huì)有2個(gè)Survivor,是由于必須經(jīng)過(guò)15次MinGC才會(huì)轉(zhuǎn)移到Old GC渐行,起到轉(zhuǎn)騰的作用轰坊。
比如 第一次MinGC:Eden +Suv1 -> Surv2,第二次Min GC: Eden + Surv2 -> surv1。
這里虛擬機(jī)為每個(gè)對(duì)象定義了年齡計(jì)數(shù)器祟印,當(dāng)年齡大于閥值得時(shí)候就進(jìn)入老年代肴沫。
大對(duì)象直接進(jìn)入老年代,大對(duì)象是指需要大量連續(xù)內(nèi)存空間的對(duì)象)蕴忆。這樣做的目的是避免在Eden區(qū)和兩個(gè)Survivor區(qū)之間發(fā)生大量的內(nèi)存拷貝(新生代采用復(fù)制算法收集內(nèi)存)颤芬。
堆內(nèi)存如何被申請(qǐng)
虛擬機(jī)如何執(zhí)行 Class1 c1 = new Class1()這條指令。
在執(zhí)行這條指令的時(shí)候要做兩件事情:
1.檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)符號(hào)引用孽文,并且檢查這個(gè)符號(hào)引用是否被加載驻襟、解析、初始化芋哭。如果沒有必須先執(zhí)行類加載過(guò)程沉衣。
2.申請(qǐng)內(nèi)存,內(nèi)容申請(qǐng)需要看堆中內(nèi)存是否連續(xù)减牺,如果是連續(xù)通過(guò)移動(dòng)指針頭(稱為指針膨脹)豌习。如果不連續(xù)的話就通過(guò)“空閑列表”定位。 而內(nèi)存是否連續(xù)跟垃圾回收算法是否帶有壓縮功能有關(guān)拔疚。
serial和parNew帶有壓縮功能肥隆,而CMS基于mark-sweep算法的收集器,通常采用空閑列表稚失。
分配空間的過(guò)程中栋艳,還需要考慮一個(gè)問(wèn)題是。如果多個(gè)線程同時(shí)修改指針會(huì)出現(xiàn)并發(fā)的問(wèn)題句各。
虛擬機(jī)采用兩種辦法 a).CAS+失敗重試(CAS就是我們所說(shuō)的樂觀鎖機(jī)制)吸占。 b).內(nèi)存分配的動(dòng)作劃分到不同的線程空間中。
3.內(nèi)存申請(qǐng)完之后就是初始化凿宾。
這里涉及到對(duì)象的內(nèi)存布局矾屯,對(duì)象分為對(duì)象頭(如hashcode,對(duì)象類型指針)、實(shí)例數(shù)據(jù)初厚、對(duì)象填充件蚕。
對(duì)象如何被定位呢?
我們知道對(duì)象的引用放在棧里,所以我們有兩種方式引用到堆里的內(nèi)存排作。
1.棧里的reference 直接指向 堆里的對(duì)象牵啦。
2.棧里reference 指向堆里 對(duì)象句柄(對(duì)象句柄這個(gè)地址是固定的),對(duì)象類型數(shù)據(jù)指針是放在 對(duì)象句柄中的纽绍,所以當(dāng)對(duì)象移動(dòng)了只會(huì)修改 對(duì)象句柄中實(shí)例數(shù)據(jù)指針蕾久。
由于使用句柄的方式多了一次指針定位的開銷势似,所以比第一種會(huì)慢一些拌夏。
什么樣的數(shù)據(jù)會(huì)被回收?
簡(jiǎn)單的說(shuō)就是無(wú)用的數(shù)據(jù)應(yīng)該被回收履因,什么是無(wú)用的呢障簿?
怎么判定無(wú)用:市面上有
1).引用計(jì)數(shù)法(解決不了循環(huán)引用的場(chǎng)景 A與B互相引用)
2).可達(dá)性分析算法
這個(gè)算法就是通過(guò) 一系列稱為 "GC Roots"的對(duì)象作為起點(diǎn),搜索鎖走過(guò)的路徑稱為引用鏈栅迄。當(dāng)一個(gè)對(duì)象到GC Roots沒有任可達(dá)路徑則會(huì)被回收站故。
那什么是GC Roots對(duì)象呢?
GC Roots對(duì)象包含如下幾點(diǎn):a.虛擬機(jī)棧中引用的對(duì)象毅舆。 b.方法去中類靜態(tài)屬性引用的對(duì)象西篓。 c.方法區(qū)中常量引用的對(duì)象。 d.本地方法棧中JNI引用的對(duì)象憋活。
什么時(shí)候被回收
什么時(shí)候被回收是根據(jù)其引用的類型來(lái)決定的岂津,java中的引用分為以下幾種。
1.強(qiáng)引用 2.軟引用 3.弱引用 3.虛引用
強(qiáng)引用就是類似于 Class1 c1 = new Class1(); 永遠(yuǎn)不會(huì)被回收
軟引用用來(lái)描述有用但是非必須的悦即,在內(nèi)存溢出的會(huì)被回收吮成。 java中通過(guò)SoftReference實(shí)現(xiàn)軟引用。
弱引用強(qiáng)度比軟引用更加的弱一些辜梳,再下一次垃圾回收時(shí)就會(huì)被回收粱甫。
虛引用,我們不會(huì)通過(guò)虛引用來(lái)獲取一個(gè)對(duì)象也不會(huì)對(duì)生存時(shí)間構(gòu)成影響作瞄,它的作用就是在被回收掉后收到一個(gè)系統(tǒng)通知茶宵。
怎么被回收
垃圾回收算法
這里描述使用什么算法及其如何回收內(nèi)存。
垃圾回收算法應(yīng)該考慮以下幾點(diǎn):
1).回收的效率和時(shí)間宗挥。
2).垃圾回收的時(shí)候是否影響正在運(yùn)行的程序乌庶。
我們先不考慮分代的問(wèn)題,垃圾回收就是想把沒有用的清楚掉把有用的留下來(lái)属韧。對(duì)于無(wú)用的對(duì)象安拟,我們可以標(biāo)記清楚 - 標(biāo)記清楚法。 對(duì)于有用的對(duì)象我們可以采用標(biāo)記整理法 或者 復(fù)制法宵喂。
這里提一下復(fù)制法:新生代中包含(Eden + Survivor1 + Survivor2)糠赦,每次會(huì)把Eden +Survivor中的存活對(duì)象移動(dòng)到另一個(gè) Survivor中,如果另一個(gè)Survivor空間不夠就需要老生代擔(dān)保。
而實(shí)際jvm中是分為新生代拙泽、老生代淌山。對(duì)于新生代由于回收頻率高存活對(duì)象比較少,所以采用復(fù)制法顾瞻。對(duì)于老生代由于對(duì)象存活率高沒有額外的區(qū)域進(jìn)行擔(dān)保所以使用(標(biāo)記清除泼疑、標(biāo)記整理)法。
垃圾回收器
前面談到了收集算法荷荤,但是垃圾具體是通過(guò)收集器進(jìn)行回收的退渗。市面上最簡(jiǎn)單的垃圾收集器是serial收集器,由于其簡(jiǎn)單專心做垃圾收集適用于運(yùn)行在client模式下的虛擬機(jī)(新生代也只有幾十到100多兆)蕴纳。
a).parNew是Serial的多線程版本会油,其余行為包括垃圾收集算法(復(fù)制算法)都與Serial收集器一致。parNew是運(yùn)行在server下的首選新生代收集器古毛。
b).Cocurrent Mark-sweep翻翩,這個(gè)是虛擬機(jī)真正意義上第一款并發(fā)收集器,由于其使用Mark-sweep算法所以其適用于老生代稻薇。
c).Parallel Scavenge 提供兩個(gè)參數(shù)MaxGCPauseMillis,GCTimeRatio來(lái)設(shè)置虛擬機(jī)優(yōu)化目標(biāo)嫂冻,該收集器注重的的是吞吐量,對(duì)于與用戶交互少但是有很多后臺(tái)程序比較適合塞椎。 -- 老生代還是新生代桨仿??忱屑?
d).Serial old 收集器是Serial的老年代版本(使用標(biāo)記-整理算法)蹬敲, Parralle Scavenge無(wú)法與CMS工作只能與Serial Old配合一起工作。這是parralle old收集器出現(xiàn)它采用的是標(biāo)記-整理 算法
d).CMS收集器莺戒,為什么Cocurrent特性呢伴嗡?仔細(xì)分析然來(lái)CMS收集器把收集過(guò)程分為4個(gè)子階段:
1).初始標(biāo)記 -- 把GC Root對(duì)象找出來(lái)
2).并發(fā)標(biāo)記 -- GC Roots tracing
3).重新標(biāo)記 -- 把進(jìn)行 并發(fā)標(biāo)記 那一段時(shí)間,標(biāo)記變動(dòng)的那一部分重新標(biāo)記从铲,這個(gè)階段比初始標(biāo)記時(shí)間長(zhǎng)比并發(fā)標(biāo)記時(shí)間短瘪校,但是必須停止用戶線程。
4).并發(fā)清除
初始標(biāo)記名段、重新標(biāo)記是需要”stop the world“阱扬,并發(fā)標(biāo)記和并發(fā)清除這兩個(gè)階段還是可以垃圾回收線程與用戶線程一起執(zhí)行。由于CMS是并發(fā)的執(zhí)行收集工作伸辟,所以它是一個(gè)很耗資源的收集器麻惶,會(huì)使得用戶程度整體執(zhí)行時(shí)間長(zhǎng)(雖然瞬時(shí)停頓感少了)。
由于CMS收集器與用戶線程一起工作信夫,所以不能像其他收集器比如par Old一樣等到老生代占到100%在啟用窃蹋,一般等到60%就啟用CMS收集器卡啰,這里有一個(gè)參數(shù)配置-XXCMSInitiatingOccupancyFraction。平常我們可以提高這個(gè)參數(shù)來(lái)降低CMS啟動(dòng)收集次數(shù)警没,但是如果比率過(guò)高匈辱,預(yù)留的空間不足用戶線程使用會(huì)出現(xiàn)“Cocurrent Mode Failure”,這樣的話性能反而會(huì)降低杀迹。
還有一個(gè)問(wèn)題CMS容易產(chǎn)生碎片亡脸,如果需要進(jìn)行碎片整理(無(wú)法并發(fā))的話必須停止用戶線程。我們通過(guò)一個(gè)參數(shù)CMSFullGCBeforeCompaction來(lái)控制執(zhí)行多少次無(wú)壓縮的收集然后執(zhí)行一次帶壓縮的收集树酪。
e).G1收集器
G1收集器在多核CPU等可以充分利用硬件環(huán)境浅碾,它是面向服務(wù)端的垃圾回收器。采用的是“標(biāo)記-整理”的方式嗅回,所以沒有碎片的出現(xiàn)及穗。
G1收集器的堆內(nèi)存與其他收集器有很大的區(qū)別,它將整個(gè)堆劃分為很多region绵载。G1能夠建立預(yù)測(cè)模型會(huì)跟蹤每個(gè)Region垃圾回收價(jià)值大小(回收所獲得的空間大小和及回收所需時(shí)間的經(jīng)驗(yàn)值)苛白,然后選一個(gè)價(jià)值最高的region進(jìn)行回收(這是Garbage-First 名稱的由來(lái))娃豹。
堆被分成許多region也會(huì)帶來(lái)一個(gè)問(wèn)題,如果一個(gè)region A被其他region B引用购裙,當(dāng)我回收A的時(shí)候我還要掃描整個(gè)堆懂版,把引用A的regin掃描出來(lái)。所以G1采用空間換時(shí)間的辦法即提供一個(gè)rememerSet的數(shù)據(jù)結(jié)構(gòu)來(lái)維護(hù)這些引用關(guān)系躏率。
如何查看垃圾收集器日志
開啟GC日志:參數(shù) - XX:+PrintGC(或者 - verbose:gc)開啟了簡(jiǎn)單 GC 日志模式躯畴,
簡(jiǎn)單GC日志不會(huì)打印具體使用的算法,比如下面日志:
[GC 246656K->243120K(376320K), 0.0929090 secs]
[Full GC 243120K->241951K(629760K), 1.5589690 secs]
第一行表示執(zhí)行的MinGC 堆空間使用從246656K 到 243120K 堆的總大小為376320K,執(zhí)行時(shí)間消耗0.0929090 secs薇芝。
第二行表示執(zhí)行了FUll GC堆空間從243120K 到 241951K蓬抄,執(zhí)行時(shí)間消耗1.5589690 secs。
這里沒有描述執(zhí)行GC的時(shí)間夯到,數(shù)據(jù)有沒有從新生代轉(zhuǎn)到老生代嚷缭,也沒有顯示具體使用的算法。
如果想要看詳細(xì)的GC日志耍贾,則-XX:PrintGCDetails,這時(shí)候會(huì)打印出詳細(xì)日志:
如下描述的是執(zhí)行了一次新生代GC阅爽,新生代使用由142816K減少到10752K。堆的總?cè)萘看笮∮?46648K減少到243136K荐开。
[GC
[PSYoungGen: 142816K->10752K(142848K)] 246648K->243136K(375296K), 0.0935090 secs
]
[Times: user=0.55 sys=0.10, real=0.09 secs]
對(duì)于FullGC 詳細(xì)日志如下:
打印了每一代的堆使用情況變化付翁,其中PsYoungGen 9707 +ParOldGen 232244 = 堆的總使用大小 241951.
[Full GC
[PSYoungGen: 10752K->9707K(142848K)]
[ParOldGen: 232384K->232244K(485888K)] 243136K->241951K(628736K)
[PSPermGen: 3162K->3161K(21504K)], 1.5265450 secs
]
如果想打印出時(shí)間參數(shù)可以使用- XX:+PrintGCTimeStamps 可以將時(shí)間和日期也加到 GC 日志中。
如果指定了 - XX:+PrintGCDateStamps晃听,每一行就添加上了絕對(duì)的日期和時(shí)間百侧。
2014-01-03T12:08:38.102-0100: [GC 66048K->53077K(251392K), 0.0959470 secs]
2014-01-03T12:08:38.239-0100: [GC 119125K->114661K(317440K), 0.1421720 secs]
2014-01-03T12:08:38.513-0100: [GC 246757K->243133K(375296K), 0.2761000 secs]
如果想輸出到文件 就加上 -Xloggc
寫完后的想法
對(duì)這塊知識(shí)着帽,邊看書邊收集資料整理理解。
下一篇文章將通過(guò)代碼具體模擬觸發(fā)MinGC移层,及其通過(guò)GC 日志分析仍翰,查看JVM活動(dòng)的細(xì)節(jié)。
這是我個(gè)人面對(duì)這個(gè)問(wèn)題的邏輯推導(dǎo)不是粘貼別人的答案花費(fèi)了我大半天的時(shí)間但是很值观话。