天天看點

JVM GC和類加載機制

Java記憶體模型

 線程之間的共享變量存儲在主記憶體(main memory)中,每個線程都有一個私有的本地記憶體(local memory),本地記憶體中存儲了該線程以讀/寫共享變量的副本。

JVM GC和類加載機制

JVM記憶體結構

JVM GC和類加載機制

擴充

棧:Java棧總是和線程關聯在一起,每當建立一個線程,JVM就會為此線程建立一個Java棧;Java棧中有許多棧幀,棧幀與方法關聯在一起,每運作一個方法就會建立一個棧幀。

垃圾回收算法

标記清除法

先标記出需要回收的對象,然後一次性回收。缺點:會産生記憶體碎片,并且效率也不高。

标記壓縮法

先标記出需要回收的對象,然後讓存活對象向一端移動,移動的過程中進行回收辣雞。避免了記憶體碎片問題。

複制算法

把記憶體劃分出相等的兩塊,每次隻用其中一塊。當一塊用完了,就将還存活的對象移動到另一塊記憶體上,然後把辣雞清理掉。記憶體配置設定不用來考慮碎片問題,隻需要順序配置設定即可。缺點:總有一塊記憶體閑着沒事幹。

如圖

JVM GC和類加載機制

預設年輕代和老年代比例為1:2,Eden區和存活區的比例為8:1:1

JVM GC和類加載機制

年輕代GC(Minor GC):發生在年輕代的垃圾收集動作。

老年代GC(Major GC):發生在老年代的垃圾收集動作。

Full GC:對整個堆空間的垃圾收集。

什麼樣的對象需要回收

JVM主要靠“可達性分析”來判定對象是否存活的。也就是從一系列 GC根作為起點向下搜尋,如果從GC根到這個對象可達,證明這個對象是存活對象,否則如果不可達,則證明這是個需要回收的對象

可以作為GC根的對象:

1. 虛拟機棧中引用的對象,具體來說是棧幀的局部變量表中

2. 方法區中類靜态屬性引用的對象

3. 方法區中常量引用的對象

4. Native方法引用的對象

關于可達性分析,如下圖所示,兩個紅色的對象雖然互相引用,但是從GC根到它們不可達,是以依然是可回收對象。

JVM GC和類加載機制

垃圾回收過程

首先新的對象都會放在Eden區,當Eden區沒有足夠空間進行配置設定時,會觸發一次Minor GC,把存活的對象放在其中一個存活區S0。當Eden區再次填滿,除了把Eden區中存活對象移動到S1中,S0中的存活對象也會被移動到S1,并删除垃圾對象,S0騰空。之後的垃圾回收會重複此過程。這裡為什麼要用兩個存活區,主要為了解決記憶體碎片問題。

當一個對象被重複回收達到一定門檻值之後仍然存活,則将此移動到老年代。當老年代空間使用達到一定的門檻值,則會觸發針對老年代的收集Major GC。

對于CMS收集器,老年代回收啟動的門檻值為92%(JDK1.6之前是68%)

對于G1收集器,老年代回收啟動的門檻值是45%

進入老年代的條件:

  • YGC時,To Survivor區不足以存放存活的對象,對象會直接進入到老年代。
  • 經過多次YGC後,如果存活對象的年齡達到了設定門檻值,則會晉升到老年代中。
  • 動态年齡判定規則,To Survivor區中相同年齡的對象,如果其大小之和占到了 To Survivor區一半以上的空間,那麼大于此年齡的對象會直接進入老年代,而不需要達到預設的分代年齡。
  • 大對象:由-XX:PretenureSizeThreshold啟動參數控制,若對象大小大于此值,就會繞過新生代, 直接在老年代中配置設定。
觸發FullGC條件:
  • 無法容納新晉升上來的對象時,會觸發FullGC。
  • Metaspace(元空間)在空間不足時會進行擴容,當擴容到了-XX:MetaspaceSize 參數的指定值時,也會觸發FullGC。
  • System.gc() 或者Runtime.gc() 被顯式調用時,觸發FullGC。
程式排查FullGC:
  • 大對象:系統一次性加載了過多資料到記憶體中(比如SQL查詢未做分頁),導緻大對象進入了老年代。
  • 記憶體洩漏:頻繁建立了大量對象,但是無法被回收(比如IO對象使用完後未調用close方法釋放資源),先引發FGC,最後導緻OOM.
  • 程式頻繁生成一些長生命周期的對象,當這些對象的存活年齡超過分代年齡時便會進入老年代,最後引發FGC. 
  • 動态生成了很多新類,使得 Metaspace 不斷被占用,先引發FGC,最後導緻OOM.
  • 代碼中顯式調用了gc方法,包括自己的代碼甚至架構中的代碼。
  • JVM參數設定問題:包括總記憶體大小、新生代和老年代的大小、Eden區和S區的大小、元空間大小、垃圾回收算法等等。

永久代的垃圾回收

hotspot的方法區存放在永久代中,是以方法區被人們稱為永久代。永久代的垃圾回收主要包括類型的解除安裝和廢棄常量池的回收。

對于常量池:Java1.7之前,不斷将新常量添加到方法區,會導緻方法區溢出。Java1.7中,運作時常量池已從永久代移除,轉移到堆中,不斷添加新常量的方法不再導緻方法區溢出。Java1.8開始,廢棄了永久代,取而代之的是一個中繼資料區的存儲空間。

對于類資訊:在大量使用反射、動态代理CGLib等位元組碼架構、動态生成JSP以及OSGI這類頻繁自定義ClassLoader的場景都需要虛拟機具備類解除安裝的功能,以保證永久代不會溢出。

在G1收集器中,隻有進行Full GC才會觸發永久代的回收,反過來,永久代滿了之後也會觸發Full GC。

Java8為什麼廢棄方法區(永久代)?

永久代經常不夠用導緻記憶體溢出或者發生記憶體洩漏。

類方法資訊的大小難于确定,也就是說永久代大小的指定很困難。

元空間(MetaSpace)

元空間是方法區的實作,主要用于存儲類資訊、靜态變量等。方法區邏輯上屬于堆的一部分,但是為了與堆進行區分,通常又叫“非堆”。

元空間的本質和永久代類似,都是對JVM規範中方法區的實作。元空間與永久代之間最大的差別在于:元空間并不在虛拟機中,而是使用本地記憶體。

垃圾回收器

串行收集器

JVM GC和類加載機制

串行辣雞收集器隻用一個單線程做所有工作,其記憶體占用空間大小也是所有辣雞收集器裡最低的。

  • 新生代辣雞回收使用複制算法
  • 老年代使用标記壓縮算法

并行收集器

JVM GC和類加載機制

并行收集器相對于串行收集器,使用了多線程來完成辣雞回收的工作,但是同樣也需要Stop-The-World。新生代和老年代的回收都是并行的。

(曆史:剛引入的時候,新生代使用多線程,而老年代則是單線程進行辣雞回收。随着堆的尺寸和老年代對象的數量和大小不斷增長,老年代辣雞回收的時間不斷變長,增加了一個多線程的老年代收集器和多線程的新生代收集器同時使用的方式,由此得到了增強的并行辣雞收集器)

  • -XX:+UseParallelGC  新生代使用并行收集器,老年代使用串行收集器
  • -XX:+UseParallelOldGC  新生代和老年代都使用并行收集器
  • 新生代使用複制算法

CMS收集器(Concurrent Mark Sweep)

JVM GC和類加載機制

在CMS垃圾回收中,新生代的回收與并行垃圾收集器很類似。它們是多線程的并且會使應用程式線程暫停。主要差別在于老年代的收集上。

CMS做垃圾回收的時候與應用線程同時進行,除了少數的相對短暫的GC同步暫停,可以說是大多數情況是并發進行的。

CMS老年代收集活動從初始标記開始,這個階段标記GC根可以直接關聯到的對象,這個階段是要暫停應用線程的。之後進入并發标記階段,這個階段标記所有存活對象,和應用線程一同運作。接着,進入重新标記階段,這個階段主要處理初始标記,并發标記過程中可能錯過的對象,這個階段是要暫停應用線程的。最後,并發清除啟動,釋放所有死亡對象所占用的空間。

  • 挑戰1:要在應用消耗完Java的可用堆之前完成并發收集工作,是以選擇一個合适的時機來啟動這個并發收集工作尤為重要。
  • 挑戰2:處理老年代中的空間碎片。如果老年代中空間碎片太小,無法容納剛晉升上來的對象,因為CMS并發收集循環中并不執行壓縮,是以可能導緻CMS回過來使用串行GC,觸發一次full收集,導緻一個漫長的暫停。
  • 缺點:1. 老年代主要使用标記清除算法,不進行壓縮,清理碎片顯得很重要。2. 不能處理浮動垃圾,也就是在回收過程中新産生在标記過程之後的垃圾無法處理。
  • -XX:+UseCMSCompactAtFullCollection   垃圾收集完成後,進行一次記憶體碎片整理
  • -XX:CMSFullGCsBeforeCompaction  回收一定次數後,壓縮一次記憶體

JDK 8中兩個主要的并發收集器:

  并發标記掃描(CMS)收集器:此收集器适用于喜歡較短垃圾收集暫停且可以與垃圾收集共享處理器資源的應用程式(并發收集&低停頓)。

  Garbage-First垃圾收集器:這種伺服器式收集器适用于具有大記憶體的多處理器機器。它以高機率滿足垃圾收集暫停時間目标,同時實作高吞吐量。

G1收集器(Garbage First)

G1 是一種低延時的垃圾回收器,旨在避免Full GC。G1把Java堆拆成一系列分區,這樣的話,在某一個時間段内,大部分辣雞回收隻在一個區内而不是整個堆中進行。區域大小可以從1 MB到32 MB不等,具體取決于堆大小,每個分區的大小必須是2的幂。目标是不超過2048個分區。伊甸區,幸存區和老年代是這些地區的邏輯集合,并不是連續的。

年輕代預設是整個Java堆尺寸的5%,最大60%;

預設老年代堆空間占用超過45%,就會啟動一次老年代收集。巨型對象:大小≥分區空間50%的對象。G1裡的full GC使用的是與串行收集器相同的算法。發生full GC時,執行對整個記憶體堆的全面壓縮。

一個新概念:新生代不再是一個連續記憶體塊,一個分區既可以變成新生代,也可以變成老年代。

堆被劃分為一組大小相同的堆區域,每個區域都是一個連續的虛拟記憶體區域。G1執行一個并發全局标記階段,以确定整個堆中對象的活性。

标記階段完成後,G1知道哪些區域大部分是空的。它首先收集這些區域,這通常會産生大量的自由空間。這就是為什麼這種垃圾收集方法被稱為Garbage-First。

G1将對象從堆的一個或多個區域複制到堆上的單個區域,并且在此過程中壓縮并釋放記憶體。這個過程是多線程執行,以減少暫停時間并提高吞吐量。是以,随着每次垃圾收集,G1不斷努力減少碎片。這超出了以前兩種方法的能力->CMS(Concurrent Mark Sweep)垃圾收集不進行壓縮。并行壓縮僅執行整堆壓縮,這會導緻相當長的暫停時間。

JVM GC和類加載機制

所有Eden區+幸存區=新生代

..

JVM GC和類加載機制

新生代的收集和前面的沒啥差別,都要暫停應用線程。老年代比CMS的老年代收集還要與衆不同。↓↓

一個G1并發周期包含:初始标記、并發根分區掃描、并發标記、重新标記和清除。

在G1中,一旦達到記憶體堆的占用門檻值[yu zhi],一次并發stop-the-world方式的初始标記階段就會被安排執行,在此階段标記所有GC根,根是對象圖的起點。這個階段會跟着下一次新生代收集同時進行。然後進入并發根分區掃描,掃描和跟蹤survivor分區裡所有對象的引用,唯一的限制是在下一次GC前必須先完成掃描,因為一次新的GC會産生一個新的存活對象集合,它跟初始标記的存活對象是有差別的。

然後進入并發标記階段,标記老年代中所有存活對象。當并發标記階段結束,并行stop-the-world的重新标記階段就被啟動,标記那些因為在标記階段同時執行的應用線程導緻産生的錯過的對象。重新标記結束,就執行清除階段,優先回收沒有任何存活對象的分區,然後把每個收集過辣雞的分區中的存活對象轉移到一個可用分區中,一旦存活對象被轉移,那麼這個分區(新生代或者老年代)就可以被回收為可用分區。

G1最大的暫停時間來源于 新生代收集和混合收集(新生代和老年代一起)

特點:

  • 低延時收集器,旨在避免full GC
  • G1可以根據使用者設定的暫停時間目标自動調整年輕代和總堆大小,暫停目标越短年輕代空間越小、總空間就越大。
  • 盡可能多的回收垃圾(Garbage First),采用啟發式收集算法,在老年代找出具有高收集收益的分區進行收集(CMS則會在将要耗盡記憶體時候再回收).
  • 無記憶體碎片:與CMS的“标記-清除”算法不同,G1從整體來看是基于“标記-壓縮”算法實作的收集器;從局部上來看是基于“複制”算法實作的。

收集器搭配

JVM GC和類加載機制

Serial:-XX:+UseSerialGC

ParNew:-XX:+UseParNewGC

Parallel Scavenge :-XX:+UseParallelGC

Parallel Old :-XX:-UseParallelOldGC(替代Serial Old)

CMS:-XX:+UseConcMarkSweepGC

G1:-XX:+UseG1GC

為什麼JVM要有類加載機制?

1. 任意一個類,在JVM中必須是唯一的存在。基于雙親委派模型設計的類加載器,經過層層傳遞,加載請求最終都會被BootstrapClassLoader所響應,保證了類的唯一性。

2. 考慮到安全因素,Java核心API中定義的類不會被随意替換,假設通過網絡傳遞一個名為java.lang.Integer的類,通過雙親委托模式傳遞到啟動類加載器,而啟動類加載器在核心API發現存在名字的類并且該類已被加載,則不會加載不明來源的java.lang.Integer,而是直接傳回已加載過的Integer.class,這樣便可以防止核心API庫被随意篡改。

類加載過程

主要為五個階段:加載、驗證、準備、解析、初始化。

1、加載

”加載“是”類加機制”的第一個過程,在加載階段,虛拟機主要完成三件事:

(1)通過一個類的全限定名來擷取其定義的二進制位元組流

(2)将這個位元組流所代表的的靜态存儲結構轉化為方法區的運作時資料結構

(3)在堆中生成一個代表這個類的Class對象,作為方法區中這些資料的通路入口。

相對于類加載的其他階段而言,加載階段是可控性最強的階段,因為程式員可以使用系統的類加載器加載,還可以使用自己的類加載器加載。

2、驗證

驗證的主要作用就是確定被加載的類的正确性。也是連接配接階段的第一步。說白了也就是我們加載好的.class檔案不能對我們的虛拟機有危害,是以先檢測驗證一下。他主要是完成四個階段的驗證:

(1)檔案格式的驗證:驗證.class檔案位元組流是否符合class檔案的格式的規範,并且能夠被目前版本的虛拟機處理。這裡面主要對魔數、主版本号、常量池等等的校驗(魔數、主版本号都是.class檔案裡面包含的資料資訊、在這裡可以不用了解)。

(2)中繼資料驗證:主要是對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合java語言規範的要求,比如說驗證這個類是不是有父類,類中的字段方法是不是和父類沖突等等。

(3)位元組碼驗證:這是整個驗證過程最複雜的階段,主要是通過資料流和控制流分析,确定程式語義是合法的、符合邏輯的。在中繼資料驗證階段對資料類型做出驗證後,這個階段主要對類的方法做出分析,保證類的方法在運作時不會做出威海虛拟機安全的事。

(4)符号引用驗證:它是驗證的最後一個階段,發生在虛拟機将符号引用轉化為直接引用的時候。主要是對類自身以外的資訊進行校驗。目的是確定解析動作能夠完成。

對整個類加載機制而言,驗證階段是一個很重要但是非必需的階段,如果我們的代碼能夠確定沒有問題,那麼我們就沒有必要去驗證,畢竟驗證需要花費一定的的時間。當然我們可以使用-Xverfity:none來關閉大部分的驗證。

3、準備

準備階段主要為類變量配置設定記憶體并設定初始值。這些記憶體都在方法區配置設定。在這個階段我們隻需要注意兩點就好了,也就是類變量和初始值兩個關鍵詞:

(1)類變量(static)會配置設定記憶體,但是執行個體變量不會,執行個體變量主要随着對象的執行個體化一塊配置設定到java堆中,

(2)這裡的初始值指的是資料類型預設值,而不是代碼中被顯示賦予的值。比如

public static int value = 1; //在這裡準備階段過後的value值為0,而不是1。指派為1的動作在初始化階段。      

4、解析

解析階段主要是虛拟機将常量池中的符号引用轉化為直接引用的過程。什麼是符号引用和直接引用呢?

  • 符号引用:以一組符号來描述所引用的目标,可以是任何形式的字面量,隻要是能無歧義的定位到目标就好,就好比在班級中,老師可以用張三來代表你,也可以用你的學号來代表你,但無論任何方式這些都隻是一個代号(符号),這個代号指向你(符号引用)
  • 直接引用:直接引用是可以指向目标的指針、相對偏移量或者是一個能直接或間接定位到目标的句柄。和虛拟機實作的記憶體有關,不同的虛拟機直接引用一般不同。

解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符号引用進行。

5、初始化

這是類加載機制的最後一步,在這個階段,java程式代碼才開始真正執行。我們知道,在準備階段已經為類變量賦過一次值。在初始化階端,程式員可以根據自己的需求來指派了。一句話描述這個階段就是執行類構造器< clinit >()方法的過程。

在初始化階段,主要為類的靜态變量賦予正确的初始值(JVM負責對類進行初始化,主要對靜态變量進行初始化)。在Java中對類變量進行初始值設定有兩種方式:

①聲明類變量是指定初始值

②使用靜态代碼塊為類變量指定初始值

JVM初始化步驟

1、假如這個類還沒有被加載和連接配接,則程式先加載并連接配接該類

2、假如該類的直接父類還沒有被初始化,則先初始化其直接父類

3、假如類中有初始化語句,則系統依次執行這些初始化語句

類初始化時機:隻有當對類的主動使用的時候才會導緻類的初始化,類的主動使用包括以下六種:

  1. 建立類的執行個體,也就是new的方式
  2. 通路某個類或接口的靜态變量,或者對該靜态變量指派
  3. 調用類的靜态方法
  4. 反射(如 Class.forName(“com.shengsiyuan.Test”))
  5. 初始化某個類的子類,則其父類也會被初始化
  6. Java虛拟機啟動時被标明為啟動類的類( JavaTest),直接使用 java.exe指令來運作某個主類

 問題排查思路

排查CPU100%

1. top -c

2. top -Hp [pid]             檢視該程序各個線程的cpu使用情況

3. printf "%x\n" 20037       PID的16進制

4. jstack 20037 | grep 4e45 -C5 線程20037(16進制為4e45)的堆棧

頻繁GC

jstat -gc [pid] 1000        每隔1000ms輸出一次gc情況

記憶體洩漏

1. jmap -histo:live [pid] | more      堆内對象

2. jmap -heap [pid]        記憶體配置設定

top指令

輸入top之後,鍵盤輸入P,按照CPU排序;輸入M,按照記憶體占用排序

小LUA

面對敵人的嚴刑逼供,我一個字也沒說,而是一五一十寫了下來。