如果你想要從太空觀察地球,衛星技術就能夠做到這一點。圖中的海洋流是衛星地圖展現的,紫色和粉紅色的漩渦代表更暖的洋流,而藍色和綠色是較冷的洋流。衛星地圖為天氣預報提供有利的證據,我們能夠準確的知道氣溫以及對海洋健康做長期分析監控。
垃圾回收機制的意義
Java語言中一個顯著的特點就是引入了垃圾回收機制,使c++程序員最頭疼的內存管理的問題迎刃而解,它使得Java程序員在編寫程序的時候不再需要考慮內存管理。由于有個垃圾回收機制,Java中的對象不再有“作用域”的概念,只有對象的引用才有“作用域”。垃圾回收可以有效的防止內存泄露,有效的使用空閑的內存。
ps:內存泄露是指該內存空間使用完畢之后未回收,在不涉及復雜數據結構的一般情況下,Java 的內存泄露表現為一個內存對象的生命周期超出了程序需要它的時間長度,我們有時也將其稱為“對象游離”。
垃圾回收機制中的算法
Java語言規范沒有明確地說明JVM使用哪種垃圾回收算法,但是任何一種垃圾回收算法一般要做2件基本的事情:
(1)發現無用信息對象;
(2)回收被無用對象占用的內存空間,使該空間可被程序再次使用。
Jvm(Java虛擬機)內存模型
在了解垃圾回收算法之前先簡單了解一下jvm內存模型.
從Jvm內存模型中入手對于理解GC會有很大的幫助,不過這里只需要了解一個大概,說多了反而混淆視線。
Jvm(Java虛擬機)主要管理兩種類型內存:堆和非堆。
- 堆是運行時數據區域,所有類實例和數組的內存均從此處分配。
- 非堆是JVM留給自己用的,包含方法區、JVM內部處理或優化所需的內存(如 JIT Compiler,Just-in-time Compiler,即時編譯后的代碼緩存)、每個類結構(如運行時常數池、字段和方法數據)以及方法和構造方法的代碼。
簡言之,Java程序內存主要(這里強調主要二字)分兩部分,堆和非堆。大家一般new的對象和數組都是在堆中的,而GC主要回收的內存也是這塊堆內存。
配一張示意圖總結一下:
[圖片上傳失敗...(image-4de587-1517713417487)]
堆內存(Heap Memory): 存放Java對象
非堆內存(Non-Heap Memory): 存放類加載信息和其它meta-data
其它(Other): 存放JVM 自身代碼等
重點是堆內存,我們就再看看堆的內存模型。
- 堆內存由垃圾回收器的自動內存管理系統回收。
- 堆內存分為兩大部分:新生代和老年代。比例為1:2。
- 老年代主要存放應用程序中生命周期長的存活對象。
- 新生代又分為三個部分:一個Eden區和兩個Survivor區,比例為8:1:1。
- Eden區存放新生的對象。
- Survivor存放每次垃圾回收后存活的對象。
[圖片上傳失敗...(image-c20973-1517713417487)]
其實只需要關注這幾個問題:
- 為什么要分新生代和老年代?
- 新生代為什么分一個Eden區和兩個Survivor區?
- 一個Eden區和兩個Survivor區的比例為什么是8:1:1?
現在還不能解釋為什么,但這幾個問題都是垃圾回收機制所采用的算法決定的。
所以問題轉化為,是何種算法?為什么要采用此種算法?
可回收對象的判定
講算法之前,我們先要搞清楚一個問題,什么樣的對象是垃圾(無用對象),需要被回收?
目前市面上有兩種算法用來判定一個對象是否為垃圾。
- 引用計數算法
- 可達性分析算法(根搜索算法)
引用計數法(Reference Counting Collector)
算法分析
引用計數是垃圾收集器中的早期策略。在這種方法中,堆中每個對象實例都有一個引用計數。當一個對象被創建時,且將該對象實例分配給一個變量,該變量計數設置為1。當任何其它變量被賦值為這個對象的引用時,計數加1(a = b,則b引用的對象實例的計數器+1),但當一個對象實例的某個引用超過了生命周期或者被設置為一個新值時,對象實例的引用計數器減1。任何引用計數器為0的對象實例可以被當作垃圾收集。當一個對象實例被垃圾收集時,它引用的任何對象實例的引用計數器減1。
[圖片上傳失敗...(image-40d1e0-1517713417487)]
優缺點
- 優點: 簡單,高效
引用計數收集器可以很快的執行,交織在程序運行中。對程序需要不被長時間打斷的實時環境比較有利。 - 缺點: 無法檢測出循環引用
如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數永遠不可能為0.
public class Main {
public static void main(String[] args) {
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();
object1.object = object2;
object2.object = object1;
object1 = null;
object2 = null;
}
}
最后面兩句將object1和object2賦值為null,也就是說object1和object2指向的對象已經不可能再被訪問,但是由于它們互相引用對方,導致它們的引用計數器都不為0,那么垃圾收集器就永遠不會回收它們。
可達性分析算法(根搜索算法)
算法分析
根搜索算法是從離散數學中的圖論引入的,程序把所有的引用關系看作一張圖,從一個節點GC ROOT開始,尋找對應的引用節點,找到這個節點以后,繼續尋找這個節點的引用節點,當所有的引用節點尋找完畢之后,剩余的節點則被認為是沒有被引用到的節點,即無用的節點。
java中可作為GC Root的對象有:
- 虛擬機棧中引用的對象(本地變量表)
- 方法區中靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中引用的對象(Native對象)
優缺點
- 優點:
解決循環引用問題 - 缺點:
Stop The World
在學習GC前,你應該知道一個技術名詞:這個詞是“stop-the-world。“ 無論你選擇哪種GC算法,Stop-the-world都會發生。Stop-the-world意味著JVM停止應用程序,而去進行垃圾回收。當stop-the-world發生時,除了進行垃圾回收的線程,其他所有線程都將停止運行。被中斷的任務將在GC任務完成后恢復執行。GC調優往往意味著減少stop-the-world的時間.
垃圾回收的時候,需要整個的引用狀態保持不變,否則判定是判定垃圾,等我稍后回收的時候它又被引用了,這就全亂套了。所以,GC的時候,其他所有的程序執行處于暫停狀態,卡住了。
幸運的是,這個卡頓是非常短(尤其是新生代),對程序的影響微乎其微 (關于其他GC比如并發GC之類的,在此不討論)。
所以GC的卡頓問題由此而來,也是情有可原,暫時無可避免。
垃圾回收算法
標記清除算法 (Mark-Sweep)
原理
分為兩個階段: 標記階段(Mark) 和清除階段(Sweep):
- 標記階段:
collector從mutator根對象開始進行遍歷,對從mutator根對象可以訪問到的對象都打上一個標識,一般是在對象的header中,將其記錄為可達對象。 - 清除階段:
collector對堆內存(heap memory)從頭到尾進行線性的遍歷,如果發現某個對象沒有標記為可達對象-通過讀取對象的header信息,則就將其回收。
collector指的就是垃圾收集器,而mutator是指除了垃圾收集器之外的部分,比如說我們應用程序本身。mutator的職責一般是NEW(分配內存),READ(從內存中讀取內容),WRITE(將內容寫入內存),而collector則就是回收不再使用的內存來供mutator進行NEW操作的使用。
mutator根對象一般指的是分配在堆內存之外,可以直接被mutator直接訪問到的對象,一般是指靜態/全局變量以及Thread-Local變量
可達對象的定義,從mutator根對象開始進行遍歷,可以被訪問到的對象都稱為是可達對象。這些對象也是mutator(你的應用程序)正在使用的對象。
[圖片上傳失敗...(image-4e7592-1517713417487)]
優缺點
- 優點:簡單,容易實現
- 缺點:容易產生內存碎片,碎片太多可能會導致后續過程中需要為大對象分配空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作。
復制算法 (Copying)
原理
復制算法將內存劃分為兩個區間,活動區和空閑區,在任意時間點,所有動態分配的對象都只能分配在活動區
當有效內存空間耗盡時,JVM將暫停程序運行,開啟復制算法GC線程。接下來GC線程會將活動區間內的存活對象,全部復制到空閑區間,且嚴格按照內存地址依次排列,與此同時,GC線程將更新存活對象的內存引用地址指向新的內存地址。
此時,空閑區間已經與活動區間交換,而垃圾對象現在已經全部留在了原來的活動區間,也就是現在的空閑區間。事實上,在活動區間轉換為空間區間的同時,垃圾對象已經被一次性全部回收。
清理前:
清理后:
通俗的講,就是:
復制算法將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。
優缺點
- 優點:實現簡單,運行高效且不容易產生內存碎片
- 缺點:浪費內存
標記整理算法 (Mark-Compact)
原理
分為兩個階段: 標記階段(Mark) 和整理階段(Compact)
- 標記階段: 與標記/清除算法是一模一樣
- 整理階段: 移動所有存活的對象,且按照內存地址次序依次排列,然后將末端內存地址以后的內存全部回收。
標記-整理算法采用標記-清除算法一樣的方式進行對象的標記,但在清除時不同,在回收不存活的對象占用的空間后,會將所有的存活對象往左端空閑空間移動,并更新對應的指針。標記-整理算法是在標記-清除算法的基礎上,又進行了對象的移動,因此成本更高,但是卻解決了內存碎片的問題。
優缺點
- 優點: 標記/整理算法不僅可以彌補標記/清除算法當中,內存區域分散的缺點,也消除了復制算法當中,內存減半的高額代價
- 缺點: 低效
分代回收算法(Generational Collector)
分代回收算法其實不算一種新的算法,而是根據復制算法和標記整理算法的的特點綜合而成。這種綜合是考慮到java的語言特性的。
這里重復一下兩種老算法的適用場景:
- 復制算法:適用于存活對象很少。回收對象多
- 適用用于存活對象多,回收對象少
復習下面這個圖:
[圖片上傳失敗...(image-1560a8-1517713417487)]
解析:
- 堆內存分為兩大部分:新生代和老年代。比例為1:2。
- 新生代又分為三個部分:一個Eden區和兩個Survivor區,比例為8:1:1。
- Eden區存放新生的對象。new
- Survivor存放每次垃圾回收后存活的對象。
- 老年代主要存放應用程序中生命周期長的存活對象。
原理(過程)
所有新生成的對象首先都是放在年輕代的。年輕代的目標就是盡可能快速的收集掉那些生命周期短的對象。
- 分配對象時,保存在Eden區
- Eden區滿,觸發GC(Minor GC), 將Eden區存活對象復制到一個survivor0區,然后清空Eden區
- 當這個survivor0區也存放滿了時,則將eden區和survivor0區存活對象復制到另一個survivor1區,然后清空eden和這個survivor0區
- 此時survivor0區是空的,然后將survivor0區和survivor1區交換,即保持survivor1區為空, 如此往復
- 當survivor1區不足以存放 eden和survivor0的存活對象時,就將存活對象直接存放到老年代。若是老年代也滿了就會觸發一次Full GC,也就是新生代、老年代都進行回收
年輕代 年老代 持久代
- 年輕代(Young Generation):
所有新生成的對象首先都是放在年輕代的。年輕代的目標就是盡可能快速的收集掉那些生命周期短的對象。
新生代發生的GC也叫做Minor GC,MinorGC發生頻率比較高(不一定等Eden區滿了才觸發) - 年老代(Old Generation):
年老代中存放的都是一些生命周期較長的對象。
老年代內存滿時觸發Major GC即Full GC,Full GC發生頻率比較低,老年代對象存活時間比較長,存活率標記高。 - 持久代(Permanent Generation):
用于存放靜態文件,如Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者調用一些class,例如Hibernate 等,在這種時候需要設置一個比較大的持久代空間來存放這些運行過程中新增的類。
深入理解分代回收算法
為什么不是一塊Survivor空間而是兩塊?
這里涉及到一個新生代和老年代的存活周期的問題,比如一個對象在新生代經歷15次(僅供參考)GC,就可以移到老年代了。問題來了,當我們第一次GC的時候,我們可以把Eden區的存活對象放到Survivor A空間,但是第二次GC的時候,Survivor A空間的存活對象也需要再次用Copying算法,放到Survivor B空間上,而把剛剛的Survivor A空間和Eden空間清除。第三次GC時,又把Survivor B空間的存活對象復制到Survivor A空間,如此反復。
所以,這里就需要兩塊Survivor空間來回倒騰
為什么Eden空間這么大而Survivor空間要分的少一點?
新創建的對象都是放在Eden空間,這是很頻繁的,尤其是大量的局部變量產生的臨時對象,這些對象絕大部分都應該馬上被回收,能存活下來被轉移到survivor空間的往往不多。所以,設置較大的Eden空間和較小的Survivor空間是合理的,大大提高了內存的使用率,緩解了Copying算法的缺點。
我看8:1:1就挺好的,當然這個比例是可以調整的,包括上面的新生代和老年代的1:2的比例也是可以調整的。
新的問題又來了,從Eden空間往Survivor空間轉移的時候Survivor空間不夠了怎么辦?直接放到老年代去
垃圾收集器
- 新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge
- 老年代收集器使用的收集器:Serial Old、Parallel Old、CMS
Serial收集器(復制算法)
新生代單線程收集器,標記和清理都是單線程,優點是簡單高效。
Serial Old收集器(標記-整理算法)
老年代單線程收集器,Serial收集器的老年代版本。
ParNew收集器(停止-復制算法)
新生代收集器,可以認為是Serial收集器的多線程版本,在多核CPU環境下有著比Serial更好的表現。
Parallel Scavenge收集器(停止-復制算法)
并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般為99%, 吞吐量= 用戶線程時間/(用戶線程時間+GC線程時間)。適合后臺應用等對交互相應要求不高的場景。
Parallel Old收集器(停止-復制算法)
Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量優先
CMS(Concurrent Mark Sweep)收集器(標記-清理算法)
高并發、低停頓,追求最短GC回收停頓時間,cpu占用比較高,響應時間快,停頓時間短,多核cpu 追求高響應時間的選擇
GC的執行機制
Scavenge GC(Minor GC)
一般情況下,當新對象生成,并且在Eden申請空間失敗時,就會觸發Scavenge GC,對Eden區域進行GC,清除非存活對象,并且把尚且存活的對象移動到Survivor區。然后整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。因為大部分對象都是從Eden區開始的,同時Eden區不會分配的很大,所以Eden區的GC會頻繁進行。因而,一般在這里需要使用速度快、效率高的算法,使Eden去能盡快空閑出來。
Full GC
對整個堆進行整理,包括Young、Tenured和Perm。Full GC因為需要對整個堆進行回收,所以比Scavenge GC要慢,因此應該盡可能減少Full GC的次數。在對JVM調優的過程中,很大一部分工作就是對于FullGC的調節。有如下原因可能導致Full GC:
- 年老代(Tenured)被寫滿
- 持久代(Perm)被寫滿
- System.gc()被顯示調用
- 上一次GC之后Heap的各域分配策略動態變化
參考: