Java虛擬機淺談——垃圾收集器與內存分配策略


在C語言中,有些由內存須要程序員在代碼中進行手動回收,可是在Java中,沒有這樣的聲明式操做。有沒有人有去想過,Java到底作了什麼能夠自動進行垃圾回收呢?Java中的垃圾回收,是一點都不須要程序員關心,萬無一失的嗎?

本文將從:Jvm中的垃圾收集器和內存分配策略。虛擬機中對已經死亡的對象都有哪些垃圾回收是算法,兩部分 和你們談談Java虛擬機的垃圾收集器與內存分配策略。



重垃圾收集器和內存分配策略java



垃圾收集(Garbage Collection,GC),並非隨着Java一塊兒誕生的。GC的歷史比Java來得更加久遠,早在1960年的時候,MIT的Lisp是第一門真正使用內存動態分配和垃圾收集技術的語言。當Lisp還在胚胎時期時,人們就在思考GC須要完成的三件事情:

  • 哪些內存須要回收?python

  • 何時回收?程序員

  • 如何回收?web


在通過半個世紀的發展後,對於這三個問題的答案愈來愈清晰,總結成就是:當須要排查各類內存溢出、內存泄漏問題時,當垃圾收集成爲系統達到更高併發量的瓶頸時,咱們就須要對這些「自動化」的技術實施必要的監控和調節。

在Java程序編寫的過程當中,咱們能夠知道代碼的邏輯是怎樣的,可是具體的分支只有在運行過程當中才能知道。而這部分的內存分配和回收也是動態進行的,垃圾收集器主要關注的就是這部份內存。

那麼實際中,一個須要解決的問題就是,如何判斷對象是否存活,對於再也不存活的對象,進行垃圾回收。

在通過漫長的發展後,目前主要有下面幾種算法來進行對象存活判斷。

▐  引用計數算法


算法的定義爲:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器都爲0的對象就是不可能再被使用的。

這是實現簡單,且效率很是高效的一種算法。在redis、python的虛擬機、FlashPlayer等應用中,也都有采用這樣的算法。可是Java中並無採用這樣的算法實現,主要緣由是其存在相互循環引用的問題。

簡單來講,A對象引用B對象,B對象引用A對象的狀況下。A和B互相引用,因而他們的計數器都不會爲0,因而GC收集器便就永遠沒法回收他們。


▐  根搜索算法


算法的定義爲:經過一系列名爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連,或者說不可達的時候,則證實此對象不可用。

在Java語言中,能夠做爲GC Roots的對象包括下面幾種:

  • 虛擬機棧(棧幀中的本地變量表)中的引用的對象。redis

  • 方法區中的類靜態屬性引用的對象。算法

  • 方法區中的常量引用的對象。緩存

  • 本地方法棧中JNI(即通常說的Native方法)的引用的對象安全


▐  引用


在早期的JDK定義中,引用的定義爲,若是reference類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用。但這樣的定義方式過於純粹,一個對象只有兩種狀態,即被引用或者沒有被引用兩種。對於一些緩存類型的數據,則顯得有些雞肋,更沒法體現內存分配的價值。

以後JDK對於引用進行了概念擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種,這四種引用強度依次逐漸減弱。

  • 強引用就是指在程序代碼之中廣泛存在的,相似「Object obj = new Object()」這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。微信

  • 軟引用用來描述一些還有用,但並不是必需的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中並進行第二次回收。若是此次回收仍是沒有足夠的內存,纔會拋出內存溢出異常。在JDK 1.2以後,提供了SoftReference類來實現軟引用。多線程

  • 弱引用也是用來描述非必需對象的,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在JDK 1.2以後,提供了WeakReference類來實現弱引用。

  • 虛引用也稱爲幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的惟一目的就是但願能在這個對象被收集器回收時收到一個系統通知。在JDK 1.2以後,提供了PhantomReference類來實現虛引用。


▐  是否死亡


在根搜索算法中,在GCRoots沒有能夠到達的引用鏈以後,就必定會「死亡」嗎?其實也不必定,要真正宣告一個對象死亡,至少要經歷兩次標記過程:若是對象在進行根搜索後發現沒有與GCRoots相鏈接的引用鏈,那它將會被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲「沒有必要執行」。

當這個對象須要執行finalize()方法時,這個對象會被放置在一個名爲F-Queue的隊列中,並稍後由一條虛擬機自動創建的、低優先級的Finalizer線程去執行。這裏的「執行」是虛擬機會觸發這個方法,但並不必定會等待它運行結束。由於若是對象在finalize()方法中死循環或者超長時間執行,可能致使F-Queue隊列中的其餘對象永久處於等待狀態,甚至可能致使內存回收系統奔潰。

finalize()方法是對象能夠存活的最後一次機會,在這裏能夠將本身和引用鏈上的任何一個對象創建關聯便可,不然就會進入到垃圾回收的系統中。但finalize()依舊是一種充滿不肯定性的方法,在誕生之初亦是爲了C/C++程序員的更容易接受的一種妥協,推薦目前的try-finally方法處理更加優雅,也更安全可靠。


接着咱們一塊兒來看看虛擬機中對已經死亡的對象都有哪些垃圾回收是算法。


▐  標記-清除算法


標記-清除算法(Mark-Sweep)能夠說應該是最基礎的收集算法了。從字面意思很好理解,算法的過程分爲標記過程和清楚過程。首先標記出全部須要回收的對象,在標記完了以後,對標記對象進行統一的回收工做。哪些對象須要標記,哪些對象不須要標記,這個再上一篇文章中進行了詳細的介紹,能夠回顧再瞭解下。


這個算法的缺點也很是明顯,內存中的被標記的數據不必定都是連續,所以標記清楚以後,內存中會產生大量的內存碎片,碎片的存在也會致使在後續分配較大對象時候找不到足夠的連續空間,致使內存不足。還有一個問題,即是標記和清楚的效率都不高。


但之因此說這是最基礎的收集算法,是由於後續是算法基本上都是由此改進得來的。



▐  複製算法


爲了解決效率問題,誕生了一種叫複製(Copying)的算法。該算法將能夠用的內存空間劃分爲兩大塊,每次只使用其中的一塊。當這塊內存使用完了以後,就將還存活的對象複製到另外一塊空間中去。這樣就不須要考慮內存碎片的問題,只須要移動堆頂指針,按順序分配內存便可,簡單高效。一樣缺點也很明顯,這樣作了以後很明顯,咱們只能使用內存中的一半內存。代價仍是比較高。


那麼目前的虛擬機新生代中,就採用了這種回收算法。新生代的空間相對較小,內存空間由Eden,和兩塊Survivor空間組成,分配比例爲8:1:1,也就是最多隻有10%的空間是處於空閒的。當進行回收時,將新生代的Eden和其中一塊的Survivor中的還存活的對象一次性拷貝到另外一塊Survivor的空間上,而後清理掉Eden和剛纔用過的Survivor的空間。若是當Survivor的沒法存放時候,就會進入老年代存放。



▐  標記-整理算法


複製算法在對象存活較高的時候,就會執行較多的複製操做,從而下降總體的回收效率,還有存在50%的空間浪費。基於這種狀況,有人對標記-清楚算法進行改進,從而衍生出標記-整理(Mark-Compact)算法。


這種算法的標記過程和」標記-清楚「算法一致,不一樣的是標記完成以後,讓全部存活的對象都移動到內存的一端,而後清理掉邊界外面的內存。



▐  分代收集算法


當前商業虛擬機的垃圾收集都採用「分代收集」(Generational Collection)算法,這種算法並無什麼新的思想,只是根據對象的存活週期的不一樣將內存劃分爲幾塊。通常是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用「標記-清理」或「標記-整理」算法來進行回收。



哪些垃圾回收是算法?



▐  垃圾收集器


收集算法是用以支撐內存回收的理論,在虛擬機中對應的具體實現就是垃圾收集器。不一樣的廠商和開發者,能夠依據本身的應用特色來實現對應的收集器,所以不一樣版本之間的收集器可能存在較大的差異。
如下收集器內容摘錄自參考書籍《深刻理解Java虛擬機》


▐  Serial垃圾收集器


Serial是最基本、歷史最悠久的垃圾收集器,使用複製算法,曾經是JDK1.3.1以前新生代惟一的垃圾收集器。


Serial是一個單線程的收集器,它不只僅只會使用一個CPU或一條線程去完成垃圾收集工做,而且在進行垃圾收集的同時,必須暫停其餘全部的工做線程,直到垃圾收集結束。


Serial垃圾收集器雖然在收集垃圾過程當中須要暫停全部其餘的工做線程,可是它簡單高效,對於限定單個CPU環境來講,沒有線程交互的開銷,能夠得到最高的單線程垃圾收集效率,所以Serial垃圾收集器依然是java虛擬機運行在Client模式下默認的新生代垃圾收集器。


▐  ParNew垃圾收集器


ParNew垃圾收集器實際上是Serial收集器的多線程版本,也使用複製算法,除了使用多線程進行垃圾收集以外,其他的行爲和Serial收集器徹底同樣,ParNew垃圾收集器在垃圾收集過程當中一樣也要暫停全部其餘的工做線程。


ParNew收集器默認開啓和CPU數目相同的線程數,能夠經過-XX:ParallelGCThreads參數來限制垃圾收集器的線程數。


ParNew雖然是除了多線程外和Serial收集器幾乎徹底同樣,可是ParNew垃圾收集器是不少java虛擬機運行在Server模式下新生代的默認垃圾收集器。


▐  Parallel Scavenge收集器


Parallel Scavenge收集器也是一個新生代垃圾收集器,一樣使用複製算法,也是一個多線程的垃圾收集器,它重點關注的是程序達到一個可控制的吞吐量(Thoughput,CPU用於運行用戶代碼的時間/CPU總消耗時間,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)),高吞吐量能夠最高效率地利用CPU時間,儘快地完成程序的運算任務,主要適用於在後臺運算而不須要太多交互的任務。


Parallel Scavenge收集器提供了兩個參數用於精準控制吞吐量:


  • XX:MaxGCPauseMillis:控制最大垃圾收集停頓時間,是一個大於0的毫秒數。

  • XX:GCTimeRation:直接設置吞吐量大小,是一個大於0小於100的整數,也就是程序運行時間佔總時間的比率,默認值是99,即垃圾收集運行最大1%(1/(1+99))的垃圾收集時間。

Parallel Scavenge是吞吐量優先的垃圾收集器,它還提供一個參數:-XX:+UseAdaptiveSizePolicy,這是個開關參數,打開以後就不須要手動指定新生代大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRation)、新生代晉升年老代對象年齡(-XX:PretenureSizeThreshold)等細節參數,虛擬機會根據當前系統運行狀況收集性能監控信息,動態調整這些參數以達到最大吞吐量,這種方式稱爲GC自適應調節策略,自適應調節策略也是ParallelScavenge收集器與ParNew收集器的一個重要區別。


▐  Serial Old收集器


Serial Old是Serial垃圾收集器年老代版本,它一樣是個單線程的收集器,使用標記-整理算法,這個收集器也主要是運行在Client默認的java虛擬機默認的年老代垃圾收集器。


在Server模式下,主要有兩個用途:


  • 在JDK1.5以前版本中與新生代的Parallel Scavenge收集器搭配使用。

  • 做爲年老代中使用CMS收集器的後備垃圾收集方案。


▐  Parallel Old收集器


Parallel Old收集器是Parallel Scavenge的年老代版本,使用多線程的標記-整理算法,在JDK1.6纔開始提供。


在JDK1.6以前,新生代使用ParallelScavenge收集器只能搭配年老代的Serial Old收集器,只能保證新生代的吞吐量優先,沒法保證總體的吞吐量,Parallel Old正是爲了在年老代一樣提供吞吐量優先的垃圾收集器,若是系統對吞吐量要求比較高,能夠優先考慮新生代Parallel Scavenge和年老代Parallel Old收集器的搭配策略。


▐  CMS收集器


Concurrent mark sweep(CMS)收集器是一種年老代垃圾收集器,其最主要目標是獲取最短垃圾回收停頓時間,和其餘年老代使用標記-整理算法不一樣,它使用多線程的標記-清除算法。


最短的垃圾收集停頓時間能夠爲交互比較高的程序提升用戶體驗,CMS收集器是Sun HotSpot虛擬機中第一款真正意義上併發垃圾收集器,它第一次實現了讓垃圾收集線程和用戶線程同時工做。


CMS工做機制相比其餘的垃圾收集器來講更復雜,整個過程分爲如下4個階段:


  • 初始標記:只是標記一下GC Roots能直接關聯的對象,速度很快,仍然須要暫停全部的工做線程。

  • 併發標記:進行GC Roots跟蹤的過程,和用戶線程一塊兒工做,不須要暫停工做線程。

  • 從新標記:爲了修正在併發標記期間,因用戶程序繼續運行而致使標記產生變更的那一部分對象的標記記錄,仍然須要暫停全部的工做線程。

  • 併發清除:清除GC Roots不可達對象,和用戶線程一塊兒工做,不須要暫停工做線程。


因爲耗時最長的併發標記和併發清除過程當中,垃圾收集線程能夠和用戶如今一塊兒併發工做,因此整體上來看CMS收集器的內存回收和用戶線程是一塊兒併發地執行。


CMS收集器有如下三個不足:


  • CMS收集器對CPU資源很是敏感,其默認啓動的收集線程數=(CPU數量+3)/4,在用戶程序原本CPU負荷已經比較高的狀況下,若是還要分出CPU資源用來運行垃圾收集器線程,會使得CPU負載加劇。

  • CMS沒法處理浮動垃圾(Floating Garbage),可能會致使Concurrent ModeFailure失敗而致使另外一次Full GC。因爲CMS收集器和用戶線程併發運行,所以在收集過程當中不斷有新的垃圾產生,這些垃圾出如今標記過程以後,CMS沒法在本次收集中處理掉它們,只好等待下一次GC時再將其清理掉,這些垃圾就稱爲浮動垃圾。
    CMS垃圾收集器不能像其餘垃圾收集器那樣等待年老代機會徹底被填滿以後再進行收集,須要預留一部分空間供併發收集時的使用,能夠經過參數-XX:CMSInitiatingOccupancyFraction來設置年老代空間達到多少的百分比時觸發CMS進行垃圾收集,默認是68%。
    若是在CMS運行期間,預留的內存沒法知足程序須要,就會出現一次ConcurrentMode Failure失敗,此時虛擬機將啓動預備方案,使用Serial Old收集器從新進行年老代垃圾回收。

  •  CMS收集器是基於標記-清除算法,所以不可避免會產生大量不連續的內存碎片,若是沒法找到一塊足夠大的連續內存存放對象時,將會觸發所以Full GC。CMS提供一個開關參數-XX:+UseCMSCompactAtFullCollection,用於指定在Full GC以後進行內存整理,內存整理會使得垃圾收集停頓時間變長,CMS提供了另一個參數-XX:CMSFullGCsBeforeCompaction,用於設置在執行多少次不壓縮的Full GC以後,跟着再來一次內存整理。


▐  G1收集器


Garbage first垃圾收集器是目前垃圾收集器理論發展的最前沿成果,相比與CMS收集器,G1收集器兩個最突出的改進是:


  • 基於標記-整理算法,不產生內存碎片。

  • 能夠很是精確控制停頓時間,在不犧牲吞吐量前提下,實現低停頓垃圾回收。


G1收集器避免全區域垃圾收集,它把堆內存劃分爲大小固定的幾個獨立區域,而且跟蹤這些區域的垃圾收集進度,同時在後臺維護一個優先級列表,每次根據所容許的收集時間,優先回收垃圾最多的區域。


區域劃分和優先級區域回收機制,確保G1收集器能夠在有限時間得到最高的垃圾收集效率。



總結



其實相對於C和C++語言,Java程序員依賴JVM的強大內存管理能力,已經再也不須要對內存進行分配或者釋放等操做。因此Java程序員每每不多關注內存中潛在的泄露和溢出等問題。但當這個問題出現時候,若是對虛擬機內存管理機制沒有足夠多的掌握,會難以定位和解決問題。去了解虛擬機的發展歷程以及現有的管理機制,能夠更好地理解爲何這樣設計,一樣能提升本身的問題解決能力。


淘系技術部-天貓奢侈品團隊

咱們是一支支撐 天貓奢侈品、品牌客戶、淘寶心選等大店數據化經營解決方案的技術團隊,依託於阿里大中臺推進品牌經營解決方案升級,不斷提高客戶經營的效率,持續提高業務價值賦能業務。

若是您有興趣可講簡歷發至: gangmin.zgm@alibaba-inc.com ,期待您的加入!


✿  拓展閱讀


做者| 鄭鋼民(千夏)
編輯| 橙子君
出品| 阿里巴巴新零售淘系技術


本文分享自微信公衆號 - 淘系技術(AlibabaMTT)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索