《深刻java虛擬機》讀書筆記之垃圾收集器與內存分配策略

前言

該讀書筆記用於記錄在學習《深刻理解Java虛擬機——JVM高級特性與最佳實踐》一書中的一些重要知識點,對其中的部份內容進行概括,或者是對其中不明白的地方作一些註釋。主要是方便以後進行復習。html

目錄

《深刻java虛擬機》讀書筆記之Java內存區域java

垃圾收集器與內存分配策略

哪些內存須要垃圾回收

在上一節中有提到在運行時數據區域包括:堆、虛擬機棧、本地方法棧、程序計數器、方法區(JDK1.7及以前)、元空間(JDK1.8及以後)。在這些區域中,程序計數器佔用內存極小,能夠忽略;棧區域在編譯期就能夠肯定下來,而且其聲明週期隨線程保持一致,也不用管;而Java堆和方法區、元空間中接口的不一樣實現類須要的內存不一樣,方法的不一樣實現須要的內存也不一樣,並且這些所需的內存須要在運行時才能肯定,因此垃圾回收關注的主要內容就是這些區域。算法

對象是否再也不使用

引用計數法

給對象添加一個引用計數器,每當有一個地方引用它時,計數器加一;引用失效的時候,計數器就減一;在任什麼時候候只要計數器爲0就表明該對象就是不會再被使用的。數組

該方法的優勢:安全

  1. 實現較爲簡單
  2. 斷定效率很高,基本沒有其餘額外的操做

缺點:bash

很難解決對象之間相互循環引用的問題。即兩個對象相互持有對方的引用,除此以外再沒有別的地方使用這兩個對象,可是由於相互引用致使計數器不可能爲0,因此沒法被回收數據結構

可達性分析算法

算法描述

經過選擇一些知足必定條件的對象做爲節點,從這些節點開始往下搜索,搜索通過的路徑被稱爲引用鏈(有直接或間接引用關係的對象都在引用鏈上),這些對象被成爲"GC Roots",當一個對象達到GC Roots沒有任何引用鏈時則斷定該對象不可用,即便該對象仍舊被其餘對象引用,只要其與GC Roots沒有關係既是不可用的。多線程

可做爲GC Roots的對象
  1. 虛擬機棧中引用的對象。
  2. 方法區中常量引用的對象。
  3. 方法區中類靜態屬性引用的對象。
  4. 本地方法區中JNI引用的對象。

簡單來講包括如下幾種類型:併發

  • Class - 由系統類加載器(system class loader)加載的對象,這些類是不可以被回收的
  • Thread - 活着的線程
  • Stack Local - Java方法的local變量或參數
  • JNI Local - JNI方法的local變量或參數
  • JNI Global - 全局JNI引用
  • Monitor Used - 用於同步的監控對象
Java中的引用

在最初的Java中,引用僅僅是指一個對象的數據中存儲的值是另一塊內存的起始地址。在JDK1.2以後將引用分爲多種:jvm

  1. 強引用:強引用是相似於User user = new User(),是在代碼中最經常使用的一種方式。只要強引用存在,那麼垃圾回收器就永遠不會回收掉被引用的對象。

  2. 軟引用:軟引用用於描述一些有用可是並非必定須要的對象,對於軟引用的對象,當內存將要發生溢出時,會將這些對象列入回收範圍中進行一次回收,若是將軟引用的對象回收後內存仍是不足纔會拋出內存溢出異常。在JDK中使用SoftReference類實現軟引用。SoftReference<Object> softReference = new SoftReference<Object>(new Object());

  3. 弱引用:弱引用用於描述非必須的對象,弱引用對象在下一次垃圾回收時必定會被回收,不管當前內存是否足夠。在JDK中使用WeakReference定義弱引用。

  4. 虛引用:一個對象是否存在虛引用對其生存時間不會有任何關係,只是在這個對象唄收集器回收時收到一個系統通知。在JDK中使用PhantomReference來實現虛引用。

對象的自救

實際上,在可達性算法中即便是不可達的對象也並不是必定會被回收的,判斷其是否會被回收還須要走如下流程:

  1. 若是對象在可達性分析中被斷定沒有與GC Roots相鏈接的引用鏈那麼改對象將會被標記,而後進行一次篩選。

  2. 篩選的條件是判斷該對象是否有必要執行finalize()方法。是否有必要執行finalize()方法的條件是當對象沒有覆蓋finalize()方法或者該對象的finalize()方法已經被虛擬機調用過,這兩種狀況都會被斷定爲沒有必要執行。

  3. 若是被斷定爲有必要執行finalize方法,則會將其放在一個隊列中,稍後執行。在finalize()方法中是對象逃脫被回收的最後機會,只要從新與引用鏈中的任何一個對象創建關係便可。

public class FinalizeEscape {

    private static FinalizeEscape escape = null;

    public static void main(String[] args) throws InterruptedException {
        escape = new FinalizeEscape();

        //模擬對象使用後斷開引用鏈
        escape = null;
        //對象自救
        System.gc();
        Thread.sleep(500);
        if(escape != null){
            System.out.println("對象沒有被清除!");
        }else {
            System.out.println("對象已經被清除!");
        }

        //模擬第二次逃脫gc
        escape = null;
        System.gc();
        Thread.sleep(500);
        if(escape != null){
            System.out.println("對象沒有被清除!");
        }else {
            System.out.println("對象已經被清除!");
        }
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize execute!");
        escape = this;
    }
}
運行結果:
finalize execute!
對象沒有被清除!
對象已經被清除!
複製代碼

在對同一對象進行兩次模擬逃脫gc,第一次成功第二次失敗,是由於一個對象的finalize()方法只會被調用一次。

方法區回收

在方法區的回收主要包括兩個方面:

  • 廢棄常量
  • 無用的類
廢棄常量

廢棄常量是指在常量池中存在一個值,假設爲一個字面量,可是在當前系統中沒有任何的一個對象引用了該字面量。那麼久認爲該字面量是廢棄的,在下一次垃圾回收的時候將其進行回收。同理常量池中的其餘類、接口、方法、字段等的符號引用的回收也是相似。

無用的類

要斷定一個類是否能夠被回收須要知足如下幾個條件:

  • 該類全部的實例都已經被回收
  • 加載該類的ClassLoader已經被回收
  • 該類對應的字節碼對象Class沒有在任何地方被引用,沒法再任何地方經過反射訪問到該類的方法

當一個類知足以上條件後就容許被回收,但不是必定會被回收。是否對類進行回收再HotSpot虛擬機中提供了-Xnoclassgc參數進行控制。也可使用-verbose:class,-XX:+TraceClassLoading,-XX:+TraceClassUnloading參數查看類加載和卸載信息。

在使用反射、動態代理、動態生成jsp和OSGI等頻繁自定義ClassLoader的場景都須要虛擬機具有卸載類的功能,保證永久代不會溢出。

須要注意的是在JDK1.7時HotSpot就已經將運行時常量池遷移到堆中,在JDK1.8中更是直接移除了方法區,因此上面的介紹須要對應到具體的版本,並非指着必定是在方法區完成。雖然區域發生變化可是回收的原則基本仍是這樣。

元空間回收

JDK1.8開始把類的元數據放到本地堆內存(native heap)中,若是Metaspace的空間佔用達到了設定的最大值,那麼就會觸發GC來收集死亡對象和進行類卸載,這一塊的回收要求較高,上文中有簡單說過。

有關元空間的JVM參數:

  • -XX:MetaspaceSize :是分配給類元數據空間(字節)的初始大小。該值設置的過大會延長垃圾回收時間。垃圾回收事後,引發下一次垃圾回收的類元數據空間的大小可能會變大。
  • -XX:MaxMetaspaceSize :是分配給類元數據空間的最大值,超過此值就會觸發Full GC,此值默認沒有限制,但應取決於系統內存的大小。JVM會動態地改變此值。
  • -XX:MinMetaspaceFreeRatio/-XX:MaxMetaspaceFreeRatio :表示一次GC之後,爲了不增長元數據空間的大小,空閒的類元數據的容量的最小/最大比例,不夠就會觸發垃圾回收。

垃圾收集算法

標記-清除算法

標記-清除算法的基本內容就同其名字同樣,存在着標記和清除兩個階段:首先查找與GC Roots無任何關聯的對象,標記處所需回收的對象(如何標記在內存回收中已經介紹了,經過判斷是否有必要或已經執行了finalize()方法),在標記完成以後再統一清除。

標記過程:虛擬機從做爲GC Roots的根節點出發進行搜索,對可被訪問到的對象作一個標記,其餘未被標記的對象就是須要被回收的。效率低是由於目前來講項目中的對象極多,單單是進行遍歷就須要耗費較長的時間。

好處:實現簡單,標記-清除算法流程十分簡單,實現也沒有很複雜的地方。

缺點: 1.效率較低:由於標記和清除的過程效率都不高 2.浪費內存空間:在清除標記的對象後形成了內存中大量不連續的空間,一旦有大的對象進入可能會由於沒有合適的存放的地方而形成再一次的GC。

複製算法(多用於新生代)

複製算法的基本內容是要求虛擬機並不將全部的內存空間用來存放對象。複製算法將內存分爲兩塊,每一次都只是使用其中的一塊,當觸發GC時,將存放對象的那一塊內存上還存活的對象複製到另外一塊上去,而後將以前的內存塊所有清除。

優勢:實現簡單,並且由於在將存活對象轉移時順序內存存放不用考慮內存碎片的問題,效率較高。

缺點: 1.始終有一部份內存沒有獲得使用,形成空間浪費。要保證存活的對象可以徹底複製,那麼就要求兩塊內存大小一致(50%),由於可能存在沒有任何對象死亡的極端狀況,可是這樣將會極其浪費,而若是不這樣分配,就必須引入其餘機制保證對象可以被完整的複製。

標記-整理算法

標記整理算法的標記階段同標記-清除算法一致,不過標記後並不當即清除,而是將存活(不會被GC)的對象移向內存的一端,將存活的對象所有移動後將邊界外的清除掉。

優勢:解決了內存碎片的問題

缺點:標記階段效率自己較低,還多加了一個整理階段,仍是在於整體效率較低

分代收集算法

分代收集算法實際上並非一個新的實現方式,只是將虛擬機分紅幾塊,每一塊根據它的實際做用來選擇適合的算法,這些算法能夠是標記-清除,複製算法等等。

基於分代的收集思想,將堆內存分爲如下幾個部分:

分代收集

將堆內存分爲新生代(Young)和老年代(Old),新生代又分爲Eden區、from區和to區,默認Eden:from:to=8:1:1。通常狀況下,新建立的對象都會被分配到Eden區(一些大對象可能會直接放到老年代),具體的內存分配在後面記錄。

HotSpot中的算法實現

枚舉根節點

在可達性分析中,可做爲GC Roots的節點主要是全局性的引用與執行上下文,若是要逐個檢查引用,必然消耗時間。 另外可達性分析對執行時間的敏感還體如今GC停頓上,由於這項分析工做必須在一個能確保一致性的時間間隔中進行,這裏的「一致性」的意思是指整個分析期間整個系統執行系統看起來就像被暫停在某個時間點,不能夠出現分析過程當中對象引用關係還在不斷變化的狀況,也就是在分析過程當中用戶線程還在工做。這點是致使GC進行時必須暫停全部Java執行線程的其中一個重要緣由。

可是目前主流的Java虛擬機都是準確式GC(準確式GC是指就是讓JVM知道內存中某個位置數據的類型是什麼),因此在執行系統停頓下來以後,並不須要一個不漏的檢查執行上下文和全局的引用位置,虛擬機是有辦法得知哪些地方存放的是對象的引用。在HotSpot的實現中,是使用一組OopMap的數據結構來達到這個目的的。

安全點

在OopMap的協助下,HotSpot能夠快速且準確的完成GC Roots的枚舉,但可能致使引用關係變化的指令很是多,若是爲每一條指令都生成OopMap,那將會須要大量的額外空間,這樣GC的空間成本會變的很高。

實際上,HotSpot也的確沒有爲每條指令生成OopMap,只是在特定的位置記錄了這些信息,這些位置被稱爲安全點(SafePoint)。SafePoint的選定既不能太少,以至讓GC等待時間過久,也不能設置的太頻繁以致於增大運行時負荷。因此安全點的設置是以讓程序「是否具備讓程序長時間執行的特徵」爲標準選定的。「長時間執行」最明顯的特徵就是指令序列的複用,例如方法調用、循環跳轉、異常跳轉等,因此具備這些功能的指令纔會產生SafePoint。

對於SafePoint,另外一個問題是如何在GC發生時讓全部線程都跑到安全點在停頓下來。這裏有兩種方案:搶先式中斷和主動式中斷。搶先式中斷不須要線程代碼主動配合,當GC發生時,首先把全部線程中斷,若是發現線程中斷的地方不在安全點上,就恢復線程,讓他跑到安全點上。如今幾乎沒有虛擬機實現採用搶先式中斷來暫停線程來響應GC。 而主動式中斷的思想是當GC須要中斷線程的時候,不直接對線程操做,僅僅簡單的設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就本身中斷掛起,輪詢標誌的地方和安全點是重合的另外再加上建立對象須要分配的內存的地方。

安全區域

使用安全點彷佛已經完美解決了如何進入GC的問題,但實際狀況卻並不必定,安全點機制保證了程序執行時,在不太長的時間內就會進入到可進入的GC的安全點。可是程序若是不執行呢?所謂的程序不執行就是沒有分配cpu時間,典型的例子就是線程處於sleep狀態或者blocked狀態,這時候線程沒法響應jvm中斷請求,走到安全的地方中斷掛起,jvm顯然不太可能等待線程從新分配cpu時間,對於這種狀況,咱們使用安全區域來解決。

安全區域是指在一段代碼片斷之中,你用關係不會發生變化。在這個區域的任何地方開始GC都是安全的,咱們能夠把安全區域看作是擴展了的安全點。

當線程執行到安全區域中的代碼時,首先標識本身已經進入了安全區,那樣當在這段時間裏,JVM要發起GC時,就不用管標識本身爲安全區域狀態的線程了。當線程要離開安全區域時,他要檢查系統是否完成了根節點枚舉,若是完成了,那線程就繼續執行,不然他就必須等待,直到收到能夠安全離開安全區域的信號爲止。

垃圾收集器

Serial收集器

Serial是一個單線程的收集器,這表示其Serial只會使用一個CPU或者是一條收集線程進行垃圾回收的工做,同時須要注意的是它在進行回收工做是會停掉全部的其餘工做線程(Stop the World),知道它的回收工做結束。

Serial雖然存在上面的問題,可是這並不表示它是一個無用的收集器,反而到目前爲止Serial收集器在Client模式下被用在新生代的收集(64位虛擬機默認支持Server模式,而且沒法切換;32位虛擬機可在Client和Server之間切換。正常狀況下,Server模式啓動較慢,但啓動後性能遠高於Client模式)。可是實際上使用也很少了。。。

Serial收集器的優勢在於:在單CPU環境中,因爲Serial因爲沒有線程的開銷,專心作垃圾回收天然能得到極高的回收效率。

ParNew收集器

ParNew實際上就是一個多線程版的Serial收集器,除了多線程進行垃圾回收外其餘都和Serial基本一致。

ParNew在不少運行於Server模式下的虛擬機中被用於新生代的首選。最大的緣由在於目前爲止只有其能CMS(Concurrent Mark Sweep)收集器配合使用。

併發與並行

  • 併發:簡單來說是指一個處理器在同一時間間隔處理多個任務。放到垃圾回收這裏指垃圾回收線程和用戶工做線程同時進行工做。
  • 並行:並行是指多個處理器在同一時刻處理多個任務。放到垃圾回收這裏指多個垃圾回收線程同時工做,可是用戶工做線程處於等待狀態。

Parallel Scavenge收集器

Parallel Scavenge是一個新生代的收集器,同時它是一個並行的多線程的收集器,其使用複製算法。Parallel Scavenge的目標是達到一個可控制的吞吐量(throughput)。

吞吐量:CPU用於運行用戶代碼的時間和CPU總共運行時間的比值

吞吐量越高表示停頓時間短,程序響應速度快,CPU利用率越高。

Parallel Scavenge提供了幾個用於精確控制吞吐量的參數:

  • -XX:MaxGCPauseMillis:控制最大垃圾收集停頓時間,該參數的值運行爲一個大於0的毫秒數,虛擬機會盡可能將垃圾回收花費的時間控制在設定的值之下。可是並不表明將每次垃圾回收的時間減少就必定是有利的,由於回收時間的下降是以下降吞吐量和新生代空間爲代價的。爲了保證每次垃圾回收的時間減少,就須要下降每一次垃圾回收的區域,因此須要減少新生代的大小。但在下降新生代大小的同時也增長了垃圾回收的次數。因此在設置該值是不能盲目的追求小的回收時間,而應該根據項目實際狀況進行設置。
  • -XX:GCTimeRatio:直接設置吞吐量大小。該值表明吞吐量比率,因此其應該是一個大於0而且小於100的數,這裏還要求其爲整數。
  • -XX:UseAdaptiveSizePolicy:用於肯定是否開啓GC自適應的調整策略,若是打開該策略,就不須要手工指定新生代的大小(-Xmn)、Eden區和Survivor區的比例(-XX:SurvivorRatio)、晉升老年代年齡(-XX:PretentureSizeThreshold)等參數。虛擬機會監控當前系統運行狀態收集性能監控信息,自動的調整系統中垃圾回收停頓時間或者最大的吞吐量。該方式適合不太清楚如何設置上面那些參數的同窗。

Serial Old

Serial Old是Serial的老年代版本,它也是一個單線程收集器,使用「標記-整理」算法。它的做用主要是兩個:一個是搭配Parallel Scavenge收集器使用;另外就是當CMS收集器發生Concurrency Mode Failure時做爲備用收集器。

Parallel Old

同Serial Old同樣,Parallel Old是Parallel Scavenge的老年代版本。在注重吞吐量和CPU資源敏感的地方均可以優先考慮Parallel Old能夠和Parallel Scavenge一塊兒搭配使用。

CMS收集器

CMS(Concurrency Mark Sweep)是一個以獲取最短回收停頓時間爲目標的收集器,容許垃圾回收線程和用戶工做線程同時運行。其使用「標記-清除」算法。目前來講例如淘寶等大型互聯網企業都但願請求響應時間能儘可能短,而且垃圾回收的停頓時間也儘可能短,這種狀況就可使用CMS收集器。

CMS的「標記-清除」算法分爲多個步驟:

  1. 初始標記:初始標記是用於標記直接與GC Roots關聯的對象,不須要遍歷下去,所需的時間很短。這一過程會發生STW(Stop the World)。
  2. 併發標記:併發標記就是遍歷全部與GC Roots直接或者間接關聯的對象。
  3. 從新標記:前面說過CMS容許垃圾回收線程和用戶工做線程同時運行,因此這一過程是爲了標記在前面標記過程當中發生變更的對象。這一過程會發生STW(Stop the World)。
  4. 併發清除:清除掉那些沒有被標記的對象。

其中併發標記和併發清除過程耗費時間最長,可是這兩個階段均可以併發進行,因此對用戶的影響也不會太大。

雖然CMS確實是一款很不錯的垃圾收集器,可是其也還有幾個缺點:

  • CMS收集器對CPU資源很是敏感。在併發階段,它雖然不會致使用戶線程停頓,可是會由於佔用了一部分線程而致使應用程序變慢,總吞吐量會下降。
  • CMS收集器沒法處理浮動垃圾,因爲CMS併發清理階段用戶線程還在運行着,伴隨着程序運行天然就會有新的垃圾不斷產生,這部分垃圾出現的標記過程以後,CMS沒法在當次收集中處理掉它們,只好留待下一次GC中再清理。這些垃圾就是「浮動垃圾」。同時爲了保證在垃圾回收的同時用戶線程也能夠正常工做,因此不可能對整個區域進行回收,須要預留一部分區域給用戶線程,若是在垃圾回收階段,預留的垃圾回收區域不足,就可能會出現「Concurrent Mode Failure(併發模式故障)」失敗而致使Full GC產生。
  • CMS是一款「標記--清除」算法實現的收集器,容易出現大量空間碎片。當空間碎片過多,將會給大對象分配帶來很大的麻煩,每每會出現老年代還有很大空間剩餘,可是沒法找到足夠大的連續空間來分配當前對象,不得不提早觸發一次Full GC。

G1收集器

G1是一款面向服務端應用的垃圾收集器。G1具有以下特色:

  • 並行與併發:G1收集器能充分利用CPU、多核環境下的硬件優點,使用多個CPU(CPU或者CPU核心)來縮短STW停頓時間。部分其餘收集器本來須要停頓Java線程執行的GC動做,G1收集器仍然能夠經過併發的方式讓java程序繼續執行。
  • 分代收集:雖然G1能夠不須要其餘收集器配合就能獨立管理整個GC堆,可是仍是保留了分代的概念。它可以採用不一樣的方式去處理新建立的對象和已經存活了一段時間,熬過屢次GC的舊對象以獲取更好的收集效果。
  • 空間整合:與CMS的「標記--清理」算法不一樣,G1從總體來看是基於「標記整理」算法實現的收集器。從局部上來看是基於「複製」算法實現的。
  • 可預測的停頓:這是G1相對於CMS的另外一個大優點,下降停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能創建可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片斷內。

在G1中Heap被分紅一塊塊大小相等的region,Region的大小能夠經過參數-XX:G1HeapRegionSize設定,取值範圍從1M到32M,且是2的指數。若是不指定,那麼G1會根據Heap大小自動決定。保留新生代和老年代的概念,但它們不須要物理上的隔離。每塊region都會被打惟一的分代標誌(eden,survivor,old),表明一個分代類型的region能夠是不連續的。eden regions構成Eden空間,survivor regions構成Survivor空間,old regions構成了old 空間。經過命令行參數-XX:NewRatio=n來配置新生代與老年代的比例,n爲整數,默認爲2,即比例爲2:1;-XX:SurvivorRatio=n能夠配置Eden與Survivor的比例,默認爲8。

G1收集器進行回收大體可分爲如下幾個階段:

  • 初始標記:同CMS功能基本一致初始標記是用於標記直接與GC Roots關聯的對象,不須要遍歷下去,所需的時間很短。這一過程會發生STW(Stop the World)。
  • 併發標記:併發標記就是遍歷全部與GC Roots直接或者間接關聯的對象。遍歷整個堆尋找活躍對象,這個發生在應用運行時,這個階段能夠被年輕代垃圾回收打斷。
  • 從新標記:這一過程是爲了標記在前面標記過程當中發生變更的對象,和CMS的從新標記過程功能上基本保持一致。可是G1使用一個叫做snapshot-at-the-beginning(SATB)的比CMS收集器的更快的算法。
  • 篩選回收:進行垃圾回收,G1保留了YGC並加上了一種全新的MIXGC用於收集老年代。G1中沒有Full GC,G1中的Full GC是採用serial old的Full GC。
Young GC

當Eden空間不足時就會觸發YGC。在G1中YGC也是採用複製存活對象到survivor空間,對於對象的存活年齡知足晉升條件時,把對象移到老年代。

在對新生代進行垃圾回收時,須要判斷哪些對象可以會被回收。這裏判斷的方法也是採用可達性分析,標記與GC Roots直接或間接關聯的對象。在CMS中使用了Card Table的結構,裏面記錄了老年代對象到新生代引用。G1也是使用這個思路,定義了一個新的數據結構:Remembered Set。在G1收集器中,Region之間的對象引用以及其餘收集器中的新生代與老年代之間的對象引用,虛擬機都是使用Remembered Set來避免全堆掃描的。G1中每一個Region都有一個與之對應的Remembered Set,虛擬機發現程序在對Reference類型的數據進行寫操做時,會產生一個Write Barrier暫時中斷寫操做,檢查Reference引用的對象是否處於不一樣的Region之中(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象),若是是,便經過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set之中。在進行內存回收時,在GC根節點的枚舉範圍中加入Remembered Set便可保證不對全堆掃描也不會有遺漏。

Full GC

full gc是指對包括新生代、老年代和方法區(元空間)等地區進行垃圾回收。

full gc的觸發包括如下幾種狀況:

  • 老年代空間不足
  • 新生代對象晉升到老年代時,老年代剩餘空間低於新生代晉升爲老年代的速率,會觸發老年代回收
  • minor gc以後,survior區內存不足,將存活對象放入老年代,老年代也不足,觸發Full GC。本質上仍是老年代內存不足。
  • System.gc().

理解GC日誌

這裏介紹一些打印出的gc日誌的信息:

爲了觸發gc寫一段代碼,實際上也能夠直接使用System.gc()

public class Test {

    public static void main(String[] args) {
        byte[] bytes1 = new byte[1024 * 1024];
        byte[] bytes2 = new byte[1024 * 1024];
        byte[] bytes3 = new byte[1024 * 1024];
        byte[] bytes4 = new byte[1024 * 1024];
        byte[] bytes5 = new byte[1024 * 1024];
    }

    public static void test(){
        test();
    }
}
複製代碼

要在控制檯打印gc信息須要咱們手動的配一些參數:

  • -XX:+PrintGCDetails 輸出GC的詳細日誌
  • -XX:+PrintGCTimeStamps/PrintGCDateStamps 輸出GC的時間戳
  • -XX:+PrintHeapAtGC 在進行GC的先後打印出堆的信息
  • -Xloggc:../logs/gc.log 日誌文件的輸出路徑

我這裏使用Idea,直接在VM args配置便可:

image

如今運行上面的程序便可在控制檯得到gc信息:

2019-01-24T20:08:25.811+0800: [GC (Allocation Failure) [PSYoungGen: 1019K->488K(1536K)] 1019K->608K(5632K), 0.0036115 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2019-01-24T20:08:25.872+0800: [GC (Allocation Failure) [PSYoungGen: 1504K->488K(1536K)] 1624K->780K(5632K), 0.0016239 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2019-01-24T20:08:25.879+0800: [GC (Allocation Failure) [PSYoungGen: 653K->504K(1536K)] 4017K->3940K(5632K), 0.0009844 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2019-01-24T20:08:25.880+0800: [GC (Allocation Failure) [PSYoungGen: 504K->504K(1536K)] 3940K->3948K(5632K), 0.0006796 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2019-01-24T20:08:25.881+0800: [Full GC (Allocation Failure) [PSYoungGen: 504K->0K(1536K)] [ParOldGen: 3444K->3832K(4096K)] 3948K->3832K(5632K), [Metaspace: 3426K->3426K(1056768K)], 0.0076471 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
2019-01-24T20:08:25.888+0800: [GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 3832K->3832K(5632K), 0.0003390 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2019-01-24T20:08:25.889+0800: [Full GC (Allocation Failure) Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at Test.main(Test.java:17)
[PSYoungGen: 0K->0K(1536K)] [ParOldGen: 3832K->3814K(4096K)] 3832K->3814K(5632K), [Metaspace: 3426K->3426K(1056768K)], 0.0065960 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 1536K, used 65K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
  eden space 1024K, 6% used [0x00000000ffe00000,0x00000000ffe104d8,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 4096K, used 3814K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
  object space 4096K, 93% used [0x00000000ffa00000,0x00000000ffdb9a60,0x00000000ffe00000)
 Metaspace       used 3472K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 377K, capacity 388K, committed 512K, reserved 1048576K
複製代碼

上面的gc信息取一條分析:

[GC/Full GC (Allocation Failure) [PSYoungGen: 1019K->488K(1536K)] 1019K->608K(5632K), 0.0036115 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

最前面的GC/FullGC表示gc類型,GC表示新生代gc(Minor GC),Full GC表示對新生代和老年代一塊兒收集。

[PSYoungGen: 1019K->488K(1536K)]這個表示GC前該內存區域已使用容量-->GC後該內存區域已使用容量,後面圓括號裏面的1536K爲該內存區域的總容量。

緊跟着後面的1019K->608K(5632K),表示GC前Java堆已使用容量->GC後Java堆已使用容量,後面圓括號裏面的5632K爲Java堆總容量。

[Times: user=0.00 sys=0.00, real=0.00 secs]分別表示用戶消耗的CPU時間,內核態消耗的CPU時間和操做從開始到結束所通過的牆鍾時間,CPU時間和牆鍾時間的差異是,牆鍾時間包括各類非運算的等待耗時,例如等待磁盤I/O、等待線程阻塞,而CPU時間不包括這些耗時。由於這裏是測試在幾乎一開始就發生了gc,而且設置的堆棧容量都較小,因此看不出時間。

PSYoungGen和ParOldGen分別表明新生代和老年代所使用的垃圾收集器。PSYoungGen表示Parallel Scavenge收集器,ParOldGen表示Parallel Old。要查看當前jvm使用那種收集器可使用-XX:+PrintCommandLineFlags,命令行下運行便可。

java -XX:PrintCommandLineFlags -version

-XX:InitialHeapSize=132485376 -XX:MaxHeapSize=2119766016 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_152"
Java(TM) SE Runtime Environment (build 1.8.0_152-b16)
Java HotSpot(TM) 64-Bit Server VM (build 25.152-b16, mixed mode)
複製代碼

其中的-XX:+UseParallelGC表示使用Parallel Scavenge+Serial Old的組合,可是上面是Parallel Scavenge+parallel old的組合,這是爲何???

GC中的參數

這裏有一篇不錯的文章總結gc中的參數,比較詳細:GC

內存分配與回收策略

對象內存的分配,通常是在堆上進行分配,可是隨着JIT技術的發展,部分對象直接在棧上進行內存分配。

在前面的分代收集算法小節處,已經描述了jvm中的分代,將堆分爲新生代和老年代。在描述內存分配前,咱們先來了解下不一樣的GC類型:

  • Minor GC:當Eden區可分配的內存不足以建立對象時就會觸發一次Minor GC,Minor GC發生在新生代,因爲新生代中大多數對象都是使用事後就不須要因此Minor GC的觸發很是頻繁。在Minor GC中存活下來的對象會被移到Survivor中,若是Survivor內存不夠就直接移動到老年代。
  • full GC:當準備要觸發一次young GC時,若是發現統計數聽說以前young GC的平均晉升大小比目前old gen剩餘的空間大,則不會觸發young GC而是轉爲觸發full GC(由於HotSpot VM的GC裏,除了CMS的concurrent collection以外,其它能收集old gen的GC都會同時收集整個GC堆,包括young gen,因此不須要事先觸發一次單獨的young GC)。或者,若是有perm gen的話,要在perm gen分配空間但已經沒有足夠空間時,也要觸發一次full GC;或者System.gc()、heap dump帶GC,默認也是觸發full GC。本內容來源於R大回答

對象分配

在大多數狀況下,對象的內存分配都優先在Eden中進行分配,當Eden區可分配的內存不足以建立對象時就會觸發一次Minor GC。將Eden區和其中一塊Survivor區內尚存活的對象放入另外一塊Survivor區域。如Minor GC時survivor空間不夠,對象提早進入老年代,老年代空間不夠時就進行Full GC。大對象直接進入老年代,避免在Eden區和Survivor區之間產生大量的內存複製,虛擬機提供了一個-XX:PretureSizeThreshold參數,令大於這個值得對象直接進入老年代,可是該參數支隊Serial和ParNew收集器有效。 此 外大對象容易致使還有很多空閒內存就提早觸發GC以獲取足夠的連續空間。

這裏大對象主要是指那種須要大量連續內存的java對象,好比大數組或者特別長的字符串等。

對象晉級

年齡閾值:虛擬機爲每一個對象定義了一個對象年齡(Age)計數器, 經第一次Minor GC後 仍然存活,被移動到Survivor空間中, 並將年齡設爲1。之後對象在Survivor區中每熬 過一次Minor GC年齡就+1。 當增長到必定程度(默認 15),將會晉升到老年代(晉級的年齡能夠經過-XX:MaxTenuringThreshold進行設置)。

提早晉升: 動態年齡斷定,若是在Survivor空間中相同年齡全部對象大小的總和大 於Survivor空間的一半, 年齡大於或等於該年齡的對象就能夠直接進入老年代,而無 須等到晉升年齡。

空間分配擔保

在前面說垃圾收集算法時關於複製對象有說過可能會存在存活下來的對象沒法被survivor容納,這時就須要老年代容納沒法被survivor容納的對象。而若是老年代也沒有足夠的空間來存放這些對象的話就會觸發一次Full GC。

相關文章
相關標籤/搜索