1. 程式人生 > >JVM效能優化系列-(2) 垃圾收集器與記憶體分配策略

JVM效能優化系列-(2) 垃圾收集器與記憶體分配策略

目前已經更新完《Java併發程式設計》和《Docker教程》,歡迎關注【後端精進之路】,輕鬆閱讀全部文章。

Java併發程式設計:

  • Java併發程式設計系列-(1) 併發程式設計基礎
  • Java併發程式設計系列-(2) 執行緒的併發工具類
  • Java併發程式設計系列-(3) 原子操作與CAS
  • Java併發程式設計系列-(4) 顯式鎖與AQS
  • Java併發程式設計系列-(5) Java併發容器
  • Java併發程式設計系列-(6) Java執行緒池
  • Java併發程式設計系列-(7) Java執行緒安全
  • Java併發程式設計系列-(8) JMM和底層實現原理
  • Java併發程式設計系列-(9) JDK 8/9/10中的併發

Docker教程:

  • Docker系列-(1) 原理與基本操作
  • Docker系列-(2) 映象製作與釋出
  • Docker系列-(3) Docker-compose使用與負載均衡

JVM效能優化:

  • JVM效能優化系列-(1) Java記憶體區域
  • JVM效能優化系列-(2) 垃圾收集器與記憶體分配策略

2. 垃圾收集器與記憶體分配策略

垃圾收集(Garbage Collection, GC)是JVM實現裡非常重要的一環,JVM成熟的記憶體動態分配與回收技術使Java(當然還有其他執行在JVM上的語言,如Scala等)程式設計師在提升開發效率上獲得了驚人的便利。理解GC,對於理解JVM和Java語言有著非常重要的作用。並且當我們需要排查各種記憶體溢位、記憶體洩漏問題時,當垃圾收集稱為系統達到更高併發量的瓶頸時,只有深入理解GC和記憶體分配,才能對這些“自動化”的技術實施必要的監控和調節。

GC主要需要解決以下三個問題:

  • 哪些記憶體需要回收?
  • 什麼時候回收?
  • 如何回收?

下面將對這些問題進行一一介紹。

2.1 如何判斷物件存活

在堆裡存放著Java世界中幾乎所有的物件例項,垃圾收集器在對堆進行回收前,首要的就是確定這些物件中哪些還“存活”著,哪些已經“死去”(即不可能再被任何途徑使用的物件)。

引用計數演算法

引用計數器判斷物件是否存活的過程是這樣的:給物件中新增一個引用計數器,每當有一個地方引用它時,計數器加1;當引用失效時,計數器減1;任何時刻計數器為0的物件就是不可能再被使用的。

引用計數演算法的實現簡單,判定效率也很高,大部分情況下是一個不錯的演算法。它沒有被JVM採用的原因是它很難解決物件之間迴圈引用的問題。

可達性分析演算法

在主流商用程式語言的實現中,都是通過可達性分析(tracing GC)來判定物件是否存活的。

演算法的基本思路是:通過一系列的稱為“GC Roots”的物件作為起點,從這些節點向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是GC Roots 到這個物件不可達)時,則證明此物件時不可用的。用下圖來加以說明:

作為GC Roots的物件包括下面幾種:

  • 虛擬機器棧(棧幀中的本地變量表)中引用的物件。
  • 方法區中類靜態屬性引用的物件。
  • 方法區中常量引用的物件。
  • 本地方法棧中JNI(即一般說的Native方法)引用的物件。

2.2 各種引用

強引用

一般的Object obj = new Object() ,就屬於強引用。被強引用關聯的物件不會被回收。

軟引用

一些有用但是並非必需,用軟引用關聯的物件,系統將要發生OOM之前,這些物件就會被回收。

下面的例子中,當程式發生OOM之前,嘗試去回收軟引用所關聯的物件,導致後面獲取到的值為null。

public class TestSoftRef {
    
    public static class User{
        public int id = 0;
        public String name = "";
        public User(int id, String name) {
            super();
            this.id = id;
            this.name = name;
        }
        @Override
        public String toString() {
            return "User [id=" + id + ", name=" + name + "]";
        }
        
    }
    
    public static void main(String[] args) {

        User u = new User(1,"Vincent");
        SoftReference<User> userSoft = new SoftReference<>(u);
        u = null;//保證new User(1,"Vincent")這個例項只有userSoft在軟引用
        
        System.out.println(userSoft.get());
        System.gc();//展示gc的時候,SoftReference不一定會被回收
        System.out.println("AfterGc");
        System.out.println(userSoft.get());//new User(1,"Vincent")沒有被回收
        List<byte[]> list = new LinkedList<>();
        
        try {
            for(int i=0;i<100;i++) {
                //User(1,"Vincent")例項一直存在
                System.out.println("********************"+userSoft.get());
                list.add(new byte[1024*1024*1]);
            }
        } catch (Throwable e) {
            //丟擲了OOM異常後列印的,User(1,"Vincent")這個例項被回收了
            System.out.println("Throwable********************"+userSoft.get());
        }
        
    }
}

程式輸出結果:

弱引用 WeakReference

一些有用(程度比軟引用更低)但是並非必需,用弱引用關聯的物件,只能生存到下一次垃圾回收之前,GC發生時,不管記憶體夠不夠,都會被回收。

下面的例子中,發生gc後,弱引用所關聯的物件被回收。

public class TestWeakRef {
    public static class User{
        public int id = 0;
        public String name = "";
        public User(int id, String name) {
            super();
            this.id = id;
            this.name = name;
        }
        @Override
        public String toString() {
            return "User [id=" + id + ", name=" + name + "]";
        }
        
    }
    
    public static void main(String[] args) {
        User u = new User(1,"Vincent");
        WeakReference<User> userWeak = new WeakReference<>(u);
        u = null;
        System.out.println(userWeak.get());
        System.gc();
        System.out.println("AfterGc");
        System.out.println(userWeak.get());
        
    }
}

輸出結果如下:

虛引用

又稱為幽靈引用或者幻影引用。一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用取得一個物件。

為一個物件設定虛引用關聯的唯一目的,就是能在這個物件被回收時收到一個系統通知。

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;

注意:軟引用 SoftReference和弱引用 WeakReference,可以用在記憶體資源緊張的情況下以及建立不是很重要的資料快取。當系統記憶體不足的時候,快取中的內容是可以被釋放的。
例如,一個程式用來處理使用者提供的圖片。如果將所有圖片讀入記憶體,這樣雖然可以很快的開啟圖片,但記憶體空間使用巨大,一些使用較少的圖片浪費記憶體空間,需要手動從記憶體中移除。如果每次開啟圖片都從磁碟檔案中讀取到記憶體再顯示出來,雖然記憶體佔用較少,但一些經常使用的圖片每次開啟都要訪問磁碟,代價巨大。這個時候就可以用軟引用構建快取。

2.3 方法區回收

很多人認為方法區沒有垃圾回收,Java虛擬機器規範中確實說過不要求,而且在方法區中進行垃圾收集的“價效比”較低:在堆中,尤其是新生代,常規應用進行一次垃圾收集可以回收70%~95%的空間,而方法區的效率遠低於此。在JDK 1.8中,JVM摒棄了永久代,用元空間來作為方法區的實現,下面介紹的將是元空間的垃圾回收。

元空間的記憶體管理由元空間虛擬機器來完成。先前,對於類的元資料我們需要不同的垃圾回收器進行處理,現在只需要執行元空間虛擬機器的C++程式碼即可完成。在元空間中,類和其元資料的生命週期和其對應的類載入器是相同的。

話句話說,只要類載入器存活,其載入的類的元資料也是存活的,因而不會被回收掉。當一個類載入器被垃圾回收器標記為不再存活,其對應的元空間會被回收。

2.4 垃圾收集演算法

標記-清除演算法(Mark-Sweep)

演算法分成“標記”、“清除”兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件。

演算法的執行過程如下圖所示:

標記-清除演算法的不足主要有以下兩點:

  • 空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不觸發另一次垃圾收集動作。

  • 效率問題,因為記憶體碎片的存在,操作會變得更加費時,因為查詢下一個可用空閒塊已不再是一個簡單操作。

複製演算法(Copying)

將可用記憶體按容量分成大小相等的兩塊,每次只使用其中的一塊。當這一塊記憶體用完,就將還存活著的物件複製到另一塊上面,然後再把已使用過的記憶體空間一次清理掉。

這樣做使得每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可,實現簡單,執行高效。只是這種演算法的代價是將記憶體縮小為原來的一半。 複製演算法的執行過程如下圖所示:

標記-整理演算法(Mark-Compact)

根據老年代的特點,標記-整理(Mark-Compact)演算法被提出來,主要思想為:此演算法的標記過程與標記-清除演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉邊界以外的記憶體。具體示意圖如下所示:

分代收集演算法(Generational Collection)

當前商業虛擬機器的垃圾收集都採用分代收集(Generational Collection)演算法,此演算法相較於前幾種沒有什麼新的特徵,主要思想為:根據物件存活週期的不同將記憶體劃分為幾塊,一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點採用最適合的收集演算法:

  • 新生代

在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,那就選用複製演算法,只需要付出少量存活物件的複製成本就可以完成收集。

  • 老年代

在老年代中,因為物件存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清除”或“標記-整理”演算法來進行回收。

Minor GC與複製演算法

現在的商業虛擬機器都使用複製演算法來回收新生代。新生代的GC又叫“Minor GC”,IBM公司的專門研究表明:新生代中的物件98%是“朝生夕死”的,所以Minor GC非常頻繁,一般回收速度也比較快,同時“朝生夕死”的特性也使得Minor GC使用複製演算法時不需要按照1:1的比例來劃分新生代記憶體空間。

  • Minor GC過程

事實上,新生代將記憶體分為一塊較大的Eden空間兩塊較小的Survivor空間(From Survivor和To Survivor),每次Minor GC都使用Eden和From Survivor,當回收時,將Eden和From Survivor中還存活著的物件都一次性地複製到另外一塊To Survivor空間上,最後清理掉Eden和剛使用的Survivor空間。一次Minor GC結束的時候,Eden空間和From Survivor空間都是空的,而To Survivor空間裡面儲存著存活的物件。在下次MinorGC的時候,兩個Survivor空間交換他們的標籤,現在是空的“From” Survivor標記成為“To”,“To” Survivor標記為“From”。因此,在MinorGC結束的時候,Eden空間是空的,兩個Survivor空間中的一個是空的,而另一個儲存著存活的物件。

HotSpot虛擬機器預設的Eden : Survivor的比例是8 : 1,由於一共有兩塊Survivor,所以每次新生代中可用記憶體空間為整個新生代容量的90%(80%+10%),只有10%的容量會被“浪費”。

  • 分配擔保

上文說的98%的物件可回收只是一般場景下的資料,我們沒有辦法保證每次回收都只有不多於10%的物件存活,當Survivor空間不夠用時,需要依賴老年代記憶體進行分配擔保(Handle Promotion)。如果另外一塊Survivor上沒有足夠空間存放上一次新生代收集下來的存活物件,這些物件將直接通過分配擔保機制進入老年代。

2.5 HotSpot的演算法實現

列舉根節點

  • GC鏈逐個檢查引用,會消耗比較多時間
  • GC停頓,為了保持“一致性”,需要“Stop the world”
  • HotSpot使用一組稱為OopMap的資料結構來記錄哪些地方存著物件的引用。在類載入過程中,HotSpot就把物件內什麼偏移量上是什麼型別的資料計算出來,在JIT編譯過程中會在特定的位置記錄下棧和暫存器中哪些位置是引用。

安全點

HotSpot沒有為每條指令都生成OopMap,只是在特定位置記錄了這些資訊,這些位置稱為安全點。程式執行時並非在所有地方都能停頓下來開始GC,只有在到達安全點時才能暫停。

安全區域

安全區域是指在一段程式碼片段之中,引用關係不會發生變化,在這個區域內的任何地方進行GC都是安全的。可以看成是擴充套件的安全點。

2.6 垃圾收集器

目前為止並沒有一個最好的收集器,也沒有萬能的收集器,通常是根據具體情況選擇合適的收集器。

接下來要介紹的收集器如下圖所示,7種收集器分別作用於不同的區域,如果兩個收集器之間存在連線,就說明可以搭配使用。虛擬機器所處的位置,代表是屬於新生代收集器還是老年代收集器。

基本概念

1. 並行與併發

  • 並行(Parallel):指多條垃圾收集執行緒並行工作,但此時使用者執行緒仍然處於等待狀態。

  • 併發(Concurrent):指使用者執行緒與垃圾收集執行緒同時執行(但不一定是並行的,可能會交替執行),使用者程式在繼續執行。而垃圾收集程式執行在另一個CPU上。

2. 吞吐量(Throughput)

吞吐量就是CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值,即

吞吐量 = 執行使用者程式碼時間 /(執行使用者程式碼時間 + 垃圾收集時間)

假設虛擬機器總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。

3. Minor GC 和 Full GC

新生代GC(Minor GC):指發生在新生代的垃圾收集動作,因為Java物件大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。

老年代GC(Major GC / Full GC):指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略裡就有直接進行Major GC的策略選擇過程)。Major GC的速度一般會比Minor GC慢10倍以上。

Serial/Serial Old 收集器

Serial是一個“單執行緒”的新生代收集器,使用複製演算法,它只會使用一個CPU或者一條收集器執行緒去完成垃圾收集工作,並且它在垃圾收集時,必須暫停所有其他的工作執行緒,直到它收集結束。“Stop The World”會在使用者不可見的情況下,把使用者的工作執行緒全部停掉。

Serial Old是Serial收集器的老年代版本,同樣是一個“單執行緒”收集器,使用標記-整理演算法。這個收集器主要是給Client模式下的虛擬機器使用,Server模式下還有兩個用途,一個是在JDK1.5及之前的版本中與Parallel Scavenge收集器搭配使用,另一個是作為CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。

下圖是 Serial/Serial Old 收集器執行示意圖:

上圖中,新生代是Serial收集器採用複製演算法老年代是Serial Old收集器採用標記-整理演算法。Serial雖然是一個缺點鮮明的收集器,但它依然是虛擬機器在Client模式下的預設收集器,它也有優點,比如簡單高效(與其他收集器單執行緒相比),對於單個CPU來說,Serial由於沒有執行緒互動的開銷,效率比較高

ParNew 收集器

ParNew收集器是Serial收集器的多執行緒版本,也是使用複製演算法的新生代收集器,它除了使用多條執行緒進行垃圾收集以外,其他的比如收集器的控制引數、收集演算法、Stop-The-World、物件分配規則、回收策略都和Serial收集器完全一樣。

下圖是 ParNew/Serial Old 收集器執行示意圖:

上圖中,新生代是ParNew收集器採用複製演算法,老年代是Serial Old收集器採用標記-整理演算法。ParNew是許多Server模式下虛擬機器的首選新生代收集器,因為它能與CMS收集器配合工作。CMS收集器是HotSpot虛擬機器中第一個併發的垃圾收集器,CMS第一次實現了讓使用者執行緒與垃圾收集執行緒同時工作。

Parallel Scavenge(ParallerGC)/ Parallel Old 收集器

Parallel Scavenge也是使用複製演算法的新生代收集器,並且也是一個並行的多執行緒收集器。Parallel收集器跟其它收集器關注GC停頓時間不同,它關注的是吞吐量。低停頓時間適合需要與使用者互動的程式,而高吞吐量可以高效率的利用CPU時間,能儘快完成運算任務,適合用於後臺計算較多而互動較少的任務。

Parallel收集器提供了兩個虛擬機器引數用以控制吞吐量,-XX:MaxGCPauseMillis引數可以控制垃圾收集的最大停頓時間,-XX:GCTimeRatio引數可以直接設定吞吐量大小。

-XX:MaxGCPauseMillis的值是一個大於0的毫秒數,使用它減小GC停頓時間是犧牲吞吐量和新生代空間換來的,例如系統把新生代調小,收集300M的新生代肯定比500M的快,這也導致垃圾收集發生的更頻繁,原來10秒收集一次每次停頓100毫秒,現在5秒收集一次每次停頓70毫秒,停頓時間下降了,但是吞吐量也下降了。

-XX:GCTimeRatio的值是一個0到100的整數,通過它我們告訴JVM吞吐量要達到的目標值,-XX:GCTimeRatio=N指定目標應用程式執行緒的執行時間(與總的程式執行時間)達到N/(N+1)的目標比值。例如,它的預設值是99,就是說要求應用程式執行緒在整個執行時間中至少99/100是活動的(GC執行緒佔用其餘的1/100),也就是說,應用程式執行緒應該執行至少99%的總執行時間。

除這兩個引數外,還有一個引數-XX:-UseAdaptiveSizePolicy值得關注,這是一個開關引數,當它開啟之後,就不需要手工指定新生代大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代物件年齡(-XX:PretenureSizeThreshold)等細節引數了,虛擬機器會根據系統的執行情況收集效能監控資訊,動態的調整這些引數來提高GC效能,這種調節方式稱為GC自適應調節策略。這個引數是預設啟用的,自適應行為也是JVM優勢之一。

Parallel Old收集器是Parallel Scavenge的老年代版本,使用多執行緒和標記-整理演算法。此收集器在JDK1.6中開始出現,在Parallel Old出現之前,只有Serial Old能夠與Parallel Scavenge收集器配合使用。由於Serial Old這種單執行緒收集器的效能拖累,導致在老年代比較大的場景下,Parallel Scavenge和Serial Old的組合吞吐量甚至還不如ParNew加CMS的組合。而有了Parallel Old收集器之後,Parallel Scavenge與Parallel Old成了名副其實的吞吐量優先的組合,在注重吞吐量和CPU資源敏感的場景下,都可以優先考慮這對組合。

下圖是 Parallel Scavenge(ParallerGC)/ Parallel Old 收集器執行示意圖:

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是基於標記-清除演算法老年代收集器,它以獲取最短回收停頓時間為目標。CMS是一款優秀的收集器,特點是併發收集、低停頓,它的執行過程稍微複雜些,分為4個步驟:

  1. 初始標記(CMS initial mark)
  2. 併發標記(CMS concurrent mark)
  3. 重新標記(CMS remark)
  4. 併發清除(CMS concurrent sweep)

4個步驟中只有初始標記、重新標記這兩步需要“Stop The World”。初始標記只是標記一下GC Roots能直接關聯的物件,速度很快。併發標記是進行GC Roots Tracing的過程,也就是從GC Roots開始進行可達性分析。重新標記則是為了修正併發標記期間因使用者執行緒繼續執行而導致標記發生變動的那一部分記錄。併發清理當然就是進行清理被標記物件的工作。

下圖是 CMS 收集器執行示意圖:

整個過程中,併發標記與併發清除過程耗時最長,但它們都可以與使用者執行緒一起工作,所以整體上說,CMS收集器的記憶體回收過程是與使用者執行緒一起併發執行的。

但是CMS收集器也並不完美,它有以下3個缺點:

  1. CMS收集時對CPU資源非常敏感,併發階段雖然不會導致使用者執行緒停頓,但是會因為佔用CPU資源導致應用程式變慢、總吞吐量變低。
  2. CMS收集器無法處理浮動垃圾(Floating Garbage),可能會產生Full GC。浮動垃圾就是在併發清理階段,依然在執行的使用者執行緒產生的垃圾。這部分垃圾出現在標記過程之後,CMS無法在當次集中處理它們,只能等下一次GC時清理。
  3. CMS是基於標記-清除演算法的收集器,可能會產生大量的空間碎片,從而無法分配大物件而導致Full GC提前產生。

由於存在浮動垃圾,以及使用者執行緒正在執行,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供併發收集時的程式運作使用。可以使用-XX:CMSInitialOccupyFraction引數調整預設CMS收集器的啟動閾值。要是CMS執行期間預留的記憶體無法滿足程式需要,就會出現一次“Concurrent Mode Failure”失敗,這時虛擬機器將啟動後備預案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。所以說引數-XX:CMSInitiatingOccupancyFraction設定得太高很容易導致大量“Concurrent Mode Failure”失敗,效能反而降低。
-XX:+UseCMSCompactAtFullCollection用於在CMS收集器頂不住要進行FullGC時開啟記憶體碎片的合併整理過程,記憶體整理的過程是無法併發的,空間碎片問題沒有了,但停頓時間不得不變長。-XX:CMSFullGCsBeforeCompaction,這個引數是用於設定執行多少次不壓縮的Full GC後,跟著來一次帶壓縮的(預設值為0,表示每次進入FullGC時都進行碎片整理)。

G1收集器

G1(Garbage-First)收集器是面向服務端應用的垃圾收集器,它被寄予厚望以用來替換CMS收集器。在G1之前的收集器中,收集的範圍要麼是整個新生代要麼就是老年代,而G1不再從物理上區分新生代老年代,G1可以獨立管理整個Java堆。它將Java堆劃分為多個大小相等的獨立區域(Region),雖然還有新生代老年代的概念,但不再是物理隔離的,而都是一部分Region(不需要連續)的集合。

與其他收集器相比,G1收集器的特點有:

  • 並行與併發:G1能充分利用多CPU或者多核心的CPU,來縮短Stop The World的停頓時間。
  • 分代收集:雖然G1收集器可以獨立管理整個GC堆,但它能採用不同的方式處理“新物件”和“老物件”,以達到更好的收集效果。
  • 空間整合:G1從整體看是基於標記-整理演算法的,從區域性看(兩個Region之間)是基於複製演算法實現的,這兩個演算法在收集時都不會產生空間碎片,這樣就有連續可用的記憶體用以分配大物件。
  • 可預測的停頓:G1除了追求低停頓外,還能建立可預測的停頓時間模型,可以明確指定一個最大停頓時間(-XX:MaxGCPauseMillis),停頓時間需要不斷調優找到一個理想值,過大過小都會拖慢效能。

G1收集器之所以能建立可預測的停頓時間模型,是因為它可以避免在整個Java堆中進行全區域的垃圾收集,G1根據各個Region裡垃圾堆積的價值大小(回收所獲空間大小及所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region,這也是Garbage-First名稱的由來。

G1收集器的Region如下圖所示:

圖中的E代表是Eden區,S代表Survivor,O代表Old區,H代表humongous表示巨型物件(大於Region空間的物件)。從圖中可以看出各個區域邏輯上並不是連續的,並且一個Region在某一個時刻是Eden,在另一個時刻就可能屬於老年代。G1在進行垃圾清理的時候就是將一個Region的物件拷貝到另外一個Region中。

避免全堆掃描:G1中引入了Remembered Set(記憶集)。每個Region中都有一個Remembered Set,記錄的是其他Region中的物件引用本Region物件的關係(誰引用了我的物件)。所以在垃圾回收時,在GC根節點的列舉範圍中加入Remembered Set即可保證不對全堆掃描也不會有遺漏。G1裡面還有另外一種資料結構叫Collection Set,Collection Set記錄的是GC要收集的Region的集合,Collection Set裡的Region可以是任意代的。在GC的時候,對於跨代物件引用,只要掃描對應的Collection Set中的Remembered Set即可。

G1收集器的收集過程如下圖所示:

如圖所示,G1收集過程有如下幾個階段:

  • 初始標記(Initial Marking):標記一下GC Roots能關聯到的物件,需要停頓執行緒但是耗時短,會停頓使用者執行緒(Stop the World)
  • 併發標記(Concurrent Marking):從GC Root開始對堆中物件進行可達性分析,找出存活物件,這階段耗時長但是可以與使用者執行緒併發執行。
  • 最終標記(Final Marking):修正在併發標記階段,因使用者執行緒繼續執行而導致標記產生變動的那一部分標記記錄,這階段需要停頓使用者執行緒(Stop the World),但是可並行執行。
  • 篩選回收(Live Data Counting and Evacuation):會對各個Region的回收價值和成本進行排序,根據使用者期望的GC停頓時間來制定回收計劃,該階段也是會停頓使用者執行緒(Stop the World)。

以下是對所有垃圾收集器的總結:

常用的垃圾收集器引數

以下是JVM中常用的垃圾收集器引數:

VM引數 描述
-XX:+UseSerialGC 指定Serial收集器+Serial Old收集器組合執行記憶體回收
-XX:+UseParNewGC 指定ParNew收集器+Serilal Old組合執行記憶體回收
-XX:+UseParallelGC 指定Parallel收集器+Serial Old收集器組合執行記憶體回收
-XX:+UseParallelOldGC 指定Parallel收集器+Parallel Old收集器組合執行記憶體回收
-XX:+UseConcMarkSweepGC 指定CMS收集器+ParNew收集器+Serial Old收集器組合執行記憶體回收。優先使用ParNew收集器+CMS收集器的組合,當出現ConcurrentMode Fail或者Promotion Failed時,則採用ParNew收集器+Serial Old收集器的組合
-XX:+UseG1GC 指定G1收集器併發、並行執行記憶體回收
-XX:+PrintGCDetails 列印GC詳細資訊
-XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準時間的形式)
-XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式)
-XX:+PrintHeapAtGC 在進行GC的前後打印出堆的資訊
-XX:+PrintTenuringDistribution 在進行GC時列印survivor中的物件年齡分佈資訊
-Xloggc:$CATALINA_HOME/logs/gc.log 指定輸出路徑收集日誌到日誌檔案
-XX:NewRatio 新生代與老年代(new/old generation)的大小比例(Ratio). 預設值為 2
-XX:SurvivorRatio eden/survivor 空間大小的比例(Ratio). 預設值為 8
-XX:GCTimeRatio GC時間佔總時間的比率,預設值99%,僅在Parallel Scavenge收集器時生效
-XX:MaxGCPauseMills 設定GC最大停頓時間,僅在Parallel Scavenge收集器時生效
-XX:PretensureSizeThreshold 直接晉升到老年代的物件大小,大於這個引數的物件直接在老年代分配
-XX:MaxTenuringThreshold 提升老年代的最大臨界值(tenuring threshold). 預設值為 15
-XX:UseAdaptiveSizePolicy 動態調整Java堆中各個區域的大小及進入老年代的年齡
-XX:HandlePromotionFailure 是否允許分配擔保失敗,即老年代的剩餘空間不足以應付新生代整個Eden和Survivor中物件都存活的極端情況
-XX:ParallelGCThreads 設定垃圾收集器在並行階段使用的執行緒數,預設值隨JVM執行的平臺不同而不同
-XX:ParallelCMSThreads 設定CMS的執行緒數量
-XX:ConcGCThreads 併發垃圾收集器使用的執行緒數量. 預設值隨JVM執行的平臺不同而不同
-XX:CMSInitiatingOccupancyFraction 設定CMS收集器在老年代空間被使用多少後觸發垃圾收集,預設68%
-XX:+UseCMSCompactAtFullCollection 設定CMS收集器在完成垃圾收集後是否要進行一次記憶體碎片的整理
-XX:CMSFullGCsBeforeCompaction 設定進行多少次CMS垃圾回收後,進行一次記憶體壓縮
-XX:+CMSClassUnloadingEnabled 允許對類元資料進行回收
-XX:CMSInitiatingPermOccupancyFraction 當永久區佔用率達到這一百分比時,啟動CMS回收
-XX:UseCMSInitiatingOccupancyOnly 表示只在到達閥值的時候,才進行CMS回收
-XX:InitiatingHeapOccupancyPercent 指定當整個堆使用率達到多少時,觸發併發標記週期的執行,預設值是45%
-XX:G1HeapWastePercent 併發標記結束後,會知道有多少空間會被回收,再每次YGC和發生MixedGC之前,會檢查垃圾佔比是否達到此引數,達到了才會發生MixedGC
-XX:G1ReservePercent 設定堆記憶體保留為假天花板的總量,以降低提升失敗的可能性. 預設值是 10
-XX:G1HeapRegionSize 使用G1時Java堆會被分為大小統一的的區(region)。此引數可以指定每個heap區的大小. 預設值將根據 heap size 算出最優解. 最小值為 1Mb, 最大值為 32Mb

2.7 記憶體分配策略

物件優先在Eden區分配

大多數情況下,物件在新生代的Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機器將發起一次Minor GC。

大物件直接進入老年代

所謂的大物件是指,需要大量連續記憶體空間的Java物件,最典型的大物件就是很長的字串以及陣列。大物件對虛擬機器的記憶體分配來說是一個壞訊息,經常出現大物件容易導致記憶體還有不少空間時,就提前觸發GC以獲取足夠的連續空間來安置它們。

虛擬機器提供了一個-XX:PretenureSizeThreshold引數,令大於這個設定值的物件直接在老年代分配。這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的記憶體複製。預設為0,表示絕不會直接分配在老年代。

長期存活的物件將進入老年代

虛擬機器給每個物件定義了一個物件年齡(Age)計數器。如果物件在Eden出生,並經過第一次Minor GC後仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並且物件年齡設為1。物件在Survivor區中每“熬過”一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(預設為15歲),就將會被晉升到老年代中。物件晉升老年代的年齡閾值,可以通過引數-XX:MaxTenuringThreshold設定。

動態物件年齡判定

為了能更好地適應不同程式的記憶體狀況,虛擬機器並不是永遠地要求物件的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

空間分配擔保

新生代中有大量的物件存活,survivor空間不夠,當出現大量物件在MinorGC後仍然存活的情況(最極端的情況就是記憶體回收後新生代中所有物件都存活),就需要老年代進行分配擔保,把Survivor無法容納的物件直接進入老年代.只要老年代的連續空間大於新生代物件的總大小或者歷次晉升的平均大小,就進行Minor GC,否則FullGC。

2.8 Full GC的觸發條件

對於Minor GC,其觸發條件非常簡單,當Eden區空間滿時,就將觸發一次Minor GC。而Full GC則相對複雜,因此本節我們主要介紹Full GC的觸發條件。

  • 呼叫System.gc()

此方法的呼叫是建議JVM進行Full GC,雖然只是建議而非一定,但很多情況下它會觸發 Full GC,從而增加Full GC的頻率,也即增加了間歇性停頓的次數。因此強烈建議能不使用此方法就不要使用,讓虛擬機器自己去管理它的記憶體,可通過-XX:+ DisableExplicitGC來禁止RMI呼叫System.gc()。

  • 老年代空間不足

老年代空間不足的常見場景為前文所講的大物件直接進入老年代、長期存活的物件進入老年代等,當執行Full GC後空間仍然不足,則丟擲如下錯誤:
Java.lang.OutOfMemoryError: Java heap space
為避免以上兩種狀況引起的Full GC,調優時應儘量做到讓物件在Minor GC階段被回收、讓物件在新生代多存活一段時間及不要建立過大的物件及陣列。

  • 空間分配擔保失敗

前文介紹過,使用複製演算法的Minor GC需要老年代的記憶體空間作擔保,如果出現了HandlePromotionFailure擔保失敗,則會觸發Full GC。

  • JDK 1.7及以前的永久代空間不足

在JDK 1.7及以前,HotSpot虛擬機器中的方法區是用永久代實現的,永久代中存放的為一些class的資訊、常量、靜態變數等資料,當系統中要載入的類、反射的類和呼叫的方法較多時,Permanet Generation可能會被佔滿,在未配置為採用CMS GC的情況下也會執行Full GC。如果經過Full GC仍然回收不了,那麼JVM會丟擲如下錯誤資訊:
java.lang.OutOfMemoryError: PermGen space
為避免PermGen佔滿造成Full GC現象,可採用的方法為增大PermGen空間或轉為使用CMS GC。

在JDK 1.8中用元空間替換了永久代作為方法區的實現,元空間是本地記憶體,因此減少了一種Full GC觸發的可能性。

  • Concurrent Mode Failure

執行CMS GC的過程中同時有物件要放入老年代,而此時老年代空間不足(有時候“空間不足”是CMS GC時當前的浮動垃圾過多導致暫時性的空間不足觸發Full GC),便會報Concurrent Mode Failure錯誤,並觸發Full GC。

2.9 新生代配置實戰

關於新生代的配置,主要有下面三種引數:

-XX:NewSize/MaxNewSize : 新生代的size和最大size,該引數優先順序最高。
-Xmn(可以看成NewSize= MaxNewSize):新生代的大小,該引數優先順序次高。
-XX:NewRatio: 表示比例,例如=2,表示 新生代:老年代 = 1:2,該引數優先順序最低。

還有引數:-XX:SurvivorRatio 表示Eden和Survivor的比值,預設為8,表示 Eden:FromSurvivor:ToSurvivor= 8:1:1

下面舉例引數配置進行實戰,程式中生成了10個大小為1M的陣列,

public class NewSize {

    public static void main(String[] args) {
        int cap = 1*1024*1024;//1M
        byte[] b1 = new byte[cap];
        byte[] b2 = new byte[cap];
        byte[] b3 = new byte[cap];
        byte[] b4 = new byte[cap];
        byte[] b5 = new byte[cap];
        byte[] b6 = new byte[cap];
        byte[] b7 = new byte[cap];
        byte[] b8 = new byte[cap];
        byte[] b9 = new byte[cap];
        byte[] b0 = new byte[cap];
    }
}
  1. -Xms20M -Xmx20M -XX:+PrintGCDetails –Xmn2m -XX:SurvivorRatio=2

沒有垃圾回收,陣列都在老年代。

  1. -Xms20M -Xmx20M -XX:+PrintGCDetails -Xmn7m -XX:SurvivorRatio=2

發生了垃圾回收,新生代存了部分陣列,老年代也儲存了部分陣列,發生了晉升現象。

  1. -Xms20M -Xmx20M -XX:+PrintGCDetails -Xmn15m -XX:SurvivorRatio=8

新生代可以放下所有的陣列,老年代沒放。

  1. -Xms20M -Xmx20M -XX:+PrintGCDetails -XX:NewRatio=2

發生了垃圾回收,出現了空間分配擔保,而且發生了FullGC。

2.10 記憶體洩漏和記憶體溢位

  • 記憶體溢位:實實在在的記憶體空間不足導致;

  • 記憶體洩漏:該釋放的物件沒有釋放,多見於自己使用容器儲存元素的情況下。

下面舉例說明,例子中實現了一個基本的棧,注意看出棧的部分,為了幫助GC,當出棧完成後,手動將棧頂的引用清空,有助於後續元素的gc。這裡如果不清空,當元素出棧後,棧頂原來的位置還有該元素的引用,所以可能造成無法對已經出棧的元素進行回收,造成記憶體洩露。

public class Stack {
    
    public  Object[] elements;
    private int size = 0;//指示器,指示當前棧頂的位置

    private static final int Cap = 16;

    public Stack() {
        elements = new Object[Cap];
    }

    //入棧
    public void push(Object e){
        elements[size] = e;
        size++;
    }

    //出棧
    public Object pop(){
        size = size-1;
        Object o = elements[size];
        elements[size] = null;//help gc
        return o;
    }
    
    public static void main(String[] args) {
        Stack stack = new Stack();
        Object o = new Object();
        System.out.println("o="+o);
        stack.push(o);
        Object o1 =  stack.pop();
        System.out.println("o1="+o1);
        
        System.out.println(stack.elements[0]);
    }
}

2.11 淺堆和深堆

淺堆 :(Shallow Heap)是指一個物件所消耗的記憶體。例如,在32位系統中,一個物件引用會佔據4個位元組,一個int型別會佔據4個位元組,long型變數會佔據8個位元組,每個物件頭需要佔用8個位元組。

深堆 :這個物件被GC回收後,可以真實釋放的記憶體大小,也就是隻能通過物件被直接或間接訪問到的所有物件的集合。通俗地說,就是指僅被物件所持有的物件的集合。

舉例:物件A引用了C和D,物件B引用了C和E。那麼物件A的淺堆大小隻是A本身,不含C和D,而A的實際大小為A、C、D三者之和。而A的深堆大小為A與D之和,由於物件C還可以通過物件B訪問到,因此不在物件A的深堆範圍內。

2.12 jdk工具

jps

列出當前機器上正在執行的虛擬機器程序
-p:僅僅顯示VM 標示,不顯示jar,class, main引數等資訊.
-m:輸出主函式傳入的引數. 下的hello 就是在執行程式時從命令列輸入的引數
-l: 輸出應用程式主類完整package名稱或jar完整名稱.
-v: 列出jvm引數, -Xms20m -Xmx50m是啟動程式指定的jvm引數

jstat

是用於監視虛擬機器各種執行狀態資訊的命令列工具。它可以顯示本地或者遠端虛擬機器程序中的類裝載、記憶體、垃圾收集、JIT編譯等執行資料,在沒有GUI圖形介面,只提供了純文字控制檯環境的伺服器上,它將是執行期定位虛擬機器效能問題的首選工具。

假設需要每250毫秒查詢一次程序2764垃圾收集狀況,一共查詢20次,那命令應當是:jstat-gc 2764 250 20

常用引數:
-class (類載入器)
-compiler (JIT)
-gc (GC堆狀態)
-gccapacity (各區大小)
-gccause (最近一次GC統計和原因)
-gcnew (新區統計)
-gcnewcapacity (新區大小)
-gcold (老區統計)
-gcoldcapacity (老區大小)
-gcpermcapacity (永久區大小)
-gcutil (GC統計彙總)
-printcompilation (HotSpot編譯統計)

jinfo

檢視和修改虛擬機器的引數jinfo –sysprops 可以檢視由System.getProperties()取得的引數
jinfo –flag 未被顯式指定的引數的系統預設值
jinfo –flags(注意s)顯示虛擬機器的引數
jinfo –flag +[引數] 可以增加引數,但是僅限於由java -XX:+PrintFlagsFinal –version查詢出來且為manageable的引數
jinfo –flag -[引數] 可以去除引數
Thread.getAllStackTraces();

jmap

用於生成堆轉儲快照(一般稱為heapdump或dump檔案)。jmap的作用並不僅僅是為了獲取dump檔案,它還可以查詢finalize執行佇列、Java堆和永久代的詳細資訊,如空間使用率、當前用的是哪種收集器等。和jinfo命令一樣,jmap有不少功能在Windows平臺下都是受限的,除了生成dump檔案的-dump選項和用於檢視每個類的例項、空間佔用統計的-histo選項在所有作業系統都提供之外,其餘選項都只能在Linux/Solaris下使用。
jmap -dump:live,format=b,file=heap.bin

jhat

jhat dump檔名
後螢幕顯示“Server is ready.”的提示後,使用者在瀏覽器中鍵入http://localhost:7000/就可以訪問詳情.

jstack

(Stack Trace for Java)命令用於生成虛擬機器當前時刻的執行緒快照。執行緒快照就是當前虛擬機器內每一條執行緒正在執行的方法堆疊的集合,生成執行緒快照的主要目的是定位執行緒出現長時間停頓的原因,如執行緒間死鎖、死迴圈、請求外部資源導致的長時間等待等都是導致執行緒長時間停頓的常見原因。
在程式碼中可以用java.lang.Thread類的getAllStackTraces()方法用於獲取虛擬機器中所有執行緒的StackTraceElement物件。使用這個方法可以通過簡單的幾行程式碼就完成jstack的大部分功能,在實際專案中不妨呼叫這個方法做個管理員頁面,可以隨時使用瀏覽器來檢視執行緒堆疊。

jconsole

Java提供的GUI監視與管理平臺。

visualvm

和jconsole類似,但是通過外掛擴充套件,可以具備遠優於jconsole的視覺化功能。


參考:

  • http://www.cellei.com/blog/2018/04251
  • https://crowhawk.github.io/2017/08/15/jvm_3/
  • https://meandni.com/2019/01/11/jvm_note2/
  • https://juejin.im/post/5d7ba549e51d453b5e465bd4#heading-13

    本文由『後端精進之路』原創,首發於部落格 http://teckee.github.io/ , 轉載請註明出處

搜尋『後端精進之路』關注公眾號,立刻獲取最新文章和價值2000元的BATJ精品面試課程。

相關推薦

JVM效能優化系列-(2) 垃圾收集記憶體分配策略

目前已經更新完《Java併發程式設計》和《Docker教程》,歡迎關注【後端精進之路】,輕鬆閱讀全部文章。 Java併發程式設計: Java併發程式設計系列-(1) 併發程式設計基礎 Java併發程式設計系列-(2) 執行緒的併發工具類 Java併發程式設計系列-(3) 原子操作與CAS Java

深入理解Java虛擬機器讀書筆記2----垃圾收集記憶體分配策略

二 垃圾收集器與記憶體分配策略 1 JVM中哪些記憶體需要回收?     JVM垃圾回收主要關注的是Java堆和方法區這兩個區域;而程式計數器、虛擬機器棧、本地方法棧這3個區域隨執行緒而生,隨執行緒而滅,隨著方法結束或者執行緒結束記憶體自然

深入理解JVM讀書筆記二:垃圾收集記憶體分配策略

一、判斷物件死亡的兩種常用演算法:                在堆裡面存放著java世界中幾乎所有的例項物件,垃圾收集器在堆進行回收前,第一件事情就是要確定哪些物件還存活著,哪些已經死去。 1、引

深入理解JVM—第三章:垃圾收集記憶體分配策略

概述 對於Java記憶體執行時區域的各位部分,其中程式計數器、虛擬機器棧、本地方法棧這三個區域都是隨執行緒而生,隨執行緒而滅。並且棧幀中分配的記憶體也是在編譯後就已知的。因此這幾個區域的記憶體分配和回收都具備確定性,所以我們在這幾個區域就不必過多地考慮

2. 垃圾收集記憶體分配策略(二)

上一篇我們已經討論了記憶體回收的內容,我們再來說一下,物件的記憶體分配策略往大方向講,就是在堆上分配,物件主要分配在新生代的Eden區上,如果啟用了本地執行緒分配緩衝,將按執行緒優先在TLAB上分配。普

JVM(三) 垃圾收集記憶體分配策略

一 重點關注的資料區域: 堆 和 方法區 Java記憶體執行時區域中的程式計數器、虛擬機器棧、本地方法棧3個區域隨執行緒生,隨執行緒滅;每 一個棧幀中分配多少記憶體是在類結構確定下來就已知的,因此這幾個區域的記憶體分配和回收都 具備確定性. 二 垃圾回收

JVM垃圾收集記憶體分配策略(總結自《深入理解Java虛擬機器》)

1、物件可用性判斷 垃圾收集器在回收物件前,需要判斷哪些物件沒有被廢棄,哪些物件已經廢棄了(即無法通過任何途徑使用的物件)。所以,垃圾收集器需要一種演算法來判定這個物件是否需要回收。 (1)引用計數演算法 引用計數演算法的基本思想是給一個物件新增一個引用計數器,

【深入理解JVM虛擬機器】第3章 垃圾收集記憶體分配策略

3.1 概述 垃圾收集(Garbage Collection , GC)的歷史遠遠比Java久遠。它需要完成三件事: 哪些記憶體需要回收 什麼時候回收 如何回收 程式計數器、虛擬機器棧、本地

JVM 第3章垃圾收集記憶體分配策略

1 概述 程式計數器、虛擬機器棧、本地方法棧這3個區域隨執行緒而生,隨執行緒而滅。每個棧幀中分配多少記憶體基本是在類結構確定下來時就已知的,因此這幾個區域的記憶體分配和回收都具備確定性。而java堆和方法區是執行緒共享的記憶體,且一個介面中的多個實現類需要的記憶體可能不一樣,一個方法中的

JVM垃圾收集記憶體分配策略

物件已死嗎 在堆裡面存放著java世界幾乎所有的物件例項,垃圾收集器在對堆進行回收前,第一件事情就是要確定這些物件哪些還“存活”,哪些已經“死去” 引用計數演算法 給物件中新增一個引用計數器,每當有一個地方引用它的時候,計數器就加1,;當引用失效時,計

jvm--垃圾收集記憶體分配策略

標記—清除演算法  (1)標記階段:先通過根節點,標記所有從根節點開始的物件,未被標記的為垃圾物件  (2)清除階段:清除所有未被標記的物件 複製演算法:    將原有的記憶體空間分成兩塊,每次只使用其中一塊,在垃圾回收時,將正在使用的記憶體中的存活物件複製到未使用的記憶體

JVM總結-垃圾收集記憶體分配策略

垃圾收集器 需要回收的物件例項 垃圾收集器在對堆進行回收時,首先要判斷物件是否還存活。 判斷物件是否存貨的演算法: 1、

深入理解Java虛擬機器總結一垃圾收集記憶體分配策略(二)

深入理解Java虛擬機器總結一垃圾收集器與記憶體分配策略(二) 垃圾回收概述 如何判定物件為垃圾物件 垃圾回收演算法 垃圾收集器詳解 記憶體分配策略 垃圾回收概述 如何判定物件為垃圾物件 引用計數法: 在物件

第三章垃圾收集記憶體分配策略

3.2物件死亡的判斷方法 3.2.1引用計數法 給物件新增一個引用計數器,每當一個地方引用它就+1,引用失效就-1,當計數器為0時就表示物件已經死亡。 缺點是無法解決迴圈引用問題 3.2.2可達性分析 將GC root作為根節點向下遍歷,無法遍歷到的物件(GC Root到這個物件不可達)就表示該物件

《深入理解JAVA虛擬機器》詳細解讀(第三章 ):垃圾收集記憶體分配策略

  目錄 一、垃圾收集器與記憶體分配策略 1 概述 2 物件已經死亡? 2.1引用計數法(未使用) 2.2可達性分析演算法 2.3 再談引用 2.4 生存還是死亡 2.5 回收方法區 3 垃圾收集演算法 3.1 複製演算法(Copy) 3

Java虛擬機器筆記-1(Java技術體系&自動記憶體管理機制&記憶體區域記憶體溢位&垃圾收集記憶體分配策略

世界上沒有完美的程式,但寫程式是不斷追求完美的過程。 Devices(裝置、裝置)、GlassFish(商業相容應用伺服器) 目錄 1. Java技術體系包括: Java技術體系的4個平臺 虛擬機器分類 HotSpot VM 模組化、混合程式設計 多核並行

JAVA垃圾收集記憶體分配策略

3.1 概述 LISP是第一門使用記憶體動態分配和垃圾收集技術的語言。 CG需要完成的三件事: 1、哪些記憶體需要回收? 2、什麼時候回收? 3、如何回收? JAVA堆和方法區中,一個介面中的多個實現類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也不一

垃圾收集記憶體分配策略(一)

參考:《深入理解Java虛擬機器》-jvm高階特性與最佳實現(周志明著) 前言 1、Java記憶體執行時區域的各個部分,其中程式計數器,虛擬機器棧,本地方法棧3個區域隨著執行緒而生,隨著執行緒而滅;在方法結束後,佔用的記憶體跟著就回收了,不需要過多考慮垃圾回收問題; 2

垃圾收集記憶體分配策略——垃圾收集演算法HotSpot虛擬機器演算法實現

垃圾收集演算法的具體實現涉及大量的程式細節,這裡只描述其演算法的基本思想和發展過程 一、常見的垃圾收集演算法對比如下 收集演算法 具體實現 優點 不足 標記-清除演算法 1、首先標記出所有需要回收的物件 2、標記完成之後,統一回

深入理解Java虛擬機器——垃圾收集記憶體分配策略(讀書筆記)

判斷物件是否存活 1、引用計數法 給物件新增一個引用計數器,每當有一個地方引用它時,計數器值加1,當引用失效時,計數器值減1, 任何時刻計數器為0的物件就是不可能再被使用的。 缺點:不能解決物件之間迴圈引用的問題 2、根搜尋演算法(GC Roots Tracing)