分享者:銳哥
JVM在執行java程序的過程當中會把它所管理的內存劃分紅若干個不一樣的數據區域。java
(1)程序計數器算法
程序計數器(Program Counter Register)是一塊比較小的內存區域,它能夠看做是當前線程所執行的字節碼指令的行號計數器。在虛擬機的概念模型裏,字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條要執行的字節碼指令。 c#
因爲java虛擬機的多線程是經過線程的輪流切換並分配CPU時間片來實現的,在任何一個肯定的時刻,一個核只會執行一條線程中的指令,爲了線程切換後能恢復到正確的執行位置,每一條線程都須要有一個獨立的程序計數器,所以,程序計數器是線程私有的內存。 數組
(2)虛擬機棧安全
虛擬機棧(Java Virtual Machine Stack)也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是java方法執行的內存模型:每一個方法在執行的同時都會建立一個稱爲棧幀(Stack Frame)的東西,用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每一個方法從調用開始直至執行完成,都對應着一個棧幀在虛擬機棧中入棧和出棧的過程。 多線程
若是線程請求的棧深度超過了虛擬機的最大深度,那麼就會拋出StackOverFlowError異常;若是虛擬機能夠動態拓展而且在拓展時沒法申請到足夠的內存,將拋出OutOfMemoryError異常。
併發
(3)本地方法棧佈局
本地方法棧(Native Method Stack)和虛擬機棧同樣,都是線程私有的,只不過虛擬機棧爲虛擬機執行java方法服務,而本地方法棧則爲虛擬機執行native方法服務。
性能
(4)Java堆spa
對大多數應用程序來講,java堆(Java Heap)都是java虛擬機所管理的內存區域中最大的一塊。java堆是被全部線程所共享的一塊內存區域,在虛擬機啓動時建立。此內存區域存在的惟一目的就是存放對象實例,幾乎全部的對象都在此內存區域上進行分配。
根據java虛擬機規範的規定,java堆能夠處於物理上不連續的內存空間,只要邏輯上是連續的便可。在實現時,能夠實現成固定大小的,也能夠實現成可拓展的。當拓展時,若是沒法申請到足夠的內存,將拋出OutOfMemoryError異常。
(5)方法區
方法區(Method Area)也是被各個線程所共享的內存區域,它用於存儲已經被虛擬機加載的類信息、常量、靜態變量、JIT編譯器編譯後的代碼等數據。
對於習慣在HotSpot虛擬機上開發、部署程序的的開發者來講,更習慣於把方法區稱爲」永久代「,但本質上二者並不等價。
(6)運行時常量池
運行時常量池(Runtime Constant Pool)是方法區的一部分。class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載之後進入方法區的運行時常量池中存放。
符號引用用一組符號來描述所引用的目標,符號能夠是任何形式的字面量,只要使用時可以無歧義地定位到目標便可。符號引用與虛擬機的內存佈局無關,引用的目標不必定加載到內存中。例如org.simple.People類引用了org.simple.Language類,在編譯時,People類並不知道Language類的實際內存地址,所以只能使用符號來代替,這就是符號引用。
HotSpot虛擬機中,對象在內存中存儲的佈局能夠分爲3塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。
實例數據部分是對象真正存儲的有效信息。這部分的存儲順序會受到虛擬機分配策略參數和字段在Java源碼中定義順序的影響。
使用句柄訪問方式的話,java堆中將會劃分出一塊內存來做爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例和類型數據各自具體的地址信息。
使用句柄訪問的最大好處就是reference中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而reference自己不須要修改。使用直接指針訪問的最大好處是速度更快,節省了一次指針定位的時間開銷。HotSpot虛擬機採用的是直接指針方式。
對於第一個問題,哪些內存須要回收,就是哪些對象是不可用的。第二個問題,何時回收,一句話概述就是內存不夠用的時候進行回收。第三個問題,就涉及到回收的具體實現上了。
(1)引用計數算法
(2)可達性分析
在Java中,能夠做爲GC Roots的對象包括下面幾種:
a. 虛擬機棧(棧幀中的本地變量表)中引用的對象
b. 方法區中類靜態變量所引用的對象
d. 本地方法棧中JNI(即Native方法)引用的對象
主流的商用程序語言(java、c#等)都是採用的可達性分析算法來斷定對象是否存活。
(1)標記-清除算法
這種算法存在兩個缺點:
a. 效率問題。」標記」和」清除」兩個階段的效率都不高
(2)複製算法
這種算法的缺點是內存利用率只有50%
(3)標記-整理算法
標記-整理算法(Mark-Compact)是針對老年代的特色(對象存活率較高、沒有額外空間進行分配擔保)而提出的。這種算法分爲標記和整理兩個階段,標記階段和標記-清除算法的標記階段一致,可是整理階段不是直接對標記對象進行回收,而是讓還存活的對象向一端移動,而後一次性清理掉端邊界之外的內存。
(4)分代收集
新生代中,對象的存活率較低,就採用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集;老年代中由於對象存活率高,也沒有額外空間進行分配擔保,因此採用標記-清除算法或者標記-整理算法。
若是說垃圾收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。
(1)Serial收集器
它是虛擬機運行在client模式下的新生代默認的收集器:由於它簡單而高效,對於限定單個CPU的環境來講,Serial收集器由於沒有線程交互的開銷,專心作垃圾收集天然能夠得到最高的單線程收集效率。
(2)ParNew收集器
ParNew收集器是許多運行在Server模式下的虛擬機中首選的新生代收集器,一個很重要可是與性能無關的緣由是:除了Serial收集器外,只有它能與CMS收集器配合工做。
(3)Parallel Scavenge收集器
停頓時間越短,就越適合須要與用戶交互的程序,良好的響應速度可以提高用戶體驗,而高吞吐量則能夠高效率地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不須要太多交互的任務。
除上述兩個參數以外,Parallel Scavenge還提供一個參數 -XX:UseAdaptiveSizePolicy。若是開啓了這個參數,就不須要手動指定新生代的大小(-Xmn)、Eden與Survivor的比率(-XX:SurvivorRatio)、晉升老年代對象的年齡(-XX:PretenureSizeThreshold)等細節參數了,虛擬機會根據當前系統的運行狀況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱爲 GC自適應調節策略。
(4)Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本,使用」標記-整理」算法。這個收集器的主要意義也是給client模式下的虛擬機使用。若是在Server模式下,它還有另外兩個做用:一種用途是在JDK 1.5及以前的版本中與Parallel Scavenge收集器搭配使用,另外一種用途就是做爲CMS收集器的後備預案,在CMS收集器發生Concurrent Mode Failer時使用。
(5)Parallel Old收集器
(6)CMS收集器
CMS收集器(Concurrent Mark Sweep,併發標記清除)是一種以獲取最短停頓時間爲目標的收集器。基於「標記-清除」算法實現,它的運做過程大體分爲4個步驟:
a. 初始標記
c. 從新標記
其中,初始標記和從新標記過程仍然須要「Stop The World」。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快。從新標記階段就是爲了修正併發標記階段因用戶程序繼續運行而致使標記產生變更的那一部分對象的標記記錄。
這個收集器存在3個明顯的缺點:
b. CMS沒法處理浮動垃圾(Floating Garbage),可能出現「Concurrent Mode Failer」失敗而致使另外一次Full GC的產生。併發清理階段,用戶程序還在運行着,伴隨着就會產生新的垃圾,這一部分的垃圾出如今標記以後,沒法在當次收集中處理掉,只好等待下一次收集時處理,這一部分垃圾稱之爲「浮動垃圾」。也是由於併發清除階段,用戶程序還在運行,那也就必需要預留一部份內存空間給併發收集時用戶程序使用。在JDK1.5的默認設置下,CMS收集器在當老年代使用了68%的空間後就會被激活,在JDK1.6種,這個閾值被提高到了92%。要是CMS運行期間預留的內存沒法知足程序須要,就會出現一次「Concurrent Mode Failer」失敗,這時虛擬機將啓動後背預案:臨時啓用Serial Old收集器來從新進行老年代的垃圾收集。
(7)G1收集器
G1(Garbage First)收集器是一款面向服務端應用的垃圾收集器,JDK 1.7中才出現。相比其餘的垃圾收集器,G1具備更加明顯的優點:
a. G1中雖然還保留分代的概念,可是G1能獨立管理整個Java堆,再也不須要像之前那樣要多個收集器配合。由於G1將整個堆劃分紅多個大小相等的獨立區域(Region),新生代和老年代的概念雖然保留着,可是再也不是物理隔離的了,他們都是一部分Region(不須要連續)的集合。
b. 空間整合。G1從總體上來看是基於標記-整理算法實現,從局部(兩個Region之間)來看是基於複製算法實現,避免了內存碎片的問題。
c. 可預測的停頓。這是G1相對於CMS的另外一大優點。G1除了追求低停頓之外,還能創建可預測的停頓時間模型,能讓使用者指明在一個長度爲M毫秒的時間片斷內,消耗在垃圾收集上的時間不得超過N毫秒。
G1之因此能創建可預測的停頓時間模型,是由於它能夠有計劃的避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤每一個Region裏面的垃圾堆積的價值大小(回收這塊Region所得到的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的Region(這也是Garbage First名稱的由來)。
Java技術體系中所講的自動內存管理最終能夠歸結爲自動化的解決了兩個問題:給對象分配內存以及回收分配給對象的內存。內存的回收上面已經說過原理,下面看一下內存分配。
對象在新生代Eden區中分配,當Eden區中沒有足夠的內存進行分配時,虛擬機將發起一次Minor GC。
虛擬機提供了一個參數 -XX:PretenureSizeThreshold ,大於這個參數設置值的對象將不會在Eden區域上進行分配,而是直接在老年代進行分配。這樣作的目的是爲了不在Eden區以及兩個Survivor區之間發生大量的內存複製(當大對象的存活率比較高時)。
前面在說對象的內存佈局時提到對象頭中有一部分存儲對象自身運行時所須要的數據,例如哈希碼、GC分代年齡,這裏面的GC分代年齡指的就是對象在新生代中熬過的GC次數,每熬過一次Minor GC,對象的年齡就增長一歲,當它的年齡增長到必定程度(默認是15歲)就會被晉升到老年代。這個晉升老年代的閾值能夠經過參數 -XX:MaxTenuringThreshold 來設置。
對象的年齡不必定非要達到 MaxTenuringThreshold 設置的值才能晉升老年代。若是在Survivor中相同年齡的對象的大小之和超過了Survivor空間大小的一半,那麼年齡大於或等於該年齡的對象將直接進入老年代,無需等到 MaxTenuringThreshold 要求的年齡。
在新生代進行Minor GC以前,虛擬機會作以下的事情:
(1)先檢查老年代最大可用的連續內存空間是否大於新生代全部對象總空間,若是大於,那麼Minor GC就是安全的,由於不會有對象進入老年代。不然進行步驟(2)
(2)查看 HandlePromotionFailure設置值是否容許分配擔保失敗,若是不容許,那麼也要進行一次Full GC。不然進行步驟(3)
(3)檢查老年代最大可用連續空間是否大於歷次晉升老年代的平均大小,若是大於,會嘗試進行一次Minor GC,可是可能會有風險。不然進行步驟(4)
(4)也要進行一次Full GC。
步驟(3)中提到會嘗試進行一次Minor GC,但此次GC會存在風險,這句話是什麼意思呢?前面在講複製算法時提到,新生代在進行GC時,若是Eden空間和From Survivor空間中存活的對象大小之和超過了To Survivor空間的大小,那麼這些存活對象將經過分配擔保機制進入老年代。由於尚未進行Minor GC,因此虛擬機並不知道本次GC中存活對象的大小,因此只好採用歷次晉升老年代對象的平均值來做爲經驗值,若是老年代可用的最大連續內存空間大於經驗值,那麼頗有可能說明老年代可用的最大連續內存空間也大於本次Minor GC存活的對象大小之和,因此會嘗試進行Minor GC。可是若是在進行Minor GC後發現存活對象大小之和大於老年代最大可用連續內存空間,那麼這時老年代將沒法存放這部分存活對象,這就是風險所在,此時老年代就要來一次Full GC以騰出空間。