點擊上方藍色字體,選擇「標星公衆號」
java
優質文章,第一時間送達算法
做者 | aduner緩存
來源 | urlify.cn/jI3AJj安全
前言每每被問到Java與C/C++有什麼區別的時候,最早想到的答案就是Java可與自動回收內存垃圾。微信
在JVM學習中,垃圾回收幾乎是最重要的知識點。多線程
那麼,自動垃圾回收機制究竟是如何實現的呢,下面咱們來梳理一遍。閉包
什麼是垃圾回收垃圾回收(Garbage Collection)誕生於1960年 MIT 的 Lisp 語言,距今已經超過半個世紀了。併發
垃圾回收顧名思義,就是收集垃圾,JVM中的垃圾就是指的內存中再也不使用的對象。app
將這些再也不使用的對象清除,給後來的新對象騰地方。
後文咱們簡稱GC。
垃圾回收的區域Java 的自動內存管理主要是針對對象內存的回收和對象內存的分配。
Java 堆是垃圾收集器管理的主要區域,而 Java 自動內存管理最核心的功能是 堆 內存中對象的分配與回收,所以也被稱做GC 堆(Garbage Collected Heap)。
從垃圾回收的角度,因爲如今收集器基本都採用分代垃圾收集算法,因此 Java 堆還能夠細分:
堆分爲新生代(佔堆1/3),老生代(佔堆2/3)
新生代(內部比例8:1:1)
Eden 空間
From Survivor 空間
To Survivor 空間
老年代
進一步劃分的目的是更好地回收內存,或者更快地分配內存。
大多數狀況,對象都會首先在 Eden 區域分配,當 eden 區沒有足夠空間進行分配時,虛擬機將發起一次新生代垃圾回收(Minor GC)。
大對象會直接進入老年代,爲了不爲大對象分配內存時因爲分配擔保機制帶來的複製而下降效率。
在一次Minor GC後,若是對象還存活,則會進入兩個Survivor中的一個,而後對象的年齡加 1。
它的年齡增長到年齡閾值(默認爲 15 ),就會被晉升到老年代中。
當老年代空間不足時,將會觸發老年代回收(Major GC)
針對 HotSpot 實現,它裏面的 GC 其實準確分類只有兩大種:
部分收集 (Partial GC):
新生代收集(Minor GC / Young GC):只對新生代進行垃圾收集;
老年代收集(Major GC / Old GC):只對老年代進行垃圾收集。
混合收集(Mixed GC):對整個新生代和部分老年代進行垃圾收集。
整堆收集 (Full GC):收集整個 Java 堆和方法區。
對象晉升到老年代的年齡閾值,能夠經過參數
-XX:MaxTenuringThreshold
設置
垃圾回收前的第一步就是要判斷哪些對象已經死亡,主要用到以下幾種算法來判斷。
原理很簡單,以下:
給對象中添加一個引用計數器,每當有一個地方引用它,計數器就加 1;
當引用失效,計數器就減 1;任什麼時候候計數器爲 0 的對象就是不可能再被使用的。
這個算法實現簡單,效率高,可是目前主流的虛擬機中並無選擇這個算法來管理內存,其最主要的緣由是它很難解決對象之間相互循環引用的問題。
循環引用就是兩個對象互相引用,可是又沒有其餘任何對象使用這兩個對象,兩個對象就像是互相抱着的兩個孤兒,很是可憐。
這個原理也很簡單,以下:
定義一系列的稱爲 「GC Roots」 的對象做爲根起點
從這些節點開始向下搜索,節點所走過的路徑稱爲引用鏈
當一個對象到 GC Roots 沒有任何引用鏈相連的話,則證實此對象是不可用的
可做爲 GC Roots 的對象包括下面幾種:
虛擬機棧(棧幀中的本地變量表)中引用的對象
本地方法棧(Native 方法)中引用的對象
方法區中類靜態屬性引用的對象
方法區中常量引用的對象
全部被同步鎖持有的對象
發現不可達時,這些對象暫時處於「緩刑階段」,要真正宣告一個對象死亡,至少要經歷兩次標記過程:
第一次標記,篩選的條件是此對象是否有必要執行 finalize 方法。
被斷定爲須要執行的對象將會被放在一個隊列中進行第二次標記,除非這個對象與引用鏈上的任何一個對象創建關聯,不然就會被真的回收。
JDK1.2 以前,Java 中引用的定義很傳統:若是 reference 類型的數據存儲的數值表明的是另外一塊內存的起始地址,就稱這塊內存表明一個引用。
JDK1.2 之後,Java 對引用的概念進行了擴充,將引用分爲強引用、軟引用、弱引用、虛引用四種(引用強度逐漸減弱)
下面咱們來看看這四種引用
強引用很是霸道,只要是強引用,必定不會被GC回收,即使是內存不夠,即使要OOM也不會回收它。
若是內存空間足夠,垃圾回收器就不會回收它,若是內存空間不足了,就會回收這些對象的內存。
只要垃圾回收器沒有回收它,該對象就能夠被程序使用。軟引用可用來實現內存敏感的高速緩存。
只要發現了只具備弱引用的對象,就會直接回收。
不過,因爲垃圾回收器是一個優先級很低的線程, 所以不必定會很快發現那些只具備弱引用的對象。
弱引用與軟引用的區別在於:只具備弱引用的對象擁有更短暫的生命週期。
"虛引用"顧名思義,就是形同虛設,與其餘幾種引用都不一樣,虛引用並不會決定對象的生命週期。
若是一個對象僅持有虛引用,那麼它就和沒有任何引用同樣,在任什麼時候候均可能被垃圾回收。
虛引用主要用來跟蹤對象被垃圾回收的活動。
虛引用與軟引用和弱引用的一個區別在於: 虛引用必須和引用隊列(ReferenceQueue)聯合使用。
當垃圾回收器準備回收一個對象時,若是發現它還有虛引用,就會在回收對象的內存以前,把這個虛引用加入到與之關聯的引用隊列中。程序能夠經過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。
程序若是發現某個虛引用已經被加入到引用隊列,那麼就能夠在所引用的對象的內存被回收以前採起必要的行動。
特別注意,在程序設計中通常不多使用弱引用與虛引用,使用軟引用的狀況較多,這是由於軟引用能夠加速 JVM 對垃圾內存的回收速度,能夠維護系統的運行安全,防止內存溢出(OOM)等問題的產生。
假如在字符串常量池中存在字符串 "abc",若是當前沒有任何 String 對象引用該字符串常量的話,就說明常量 "abc" 就是廢棄常量,若是這時發生內存回收的話並且有必要的話,"abc" 就會被系統清理出常量池了。
類須要同時知足下面 3 個條件才能算是 「無用的類」 :
該類全部的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
加載該類的 ClassLoader 已經被回收。
該類對應的 java.lang.Class
對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。
虛擬機能夠對知足上述 3 個條件的無用類進行回收,這裏說的僅僅是「能夠」,而並非和對象同樣不使用了就會必然被回收。
垃圾收集算法該算法分爲「標記」和「清除」階段:
首先標記出全部不須要回收的對象
標記完成後統一回收掉全部沒有被標記的對象。
它是最基礎的收集算法,後續的算法都是對其不足進行改進獲得。
這種垃圾收集算法會帶來兩個明顯的問題:
效率問題,須要遍歷兩次進行清除
空間問題,標記清除後會產生大量不連續的碎片
標記-複製算法是標記-清除算法的改進版本。
能夠將內存分爲大小相同的兩塊,每次使用其中的一塊。
當這一塊的內存使用完後,就將還存活的對象複製到另外一塊去,而後再把使用的空間一次清理掉。
這樣就使每次的內存回收都是對內存區間的一半進行回收。
根據老年代的特色提出的一種標記算法,標記過程仍然與「標記-清除」算法同樣,但後續步驟不是直接對可回收對象回收,而是讓全部存活的對象向一端移動,而後直接清理掉端邊界之外的內存。
分代收集就是將新生代和老年代分開,根據各自的特色選擇合適的收集算法。
好比在新生代中,收集很頻繁,而且數量不少,因此能夠選擇標記-複製算法,只須要付出少許對象的複製成本就能夠完成每次垃圾收集。
而老年代的對象存活概率是比較高的,並且沒有額外的空間對它進行分配擔保,標記-整理算法就很合適
垃圾收集器垃圾收集算法是垃圾收集的實現原理,而垃圾收集器就是內存回收的具體實現。
實際生產中,咱們須要根據本身的需求來選擇合適的垃圾收集器,須要記住一點,沒有最好的,只有最合適的。
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。
CMS收集器很是符合在注重用戶體驗的應用上使用
CMS收集器是 HotSpot 第一款真正意義上的併發收集器,實現了讓垃圾收集線程與用戶線程(基本上)同時工做。
CMS 收集器使用 「標記-清除」算法。
整個過程分爲四個步驟:
初始標記: 暫停全部的其餘線程,並記錄下直接與 root 相連的對象,速度很快 ;
併發標記:
同時開啓 GC 和用戶線程,用一個閉包結構去記錄可達對象。
在階段結束,閉包結構不能保證包含當前全部的可達對象。
由於用戶線程可能會不斷的更新引用域,因此 GC 線程沒法保證可達性分析的實時性。
因此此算法裏會跟蹤記錄這些發生引用更新的地方。
從新標記:
修正併發標記期間由於用戶程序繼續運行而致使標記產生變更的那一部分對象的標記記錄,
停頓時間通常會比初始標記階段的時間稍長,遠遠比並發標記階段時間短。
併發清除: 開啓用戶線程,同時 GC 線程開始對未標記的區域作清掃。
優勢:併發收集、低停頓。
缺點:
對 CPU 資源敏感。
沒法處理浮動垃圾。
收集結束時會有大量空間碎片產生。
Serial(串行)收集器是最基本、歷史最悠久的垃圾收集器了。
它收集器是一個單線程收集器了:
新生代採用標記-複製算法
老年代採用標記-整理算法
它最大的特色就是進行GC時,會阻塞其餘線程。
它的優勢是簡單高效,在單線程收集器中幾乎就是最快的存在,可是因爲會阻塞其餘線程,這讓他的使用起來體驗並不算好。
ParNew 收集器是 Serial 收集器的多線程版本,除了使用多線程進行垃圾收集外,其他行爲和 Serial 收集器徹底同樣。
它是許多運行在 Server 模式下的虛擬機的首要選擇,除了 Serial 收集器外,只有它能與 CMS 收集器配合工做。
JDK1.8 默認使用的是 Parallel Scavenge + Parallel Old
JDK1.8 默認收集器
Parallel Scavenge 收集器幾乎和 ParNew 是同樣。
區別在於:
Parallel Scavenge 收集器關注點是吞吐量(高效率的利用 CPU)
CMS 等垃圾收集器的關注點更多的是用戶線程的停頓時間(提升用戶體驗)。
Serial 收集器的老年代版本,它一樣是一個單線程收集器。它主要有兩大用途:
一種用途是在 JDK1.5 以及之前的版本中與 Parallel Scavenge 收集器搭配使用,另外一種用途是做爲 CMS 收集器的後備方案。
Parallel Scavenge 收集器的老年代版本。
使用多線程和「標記-整理」算法。在注重吞吐量以及 CPU 資源的場合,均可以優先考慮 Parallel Scavenge 收集器和 Parallel Old 收集器。
G1 (Garbage-First) 是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器。
以極高機率知足 GC 停頓時間要求的同時,還具有高吞吐量性能特徵。
很是強的一款垃圾收集器,甚至它可能會引領JVM垃圾收集的將來。
它具有一下特色:
並行與併發:
G1 能充分利用 CPU、多核環境下的硬件優點,使用多個 CPU(CPU 或者 CPU 核心)來縮短停頓時間。
部分其餘收集器本來須要停頓 Java 線程執行的 GC 動做,G1 收集器仍然能夠經過併發的方式讓 Java 程序繼續執行。
分代收集:雖然 G1 能夠不須要其餘收集器配合就能獨立管理整個 GC 堆,可是仍是保留了分代的概念。
空間整合:
G1 從總體來看是基於標記-整理算法實現的收集器;
從局部上來看是基於標記-複製算法實現的。
可預測的停頓:下降停頓時間是 G1 和 CMS 共同的關注點,但 G1 除了追求低停頓外,還能創建可預測的停頓時間模型,能讓使用者明確指定在一個長度爲 M 毫秒的時間片斷內。
G1 收集器的運做大體分爲如下幾個步驟:
初始標記
併發標記
最終標記
篩選回收
G1 收集器在後臺維護了一個優先列表,每次根據容許的收集時間,優先選擇回收價值最大的 Region(這也就是它的名字 Garbage-First 的由來) 。
這種使用 Region 劃份內存空間以及有優先級的區域回收方式,保證了 G1 收集器在有限時間內能夠儘量高的收集效率(把內存化整爲零)。
與G1 相似,但又互有不一樣,這裏不展開了,感興趣能夠自行了解。
粉絲福利:Java從入門到入土學習路線圖
????????????
????長按上方微信二維碼 2 秒
感謝點贊支持下哈