垃圾回收
如何判斷對象爲垃圾對象
引用計數法
在對象中添加一個引用計數 器,當有地方引用這個對象的時候,
計數器+1,當失效的時候,計數器-1
引用計數法無法解決循環引用問題
證明沒有用引用計數法:
/**
* 循環引用
*/
public class RefCountGC {
public Object instance = null;
private byte[] bigSize = new byte[2*1024*1024];
public static void main(String[] args) {
RefCountGC obj1 = new RefCountGC();
RefCountGC obj2 = new RefCountGC();
obj1.instance = obj2;
obj2.instance = obj1;
//設置爲空
obj1 = null;
obj2 = null;
//垃圾回收
System.gc();
}
}
運行程序前設置參數:-verbose:gc -XX:+PrintGCDetails
表示開啓gc 打印gc信息
輸出結果:
從輸出日誌空可以看出程序進行了垃圾回收 而引用計數法堆循環引用的對象是不能回收的 所以虛擬機並不是用引用計數法來判斷對象是否存活的
可達性分析
作爲GCRoot的對象
• 虛擬機棧(局部變量表中的)
• 方法區的類屬性所引用的對象
• 方法區的常量所引用的對象
• 本地方法棧所引用的對象
如何回收
回收策略(方法論)
標記清除
分成標記和清除兩個階段
缺點
• 效率問題
• 內存碎片
複製算法
Minor GC會把Eden中的所有活的對象移動到Survivor區,如果Survivor區中放不下,剩下的活的對象被移動到Old區,收集後Eden爲空
當對象在Eden(包括一個Survivor區域)出生後,再經過一次Minor GC後,如果對象還存活,並且能夠被另外一塊Survivor區域所容納,則使用複製算法將這些存活的對象複製到另外一塊Survivor區中,然後清理所使用的Eden以及Survivor區域,並且將這些對象的年齡設置爲1,以後對象在Survivor區,每熬過一次Minor GC,對象年齡+1,當對象年齡到15歲,這些對象就成爲老年代
複製算法原理:
從根集合(GC Root)開始 通過Tracing 從From中找到存活對象 拷貝到to區 from和to交換身份 下次內存分配從to開始
eden區 from區 to區 默認8:1:1
優點 效率高 缺點浪費空間
標記整理
標記清除壓縮
分代算法
設計者一般至少會把Java堆劃分爲新生代(Young Generation)和老年代(Old Generation)兩個區域 。顧名思義,在新生代中,每次垃圾收集時都發現有大批對象死去,而每次回收後存活的少量對象,將會逐步晉升到老年代中存放
垃圾回收器(實踐)
jdk1.7 默認垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默認垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默認垃圾收集器G1
serial
單線程 stop the world。。 它進行垃圾回收的時候 世界都停了 地球都不轉了。。
parNew
ParNew收集器實質上是Serial收集器的多線程並行版本
Parallel Scavenge 收集器
- 採用複製算法 1.7默認新生代收集器
- 多線程收集器
- 達到可控制的吞吐量
- 吞吐量:CPU用於運行用戶代碼時間與CPU消耗總時間的比值
• 吞吐量=執行用戶代碼時間/(執行用戶代碼時間 + 垃圾回收使用的時間)
• -XX:MaxGCPauseMillis 垃圾收集停頓時間 並不是越短越好 越短 系統新生代會變小 頻繁垃圾回收 吞吐量下降
• -XX:GCTimeRatio 吞吐量大小 範圍(0,100) 垃圾收集時間佔總時間比
並行的垃圾收集器
減少停頓時間
CMS 收集器 (Concurrent Mark Sweep)
1.8 老年代默認垃圾收集器 CMS是一款基於**“標記-清除”算法**實現的收集器
工作過程四個步驟:
• 初始標記
• 併發標記
• 重新標記
• 併發清理
CMS收集過程
初始標記:這一步的作用是標記存活的對象,有兩部分:
- 標記老年代中所有的GC Roots對象,如下圖節點1;
- 標記年輕代中活着的對象引用到的老年代的對象,如下圖節點2、3;
併發標記:從“初始標記”階段標記的對象開始找出所有存活的對象;
預清理階段 :這個階段就是用來處理前一個階段因爲引用關係改變導致沒有標記到的存活對象的,它會掃描所有標記爲Direty的Card 如下圖所示,在併發標記階段,節點3的引用指向了6;則會把節點3的card標記爲Dirty;
預清理,也是用於標記老年代存活的對象,目的是爲了讓重新標記階段的StopTheWorld儘可能短
重新標記 :該階段的任務是完成標記整個年老代的所有的存活對象。
併發清理 : 這個階段主要是清除那些沒有標記的對象並且回收空間;
優點:併發收集 低停頓
缺點:
-
佔用CPU資源
-
無法處理浮動垃圾
在CMS的併發標記和併發清理階段,用戶線程是還在繼續運行的,程序在運行自然就還會伴隨有新的垃圾對象不斷產生,但這一部分
垃圾對象是出現在標記過程結束以後,CMS無法在當次收集中處理掉它們,只好留待下一次垃圾收集時再清理掉。這一部分垃圾就稱爲“浮動垃圾”。 -
出現Concurrent Mode Failure
要是CMS運行期間預留的內存無法滿足程序分配新對象的需要,就會出現一次“併發失敗”(Concurrent Mode Failure),這時候虛擬機將不得不啓動後備預案:凍結用戶線程的執行,臨時啓用Serial Old收集器來重新進行老年代的垃圾收集,但這樣停頓時間就很長了
-
空間碎片
CMS是一款基於“標記-清除”算法實現的收集器 因此有空間碎片產生
G1收集器
• 歷史
• 2004年 Sun公司實驗室發表了論文
• JDk7 才使用了G1
jdk9中默認使用 採用標記整理算法
運行示意圖:
優勢:
-
並行與併發
-
分代收集
-
空間整合
-
可預測的停頓
步驟:
- 初始標記(Initial Marking):僅僅只是標記一下GC Roots能直接關聯到的對象,並且修改TAMS
指針的值,讓下一階段用戶線程併發運行時,能正確地在可用的Region中分配新對象。這個階段需要
停頓線程,但耗時很短,而且是借用進行Minor GC的時候同步完成的,所以G1收集器在這個階段實際
並沒有額外的停頓。 - 併發標記(Concurrent Marking):從GC Root開始對堆中對象進行可達性分析,遞歸掃描整個堆
裏的對象圖,找出要回收的對象,這階段耗時較長,但可與用戶程序併發執行。當對象圖掃描完成以
後,還要重新處理SATB記錄下的在併發時有引用變動的對象。 - 最終標記(Final Marking):對用戶線程做另一個短暫的暫停,用於處理併發階段結束後仍遺留
下來的最後那少量的SATB記錄。 - 篩選回收(Live Data Counting and Evacuation):負責更新Region的統計數據,對各個Region的回
收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region
構成回收集,然後把決定回收的那一部分Region的存活對象複製到空的Region中,再清理掉整個舊
Region的全部空間。這裏的操作涉及存活對象的移動,是必須暫停用戶線程,由多條收集器線程並行
完成的。
G1內存模型:
G1分代模型:
G1 分區模型:
收集集合(CSet)代表每次GC暫停時回收的一系列目標分區
ZGC
ZGC原理
• ZGC在指針上做標記,在訪問指針時加入Load Barrier(讀屏障),比如當對象正被GC移動,指針上的顏色就會不對,這個屏障就會先把指針更新爲有效地址再返回,也就是,永遠只有單個對象讀取時有概率被減速,而不存在爲了保持應用與GC一致而粗暴整體的Stop The World。
• Colored Pointer(着色指針) 和 Load Barrier(併發執行的保證機制)
ZGC 內存結構
ZGC將堆劃分爲Region作爲清理,移動,以及並行GC線程工作分配的單位。分爲有2MB,32MB,N× 2MB 三種Size Groups,動態
地創建和銷燬Region,動態地決定Region的大小。
ZGC 回收過程
- Pause Mark Start -初始停頓標記
停頓JVM,標記Root對象,1,2,4三個被標爲live
- Concurrent Mark -併發標記
併發地遞歸標記其他對象,5和8也被標記爲live
- Relocate - 移動對象
對比發現3、6、7是過期對象,也就是中間的兩個灰色region需要被壓縮清理,所以陸續將4、5、8 對象移動到最右邊的新Region。移動過程中,有個forward table記錄這種轉向
- Remap - 修正指針
最後將指針更新指向新地址。
何時回收
到達安全點時。。
內存分配策略
優先分配Eden區
/**
* 內存分配策略之Eden區優先被分配
*/
public class EdenAllocator {
public static void main(String[] args) {
byte[] data = new byte[20*1024*1024];
}
}
運行程序前設置參數:-verbose:gc -XX:+PrintGCDetails
日誌信息:
大對象直接分配到老年代
-XX:PretenureSizeThreshold
/**
* 內存分配策略之大對象直接分配到老年代
* -verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:PretenureSizeThreshold=6M
*/
public class BigObjectIntoOldGen {
public static void main(String[] args) {
byte[] d1 = new byte[6*1024*1024];
}
}
日誌信息:
參數-Xms20M -Xmx20M -Xmn10M 表示java堆大小20M 不可擴展 其中10M分配給新生代 剩下10M爲老年代
而代碼中的對象6M 正好佔老年代60%的空間 正如日誌中打印的那樣。。
長期存活的對象分配老年代
-XX:MaxTenuringThreshold=15
經過15次gc仍然存活
空間分配擔保
-XX:+HandlePromotionFailure
檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小。
/**
* 內存分配策略之空間擔保
* -verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M
*/
public class SpaceGuarantee {
public static void main(String[] args) {
byte[] d1 = new byte[2*1024*1024];
byte[] d2 = new byte[2*1024*1024];
byte[] d3 = new byte[2*1024*1024];
byte[] d4 = new byte[4*1024*1024];
System.gc();
}
}
日誌信息:
總共20M 新生代10M eden區8M from區1M to區1M 老年代10M
當創建完d3後 eden區還剩2M 而接下來創建4M對象 不夠用 進行空間擔保 把新生代的對象移動到老年代 佔6M 也就是60% 創建4M對象
分配到 8M的eden區 佔50% 還有其他信息佔10% 因此日誌顯示60%
動態對象年齡對象
如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代
-XX:TargetSurvivorRatio
逃逸分析與棧上分配
逃逸分析:對象或變量有沒有超出方法的範圍
棧上分配:對沒有逃逸分析的對象進行在棧空間進行分配
/**
* 逃逸分析和棧上分配
*/
public class StackAllocation {
public StackAllocation obj;
/**
* 逃逸 return了
* @return
*/
public StackAllocation getInstance() {
return obj ==null?new StackAllocation():obj;
}
/**
* 逃逸 給到了成員變量
* @return
*/
public void setObj() {
this.obj = new StackAllocation();
}
/**
* 沒有逃逸 進行棧上分配
*/
public void useStackAllocation() {
StackAllocation stackAllocation = new StackAllocation();
}
}