咱們都知道Java的內存管理機制很是的「自動化」,可讓咱們Java工程師能夠免去內存管理的苦惱,但咱們學習GC和內存分配也是有意義的:當須要排查各類內存溢出、內存泄露問題時,當垃圾收集成爲系統達到更高併發量的瓶頸時,只有瞭解了其原理,咱們才能更好的監控與調節這些問題。提及垃圾回收(Garbage Collection,GC),大部分人都把這項技術當作Java語言的伴生產物。這其中GC真正須要解決的3個問題值得咱們全部想要理解和學習GC的人去思考:html
1.哪些內存須要回收? 2.何時回收內存? 3.如何回收內存?java
帶着這三個問題去思考和學習,我相信必然可以讓咱們更加的理解Java的垃圾回收機制。面試
首先咱們看第一個問題,哪些內存須要回收?咱們知道,在Java堆中存放着幾乎全部的對象實例,垃圾回收器在對堆進行回收前,第一件事就是要肯定這些對象哪些還「存活」着,哪些已經「死去」(即不可能再被任何途徑引用的對象)。 那麼「死去」的這些對象就是咱們須要回收的對象了。下面就介紹判斷對象是否存活所用到的算法。算法
算法分析數據庫
引用計數是垃圾收集器中的早期策略,它的原理是給對象添加一個引用計數器,每當一個地方引用它時,計數器值就加1;讓引用失效時,計數器值就減1;任什麼時候刻計數器爲0的對象是不可能再被使用的。安全
優缺點bash
優勢:引用計數收集器能夠很快的執行,交織在程序運行中。對程序須要不被長時間打斷的實時環境比較有利。網絡
缺點:沒法檢測出循環引用。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數永遠不可能爲0。 引用計數算法沒法解決循環引用問題,例如:數據結構
public class Main {
public static void main(String[] args) {
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();
object1.object = object2;
object2.object = object1;
object1 = null;
object2 = null;
//假設在這行發生GC,object1和object2可否被回收?
System.gc();
}
}複製代碼
正是由於該方法存在這樣的問題,因此目前主流的Java虛擬機裏面已沒有選用引用計數算法來判斷對象是否存活(管理內存)。多線程
2.1算法分析
在主流的商用程序語音(Java、C#)的主流實現中,都是經過可達性分析算法(也可稱爲根搜索算法)來判斷對象是否存活的。該算法是從離散數學中的圖論引入的,這個算法的基本思路是:經過一系列稱爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路稱爲引用鏈(Refence Chain),當一個對象到GC Roots沒有任何鏈表相連(用圖論的話來講,就是GC Roots到這個對象不可達)時,則證實此對象不可用。下圖所展現的,對象object五、object六、object7雖然互相有關聯,可是他們到GC Roots是不可達的,因此他們將會被斷定是可回收的對象。
Java中可做爲GC Roots的對象有
1.虛擬機棧中引用的對象(本地變量表)
2.方法區中靜態屬性引用的對象
3. 方法區中常量引用的對象
4.本地方法棧中引用的對象(Native對象)
接下來是咱們須要思考的第二個問題,何時回收內存?這個問題能夠從HotSpot的算法實現中去找到答案
以可達性分析中從GC Roots 節點找引用鏈這個操做爲例,可做爲GC Roots 的節點主要在全局性的引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中,如今的不少應用僅僅方法區就有數百兆,若是要逐個檢查這裏面的引用,那麼必然會消耗不少時間。
另外,可達性分析對執行時間的敏感還體如今GC停頓上,由於這項分析工做必須在一個能確保一致性的快照中進行—這裏的一致性的意思是指在整個分析期間整個執行系統看起來像被凍結在某個時間點上,不能夠出如今分析過程當中對象引用關係還在不斷的變化,該點不知足的話分析結果的準確性就沒法獲得保證。這點致使GC進行時必須停頓全部Java執行線程(Sun稱這件事情爲「Stop The World」)的其中一個重要的緣由,即便在號稱(幾乎)不會發生停頓的CMS收集器中,枚舉根節點時也是必需要停頓的。
目前主流的Java虛擬機使用的都是準確式GC,因此當執行系統停頓下來後,並不須要一個不漏的檢查完全部的執行上下文和全局的引用位置,虛擬機應當是有辦法直接得知哪些地方存在着對象引用。在HotSpot的實現中,是使用一組稱爲OopMap的數據結構來達到這個目的的,在類加載完成的時候,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程當中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。這樣,GC在掃描時就能夠直接得知這些信息了。
在OopMap的協助下,HotSpot能夠快速且準確的完成GC Roots枚舉,但一個很現實的問題隨之而來:可能致使引用關係變化,或者說OopMap內容變化的指令很是多,若是爲每一條指令都生成對應的OopMap,那麼將須要大量的額外空間,這樣GC 的空間成本將會變得很高。
實際上,HotSpot也的確沒有爲每條指令都生成OopMap,前面已經提到,只是在「特定的位置」記錄這些信息,這些問題稱爲(Safepoint),即程序執行時並不是全部地方都能停頓下來開始GC,只有到達安全點才能暫停。Safepoint的選定既不能太少以致於讓GC等待時間太長,也不能過於頻繁以致於過度增大運行時負荷。因此,安全點的選定基本上是以程序「是否具備讓程序長時間執行的特徵」爲標準進行選定的—由於每條指令執行的時間都很是短暫,程序不太可能由於指令流長度太長這個緣由而過長時間運行,「長時間執行」的最明顯特徵就是指令序列複用。例如:方法調用、循環跳轉、異常跳轉等,因此具備這些功能的指令纔會產生Safepoint。
對於SafePoint,另一個須要考慮的問題是如何在GC發生時讓全部線程(這裏不包括執行JNI調用的線程)都跑到最近的安全點再停頓下來,有兩種方案:
1. 搶先式中斷(Preemptive Suspension):在GC發生時,首先把全部線程所有中斷,若是發現有線程中斷的地方不在安全點上,就恢復線程,讓其跑到安全點上;(幾乎沒有虛擬機使用這種方式)
2. 主動式中斷(Voluntary Suspension):當GC須要中斷線程時,不直接對線程操做,僅僅簡單設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就本身中斷掛起。其中輪詢標誌的地方和安全點是重合的,另外再加上建立對象須要分配內存的地方。
如今主流的虛擬機都是使用的主動式中斷的方式想要GC事件。
上面講述的Safepoint彷佛已經完美的解決了如何進入GC的問題,但實際狀況卻不必定。Safepoint機制保證了程序執行時,在不長的時間內就會遇到可進入GC的Safepoint。可是,程序不執行的時候呢?所謂的程序不執行就是沒有分配CPU時間,典型的例子就是線程處於sleep狀態或者Blocked狀態,這時候線程沒法響應JVM的中斷請求,「走」到安全的地方去中斷掛起,JVM顯然也不太可能等待線程從新被分配CPU時間。這種狀況,就須要安全區域來解決了。
安全區域是指在一段代碼片斷之中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的。能夠把安全區域看作是擴展了的安全點。
在線程執行到安全區域的代碼時,首先標識本身進入到了安全區域,這樣,當這段時間內JVM要發起GC時,就不用管標識本身爲安全區域狀態的線程了;在線程要離開安全區域時,它要檢查是否系統已經完成了枚舉根節點(或整個GC過程),若是完成了,那麼線程就繼續執行,不然就必須等待到直到收到能夠安全離開安全區域的信號爲止。
好了,到了最最精彩也最最核心的時刻,就是關於如何回收內存。這一塊的知識點也是平時面試官會屢次考到的地方,因此須要重點學習!
最基礎的垃圾收集算法,算法分爲「標記」和「清除」兩個階段:首先標記出全部須要回收的對象,在標記完成以後統一回收掉全部被標記的對象。
優勢:原理簡單
缺點有兩個:1.效率問題,標記和清除效率都不高。2,標記清除以後會產生大量的不連續的內存碎片,空間碎片太多會致使當程序須要爲較大對象分配內存時沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。
將可用內存按容量分紅大小相等的兩塊,每次只使用其中一塊,當這塊內存使用完了,就將還存活的對象複製到另外一塊內存上去,而後把使用過的內存空間一次清理掉。
優勢:可以解決內存碎片化的問題。
缺點:堆空間的使用效率極其低下(畢竟分紅兩半,一次只使用一半)
標記-整理算法在標記-清除算法基礎上作了改進,標記階段是相同的標記出全部須要回收的對象,在標記完成以後不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,在移動過程當中清理掉可回收的對象,這個過程叫作整理。
優勢:內存被整理之後不會產生大量不連續內存碎片問題。
缺點:複製算法在對象存活率高的狀況下就要執行較多的複製操做,效率將會變低,而在對象存活率低的狀況下使用標記-整理算法效率會大大提升。
根據內存中對象的存活週期不一樣,將內存劃分爲幾塊,java的虛擬機中通常把內存劃分爲新生代和年老代,當新建立對象時通常在新生代中分配內存空間,當新生代垃圾收集器回收幾回以後仍然存活的對象會被移動到年老代內存中,當大對象在新生代中沒法找到足夠的連續內存時也直接在年老代中建立。
分代的垃圾回收策略,是基於這樣一個事實:不一樣的對象的生命週期是不同的。所以,不一樣生命週期的對象能夠採起不一樣的回收算法,以便提升回收效率。
年輕代(Young Generation)
1.全部新生成的對象首先都是放在年輕代的。年輕代的目標就是儘量快速的收集掉那些生命週期短的對象。
2.新生代內存按照8:1:1的比例分爲一個eden區和兩個survivor(survivor0,survivor1)區。一個Eden區,兩個 Survivor區(通常而言)。大部分對象在Eden區中生成。回收時先將eden區存活對象複製到一個survivor0區,而後清空eden區,當這個survivor0區也存放滿了時,則將eden區和survivor0區存活對象複製到另外一個survivor1區,而後清空eden和這個survivor0區,此時survivor0區是空的,而後將survivor0區和survivor1區交換,即保持survivor1區爲空, 如此往復。
3.當survivor1區不足以存放 eden和survivor0的存活對象時,就將存活對象直接存放到老年代。如果老年代也滿了就會觸發一次Full GC,也就是新生代、老年代都進行回收
4.新生代發生的GC也叫作Minor GC,MinorGC發生頻率比較高(不必定等Eden區滿了才觸發)
年老代(Old Generation)
1.在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。所以,能夠認爲年老代中存放的都是一些生命週期較長的對象。
2.內存比新生代也大不少(大概比例是1:2),當老年代內存滿時觸發Major GC即Full GC,Full GC發生頻率比較低,老年代對象存活時間比較長,存活率標記高。
持久代(Permanent Generation)
用於存放靜態文件,如Java類、方法等。持久代對垃圾回收沒有顯著影響,可是有些應用可能動態生成或者調用一些class,例如Hibernate 等,在這種時候須要設置一個比較大的持久代空間來存放這些運行過程當中新增的類。
新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge
老年代收集器使用的收集器:Serial Old、Parallel Old、CMS
Serial收集器(複製算法)
新生代單線程收集器,標記和清理都是單線程,優勢是簡單高效。
ParNew收集器(中止-複製算法)
新生代收集器,能夠認爲是Serial收集器的多線程版本,在多核CPU環境下有着比Serial更好的表現。
Parallel Scavenge收集器(中止-複製算法)
並行收集器,追求高吞吐量,高效利用CPU。吞吐量通常爲99%, 吞吐量= 用戶線程時間/(用戶線程時間+GC線程時間)。適合後臺應用等對交互相應要求不高的場景。
Serial Old收集器(標記-整理算法)
老年代單線程收集器,Serial收集器的老年代版本。
Parallel Old收集器(中止-複製算法)
Parallel Scavenge收集器的老年代版本,並行收集器,吞吐量優先
CMS(Concurrent Mark Sweep)收集器(標記-清理算法)
高併發、低停頓,追求最短GC回收停頓時間,cpu佔用比較高,響應時間快,停頓時間短,多核cpu 追求高響應時間的選擇
G1(Garbage-First)收集器 (最前沿也是最複雜的收集器)
Region 區域化垃圾收集器:最大好處是化整爲零,避免全內存掃描,只須要按照區域來進行掃描便可。
因爲對象進行了分代處理,所以垃圾回收區域、時間也不同。GC有兩種類型:Scavenge GC和Full GC。
Scavenge GC
通常狀況下,當新對象生成,而且在Eden申請空間失敗時,就會觸發Scavenge GC,對Eden區域進行GC,清除非存活對象,而且把尚且存活的對象移動到Survivor區。而後整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。由於大部分對象都是從Eden區開始的,同時Eden區不會分配的很大,因此Eden區的GC會頻繁進行。於是,通常在這裏須要使用速度快、效率高的算法,使Eden去能儘快空閒出來。
Full GC
對整個堆進行整理,包括Young、Tenured和Perm。Full GC由於須要對整個堆進行回收,因此比Scavenge GC要慢,所以應該儘量減小Full GC的次數。在對JVM調優的過程當中,很大一部分工做就是對於FullGC的調節。有以下緣由可能致使Full GC:
1.年老代(Tenured)被寫滿
2.持久代(Perm)被寫滿
3.System.gc()被顯示調用
4.上一次GC以後Heap的各域分配策略動態變化
1.靜態集合類像HashMap、Vector等的使用最容易出現內存泄露,這些靜態變量的生命週期和應用程序一致,全部的對象Object也不能被釋放,由於他們也將一直被Vector等應用着。
Static Vector v = new Vector();
for (int i = 1; i<100; i++) {
Object o = new Object();
v.add(o);
o = null;
}複製代碼
在這個例子中,代碼棧中存在Vector 對象的引用 v 和 Object 對象的引用 o 。在 For 循環中,咱們不斷的生成新的對象,而後將其添加到 Vector 對象中,以後將 o 引用置空。問題是當 o 引用被置空後,若是發生 GC,咱們建立的 Object 對象是否可以被 GC 回收呢?答案是否認的。由於, GC 在跟蹤代碼棧中的引用時,會發現 v 引用,而繼續往下跟蹤,就會發現 v 引用指向的內存空間中又存在指向 Object 對象的引用。也就是說盡管o 引用已經被置空,可是 Object 對象仍然存在其餘的引用,是能夠被訪問到的,因此 GC 沒法將其釋放掉。若是在此循環以後, Object 對象對程序已經沒有任何做用,那麼咱們就認爲此 Java 程序發生了內存泄漏。
2.各類鏈接,數據庫鏈接,網絡鏈接,IO鏈接等沒有顯示調用close關閉,不被GC回收致使內存泄露。
3.監聽器的使用,在釋放對象的同時沒有相應刪除監聽器的時候也可能致使內存泄露。
參考資料:
書籍:周志明-《深刻Java虛擬機》 第3章
博客: 1.深刻理解 Java 垃圾回收機制