原文來自公衆號:猿人谷java
在Java中內存是由虛擬機自動管理的,虛擬機在內存中劃出一片區域,做爲知足程序內存分配請求的空間。內存的建立仍然是由程序猿來顯示指定的,可是對象的釋放卻對程序猿是透明的。就是解放了程序猿手動回收內存的工做,交給垃圾回收器來自動回收。面試
在虛擬機中,釋放哪些再也不被使用的對象所佔空間的過程稱爲垃圾收集(Garbage Collection,GC)。負責垃圾收集的程序模塊,成爲垃圾收集器(Garbage Collector)。算法
當須要排查各類內存溢出、內存泄漏問題時,當垃圾收集成爲系統達到更高併發量的瓶頸時,咱們就須要對虛擬機的自動管理技術實施必要的監控和調節了。這也是JVM調優,故障排查,重點須要掌握的知識了。安全
本篇咱們的重點是介紹何謂垃圾及垃圾回收算法,那咱們就要弄清到底什麼是垃圾?能不能設計一種強大的垃圾回收算法來解決垃圾回收的全部問題?確定是沒有的,後面介紹的每一種垃圾回收算法都有它得天獨厚的優勢,也有它避之不及的缺點。針對具體的場景,靈活運用方是上策。markdown
但願你們能帶着以下問題進行學習,會收穫更大。多線程
在堆裏面存放着Java世界中幾乎全部的對象實例,垃圾收集器在堆進行回收前,第一件事就是要肯定這些對象之中哪些還「存活」着,哪些已經「死亡」(即不可能再被任何途徑使用的對象)。垃圾回收,其實就是將已經分配出去的,但再也不使用的內存回收,以便可以再次分配。在Java虛擬機的規範中,垃圾指的就是死亡的對象所佔據的堆空間。併發
給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的。也就是說,須要截獲全部的引用更新操做,而且相應地增減目標對象的計數器。app
題外話:記得研一那段時間對iOS開發感興趣,找個公司去實習,現學現搞iOS開發,當時是作了一個模擬炒股的app。用的就是Objective-C,這門語言起初管理內存的方式就是用的這種引用計數算法,不事後面也有了自動管理內存。接觸的對象多了,發現不少東西在本質的原理有很是多的類似之處。高併發
其中沒法處理循環引用對象,算是引用計數法的一個重大漏洞。學習
可達性是指,若是一個對象會被至少一個在程序中的變量經過直接或間接的方式被其餘可達的對象引用,則稱該對象是可達的(reachable)。更準確的說,一個對象只有知足下述兩個條件之一,就會被判斷爲可達的:
這個算法的基本思路就是經過一系列的成爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(即從GC Roots到這個對象不可達),則證實此對象是不可用的。
GC Roots又是什麼呢?能夠暫時理解爲由堆外指向堆內的引用。
在Java語言中,能夠做爲GC Roots的對象包括下面幾種:
可達性分析算法能夠解決引用計數算法不能解決的循環引用問題。舉個例子,即使對象a和b相互引用,只要從GC Roots出發沒法到達a或者b,那麼可達性分析便不會將它們加入存活對象合集之中。
關於Java中的引用的定義及分類(強引用、軟引用、弱引用、虛引用)會在單獨出一篇進行詳細介紹,Java引用的內容雖然有點冷門,可是不少公司面試的常考點。
可達性分析算法自己雖然很簡明,可是在實踐中仍是有很多其餘問題須要解決的。好比,在多線程環境下,其餘線程可能會更新已經訪問過的對象中的引用,從而形成誤報(將引用設置爲null)或者漏報(將引用設置爲未被訪問過的對象)。誤殺還能夠接受,Java虛擬機至多損失了部分垃圾回收的機會。漏報就問題大了,由於垃圾回收器可能回收事實上仍被引用的對象內存。一旦從原引用訪問已經被回收了的對象,則頗有可能會直接致使Java虛擬機奔潰。
上面咱們介紹什麼是Java中的垃圾,接下來咱們就開始介紹如何高效的回收這些垃圾。
標記-清除(Mark-Sweep)算法能夠分爲兩個階段:
該算法存在以下不足:
標記-清除算法的示意圖以下:
複製算法的過程以下:
該算法解決了內存碎片化問題,但堆空間的使用效率極其低下。在對象存活率較高時,須要進行較多的複製操做,效率會變得很低。
該算法分爲兩個階段:
該算法的標記過程與標記-清除算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。
解決了內存碎片的問題,也規避了複製算法只能利用一半內存區域的弊端。看起來很美好,但它對內存變更更頻繁,須要整理全部存活對象的引用地址,在效率上比複製算法要差不少。
標記-整理算法的示意圖以下:
分代收集算法倒並無什麼新的思想,只是根據對象存活週期的不一樣將內存劃分爲幾塊。通常是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。
在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記-清理算法或標記-整理算法來進行回收。
以可達性分析中從GC Roots節點找引用鏈這個操做爲例,可做爲GC Roots的節點主要在全局性的引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中。上面介紹可達性分析算法時有詳細介紹GC Roots,能夠參看上面。
安全點,即程序執行時並不是在全部地方都能停頓下來開始GC,只有在到達安全點時才能暫停。Safepoint的選定既不能太少以致於讓GC等待時間太長,也不能過於頻繁以至於過度增大運行時的負荷。
安全點的初始目的並非讓其餘線程停下,而是找到一個穩定的執行狀態。在這個執行狀態下,Java虛擬機的堆棧不會發生變化。這麼一來,垃圾回收器便可以「安全」地執行可達性分析。只要不離開這個安全點,Java虛擬機便可以在垃圾回收的同時,繼續運行這段本地代碼。
程序運行時並不是在全部地方都能停頓下來開始GC,只有在到達安全點時才能暫停。安全點的選定基本上是以程序「是否具備讓程序長時間執行的特徵」爲標準進行選定的。「長時間執行」的最明顯特徵就是指令序列複用,例如方法調用、循環跳轉、異常跳轉等,因此具備這些功能的指令纔會產生Safepoint。
對於安全點,另外一個須要考慮的問題就是如何在GC發生時讓全部線程(這裏不包括執行JNI調用的線程)都「跑」到最近的安全點上再停頓下來。
兩種解決方案:
搶先式中斷不須要線程的執行代碼主動去配合,在GC發生時,首先把全部線程所有中斷,若是發現有線程中斷的地方不在安全點上,就恢復線程,讓它「跑」到安全點上。如今幾乎沒有虛擬機採用這種方式來暫停線程從而響應GC事件。
主動式中斷的思想是當GC須要中斷線程的時候,不直接對線程操做,僅僅簡單地設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就本身中斷掛起。輪詢標誌地地方和安全點是重合的,另外再加上建立對象須要分配內存的地方。
指在一段代碼片斷中,引用關係不會發生變化。在這個區域中任意地方開始GC都是安全的。也能夠把Safe Region看做是被擴展了的Safepoint。
Minor GC:
Major GC:
Full GC:
GC進行時必須停頓全部Java執行線程,這就是Stop-the-world。
可達性分析時必須在一個能確保一致性的快照中進行,這裏「一致性」的意思是指在整個分析期間整個執行系統看起來就像被凍結在某個時間點上,不能夠出現分析過程當中對象引用關係還在不斷變化的狀況,這一點不知足的話分析結果準確性就沒法獲得保證。
Stop-the-world是經過安全點機制來實現的。當Java虛擬機接收到Stop-the-world請求,它便會等待全部的線程都到達安全點,才容許請求Stop-the-world的線程進行獨佔的工做。
有個場景,老年代的對象可能引用新生代的對象,那標記存活對象的時候,須要掃描老年代中的全部對象。由於該對象擁有對新生代對象的引用,那麼這個引用也會被稱爲GC Roots。那不是得又作全堆掃描?成本過高了吧。
HotSpot給出的解決方案是一項叫作卡表(Card Table)的技術。該技術將整個堆劃分爲一個個大小爲512字節的卡,而且維護一個卡表,用來存儲每張卡的一個標識位。這個標識位表明對應的卡是否可能存有指向新生代對象的引用。若是可能存在,那麼咱們就認爲這張卡是髒的。
在進行Minor GC的時候,咱們即可以不用掃描整個老年代,而是在卡表中尋找髒卡,並將髒卡中的對象加入到Minor GC的GC Roots裏。當完成全部髒卡的掃描以後,Java虛擬機便會將全部髒卡的標識位清零。
想要保證每一個可能有指向新生代對象引用的卡都被標記爲髒卡,那麼Java虛擬機須要截獲每一個引用型實例變量的寫操做,並做出對應的寫標識位操做。
卡表能用於減小老年代的全堆空間掃描,這能很大的提高GC效率。