這里主要是闡明各算法的實現思想,而不去細論算法的具體實現
標記—清除算法(Mark-Sweep)
標記—清除算法是最基礎的收集算法,它分為“標記”和“清除”兩個階段:首先標記出所需回收的對象,在標記完成后統一回收掉所有被標記的對象,它的標記過程其實就是前面的可達性分析算法中判定垃圾對象的標記過程。標記—清除算法的執行情況如下圖所示:
- 回收前狀態
- 回收后狀態
該算法有如下缺點:
-
標記和清除過程的效率都不高
-
標記清除后會產生大量不連續的內存碎片,空間碎片太多可能會導致,當程序在以后的運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不觸發另一次垃圾收集動作
復制算法(Copy)
復制算法是針對標記—清除算法的缺點,在其基礎上進行改進而得到的,它將可用內存按容量分為大小相等的兩塊,每次只使用其中的一塊,當這一塊的內存用完了,就將還存活着的對象復制到另外一塊內存上面,然后再把已使用過的內存空間一次清理掉。復制算法有如下優點:
-
每次只對一塊內存進行回收,運行高效
-
只需移動棧頂指針,按順序分配內存即可,實現簡單
-
內存回收時不用考慮內存碎片的出現
它的缺點是:可一次性分配的最大內存縮小了一半
復制算法的執行情況如下圖所示:
- 回收前狀態
- 回收后狀態
現在的商業虛擬機都采用這種收集算法來回收新生代,新生代中的對象98%都是“朝生夕死”的,所以並不需要按照1:1的比例來划分內存空間,而是將內存分為一塊比較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活着的對象一次性地復制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,也就是說,每次新生代中可用內存空間為整個新生代容量的90%(80%+10%),只有10%的空間會被浪費。
當然,98%的對象可回收只是一般場景下的數據,我們沒有辦法保證每次回收都只有不多於10%的對象存活,當Survivor空間不夠用時,需要依賴於老年代進行分配擔保,所以大對象直接進入老年代。
標記—整理算法(Mark-Compact)
復制算法比較適合於新生代,在老年代中,對象存活率比較高,如果執行較多的復制操作,效率將會變低,所以老年代一般會選用其他算法,如標記—整理算法。該算法標記的過程與標記—清除算法中的標記過程一樣,但對標記后出的垃圾對象的處理情況有所不同,它不是直接對可回收對象進行清理,而是讓所有的對象都向一端移動,然后直接清理掉端邊界以外的內存。標記—整理算法的回收情況如下所示:
- 回收前狀態:
- 回收后狀態:
分代收集
當前商業虛擬機的垃圾收集都采用分代收集,它根據對象的存活周期的不同將內存划分為幾塊,一般是把Java堆分為新生代和老年代。
-
在新生代中,每次垃圾收集時都會發現有大量對象死去,只有少量存活,因此可選用復制算法來完成收集
-
老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記—清除算法或標記—整理算法來進行回收
| GC算法 | 優點 | 缺點 | 存活對象移動 | 內存碎片 | 適用場景 |
|---|---|---|---|---|---|
| 引用計數 | 實現簡單 | 不能處理循環引用 | |||
| 標記清除 | 不需要額外空間 | 兩次掃描,耗時嚴重 | N | Y | 老年代 |
| 復制 | 沒有標記和清除 | 需要額外空間 | Y | N | 新生代 |
| 標記整理 | 沒有內存碎片 | 需要移動對象的成本 | Y | N | 老年代 |
補充:
增量算法 (Incremental Collecting)
在垃圾回收過程中,應用軟件將處於一種 CPU 消耗很高的狀態。在這種 CPU 消耗很高的狀態下,應用程序所有的線程都會掛起,暫停一切正常的工作,等待垃圾回收的完成。如果垃圾回收時間過長,應用程序會被掛起很久,將嚴重影響用戶體驗或者系統的穩定性。
增量算法的基本思想是,如果一次性將所有的垃圾進行處理,需要造成系統長時間的停頓,那么就可以讓垃圾收集線程和應用程序線程交替執行。每次垃圾收集線程只收集一小片區域的內存空間,接着切換到應用程序線程。依次反復,直到垃圾收集完成。
使用這種方式,由於在垃圾回收過程中,間斷性地還執行了應用程序代碼,所以能減少系統的停頓時間。但是,因為線程切換和上下文轉換的消耗,會使得垃圾回收的總體成本上升,造成系統吞吐量的下降。
深入理解分代收集算法
1. 為什么要分代
分代的垃圾回收策略,是基於這樣一個事實:不同對象的生命周期是不一樣的。因此,不同生命周期的對象可以采取不同的收集方式,以便提高回收效率。
在Java程序運行的過程中,會產生大量的對象,其中有些對象是與業務信息相關,比如Http請求中的Session對象、線程、Socket連接,這類對象跟業務直接掛鈎,因此生命周期比較長。還有一些對象主要是程序運行過程中生成的臨時變量,這些對象生命周期會比較短,比如String對象,由於其不變類的特性,系統會產生大量的這些對象,有些對象甚至只用一次即可回收。
試想,在不進行對象存活時間區分的情況下,每次垃圾回收都是對整個堆空間進行回收,花費時間相對會長,同時,因為每次回收都需要遍歷所有存活對象,但實際上,對於生命周期長的對象而言,這種遍歷是沒有效果的,因為可能進行了很多次遍歷,但是他們依舊存在。因此,分代垃圾回收采用分治的思想,進行代的划分,把不同生命周期的對象放在不同代上,不同代上采用最適合它的垃圾回收方式進行回收。
分代的好處:如果單純從JVM的功能考慮(用簡單粗暴的標記-清理刪除垃圾對象),並不需要新生代,完全可以針對整個堆進行操作,但是每次GC都針對整個堆標記清理回收對象太慢了。把堆划分為新生代和老年代有2個好處:
- 簡化了新對象的分配(只在新生代分配內存)
- 可以更有效的清除不再需要的對象(死對象)。
在新生代中,GC可以快速標記回收“死對象”,而不需要掃描整個堆中的存活一段時間的“老對象”。
2. 什么情況下觸發垃圾回收
由於對象進行了分代處理,因此垃圾回收區域、時間也不一樣。GC有兩種類型:Minor GC和Full GC。
-
Minor GC(新生代回收)的觸發條件比較簡單,Eden空間不足就開始進行Minor GC回收新生代。
-
Full GC(老年代回收,一般伴隨一次Minor GC)則有幾種觸發條件:
-
(1)老年代空間不足
-
(2)PermSpace空間不足
-
(3)統計得到的Minor GC晉升到老年代的平均大小大於老年代的剩余空間
這里注意一點:PermSpace並不等同於方法區,只不過是Hotspot JVM用PermSpace來實現方法區而已,有些虛擬機沒有PermSpace而用其他機制來實現方法區。
-
補充:對象的空間分配和晉升
(1)對象優先在Eden上分配
(2)大對象直接進入老年代
虛擬機提供了-XX:PretenureSizeThreshold參數,大於這個參數值的對象將直接分配到老年代中。因為新生代采用的是復制算法,在Eden中分配大對象將會導致Eden區和兩個Survivor區之間大量的內存拷貝。
(3)長期存活的對象將進入老年代對象在Survivor區中每熬過一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15歲)時,就會晉升到老年代中。
3. 為什么不是一塊Survivor空間而是兩塊?
這里涉及到一個新生代和老年代的存活周期的問題,比如一個對象在新生代經歷15次(僅供參考)GC,就可以移到老年代了。問題來了,當我們第一次GC的時候,我們可以把Eden區的存活對象放到Survivor A空間,但是第二次GC的時候,Survivor A空間的存活對象也需要再次用Copying算法,放到Survivor B空間上,而把剛剛的Survivor A空間和Eden空間清除。第三次GC時,又把Survivor B空間的存活對象復制到Survivor A空間,如此反復。
所以,這里就需要兩塊Survivor空間來回倒騰。
4. 為什么Eden空間這么大而Survivor空間要分的少一點?
新創建的對象都是放在Eden空間,這是很頻繁的,尤其是大量的局部變量產生的臨時對象,這些對象絕大部分都應該馬上被回收,能存活下來被轉移到survivor空間的往往不多。所以,設置較大的Eden空間和較小的Survivor空間是合理的,大大提高了內存的使用率,緩解了Copying算法的缺點。
我看8:1:1就挺好的,當然這個比例是可以調整的,包括上面的新生代和老年代的1:2的比例也是可以調整的。
新的問題又來了,從Eden空間往Survivor空間轉移的時候Survivor空間不夠了怎么辦?直接放到老年代去。
5. Eden空間和兩塊Survivor空間的工作流程
這里本來簡單的Copying算法被划分為三部分后很多朋友一時理解不了,也確實不好描述,下面我來演示一下Eden空間和兩塊Survivor空間的工作流程。
現在假定有新生代Eden,Survivor A, Survivor B三塊空間和老生代Old一塊空間。
// 分配了一個又一個對象 放到Eden區 // 不好,Eden區滿了,只能GC(新生代GC:Minor GC)了 把Eden區的存活對象copy到Survivor A區,然后清空Eden區(本來Survivor B區也需要清空的,不過本來就是空的) // 又分配了一個又一個對象 放到Eden區 // 不好,Eden區又滿了,只能GC(新生代GC:Minor GC)了 把Eden區和Survivor A區的存活對象copy到Survivor B區,然后清空Eden區和Survivor A區 // 又分配了一個又一個對象 放到Eden區 // 不好,Eden區又滿了,只能GC(新生代GC:Minor GC)了 把Eden區和Survivor B區的存活對象copy到Survivor A區,然后清空Eden區和Survivor B區 // ... // 有的對象來回在Survivor A區或者B區呆了比如15次,就被分配到老年代Old區 // 有的對象太大,超過了Eden區,直接被分配在Old區 // 有的存活對象,放不下Survivor區,也被分配到Old區 // ... // 在某次Minor GC的過程中突然發現: // 不好,老年代Old區也滿了,這是一次大GC(老年代GC:Major GC) Old區慢慢的整理一番,空間又夠了 // 繼續Minor GC // ... // ...
6. Java 垃圾回收會出現不可回收的對象嗎?
內存泄露問題
作者:pgl2011
鏈接:https://www.jianshu.com/p/b5c8ff84e5e4
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。
