JVM內存回收機制涉及的知識點太多了,了解越多越迷糊,汗一個,這里僅簡單做個筆記,主要參考《深入理解Java虛擬機:JVM高級特性與最佳實踐(第二版)》
目前java的jdk默認虛擬機為HotSpot,因此本文涉及虛擬機相關內容都指HotSpot虛擬機
本文主要關注GC的回收:判斷哪些對象可回收,如何回收,回收機制
判斷哪些對象可回收
GC是通過對象是否存活來決定是否進行回收,判斷對象是否存活主要有兩種算法:引用計數算法、可達性分析算法
- 引用計數算法
引用計數的算法原理是給對象添加一個引用計數器,每被引用一次計數器加1,引用失效時減1,當計數器0后表示對象不在被引用,可以被回收了,引用計數法簡單高效,但是存在對象之間循環引用問題,可能導致無法被GC回收,需要花很大精力去解決循環引用問題 - 可達性分析算法
可達性分析的算法原理是從對象根引用(堆棧、方法表的靜態引用和常量引用區、本地方法棧)開始遍歷搜索所有可到達對象,形成一個引用鏈,遍歷的同時標記出可達對象和不可達對象,不可達對象表示沒有任何引用存在,可以被GC回收
如何回收
找到可回收對象后,如何進行回收呢?
內存回收算法主要有標記-清除、停止-復制、標記-整理,不同算法使用不同的場景,總體來說停止-復制算法適合對象存活時間短,存活率低的新生代,標記-清除和標記-整理算法適合對象存活時間長,存活率高的老年代
- 標記-清除(Mark-Sweep)
通過可達性分析算法標記所有不可達對象,然后清理不可達對象。這種算法會形成大量的內存碎片 - 停止-復制(Stop-Copy)
將新生代內存按照8:1:1的比例分為一個eden區和兩個survivor(survivor0,survivor1)區,回收時先將eden區存活對象復制到一個survivor0區,然后清空eden區,當這個survivor0區也存放滿了時,則將eden區和survivor0區存活對象復制到另一個survivor1區,然后清空eden和這個survivor0區,此時survivor0區是空的,然后將survivor0區和survivor1區交換,即保持survivor1區為空, 如此往復,當survivor1區不足以存放 eden和survivor0的存活對象時,就將存活對象直接存放到老年代(這時我們可能回想,若是老年代也滿了咋辦,若是老年代也滿了就會觸發一次Full GC,也就是新生代、老年代都進行回收,若是內存還不夠呢。。。,還不夠那不廢話了嗎,OutOfMemory,不陌生吧哈哈)。從停止-復制算法的原理上我們可以看到,這種算法對於存活率較低的對象回收有着非常高的效率,而且不會形成內存碎片,但是會浪費一定的內存空間,適合對象存活率較低的新生代使用,如果在對象存活率較高的老年代采用這種算法,那將會是一場災難 - 標記-整理(Mark-Compact)
通過可達性分析算法標記所有不可達對象,然后將存活對象都向一個方向移動,然后清理掉邊界外的內存。這種算法是將存活對象向着一個方向聚集,然后將剩余區域清空,這種算法適合對象存活率較高的老年代
GC回收機制:分代收集算法
JVM內存收集算法基本上都是采用分代收集算法,即將內存划分為新生代、老年代,也有人把方法區算做永久代
- 新生代
對象被創建時,內存分配都是發生在新生代(大對象直接分配在老年代),絕大多數對象都是朝生夕滅,創建后很快就會不在使用,變為不可達的對象,被GC回收掉。新生代的對象存活率很低(到底有多低?研究表明有高達98%的對象創建后很快就消亡,想象一下平時編程,除了全局變量,局部變量在退出調用方法后還有幾個能存活),存活時間都很短,新生代發生的GC也叫做Minor GC,MinorGC發生頻率比較高(不一定等Eden區滿了才觸發) - 老年代
當對象在新生代發生了多次Minor GC后仍然存活的對象即進入老年代,老年代的對象比新生多很多,當然了內存比新生代也大很多(大概比例是1:2,即新生代占用堆內存總量的1/3),當老年代內存滿時觸發Major GC即Full GC,Full GC發生頻率比較低,老年代對象存活時間比較長,存活率標記高
一般來說我們所說的GC都是發生在新生代和老年代,新生代對象存活時間短,存活率低一般采用停止-復制算法,老年代對象存活時間長,存活率高,一般采用標記-整理、標記-清楚算法,具體采用何種算法和具體采用的垃圾收集器有關
GC里面有些類似未成年人和成年人,新創建的對象為新生代,新生代想要成為老年代需要經過一定的成長(總得一點點長大是吧),新創建的對象年齡為1,每發生一次Minor GC,存活對象的年齡增加1,當經歷了15次Minor GC后,仍然存活的對象達到15歲,成達到法定成年年齡15歲(默認是15),正式成為成年人(老年代),對象成年后也就沒有了年齡概念,直到對象死亡,會一直呆在老年代,當然也有一些老不死的(靜態變量、常量等),會與世長存,除非地球滅亡(GC崩潰)
GC收集器
GC采用分代回收算好后,起着重要作用的是GC收集器,GC收集器分為新生代收集器和老年代收集器,不同的收集器使用不同的收集算法,有着不同的特點,由於目前的收集器在內存回收時無法消除(Stop-the-world),即在回收內存時不可避免的停止用戶線程,目前的收集器只能使停頓時間越來越短,但是無法徹底消除,主要的收集其中Parallel Scavenge和Parallel Old是追求吞吐量為目標,其它的收集器都是追求高響應,低停頓,
新生代收集器: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 追求高響應時間的選擇 - G1(Garbage-First)收集器(標記-整理算法、停止復制算法)
GC最新型號,高富帥,高並發、可預測停頓、分代收集,不出意外未來主流將會逐步替代CMS,g1模糊了分代概念,雖然還是分為新生代和老年代,但是新生代和老年代不再是物理隔離,而是將內存分為n個region,以region為清理單位,整體采用標記-整理算法,region內部使用停止-復制算法。