Java內(nèi)存區(qū)域
以下的這張圖給出了JVM所管理的內(nèi)存在運(yùn)行時(shí)的數(shù)據(jù)區(qū)域:
JVM棧:它的生命周期和線程相同。它描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法被執(zhí)行的時(shí)候都會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表、操作棧、動(dòng)態(tài)鏈接、方法出口等信息。每一個(gè)方法被調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過程。
Java堆:Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域的唯一目的是存放對象的實(shí)例,幾乎所有的對象實(shí)例都在這里進(jìn)行分配內(nèi)存。是垃圾收集器管理的主要區(qū)域,因此很多時(shí)候也被稱為“GC堆”。可分為新生代和老年代。
方法區(qū):和Java堆一樣是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。也稱為永久代。
運(yùn)行時(shí)常量池:是方法區(qū)的一部分。用于存放編譯期生成的各種字面量和符號(hào)引用。它具備動(dòng)態(tài)性。
程序計(jì)數(shù)器:它的作用可以看做是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。字節(jié)碼解釋器工作時(shí)就是通過改變程序計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令(分支、循環(huán)、跳轉(zhuǎn)、異常處理等)。在任何一個(gè)確定的時(shí)刻,一個(gè)處理器只會(huì)執(zhí)行一條線程中的指令。
本地方法棧:與JVM棧相似。本地方法棧服務(wù)于虛擬機(jī)執(zhí)行Native方法,JVM棧服務(wù)于執(zhí)行Java方法。
對象訪問
在最簡單的訪問中也會(huì)涉及Java棧、Java堆、方法區(qū)這三個(gè)最重要的內(nèi)存區(qū)域之間的關(guān)系。
比如在代碼:Object obj = new Object();
如果該語句出現(xiàn)在方法體中,那么Object obj
這一部分的語義將會(huì)反映在本地變量表中,作為一個(gè)reference類型數(shù)據(jù)出現(xiàn)。new Object()
這部分的語義將反映到Java堆中,形成一塊存儲(chǔ)了Object類型所有實(shí)例數(shù)據(jù)值的結(jié)構(gòu)化內(nèi)存,根據(jù)具體類型以及虛擬機(jī)實(shí)現(xiàn)的對象內(nèi)存布局的不同,這塊內(nèi)存的長度是不固定的。另外在方法區(qū)中還存儲(chǔ)有能找到次對象類型數(shù)據(jù)的地址信息。
主流訪問對象的方式有兩種:使用句柄和直接指針。
-
使用句柄的方式:Java堆中將會(huì)劃出一塊內(nèi)存來作為句柄池,reference中存儲(chǔ)的就是對象的句柄地址,而句柄中包含了對象實(shí)例數(shù)據(jù)和類型數(shù)據(jù)各自的具體地址信息。如下圖:
-
直接指針的方式:Java堆對象的布局中必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息,reference中直接存儲(chǔ)的就是對象地址。如下圖:
使用句柄訪問方式的最大的好處就是reference中存儲(chǔ)的是穩(wěn)定的句柄地址,在對象被移動(dòng)時(shí)只會(huì)改變句柄中實(shí)例數(shù)據(jù)指針,而reference本身不需要被修改。使用直接指針訪問方式的最大好處就是速度更快,它節(jié)省了一次指針定位的時(shí)間開銷,由于對象的訪問在Java中非常頻繁,因此這類開銷積少成多也是一項(xiàng)非??捎^的執(zhí)行成本。
判斷對象是否還活著的算法
引用計(jì)數(shù)算法
給對象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器值就加1;當(dāng)引用失效時(shí),計(jì)數(shù)值就減1;任何時(shí)刻計(jì)數(shù)器都未0的對象就是不可能再被使用的。
ps:JVM不是通過使用引用計(jì)數(shù)算法來判斷對象是否存活的。
根搜索算法(GC Roots Tracing)
Java虛擬機(jī)使用該算法判斷對象是否存活的。
基本思路:通過一系列的名為“GC Roots”的對象作為起始點(diǎn),從這些起始點(diǎn)開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當(dāng)一個(gè)對象到GC Roots沒有任何引用鏈想連(用圖論的話來說就是從GC Roots到這個(gè)對象不可達(dá))時(shí),則證明此對象是不可用對象。
在Java語言中,可以作為GC Roots的對象包括:
-
虛擬機(jī)棧(棧幀中的本地變量表)中的引用的對象。
-
方法區(qū)中的類靜態(tài)屬性引用的對象。
-
方法區(qū)中的常量引用的對象。
-
本地方法棧中JNI的引用的對象。
4種引用類型
引用可以分為強(qiáng)引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference)。
-
強(qiáng)引用就是指在程序代碼中普遍存在的,類似
Object obj = new Object()
這類的引用。只要強(qiáng)引用還存在,垃圾回收器永遠(yuǎn)不會(huì)回收掉被引用的對象。 -
軟引用用老描述一些還有用的,但并非必須的對象。對于軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要發(fā)生內(nèi)存異常之前,將會(huì)把這些對象列進(jìn)回收范圍之中并進(jìn)行第二次回收。如果這次回收還是沒有足夠的內(nèi)存,才會(huì)拋出內(nèi)存溢出異常。JDK提供SoftReference類來實(shí)現(xiàn)軟引用。
-
弱引用也是用來描述非必須對象的,它比軟引用更弱一些,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前。JDK提供WealReference類來實(shí)現(xiàn)弱引用。
-
虛引用是最弱的引用關(guān)系。一個(gè)對象是否有虛引用的存在,完全不會(huì)對其生存時(shí)間構(gòu)成影響,也無法通過虛引用來取得一個(gè)對象實(shí)例。為一個(gè)對象設(shè)置虛引用的唯一目的是希望能在這個(gè)對象被收集器回收的時(shí)候收到一個(gè)系統(tǒng)通知。JDK中使用PhantomReference類實(shí)現(xiàn)。
判斷一個(gè)對象是生存還是死亡的算法
在根搜索算法中不可達(dá)的對象,并非是必須死亡的。要真正宣告一個(gè)對象的死亡,至少需要經(jīng)歷兩次標(biāo)記過程:如果對象在進(jìn)行根搜索后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈,那它將會(huì)被第一次標(biāo)記并且進(jìn)行一次篩選,篩選的條件是此對象是否有必要執(zhí)行finalize()方法。當(dāng)對象沒有覆蓋finalize()方法,或者finalize()方法以及被虛擬機(jī)調(diào)用過,虛擬機(jī)將這兩種情況視為“沒有必要執(zhí)行”,那么這個(gè)對象就可以死亡了。如果這個(gè)對象被判斷為有必要執(zhí)行finalize()方法,那么這個(gè)對象將會(huì)被放置在一個(gè)叫做F-Queue的隊(duì)列中,并在稍后由一條由虛擬機(jī)自動(dòng)建立的、低優(yōu)先級(jí)的Finalize線程去執(zhí)行(虛擬機(jī)執(zhí)行這個(gè)方法,但是不保證運(yùn)行結(jié)束)。finalize()方法是對象逃離死亡的最后一次機(jī)會(huì),GC將對F-Queue的對象進(jìn)行第二次標(biāo)記,如果對象在finalize()中重新建立了引用,那么就不會(huì)死亡,否則將會(huì)死亡。
垃圾收集算法
以下介紹了“標(biāo)記-清除算法”、“復(fù)制算法”、“標(biāo)記-整理算法”以及“分代收集算法”。
標(biāo)記-清除算法
標(biāo)記-清除算法(Mark-Sweep)是最基本的算法,分為“標(biāo)記”和“清除”兩個(gè)階段。首先標(biāo)記處所有需要回收的對象,在標(biāo)記完成之后就統(tǒng)一清除掉所有被標(biāo)記的對象。
主要缺點(diǎn):
-
效率問題。標(biāo)記和清除的過程效率都不高
-
空間問題。標(biāo)記清除以后會(huì)產(chǎn)生大量不連續(xù)的空間碎片,空間碎片太多會(huì)導(dǎo)致程序以后的內(nèi)存分配問題。
復(fù)制算法
復(fù)制算法為了解決效率問題。它將可同內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)一塊的內(nèi)存用完了,就將還存活的對象復(fù)制到另一塊上,然后再把原先那塊內(nèi)存空間一次清理掉。這樣,就每次只對一塊內(nèi)存進(jìn)行分配,也不用考慮內(nèi)存碎片問題。
主要缺點(diǎn):
-
沒存縮小為原來的一半,代價(jià)高。
-
在對象存活率較高的時(shí)候就要執(zhí)行較多的復(fù)制操作,效率將會(huì)變低。
現(xiàn)在的商業(yè)虛擬機(jī)都采用這種算法回收新生代。
在新生代中,有一塊比較大的Eden和兩塊比較小的Survivor空間,每次使用Eden和其中的一塊Survivor;回收的時(shí)候,將Eden和Survivor中還活著的對象一次性拷貝到另一塊Survivor上,清除已被使用的Eden和Survivor。當(dāng)Survivor不夠用的時(shí)候,使用老年代進(jìn)行分擔(dān)。
所以,默認(rèn)的Eden和兩塊Survivor大小比為8:1:1。
標(biāo)記-整理算法
Mark-Compact算法的標(biāo)記過程和“標(biāo)記-清除算法”一樣,但后續(xù)步驟不是直接對可回收對象進(jìn)行清理,而是讓所有存活的對象都向一端移動(dòng),然后直接清理掉邊界以外的內(nèi)存。
分代收集算法
當(dāng)前的商業(yè)虛擬機(jī)都采用“分代收集(Generation Collection)”算法。這種算法根據(jù)對象的存活周期的不同將內(nèi)存劃分為幾塊。一般把Java的堆分為新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)厥占惴ā?/p>
在新生代中,每次垃圾收集時(shí)都有大量對象死去,只有少量存活,那就使用復(fù)制算法,只需要付出少量存活對象的復(fù)制成本就可以完成收集。
在老年代中,對象存活率高,沒有額外空間對它進(jìn)行分配擔(dān)保,使用“標(biāo)記-清除”算法或者“標(biāo)記-整理”算法。
垃圾回收器
收集算法是內(nèi)存回收的方法論,垃圾收集器就是內(nèi)存回收的具體實(shí)現(xiàn)。
Serial收集器
這個(gè)收集器是個(gè)單線程收集器。它在工作的時(shí)候必須暫停其他所有的工作線程(Stop The World),直到它收集結(jié)束。這項(xiàng)工作實(shí)際上是由虛擬機(jī)在后臺(tái)自動(dòng)發(fā)起和自動(dòng)完成的,在用戶不可見的情況下把用戶的正常工作的線程停止掉,然后進(jìn)行垃圾收集。
它是虛擬機(jī)運(yùn)行在Client模式下的默認(rèn)新生代收集器。優(yōu)于其他收集器的地方是:簡單而高效。
ParNew收集器
ParNew收集器是Serial收集器的多線程版本。在控制參數(shù)、收集算法、Stop The World、對象分配規(guī)則、回收策略等都與Serial收集器一樣。
它是虛擬機(jī)運(yùn)行在Server模式下的默認(rèn)新生代收集器。它能與CMS收集器配合工作。它默認(rèn)開啟的線程數(shù)和CPU的數(shù)量相同。
Parallel Scavenge收集器
它也是一個(gè)新生代收集器,也是使用復(fù)制算法,是并行的多線程收集器。它的目標(biāo)是達(dá)到一個(gè)可控制的吞吐量(Throughput = 運(yùn)行用戶代碼的時(shí)間/(運(yùn)行用戶代碼的時(shí)間+垃圾回收時(shí)間))。所以被稱為“吞吐量優(yōu)先”收集器。
主要適用于在后臺(tái)運(yùn)行而不需要太多交互的任務(wù)。
Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本。也是一個(gè)單線程收集器,使用“標(biāo)記-整理”算法。
它主要是被在Client模式下虛擬機(jī)使用。如果在Server模式下,它有:1)在1.5及以前版本中與Parallel Scavenge收集器搭配使用;2)作為CMS收集器的后備預(yù)案,在收集器發(fā)生Concurrent Mode Failure的時(shí)候使用。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標(biāo)記-整理”算法。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。它是基于“標(biāo)記-清除”算法實(shí)現(xiàn)的。整個(gè)過程分為4個(gè)步驟:
-
初始標(biāo)記(CMS initial mark)
-
并發(fā)標(biāo)記(CMS concurrent mark)
-
重新標(biāo)記(CMS remark)
-
并發(fā)清除(CMS concurrent sweep)
初始標(biāo)記需要進(jìn)行Stop The World,它僅僅是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對象,速度很快;
并發(fā)標(biāo)記就是進(jìn)行GC Roots Tracing的過程;這個(gè)階段的耗時(shí)比較長;
重新標(biāo)記也需要進(jìn)行Stop The World,該階段是為了修正并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運(yùn)行而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對象的標(biāo)記記錄。這個(gè)階段的停頓時(shí)間會(huì)比初始標(biāo)記長些而遠(yuǎn)遠(yuǎn)短于并發(fā)標(biāo)記;
并發(fā)清除就是進(jìn)行清除的過程;這個(gè)階段的耗時(shí)比較長;
CMS收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)地執(zhí)行的。
CMS收集器很符合現(xiàn)在互聯(lián)網(wǎng)或者B/S系統(tǒng)服務(wù)器的需求——重視服務(wù)的響應(yīng)速度、希望系統(tǒng)停頓時(shí)間短。
CMS收集器的顯著缺點(diǎn):
-
CMS收集器對CPU資源非常敏感。CMS默認(rèn)的回收線程數(shù)為:(CPU數(shù)量+3)/4。也就是當(dāng)CPU在4個(gè)以上的時(shí)候,并發(fā)回收時(shí)垃圾收集器線程最多占用不超過25%的CPU資源;當(dāng)CPU不足4個(gè)的時(shí)候,那么CMS對用戶程序的影響就比較大。
-
CMS收集器無法處理浮動(dòng)垃圾(Floating Garbage),可能會(huì)出現(xiàn)“Concurrent Mode Failure”失敗而導(dǎo)致另一次Full GC的產(chǎn)生。
-
CMS收集器采用“標(biāo)記-清除”算法,在收集結(jié)束的時(shí)候可能會(huì)產(chǎn)生大量的空間碎片。
G1收集器
Garbage First收集器?;?ldquo;標(biāo)記-整理”算法,可以非常顯著的控制停頓。
特點(diǎn):
-
并行與并發(fā):和CMS類似。
-
分代收集:保留了新生代和來年代的概念,但新生代和老年代不再是物理隔離的了它們都是一部分Region(不需要連續(xù))的集合。同時(shí),為了避免全堆掃描,G1使用了Remembered Set來管理相關(guān)的對象引用信息。
-
空間整合:由于G1使用了獨(dú)立區(qū)域(Region)概念,G1從整體來看是基于“標(biāo)記-整理”算法實(shí)現(xiàn)收集,從局部(兩個(gè)Region)上來看是基于“復(fù)制”算法實(shí)現(xiàn)的,但無論如何,這兩種算法都意味著G1運(yùn)作期間不會(huì)產(chǎn)生內(nèi)存空間碎片。
-
可預(yù)測的停頓:這是G1相對于CMS的另一大優(yōu)勢,降低停頓時(shí)間是G1和CMS共同的關(guān)注點(diǎn),但G1除了追求低停頓外,還能建立可預(yù)測的停頓時(shí)間模型,能讓使用這明確指定一個(gè)長度為M毫秒的時(shí)間片段內(nèi),消耗在垃圾收集上的時(shí)間不得超過N毫秒。
步驟:
-
初始標(biāo)記(Initial Making)
-
并發(fā)標(biāo)記(Concurrent Marking)
-
最終標(biāo)記(Final Marking)
-
篩選回收(Live Data Counting and Evacuation)
初始階段僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對象,并且修改TAMS(Next Top Mark Start)的值,讓下一階段用戶程序并發(fā)運(yùn)行時(shí),能在正確可以用的Region中創(chuàng)建新對象,這個(gè)階段需要停頓線程,但耗時(shí)很短。并發(fā)標(biāo)記階段是從GC Roots開始對堆中對象進(jìn)行可達(dá)性分析,找出存活對象,這一階段耗時(shí)較長但能與用戶線程并發(fā)運(yùn)行。而最終標(biāo)記階段需要吧Remembered Set Logs的數(shù)據(jù)合并到Remembered Set中,這階段需要停頓線程,但可并行執(zhí)行。最后篩選回收階段首先對各個(gè)Region的回收價(jià)值和成本進(jìn)行排序,根據(jù)用戶所期望的GC停頓時(shí)間來制定回收計(jì)劃,這一過程同樣是需要停頓線程的,但Sun公司透露這個(gè)階段其實(shí)也可以做到并發(fā),但考慮到停頓線程將大幅度提高收集效率,所以選擇停頓。
內(nèi)存配置與回收策略
Java技術(shù)體系中的自動(dòng)內(nèi)存管理最終可以歸納為自動(dòng)化地解決了兩個(gè)問題:給對象分配內(nèi)存以及回收分配給對象的內(nèi)存。
對象優(yōu)先在Eden中分配
大多數(shù)情況下,對象在新生代Eden區(qū)中分配。檔Eden區(qū)沒有足夠的空間進(jìn)行分配時(shí),虛擬機(jī)將發(fā)起一次Minor GC。
-Minor GC:新生代GC,指發(fā)生在新生代的垃圾收集動(dòng)作,因?yàn)镴ava對象大多數(shù)都具備朝生夕死的特性,所以Minor GC非常頻繁,一般回收速度也非???。
-Major GC/Full GC:老年代GC,指發(fā)生在老年代的GC,出現(xiàn)了Major GC經(jīng)常就會(huì)至少有一次Minor GC。Major GC的速度比Minor GC慢10倍以上。
大對象直接進(jìn)入老年代
大對象是指,需要大量連續(xù)內(nèi)存空間的Java對象,最典型的大對象就是那種很長的字符串以及數(shù)組。經(jīng)常出現(xiàn)大對象容易導(dǎo)致內(nèi)存還有不少空間時(shí)就提前觸發(fā)垃圾收集以獲取足夠的連續(xù)空間來“安置”它們。
長期存活的對象將進(jìn)入老年代
虛擬機(jī)給每個(gè)對象定義一個(gè)年齡計(jì)數(shù)器,對象在Eden中經(jīng)歷第一次Minor GC仍然存活,就被移動(dòng)到Survivor空間,設(shè)置年齡為1,在Survivor空間中沒經(jīng)歷一次Minor GC,年齡加1,當(dāng)達(dá)到默認(rèn)的年齡15以后,就將被放到老年代。
動(dòng)態(tài)對象年齡判定
如果在Survivor空間中,相同年齡所有對象大小的總和大于Survivor空間的一般,那么年齡大于或等于該年齡的對象就可以直接進(jìn)入老年代。
空間分配擔(dān)保
發(fā)生Minor GC時(shí),虛擬機(jī)會(huì)檢測之前每次晉升到老年代的平均大小是否大于老年代的剩余空間大小,如果大于,則改為直接進(jìn)行一次Full GC。如果小于,則查看HandlePromotionFailure設(shè)置是否允許擔(dān)保失?。蝗绻试S,則只會(huì)進(jìn)行Minor GC;如果不允許,則也要改為進(jìn)行一次Full GC。
新生代Eden,Survivor A, Survivor B三塊空間和老生代Old之間的流程關(guān)系: