垃圾收集策略與算法
垃圾收集 Garbage Collection 一般被稱爲「GC」,它誕生於1960年 MIT 的 Lisp 語言,通過半個多世紀,目前已經十分紅熟了。 在jvm 中,程序計數器、java虛擬機棧、本地方法棧都是隨線程而生隨線程而滅,棧幀隨着方法的進入和退出作入棧和出棧操做,實現了自動的內存清理,所以,咱們的內存垃圾回收主要集中於 java 堆和方法區中,在程序運行期間,這部份內存的分配和使用都是動態的.java
程序計數器、虛擬機棧、本地方法棧隨線程而生,也隨線程而滅;棧幀隨着方法的開始而入棧,隨着方法的結束而出棧。這幾個區域的內存分配和回收都具備肯定性,在這幾個區域內不須要過多考慮回收的問題,由於方法結束或者線程結束時,內存天然就跟隨着回收了。:而對於 Java 堆和方法區,咱們只有在程序運行期間才能知道會建立哪些對象,這部份內存的分配和回收都是動態的,垃圾收集器所關注的正是這部份內存。算法
1.斷定對象是否存活
若一個對象不被任何對象或變量引用,那麼它就是無效對象,須要被回收。緩存
引用計數法
在對象頭維護着一個 counter 計數器,對象被引用一次則計數器 +1;若引用失效則計數器 -1。當計數器爲 0 時,就認爲該對象無效了。安全
引用計數算法的實現簡單,斷定效率也很高,在大部分狀況下它都是一個不錯的算法。可是主流的 Java 虛擬機裏沒有選用引用計數算法來管理內存,主要是由於它很難解決對象之間循環引用的問題。jvm
舉個栗子👉對象 objA 和 objB 都有字段 instance,令 objA.instance = objB 而且 objB.instance = objA,因爲它們互相引用着對方,致使它們的引用計數都不爲 0,因而引用計數算法沒法通知 GC 收集器回收它們。this
可達性分析法
全部和 GC Roots 直接或間接關聯的對象都是有效對象,和 GC Roots 沒有關聯的對象就是無效對象。spa
GC Roots 是指:線程
- Java 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 本地方法棧中引用的對象
- 方法區中常量引用的對象
- 方法區中類靜態屬性引用的對象
GC Roots 並不包括堆中對象所引用的對象,這樣就不會有循環引用的問題。設計
2.引用的種類
斷定對象是否存活與「引用」有關。在 JDK 1.2 之前,Java 中的引用定義很傳統,一個對象只有被引用或者沒有被引用兩種狀態,咱們但願能描述這一類對象:當內存空間還足夠時,則保留在內存中;若是內存空間在進行垃圾手收集後仍是很是緊張,則能夠拋棄這些對象。不少系統的緩存功能都符合這樣的應用場景。code
在 JDK 1.2 以後,Java 對引用的概念進行了擴充,將引用分爲了如下四種。不一樣的引用類型,主要體現的是對象不一樣的可達性狀態reachable
和垃圾收集的影響。
強引用(Strong Reference)
相似 "Object obj = new Object()" 這類的引用,就是強引用,只要強引用存在,垃圾收集器永遠不會回收被引用的對象。可是,若是咱們錯誤地保持了強引用,好比:賦值給了 static 變量,那麼對象在很長一段時間內不會被回收,會產生內存泄漏。
咱們使用的大部分的引用都是強引用,這是使用最廣泛的引用。若是一個對象具備強引用,那就相似於必不可少的生活用品,垃圾回收器毫不會回收它。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具備強引用的對象來解決內存不足問題。
強引用就是指在程序代碼之中廣泛存在的,好比下面這段代碼中的object和str都是強引用: Object object = new Object(); String str = "hello";
軟引用(Soft Reference)
軟引用是一種相對強引用弱化一些的引用,可讓對象豁免一些垃圾收集,只有當 JVM 認爲內存不足時,纔會去試圖回收軟引用指向的對象。JVM 會確保在拋出 OutOfMemoryError 以前,清理軟引用指向的對象。軟引用一般用來實現內存敏感的緩存,若是還有空閒內存,就能夠暫時保留緩存,當內存不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡內存。
弱引用(Weak Reference)
弱引用的強度比軟引用更弱一些。當 JVM 進行垃圾回收時,不管內存是否充足,都會回收只被弱引用關聯的對象。
虛引用(Phantom Reference)
虛引用也稱幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響。它僅僅是提供了一種確保對象被 finalize 之後,作某些事情的機制,好比,一般用來作所謂的 Post-Mortem 清理機制。
"虛引用"顧名思義,就是形同虛設,與其餘幾種引用都不一樣,虛引用並不會決定對象的生命週期。若是一個對象僅持有虛引用,那麼它就和沒有任何引用同樣,在任什麼時候候均可能被垃圾回收。
特別注意,在程序設計中通常不多使用弱引用與虛引用,使用軟引用的狀況較多,這是由於軟引用能夠加速JVM對垃圾內存的回收速度,能夠維護系統的運行安全,防止內存溢出(OutOfMemory)等問題的產生。
3.回收堆中無效對象
Java中負責內存回收的是JVM
對於可達性分析中不可達的對象,也並非沒有存活的可能。
斷定 finalize() 是否有必要執行
JVM 會判斷此對象是否有必要執行 finalize() 方法,若是對象沒有覆蓋 finalize() 方法,或者 finalize() 方法已經被虛擬機調用過,那麼視爲「沒有必要執行」。那麼對象基本上就真的被回收了。
若是對象被斷定爲有必要執行 finalize() 方法,那麼對象會被放入一個 F-Queue 隊列中,虛擬機會以較低的優先級執行這些 finalize()方法,但不會確保全部的 finalize() 方法都會執行結束。若是 finalize() 方法出現耗時操做,虛擬機就直接中止指向該方法,將對象清除。
對象重生或死亡
若是在執行 finalize() 方法時,將 this 賦給了某一個引用,那麼該對象就重生了。若是沒有,那麼就會被垃圾收集器清除。
任何一個對象的 finalize() 方法只會被系統自動調用一次,若是對象面臨下一次回收,它的 finalize() 方法不會被再次執行,想繼續在 finalize() 中自救就失效了。
finalize() 是Object中的方法,當垃圾回收器將要回收對象所佔內存以前被調用,即當一個對象被虛擬機宣告死亡時會先調用它finalize()方法,讓此對象處理它生前的最後事情(這個對象能夠趁這個時機掙脫死亡的命運)。
4.回收方法區內存
方法區中存放生命週期較長的類信息、常量、靜態變量,每次垃圾收集只有少許的垃圾被清除。方法區中主要清除兩種垃圾:
- 廢棄常量
- 無用的類
斷定廢棄常量
只要常量池中的常量不被任何變量或對象引用,那麼這些常量就會被清除掉。好比,一個字符串 "bingo" 進入了常量池,可是當前系統沒有任何一個 String 對象引用常量池中的 "bingo" 常量,也沒有其它地方引用這個字面量,必要的話,"bingo"常量會被清理出常量池。
斷定無用的類
斷定一個類是不是「無用的類」,條件較爲苛刻。
- 該類的全部對象都已經被清除
- 加載該類的 ClassLoader 已經被回收
- 該類的 java.lang.Class 對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。
一個類被虛擬機加載進方法區,那麼在堆中就會有一個表明該類的對象:java.lang.Class。這個對象在類被加載進方法區時建立,在方法區該類被刪除時清除。
5.垃圾收集算法
學會了如何斷定無效對象、無用類、廢棄常量以後,剩餘工做就是回收這些垃圾。常見的垃圾收集算法有如下幾個:
標記-清除算法
- 標記的過程:遍歷全部的
GC Roots
,而後將全部GC Roots
可達的對象標記爲存活的對象。 - 清除的過程:將遍歷堆中全部的對象,將沒有標記的對象所有清除掉。與此同時,清除那些被標記過的對象的標記,以便下次的垃圾回收。
這種方法有兩個不足:
- 效率問題:標記和清除兩個過程的效率都不高。
- 空間問題:標記清除以後會產生大量不連續的內存碎片,碎片太多可能致使之後須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。
標記-整理算法(老年代)
- 標記:它的第一個階段與標記/清除算法是如出一轍的,均是遍歷
GC Roots
,而後將存活的對象標記。 - 整理:移動全部存活的對象,且按照內存地址次序依次排列,而後將末端內存地址之後的內存所有回收。所以,第二階段才稱爲整理階段。
這是一種老年代的垃圾收集算法。老年代的對象通常壽命比較長,所以每次垃圾回收會有大量對象存活,若是採用複製算法,每次須要複製大量存活的對象,效率很低。
複製算法(新生代)
爲了解決效率問題,「複製」收集算法出現了。將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊內存用完,須要進行垃圾收集時,就將存活者的對象複製到另外一塊上面,而後將第一塊內存所有清除。
- 優勢:不會有內存碎片的問題。
- 缺點:內存縮小爲原來的一半,浪費空間。
爲了解決空間利用率問題,能夠將內存分爲三塊: Eden、From Survivor、To Survivor,比例是 8:1:1,每次使用 Eden 和其中一塊 Survivor。回收時,將 Eden 和 Survivor 中還存活的對象一次性複製到另一塊 Survivor 空間上,最後清理掉 Eden 和剛纔使用的 Survivor 空間。這樣只有 10% 的內存被浪費。
可是咱們沒法保證每次回收都只有很少於 10% 的對象存活,當 Survivor 空間不夠,須要依賴其餘內存(指老年代)進行分配擔保。
分配擔保
爲對象分配內存空間時,若是 Eden+Survivor 中空閒區域沒法裝下該對象,會觸發 MinorGC 進行垃圾收集。但若是 Minor GC 事後依然有超過 10% 的對象存活,這樣存活的對象直接經過分配擔保機制進入老年代,而後再將新對象存入 Eden 區。
增量算法
增量算法的基本思想是,若是一次性將全部的垃圾進行處理,須要形成系統長時間的停頓,那麼就可讓垃圾收集線程和應用程序線程交替執行。每次,垃圾收集線程只收集一小片區域的內存空間,接着切換到應用程序線程。依次反覆,直到垃圾收集完成。使用這種方式,因爲在垃圾回收過程當中,間斷性地還執行了應用程序代碼,因此能減小系統的停頓時間。可是,由於線程切換和上下文轉換的消耗,會使得垃圾回收的整體成本上升,形成系統吞吐量的降低。
分代收集算法
根據對象存活週期的不一樣,將內存劃分爲幾塊。通常是把 Java 堆分爲新生代和老年代,針對各個年代的特色採用最適當的收集算法。
- 新生代:複製算法
- 老年代:標記-清除算法、標記-整理算法