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