詳解JVM內存布局及GC原理,值得收藏
java發展歷史上出現過很多垃圾回收器,各有各的適應場景,不僅僅是開發,作為運維也需要對這方面有一定的掌握,今天簡單介紹一下java的內存布局以及各種垃圾回收器的原理。
JVM內存布局
JVM從概念上大致分為6個(邏輯)區域:
這6塊區域按是否被線程共享,可以分為兩大類:
一類是每個線程所獨享的:
- PC Register:也稱為程序計數器, 記錄每個線程當前執行的指令信。eg:當前執行到哪一條指令,下一條該取哪條指令。
- JVM Stack:也稱為虛擬機棧,記錄每個棧幀(Frame)中的局部變量、方法返回地址等。
- Native Method Stack:本地(原生)方法棧,顧名思義就是調用操作系統原生本地方法時,所需要的內存區域。
上述3類區域,生命周期與Thread相同,即:線程創建時,相應的區域分配內存,線程銷毀時,釋放相應內存。
另一類是所有線程共享的:
- Heap:即鼎鼎大名的堆內存區,也是GC垃圾回收的主站場,用于存放類的實例對象及Arrays實例等。
- Method Area:方法區,主要存放類結構、類成員定義,static靜態成員等。
- Runtime Constant Pool:運行時常量池,比如:字符串,int -128~127范圍的值等,它是Method Area中的一部分。
Heap、Method Area 都是在虛擬機啟動時創建,虛擬機退出時釋放。
總之,程序運行時,內存中的信息大致分為兩類,一是跟程序執行邏輯相關的指令數據,這類數據通常不大,而且生命周期短;一是跟對象實例相關的數據,這類數據可能會很大,而且可以被多個線程長時間內反復共用,比如字符串常量、緩存對象這類。
將這兩類特點不同的數據分開管理,體現了軟件設計上“模塊隔離”的思想。好比我們通常會把后端service與前端website解耦類似,也更便于內存管理。
GC垃圾回收原理
1. 哪些內存區域需要GC ?
thread獨享的區域:PC Regiester、JVM Stack、Native Method Stack,其生命周期都與線程相同(即:與線程共生死),所以無需GC。線程共享的Heap區、Method Area則是GC關注的重點對象。
2. 常用的GC算法
(1) mark-sweep 標記清除法
如上圖,黑色區域表示待清理的垃圾對象,標記出來后直接清空。該方法簡單快速,但是缺點也很明顯,會產生很多內存碎片。
(2) mark-copy 標記復制法
思路也很簡單,將內存對半分,總是保留一塊空著(上圖中的右側),將左側存活的對象(淺灰色區域)復制到右側,然后左側全部清空。避免了內存碎片問題,但是內存浪費很嚴重,相當于只能使用50%的內存。
(3) mark-compact 標記-整理(也稱標記-壓縮)法
避免了上述兩種算法的缺點,將垃圾對象清理掉后,同時將剩下的存活對象進行整理挪動(類似于windows的磁盤碎片整理),保證它們占用的空間連續,這樣就避免了內存碎片問題,但是整理過程也會降低GC的效率。
(4) generation-collect 分代收集算法
上述三種算法,每種都有各自的優缺點,都不完美。在現代JVM中,往往是綜合使用的,經過大量實際分析,發現內存中的對象,大致可以分為兩類:有些生命周期很短,比如一些局部變量/臨時對象,而另一些則會存活很久,典型的比如websocket長連接中的connection對象,如下圖:
縱向y軸可以理解分配內存的字節數,橫向x軸理解為隨著時間流逝(伴隨著GC),可以發現大部分對象其實相當短命,很少有對象能在GC后活下來。因此誕生了分代的思想,以Hotspot為例(JDK 7):
將內存分成了三大塊:年青代(Young Genaration),老年代(Old Generation),永久代(Permanent Generation),其中Young Genaration更是又細為分eden,S0,S1三個區。
結合我們經常使用的一些jvm調優參數后,一些參數能影響的各區域內存大小值,示意圖如下:
GC主要過程
下圖引自阿里出品的《碼出高效-Java開發手冊》一書,梳理了GC的主要過程。