面試題:JVM與G1要點總結

一.Java內存區域

1.運行時數據區的介紹

2.站在線程的角度看Java內存區域

3.深入分析堆和棧的區別

4.方法的出入棧和棧上分配、逃逸分析及TLAB

5.虛擬機中的對象創建步驟

6.對象的內存布局

1.運行時數據區的介紹

運行時數據區的類型:程序計數器、Java虛擬機棧、本地方法棧、Java堆、方法區(運行時常量池)、直接內存。

程序計數器:當前線程所執行的字節碼的行號指示器,占用內存空間小,線程私有,各線程間獨立存儲,互不影響。Java虛擬機規范中程序計數器不會出現OOM情況。

Java虛擬機棧:是線程私有的,其生命周期和線程同生共死。每個方法在開始執行時,Java虛擬機會同步創建一個棧幀用于存儲:局部變量表、操作數棧、動態鏈接、方法出口等信息。每個方法的執行,對應著棧幀在虛擬機棧中入棧和出棧的過程。這也是Java方法執行的線程內存模型,可用-Xss進行虛擬機棧大小的設置,默認為1M。

如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常。如果Java虛擬機棧容量可以動態擴展,當棧擴展時無法申請到足夠的內存也會拋出OutOfMemoryError異常。注意:HotSpot虛擬機的棧容量是不能動態擴展的。

本地方法棧:作用和Java棧一樣,本地方法棧保存的是native方法的信息。當一個JVM創建的線程調用native方法后,JVM不再為其創建棧幀。JVM只會簡單地動態鏈接并直接調用native方法。本地方法棧也會在棧深度溢出或者棧擴展失敗時,分別拋出StackOverflowError和OutOfMemoryError異常。

Java堆:Java中幾乎所有對象實例都在堆上分配內存,因為部分對象由于逃逸分析、棧上分配、標量替換而不在堆上分配的。Java堆是Java開發者需要重點關注的一塊區域,因為涉及到內存的分配(new關鍵字、反射)與回收(回收算法、收集器)。

如果從分配內存的角度看,所有線程共享的Java堆中可以劃分出多個線程私有的分配緩沖區(TLAB)。在Java堆中的這些線程私有的分配緩沖區(TLAB)可以提升對象分配效率,而將Java堆細分的目的就是為了更好地回收內存,或者更快地分配內存。

當前主流的Java虛擬機對Java堆都支持可動態擴展。如果在Java堆中沒有內存完成實例分配,并且堆也無法再擴展時,Java虛擬機將會拋出OutOfMemoryError異常。-Xms:堆的最小值,-Xmx:堆的最大值,-Xmn:新生代的大小。

方法區:方法區和Java堆一樣,是共享的區域,所有線程都可以共享這一區域。方法區主要存放:類信息、常量、靜態變量和JIT編譯后的代碼緩存等。在JVM啟動的時候,方法區就被創建為固定大小或可動態擴容的區域。方法區在邏輯上屬于堆的一部分,但一些簡單的實現不會進行GC回收。因而方法區可看作是獨立于Java堆的一塊空間。

方法區的垃圾回收主要包含兩部分內容:廢棄的常量、不再使用的類。判斷一個常量是否廢棄:沒有任何地方引用該常量。判斷一個類是否不再使用的條件如下:該類所有的實例都已被回收,堆中不存在該類及其子類的實例、加載該類的類加載器已被回收,通常該條件很難達成、該類對應的java.lang.class對象沒有在任何地方被引用(如反射)。

在大量使用反射、動態代理、CGLib等字節碼框架,動態生成JSP以及OSGi這類頻繁自定義類加載器的場景中,通常需要JVM具備卸載類的能力,保證不會對方法區造成過大內存壓力。

運行時常量池:運行時常量池是方法區的一部分,運行時常量池用于存放編譯期生成的各種字面量和符號引用。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池表(Constant Pool Table)。常量池表用于存放編譯期生成的各種字面量與符號引用,這部分內容將在類加載后存放到方法區的運行常量池中。

2.站在線程的角度看Java內存區域

一個線程在一個時刻只能運行一個方法,只能運行一行代碼,所以一個線程只需要一個本地方法棧,只需要一個程序計數器。

虛擬機棧、本地方法棧、程序計數器是和線程同生共死的。Java堆、方法區是和Java進程同生共死的。GC是不發生在棧上的,GC只發生在堆和方法區上。一個線程調用本地方法的話,會開辟一個虛擬機棧和本地方法棧。

3.深入分析堆和棧的區別

(1)堆和棧在功能上的區別

棧內存會以棧幀的方式存儲方法調用的過程和所使用的變量。方法調用過程中使用的變量包括:基本數據類型變量和對象的引用變量。其內存分配在棧上,里面的變量出了作用域就會自動釋放。堆內存用來存儲Java中的對象。無論是成員變量、局部變量、還是類變量,這些變量指向的對象都是存儲在堆內存的。

(2)堆和棧是線程獨享還是線程共享

棧內存歸屬于單個線程,每個線程都會有一個棧內存,其存儲的變量只能在其所屬線程中可見。棧內存可理解成線程的私有內存,堆內存中的對象對所有線程可見,堆內存中的對象可被所有線程訪問。

(3)堆和棧的空間大小對比

棧的內存要遠遠小于堆內存,棧的深度是有限制的,可能發生StackOverFlowError問題。

(4)堆中創建出來的類的對象不包含類的成員方法

Java堆是用來存放動態產生的數據,比如new出來的對象,注意創建出來的對象只包含屬于各自的成員變量,并不包括成員方法。因為同一個類的對象擁有各自的成員變量,存儲在堆中,但是它們共享該類的方法,并不是每創建一個對象就復制成員方法一次。

4.方法的出入棧和棧上分配、逃逸分析及TLAB

(1)方法會打包成棧幀

一個棧幀至少要包含局部變量表、操作數棧和幀數據區。執行任何一個方法時,方法會打包成一個棧幀。

(2)棧上分配

幾乎所有的對象都是在堆上分配的,棧上分配就是虛擬機提供的一種優化技術。其基本思想是將線程私有的對象打散分配在棧上,而不分配在堆上。這樣的好處是對象跟著方法調用自行銷毀,不需要進行垃圾回收,從而提高性能。

(3)逃逸分析

棧上分配需要的技術基礎:逃逸分析。逃逸分析的目的是判斷對象的作用域是否會逃逸出方法體。注意:任何可以在多個線程間共享的對象,一定都屬于逃逸對象。

(4)線程本地分配緩沖TLAB

TLAB全稱是ThreadLocalAllocBuffer,線程本地分配緩沖。創建對象是在堆上分配的,需要在堆上申請指定大小內存的。一個堆一塊區域,線程A進來分配區域a,線程B進來分配區域b。

如果有大量線程同時申請堆上的內存,為避免兩個線程申請內存時不會申請同一塊內存,需要對申請進行加鎖。加鎖不僅在并發編程時會有,虛擬機在實現時同樣要考慮并發而加鎖;如果不加鎖,就有可能兩個線程同時分配到同一塊內存,導致數據錯亂。

我們經常會new一個對象出來,所以內存分配是一個非常頻繁的動作。因此內存分配時也就需要頻繁加鎖,而頻繁加鎖就會影響性能。一旦加鎖,這種動作就會變成串行的模式,對性能影響很大。

所以TLAB的作用就是:它會事先在堆里面為每個線程分配一塊私有內存,在線程A中new出的對象只在線程A的私有內存上進行分配。

所以TLAB的好處就是:由于線程的堆內存事前分配好了,因此同時分配時就不存在競爭了。從而大大提高了分配的效率,當私有內存用完了再重新申請繼續使用。不過要注意的是,重新申請堆內存的動作還是需要保證原子性的。

TLAB涉及到線程私有,每個線程在new對象時會在私有內存上分配內存。盡管線程A在私有內存區域a位置擁有一塊私有內存并在上面分配了對象,但是這些對象對所有線程都是可見并可用的。也就是說這些A線程的對象在分配的時候只能在a區域分配而已,B線程、C線程也是可以看見它們并使用它們的。

5.虛擬機中的對象創建步驟

Java程序幾乎無時無刻都有對象被創建出來,虛擬機碰到一個new關鍵字時是如何創建對象的呢?

步驟一:進行類加載

步驟二:為對象分配內存

步驟三:為分配的內存空間初始化零值

步驟四:設置對象的對象頭

步驟五:對象初始化

步驟一:進行類加載

首先檢查new指令的參數是否能在常量池中定位到一個類的符號引用,并檢查該符號引用代表的類是否被加載、解析和初始化過。如果沒有,則執行相應的類加載過程。

步驟二:為對象分配內存

類加載完成后,虛擬機就要為這個新生對象分配內存,也就是需要把一塊確定大小的內存從Java堆中劃分出來。

如果Java堆中的內存是絕對規整的,所有用過的內存都放一邊,空閑的內存放另一邊,并且中間放一個指針作為已用內存和空閑內存的分界點的指示器,分配內存時就把該指針向空閑空間那邊移動一段與對象大小相等的距離,這種分配方式稱為指針碰撞。

如果Java堆中的內存并不是規整的,已使用的內存和空閑的內存相互交錯,那就沒有辦法進行指針碰撞了,此時虛擬機就必須要維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的記錄,這種分配方式稱為空閑列表。

選擇哪種分配方式由Java堆是否規整決定,而Java堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。當使用Serial、ParNew等帶壓縮整理過程的收集器時,系統采用的分配算法是指針碰撞,既簡單又高效。當使用CMS這種基于清除算法的收集器時,理論上采用空閑列表來分配內存,但實際為了分配更快也加入指針碰撞。

除如何劃分可用空間外,還有另外一個需要考慮的問題是對象創建在虛擬機中是非常頻繁的行為。即使僅僅修改一個指針所指向的位置,在并發情況下也不是線程安全的。可能出現正在給對象A分配內存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存的情況。

解決這個問題有兩種方案:一種是對分配內存空間的動作進行同步處理,實際上虛擬機采用CAS+失敗重試的方式保證更新操作的原子性。另一種是把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊私有內存TLAB。

開啟使用TLAB本地線程分配緩沖(Thread Local Allocation Buffer)時,在線程初始化時會申請一塊指定大小的內存,只給當前線程使用。這樣每個線程都單獨擁有一個Buffer,如果要分配內存,就在自己的Buffer上分配,這樣就不存在競爭的情況。當Buffer容量不夠的時候,再重新從Eden區域申請一塊內存繼續使用。

TLAB的目的是在為新對象分配內存內存時,讓每個Java應用線程用自己專屬的分配指針來分配內存,減少同步開銷。TLAB只是讓每個線程擁有私有的分配指針,創建的對象還是線程共享的。當一個TLAB用完了(分配指針top撞上end了),那么就重新申請一個TLAB。

步驟三:為分配的內存空間初始化零值

內存分配完成后,虛擬機就需要將分配到的內存空間都初始化為零值。這一步操作保證了對象的實例字段在代碼中可以不賦初始值就直接使用,也就是程序能訪問到這些字段的數據類型所對應的零值。

步驟四:設置對象的對象頭

內存空間初始化零值后,虛擬機就要對對象進行必要的設置,需要在對象的對象頭中設置這些內容:這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。

步驟五:對象初始化

設置完對象的對象頭信息后,從虛擬機視角來看,一個新的對象已產生。但從Java程序視角來看,對象創建才剛剛開始,所有的字段都還為零值。所以,執行new指令后會接著把對象按照程序員的意愿進行初始化,這樣一個真正可用的對象才算完全產生出來。

6.對象的內存布局

在HotSpot虛擬機中,對象在內存中存儲的布局可以分為3塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。

對象頭的第一部分是用于存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖等。

對象頭的第二部分是類型指針,即對象指向它的類的元數據的指針,JVM虛擬機可以通過這個指針確定這個對象是哪個類的實例。

對象頭的第三部分是對齊填充,它僅僅起著占位符的作用。因為HotSpot的自動內存管理系統要求對象的大小必須是8字節的整數倍。當對象其他數據部分沒有對齊時,就需要通過對齊填充來補全。

二.垃圾回收機制

1.如何判斷對象存活

2.各種引用介紹

3.垃圾收集的算法

4.垃圾收集器的設計

5.Serial和Serial Old收集器

6.ParNew收集器

7.Parallel Scavenge/Parallel Old收集器

8.Concurrent Mark Sweep(CMS)收集器

1.如何判斷對象存活

(1)引用計數算法

給對象添加一個引用計數器,每當一個地方引用它時就將計數器加1,當引用失效時就將計數器減1,任何時刻計數器為0的對象都不再被使用。這種算法簡單,但是有個致命的缺點,就是不能用于相互引用的情況。優點:快、方便、實現簡單。缺點:對象相互引用時,很難判斷對象是否應被回收。PHP、Python的垃圾回收就是使用了引用計數算法,Java的垃圾回收使用的是可達性分析。

(2)可達性分析算法

通過一系列稱為"GC Roots"的根對象作為起始點集,根據引用關系從這些節點往下搜索,搜索走過的路徑稱為引用鏈(Reference Chain)。當一個對象到"GC Roots"之間不存在任何引用鏈的時候,就表示這個對象不可達,不可用了。

在Java中,可作為GC Roots的對象包括:

一.在虛擬機棧(棧幀中的本地變量表)中引用的對象,比如各線程棧幀對應的方法中用到的參數、局部變量、臨時變量
二.方法區中靜態屬性引用的對象,比如Java類的引用類型的靜態變量
三.方法區中常量引用的對象,比如字符串常量池(StringTable)里的引用
四.本地方法棧中引用的對象
五.Java虛擬機內部的引用對象,比如基本數據類型對應的Class、系統類加載器、一些常駐的異常對象如NullPointException和OutOfMemoryError等
六.被同步鎖(Synchronized)持有的對象
七.反映Java虛擬機內部情況的對象,比如JMXBean、JVMTI中的回調、本地代碼緩存等
從"GC Roots"出發到達不了的那些對象都是可以被回收的。這里從"GC Roots"出發的引用鏈中的"引用"包括4種引用:強引用、軟引用、弱引用、虛引用。

2.各種引用介紹

(1)強引用

一般Object obj = new Object()就屬于強引用。

(2)軟引用(SoftReference)

一些有用但并非必需的對象,可以使用軟引用進行關聯。在系統將要發生OOM之前,這些軟引用對象才會被回收。如果這些軟引用對象被回收后內存還不夠,才會發出OOM異常。

(3)弱引用(WeakReference)

弱引用對象是一些有用(程度比軟引用更低)但是并非必需的對象。弱引用關聯的對象,只能生存到下一次GC之前。WeakHashMap就用到了弱引用。GC發生時,不管內存夠不夠,弱引用都會被回收。

(4)虛引用(PhantomReference)

也叫幽靈引用,最弱的。一個對象是否存在虛引用對它的生存完全不構成任何影響,同時通過虛引用也沒法拿到一個對象的實例。虛引用唯一的作用就是被垃圾回收的時候會收到一個通知。

注意:軟引用SoftReference和弱引用WeakReference,可以用在內存資源緊張的情況下以及創建不是很重要的數據緩存。當系統內存不足的時候,緩存中的內容是可以被釋放的。另外日常編程中不要手動調用"System.gc();"。

(5)軟引用和弱引用的應用場景

場景一:二級緩存可以用弱引用WeakReference

場景二:構建圖片緩存可以用軟引用SoftReference

一個程序需要處理用戶提供的圖片。如果將所有圖片讀入內存,這樣雖然能很快打開圖片,但內存使用巨大。而且一些使用較少的圖片會浪費內存空間,需要手動從內存中移除。如果每次打開圖片都從磁盤文件中讀取到內存再顯示出來,雖然內存占用少,但一些經常使用的圖片每次打開都要訪問磁盤速度慢。這時就可以用軟引用構建緩存。

很多系統的緩存功能都符合這樣的場景:當內存空間還足夠時,能夠保留在內存中。如果內存空間在進行GC后仍然非常緊張,那就可以拋棄這些對象。

3.垃圾收集的算法

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

首先標記出所有要回收的對象,標記完成后統一回收所有被標記的對象。也可以先標記存活的對象,標記完成后統一回收未被標記的對象。

缺點一:執行效率不穩定

如果Java堆中包含大量對象,而且其中大部分是需要被回收的,這時就必須要進行大量的標記和清除動作。這會導致標記和清除兩個過程的執行效率都隨對象數量增長而降低。

缺點二:內存空間的碎片化問題

標記、清除之后會產生大量不連續的內存碎片。空間碎片太多可能會導致:在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發一次垃圾回收。

(2)標記-復制算法(Copying)

策略一:半區復制策略

將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。如果內存中多數對象都是存活的,該策略會產生大量的內存間復制開銷。但對于多數對象都是可回收的情況,需要復制的就是占少數的存活對象。

由于每次都是針對整個半區進行內存回收,所以內存分配時就不用考慮有內存碎片的復雜情況,只要移動堆頂指針,按順序分配即可。這樣實現簡單,運行高效。只是這種算法的代價是將內存縮小為原來的一半,浪費50%的內存空間。

策略二:更優化的半區復制策略

HotSpot虛擬機的Serial、ParNew等新生代收集器,就是采用這種策略的。具體就是把新生代分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次分配內存只使用Eden區和其中一塊Survivor區。

進行垃圾收集時,將Eden和Survivor中存活的對象一次性復制到另外一塊Survivor空間上,然后直接清理掉Eden區和已使用過的那塊Survivor區的空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8 : 1,也就是每次新生代中可用內存空間為整個新生代空間的90%,只有一個Survivor空間(即10%的新生代空間)是會被浪費掉的。此外,當Survivor空間不足以容納一次Young GC之后存活的對象,就需要依賴其他內存區域(大多數是老年代)進行分配擔保。

標記-復制算法總結:標記-復制算法在對象存活率較高時要進行較多的復制操作,效率會降低。更關鍵的是,如果不想浪費50%的空間,就要額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況。所以在老年代一般不推薦選用這種算法。

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

首先標記出所有需要回收的對象。在標記完成后,后續步驟不是直接對可回收對象進行清理。而是讓所有存活的對象向一端移動,然后直接清理掉端邊界外的內存。

標記-清除算法和標記-整理算法的本質差異在于:前者是一種非移動式的回收算法,而后者是移動式的回收算法。

(4)是否移動回收后的存活對象分析

情況一:如果移動存活對象

尤其是在老年代這種每次回收都有大量對象存活區域,移動存活對象并更新所有引用這些對象的地方將會負擔極大,而且這種對象移動操作必須全程暫停用戶應用程序才能進行(STW)。

情況二:如果不移動存活對象

比如像標記-清除算法那樣不考慮移動和整理存活對象的話,則空間碎片化問題只能依賴更為復雜的內存分配器和內存訪問器來解決。內存的訪問是用戶程序最頻繁的操作,這個環節增加額外負擔將影響應用程序的吞吐量。

可見是否移動回收后的存活對象都存在弊端:移動則內存回收時更復雜,不移動則內存分配時更復雜。從垃圾收集的停頓時間來看,不移動對象停頓時間更短甚至不需要停頓。但從整個應用程序的吞吐量來看,移動對象會更劃算。因為不移動對象雖然會使得收集器的效率提升了,但因為內存分配和訪問相比垃圾收集頻率高得多,這部分的耗時增加后,總吞吐量仍然是下降的。HotSpot虛擬機里關注吞吐量的Parallel Old收集器是基于標記-整理算法,而關注延遲的CMS收集器則是基于標記-清除算法。

(5)CMS收集器如何處理空間碎片過多

和稀泥式不在內存分配和訪問上增加太大額外負擔,具體做法是:讓虛擬機平時采用標記-清除算法,暫時容忍內存碎片的存在。直到內存空間的碎片化程度已經大到影響對象分配時,再采用標記-整理算法收集一次,以獲得規整的內存空間。

4.垃圾收集器的設計

(1)分代收集理論的3個假說

弱分代假說:絕大多數對象都朝生夕滅

強分代假說:熬過多次GC過程的對象越難消亡

跨代引用假說:跨代引用相對于同代引用來說僅占極少數

(2)垃圾收集器的設計原則

假說一和二奠定了垃圾收集器的設計原則。收集器首先應該將Java堆劃分出不同的區域,然后將回收對象依據其年齡分配到不同的區域之中存儲。其中對象的年齡就是對象熬過垃圾收集過程的次數。

如果一個區域中大都是朝生夕滅的對象,則應以最低代價回收大量空間,即每次回收只須關注如何保留少量存活對象而非標記大量被回收的對象。如果一個區域中大都是難以消亡的對象,則應該以較低的頻率回收這個區域,同時兼顧垃圾收集的時間開銷和內存利用的效率。

現在商用Java虛擬機,一般把Java堆劃分為新生代和老年代兩個區域。新生代中,每次垃圾收集時都發現有大批對象死去,而新生代每次回收后存活的少量對象將會逐步晉升到老年代中存放。

(3)新生代和老年代出現跨代引用的處理

分代收集并非簡單劃分一下內存區域這么容易,至少存在一個明顯的問題:對象不是孤立的,對象之間會存在跨代引用。

假如要進行一次只局限于新生代區域內的收集(Young GC),但新生代中的對象是完全有可能被老年代所引用的,為了找出該區域中的存活對象,不得不在固定的GC Roots之外,再額外遍歷整個老年代中所有的對象來確保可達性分析結果的正確性。遍歷整個老年代所有對象的方案雖然理論可行,但無疑會為內存回收帶來了很大的性能負擔。

根據假說三,不應為少量的跨代引用去掃描整個老年代,也不必浪費空間專門記錄每一個對象是否存在哪些跨代引用。只需在新生代上建立一個全局的數據結構(該結構被稱為"記憶集"),記憶集會把老年代劃分成若干小塊,標識出那塊內存會存在跨代引用。此后當發生Young GC時,包含了跨代引用的小塊內存里的對象會被加入到GC Roots進行掃描。

(4)分代收集算法

當前商業虛擬機的垃圾收集都采用分代收集算法,這種算法就是根據對象存活周期的不同將內存劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。

專門研究表明,新生代中的對象98%是朝生夕死的。所以并不需要按照1 : 1的比例來劃分內存空間,而是將內存分為一塊較大的Eden空間和兩塊較小的Survivor空間。每次使用Eden和其中一塊Survivor空間,回收時將Eden和Survivor中存活的對象一次性復制到另一塊Survivor,最后清理掉Eden和剛才用過的Survivor空間。

HotSpot虛擬機默認Eden和Survivor的大小比例是8 : 1,也就是每次新生代中可用內存空間為整個新生代容量的90%,只有10%的內存會被浪費。當然,98%的對象可回收只是一般場景下的數據,我們沒有辦法保證每次回收都只有不多于10%的對象存活。當Survivor空間不夠用時,需要依賴其他內存(老年代)進行分配擔保。

在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活。那就選標記-復制算法,只需付出少量存活對象的復制成本就能完成收集。而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記-清除算法或者標記-整理算法來進行回收。

新生代的回收:Young GC(只發生在新生代的垃圾收集)。老年代的回收:Full GC(發生在整個Java堆和方法區的垃圾收集)。新生代不斷會有少量對象進入老年代,當老年代快填滿時會發生Full GC。

垃圾回收器新生代列表:
在這里插入圖片描述

垃圾回收器老年代列表:

在這里插入圖片描述
5.Serial和Serial Old收集器

單線程,適合單CPU或CPU核心數少的服務器。Serial收集器依然是HotSpot運行在客戶端模式下的默認新生代收集器。它有優于其他收集器的地方,即簡單而高效(與其他收集器的單線程相比)。對于內存資源受限的環境,Serial是所有收集器里內存消耗最小的。對于單核處理器或處理器核心數較少的環境來說,Serial由于沒有線程交互開銷,專心做GC而獲得最高的單線程收集效率。Serial收集器對于運行在客戶端模式下的虛擬機來說是一個很好的選擇。

6.ParNew收集器

ParNew收集器是Serial收集器的多線程并行版本。和Serial收集器相比,基本沒區別(回收策略、算法),唯一的區別就是:多線程,適合于多CPU的,停頓時間比Serial少。

和Parallel Scavenge收集器相比,它關注的是盡可能縮短垃圾收集時用戶線程的停頓時間,也就是關注停頓時間。停頓時間短的收集器適合用戶交互的程序,以便于提高用戶體驗。

除了Serial收集器外,目前只有ParNew能與CMS收集器配合工作。自JDK9開始,ParNew+CMS就不再是官方推薦的Server下的解決方案。官方希望被G1取代,甚至取消ParNew+Serial Old和Serial+CMS的支持。

7.Parallel Scavenge/Parallel Old收集器

Parallel Scavenge收集器的特點是它的關注點與其他收集器不同。CMS等收集器的關注點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標是盡可能地達到一個可控制的吞吐量。

所謂吞吐量就是CPU用于運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)。虛擬機總共運行100分鐘,其中垃圾收集花了1分鐘,那吞吐量就是99%。

停頓時間越短越適合需要與用戶交互或需要保證服務響應質量的程序,高吞吐量則可以高效率地利用CPU時間,盡快完成程序的運算任務,關注高吞吐量主要適合在后臺運算而不需要太多交互的任務。

8.Concurrent Mark Sweep(CMS)收集器

(1)CMS的階段(初始標記 + 并發標記 + 重新標記 + 并發清除)

CMS收集器是一種以獲取最短回收停頓時間為目標的收集器。CMS收集器可以讓系統停頓時間最短,給用戶帶來較好的體驗。從名字Mark Sweep可看出,CMS收集器是基于標記-清除算法實現的。它的運作過程相對于前面幾種收集器來說更復雜,整個過程分4個階段。

階段一:初始標記

用戶程序短暫暫停,僅標記GC Roots能直接關聯到的對象,速度很快。

階段二:并發標記

和用戶程序同時進行,進行GC RootsTracing。

階段三:重新標記

用戶程序短暫暫停,為修正并發標記期間,因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般比初始標記長,但遠比并發標記的時間短。

階段四:并發清除

在耗時最長的并發標記和并發清除階段,GC線程都與用戶線程一起工作,所以從總體上看,CMS收集器的內存回收過程與用戶線程一起并發執行。

CMS收集器的主要優點:并發收集、低停頓。CMS收集器有三個明顯缺點:資源敏感、浮動垃圾、內存碎片。

(2)CMS的缺點(資源敏感 + 浮動垃圾 + 內存碎片)

缺點1:資源敏感

在并發階段,它雖然不會導致用戶線程停頓,但卻因為占用一部分線程,也就是處理器的計算能力,而導致應用程序變慢,降低總吞吐。CMS默認啟動的回收線程數:(CPU核數 + 3) / 4。如果CPU核心數在4個以上,那么并發回收時,GC線程數只占不超過25%的CPU運算資源,并且會隨著CPU核心數量的增加而下降。

如果CPU核心數不足4個,CMS對用戶程序的影響就可能就變得很大。比如應用原來的CPU負載很高,還要分一半的運算能力去處理GC線程,那么就可能導致用戶程序的執行速度忽然大幅降低。

缺點2:浮動垃圾

由于并發清理時用戶線程還在運行,所以還會有新的垃圾不斷產生。這一部分垃圾出現在標記過程后,這些垃圾就稱為浮動垃圾。CMS無法在當次收集中處理掉它們,只好等下次GC時再清理掉。同時用戶的線程還在運行,要給用戶線程留下運行的內存空間。

參數-XX:CMSInitiatingOccupancyFraction。由于浮動垃圾和需要留內存給運行著的用戶線程,因此CMS不能像其他收集器那樣等老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供并發收集時的程序運作使用。

早期JDK默認下,CMS收集器當老年代使用了68%的空間后就會被激活。如果在應用中老年代增長不是太快,可以適當調高該CMS的激活閾值,以便降低內存回收次數從而獲取更好的性能。

在JDK1.6中,CMS收集器的啟動閾值已經提升至92%。要是CMS運行期間預留的內存無法滿足程序需要,就會出現一次Concurrent Mode Failure,這時虛擬機將啟動后備預案,臨時啟用Serial Old收集器進行老年代的GC,這樣停頓時間就很長了。

如果-XX:CMSInitiatingOccupancyFraction設置得太高,就會很容易導致出現大量Concurrent Mode Failure,性能反而降低。

缺點3:內存碎片

由于CMS使用標記-清除算法,因此會產生大量空間碎片。空間碎片過多時,分配大對象就會出現問題而提前觸發Full GC。

參數-XX:+UseCMSCompactAtFullCollection。CMS提供這個開關參數(默認開啟)便是為了解決內存碎片問題,用于在CMS頂不住要進行Full GC時開啟內存碎片的合并整理過程。內存整理的過程是無法并發的,空間碎片問題沒有了,但停頓時間更長。

這個參數用于設置執行多少次不壓縮Full GC后,跟著來一次帶壓縮的。默認值0,表示每次進入Full GC時都進行碎片整理。

三.類和類加載相關

1.類的生命周期

2.類加載的全過程

3.類加載器

4.雙親委派模型

5.類加載器和雙親委派機制總結

1.類的生命周期

類從被加載到虛擬機內存中開始,到卸載出內存為止。類的整個生命周期包括:加載、驗證、準備、解析、初始化、使用和卸載7個階段。其中驗證、準備、解析3個部分統稱為連接。

2.類加載的全過程

Java虛擬機中類加載的全過程,即加載、驗證、準備、解析和初始化這5個階段所執行的具體動作:

(1)加載階段

首先代碼中包含main()方法的主類會在JVM進程啟動后被加載到內存,然后JVM進程會開始執行main()方法中的代碼,接著遇到使用別的類就會從對應的".class"字節碼文件加載該類到內存。

(2)驗證階段

這一步會根據虛擬機規范,校驗加載的".class"文件內容,是否符合規范。假如".class"文件被篡改,里面的字節碼不符合規范,JVM是沒法執行的。

(3)準備階段

這是正式為類變量分配內存并設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這個階段中有兩個容易產生混淆的概念需要強調一下。

首先,此時進行內存分配的只是類變量(被static修飾),不包括實例變量。實例變量將會在對象實例化時隨著對象一起分配在Java堆中。其次,這里所說的初始值"通常情況"下是數據類型的零值。假設一個類變量的定義為:public static int value=123,那變量value在準備階段過后的初始值為0而不是123,因為這時候尚未開始執行任何Java方法。而把value賦值為123的putstatic指令是程序被編譯后,存放于類構造器<clinit>()方法之中,所以把value賦值為123的動作將在初始化階段才會執行。

假設上面類變量value的定義變為:public static final int value=123。編譯時Javac將會為value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value賦值為123。

(4)解析階段

這是虛擬機將常量池內的符號引用替換為直接引用的過程,比如有:類或接口的解析、字段解析、方法解析、接口方法解析等。驗證、準備、解析這三個階段里,最關鍵的其實就是準備階段。準備階段會給加載進來的類分配好內存空間,以及也會對類變量分配好內存空間,并設置默認初始值。

(5)類初始化階段

這是類加載過程的最后一步。在前面的類加載過程中:除了在加載階段用戶應用程序可以通過自定義類加載器參與外,其余動作完全由虛擬機主導和控制。

到了初始化階段,才真正開始執行類中定義的Java程序代碼。在準備階段,變量已經賦過一次系統要求的初始值。而在初始化階段,則根據初始化方法去初始化類變量和其他資源,或者說初始化階段是執行類構造器<clinit>()方法的過程。

<clinit>()方法是由編譯器自動收集類中所有類變量的賦值動作,以及靜態語句塊(static{}塊)中的語句合并產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的。

<clinit>()方法對于類或接口來說并不是必需的。如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那么編譯器可不為這個類生成<clinit>()方法。

JVM會保證一個類的<clinit>()方法在多線程環境中被正確加鎖、同步。如果多個線程同時去初始化一個類,那么只會有一個線程去執行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。所以如果在一個類的<clinit>()方法中有耗時很長的操作,那么就可能造成多個線程阻塞。

什么時候會初始化一個類,一般來說有以下時機:

時機一.使用new關鍵字實例化類的對象

此時會觸發類的加載到初始化的全過程,把類準備好,然后實例化對象。

時機二.包含main()方法的主類必須馬上初始化

時機三.如果初始化一個類時發現父類還沒初始化,則必須先初始化其父類

3.類加載器

(1)類加載器的定義及其用途

類加載的5個階段中:除了在加載階段用戶應用程序可以通過自定義類加載器參與之外,其余動作完全由虛擬機主導和控制。

一.類加載器的定義

類加載的階段通過一個類的全限定名來獲取描述該類的二進制字節流,這個動作放到Java虛擬機外部去實現,以便讓應用程序自己決定如何去獲取所需的類,實現這個動作的代碼被稱為類加載器(Class Loader)。

二.類加載器的用途

類加載器的用途有:熱加載、代碼保護和加解密、類層次劃分、OSGi等。

(2)類加載器與類是否相等

對于任意一個類,都必須由加載它的類加載器和這個類本身,一同確立其在Java虛擬機中的唯一性。每一個類加載器,都擁有一個獨立的類名稱空間。這句話可以表達得更通俗一些:比較兩個類是否"相等",只有在這兩個類是由同一個類加載器加載的前提下才有意義。否則即使這兩個類來源于同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。

這里所指的"相等",包括代表類的Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果,也包括使用instanceof關鍵字做對象所屬關系判定等情況。

(3)如何重寫類的加載方法

在自定義ClassLoader的子類時,常見的會有兩種做法。一種是重寫loadClass()方法,另一種是重寫findClass()方法。

其實這兩種方法本質上差不多,畢竟loadClass()也會調用findClass(),但是從邏輯上講最好不要直接修改loadClass()的內部邏輯,建議只在findClass()里重寫自定義類的加載方法。

loadClass()這個方法是實現雙親委托模型邏輯的地方,擅自修改這個方法會導致模型被破壞,容易造成問題。因此最好在雙親委托模型框架內進行小范圍改動,不破壞原有的結構,同時也避免了重寫loadClass()方法的過程中必須寫雙親委托的重復代碼。從代碼的復用性來看,不直接修改loadClass()方法始終是比較好的選擇。

4.雙親委派模型

(1)JVM角度的兩種類加載器

從Java虛擬機的角度來講,只存在兩種不同的類加載器。第一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現,是虛擬機自身的一部分。第二種是所有其他的類加載器,這些類加載器都由Java語言實現,獨立于虛擬機外部,并且全都繼承自抽象類java.lang.ClassLoader。

(2)開發者角度的三層類加載器

站在Java開發人員的角度來看,類加載器應該劃分得更細致些。Java一直保持著三層類加載器、雙親委派的類加載架構。三層類加載器如下所示:
在這里插入圖片描述

(3)雙親委派模型各個類加載器之間的關系
在這里插入圖片描述

雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應當有自己的父類加載器。這里類加載器之間的父子關系一般不會以繼承的關系來實現,而是使用組合關系(委托)來復用父加載器的代碼。

(4)雙親委派模型的工作過程

如果一個類加載器收到了類加載請求,它首先不會自己嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每個層次的類加載器都如此。

因此所有的加載請求最終都應該傳送到最頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求時(即搜索不到所需的類),才會讓子加載器嘗試自己去完成加載。

(5)雙親委派模型的好處

使用雙親委派模型來組織類加載器之間的關系,有個顯而易見的好處是:Java類隨著它的類加載器一起具備了一種帶有優先級的層次關系。

例如類java.lang.Object,它存放在rt.jar之中。無論哪個類加載器要加載這個類,最終都委派給啟動類加載器進行加載。因此Object類在程序的各種類加載器環境中都是同一個類。

相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話。如果用戶自己編寫了一個java.lang.Object類并放在程序的ClassPath中,那系統中將會出現多個不同的Object類,應用程序將會變得一片混亂。

(6)雙親委派模型的實現

雙親委派模型的實現就放在loadClass()方法里,所以自定義的類加載器最好不要覆蓋loadClass(),而是覆蓋findClass()。

//代碼邏輯為:
//先檢查請求加載的類是否已經被加載過, 若沒有則調用父加載器的loadClass()方法;
//若父加載器為空則默認使用啟動類加載器作為父加載器;
//假如父加載器加載失敗, 拋出ClassNotFoundException異常, 才調用自己的findClass()方法嘗試進行加載
protected Class<?> loadClass(String name, boolean resolve) throwsClassNotFoundException {synchronized (getClassLoadingLock(name)) {
//首先檢查請求的類是否已經被加載過
Class<?> c = findLoadedClass(name);
if (c == null) {try {
if (parent != null) {
c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {//如果父類加載器拋出ClassNotFoundException異常, 說明父類加載器無法完成加載請求}
if (c == null) {//在父類加載器無法加載時, 再調用本身的findClass方法來進行類加載long t1 = System.nanoTime();
c = findClass(name);}}
if (resolve) {      resolveClass(c);}
returnc;}
}

(7)雙親委派模型被破壞的情形

雙親委派很好解決了各個類加載器協作時基礎類的一致性問題,越基礎的類由越上層的加載器進行加載。

基礎類型之所以被稱為基礎,是因為它們總是作為被用戶代碼繼承、調用的API,但如果有基礎類又要調用回用戶的代碼,該如何處理。

為了解決該困境,Java引入了線程上下文類加載器。線程上下文類加載器可通過Thread.setContextClassLoader()方法設置。如果創建線程時還未設置,它將會從父線程中繼承一個。如果在應用程序的全局范圍內都沒有設置過的話,那這個線程上下文類加載器默認是應用程序類加載器。

有了線程上下文加載器,JNDI服務就可使用它去加載所需的SPI服務代碼。這是一種父類加載器去請求子類加載器完成類加載的行為,這種行為實際上是打通了雙親委派模型的層次結構來逆向使用類加載器,這種行為已經違背了雙親委派模型的一般性原則。

Java中涉及SPI的加載基本上都是采用線程上下文加載器來完成,例如:JNDI、JDBC、JCE、JAXB和JBI等。

5.類加載器和雙親委派機制總結

Java里有如下這些幾種類加載器:

(1)啟動類加載器

這個類加載器主要負責加載Java安裝目錄("lib"目錄)下的類。如果要在一個機器上運行寫好的Java系統,都得先裝一下JDK。那么在Java安裝目錄下,就有一個"lib"目錄。"lib"目錄里就有Java最核心的一些類庫,支撐寫好的Java系統的運行。JVM一旦啟動,就會使用啟動類加載器去加載"lib"目錄中的核心類庫。

(2)擴展類加載器

這個類加載器主要負責加載Java安裝目錄("lib\ext"目錄)下的類。這個類加載器其實也是類似的,就是在Java安裝目錄下,有一個"lib\ext"目錄。該目錄有一些類需要使用這個類加載器來加載,以支撐Java系統的運行。那么JVM一旦啟動,也得在Java安裝目錄下加載"lib\ext"目錄中的類。

(3)應用程序類加載器

這個類加載器就是負責加載"ClassPath"環境變量所指定的路徑中的類。其實可以理解為去加載我們寫好的Java代碼,這個類加載器就負責加載我們寫好的那些類到內存里。

(4)自定義類加載器

除了上面幾種外,還可以自定義類加載器,根據需求加載我們的類。

(5)雙親委派機制

JVM的類加載器是有親子層級結構的:啟動類加載器在最上層,擴展類加載器在第二層,第三層是應用程序類加載器,最后一層是自定義類加載器。
在這里插入圖片描述

然后基于這個親子層級結構,就有一個雙親委派的機制。當我們的應用程序類加載器需要加載一個類的時候,它會委派給自己的父類加載器去加載,最終傳導到啟動類加載器去加載。但是如果父類加載器在自己負責加載的范圍內,沒找到這個類,那么父類加載器就會下推加載權利給自己的子類加載器去進行加載。

比如JVM現在需要加載ReplicaManager類,此時應用程序類加載器會找其父類加載器(擴展類加載器)幫忙加載該類。然后擴展類加載器也會找其父類加載器(啟動類加載器)幫忙加載該類。啟動類加載器在"lib"下沒找到該類,就會下推加載權利給擴展類加載器。擴展類加載器在"lib/ext"下也沒找到該類,則讓應用程序類加載器加載。然后應用程序類加載器在其負責范圍內,比如由系統打包成的jar包中,發現了ReplicaManager類,于是它就會把這個類加載到內存里去。

這就是所謂的雙親委派模型:先找父親去加載,加載不了再由兒子加載,這樣就可以避免多層級的類加載器重復加載某些類。否則不同層級的類加載器加載高層級類加載器的類時就會出現重復加載。

(6)問題

為什么要一級一級的往上找,能否直接從頂層類加載器開始往下找?

答:雙親委派模型的工作過程是:如果一個類加載器收到了類加載請求,它首先不會自己嘗試加載這個類。而是把該請求委派給父類加載器去完成,每一個層次的類加載器都如此。因此所有的加載請求最終都應該傳送到最頂層的啟動類加載器中。只有當父加載器己無法完成加載請求時,子加載器才會嘗試去完成加載。

這樣做的一個顯而易見的好處就是:Java中的類隨著它的類加載器一起具備了一種帶有優先級的層次關系,可保證例如Object類在程序的各種類加載器環境中都是同一個類。否則用戶編寫一個名為java.lang.Object類,并放在程序的ClassPath中,那么系統就會出現多個不同的Object類,當然還雙親委派機制可以避免重復加載同一個類。

從java.lang.ClassLoader的loadClass()方法就可以知道:如果從頂層開始找,就要將parent換成child + 對child硬編碼,才能找到。也就是說需要改動loadClass()方法,需要把parent換成child + 硬編碼,硬編碼也要先從下往上逐層獲得整個父子路徑才能直接從頂層往下找到。

loadClass()方法的邏輯如下:先檢查請求加載的類是否已被加載過。若沒有則調用父加載器的loadClass(),若父加載器為空則默認使用啟動類加載器作為父加載器。如果父類加載器加載失敗則拋異常,才調用自己的findClass()嘗試進行加載。

四.JVM實戰總結

1.每日百萬交易的支付系統的壓力

2.JVM內存中的對象何時會被垃圾回收

3.JVM垃圾回收的原理核心流程

4.CMS是如何工作的

5.CMS會產生什么問題

6.哪些情況下對象會進入老年代

7.G1垃圾回收器的工作原理

8.G1分代回收原理—性能為何比傳統GC好

9.使用G1垃圾回收器時應如何設置參數

10.如何基于G1垃圾回收器優化性能

11.使用jstat了解線上系統的JVM運行狀況

12.線上FGC的幾種案例

13.CPU負載過高的原因

14.JVM運行原理和GC原理總結

15.JVM性能優化的思路和步驟

1.每日百萬交易的支付系統的壓力

(1)支付系統每秒要處理多少筆支付訂單

假設每天100萬個支付訂單。那么一般用戶交易行為都會發生在每天的高峰期,比如中午或者晚上。假設每天高峰期大概是3個小時,將100萬平均分配到3個小時里。那么大概每秒100筆訂單,所以就以每秒100筆訂單來進行計算。假設支付系統部署3臺機器,則每臺機器實際上每秒大概處理30筆訂單。

(2)每個支付訂單處理要耗時多久

如果用戶發起一次支付請求:那么支付需要在JVM中創建一個支付訂單對象,填充進數據。然后把這個支付訂單寫入數據庫,以及可能處理一些其他事情。

假設一次支付請求的處理包含一個支付訂單的創建,大概需要1秒時間,那么每臺機器一秒鐘會接收到30筆支付訂單的請求。然后會在JVM新生代里創建30個支付訂單的對象,進行寫庫等處理。接著1秒后這30個支付訂單就處理完畢,此時棧幀中對這些支付訂單對象的引用就被回收了。然后這些訂單對象在JVM的新生代里就是沒被引用的垃圾對象了。接著下一秒會繼續來處理30個支付訂單,重復這個步驟。

(3)每個支付訂單大概需要多大的內存空間

可以直接根據支付訂單類中的實例變量的類型來計算。比如在支付訂單類中:一個Integer類型的變量數據4字節,一個Long類型的變量數據是8字節,還有別的類型的變量數據占據多少字節等,這樣就可以計算出每個支付訂單對象大致占多少字節了。

一般像支付訂單這種核心類,可以按20個實例變量來計算。然后大概一個訂單對象也就一兩百字節,可以算它大一點。比如一個支付訂單對象占據500字節的內存空間,也不到1K。

(4)每秒發起的支付請求對內存的占用

假設有3臺機器,每秒鐘處理30筆支付訂單的請求。那么在這1秒內,肯定有方法棧幀里的局部變量在引用這些支付訂單對象。那么30個支付訂單,大概占據的內存空間是30 * 500字節 = 15000字節。大概15K左右,其實是非常小的。

(5)讓支付系統運行起來進行分析

現在已經把整個系統運行的關鍵環節的數據都分析清楚了:每秒30個支付請求,每秒創建30個支付對象,每秒占15K的內存空間。接著1秒過后,這30個對象就沒有被引用了,成為新生代里的垃圾。下一秒請求過來,系統繼續創建30個支付對象放入新生代里,然后新生代里的對象就會持續累積增加。

直到有一刻,發現可能新生代里都有幾十萬個對象。此時占據了幾百M的內存空間,可能新生代空間就快滿了。然后就會觸發Young GC,把新生代里的垃圾對象都給回收掉。從而騰出內存空間,可以繼續在內存里分配新的對象。

這就是該支付系統在創建訂單環節的JVM運行模型。

(6)對完整的支付系統內存占用需要進行預估

前面的分析都是基于一個核心業務流程中的一個支付訂單對象來分析的,但那其實那只是整個支付系統的一個小部分而已。真實的支付系統在線上運行時,肯定會每秒創建大量其他對象。所以可以結合這個訪問壓力以及核心對象的內存占據,大致估算一下整個支付系統每秒鐘大致會占據多少內存空間。

如果要估算的話,其實可以把上述的計算結果擴大10到20倍。即每秒除了在內存里創建支付訂單對象,還會創建其他數十種對象。假設一臺機器每秒創建100個500字節的支付訂單對象,擴大20倍后,那么每秒創建出的被棧內存的局部變量引用的對象,大概占1M內存空間。然后下一秒對新請求,繼續創建1M對象放入新生代,一秒后又變成垃圾。循環多次后,新生代里垃圾太多,就會觸發Young GC回收掉這些垃圾。這就是一個支付系統在JVM層面的內存使用模型。

(7)可以考慮采用4核8G的機器來部署支付系統

此時JVM進程至少可以給4G以上內存,新生代至少可分配2G內存空間。這樣就可以做到即便新生代每秒消耗1M左右的內存,也要將近半小時到1小時才會讓新生代觸發YGC,大大降低了GC頻率。

舉個例子:機器采用4核8G,-Xms和-Xmx設置為3G,給整個堆內存3G內存空間。-Xmn設置為2G,給新生代2G內存空間。而且假設業務量如果更大,則可以考慮不只部署3臺機器,可以考慮橫向擴展部署5臺機器或者10臺機器,這樣每臺機器處理的請求更少對JVM的壓力更小。

(8)總結

從一個日百萬交易的支付系統出發,部署3臺機器的場景下。每秒鐘每臺機器需要處理多少筆訂單,每筆訂單要耗時多久處理。每筆訂單的核心對象每秒鐘會對JVM占據多大內存空間,根據單個核心對象橫向擴展預估整個系統每秒需要占據多大內存空間。接著根據上述數據模型推算出:在不同的機器配置之下,新生代大致會有多大的內存空間。然后在不同的新生代大小下,多久會觸發一次Young GC。

為了避免頻繁的GC,那么應該選用什么樣的機器配置。部署多少臺機器,設置JVM堆內存、新生代分別多大的內存空間。根據這套配置,就可以推算出來整個系統的運行模型了。每秒鐘創建多少對象在新生代,然后1秒之后成為垃圾。大概系統運行多久,新生代會觸發一次GC,頻率有多高。

2.JVM內存中的對象何時會被垃圾回收

在JVM規范中,局部變量就是可以作為GC Roots的。一個對象只要被局部變量引用,就說明它有一個GC Roots,不能被回收。靜態變量也可以看做是一種GC Roots。只要一個對象被GC Roots引用了,就不會去回收它。因此一句話總結就是:只要對象被方法的局部變量、類的靜態變量給引用了,就不會回收它們。

強引用就是最普通的代碼,一個變量引用一個對象。只要是強引用的類型,那么垃圾回收的時候絕對不會去回收這個對象。正常情況下垃圾回收是不會回收軟引用對象的。但如果垃圾回收后,發現內存空間不夠存放新對象,內存都快溢出了,就會把這些軟引用對象給回收掉,哪怕它被變量引用著。但是因為它是軟引用,所以還是要回收。弱引用就與沒有引用類似,如果發生垃圾回收,就會回收這個對象。

3.JVM垃圾回收的原理核心流程

問題一:什么時候會嘗試觸發YGC

當新生代的Eden區和其中一個Survivor區空間不足時,就會觸發YGC。

問題二:YGC前如何檢查老年代大小(涉及步驟)

步驟1:先判斷新生代中所有對象的大小是否小于老年代的可用區域。如果是則觸發YGC,如果否則繼續進行下面2中的判斷。

步驟2:如果設置了-XX:HandlePromotionFailure參數,那么進入步驟3。如果沒有設置-XX:HandlePromotionFailure參數,那么就觸發FGC。

步驟3:判斷YGC歷次進入老年代的平均大小是否小于老年代可用區域。如果是則觸發YGC,如果否則觸發FGC。

問題三:什么情況下YGC前會提前觸發FGC

(新生代現有存活對象 > 老年代剩余內存情況) + 未設置空間擔保

(新生代現有存活對象 > 老年代剩余內存情況) + (設置了空間擔保 + 但擔保失敗)

問題四:FGC的算法是什么

標記整理算法(但是CMS是標記清理再整理,FGC包含CMS)。老年代對象存活時間較長,復制算法不太適合且老年代區域不再細分。標記清除算法會產生內存碎片,標記整理算法則可以規避碎片。

問題五:YGC過后可能對應哪幾種情況

情況1:存活對象所占空間 < S區域內存大小,那么存活的對象進入Survivor區。

情況2:S區域內存大小 < 存活對象所占空間 < 老年代可用大小,那么存活的對象直接進入老年代。

情況3:(存活對象大小 > S區大小) & (存活對象大小 > 老年代可用大小),那么會觸發FGC,老年代騰出空間后,再進行YGC。如果騰出空間后還不能存放存活對象,則會導致OOM。OOM也就是堆內存空間不足、堆內存溢出。

問題六:哪些情況下YGC后的對象會進入老年代

情況1:S區域內存大小 < 存活對象所占空間 < 老年代可用大小。

情況2:經過XX:MaxTenuringThreshold次YGC的,默認最大是15次。

情況3:對象動態年齡判斷機制。年齡1 + 年齡2 + 年齡n的對象,大小總和超過了Survivor區的50%,此時就會把年齡為n及以上的對象都放入老年代。

4.CMS是如何工作的

(1)新生代垃圾回收總結

新生代的垃圾回收是通過標記-復制算法來實現的,我們最希望的是:新對象都在新生代的Eden區分配內存。然后每次垃圾回收后,存活對象都進入Survivor區。然后下一次垃圾回收后的存活對象都進入另外一個Survivor區。這樣幾乎很少對象會進入老年代,幾乎不會觸發老年代的垃圾回收。

但是理想很豐滿,現實是在寫代碼時,很少會考慮垃圾回收。都是不停寫代碼然后上線部署,很少考慮所寫代碼對垃圾回收的影響。最多有經驗的工程師在系統上線前,通過前面案例介紹的方法:估算一下系統的內存壓力以及垃圾回收的運行模型,然后合理設置一下內存各個區域大小,盡量避免太多對象進入到老年代。

實際中,線上系統很可能因各種各樣原因導致很多對象進入老年代,然后頻繁觸發老年代的Full GC。之前介紹的案例就演示過這種情況,比如Survivor區太小,容納不了每次YGC后的存活對象,從而導致對象頻繁進入老年代,最后頻繁觸發老年代Full GC。類似的情況其實很多,所以不能過于理想化的期待永遠沒有老年代GC,還是要對老年代的垃圾回收器如何進行回收有一個充分的了解和認識。

(2)CMS垃圾回收的基本原理

一般老年代選擇的垃圾回收器是CMS,它采用的是標記-清理算法。就是先標記出哪些對象是垃圾對象,然后就把這些垃圾對象清理掉。

現假設因老年代可用內存小于歷次YGC后升入老年代對象的平均大小,判斷出YGC有風險,于是就提前觸發FGC回收老年代的垃圾對象。或者一次YGC后對象太多,都要升入老年代但空間不足,于是觸發FGC。總之就是要進行FGC,此時的標記-清理算法會如下處理:首先通過追蹤GC Roots,看看各個對象是否被GC Roots給引用了。如果是的話,那就是存活對象,否則就是垃圾對象。接著將垃圾對象都標記出來,然后再一次性把垃圾對象都回收掉。這種標記-清理算法最大的問題,其實就是會造成很多內存碎片。

(3)如果Stop the World然后垃圾回收會如何

假如要先STW,再采用標記-清理算法去回收垃圾,那會有什么問題?如果停止一切工作線程,然后慢慢去執行標記-清理算法,會導致系統卡死時間過長,很多響應無法處理。所以CMS垃圾回收器采取的是:垃圾回收線程和系統工作線程盡量同時執行的模式來處理的。

(4)如何實現JVM垃圾回收的同時讓應用也工作

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

階段一:初始標記

階段二:并發標記

階段三:重新標記

階段四:并發清理

CMS進行垃圾回收時會先進入初始標記階段:這個階段會讓系統的工作線程全部停止,進入Stop the World狀態。所謂初始標記,就是標記出所有GC Roots直接引用的對象。方法的局部變量和類的靜態變量是GC Roots,類的實例變量不是GC Roots。初始標記階段雖然會造成STW暫停一切工作線程,但其實影響不大。因為它的速度很快,僅僅標記GC Roots直接引用的那些對象而已。

接著是并發標記階段,該階段系統線程可繼續運行創建新對象:在并發標記運行期間,可能會創建新的存活對象,也可能會讓部分存活對象失去引用變成垃圾對象。在這個過程中,垃圾回收線程會盡可能對已有的對象進行GC Roots追蹤。在進行并發標記的這個過程中,系統程序會不停的工作。此時系統程序可能會創建出各種新的對象,部分對象可能成為垃圾。并發標記階段會對老年代所有對象進行GC Roots追蹤,其實是最耗時的,因為需要追蹤所有對象是否從根源上被GC Roots引用了。但是這個最耗時的階段,并發標記線程是和系統程序并發運行的,所以并發標記階段不會對系統運行造成太大影響。

接著會進入重新標記階段:由于在并發標記階段里,一邊是JVM在標記存活對象和垃圾對象,一邊是系統程序在不停運行創建新對象讓老對象變成垃圾。所以并發標記階段結束后,會有很多存活對象和垃圾對象沒被標記出來。于是在重新標記階段需要讓系統程序停下來,再次進入STW。重新標記在并發標記階段新創建的存活對象,以及失去引用的垃圾對象。重新標記階段的速度是很快的,因為只是對并發標記階段中系統程序運行變動過的少數對象進行標記。

接著恢復運行系統程序,進入并發清理階段:這個階段會讓系統程序并發運行,然后CMS垃圾回收器會清理掉之前標記為垃圾的對象。這個并發清理階段其實是很耗時的,因為需要進行對象的清理。但是它也會跟系統程序并發運行,所以其實也不影響系統程序的執行。

(5)對CMS的垃圾回收機制進行性能分析

從CMS的垃圾回收機制可以發現,它已經盡可能的進行了性能優化了。

因為最耗時的:一是并發標記階段對老年代全部對象追蹤GC Roots,標記可回收對象。二是并發清理階段對各種垃圾對象先清除后整理。但由于并發標記階段和并發清理清理,都是和系統程序并發執行的,所以基本上這兩個最耗時的階段對性能影響不大。

雖然初始標記階段和重新標記階段需要Stop the World,但是這兩個階段都是簡單的標記而已,所以速度非常快,所以基本上這兩個STW的階段對系統運行響應也不大。

(6)CMS的基本工作原理總結

為了避免長時間Stop the World,CMS采用了4個階段來垃圾回收。其中初始標記和重新標記耗時很短,雖然會導致STW,但是影響不大。然后并發標記和并發清理耗時最長,但可以和系統的工作線程并發運行。所以并發標記和并發清理兩個階段對系統也沒太大影響,這就是CMS的基本工作原理。

5.CMS會產生什么問題

(1)并發回收垃圾導致CPU資源緊張

CMS垃圾回收器有一個最大的問題:雖然能在垃圾回收的同時讓應用程序也同時運行,但是在并發標記和并發清理兩個最耗時的階段,垃圾回收線程和應用程序工作線程同時工作,會導致有限的CPU資源被垃圾回收線程占用了一部分。

并發標記時要對GC Roots進行深度追蹤,看所有對象里有多少是存活的。但因老年代里存活對象比較多,該過程又追蹤大量對象,所以耗時較高。并發清理時要把垃圾對象從各種隨機的內存位置清理掉,也是很耗時的。所以在并發標記和并發清理這兩階段,CMS的垃圾回收線程會特別耗費CPU。

CMS默認啟動的垃圾回收線程的數量是:(CPU核數 + 3) / 4,下面用最普通的2核4G機器來計算一下。假設是2核CPU,本來CPU資源就有限,結果CMS還需要"(2 + 3) / 4 = 1"個垃圾回收線程,占用寶貴的1個CPU。所以CMS這個并發垃圾回收的機制,最大的問題就是會消耗CPU資源。

(2)Concurrent Mode Failure問題

一.什么是浮動垃圾

在并發清理階段,CMS只不過是回收之前標記好的垃圾對象。但這個階段系統一直在運行,隨著系統運行可能有些對象進入老年代。同時這些對象很快又失去引用變成垃圾對象,這種對象就是浮動垃圾。

比如有一些垃圾對象(新的)就是在并發清理期間,先被系統分配在新生代,然后觸發一次YGC,一些對象進入了老年代,短時間內又沒被引用了。這種對象,就是老年代的浮動垃圾。浮動垃圾在本次的并發清理階段中,由于沒有被標記,所以不能被回收,需要等到下一次GC執行到并發清理階段時才能進行回收。

二.CMS垃圾回收的觸發時機與預留空間

為了保證在CMS垃圾回收期間,能讓一些對象可以進入老年代,JVM會給老年代預留一些空間。

CMS垃圾回收的一個觸發時機就是:當老年代內存占用達到一定比例,就自動執行FGC。這個比例是由-XX:CMSInitiatingOccupancyFaction參數控制的,這個參數可以用來設置老年代占用達到多少比例時就觸發CMS垃圾回收。

-XX:CMSInitiatingOccupancyFaction參數在JDK 1.6里默認的值是92%,也就是如果老年代占用了92%的空間,就會自動進行CMS垃圾回收。此時會預留8%的空間,這樣在CMS并發回收期間,可讓系統程序把一些新對象放入老年代中。

三.如果CMS垃圾回收期間,要放入老年代的對象已大于可用內存空間

這時就會發生Concurrent Mode Failure,即并發垃圾回收失敗了。CMS一邊回收,系統程序一邊把對象放入老年代,內存不夠了。此時就會自動用Serial Old替代CMS,直接強行對系統程序STW。重新進行長時間GC Roots追蹤,標記全部垃圾對象,不允許新對象產生。最后再一次性把垃圾對象都回收掉,完成后再恢復系統程序。

所以在實踐中:老年代占用多少比例時觸發CMS垃圾回收,要設置合理。讓CMS在并發清理期間,可以預留出足夠的老年代空間來存放新對象,從而避免Concurrent Mode Failure問題。

(3)內存碎片問題

老年代的CMS垃圾回收器會采用"標記-清理"算法:每次都是標記出垃圾對象,然后一次性回收,這樣會產生大量內存碎片。內存碎片太多會導致對象進入老年代時找不到連續內存空間,觸發FGC。所以CMS不能只用標記-清理算法,因太多內存碎片會導致頻繁FGC。

"-XX:+UseCMSCompactAtFullCollection"這個CMS的參數,默認是打開的。意思是在FGC后要再次進行STW,停止工作線程,然后進行碎片整理。碎片整理就是把存活對象移動到一起,空出大片連續內存空間,避免內存碎片。

"-XX:CMSFullGCsBeforeCompaction"這個CMS的參數,意思是執行多少次FGC后再執行一次內存碎片整理的工作。該參數值默認是0,意思是每次Full GC后都會進行一次內存整理。

(4)為什么老年代的FGC要比新生代的YGC慢

為什么老年代的FGC要比新生代的YGC慢很多倍,一般在10倍以上?其實原因很簡單,下面分析一下它們的執行過程。

一.新生代Young GC執行速度很快

Young GC時首先從GC Roots出發就可以追蹤哪些對象是存活的了。由于新生代存活對象很少,這個速度會很快,不需要追蹤多少對象。然后直接把存活對象放入Survivor中,接著再一次性回收Eden和之前使用的Survivor。

二.CMS的Full GC執行速度很慢

首先在并發標記階段,需要去追蹤所有存活對象。老年代存活對象很多,這個過程就會很慢。其次在并發清理階段,不是一次性回收一大片內存,而是要找到分散的垃圾對象,速度也很慢。最后在完成Full GC后,還得執行一次內存碎片整理,把大量的存活對象給移動到一起,空出連續內存空間,這個過程還得Stop the World,就更慢了。

此外萬一并發清理期間,剩余內存空間不足以存放要進入老年代的對象,還會引發Concurrent Mode Failure問題,還得用Serial Old垃圾回收器,先進行Stop the World,再重新來一遍標記清理的過程,這就更耗時了。所以,老年代的垃圾回收比新生代的垃圾回收慢。

(6)觸發老年代GC的時機總結

時機一:老年代可用內存 < 新生代全部對象大小 + 沒開啟空間擔保,觸發FGC。所以一般都會打開空間擔保參數-XX:-HandlePromotionFailure。

時機二:老年代可用內存 < 歷次YGC后進入老年代的對象平均大小,觸發FGC。

時機三:新生代YGC存活對象 > S區(需進入老年代) + 老年代內存不足,觸發FGC。

時機四:參數-XX:CMSInitiatingOccupancyFaction可以設置CMS垃圾回收時的預留空間比例。進行YGC前的檢查時,如果發現老年代可用內存大于歷次新生代GC后進入老年代的對象平均大小,但老年代已使用的內存超過了這個參數指定的比例,就會觸發FGC。

6.哪些情況下對象會進入老年代

情況一:大對象直接分配到老年代

情況二:YGC后對象的年齡到了15

情況三:YGC后存活對象大小大于Survivor區大小

情況四:動態年齡規則觸發

情況五:YGC前檢查發現沒有配置空間擔保參數

情況六:YGC前有配置空間擔保參數 + 老年代可用內存小于歷次晉升平均內存

情況七:老年代中已經被使用的內存空間達到了-XX:CMSInitiatingOccupancyFaction設置的比例

7.G1垃圾回收器的工作原理

(1)ParNew + CMS的組合有哪些痛點

Stop the World是ParNew + CMS組合最大的問題。無論是新生代GC還是老年代GC,都會或多或少產生STW現象,這對系統的運行是有一定影響的。

所以JVM對垃圾回收器的優化,都是朝減少STW的目標去做的。在這個基礎之上,就誕生了G1垃圾回收器。G1垃圾回收器可以提供比ParNew + CMS組合更好的垃圾回收性能。

(2)G1垃圾回收器介紹

G1垃圾回收器可以同時回收新生代和老年代的對象,不需要兩個垃圾回收器配合起來運作,它自己就能搞定所有的垃圾回收。G1的一大特點就是把Java堆內存拆分為多個大小相等的Region。然后G1也會有新生代和老年代,但是只是邏輯上的概念。也就是說,某些Region屬于新生代,某些Reigon屬于老年代。

G1的另一特點,就是可以設置每次垃圾回收時的最大停頓時間,以及指定在一個長度為M毫秒的時間片段內,垃圾回收時間不超N毫秒。比如可指定,希望G1在垃圾回收時保證:在1小時內由G1垃圾回收導致系統停頓時間,不超過1分鐘。

(3)G1如何實現垃圾回收的停頓時間是可控的

如果G1要做到這一點,就必須要追蹤每個Region里的回收價值。什么是回收價值?即G1必須搞清楚每個Region里有多少垃圾對象。如果對一個Region進行垃圾回收,會耗費多長時間,可回收多少垃圾?

G1的核心設計是:G1可以讓我們設定垃圾回收對系統的影響,G1會把內存拆分為大量的小Region,G1會追蹤每個Region中可以回收的對象大小和預估時間,G1在垃圾回收時會盡量把垃圾回收對系統影響控制在指定時間范圍內,同時在有限的時間內盡量回收盡可能多的垃圾對象。

(4)Region可能屬于新生代也可能屬于老年代

在G1中,每一個Region可能屬于新生代,也可能屬于老年代。剛開始一個Region可能誰都不屬于,然后接著就被分配給了新生代。然后這個Region會被放入很多屬于新生代的對象,接著觸發了垃圾回收,需要回收這個Region。然后下一次這個Region可能又被分配給了老年代,用來存放老年代需要長期存活的的對象。所以在G1的內存模型中,一個Region會屬于新生代也會屬于老年代。于是就沒有所謂新生代給多少內存,老年代給多少內存這一說法。新生代和老年代各自的內存區域是不停變動的,由G1自己去控制。

(5)總結

這里介紹了G1垃圾回收器的設計思想:包括Region劃分、Region動態變成新生代或老年代,Region的按需分配。當觸發G1垃圾回收時,可以根據設定的預期的系統停頓時間,來選擇最少回收時間和最多回收對象的Region進行垃圾回收。保證GC對系統停頓的影響在可控范圍內,同時盡可能回收最多對象。

8.G1分代回收原理—性能為何比傳統GC好

(1)G1垃圾回收器的設計思想

G1垃圾回收器設計的思想:把內存拆分為很多Region,然后新生代和老年代各自對應一些Region。回收時盡可能挑選停頓時間最短以及回收對象最多的Region,盡量保證達到指定的垃圾回收系統停頓時間。

(2)如何設定G1對應的內存大小

如果JVM啟動時發現了指定使用G1垃圾回收器,那么默認情況下G1會自動用堆大小除以2048得出每個Region的大小。每個Region的大小范圍是1M~32M,且必須是2的倍數。如果堆大小是4G = 4096M,除以2048,每個Region的大小就是2M。當然也可以通過-XX:G1HeapRegionSize參數來手動指定Region大小。

需要注意的是:按照默認值計算,G1可以管理的最大內存為2048 * 32M = 64G。假設設置xms=32G,xmx=128G。由于Region的大小最小是1M,最大是32M,而且要是2的倍數。那么初始化時按2048個Region計算,得出每個Region分區大小為32M。然后分區個數動態變化范圍從1024個到4096個。

系統剛開始運行時,默認新生代對堆內存的占比是5%。也就是占據200M左右的內存,對應大概是100個Region。這可以通過-XX:G1NewSizePercent來設置新生代初始占比,但通常維持默認值即可。

因為在系統運行中,JVM會不停地給新生代增加更多的Region。但新生代占比最多不超60%,可通過-XX:G1MaxNewSizePercent設置。而且一旦Region進行了垃圾回收,新生代的Region數量就會減少。

(3)新生代Region還會分Eden區和Survivor區

G1雖然把內存劃分為很多的Region,但還是有新生代、老年代的區分,而且新生代里同樣有Eden和Survivor的劃分。所以前面介紹的很多原理在G1中都還是適用的。比如參數-XX:SurvivorRatio=8,系統剛開始運行時有100個Region。此時新生代中有80個Region是Eden區,20個Region是兩個Survivor區。

所以在G1中還是有Eden和Survivor的,它們會占據不同數量的Region。然后隨著對象不停地在新生代分配,屬于新生代的Region會不斷增加,Eden和Survivor對應的Region也會不斷增加。

(4)G1的新生代垃圾回收

既然G1的新生代有Eden和Survivor之分,那么垃圾回收的機制也類似。當不停往新生代Eden的Region放對象,G1會不停給新生代加入Region。直到新生代占據堆大小的最大比例60%,一旦新生代大小達到了設定的占據堆內存大小的最大比例60%。比如2048個Region中有1200個Region都是屬于新生代的了,里面的Eden占了1000個Region,每個Survivor占了100個Region,而且Eden中的Region都占滿了對象,這時就會觸發新生代GC。G1就會使用復制算法來進行垃圾回收,進入Stop the World狀態。然后把Eden對應的Region中的存活對象放入S1對應的Region中,接著回收掉Eden對應的Region中的垃圾對象。

G1的新生代垃圾回收過程和ParNew是有區別的。因為G1可以設定GC停頓時間,執行GC時最多會讓系統停頓某個時間。可以通過-XX:MaxGCPauseMills參數來設定,默認值是200ms。G1會追蹤每個Region,然后GC時根據回收各Region需要多少時間、以及可回收多少對象,來選擇回收其中一部分Region。從而保證GC時的停頓時間控制在指定范圍內,并盡可能多地去回收對象。

(5)對象什么時候進入老年代

在G1的內存模型下,新生代和老年代各自都會占據一定的Region。如果按照默認新生代最多只能占據堆內存2048個Region的60%的Region來推算,老年代最多可以占據40%的Region,大概就是800個左右的Region。

那么對象何時候會從新生代進入老年代?和ParNew幾乎一樣,還是以下幾個條件:

條件一:對象在新生代躲過多次YGC,達到參數-XX:MaxTenuringThreshold設置的年齡

條件二:動態年齡判定規則,比如年齡為1歲、2歲、3歲、4歲的對象大小總和超過了Survivor的50%,此時Survivor區還有5歲+的對象,那么4歲及以上的對象就會全部進入老年代

條件三:新生代回收后存活的對象在Survivor區的Region都放不下了

(6)大對象Region

G1提供專門的Region存放大對象,不讓大對象進入老年代的Region。G1中大對象的判定規則就是一個大對象超過了一個Region大小的50%。比如按照上面算的,每個Region是2M。那么只要一個大對象超過了1M,就會被放入大對象專門的Region中,而且一個大對象如果太大,可能會橫跨多個Region來存放。

堆內存里哪些Region會用來存放大對象?60%的Region給新生代,40%的Region給老年代,那還有哪些Region給大對象?其實在G1里,新生代和老年代的Region是不停的動態變化的。比如新生代現占1200個Region,但一次GC后里面1000個Region空了。此時這1000個Region就可以不屬于新生代,可用部分Region放大對象,所以大對象既不屬于新生代也不屬于老年代。

既然大對象既不屬于新生代也不屬于老年代,那何時會觸發垃圾回收?其實在新生代、老年代回收時,會順帶著大對象Region一起回收,這其實就是在G1內存模型下對大對象的分配和回收策略。

(7)總結

這里介紹了G1的內存模型和分配規則,包括:

規則一:每個Region多大(1-32M)

規則二:新生代包含多少Region(60%)

規則三:新生代動態增加Region(初始5% -> 60%)

規則四:G1中仍然存在Eden和Survivor兩個區域

規則五:新生代占60%且滿了觸發新生代垃圾回收

規則六:G1新生代垃圾回收使用的復制算法

規則七:G1特有的預設GC停頓時間功能

規則八:對象進入老年代(15歲+動態年齡+S區不足)

規則九:大對象的獨立Region存放和回收

(8)問題

從新生代的垃圾回收來看,G1相比ParNew的優點:

優點一:停頓時間可以預設

優點二:大對象不再進入老年代

優點三:對象進入老年代的情況少很多

優點四:同樣內存大小,Eden和Survivor都大很多

優點五:ParNew的GC需要停止系統程序,但G1的新生代GC可以不用停止

9.使用G1垃圾回收器時應如何設置參數

(1)G1的動態內存管理策略總結

G1的動態內存管理策略:根據情況動態地把Region分配給新生代(Eden+S區)、老年代和大對象。但是新生代和老年代會有一個各自的最大占比,新生代占比最大60%,老年代占比最大40%。然后在新生代的Eden滿的時候,觸發新生代垃圾回收。

G1新生代的垃圾回收還是采用了復制算法。只是會考慮預設GC停頓時間,保證垃圾回收的停頓時間不超預設時間。因此會挑選一些回收價值比較高的Region來進行垃圾回收。

然后G1新生代垃圾回收和ParNew一樣:如果一些對象在新生代熬過一定次數GC,或觸發了動態年齡判定規則,或GC后的存活對象在Survivor放不下,都會讓對象進入老年代中。所以G1中的新生代對象還是會因為各種情況而慢慢地進入老年代的。

G1對大對象的處理則與ParNew不一樣:G1的大對象會進入單獨的大對象Region,不再進入老年代。

(2)何時觸發新生代 + 老年代的混合垃圾回收

-XX:InitiatingHeapOccupancyPercent是G1的參數,默認值是45%。意思是如果老年代占據了堆內存的45%的Region時,就會嘗試觸發新生代 + 老年代一起回收的混合回收。比如按照默認情況下的堆內存有2048個Region:如果老年代占據了其中45%的Region,就會開始觸發混合回收。

(3)G1混合垃圾回收的過程

G1:初始標記-并發標記-最終標記-混合回收

CMS:初始標記-并發標記-重新標記-并發清除

首先進入初始標記階段:這個階段需要STW,標記GC Roots直接引用的對象,這個過程是很快的。

然后會進入并發標記階段:這個階段會允許系統程序的運行,同時進行GC Roots追蹤,從GC Roots開始追蹤所有的存活對象。這個并發標記階段還是很耗時的,因為要追蹤全部的存活對象。但這個階段可以跟系統程序并發運行,所以對系統程序影響不太大。而且JVM也會記錄在并發標記階段對對象進行的修改,比如哪個對象被新建了,哪個對象失去了引用。

接著會進入最終標記階段:這個階段會STW禁止系統程序運行,但會根據并發標記時的記錄,最終標記出哪些對象存活、哪些對象回收。

最后進入混合回收階段:這個階段首先會進行如下計算:老年代中各Region的存活對象數量、存活對象占比,還有執行垃圾回收的預期性能和效率。接著會Stop The World停止系統程序,選擇部分Region進行回收,因為必須讓垃圾回收的停頓時間控制在指定的范圍內。比如老年代此時有1000個Region都滿了:但是根據預定目標,本次垃圾回收可能只能停頓200毫秒。那么通過之前計算得知,可能回收其中800個Region剛好需要200ms。于是就只回收那800個Region,把GC停頓時間控制在指定范圍內。

10.如何基于G1垃圾回收器優化性能

(1)G1的運行原理總結

G1會根據預設的GC停頓時間,給新生代分配一些Region。然后到一定程度才觸發GC,并且把GC停頓時間控制在預設范圍內,盡量避免一次性回收過多Region導致GC停頓時間超出預期。

(2)新生代GC如何優化

垃圾回收器是一代比一代先進的,雖然內部實現機制越來越復雜,但是優化卻越來越簡單。比如對于G1:

首先給整個JVM的堆區域足夠的內存,比如給JVM超過5G的內存,其中堆內存有4G的內存。

接著合理設置-XX:MaxGCPauseMills參數。如果這個參數設置太小了:那么說明每次GC停頓時間可能特別短。此時G1可能在發現幾十個Region占滿時,就要開始觸發新生代GC。從而導致新生代GC頻率特別頻繁。比如如果設置每次停頓30毫秒,那么可能會每30秒觸發一次新生代GC。如果這個參數設置過大了:那么G1會允許不停地在新生代分配新對象。然后積累很多對象,再一次性回收幾百個Region。此時可能一次GC停頓時間就會達到幾百毫秒,但是GC的頻率很低。比如30分鐘才觸發一次新生代GC,但每次停頓500毫秒。

所以預期的GC停頓時間到底如何設置,需要結合系統壓測工具、GC日志、內存分析工具來進行考慮,盡量別讓系統的GC頻率太高,同時每次GC停頓時間也別太長。

(3)Mixed GC如何優化

一.頻繁觸發Mixed GC的關鍵

新生代對象進入老年代的幾個條件是:YGC后存活對象太多沒法放入S區 + 對象年齡太大 + 動態年齡判定規則。

Mixed GC的觸發條件是:老年代在堆內存里占比超過45%。在新生代對象進入老年代的幾個條件其中比較關鍵的就是:新生代GC后存活對象太多無法放入Survivor區和動態年齡判定規則,因為這兩個條件可能讓很多對象快速進入老年代。一旦老年代達到占用堆內存45%的閾值,那么就會頻繁觸發Mixed GC。

所以Mixed GC本身很復雜,很多參數可以優化。但是優化Mixed GC的核心不是優化它的參數,而是和前面分析的一樣。盡量避免對象過快進入老年代,避免頻繁觸發Mixed GC,就能實現優化。

二.合理設置-XX:MaxGCPauseMills避免頻繁觸發Mixed GC

由于G1和ParNew + CMS的組合是不同的,那應該如何來優化參數呢?其實核心的還是-XX:MaxGCPauseMills這個參數。

如果-XX:MaxGCPauseMills參數設置的值很大,導致系統運行很久,新生代都占用堆內存的60%時才觸發新生代GC。那么存活下來的對象可能就會很多,導致Survivor區放不下那么多對象。于是這些存活下來的對象就會全部進入老年代,或者存活下來的對象比較多,達到S區的50%,觸發動態年齡判定規則,那么也會導致下次新生代GC的存活對象進入老年代。

所以核心還是在于調節-XX:MaxGCPauseMills這個參數的值。在保證新生代GC不太頻繁的同時,還得考慮每次GC后有多少存活對象。避免存活對象太多快速進入老年代,頻繁觸發Mixed GC。

11.使用jstat了解線上系統的JVM運行狀況

(1)新生代對象增長的速率

需要了解JVM的第一個信息就是:隨著系統運行,每秒會在新生代的Eden區分配多少對象。要獲取該信息,只需要在Linux機器上運行命令:jstat -gc PID 1000 10,該命令意思:每隔1秒更新最新jstat統計信息,一共執行10次jstat統計。通過這行命令可以靈活地對線上機器以固定頻率輸出統計信息。從而觀察出每隔一段時間,JVM中Eden區的對象占用變化。

比如執行這行命令后:第一秒顯示出Eden區使用了200M內存,第二秒顯示出Eden區使用了205M內存,第三秒顯示出Eden區使用了209M內存,以此類推。此時我們就可以推斷出來,這個系統大概每秒鐘會新增5M左右的對象。而且這里可以根據自己系統的情況靈活多變地使用,如果系統負載很低,則不一定每秒進行統計,可以每分或每10分來統計,以此查看系統每隔1分鐘或者10分鐘大概增長多少對象。

此外,系統一般有高峰和日常兩種狀態,比如系統高峰期用戶很多,我們應該在系統高峰期去用上述命令看看高峰期的對象增長速率,然后再在非高峰的日常時間段內看看對象的增長速率,這樣就可以了解清楚系統的高峰和日常時間段內的對象增長速率了。

(2)Young GC的觸發頻率和每次耗時

接著需要了解JVM的第二個信息是:大概多久會觸發一次YGC,以及每次YGC的耗時。其實多久觸發一次YGC是很容易推測出來的,因為系統高峰和日常時的對象增長速率都知道了,根據對象增長速率和Eden區大小,就可以推測出:高峰期多久發生一次YGC,日常期多久發生一次YGC。

比如Eden區有800M內存:如果發現高峰期每秒新增5M對象,那么大概3分鐘會觸發一次YGC。如果發現日常期每秒新增0.5M對象,那么大概半小時才觸發一次YGC。

那么每次Young GC的平均耗時呢?jstat會展示迄今為止系統已經發生了多少次YGC以及這些YGC的總耗時。比如系統運行24小時后共發生了260次YGC,總耗時為20s。那么平均下來每次YGC大概耗時幾十毫秒,我們由此可以大概知道每次YGC時會導致系統停頓幾十毫秒。

(3)每次Young GC后有多少對象進入老年代

接著要了解JVM的第三個信息是:每次YGC后有多少存活對象,即有多少對象會進入老年代。

其實每次YGC過后有多少對象會存活下來,只能大致推測出來。假設已經推算出高峰期多久會發生一次YGC,比如3分鐘會有一次YGC。那么此時就可以執行下述jstat命令:jstat -gc PID 180000 10。這就相當于讓JVM每隔三分鐘執行一次統計,連續執行10次。觀察每隔三分鐘發生一次YGC時,Eden、Survivor、老年代的對象變化。

正常來說:Eden區肯定會在幾乎放滿后又變得很少對象,比如800M只使用幾十M。Survivor區肯定會放入一些存活對象,老年代可能會增長一些對象。所以這時觀察的關鍵,就是觀察老年代的對象增長速率。

正常來說:老年代不太可能不停快速增長的,因為普通系統沒那么多長期存活對象。如果每次YGC后,老年代對象都要增長幾十M,則可能存活對象太多了。存活對象太多可能會導致放入S區后觸發動態年齡判定規則進入老年代,存活對象太多也可能導致S區放不下,大部分存活對象需要進入老年代。

如果老年代每次在YGC過后就新增幾百K或幾M的對象,這個還算正常。但如果老年代對象快速過快增長,那一定是不正常的。所以通過上述觀察策略,就可以知道每次YGC后有多少對象是存活的,也就是Survivor區里增長的 + 老年代增長的對象,就是存活的對象。通過jstat -gc也可以知道老年代對象的增長速率,比如每隔3分鐘一次YGC,每次會有50M對象進入老年代,于是老年代對象的增長速率就是每隔3分鐘增長50M。

(4)Full GC的觸發時機和耗時

只要知道老年代對象的增長速率,那么Full GC的觸發時機就很清晰了。比如老年代有800M,每3分鐘新增50M,則每1小時就會觸發一次FGC。根據jstat輸出的系統運行迄今為止的FGC次數以及總耗時,就能計算出每次FGC耗時。比如一共執行了10次FGC,共耗時30s,那么每次FGC大概耗費3s左右。

12.線上FGC的幾種案例

(1)如何優化每秒十萬QPS的社交APP的JVM性能(增加S區大小 + 優化內存碎片)

(2)如何對垂直電商APP后臺系統的FGC進行深度優化(定制JVM參數模版)

(3)不合理設置JVM參數可能導致頻繁FGC(優化反射的軟引用被每次YGC回收)

(4)線上系統每天數十次FGC導致頻繁卡頓的優化(大對象問題)

(5)電商大促活動下嚴重FGC導致系統直接卡死的優化(System.gc()導致)

13.CPU負載過高的原因

(1)機器CPU負載過高有兩個原因

原因一:在系統里創建了大量線程,這些線程同時并發運行,且工作負載都很重,過多的線程同時并發運行就會導致機器CPU負載過高。

原因二:機器上運行的JVM在頻繁FGC,FGC是非常耗費CPU資源的,它也是一個非常重負載的過程。

(2)頻繁FGC會導致的兩個現象

現象一:系統可能時不時因為FGC的STW而卡頓。

現象二:機器的CPU負載很高。

(3)排查CPU負載過高的原因

知道CPU負載過高的兩個原因后,就很容易進行排查了,這時候完全可以使用排除法來做。首先看一下JVM FGC的頻率,通過jstat或監控平臺可以很容易看到現在FGC的頻率。如果FGC頻率過高,就是FGC引起的CPU負載過高。如果FGC頻率正常,就是系統創建了過多線程并發執行負載很重的任務。

所以當時直接通過監控平臺就可以看到:JVM的FGC頻率變得極為頻繁,幾乎是每分鐘都有一次FGC。每分鐘一次FGC,一次至少耗時幾百毫秒,可見這個系統性能很糟糕。

(4)排查頻繁FGC的問題

出現頻繁FGC一般有三個可能:

可能一:內存分配不合理或高并發,導致對象頻繁進入老年代,引發頻繁FGC。

可能二:存在內存泄漏,即內存里駐留了大量對象塞滿了老年代且無法回收,導致稍微有一些對象進入老年代就會引發FGC。

可能三:Metaspace里的類太多,觸發了FGC。

當然如果上述三個原因都不存在,但是還是有頻繁FGC,也許就是工程師錯誤的執行System.gc()導致的了。但這個一般很少見,而且JVM參數中可以禁止這種顯式觸發的GC。

一般排查頻繁FGC,核心利器就是jstat了。當時使用jstat分析了一下線上系統的情況,發現并不存在內存分配不合理導致對象頻繁進入老年代的問題,而且永久代的內存使用也很正常,所以排除掉了上述三個原因中的兩個。那么接下來考慮最后一個原因:老年代里是不是駐留了大量的對象。是的,當時系統就是這個問題。

通過jstat可以明顯發現老年代駐留了大量的對象,幾乎快塞滿了。所以年輕代稍微有一些對象進入老年代,就會很容易觸發FGC。而且FGC后還回收不了老年代里大量的對象,只能回收一小部分而已。所以老年代里駐留了大量本不應該存在的對象,才導致頻繁觸發FG

接下來就是要想辦法找到這些對象了,前面介紹過jmap + jhat的組合來分析內存里的大對象,接下來介紹另外一個常用的強有力的工具MAT。jhat適合快速的去分析一下內存快照,但是功能上不是太強大,所以一般會使用比較強大的而且也特別常用的內存分析工具MAT。

(5)基于MAT來進行內存泄漏分析

使用MAT打開一個內存快照后,MAT上有一個工具欄,里面有一個按鈕。這個按鈕的英文是:Leak Suspects,就是內存泄漏的分析。

接著MAT會分析選擇的內存快照,嘗試找出導致內存泄漏的一批對象。這時可以看到它會顯示出一個大的餅圖,展示哪些對象占用內存過大。這時直接會看到某種自己系統創建的對象占用量過大,這種對象的實例多達數十萬個,占用了老年代一大半的內存空間。

接著就可以找開發工程師去排查這個系統的代碼問題了,為什么會創建那么多對象,且始終回收不掉?這就是典型的內存泄漏,即系統創建了大量的對象占用了內存,很多對象不再使用但又無法回收。

后來找出了原因:就是系統里做了一個JVM本地緩存,把很多數據都加載到內存里緩存,然后提供查詢服務時會直接從本地內存里進行查詢。但因為沒有限制本地緩存大小,且沒有使用LRU算法定期淘汰緩存數據。最終導致緩存在內存里的對象越來越多,最后造成了內存泄漏。

解決問題很簡單:只要使用如Ehcache等緩存框架即可,它會固定最多緩存多少個對象,以及定期淘汰一些不常訪問的緩存,以便新數據可以進入緩存中。

14.JVM運行原理和GC原理總結

(1)JVM和YGC的運行原理

首先必須要明白,JVM是如何運行起來的。

JVM的內存區域劃分:最核心的就是:新生代、老年代、Metaspace(永久代),其中新生代又分成了Eden區和2個Survivor區,默認比例是8 : 1 : 1。

系統程序會不停在新生代Eden區創建各種對象:系統程序會不停運行,運行時會不停在新生代的Eden區中創建各種對象。

方法運行完畢,其局部變量引用的對象可被回收:一般創建對象都是在各種方法里執行的,一旦方法運行完畢,方法局部變量引用的那些對象就會成為Eden區里的垃圾對象可被回收。

隨著不斷創建對象,Eden區就會逐步被占滿:這時可能Eden區里的對象大多數都是垃圾對象,一旦Eden區被占滿后,就會觸發一次YGC。首先從GC Roots(方法局部變量、類靜態變量)開始追蹤,標記存活對象。然后用復制算法把存活對象放入第一個Survivor區中,也就是S0區。

接著新生代垃圾回收器就會回收掉Eden區里剩余的全部垃圾對象:在整個新生代垃圾回收的過程中全程會進入STW狀態。也就是暫停系統工作線程,系統代碼全部停止運行,不允許創建新對象。這樣才能讓新生代垃圾回收器專心工作,找出存活對象然后回收垃圾對象。一旦新生代垃圾回收全部完畢,存活對象都進入了Survivor區域。然后Eden區都清空了,那么YGC就會執行完畢。此時系統程序恢復工作,繼續在Eden區里創建對象。

下一次如果Eden區又滿了,就會再次觸發YGC:把Eden區和S0區里的存活對象轉移到S1區里去,然后直接清空掉Eden區和S0區中的垃圾對象。當然這個過程中系統程序是禁止工作的,處于Stop the World狀態。

負責YGC的垃圾回收器有很多種,常用的是ParNew垃圾回收器。它的核心執行原理就如上所述,只不過ParNew運行時是基于多線程并發執行垃圾回收的,以上就是最基本的JVM和YGC的運行原理。

(2)對象什么時候進入老年代

導致對象會進入老年代區域中的情況如下:

情況一:對象在新生代里躲過15次垃圾回收,年齡太大要進入老年代。

情況二:對象太大超過了一定的閾值,直接進入老年代,不經過新生代。

情況三:YGC后存活對象太多導致S區放不下,存活對象會進入老年代。

情況四:可能幾次YGC過后,Surviovr區域中的對象占用超50%的內存,此時如果年齡1+年齡2+年齡N的對象總和超過了Survivor區域的50%,那么年齡N及以上的對象都進入老年代,即動態年齡判定規則。

對象進入老年代的情況說明:

說明一:躲過15次YGC的對象畢竟是少數。

說明二:大對象一般在特殊情況下會有。

說明三:加載大量數據長時間處理及高并發,才容易導致存活對象過多。

對于這些情況,都會導致對象進入老年代中,老年代對象會越來越多。

(3)老年代的GC是如何觸發的

一旦老年代對象過多,就可能會觸發FGC。FGC必然會帶著Old GC,也就是針對老年代的GC,而且FGC一般也會跟著一次YGC,也會觸發一次永久代GC。

觸發FGC的幾個條件如下:

條件一:可以設置老年代內存使用閾值,有一個JVM參數可以控制。老年代內存使用達到閾值就會觸發FGC,一般建議調大一些,如92%。

條件二:在執行YGC前,如果發現老年代可用空間小于歷次YGC后升入老年代的平均對象大小。那么就會在YGC前觸發FGC,先回收掉老年代一批對象,再執行YGC。

條件三:在執行YGC后,如果YGC過后的存活對象太多,Survivor區放不下,要放入老年代。但是此時老年代也放不下,就會觸發FGC,回收老年代一批對象,然后再把這些年輕代的存活對象放入老年代。

觸發FGC幾個比較核心的條件就是這幾個,總結起來就是:老年代一旦快要滿了,空間不夠了,必然要進行FGC垃圾回收。

老年代的垃圾回收通常建議使用CMS垃圾回收器。此外老年代GC的速度是很慢的,少則幾百毫秒,多則幾秒。所以一旦FGC很頻繁,就會導致系統性能很差。因為頻繁FGC會頻繁停止系統工作線程,導致系統一直有卡頓的現象。而且頻繁FGC還會導致機器CPU負載過高,導致機器性能下降。

所以優化JVM的核心就是減少FGC的頻率。

(4)正常情況下系統的GC頻率

正常YGC頻率是幾分鐘或幾十分鐘一次,一次耗時幾毫秒到幾十毫秒。正常FGC頻率是幾十分鐘一次或幾小時一次,一次耗時大概幾百毫秒。

所以如果觀察線上系統就是這個性能表現,基本上問題都不太大。實際線上系統很多時候會遇到一些JVM性能問題:比如FGC過于頻繁,每次耗時很多,此時就需要進行優化了。

(5)CPU負載高原因總結

CPU負載高的兩個原因:

原因一:系統里創建了大量線程并發執行。

原因二:JVM在執行頻繁的FGC。

(6)FGC頻繁的原因總結

頻繁FGC問題的三個可能:

可能一:內存分配不合理或高并發,導致對象頻繁進入老年代,引發頻繁FGC。

可能二:存在內存泄漏,就是內存里駐留了大量對象塞滿了老年代且無法回收。

可能三:Metaspace里的類太多,觸發了FGC。

15.JVM性能優化的思路和步驟

(1)一個新系統開發完畢后應如何設置JVM參數

一個新系統開發完畢后,到底該如何預估及合理設置JVM參數呢?畢竟直接用默認的JVM參數部署上線再觀察,是非常的不靠譜的,而很多公司其實也沒有所謂的JVM參數模板。

步驟一:估算新系統每秒占用多少內存,每秒多少次請求、每次請求創建多少對象、每個對象大概多大、每秒使用多少內存空間。

步驟二:估算Eden區大概多長時間會占滿

步驟三:估算多長時間會發生一次YGC

步驟四:估算YGC時有多少對象存活而升入老年代

步驟五:估算老年代對象的增長速率 + 多久會FGC

通過一連串估算就能合理分配新生代、老年代、Eden、Survivor空間。原則就是:讓YGC后存活對象遠小于S區,避免對象頻繁進入老年代觸發FGC。

最理想的狀態就是:系統幾乎不發生FGC,老年代應該就是穩定占用一定的空間。就是那些長期存活的對象在躲過15次YGC后升入老年代占用的,然后平時主要就是幾分鐘發生一次YGC,耗時幾毫秒。

(2)在壓測之后合理調整JVM參數

任何一個新系統上線都得進行壓測,在模擬線上壓力的場景下,用jstat等工具去觀察JVM的運行指標:

指標一:Eden區的對象增長速率多快

指標二:YGC頻率多高

指標三:一次YGC多長耗時

指標四:YGC過后多少對象存活

指標五:老年代的對象增長速率多高

指標六:FGC頻率多高

指標七:一次FGC耗時多少

壓測時可以完全精準的通過jstat觀察出上述JVM運行指標,然后就可以優化JVM的內存分配:盡量避免對象頻繁進入老年代,盡量讓系統只有YGC。

(3)線上系統的監控和優化

系統上線后,務必要進行一定的監控。一般通過Zabbix等工具來監控機器和JVM的運行,頻繁FGC就要告警。沒這些工具,就在機器上運行jstat,把監控信息寫入文件,定時查看。

一旦發現頻繁FGC的情況就要進行優化,優化的核心思路是類似的:通過jstat分析出來系統的JVM運行指標,找到FGC的核心問題。然后優化一下JVM的參數,盡量讓對象別進入老年代,減少FGC的頻率。

(4)線上頻繁Full GC的幾種表現

一旦系統發生頻繁Full GC,可能會看到:

表現一:機器CPU負載過高

表現二:頻繁FGC報警

表現三:系統無法處理請求或者處理過慢

所以一旦發生上述幾個情況,第一時間應該想到是不是發生了頻繁FGC。

(5)頻繁FGC的幾種常見原因

頻繁FGC的常見原因有下面幾個:

原因一:系統承載高并發請求,或者處理數據量過大,導致YGC很頻繁。如果每次YGC后存活對象太多,內存分配不合理,Survivor區過小,必然會導致對象頻繁進入老年代,頻繁觸發FGC。

原因二:系統一次性加載過多數據進內存,創建出來很多大對象。導致頻繁有大對象進入老年代,必然頻繁觸發FGC。

原因三:系統發生了內存泄漏,莫名其妙創建大量的對象,始終無法回收。大量的對象一直占用在老年代里,必然頻繁觸發FGC。

原因四:Metaspace因加載類過多觸發FGC。

原因五:誤調用System.gc()觸發FGC。

其實常見的頻繁FGC原因無非就上述那幾種,所以在線上處理FGC時,可以就從這幾個角度入手使用jstat分析。

如果jstat分析發現FGC原因是第一種:新生代升入老年代多且頻繁,但老年代并沒有大量對象一直無法回收。那么就合理分配內存,調大Survivor區即可。

如果jstat分析發現是第二種或第三種原因:也就是老年代一直有大量對象無法回收,新生代升入老年代的對象不多。那么就dump出內存快照,用MAT工具進行分析,找出占用過多的對象。通過分析對象的引用和線程執行堆棧,找到導致那么多對象的那塊代碼,接著優化代碼即可。

如果jstat分析發現內存使用不多但頻繁觸發FGC,必然是第四第五種,此時進行對應優化即可。

16.OOM的原因

(1)可能發生OOM的區域有三塊

第一塊是存放類信息的Metaspace區域

第二塊是每個線程的虛擬機棧內存

第三塊是堆內存空間

(2)Metaspace如何因類太多而發生內存溢出

Metaspace區域發生內存溢出的原理是:Metaspace滿了之后先FGC -> 發現回收不了足夠空間就OOM。

兩種常見的觸發Metaspace內存溢出原因是:默認JVM參數導致Metaspace區域過小 + CGLIB等動態生成類過多。

因此只要合理分配Metaspace區域的內存大小,避免無限制地動態生成類,一般Metaspace區域都是比較安全的,不會觸發OOM內存溢出。

(3)無限制調用方法如何讓線程的棧內存溢出

棧內存溢出的原因和場景:原因是大量的棧幀會消耗完線程的棧內存 + 場景是方法無限遞歸調用。所以只要避免代碼出現無限方法遞歸,一般就能避免棧內存溢出。

(4)對象太多導致堆內存實在放不下而內存溢出

發生堆內存溢出的原因:有限的內存中放了過多對象,而且大多都是存活的,此時即使FGC后還是有大部分對象存活,要繼續放入更多對象已經不可能,只能引發內存溢出。

發生內存溢出有幾種場景:

場景一:系統承載高并發請求,因為請求量過大導致大量對象都是存活的。此時無法繼續往堆內存里放入新的對象了,就會引發OOM系統崩潰。

場景二:系統有內存泄漏,創建了很多對象,結果對象都是存活的沒法回收。由于不能及時取消對它們的引用,導致觸發FGC后還是無法回收。此時只能引發內存溢出,因為老年代已經放不下更多的對象了。

場景三:代碼問題創建的對象占用了大量內存,且該方法一直在長時間運行。這樣導致占用大量內存的對象一直不釋放。

因此引發堆內存OOM的原因可能是:系統負載過高、存在內存泄漏、創建大量對象長時間運行,不過OOM一般是由代碼寫得差或設計缺陷引發的。

(5)如何在JVM內存溢出時自動dump內存快照

JVM如果知道自己將要發生OOM了,那么此時完全可以讓它做點事情。比如可以讓JVM在OOM時dump一份內存快照,事后只要分析這個內存快照,就可以知道是哪些對象導致OOM的了。為此,需要在JVM的啟動參數中加入如下參數:

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/usr/local/app/oom
第一個參數的意思是:在OOM時自動進行dump內存快照。第二個參數的意思是:把內存快照放到哪里。只要加入了這兩個參數,可以事后再獲取OOM時的內存快照進行分析。

(6)OOM內存溢出排查總結

情形一:Metaspace區域溢出

通過異常信息可以直接定位出是Metaspace區域發生異常,然后分析GC日志就可以知道Metaspace發生溢出的全過程,接著再使用MAT分析內存快照,就知道是哪個類太多導致異常。

情形二:棧內存溢出

首先從異常日志中就能知道是棧內存溢出。

然后從異常日志中可以找到對應的報錯方法。

知道哪個方法后,就可以到代碼中定位問題。

情形三:堆內存溢出

堆內存溢出問題的分析和定位:

一是加入自動導出內存快照的參數

二是到線上看一下日志文件里的報錯

如果是堆溢出,則用MAT分析內存快照。

MAT分析的時候一些順序和技巧:

一.首先看占用內存最多的對象是誰

二.然后分析那個線程的調用棧

三.接著看哪個方法引發內存溢出

四.最后優化代碼即可

(7)MAT使用技巧總結

一.首先通過Histogram界面找占用大的對象

二.然后進入dominator_tree界面找對應的線程

三.接著進入thread_overview界面找線程對應的方法調用棧

(8)接口假死的兩者情況

為什么要先用top看機器資源?因為如果服務出現無法調用接口假死的情況,首先要考慮的是兩種原因。

第一種原因:這個服務可能使用了大量的內存,內存始終無法釋放,導致頻繁FGC。也許每秒都執行一次FGC,結果每次都回收不了,最終導致頻繁FGC。頻繁FGC又會頻繁Stop the World,所以接口調用時就會出現頻繁假死。

第二種原因:可能是這臺機器的CPU負載太高了,也許是某個進程耗盡了CPU資源。CPU資源耗盡會導致這個服務的線程始終無法得到CPU資源去執行。沒有CPU資源去執行也就無法響應接口調用請求,也會出現頻繁假死。

因此針對服務假死的問題,先通過top命令查看,就知道關鍵點了。

(9)JVM出現內存溢出的三種原因

原因一是并發太高,大量并發創建過多的對象,導致系統直接崩潰了。

原因二是內存泄漏,有很多對象都在內存里,無論如何GC都回收不掉。

原因三是代碼問題,導致某種情況下加載了大量數據,創建了大量對象。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/95547.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/95547.shtml
英文地址,請注明出處:http://en.pswp.cn/web/95547.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

車輛安全供電系統開發原則和實踐

摘要在汽車行業中&#xff0c;安全應用的重要性在不斷提升&#xff0c;例如受車輛自動化發展以及機械備用系統重要性降低的影響。為應對這些趨勢&#xff0c;安全相關的電氣和 / 或電子系統&#xff08;E/E 系統&#xff09;的電源輸入必須由供電系統來保障&#xff0c;這使得功…

WebSocket客戶端庫:websocket-fruge365

&#x1f680; 從零開始打造一個WebSocket客戶端庫&#xff1a;websocket-fruge365 &#x1f4d6; 前言 在現代Web開發中&#xff0c;實時通信已經成為不可或缺的功能。無論是聊天應用、實時數據監控&#xff0c;還是在線協作工具&#xff0c;WebSocket都扮演著重要角色。然而…

rocketmq批量執行跑批任務報錯

rocketmq批量執行跑批任務&#xff0c;報下面的錯誤&#xff0c;怎么處理一下呢&#xff1f;是修改配置還是修改代碼還是&#xff1f; org.apache.rocketmq.client.exception.MQBrokerException: CODE: 215 DESC: [FLOW]client has exhausted the send quota for the current …

大語言模型(LLM)簡介與應用分享

1. 什么是大語言模型&#xff08;LLM&#xff09; 大語言模型&#xff08;Large Language Model&#xff0c;簡稱 LLM&#xff09;是基于 深度學習 和 海量文本數據 訓練而成的人工智能模型。 采用 Transformer 架構參數規模巨大&#xff08;數十億到數千億&#xff09;能夠 理…

【算法筆記】選擇排序、插入排序、冒泡排序、二分查找問題

算法的筆記&#xff0c;直接上代碼&#xff0c;思路和問題這些&#xff0c;都在代碼注釋上面 1、工具類 為了生成測試代碼和比較器&#xff0c;專門寫了一個數組工具類&#xff0c;代碼如下&#xff1a; /*** 數組工具類*/ public class ArrUtil {/*** 生成隨機數組* 長度是[0,…

行業分享丨基于SimSolid的大型汽車連續沖壓模具剛度分析

*本文投稿自機械零部件制造業用戶 汽車連續模具的剛度直接決定了沖壓件質量&#xff08;尺寸精度、表面缺陷&#xff09;與模具壽命。傳統有限元分析&#xff08;FEA&#xff09;在面對大型復雜模具裝配體時&#xff0c;存在網格劃分困難、計算資源消耗大、周期長等瓶頸。本文以…

用AI生成的html頁面設計放到到Axure上實現再改造的方法

要將 AI 生成的 HTML 原型導入 Axure&#xff0c;該方法的核心邏輯是以 Figma 為 “中間橋梁”&#xff08;因 Axure 無法直接讀取 HTML&#xff0c;需通過 Figma 轉換格式&#xff09;&#xff0c;分 3 步即可完成&#xff0c;以下是詳細操作指南&#xff08;含每步目標、具體…

從入門到實戰:Linux sed命令全攻略,文本處理效率翻倍

從入門到實戰&#xff1a;Linux sed命令全攻略&#xff0c;文本處理效率翻倍 文章目錄從入門到實戰&#xff1a;Linux sed命令全攻略&#xff0c;文本處理效率翻倍一、認識sed&#xff1a;什么是流編輯器&#xff1f;二、吃透sed工作原理&#xff1a;為什么它能高效處理文本&am…

TIOBE 8月編程語言榜深度解析:Python占比突破26%,Perl成最大黑馬

根據TIOBE最新發布的2025年8月編程語言排行榜&#xff0c;一場靜默的技術變革正在上演&#xff1a;Python以26.14%的占比首次突破26%大關&#xff0c;連續12個月穩居榜首。這一數據不僅刷新了Python自身的歷史紀錄&#xff0c;更成為TIOBE指數自2001年創立以來的最高單語言占比…

從發現到恢復,看瑞數信息如何構建“抗毀重構”實戰路徑

在信息化社會&#xff0c;“韌性”“彈性”這些詞匯常被用來形容系統抵御和應對風險的能力&#xff0c;但對于身處關鍵基礎設施行業的運營者來說&#xff0c;這些概念往往過于抽象&#xff0c;難以直接指導實踐。 相比之下&#xff0c;“抗毀重構”更具畫面感。它不僅是一個管理…

深入理解 jemalloc:從內存分配機制到技術選型

在高性能服務&#xff08;如數據庫、緩存、JVM&#xff09;的底層優化中&#xff0c;內存分配效率直接影響系統整體性能。本文將從操作系統底層的malloc機制切入&#xff0c;詳解 jemalloc 的設計理念、開源應用場景、實戰案例&#xff0c;技術選型分析 一、操作系統底層的內存…

websoket使用記錄

1.項目使用記錄1.醫療項目中渲染回收柜溫濕度&#xff0c;需要實時更新2.回收柜安瓿回收和余液回收時&#xff0c;需要前端發送指令給回收柜&#xff0c;比如開門、關門等。還需要收到回收柜結果&#xff0c;比如回收的藥品信息等。我項目中用的是瀏覽器自帶的websoket&#xf…

DevOps篇之通過GitLab CI 流水線實現k8s集群中helm應用發布

一. 設計思路 構建一個 GitLab CI 流水線&#xff0c;并且要集成到 K8s 集群中的 Helm 應用發布流程。首先&#xff0c;需要了解 GitLab CI 的基本結構&#xff0c;比如.gitlab-ci.yml 文件的配置&#xff0c;包括 stages、jobs、變量設置等。然后&#xff0c;結合之前討論的 H…

詳盡 | Deeplabv3+結構理解

https://arxiv.org/pdf/1802.02611.pdf https://link.springer.com/chapter/10.1007/978-3-319-10578-9_23 目錄 Deeplabv3 Encoder部分 Decoder部分 補充摘要 SPP 空間金字塔池化層模塊 Dilated/Atrous Conv 空洞卷積 Deeplabv3 deeplab-v3是語義分割網絡&#xff0c;組…

【51單片機】【protues仿真】基于51單片機音樂盒(8首歌曲)系統

目錄 一、主要功能 二、使用步驟 三、硬件資源 四、軟件設計 五、實驗現象 一、主要功能 1、數碼管顯示當前歌曲序號 2、按鍵切換歌曲和播放暫停? 3、內置8首音樂 二、使用步驟 基于51單片機的音樂盒是一種能夠存儲和播放多首歌曲的電子設備&#xff0c;通過定時器產…

@ZooKeeper 詳細介紹部署與使用詳細指南

文章目錄 **ZooKeeper 詳細介紹、部署與使用** 1. 概述 & 核心介紹 1.1 什么是 ZooKeeper? 1.2 核心特性 1.3 核心概念 1.4 典型應用場景 2. 部署 (以 3 節點集群為例) 2.1 環境準備 2.2 安裝步驟 (在所有節點執行) 2.3 啟動與停止集群 2.4 防火墻配置 (如果開啟) 3. 基本…

騰訊Hunyuan-MT-7B翻譯模型完全指南:2025年開源AI翻譯的新標桿

&#x1f3af; 核心要點 (TL;DR) 突破性成就&#xff1a;騰訊混元MT-7B在WMT25全球翻譯競賽中獲得30/31項第一名雙模型架構&#xff1a;Hunyuan-MT-7B基礎翻譯模型 Hunyuan-MT-Chimera-7B集成優化模型廣泛語言支持&#xff1a;支持33種語言互譯&#xff0c;包括5種中國少數民…

Web 集群高可用全方案:Keepalived+LVS (DR) 負載均衡 + Apache 服務 + NFS 共享存儲搭建指南

文章目錄Keepalived LVS&#xff08;DR&#xff09; Apache NFS項目背景業務場景與核心需求傳統架構的痛點與局限技術方案的選型邏輯項目價值與預期目標項目實踐項目環境基礎配置配置 router配置免密登錄-可選配置 nfs配置 web配置 LVS-RS配置 HA 和 LVS-DS配置 ha1配置 ha2測…

Prometheus監控預警系統深度解析:架構、優劣、成本與競品

目錄 一、Prometheus是什么&#xff1f;核心定位與架構 二、競品分析&#xff08;Prometheus vs. Zabbix vs. Nagios vs. Commercial SaaS&#xff09; 三、部署成本分析 四、服務器資源消耗分析 五、給您的最終建議 一、Prometheus是什么&#xff1f;核心定位與架構 Prom…

Nginx反向代理及配置

Nginx反向代理 二級域名系統 顧名思義&#xff0c;我們有很多的這個不同的二級域名的用戶來訪問我們&#xff0c;比如說微博。它有一個主域名weibo.com。如果我叫一鳴,申請了一個微博&#xff0c;然后我就可以在微博這個主系統上申請一個二級域名來訪問我微博的主頁&#xff0…