Java與C++間有一堵由動態內存分配和垃圾收集技術所圍成的牆,外面的人想進來,裏面的人卻想出去。java
意義 目前動態內存分配和垃圾手記技術已經很成熟,一切彷佛已經進入自動化時代,爲何咱們還要去了解GC和動態內存分配呢?答案很簡單:當出現內存泄露、內存溢出問題時,當垃圾回收成爲系統達到更高併發量的瓶頸時,瞭解這些自動化技術就顯得頗有必要。算法
前章回顧 前章介紹了Java運行時內存的各個區域,其中程序計數器、虛擬機棧、本地方法棧都是隨線程而生,隨線程而滅,棧的棧幀隨方法的調用而入棧,隨方法的完成而出棧。每個棧幀中分配的內存大小在編譯期就明確可知,所以這幾個區域的內存分配和回收都具備肯定性,因此這幾個區域不須要過多考慮內存回收的問題,由於方法或線程結束時,內存也隨之跟着回收。而Java堆和方法區不同,由於只有程序處於運行期間才能知道會建立哪些對象,這部份內存的分配和回收是動態的。垃圾收集所關注的也是這部份內存,一下提到的內存都指這一部份內存。bash
給對象添加一個引用計數器,每當對象被引用時,計數器值加1,當引用失效時,計數器值減1,當計數器值爲0時,說明對象沒有被其餘地方引用,即對象已死。客觀地說,引用計數法(Reference Counting)的實現簡單,判斷效率也很高,可是,主流的Java虛擬機都沒有采用引用計數法來判斷對象是否已死,由於它有一個致命問題-沒法解決對象間相互引用的問題。
代碼展現:多線程
複製代碼
基本思路:經過一系列的稱爲「GC Roots」的對象做爲起始點,從這些起始點往下搜索,搜索走過的路徑稱爲引用鏈,當一個對象和「GC Roots」沒有任何引用鏈時(即GC Roots到這個對象是不可達的),說明對象是無用的。 併發
在Java中可做爲GC Roots的對象有下面幾種:可達性分析法中不可達的對象也不是非死不可的,而是處於緩刑階段。要宣告一個對象的死亡至少要通過兩次標記過程:當通過可達性分析後發現對象與GC Roots不可達,那麼它會被第一次標記而且進行一次刷選,刷選的條件是此兌對象是否有必要執行finalize方法。當對象沒有覆蓋finalize方法或對象的finalize方法已經被虛擬機執行過,這兩種狀況都會被視爲不須要執行finalize方法。
若是這個對象有必要執行finalize方法,那麼對象會被放在F-Queue的隊列中,而且會被由Java虛擬機自動建立的、低優先級的Finalizer線程去執行。finalize方法是對象最後一次逃脫死亡的機會,在finalize方法後,GC將會對對象進行第二次標記。若是對象在finalize方法中成功拯救本身,那麼在第二次標記時會被移出回收集合,不然就真的被回收了。
代碼展現:ide
package com.whut.java;
/**
* User: Chunguang Li
* Date: 2018/3/8
* Email: 1192126986@foxmail.com
*/
/**
* 代碼演示了兩點:
* 1. 對象能夠在GC時自救
* 2.自救的機會只有一次,由於一個對象的finalize方法只會被JVM調用一次
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC finalizeEscapeGC = null;
public void isAlive(){
System.out.println("i still alive...");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("execute finalize method...");
}
public static void main(String[] args) throws InterruptedException {
finalizeEscapeGC = new FinalizeEscapeGC();
finalizeEscapeGC = null;
// 顯示調用gc
System.gc();
// 第一次自救
// 由於Finalizer線程優先級很低,須要暫停0.5秒時間等待Finalizer線程執行對象的finalize方法
Thread.sleep(500);
if (finalizeEscapeGC != null){
finalizeEscapeGC.isAlive();
}else {
System.out.println("i am dead...");
}
finalizeEscapeGC = null;
System.gc();
// 自救失敗
Thread.sleep(500);
if (finalizeEscapeGC != null){
finalizeEscapeGC.isAlive();
}else {
System.out.println("i am dead...");
}
}
}
複製代碼
不少人認爲方法區(虛擬機中的永久代)是沒有垃圾回收的,Java虛擬機規範也確實說過不要求虛擬機在方法區實現圾回收,由於方法區的垃圾收集效率很低。
方法區的垃圾收集主要回收兩部份內容:廢棄常量和無用的類。高併發
回收廢棄常量 回收廢棄常量與回收Java堆中的對象相似,以常量池中的字面量的回收爲例:若是「abc」字符串存儲在常量池中,其餘地方沒有任何對象引用常量池中的「abc」常量,那麼進行垃圾回收時「abc」常量會被清理出常量池。常量池中的其餘類(接口)、方法、字段的符號引用也與此相似。性能
無用的類 判斷無用的類比廢棄常量條件苛刻得多。必須知足一下三個條件:spa
最基礎的收集算法。線程
工做原理 算法主要分爲兩個階段-標記和清除:首先標記出全部須要回收的對象,標記完成後統一進行清除。
缺點
爲解決效率問題,複製算法出現了:它將內存空間分爲大小相等的兩塊區域,每次只使用其中一塊,當進行垃圾收集時,將這塊區域中還存活的對象複製到另外一塊,而後將這一塊內存回收。這樣就不會產生內存碎片的問題。這種算法實現簡單,運行高效,只是代價是每次只能使用內存的一半,代價太高。
如今的商用虛擬機都採用這種收集算法回收新生代內存。根IBM公司的研究代表,新生代中的內存對象98%是朝生夕死的,因此不須要按照1:1的比例來劃份內存空間,而是將內存分爲一塊較大的Eden區域,兩塊較小的Survivor區域。每次只使用一塊Eden區域和一塊Survivor區域,當進行垃圾收集時,將Eden區域和Survivor區域仍然存活的對象複製到另外一塊Survivor區域,而後將Eden區域和使用過的Survivor區域清除。HotSpot虛擬機默認的Eden和Survivor區域大小比例爲8:1,這樣只會浪費10%的內存。
複製算法在對象成活率較低的新生代比較適用,而對於對象成活率較高的老年代就須要進行較多的複製操做,效率明顯會減低。因此針對老年代的特色,提出了標記-整理算法:標記清除過程仍然與標記清除算法同樣,只是在清除後將存活的對象都向一端移動。
當前商業的虛擬機的垃圾收集算法都採用分代收集算法:根據對象存活週期的不一樣將內存劃分爲幾塊,通常把Java堆分爲新生代和老年代,再根據各個年代的特色選擇合適的收集算法。
在新生代中,對象存活率低,適合使用複製算法,而老年代對象的存活率高,適合使用標記-清除算法或標記-整理算法。
收集算法是內存回收的方法論,那麼收集器就是收集算法的實現。
Serial收集器是最基本、最悠久的收集器。這個收集器是一個單線程收集器,在它進行垃圾收集時,必須停掉全部其餘的工做線程,而後以一條收集線程進行垃圾收集,直到收集工做結束,才能夠恢復其餘工做線程。這對於許多應用是難以接受的。可是對Client(客戶端)模式的虛擬機來講,Serial收集器是一個不錯的選擇,由於在桌面端應用,分配給虛擬機的內存不會太大,收集幾十兆到幾百兆的新生代內存停頓時間徹底能夠控制在幾十毫秒。
ParNew收集器其實就是Serial收集器的多線程版本,除了使用多協調線程進行垃圾收集外,其他的Serial收集器徹底同樣。ParNew收集器在單CPU或CPU數量少的環境中性能不會有比Serial收集器更好的結果,可是隨着CPU數量的增多,它GC時對CPU資源的的有效利用仍是頗有好處的,因此它是許多運行在Server模式下的虛擬機的首先新生代收集器。
它看上去彷佛與ParNew同樣,可是它的目標是達到一個可控制的吞吐量(吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + GC時間))。停頓時間越短,就越適合與用戶交互的程序,由於良好的響應時間能夠提升用戶的體驗,而吞吐量則能夠高效利用CPU時間儘快完成程序的計算任務,主要適合在後臺運算而須要交互任務。
Parallel Scavenge 收集器提供了兩個參數用於控制吞吐量:
Serial Old收集器是Serial收集器的老年代版本,一樣是一個單線程收集器,使用標記-整理算法。
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和標記-整理算法,主要配合Parallel Scavenge收集器組成「吞吐量優先」組合。
CMS(Concurrent Mark Sweep)是一款以獲取最短回收停頓時間爲目的的收集器。CMS很是適合B/S系統服務端的Java應用,由於這類應用尤爲注重服務的響應時間,但願系統的停頓時間越短。CMS是基於標記-清除算法的運做流程分爲4個部分: