筆記,深入理解java虛擬機
Java運行時內存區域
程序計數器,線程獨占,當前線程所執行的字節碼的行號指示器,每個線程需要記錄下執行到哪兒了,下次調度的時候可以繼續執行,這個區是唯一不會發生oom的
棧,線程獨占,包含虛擬機棧或native method stack,用於存放局部變量的
堆,線程共享,用於分布對象實例的,后面說的內存管理和垃圾回收基本都是針對堆的
方法區,線程共享,用於存放被虛擬機加載的類,常量,靜態變量; Java虛擬機規范,把方法區描述為堆的邏輯部分,所以也被稱為“永久代”,在大量使用反射,動態代理,ClassLoader的場景下,要考慮永久代的回收
對於一個簡單的,對象生成
Object obj = new Object();
會涉及到3個區,
圖中描述兩種對象訪問方式,有句柄,可以屏蔽對象實際地址的改變(gc時對象地址經常會改變),不用句柄效率更高
垃圾回收
垃圾判定
對於如何判定對象是垃圾,教科書的答案是引用計數,並且也在COM,python中得到較好的應用
引用計數的問題是,難以解決循環引用的問題,
A.instance = B; B.instance =A
這樣會導致A,B的引用計數都不為0
所以對於Java,C#,Lisp都是采用GC Roots Tracing的方式,
原理也很簡單,就是選取一系列GC Roots對象,只保留從root對象可達的對象,其余都是垃圾
Java中的GC roots包含,
- JavaStack中的引用的對象
- 方法區中靜態引用指向的對象
- 方法區中常量引用指向的對象
- Native方法中JNI引用的對象
垃圾回收算法
Mark-Sweep,最原始的想法,先標記出所有垃圾對象,然后回收掉;問題是,效率低,而且會產生大量內存碎片
Copying,最簡單的copying,把內存分兩半,先用一半,然后回收時,把有效對象copy到另外一半
這個首先只適用於年輕代,即垃圾對象占較高比例的case
再者,空間太浪費了,只能用一半
所以,現在實際使用的版本為,分為一個較大的eden區,兩個較小的survivor區
比例一般為8:1:1,其中一個survivor區是空的,這樣只浪費10%的空間
能這樣做的前提就是,每次只有最多10%的對象存活
對象先放到eden區,eden區滿了,進行minor gc,把eden區和有數據的survivor區的存活對象,放到另一個空的survivor區中
兩個survivor區,雖然命名為from和to,但是其實沒有任何區別,完全對等的
當如果survivor區的空間不夠,就需要放到年老代(稱為handle promotion,即年老代要為survivor提供擔保,你那空間不夠,可以放我這,類似貸款擔保),如果年老代的空間也不夠或不能接受Handle promotion失敗,就需要full gc去回收年老代
Mark-Compact,前面說copying只適用於存活對象比例較低的case,所以適合年輕代,但對於年老代這樣的,用copying肯定是不行的;
Mark-compact,mark還是一樣的,只是在回收時,會把對象做平移,消除碎片
垃圾收集器
Serial收集器
最簡單的,單線程,收集時會stop the world,所有用戶線程暫停
可以用於收集新生代或老年代
收集新生代,用copying算法
收集老年代,用標記-整理,稱為serial old
簡單高效,適用於單CPU環境;是虛擬機Client模式的默認收集器
缺點,會stop the world
ParNew收集器
只是serial的多線程版本,適用於多核環境,如果在單核的機器上,效率還不如serial
優勢是,可以配合CMS收集器使用,因為CMS作為老年代的收集器,只能配合Serial或ParNew作為新生代的收集器
CMS(Concurrent Mark Sweep)收集器
以最短停頓時間為目標的收集器,適合用於網站或B/S系統的服務端,重視服務響應速度的場景。
該收集器相對比較復雜,整個過程分為,
1. 初始標記(CMS initial mark),stop the world,標記GC Roots直接關聯到的對象,速度很快
2. 並發標記(CMS concurrenr mark), 並行進行GC Roots Tracing的過程
3. 重新標記(CMS remark),stop the world,由於並發標記和用戶線程是並發執行的,所以需要對標記進行最后的修正,消耗時間會大大小於並發標記時間
4. 並發清除(CMS concurrent sweep),最后進行sweep
缺點,
a. CPU敏感,頻繁GC會導致CPU卡死
b. 無法處理浮動垃圾(floating garbage),在並發清理階段產生的新垃圾無法在這次完成回收
c. 需要預留較大的內存,由於CMS收集過程是和用戶應用並發進行的,所以不能等到老年代快被占滿再做,需要提前進行收集,默認是設為68%,這是比較保守的設定,可以減少以降低gc的次數;但是如果在CMS收集過程中,出現用戶應用程序內存不夠的情況,會發生”Concurrent Mode Failure”,這樣虛擬機只能用后備方案,serial old收集器進行年老代的收集(因為應用已經無法並發執行),這樣應用停頓時間就會很長,所以需要設置合理的比例
d. 因為采用sweep,會有大量碎片
Parallel Scavenge收集器
新生代收集器,設計目的是達到可控制的throughput,CPU運行用戶代碼時間/CPU總消耗時間,說白了,就是保證CPU更多的執行用戶代碼而非gc
適合后台運算,不太需要交互的場景
但魚和熊掌不可兼得,throughput和GC停頓時間是需要tradeoff的
降低新生代的空間大小,縮小gc的間隔,都可以減少GC的停頓時間,但也會降低throughput
並且該收集器,支持UseAdaptiveSizePolicy參數, 這樣不需要使用者指定新生代大小,Eden和Survivor比例,年老代晉升年齡等,虛擬機會根據運行性能監控去優化調整
Parallel Old收集器
前面提到的parallel sacvenge收集器是無法與CMS收集器配合使用的,所以之前只能配合Serial Old來收集老年代,導致效率低
Parallel Old作為Serial Old的多線程版本,可以更好的和parallel sacvenge收集器配合,真正達到throughput優先的收集
G1(Garbage First)收集器
新一代的收集器,不同於之前的收集器,會收集整個新生代或老年代
G1會把整個Java堆划分為多個固定大小的區域,並跟蹤垃圾堆積程度,並每次收集垃圾最多的區域,可以大大提高收集效率
JVM收集器對應參數
內存分配和回收策略
對象往往在新生代的Eden區分配,Eden區空間不夠,發起minor gc,會將eden和一個survivor區的存活對象copy到另一個survivor區,如果另一個survivor區空間不夠,存入老年代
大對象會直接進入老年代,比如很長的字符串或很大的數組,大對象對於JVM內存分配是個壞消息,因為大對象需要找到連續內存,否則會觸發gc,所以短命的大對象是需要盡量避免的
長期存活的對象進入老年代,對象在新生代每經歷一次minor gc,年齡加1, 默認達到15歲會進入老年代
每次Minor GC時,虛擬機會檢測每次晉升到老年代的平均大小是否大於老年代當前剩余大小,如果小於,則進行full gc










