深刻理解Java虛擬機 - 垃圾收集器與內存分配策略

Java與C++間有一堵由動態內存分配和垃圾收集技術所圍成的牆,外面的人想進來,裏面的人卻想出去。java

概述

  • GC須要完成的3件事情
  1. 哪些內存須要回收
  2. 何時進行回收
  3. 怎麼進行回收
  • 意義 目前動態內存分配和垃圾手記技術已經很成熟,一切彷佛已經進入自動化時代,爲何咱們還要去了解GC和動態內存分配呢?答案很簡單:當出現內存泄露、內存溢出問題時,當垃圾回收成爲系統達到更高併發量的瓶頸時,瞭解這些自動化技術就顯得頗有必要。算法

  • 前章回顧 前章介紹了Java運行時內存的各個區域,其中程序計數器、虛擬機棧、本地方法棧都是隨線程而生,隨線程而滅,棧的棧幀隨方法的調用而入棧,隨方法的完成而出棧。每個棧幀中分配的內存大小在編譯期就明確可知,所以這幾個區域的內存分配和回收都具備肯定性,因此這幾個區域不須要過多考慮內存回收的問題,由於方法或線程結束時,內存也隨之跟着回收。而Java堆和方法區不同,由於只有程序處於運行期間才能知道會建立哪些對象,這部份內存的分配和回收是動態的。垃圾收集所關注的也是這部份內存,一下提到的內存都指這一部份內存。bash

對象已死嗎

引用計數法

給對象添加一個引用計數器,每當對象被引用時,計數器值加1,當引用失效時,計數器值減1,當計數器值爲0時,說明對象沒有被其餘地方引用,即對象已死。客觀地說,引用計數法(Reference Counting)的實現簡單,判斷效率也很高,可是,主流的Java虛擬機都沒有采用引用計數法來判斷對象是否已死,由於它有一個致命問題-沒法解決對象間相互引用的問題。
代碼展現:多線程

複製代碼

可達性分析法

基本思路:經過一系列的稱爲「GC Roots」的對象做爲起始點,從這些起始點往下搜索,搜索走過的路徑稱爲引用鏈,當一個對象和「GC Roots」沒有任何引用鏈時(即GC Roots到這個對象是不可達的),說明對象是無用的。 併發

在Java中可做爲GC Roots的對象有下面幾種:

  1. 虛擬機棧(棧幀中的本地變量表)中引用的對象
  2. 方法區中類靜態屬性引用的對象
  3. 方法區中常量引用的對象
  4. 本地方法棧中引用的對象

引用的四種類型

  1. 強引用
  2. 軟引用
  3. 弱引用
  4. 虛引用

對象死亡的歷程

可達性分析法中不可達的對象也不是非死不可的,而是處於緩刑階段。要宣告一個對象的死亡至少要通過兩次標記過程:當通過可達性分析後發現對象與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

  1. 該類的全部實例都已被回收
  2. 加載該類的ClassLoader已被回收
  3. 該類對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類。

垃圾收集算法

標記-清除算法

最基礎的收集算法。線程

  • 工做原理 算法主要分爲兩個階段-標記和清除:首先標記出全部須要回收的對象,標記完成後統一進行清除。

  • 缺點

  1. 效率問題:標記和清除兩個過程效率都不高
  2. 空間問題:對象清除後會產生大量不連續的空間碎片,當須要分配給大對象分配較大的內存空間時會由於找不到足夠的連續空間而不得不提早觸發下一次垃圾收集。

複製算法

爲解決效率問題,複製算法出現了:它將內存空間分爲大小相等的兩塊區域,每次只使用其中一塊,當進行垃圾收集時,將這塊區域中還存活的對象複製到另外一塊,而後將這一塊內存回收。這樣就不會產生內存碎片的問題。這種算法實現簡單,運行高效,只是代價是每次只能使用內存的一半,代價太高。

如今的商用虛擬機都採用這種收集算法回收新生代內存。根IBM公司的研究代表,新生代中的內存對象98%是朝生夕死的,因此不須要按照1:1的比例來劃份內存空間,而是將內存分爲一塊較大的Eden區域,兩塊較小的Survivor區域。每次只使用一塊Eden區域和一塊Survivor區域,當進行垃圾收集時,將Eden區域和Survivor區域仍然存活的對象複製到另外一塊Survivor區域,而後將Eden區域和使用過的Survivor區域清除。HotSpot虛擬機默認的Eden和Survivor區域大小比例爲8:1,這樣只會浪費10%的內存。

標記-整理算法

複製算法在對象成活率較低的新生代比較適用,而對於對象成活率較高的老年代就須要進行較多的複製操做,效率明顯會減低。因此針對老年代的特色,提出了標記-整理算法:標記清除過程仍然與標記清除算法同樣,只是在清除後將存活的對象都向一端移動。

分代收集算法

當前商業的虛擬機的垃圾收集算法都採用分代收集算法:根據對象存活週期的不一樣將內存劃分爲幾塊,通常把Java堆分爲新生代和老年代,再根據各個年代的特色選擇合適的收集算法。
在新生代中,對象存活率低,適合使用複製算法,而老年代對象的存活率高,適合使用標記-清除算法或標記-整理算法。

垃圾收集器

收集算法是內存回收的方法論,那麼收集器就是收集算法的實現。

Serial 收集器 - 新生代收集器

Serial收集器是最基本、最悠久的收集器。這個收集器是一個單線程收集器,在它進行垃圾收集時,必須停掉全部其餘的工做線程,而後以一條收集線程進行垃圾收集,直到收集工做結束,才能夠恢復其餘工做線程。這對於許多應用是難以接受的。可是對Client(客戶端)模式的虛擬機來講,Serial收集器是一個不錯的選擇,由於在桌面端應用,分配給虛擬機的內存不會太大,收集幾十兆到幾百兆的新生代內存停頓時間徹底能夠控制在幾十毫秒。

ParNew 收集器 - 新生代收集器

ParNew收集器其實就是Serial收集器的多線程版本,除了使用多協調線程進行垃圾收集外,其他的Serial收集器徹底同樣。ParNew收集器在單CPU或CPU數量少的環境中性能不會有比Serial收集器更好的結果,可是隨着CPU數量的增多,它GC時對CPU資源的的有效利用仍是頗有好處的,因此它是許多運行在Server模式下的虛擬機的首先新生代收集器。

Parallel Scavenge 收集器 - 新生代收集器

它看上去彷佛與ParNew同樣,可是它的目標是達到一個可控制的吞吐量(吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + GC時間))。停頓時間越短,就越適合與用戶交互的程序,由於良好的響應時間能夠提升用戶的體驗,而吞吐量則能夠高效利用CPU時間儘快完成程序的計算任務,主要適合在後臺運算而須要交互任務。
Parallel Scavenge 收集器提供了兩個參數用於控制吞吐量:

  1. 最大垃圾收集停頓時間:-XX:MAxGCPauseMillis
  2. 設置吞吐量大小:-XX:GCTimeRatio

Serial Old 收集器 - 老年代收集器

Serial Old收集器是Serial收集器的老年代版本,一樣是一個單線程收集器,使用標記-整理算法。

Parallel Old 收集器 - 老年代收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和標記-整理算法,主要配合Parallel Scavenge收集器組成「吞吐量優先」組合。

CMS 收集器 - 老年代收集器

CMS(Concurrent Mark Sweep)是一款以獲取最短回收停頓時間爲目的的收集器。CMS很是適合B/S系統服務端的Java應用,由於這類應用尤爲注重服務的響應時間,但願系統的停頓時間越短。CMS是基於標記-清除算法的運做流程分爲4個部分:

  1. 初始標記:標記GC Roots能關聯到的對象,速度很快
  2. 併發標記:進行GC Roots Tracing
  3. 從新標記:爲了修改併發標記期間因程序繼續運行而致使標記產生變更的對象的標記
  4. 併發清除 初始標記和從新標記仍須要Stop The World,而併發標記和併發清除能夠與用戶線程一塊兒併發工做。CMS的主要特色是:併發收集、低停頓。
相關文章
相關標籤/搜索