jvm 內存結構,GC相關內容和調優

參考博客:https://www.tpvlog.com/article/86

1. jvm內存結構

 大致的結構如上圖所示。

注意:

  • 新生代的地方,HotSpot VM(虛擬機的一種實例)對新生代採用了複製回收算法來實現gc的垃圾回收。而傳統的複製算法比較浪費空間,所以它將新生代又分爲了3個區域,1個Eden,和2個Survivor區。
  • 方法區只存在於JDK1.8以前的版本,從JDK1.8開始,這塊區域的名字改成了元數據區(Metaspace),元數據區直接使用本地內存,本地內存指的是直接使用物理機的內存。好處就是這樣方法區就不再佔用堆內存,如果不設置,JVM將會根據一定的策略自動增加本地元內存空間。如果你設置的元內存空間過小,你的應用程序可能得到以下錯誤:java.lang.OutOfMemoryError: Metadata space。當然JDK8也提供了一個新的設置Matespace內存大小的參數                   ( -XX:MaxMetaspaceSize),通過這個參數可以設置Matespace內存大小,這樣我們可以根據自己項目的實際情況,避免過度浪費本地內存,達到有效利用。
  • survivor一般也通過命名爲from 和 to來區分兩塊區域

1.1 方法區

方法區只存在於JDK1.8以前的版本,主要是存儲從”.class“文件里加載進來的類,包括類的名稱方法信息字段信息靜態變量常量以及編譯器編譯後的代碼等。該區是所有線程共享的

1.2 程序計數器

程序計數器是用來記錄當前執行的字節碼指令的位置。

當java類被編譯後會生成.class文件,當jvm加載.class文件後,會通過字節碼執行引擎去執行這些字節碼指令。然後程序計數器就會記錄當前執行到的字節碼指令的位置

程序計數器是線程私有的,也就是說每個線程都有自己的程序計數器,記錄當前線程執行到了哪一條字節碼指令

1.3 Java虛擬機棧

Java虛擬機棧,其實是一種表示Java方法執行的數據結構。每個方法被執行的時候,都會創建一個棧幀(Stack Frame)用於存儲局部變量表操作棧動作鏈接方法出口等信息。每個方法從被調用到執行完成的過程,其實就是一個棧幀在虛擬機棧中從入棧到出棧的過程。Java虛擬機棧是線程私有的.

調用任何方法時,爲方法創建棧幀然後入棧,棧幀裏存放了這個方法對應的局部變量之類的數據(也包括方法執行的其它相關信息),而創建的對象就是存放在堆中的。方法執行完畢後就出棧。

當出現一個方法中調用另一個方法的情況時,其實就是先後創建兩個棧幀,然後入棧

1.4 本地方法棧

本地方法棧,其作用和Java虛擬機棧類似,區別在於本地方法棧是爲虛擬機所使用到的Native方法服務,而Java虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務。本地方法棧也是線程私有的。

JDK中的很多底層API,比如IO、NIO、網絡等,會發現很多地方是調用的native修飾的方法

1.5 堆內存

Java堆內存,這是JVM內存區域中最重要的一塊區域,存放着各種Java對象,是線程共享區域。

下面代碼中,new ReplicaManager()創建了一個對象實例,這個對象實例的相關信息就存放在Java堆內存中:

public class Kafka {
    public static void main(String[] args) {
        ReplicaManager manager = new ReplicaManager();
        manager.loadReplicaFromDisk();
    }
}

main線程在執行main()方法時,會爲其創建一個棧幀併入棧,棧幀中的局部變量manager存放的ReplicaManager對象實例在Java堆內存中的地址(不考慮對象逃逸的情況)

說明:對象逃逸大致就是指對象的生命週期只存在於方法中,並沒有指向方法以外的成員變量,靜態變量等,這時,對象會很大概率直接被創建在棧內存中,而不是堆內存,一旦方法執行完畢,棧幀出棧,對象就能被一起回收。

2. 對象存活判定

上面介紹了jvm的內存結構,當一個程序執行時,會創建大量對象,如果不對這些對象進行回收,很容易就會造成內存溢出。那怎麼去判斷對象是否是垃圾對象,需要進行回收呢?一般有兩種辦法,可達性分析算法和引用計數法

2.1 可達性分析算法

可達性分析算法的基本思路就是通過一系列的名爲"GC Roots"的對象作爲起始點,從這些起始點開始搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時(即從GC Roots到這個對象不可達),則證明此對象是不可用的,就可以被回收。

GC Roots包括:

  • Java虛擬機棧中的局部變量(指向着GC堆裏的對象);
  • VM的一些靜態數據結構裏指向GC堆裏的對象的引用,例如HotSpot VM裏的Universe裏有很多這樣的引用;
  • 所有當前被加載的Java類(看情況);
  • Java類的運行時常量池裏的引用類型常量;
  • String常量池(StringTable)裏的引用。

上述大致就是在方法區中的常量池和棧中的變量,注意類中的普通成員變量不能作爲GC ROOT,除非加上static關鍵字

畫圖說明

因爲堆中的對象有可能會相互引用,所以可以看到有a指向b,b指向d這種情況。

現在來判斷d對象是否需要回收?從棧幀中的變量(GC ROOT)開始看其引用鏈,發現a,b,c,d對象都是在該引用鏈上的,所以a,b,c,d對象都是不能被回收的,而e對象由於沒有在任何一條引用鏈上,所以e對象是需要被回收的

代碼說明

// 示例1:
public class Kafka {
    public static void main(String[] args) {
        loadReplicasFromDisk();
    }
    public static void loadReplicasFromDisk(){
        ReplicaManager replicaManager = new ReplicaManager();
    }
}
// 示例2
public class Kafka {
    public static ReplicaManager replicaManager = new ReplicaManager();
}

這段代碼中,

示例1中由於 replicaManager 是棧中的局部變量,所以它可以當作是GC ROOT,所以ReplicaManager 是可達的,不需要回收 

示例2中由於 replicaManager是靜態變量,存放在方法區中的,所以它也是可以當作是GC ROOT,也不需要回收。

那如果我在示例1中添加代碼replicaManager = null,那麼該變量就找不到堆中的對象了,所以堆中的a,b,c,d,e都會被回收。

 2.2 引用計數法

就是給堆中的每個對象一個引用計數器,每當有一個地方引用它時,計數器就加1,當引用失效時,計數器值就減1,任何時刻計數器爲0的對象就是可以被回收的。那像我們2.1 中的圖,b被引用了3次,那它的計數就是3。

這種方法的不足就在於不能很好的解決循環引用的問題。假設我添加代碼replicaManager = null,那很顯然應該是a,b,c,d,e對象都是需要回收的,但是由於他們的計數並不都是0,所以只會回收e。

所以java是沒有使用引用計數法的

2.3 判斷引用類型

可達性分析與Java的引用類型有關聯,爲了更好的管理對象的內存,更好的進行垃圾回收,JVM團隊擴展了引用類型,從最早的強引用類型增加到強引用軟引用弱引用虛引用四個引用類型:

  1. 強引用: 默認的對象都是強引用類型,如果JVM在對象存活判定時,通過GC Roots可達性分析結果爲可達,表示引用類型仍然被引用着,這類對象始終不會被垃圾回收器回收。
  2. 軟引用:在JVM內存充足的情況下,軟引用是不會被GC回收的,只有在JVM內存不足的情況下,纔會被GC回收。代碼中實現使用SoftReference類包裹
  3. 弱引用:不論當前JVM內存是否充足,都只能存活到下一次垃圾收集之前,即只要發生GC弱引用對象就會被回收。ThreadlLocal中定義的ThreadLocalMap就使用到的弱引用。ThreadLocalMap的Entry,其Key就是一個弱引用對象。代碼中使用WeakReference類包裹
  4. 虛引用:不會影響對象的生命週期,所持有的引用就跟沒持有一樣,隨時都能被GC回收。在使用虛引用時,必須和引用隊列關聯使用。其使用場景是用來跟蹤對象被垃圾回收器回收的活動。

弱引用示例:

public class Kafka {
    public static WeakReference<ReplicaManager> replicaManager = new WeakReference<ReplicaManager>(new ReplicaManager());
}

2.4 finalize方法

綜上所述:有GC Roots引用的對象不能回收,沒有GC Roots引用的對象,如果是軟引用或弱引用,可能會被回收。

真正的回收環節,待被回收的對象其實還有一次機會拯救自己,那就是對象的finalize()方法。我們通過一段代碼示例來看下:

public class ReplicaManager {
    public static ReplicaManager instance;

    @Override
    protected void finalize() throws Throwable {
        ReplicaManager.instance = this;
    }
}

假如有一個ReplicaManager對象馬上就要被回收了(此時已經沒有GC Roots到達它的鏈路),此時GC會首先調用下該對象的finalize()方法,看看它是否找了一個新的GC Roots來引用自己,比如上述代碼中,GC發現有個靜態變量instance引用了該實例,那GC就不會去回收它。

3. 分代收集

如第一節中的圖所示,堆內存中有着新生代和老生代,方法區有着永久代。分代收集就是對不同的代採用不同的收集算法,一般常見的就是新生代採用parNew GC回收器(複製算法),老生代採用CMS GC回收器(標記整理).

文中提到的Minor GC ,Full GC可查看第7節說明

3.1 新生代

當在方法中創建對象時,由於在方法中創建的對象一般生命週期很短,大多數都是在方法結束後就失去引用了,所以通常這種“朝生暮死”的小對象都會在Java堆內存區域的"新生代"進行分配。

但是,並不是每次JVM都會進行回收,默認情況下當新生代的內存空間快被佔滿時,會觸發一次“Minor GC”,此時纔會進行回收。

3.2 老生代

以下情況,對象會被分配到老年代:

  1. 如果一個實例對象在新生代中,成功的在15次垃圾回收之後,還是沒有被回收到,那麼就會被轉移到老年代(15次是Java虛擬機的規範,也可以通過JVM參數-XX:MaxTenuring Threshold設置)
  2. 對於一些大對象,會直接在“老年代“分配
  3. 在一次”Minor GC“之後,如果新生代中的存活對象過多,即使這些對象年齡沒有達到15,也會直接進入老年代
public class Kafka {
    private static ReplicaFetcher fetcher = new ReplicaFetcher();

    public static void main(String[] args) throws InterruptedException {
        ReplicaManager replicaManager = new ReplicaManager();
    }
}

如上代碼,由於fetcher是一個靜態變量,其實例對象ReplicaFetcher會一直被該靜態變量引用,而ReplicaManager對象則一直“朝生暮死”。

最初時,ReplicaFetcher對象和ReplicaManager對象都被分配在新生代。而ReplicaManager對象,當方法執行完成後,棧幀就會出棧,所以新生代裏的ReplicaManager會被垃圾回收線程清理掉。而由於fetcher是一個靜態變量,其實例對象ReplicaFetcher會一直被該靜態變量引用,在15次回收後就會被分配到老年代

3.3 永久代

永久代就是方法區,方法區中存儲着類的信息,當滿足以下三個條件時,方法區裏的類會被回收:

  • 該類的所有實例對象已經從Java堆內存中被回收;
  • 加載這個類的ClassLoader已經被回收;
  • 對該類的Class對象沒有任何引用。

3.4 新生代的對象什麼時候會進入老年代

一共有五種情況:

  • 新生代對象的年齡超過一定閾值(默認15);
  • 動態年齡判斷
  • 大對象直接分配(避免新生代中出現屢次逃過GC的大對象,大對象在新生代的Eden和Survivor區的來回複製開銷比較大)
  • Survivor區空間不足(Minor GC之後發現存活對象太多,沒法放入Survivor區域)

3.5 空間分配擔保機制

前面討論了新生代的存活對象何時會轉移到老年代,那麼問題又來了,如果老年代區域的內存空間不足了怎麼辦?這裏就涉及了空間分配擔保.

所謂空間分配擔保,指在執行任何一次Minor GC之前,JVM會檢查老年代的最大連續可用空間是否大於新生代所有對象的總大小.如果大於,說明這次Minor GC肯定是安全的,因爲老年代可以容納新生代中的所有對象;

如果小於,則 JVM 會查看-XX:HandlePromotionFailure參數值,這個參數值表示是否允許擔保失敗:

  • 如果允許(HandlePromotionFailure==true),則看下老年代的最大連續可用空間是否大於歷次Minor GC後進入老年代的對象平均大小。如果大於,就進行minior GC,如果這次Minior GC失敗了,就會進行FULL GC(所謂FULL GC,就是既對老年代進行垃圾回收,也對新生代進行垃圾回收);如果小於,先進行FULL GC,再Minor GC。
  • 如果不允許(HandlePromotionFailure==false),則直接觸發FULL GC,然後再進行一次Minor GC。

如果經過上面的操作,老年代可用空間最後發現還是不夠,就會導致所謂的OOM內存溢出了。

總之,空間分配擔保機制的核心目的就是避免頻繁FULL GC,能先預判就先預判,實在不行才FULL GC,因爲FULL GC的開銷非常大,既要對老年代進行回收,也要對新生代進行回收。

總結:每次執行Minor GC前,先看下老生代的容量是否能放下整個新生代,如果放不下,就調用一次FULL GC進行垃圾回收,如果FULL GC後還不夠,那就直接報內存溢出。

4. jvm核心參數配置

通過這些參數可以設置上述提到的新生代、老年代、永久代的內存區域大小:

-Xms:Java堆內存區域的初始大小;

-Xmx:Java堆內存區域的最大大小;

-Xmn:Java堆內存區域的新生代大小,扣除新生代剩下的就是老年代的大小;

-XX:PermSize:永久代初始大小;

-XX:MaxPermSize:永久代的最大大小;

-Xss:每個線程的棧內存大小。

-XX:SurvivorRatio: 用來設置新生代中eden空間和from/to空間的比例。默認爲8,也就是說Eden佔新生代的8/10,From倖存區和To倖存區各佔新生代的1/10

-XX:NewRatio:配置新生代與老年代佔比,如配置2,表示新生代和老年代佔比爲1:2,新生代佔了堆內存的1/3

總結:

  • 在實際工作中,我們可以直接將初始的堆大小與最大堆大小相等,這樣的好處是可以減少程序運行時垃圾回收次數,從而提高效率。
  • JDK1.8以後,方法區變成了“元數據區”,-XX:PermSize和-XX:MaxPermSize這兩個參數,也相應的變成了-XX:MetaspaceSize和-XX:MaxMetaspaceSize。

5. 垃圾回收算法

5.1 複製算法

複製算法,主要用於新生代中對象的回收。其基本思路就是:將新生代內存按劃分爲大小相等的兩塊,每次只使用其中的一塊,當一塊內存用完了,將存活的對象移動到另外一塊上面,然後在把已使用過的內存空間一次清理掉。

如上圖所示,新生代內存被分爲了A,B兩個區域,紅色的表示不需要回收的對象,黃色表示需要回收的對象。

創建對象時,只會使用一塊區域A,當GC回收時會將紅色對象全部轉移至區域B,保證沒有內存碎片(順序佔用區域B的內存),然後清空區域A。

優化:

上述複製算法的缺點很明顯:即對內存的使用效率太低。比如我們給新生代分配了1G內存,那其實只有512MB是實際使用的,很浪費內存空間。那麼如何來優化呢?

HotSpot VM 採用了一種做法,把新生代區域劃分成了三塊:1個Eden區(80%),2個Survivor區(各佔10%),最開始,對象只在Eden進行分配,

如果Eden區快滿了,此時觸發GC會將Eden區中的存活對象轉移到其中一塊Survivor中,同時清空Eden。

下一次再分配空間時,依然在Eden區分配,然後觸發GC,將Eden的存活對象和上一次使用的Survivor中的存活對象轉移到另一塊空白Survivor中,然後清空Eden和使用過的Survivor,循環往復。

這種內存劃分方式的最大好處就是隻有10%的空間是閒置的,無論是垃圾回收的性能、內存碎片的控制、內存使用率,都非常好。

5.2 標記整理算法

一般用於老年代的垃圾回收算法

標記整理算法,其實就是先標記存活對象,然後將存活對象都向內存端邊界移動,然後清理掉端邊界以外的內存,這樣就可以避免出現大量內存碎片。如下圖所示,其中的整理算法有很多種實現。

 標記整理算法的好處是:

  • 內存的完全使用,不需要像複製算法,分割內存
  • 清理後的內存空間是連續的

缺點就是整理算法一般都是挺耗時間的。

對於老年代來說,一般存放在其中的對象都是很少需要回收的,所以使用算法比較好

5.3 標記清除算法

如上圖所示,標記清除算法就是先標記,後清除

標記-清除算法的比較大的缺點就是垃圾收集後有可能會造成大量的內存碎片,所以java一般不使用這種算法進行垃圾回收

6. 垃圾回收器

在新生代和老年代進行垃圾回收的時候,都需要使用回收器進行回收,不同的JVM 垃圾回收器會有所不同,不同區域一般也採用不同的垃圾回收器。JVM常見的垃圾回收器有以下幾種。

6.1 Serial/Serial Old

Serial/Serial Old收集器是最基本也是最古老的垃圾收集器,它是一個單線程收集器,並且在它進行垃圾收集時,必須暫停所有用戶線程,也就是發生“Stop the World”。一般JVM都不再使用該收集器。一般用於PC端的java應用,如百度雲盤的Windows客戶端、印象筆記的Windows客戶端等等。

Stop The World圖解說明

如上圖所示,當gc在回收垃圾時,其他的工作線程必須先停止,等到GC回收完成後,工作線程才能繼續執行。

形象點講就是, 媽媽在掃地時,你不能嗑瓜子了,要先停一下,她掃完你再繼續嗑瓜子,如果邊嗑瓜子邊打掃,那就太費神了。

6.2 ParNew

ParNew收集器是Serial收集器的多線程版本。新生代並行回收,採用複製算法,老年代串行回收,採用標記整理算法。所以,該收集器一般只用於新生代。通過JVM的參數設置,可以顯式指定使用ParNew作爲新生代的垃圾回收器。-XX:+UseParNewGC,只要加入這個選項,JVM啓動之後就會使用ParNew進行新生代的垃圾對象回收。在使用ParNew作爲生產環境的垃圾回收器時,記得使用-server指定爲服務端模式

6.3 CMS

CMS(Current Mark Sweep)收集器,目標是使回收停頓時間最短,也是多線程機制,採用標記整理算法,該回收器一般用於老年代,生產環境上也經常會使用該垃圾回收器與其它GC搭配使用。

CMS採取的策略是:垃圾回收線程和系統工作線程儘量並行執行

CMS在執行一次垃圾回收的過程一共爲4個階段:

  • 初始標記:就是標記“GC Roots”能夠引用到的對象,會進入“Stop the World”狀態
  • 併發標記:通過初始標記的對象,找到引用了該對象的其他所有對象,不會進入“Stop the World”狀態。比較耗時
  • 重新標記:由於在併發標記時,可能會存在已標記的對象又失去了引用的情況,所以這一步是停止掉工作線程,進入“Stop the World”,對併發標記後的對象重新標記。雖然進入“Stop the World”,但速度是很快的,因爲只是對第二階段中因爲並行而變動過的少數對象進行標記
  • 併發清理,清理標記的對象,該階段垃圾回收線程和工作線程是並行運行的,由於併發清理需要將垃圾對象從各種隨機的內存位置清理掉,所以也比較耗時。

性能分析:其中初始標記和重新標記階段雖然會”Stop the World“,但是耗時很短,所以影響不大;併發標記和併發清理階段雖然耗時較長,但是可以跟工作線程並行執行,所以影響也不大。

缺點:CPU消耗比較大

我們到現在還沒看到CMS 的整理部分,那它是怎麼處理內存碎片的呢?

如果內存碎片太多,會導致後續對象進入老年代找不到可用的連續空間,觸發Full GC。

CMS有一個參數-XX:+UseCMSCompactAtFullCollection(默認打開),表示是否要在Full GC之後進行Stop the World,停止工作線程,然後進行老年代的內存碎片整理。

還有另外一個參數-XX:CMSFullGCsBeforeCompaction,意思是執行多少次Full GC之後再執行一次內存碎片整理工作,默認是0,即每次Full GC之後都會進行一次內存碎片整理。

6.4 G1

G1 提供比“ParNew+CMS”組合更好的垃圾回收性能。

G1垃圾回收器是Jdk1.7的新特性之一,在Jdk1.7+版本都可以自主配置G1作爲JVM GC選項。G1垃圾回收器可以同時回收新生代和老年代的對象,它一個人就可以搞定所有的垃圾回收。

G1將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然G1還保留着新生代和老年代的概念,但它們只是邏輯上的,新生代和老年代不再是物理上隔閡的,而只是一部分Region的集合,每一個Region既可能屬於新生代,也可能屬於老年代.

剛開始時Region誰都不屬於,然後會先分配給新生代,當對象越來越多後,可能觸發G1對這個Region進行垃圾回收,然後下一次,這個Region可能又被分配給了老年代,用來存放長期存活對象.

垃圾回收的預期停頓時間

G1最大的特點就是,可以讓我們設置一個垃圾回收的預期停頓時間。比如我們可以指定:G1進行垃圾回收時,保證“Stop the World”的時間不超過1分鐘。

之前,我們採用ParNew+CMS時,爲了儘量減少GC次數,需要對JVM內存空間合理劃分,還要配置各種JVM參數。但是現在,我們可以直接給G1指定一個預期停頓時間,告訴它一段時間內因垃圾回收導致的系統停頓時間不能超過多久,剩下的全部交給G1全權負責,這樣就相當於我們可以直接控制GC對系統性能的影響

通過-XX:MaxGCPauseMills參數可以設定預期停頓時間,表示G1執行GC時最多讓系統停頓多長時間,默認200ms。

回收價值

G1之所以能夠做到控制停頓時間,是因爲它會追蹤每個Region裏的回收價值。所謂回收價值,是指每個Region裏有多少垃圾對象,如果進行回收,耗時多長,能夠回收掉多少。然後在垃圾回收的時候,G1就會判斷哪個Region更有回收價值.

根據回收價值進行GC,這個就是G1的核心設計思路

Region大小設置

G1的堆內存中,各個Region的大小是相同的,那麼要分配多少個Region呢?每個Region的大小爲多少?

JVM啓動時發現如果採用了G1作爲垃圾回收器(通過參數-XX:UseG1GC指定),那麼就會自動設置,默認2048個Regio,然後通過堆內存的大小(-Xms-Xmx)除以2048就能得到Region的大小了。Region的大小必須爲2的整數倍,如2MB、4MB、6MB等,可以通過-XX:G1HeapRegionSize參數手動指定,設置後,就會根據你設定的size大小計算region個數了。

動態Region

初始情況下,堆內存的5%空間爲新生代的大小,以4G堆內存來算,就是200MB的新生代,約100個Region。但是在系統運行期間,Region的數量是動態變化的,不過新生代最多佔比也不會超過60%。另外,一旦Region進行了垃圾回收,此時新生代的Region數量還會減少,這些其實都是動態的。

可以通過參數-XX:G1NewSizePercent來設置新生代的初始佔比,默認5%;通過參數-XX:G1MaxNewSizePercent來設置新生代的最大佔比,默認60%。

Eden和Survivor

G1垃圾回收器的新生代也有Eden和Survivor的劃分,同樣通過-XX:SurvivorRatio=8設置比例。比如說,新生代最初有100個Region,那Eden就佔80個,兩個Survivor各佔10個。

隨着對象不停的在新生代分配,屬於新生代的Region會不斷增加,Eden和Survivor對應的Region也會不斷增加。

垃圾回收原理

和CMS相同,採用的標記整理算法,也是分4步。但是它是對新生代和老生代進行共同回收,而CMS只是針對老生代

停止回收

由於在執行回收階段,基於複製算法,那就會不斷的空出一些Region,一旦空閒的Region數據量達到了堆內存的5%,就會立即停止回收,那麼本輪混合回收(Mixed GC)就結束了。可以通過參數-XX:G1HeapWastePercent配置這個空閒Region的佔比,默認爲5%。

回收失敗

由於在執行回收時,需要將存活對象拷貝到其他Region中,如果萬一在次過程中沒有空閒的Region可以承載存活對象,就會觸發Full GC。此時,JVM會立即停止程序,然後採用Serial Old收集器進行單線程標記、清除、壓縮整理,空出一批Region,這個過程是非常緩慢的。

優點:

  1. 把每次執行回收的時間控制在我們設置的預期停頓時間範圍內。
  2. 適合在大內存的機器上運行,可以完美解決大內存垃圾回收時間過長的問題

7. 各種GC分類

7.1 Minor GC/Young GC

當新生代的Eden區域被佔滿後,實際就需要觸發新生代的GC,這就是所謂的”Minor GC“,也可以稱之爲”Young GC“。

觸發時機:新生代的Eden區域被佔滿後。

7.2 Old GC

Old GC是僅僅針對老年代區域進行垃圾回收。

7.3 Full GC

Full GC則是針對新生代、老年代、永久代的全體內存空間進行垃圾回收.回收比較耗時間

觸發時機:老年代空間不夠。具體時機可細分爲以下幾種:

  1. 進行Young GC之前:如果老年代的連續可用內存空間 < 新生代歷次晉升的平均大小,此時先觸發一次Old GC清理老年代,然後再執行Young GC。
  2. 進行Young GC之後:如果存活對象要進入老年代,但是老年代的連續可用內存空間 < 存放對象的大小,此時必須觸發一次Old GC。
  3. 老年代的內存使用率超過了92%,此時也會觸發Old GC

8.4 Mixed GC

Mixed GC是G1垃圾回收器中特有的概念,在G1中,一旦老年代佔據了Java堆內存的45%,就會觸發Mixed GC,此時對新生代和老年代都進行垃圾回收。

觸發時機:G1特有,老年代空間佔據到Java堆內存的45%。G1新生代內存滿了會觸發Young GC

8.5 永久代GC

永久代一般存放着類信息、常量池等等。在進行Full GC的時候,會順帶對永久代進行GC,一般來說永久代裏的東西是不需要回收的,如果永久代真的滿了,回收之後也沒騰出足夠的空間來,就會拋出OOM異常。

9 調優

9.1 新生代調優

在系統內存不是很大的情況下,可以通過提升Eden和Survivor的空間,來容納更多的新生代對象。但是,當新生代的內存空間太大時,需要考慮每次Young GC的時間成本,傳統的ParNew回收器不太適合這種大內存場景,所以針對大內存機器建議使用G1進行垃圾回收

9.2 Full GC調優

當內存過小,導致新生代和老生代內存都不大,而數據又比較多的情況,很容易導致老生代內存溢出,而觸發Full GC,這種情況下首先要考慮的顯然就是擴內存。增大新生代和老生代內存,避免觸發Full GC

9.3 System.gc()

禁止在代碼中顯式調用System.gc()方法,因爲顯示調用後,很有可能會觸發Full GC。在訪問量很高的情況下,System.gc()方法被頻繁調用,會頻繁觸發Full GC。 所以GC需要完全交由JVM自己去處理

9.4 內存溢出

一般內存溢出就三個地方,方法區(元數據區),棧內存,堆內存

  • 方法區溢出:一般元數據區中的對象回收的條件是相當苛刻的,所以可以不考慮回收。JVM啓動時,元數據區默認情況只會分配幾十MB空間,所以生產環境一定要顯式指定該區域的大小,一般512MB就足夠了:--XX:MetaspaceSize=512m --XX:MaxMetaspaceSize=512m
  • 棧溢出:一般來說棧內存存放棧幀和方法中的參數,當棧幀出棧後內存就會即時的回收,但是如果出現像遞歸這種只入不出的情況,就會導致棧溢出.所以一般要注意代碼寫法
  • 堆溢出:Young GC過後的存活對象首先會先嚐試進行一塊Survivor區,如果Survivor區無法容納,則嘗試進入老年代,如果此時老年代也滿了就會觸發Full GC。但是,如果Full GC之後,老年代的空間還是不夠, 這時只能拋出內存溢出異常了。所以,堆內存溢出的原因,總結起來就是一句話:有限的內存中放了過多的對象,而且大多數對象是存活的,此時要繼續放入更多對象已經不可能了,只能拋出內存溢出異常。所以解決的話就是針對代碼的優化,內存大小的設置等。基本的分析思路就是dump出事發現場的內存快照,然後通過MAT進行查看,分析出內存佔用最多的對象,然後分析線程調用棧,找到代碼位置,最後進行優化即可

9.5 內存溢出和內存泄漏

內存泄漏表示:對象已經沒有被應用程序使用,但是垃圾回收器沒辦法移除他們,因爲還在被引用着,比如我在類中定義了一個靜態成員變量且同時還給這個變量new了一個對象。這種解決辦法,比如可以使用懶加載模式,或者使用上面說過的弱引用或軟引用。

內存溢出就表示,我需要2G內存存放對象,但是jvm一共就1個G內存,內存不夠放,解決辦法上面說過了

10 打印GC日誌

需要在系統的JVM參數中加入GC日誌的打印選型:

  • -XX:+PrintGCDetails:打印詳細的GC日誌
  • -XX:+PrintGCTimeStamps:打印每次GC發生的時間
  • -Xloggc:gc.log:設置將GC日誌寫入一個磁盤文件

以idea爲例:

然後就可以分析堆幀情況了

11.  JVM調優工具

我們常用的調優/檢測工具有:

jps: 可以查看jvm各個運行程序的端口,通過端口纔可以使用jstat,jmap等命令操作指定的jvm運行程序

jstat:用於監視JVM運行時狀態信息的命令,它可以顯示出虛擬機進程中的類裝載、內存、垃圾收集、JIT編譯等運行數據。一般用來查看內存的大致情況。

jmap:用於生成heap dump文件,jmap可以查詢當前Java堆內存的詳細信息(什麼對象佔據了大量的內存),比如當前各個區域使用率(總容量、已使用、未使用)、當前使用的是哪種收集器等。

jhat:一般與jmap搭配使用,用來分析jmap生成的dump文件,jhat內置了一個微型的HTTP/HTML服務器,生成dump的分析結果後,可以在瀏覽器中查看。但是一般不會直接在服務器上進行分析,因爲jhat是一個耗時並且耗費硬件資源的過程。

MAT: MAT分析dump文件所需要的額外內存比jhat要小的多的多,所以建議使用MAT來進行分析。且一般分析時,都是講dump文件複製到其他電腦上去分析

jconsole: java自帶的可視化工具,可實時查看內存,線程等詳細情況

VisualVm:  和jconsole相同,都是可視化工具,但是功能比jconsole略多一點,比如說垃圾回收次數等

12. 壓力測試

使用jmeter,通過該工具可以模擬併發的http請求,用來測試jvm性能

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章