周志明老師所著的《深刻了解JAVA虛擬機》(後文簡稱"書中")可謂是java工程師進階的必讀書籍了.最近讀了書中的第一二部分,也就是前五章,有不少收穫.所以想要寫一篇文章.來用本身理解到的知識來總結一下前五章.java
雖說是總結,可是仍然強烈推薦你們去看原著.原著並無"多出什麼東西致使須要我進行總結",而是每一個小節都讓我有所收穫.可是我並不能所有記住書中所寫,只能按照本身的思路記錄,串聯起來. 再次推薦一下你們閱讀原著程序員
書中屢次提到:算法
Java和C++之間有一堵由內存動態分配和垃圾收集所圍成的高牆,牆外的人想進去,牆裏的人想出來.安全
C/C++程序員對每個對象的內存分配擁有絕對的控制權,可是這樣就會很繁瑣.Java程序員不用處理內存的分配,有JVM動態進行,在 出現內存泄漏的時候卻比較難以排查.多線程
根據所new的對象動態的進行內存分配,以及在合適的時間回收/釋放掉不須要的對象,這就是JVM的自動內存管理機制.併發
JVM在執行java代碼的時候,會將系統分配給他的內存劃分爲幾個區域,來方便管理.比較經典的運行時數據區域圖以下:性能
程序計數器:學習
程序計數器是一塊比較小的線程獨立的內存空間,它能夠當作是當前線程執行的字節碼的行號指示器.測試
虛擬機棧spa
虛擬機棧也是線程私有內存.每一個方法在執行的時候都會建立一個"棧幀",裏面存儲了局部變量表,操做數棧,動態連接,方法出口等信息.能夠理解爲虛擬機棧存儲了方法運行時須要的一些額外信息,一個"棧幀"的入棧出棧對應了一個方法的執行開始與結束.
本地方法棧
若是咱們將上面的虛擬機棧理解爲"爲了java方法的執行而記錄一些內容",那麼本地方法棧就是爲了Native方法二記錄的.其餘方面基本一致.虛擬機規範中對這一塊的規定不嚴格,所以各個虛擬機的實現不一樣.著名的"HotSpot"把虛擬機棧和本地方法棧進行了合併.
堆
堆(Heap)是JVM內存中最大的一塊,也是垃圾收集的主要工做區域.這塊區域惟一的目的就是存放類的實例.堆中根據虛擬機的不一樣還有不一樣的區域劃分,以便垃圾收集進行工做. 其中的詳細區域劃分在後面垃圾收集的地方會詳細說明.
方法區
方法區也是一塊線程共享區域,用於存儲已經加載了的類信息,常量,靜態變量,即時編譯器編譯後的代碼等等.
他有一個更加響亮的名字"永久代",HotSpot虛擬機將方法區實現成了永久代,來避免單獨爲方法區實現垃圾收集.這一舉動的利弊不是我個小菜雞能夠分析的,可是咱們要理解爲何叫作永久代?由於這一區域存放的內容,垃圾收集的效率是比較低的(常量,靜態變量等較少須要被回收),因此當數據進入此區域,就好像永久存在了一下.
這一區域裏面還有一個單獨的區域,運行時常量池,當類加載後,各類字面量和符號引用會進入此區域. 在程序運行期間,也是能夠將新的常量放入常量池的,好比string.intern()
方法.
直接內存
直接內存並無在上圖的JVM運行時數據區域中體現,而是一塊額外的內存區域.在JDK1.4中引入的NIO中,能夠直接經過Native方法在堆外分配內存.這樣能夠提升性能.
這塊區域的大小不受到給虛擬機分配的內存大小的限制,可是總歸也是受到物理機的內存限制的,所以,當出現OutOfMemoryError,且代碼中有大量使用到NIO的時候,能夠考慮到是這一塊內存產生了溢出.
說到對象的建立過程,也許咱們都會想到那個很經典的題目:一個父類一個子類,幾個靜態方法幾個普通方法,幾個構造方法,問這些方法中的打印順序.
可是不要誤會,那些東西在如今並不重要了,須要機建立對象的過程要遠比這複雜的多.簡單歸納以下:
在第二步其實還有一個問題,那就是併發問題,若是隻有一個指針指在已經使用和未使用的內存之間,那麼在頻繁的建立過程當中,必定有併發問題.虛擬機解決這個問題的辦法主要有兩種:
在HotSpot中, 對象信息包括: 對象頭,實例數據和對齊填充.
對象頭: 對象頭中包括兩部分信息,對象的運行數據(hash碼,GC年齡等),類型指針(指明它是哪一個類的實例). 實例數據: 這塊的數據就是咱們在代碼中定義的那些字段等等. 對齊填充: 這塊數據並非必然存在的,當對象實例數據不是8字節的整數倍的時候,用空白字符對齊一下.
對象內存分配其實與選擇的垃圾收集器,虛擬機啓動參數等有很大的關係,所以並不能肯定的說:XXX在XXX上分配.可是總歸是有一些普適性的規則的.
優先在Eden分配
大多數的狀況下,對象首先在Eden區域分配,當Eden區域空間不足的時候,虛擬機將會進行一次Minor GC(新生代GC).
大對象直接進入老年代
大對象(虛擬機提供了參數:-XX:PretenureSizeThreshold來調整大對象的閾值)會直接分配在老年代.因爲新生代使用複製的垃圾收集算法,若是將大對象分配到新生代,可能會形成在兩個Survivor區域之間發生大量的內存複製.影響垃圾收集的效率.
長期存活的對象進入老年代
每一個對象都有一個年齡的計數器,當對象在eden出生而且通過一次minor GC還在新生代的話,年齡就加1. 當年齡到了15(默認值)時,會晉升到老年代中.
動態的年齡判斷
上面到達年齡以後晉升到老年代並非惟一的規則, 當Survivor空間中的相同年齡的對象的總大小的綜合大於Survivor空間的一半,虛擬機會認爲這個年齡是一個更加合適的閾值,會將年齡大於或者等於這個值的對象所有移到老年代中去.
分配擔保
當minor GC即將發生時,虛擬機會檢查老年代是否能夠做爲這次的分配擔保(老年代中的連續內存大於新生代中存活全部對象的總和),若是成立,那麼說明能夠做爲擔保,進行minorGC.
若是不成立,那就檢查虛擬機設置裏面HandlePromotionFailure是否容許進行冒險,若是容許的話,則進行minorGC,不然則進行FullGC. 若是冒險失敗了,那就進行一次FullGC來在老年代騰出足夠的空間.
提及垃圾收集,咱們老是能夠零碎的說上一些,由於JVM的應用太普遍了,除了Java開發者還有許多其餘基於JVM的開發者也須要了解這些. 可是咱們有沒有系統的整理過這裏呢?
垃圾收集,即將無用的內存釋放掉,以提供給後續的程序使用.那麼就有三個問題:
咱們一個一個問題的來看.
固然是對死掉的,即不再會用到的對象進行回收.
怎麼判斷一個對象不再會被用到了呢?
首先就是引用計數法,它的思想是給每一個對象設置一個計數器,每當有一個別的地方引用到了這個對象,計加器就加1.當其餘地方釋放掉對它的引用時,就減1.那麼計數器等於0的對象,就是不可能再被引用的對象了.
這個算法其實還能夠,實現簡單,判斷速度快,可是主流的JVM實現裏面沒有使用這個方法的,由於它有一個比較致命的問題,就是沒法解決循環引用的問題.
當兩個對象互相引用,除此以外沒有其餘引用的時候,他們應該被回收,可是此時他們的計數器都爲1.致使他們沒有辦法被回收.
咱們用如下代碼進行一下測試:
public class ReferenceCountTest {
public static final byte[] MB1 = new byte[1024 * 1024];
public ReferenceCountTest reference;
public static void main(String[] args) {
ReferenceCountTest a = new ReferenceCountTest();
ReferenceCountTest b = new ReferenceCountTest();
a.reference = b;
b.reference = a;
a = null;
b = null;
System.gc();
}
}
複製代碼
運行參數爲:+XX:PrintGC
,輸出結果[GC (System.gc()) 7057K->2294K(125952K), 0.0024641 secs]
,能夠看到,內存被回收掉了,說明我使用的HotSpot虛擬機使用的不是引用計數法來判斷對象存活與否.
這個算法的基本思想就是,經過一系列的GC ROOT來做爲起點,從這些節點開始沿着引用鏈進行搜索,當一個對象到GCROOTS沒有任何的可達路徑,就認爲此對象是可被回收的.
在上圖中,object5,6,7雖然互相之間還有引用,可是因爲從GCROOTS不可達,也是死掉的對象.
在Java中GCROOTS通常包括如下幾種:
這個問題其實比較複雜,且不少JVM的實現並不相同,咱們粗略的以HotSpot爲例說明一下.
首先咱們要知道,垃圾收集是須要"Stop The World"的,由於若是整個JVM不暫停,那麼就沒法在某一瞬間肯定哪些內存須要回收.就好像你媽媽給你打掃房間的時候會把你趕出去,由於若是你不斷製造垃圾,是沒有辦法打掃乾淨的.
目前全部的JVM實現,在進行根節點的枚舉(也就是肯定哪些內存是須要回收的)這一步驟的時候都須要停頓,你們在作的只是儘量的減小GC停頓來下降對系統的影響.
既然GC須要"Stop The World",可是一個運行中的先生並非能夠在隨時隨地停下來配合GC的.
因此當須要GC停頓的時候,須要給出一點時間,讓全部線程運行到最近的"安全點"上.此外,爲了解決在GC時有些線程處在掛起狀態,安全點概念還有一個擴展的概念,安全區域,當線程進入到安全區域,就會掛起一個牌子,告訴別人在我摘下牌子以前,GC不用問我.而當線程想離開安全區域的時候,須要檢查是否本身能夠安全離開的標識.
不一樣虛擬機的實現不同,同一個虛擬機在堆上不一樣的區域執行的可能也不同,不過總的來講,算法思想都是下面這幾種.
最基礎的就是**標記-清除(Mark-Sweep)**了,該算法的過程和名字同樣,首先標記全部須要回收的對象,以後對他們統一進行回收.以下圖所示.
他的優勢是: 思路簡單且實現方便 缺點主要有兩個:
1.效率不過高
2.在圖中回收後的狀態裏,因爲是直接的清除,因此可用內存不連續,全是碎片化的,這樣當後續須要分配大對象而沒法找到連續足夠的空間,就會提早觸發下一次GC
後續的算法主要就是對 標記-清除算法的改進.
爲了解決上面的問題,出現了"複製"算法,複製算法將內存分爲容量相等的兩塊,每次只使用其中的一塊,當用完了,將其中存活的對象copy到另一塊內存上,而後對已經使用的這一塊內存進行總體的回收. 這樣可使得回收和分配時不用考慮碎片問題,效率極大的提高了,可是,代價是永遠只能使用一半的內存,這個代價太過於高昂了.
複製算法的執行過程以下圖:
現代的商業虛擬機基本上都採用這個算法來回收新生代.由於新生代的垃圾回收比較的頻繁,對於效率的要求更加高一些.
同時對複製算法進行了一些改良.通過統計,新生代的對象98%都是朝生夕死的,因此複製算法中的內存不須要按照1:1進行劃分,而是劃分爲Eden:Survivor1:Survivor2=8:1:1
(比例可調整)三塊空間,每次使用Eden和一個 Survivor區域,當須要垃圾回收時,將其中存活的對象copy到另外一個survivor中.而後對eden和已經使用survivor進行統一回收.這樣相比於普通的複製算法,每次可使用到90%的空間,浪費較小.
可是,survivor的內存大小是咱們進行估算獲得的,咱們沒有辦法確保每次垃圾回收時存活的對象都小於10%,因此須要老年代進行分配擔保.分配擔保是指,若是survivor的空間不夠用,能夠在老年代裏申請空間存放對象.
複製算法在對象存活率較低是一種可靠的算法,可是當對象存活率較高,極端狀況下,一次gc的時候,100%的對象都存活,那麼複製算法的效率就不高了.所以在HotSpot的老年代中使用另一種算法.即標記-整理算法.
標記-整理算法,首先仍然是和標記-清除算法同樣的標記過程,可是以後並不進行直接的清除,而是將存活的對象整理的整齊一點,而後以邊界爲限,回收掉邊界之外的內存.示意圖以下:
在上面的垃圾收集算法中也提到了新生代,老年代等概念,這就是因爲如今的虛擬機都使用分代收集的算法.
分代的主要目的是:根據對象的存活週期不一樣,把內存區域分爲幾塊,存放不一樣生命週期的對象,以方便根據特色使用不一樣的垃圾收集算法來提升內存回收效率.
好比新生代中對象存活率低,那麼可使用複製算法,每次copy少許的對象便可,且效率較高.
而老年代中的對象存活率高,而且沒有人能爲他作分配擔保,所以必須使用標記-整理或者標記-清除算法.
因此在 HotSpot中,整個Java堆大體是以下的樣子(新生代和老年代的比例默認爲1:2):
Serial收集器
這是最基本也是最古老的的垃圾收集器,是一個單線程收集的過程,目前仍然是Client模式下的JVM的默認新生代收集器.
下圖是他的收集過程:
ParNew
ParNew收集器是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集以外,其他行爲和Serial收集器如出一轍.下圖是他的收集過程:
Parallel Scavenge收集器
這個收集器在定義上和ParNew很是類似,可是它主要關注的是提升系統的吞吐量.他的收集過程和ParNew類似.
Serial Old
Serial Old收集器是Serial收集器的老年代版本,使用了標記-整理算法.他的收集過程和Serial同樣.
Parallel Old
這是Parallel Scaevnge收集器的老年代版本,使用多線程進行標記-整理算法進行收集.
他的收集過程和Parallel Scavenge收集器同樣.
CMS收集器
Concurrent Mark Sweep 是一個以最短停頓時間爲目的的收集器,他的收集過程更加複雜一點,分爲四個步驟:
他的收集過程以下所示:
G1收集器
G1收集器是發展的比較好的收集器,他的收集步驟大概有如下幾個部分:
他的收集過程圖以下:
總結
垃圾收集器並非能夠無限搭配的,下面是他們的搭配圖:
這裏對垃圾收集器的介紹比較簡略,主要是垃圾收集器其實是一個很複雜的東西,可是是一個封裝的很好的東西,裏面的複雜不太須要知道,大部分時間咱們用穩定的最新的研究成果便可....
可是,咱們應該瞭解一下,在感受瓶頸出在了垃圾收集器的時候,有能夠去詳細研究的能力以及基礎知識便可.
深刻理解JVM
完。
以上皆爲我的所思所得,若有錯誤歡迎評論區指正。
歡迎轉載,煩請署名並保留原文連接。
聯繫郵箱:huyanshi2580@gmail.com
更多學習筆記見我的博客------>呼延十