垃圾收集與內存分配策略

概述

GC須要完成的三件事情:java

  • 哪些內存須要回收?
  • 何時回收?
  • 如何回收?

哪些區域的內存須要回收?

  Java內存區域分爲程序計數器、Java虛擬機棧、本地方法棧、Java堆和方法區共計五部分。前三部分都是線程私有的,也就是說隨着線程的生滅而生滅。在線程結束的時候,內存天然就跟着回收了,不須要過多考慮回收的問題。
  但後兩部分,Java堆和方法區則不同,其分配和回收都是動態的。垃圾回收所要關注的也正是這部分區域。算法

什時候回收?

  垃圾回收工做是由垃圾回收器來具體執行的,不一樣的垃圾回收器對於何時進行回收可能有着不一樣的設置,通常是收集器所管理的內存區域快滿的時候回進行回收。固然垃圾回收還分爲Minor GC(新生代回收)和Major GC/Full GC(老年代回收),按照最簡單的分代式GC策略,按HotSpot VM的serial GC的實現來看,觸發條件是:數組

  • Young GC:當新生代中的eden區分配滿的時候觸發。注意young GC中有部分存活對象會晉升到old gen,因此young GC後old gen的佔用量一般會有所升高。
  • Full GC:當老年代快被填滿的時候或者分配大對象到老年代時沒有足夠大的(連續)空間的時候,會進行Full GC;此外當進行Minor GC(Young GC)以前,會檢查老年代最大可用連續空間大小是否大於新生代全部對象的總空間大小,如是,Minor GC正常進行。如不大於時,那麼若是虛擬機設置不容許擔保失敗、或者容許擔保失敗但老年代最大可用連續空間不大於歷次晉升平均值、或者容許擔保失敗而且老年代最大可用連續空間大於歷次晉升平均值但進行了Minor GC後發生了擔保失敗(如Minor GC後的存活的對象突增,遠遠高於歷次平均值),這三種狀況任何一種都會再發起一次Full GC;或者System.gc()時,默認也是觸發full GC()。

更多內容能夠參考下列連接:
Major GC和Full GC的區別是什麼?觸發條件呢?前兩高讚的回答
Console界面裏面的GC的問題
When does System.gc() do anything安全

如何回收

  如何回收的具體內容也能夠分爲兩部分,首先是如何判斷內存是否須要回收,也就是說怎樣判斷一塊內存所存放的內容以後不再會被訪問了,這裏根據內存區域的不一樣,又可分爲判斷方法區的內存是否須要回收判斷Java堆的內存是否須要回收。其次是如何去回收內存,這也就是用到了垃圾收集算法,由於垃圾回收主要針對的是堆中的對象,因此這裏的垃圾收集算法也是主要用於Java堆中的內存回收。關於方法區的內存回收是否有相應的算法,書上和網上大多未涉及,暫不清楚。併發

判斷方法區的內存是否須要回收

  對於方法區而言,要回收的內容就是:廢棄常量和無用的類。對於常量(特別是引用常量)而言,以常量池中字符串字面量的回收爲例,假如一個字符串「abc」已經進入了常量池中,可是當前系統沒有任何一個String對象是叫作「abc」的,換句話說,就是沒有任何String對象引用常量池中的「abc」常量,也沒有其餘地方引用了這個字面量,若是這時發生內存回收,並且必要的話,這個「abc」常量就會被系統清理出常量池。常量池中的其餘類(接口)、方法、字段的符號引用也與此相似。
  而要斷定一個類是不是「無用的類」的條件則相對苛刻許多。類須要同時知足下面3個條件才能算是「無用的類」:框架

  1. 該類全部的實例都已經被回收,也就是Java堆中不存在該類的任何實例。
  2. 加載該類的ClassLoader已經被回收。
  3. 該類對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。

  虛擬機能夠對知足上述3個條件的無用類進行回收,這裏說的僅僅是「能夠」,而並非和對象同樣,不使用了就必然會回收。是否對類進行回收,HotSpot虛擬機提供了-Xnoclassgc 參數進行控制,還可使用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看類加載和卸載信息,其中-verbose:class和-XX:+TraceClassLoading能夠在Product版的虛擬機中使用,-XX:+TraceClassUnLoading參數須要FastDebug版的虛擬機支持。在大量使用反射、動態代理、CGLib等ByteCode框架、動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都須要虛擬機具有類卸載的功能,以保證永久代不會溢出.佈局

判斷Java堆的內存是否須要回收

 爲了肯定對象之中哪些仍是存活着哪些已經死去,出現了兩種方法:引用計數算法和可達性分析算法,Java虛擬機中採用的都是可達性分析算法,由於引用計數算法很難解決對象之間循環引用的問題。下面的全部垃圾收集算法的標記階段都是經過可達性分析算法的思路來完成的。post

引用計數算法

可達性分析算法

 基本思路就是經過一系列被稱爲GC Roots的對象引用變量做爲起始點,從這些結點開始往下搜索,搜索所走過的路被稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連的時候(用圖論的話來講就是從GC Roots到這個對象不可達)時,則證實此對象是不可用的。
 在Java語言中,可做爲GC Roots的對象包括下面幾種優化

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  • 方法區類靜態屬性引用的對象。
  • 方法區中常量引用的對象。
  • 本地方法棧中JNI(即通常說的Native方法)引用的對象。

HostPot的算法實現

  在枚舉根節點(即找到全部根節點的過程當中),全部的垃圾收集器(不管是否是併發),都須要停頓全部的Java執行線程(Stop The World)。
因此,虛擬機必須儘可能的優化GC過程的效率,減小暫停的時間。HotSpot採用了準確式GC以提高GC roots的枚舉速度。所謂準確式GC,就是讓JVM知道內存中某位置數據的類型什麼。好比當前內存位置中的數據到底是一個整型變量仍是一個引用類型。這樣JVM能夠很快肯定全部引用類型的位置,從而更有針對性的進行GC roots枚舉。
  HotSpot是利用OopMap來實現準確式GC的。當類加載完成後,HotSpot 就將對象內存佈局之中什麼偏移量上數值是一個什麼樣的類型的數據這些信息存放到 OopMap 中;在 HotSpot 的 JIT 編譯過程當中,一樣會插入相關指令來標明哪些位置存放的是對象引用等,這樣在 GC 發生時,HotSpot 就能夠直接掃描 OopMap 來獲取對象引用的存儲位置,從而進行 GC Roots 枚舉。spa

HotSpot安全點

  經過OopMap,HotSpot能夠很快完成GC Roots的查找,可是,若是在每一行代碼都有可能發生GC,那麼也就意味着得爲每一行代碼的指令都生成OopMap,這樣將佔用大量的空間。實際上,HotSpot也不會這麼作。
  HotSpot只在特定的位置記錄了OopMap,這些位置就叫作安全點(Safepoint),也就是說,程序並不能在任意地方均可以停下來進行GC,只有到達安全點時才能暫停進行GC。
  在安全點中,HotSpot也會開始記錄虛擬機的相關信息,如OopMap信息的錄入。安全點的選擇不能太少,不然GC等待時間太長;也不能太多,不然會增大運行負荷,其選擇的原則爲「是否具備讓程序長時間執行的特徵」,如方法調用,循環等等。具體安全點有下面幾個:
(1) 循環的末尾 (防止大循環的時候一直不進入Safepoint,而其餘線程在等待它進入Safepoint)
(2) 方法返回前
(3) 調用方法的call以後
(4) 拋出異常的位置
而安全點暫停線程運行的手段有兩種:搶先式中斷和主動式中斷。

搶先式中斷

 不須要線程的執行代碼主動配合,在GC發生時,首先把全部線程所有中斷,若是發現有線程中斷的地方不在安全點上,就恢復線程,讓它跑到安全點上再暫停。不過如今的虛擬機幾乎沒有採用此算法的

主動式中斷

  GC須要中斷線程的時候,不直接對線程操做,僅僅簡單地設置一個標誌,各個線程執行時去主動輪詢查詢此標誌,發現中斷標誌爲真時就中斷本身掛起。輪詢標誌的地方和安全點是重合的,另外再加上建立對象須要分配內存的地方。

HotSpot安全區域

產生緣由

  安全點機制保證了程序執行時進入GC的問題。可是對於非執行態下,如線程Sleep或者Block下,因爲此時程序(線程)沒法響應JVM的中斷請求,JVM也不太可能一直等待線程從新獲取時間片,此時就須要安全區域(Safe Region)了。安全區域是指在一段代碼片斷內,引用關係不會發生變化,在這段區域內,任意地方開始GC都是安全的。

運行機理

  在線程執行到Safe Region中的代碼時,首先標識本身已經進入了Safe Region。當在這段時間裏JVM要發起GC時,就不用管標識本身爲Safe Region狀態的線程了;當線程要離開Safe Region時,若是整個GC完成,那線程可繼續執行,不然它必須等待直到收到能夠安全離開Safe Region的信號爲止

引用的分類

引用能夠具體分類爲如下四種

  • 強引用:常見的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象
  • 軟引用:用來描述一些還有用但並不是必需的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行第二次回收。
  • 弱引用:也是用來描述非必須對象的,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。
  • 虛引用:最弱的一種引用關係,一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象的實例。爲一個對象設置虛引用的惟一目的就是能在這個對象被收集器回收時收到一個系統通知。

兩次標記過程

​ 即便在可達性算法中不可達的對象,也並非必定會被回收,在第一次被標記完以後,會進行一次篩選看此對象是否須要執行finalize()方法,當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲沒有必要執行。

若是該對象被斷定爲有必要執行finalize()方法,那麼這個對象將會放置在一個叫作F-Queue的隊列之中,並稍後由一個虛擬機自動創建的、低優先級的Finalizer線程去執行它。這裏的「執行」是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束。finalize()方法是該對象此時逃脫被回收命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模的標記,若是對象要在finalize()中成功拯救本身——只要從新與引用鏈上任何一個對象相關聯便可,那再第二次進行標記時它將會被移出」即將回收「的集合,若是這個時候還未逃脫,那基本上就會被真的回收了。

​ 任何一個對象的finalize()方法都只會被系統調用一次 ,若是系統面臨下一次回收,它的finalize()方法將不會再次執行。另外,finalize()。能作的全部工做,使用try-finally或者其餘方式均可以作得更好、更及時,因此建議能夠徹底忘掉java語言中有這個方法的存在。

堆的垃圾收集算法

標記-清除算法

最基礎的算法,主要存在兩處不足:

  • 效率問題,標記和清除兩個過程的效率都不高;
  • 空間問題,標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使當程序在之後的運行過程當中須要分配較大對象時沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。

標記-複製算法

  爲了解決標記清除算法的效率問題,出現了複製算法,它將可用內存按容量劃分爲大小相等的兩塊,每次使用其中的一塊。當這塊的內存用完了,就將還存活着的對象複製到另外一塊上面,而後再把已使用過的內存空間一次清理掉。優勢是每次都是對其中的一塊進行內存回收,內存分配時就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。缺點是將內存縮小爲原來的一半,代價過高了一點。
  如今的商業虛擬機都採用複製收集算法來回收新生代,IBM的研究代表,新生代中的對象98%是朝生夕死的,因此並不須要按照1:1的比例來劃份內存空間,而是將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中的一塊Survivor。當回收時,將Eden和Survivor中還存活着的對象一次性地拷貝到另一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。
  HotSpot虛擬機默認Eden和Survivor的大小比例是8:1:1,也就是每次新生代中可用內存空間爲整個新生代容量的90%(80%+10%),只有10%的內存是會被「浪費」的。固然,並不能保證每次回收都只有10%的對象存活,當Survivor空間不夠用時,須要依賴其餘內存(這裏指老年代)進行分配擔保(Handle Promotion)。即若是另一塊Survivor空間沒有足夠的空間存放上一次新生代收集下來的存活對象,這些對象將直接經過分配擔保機制進入老年代。

標記-整理算法

  複製收集算法在對象存活率較高時就須要執行較多的複製操做,效率將會變低。更關鍵的是,若是不想浪費50%的空間,就須要有額外的空間進行分配擔保,以應對被使用的內存中全部對象都100%存活的極端狀況,因此在老年代通常不能直接選用複製收集算法。
  根據老年代的特色提出了「標記-整理」算法,標記過程仍然與「標記-清除」算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。

分代收集算法

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

內存分配策略

對象優先在Eden分配

​ 大多數狀況下,對象在新生代Eden去中分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。

大對象直接進入老年代

​ 大對象指的是須要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組。大對象對虛擬機的內存分配來講是一個壞消息(更糟糕的是遇到一羣朝生夕滅的短命大對象,寫程序的時候應當避免),常常出現大對象容易致使內存還有很多空間時就提早觸發垃圾收集以獲取足夠的連續空間來安置它們。

​ 虛擬機提供了一個-XX:PretenureSizeThreshold參數,令大於這個設置值的對象直接在老年代分配,這樣作的目的是避免在Eden區以及兩個Survivor區之間發生大量的內存複製(新生代採起復制算法收集內存)。

長期存活的對象將進入老年代

位於新生代的對象,每通過一次Minor GC以後,其年齡Age都會增長1(初始值爲0),達到必定程度(默認是15歲)就會被晉升到老年代中。對象晉升老年代的閾值,能夠經過參數-XX:MaxTenuringThreshold設置。

clipboard.png

動態對象年齡斷定

​ 爲了更好地適應不一樣程序的情況,虛擬機並非永遠地要求對象的年齡必須達到MaxTenuringThreshold才能晉升老年代。若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半年齡大於等於該年齡的對象就能夠直接進入老年代,無需等到MaxTenuringThreshold中要求的年齡。

空間分配擔保

clipboard.png

參考文章
Java 中new String("字面量") 中 "字面量" 是什麼時候進入字符串常量池的?
Java虛擬機核心知識(五) HotSpot的準確式GC
聊聊JVM(六)理解JVM的safepoint

相關文章
相關標籤/搜索