JVM 学习整理
一、運行時數據區域(內存模型)
1.內存區域
? 是指JVM運行時將數據分區域存儲,強調對內存空間的劃分。
2.內存模型
? 是定義了線程和主內存之間的抽象關系,即JMM定義了JVM在計算機內存中的工作方式。共享內存的并發模型,線程之間主要通過讀-寫共享變量(堆內存中的實例域,靜態域和數組元素)來完成隱式通信。Java 內存模型(JMM)控制 Java 線程之間的通信,決定一個線程對共享變量的寫入何時對另一個線程可見。
主要的運行時數據如下:
3.內存區域圖
jdk8 之前內存區域圖:
jdk8之后內存區域圖:
Java 8 中 PermGen 為什么被移出 HotSpot JVM 了?兩個主要原因:
? 根據上面的各種原因,PermGen 最終被移除,方法區移至 Metaspace,字符串常量移至 Java Heap。
程序計數器
? 是一塊較小的內存空間,它可以看做是當前線程所執行的字節碼的行號指示器。
? 由于 Java 虛擬機的多線程是通過線程輪流切換并分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器內核都只會執行一條線程中的指令。因此,為了線程切換后能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,我們稱這類內存區域為“線程私有”的內存。
-
如果線程正在執行的是一個 Java 方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;
-
如果正在執行的是 Native 方法,這個計數器值則為空(Undefined)。
? 此內存區域是唯一一個在 Java 虛擬機規范中沒有規定任何 OutOfMemoryError 情況的區域。
java虛擬機棧
? 與程序計數器一樣,Java虛擬機棧也是線程私有的,它的生命周期與線程相同。
? 虛擬機棧描述的是 Java 方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀(Stack Frame,是方法運行時的基礎數據結構)用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。
? 在活動線程中,只有位千棧頂的幀才是有效的,稱為當前棧幀。正在執行的方法稱為當前方法,棧幀是方法運行的基本結構。在執行引擎運行時,所有指令都只能針對當前棧幀進行操作。
(1)局部變量表
? 局部變量表是存放方法參數和局部變量的區域。局部變量沒有準備階段,必須顯示初始化。如果是非靜態方法,則在index[0]位置上存儲的是方法所屬對象的實例引用,一個引用變量占4個字節,隨后存儲的是參數和局部變量。字節碼指令中的STORE指令就是將操作棧中計算完成的局部變呈寫回局部變量表的存儲空間內。
? 虛擬機棧規定了兩種異常狀況:如果線程請求的棧深度大于虛擬機所允許的深度,將拋出 StackOverflowError 異常;如果虛擬機棧可以動態擴展(當前大部分的 Java 虛擬機都可動態擴展),如果擴展時無法申請到足夠的內存,就會拋出 OutOfMemoryError 異常。
(2)操作棧
? 操作棧是個初始狀態為空的桶式結構棧。在方法執行過程中,會有各種指令往棧中寫入和提取信息。JVM的執行引擎是基于棧的執行引擎,其中的棧指的就是操作棧。字節碼指令集的定義都是基于棧類型的,棧的深度在方法元信息的stack屬性中。
i++ 和 ++i 的區別:
之前之所以說 i++ 不是原子操作,即使使用 volatile 修飾也不是線程安全,就是因為,可能 i 被從局部變量表(內存)取出,壓入操作棧(寄存器),操作棧中自增,使用棧頂值更新局部變量表(寄存器更新寫入內存),其中分為 3 步,volatile 保證可見性,保證每次從局部變量表讀取的都是最新的值,但可能這 3 步可能被另一個線程的 3 步打斷,產生數據互相覆蓋問題,從而導致 i 的值比預期的小。
(3)動態鏈接
? 每個棧幀中包含一個在常量池中對當前方法的引用, 目的是支持方法調用過程的動態連接。
(4)方法返回地址
? 方法執行時有兩種退出情況:正常退出,即正常執行到任何方法的返回字節碼指令,如 RETURN、IRETURN、ARETURN 等;異常退出。
? 無論何種退出情況,都將返回至方法當前被調用的位置。方法退出的過程相當于彈出當前棧幀,退出可能有三種方式:返回值壓入上層調用棧幀/異常信息拋給能夠處理的棧幀/PC計數器指向方法調用后的下一條指令。
本地方法棧
? 本地方法棧與虛擬機棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機棧為虛擬機執行java方法服務,而本地方法棧則為虛擬機使用到的Native方法服務。Sun HotSpot虛擬機直接就把本地方法棧和虛擬機棧合二為一。與虛擬機棧一樣,本地方法棧區域也會拋出StackOverflowErro、rOutOfMemoryError異常。
? 線程開始調用本地方法時,會進入 個不再受 JVM 約束的世界。本地方法可以通過 JNI(Java Native Interface)來訪問虛擬機運行時的數據區,甚至可以調用寄存器,具有和 JVM 相同的能力和權限。 當大量本地方法出現時,勢必會削弱 JVM 對系統的控制力,因為它的出錯信息都比較黑盒。對內存不足的情況,本地方法棧還是會拋出 nativeheapOutOfMemory。
? JNI 類本地方法最著名的應該是 System.currentTimeMillis() ,JNI使 Java 深度使用操作系統的特性功能,復用非 Java 代碼。 但是在項目過程中, 如果大量使用其他語言來實現 JNI , 就會喪失跨平臺特性。
Java堆
? Java堆是Java虛擬機所管理的內存中最大的一塊。Java堆是被所以線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內存。
? 堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC堆”(Garbage Collected Heap)。從內存回收的角度來看,由于現在收集器基本都采用分代收集算法,所以 Java 堆中還可以細分為:新生代和老年代;再細致一點的有 Eden 空間、From Survivor 空間、To Survivor 空間等。從內存分配的角度來看,線程共享的 Java 堆中可能劃分出多個線程私有的分配緩沖區(Thread Local Allocation Buffer,TLAB)。
? Java 堆可以處于物理上不連續的內存空間中,只要邏輯上是連續的即可,當前主流的虛擬機都是按照可擴展來實現的(通過 -Xmx 和 -Xms 控制)。如果在堆中沒有內存完成實例分配,并且堆也無法再擴展時,將會拋出 OutOfMemoryError 異常。
方法區
? 方法區與Java堆一樣,是各個線程共享的內存區域,它用于儲存已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。雖然Java虛擬機規范把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是與Java堆區分開來。
? Java 虛擬機規范對方法區的限制非常寬松,除了和 Java 堆一樣不需要連續的內存和可以選擇固定大小或者可擴展外,還可以選擇不實現垃圾收集。垃圾收集行為在這個區域是比較少出現的,其內存回收目標主要是針對常量池的回收和對類型的卸載。當方法區無法滿足內存分配需求時,將拋出 OutOfMemoryError 異常。
JDK8 之前,Hotspot 中方法區的實現是永久代(Perm),JDK8 開始使用元空間(Metaspace),以前永久代所有內容的字符串常量移至堆內存,其他內容移至元空間,元空間直接在本地內存分配。
為什么要使用元空間取代永久代的實現?
- 字符串存在永久代中,容易出現性能問題和內存溢出
- 類及方法的信息等比較難確定其大小,因此對于永久代的大小指定比較困難,大小容易出現永久代溢出,太大則容易導致老年代溢出。
- 永久代會為GC帶來不必要的復雜度,并且回收效率偏低。
- 將 HotSpot 與 JRockit 合二為一
(1)運行時常量池
? 運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池中存放。
? 一般來說,除了保存 Class 文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中。
? 運行時常量池相對于 Class 文件常量池的另外一個重要特征是具備動態性,Java 語言并不要求常量一定只有編譯期才能產生,也就是并非預置入 Class 文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的便是 String 類的 intern() 方法。
? 既然運行時常量池是方法區的一部分,自然受到方法區內存的限制,當常量池無法再申請到內存時會拋出 OutOfMemoryError 異常。
(2)直接內存
? 直接內存并不是虛擬機運行時數據區的一部分,也不是Java虛擬機規范中定義的內存區域。
? 在 JDK 1.4 中新加入了 NIO,引入了一種基于通道(Channel)與緩沖區(Buffer)的 I/O 方式,它可以使用 Native 函數庫直接分配堆外內存,然后通過一個存儲在 Java 堆中的 DirectByteBuffer 對象作為這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在 Java 堆和 Native 堆中來回復制數據。
? 顯然,本機直接內存的分配不會受到 Java 堆大小的限制,但是,既然是內存,肯定還是會受到本機總內存(包括 RAM 以及 SWAP 區或者分頁文件)大小以及處理器尋址空間的限制。服務器管理員在配置虛擬機參數時,會根據實際內存設置 -Xmx 等參數信息,但經常忽略直接內存,使得各個內存區域總和大于物理內存限制(包括物理的和操作系統級的限制),從而導致動態擴展時出現 OutOfMemoryError 異常。
volatile關鍵字
volatile 可以說是 JVM 提供的最輕量級的同步機制,當一個變量定義為volatile之后,它將具備兩種特性:
- 保證此變量對所有線程的可見性。
而普通變量不能做到這一點,普通變量的值在線程間傳遞均需要通過主內存來完成。注意,volatile 雖然保證了可見性,但是 Java 里面的運算并非原子操作,導致 volatile 變量的運算在并發下一樣是不安全的。而 synchronized 關鍵字則是由“一個變量在同一個時刻只允許一條線程對其進行 lock 操作”這條規則獲得線程安全的。
- 禁止指令重排序優化。
普通的變量僅僅會保證在該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操作的順序與程序代碼中的執行順序一致。
內存分配與回收策略
對象的內存分配,往大方向講,就是在堆上分配,對象主要分配在新生代的Eden區上。少數情況下也可能會直接分配在老年代中,分配的規則并不是百分之百固定的,其細節取決于當前使用的是哪一種垃圾收集器組合,還有虛擬機中與內存相關的參數的設置。
二、垃圾回收機制
[垃圾回收主要關注Java堆,Java內存運行時區域中的程序計數器、虛擬機棧、本地方法棧隨線程而生滅](https://jingyan.baidu.com/article/4ae03de33b2ca87ffe9e6b4b.html);棧中的棧幀隨著方法的進入和退出而有條不紊地執行著出棧和入棧操作。每一個棧幀中分配多少內存基本上是在類結構確定下來時就已知的,因此這幾個區域的內存分配和回收都具備確定性,不需要過多考慮回收的問題,因為方法總結或者線程結束時,內存自然就跟隨著回收了。? 而Java堆不一樣,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,我們只有在程序處于運行期間時才能知道會創建那些對象,這部分內存的分配和回收都是動態的,垃圾收集器所關注的是這部分內存。
判讀哪些對象需要被回收
- 引用計數法
給對象添加一引用計數器,被引用一次計數器值就加 1;當引用失效時,計數器值就減 1;計數器為 0 時,對象就是不可能再被使用的,簡單高效,缺點是無法解決對象之間相互循環引用的問題。
- 可達性分析算法
通過一系列的稱為 “GC Roots” 的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到 GC Roots 沒有任何引用鏈相連時,則證明此對象是不可用的。此算法解決了上述循環引用的問題。
在Java語言中,可作為 GC Roots 的對象包括下面幾種:
- a. 虛擬機棧(棧幀中的本地變量表)中引用的對象。
- b. 方法區中類靜態屬性引用的對象。
- c. 方法區中常量引用的對象。
- d. 本地方法棧中 JNI(Native方法)引用的對象
作為 GC Roots 的節點主要在全局性的引用與執行上下文中。要明確的是,tracing gc必須以當前存活的對象集為 Roots,因此必須選取確定存活的引用類型對象。
GC 管理的區域是 Java 堆,虛擬機棧、方法區和本地方法棧不被 GC 所管理,因此選用這些區域內引用的對象作為 GC Roots,是不會被 GC 所回收的。其中虛擬機棧和本地方法棧都是線程私有的內存區域,只要線程沒有終止,就能確保它們中引用的對象的存活。而方法區中類靜態屬性引用的對象是顯然存活的。常量引用的對象在當前可能存活,因此,也可能是 GC roots 的一部分。
可達性分析算法
不可達的對象將暫時處于“緩刑”階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:
- 如果對象在進行可達性分析后發現沒有與 GC Roots 相連接的引用鏈,那它將會被第一次標記并且進行一次篩選,篩選的條件是此對象是否有必要執行 finalize() 方法。
- 當對象沒有覆蓋 finalize() 方法,或者 finalize() 方法已經被虛擬機調用過,虛擬機將這兩種情況都視為“沒有必要執行”,直接進行第二次標記。
- 如果這個對象被判定為有必要執行 finalize() 方法,那么這個對象將會放置在一個叫做 F-Queue 的隊列之中,并在稍后由一個由虛擬機自動建立的、低優先級的 Finalizer 線程去執行它。
這里所謂的“執行”是指虛擬機會觸發這個方法,但并不承諾會等待它運行結束,因為如果一個對象在 finalize() 方法中執行緩慢,將很可能會一直阻塞 F-Queue 隊列,甚至導致整個內存回收系統崩潰。
值得注意的是,使用 finalize() 方法來“拯救”對象是不值得提倡的,它的運行代價高昂,不確定性大,無法保證各個對象的調用順序。finalize() 能做的工作,使用 try-finally 或者其它方法都更適合、及時。
強、軟、弱、虛引用
? JDK1.2 以前,一個對象只有被引用和沒有被引用兩種狀態。后來,Java 對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4 種,這 4 種引用強度依次逐漸減弱。
Java堆永久代的回收
? 永久代的垃圾收集主要回收二部分內容:廢棄常量和無用的類。
回收廢棄常量與回收 Java 堆中的對象非常類似。以常量池中字面量的回收為例,假如一個字符串"abc"已經進入了常量池中,但是當前系統沒有任何一個 String 對象是叫做"abc"的,也沒有其他地方引用了這個字面量,如果這時發生內存回收,而且必要的話,這個"abc"常量就會被系統清理出常量池。常量池中的其他類(接口)、方法、字段的符號引用也與此類似。
類需要同時滿足下面 3 個條件才能算是“無用的類”:
a. 該類所有的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
b. 加載該類的 ClassLoader 已經被回收。
c. 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
虛擬機可以對滿足上述 3 個條件的無用類進行回收,這里說的僅僅是“可以”,而并不是和對象一樣,不使用了就必然會回收。
在大量使用反射、動態代理、CGLib 等 ByteCode 框架、動態生成 JSP 以及 OSGi 這類頻繁自定義 ClassLoader 的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。
三、垃圾回收算法
? 四種:標記-清除算法、復制算法、標記整理算法、分代收集算法
1.標記-清除算法
? 最基礎的收集算法是標記-清除算法,分為“標記”和“清除”二個階段:首先標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象。
它的主要不足有兩個:
- 效率問題,標記和清除兩個過程的效率都不高;
- 空間問題,標記清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
2.復制算法
? 為了解決效率問題,一種稱為“復制”(Copying)的收集算法出現了,它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。
? 這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。只是這種算法的代價是將內存縮小為了原來的一半。
現在的商業虛擬機都采用這種算法來回收新生代,IBM 研究指出新生代中的對象 98% 是“朝生夕死”的,所以并不需要按照 1:1 的比例來劃分內存空間,而是將內存分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor 。
? 當回收時,將 Eden 和 Survivor 中還存活著的對象一次性地復制到另外一塊 Survivor 空間上,最后清理掉 Eden 和剛才用過的 Survivor 空間。HotSpot 虛擬機默認 Eden:Survivor = 8:1,也就是每次新生代中可用內存空間為整個新生代容量的 90%(其中一塊Survivor不可用),只有 10% 的內存會被“浪費”。
? 當然,98%的對象可回收只是一般場景下的數據,我們沒有辦法保證每次回收都只有不多于 10% 的對象存活,當 Survivor 空間不夠用時,需要依賴其他內存(這里指老年代)進行分配擔保(Handle Promotion)。內存的分配擔保也一樣,如果另外一塊 Survivor 空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接通過分配擔保機制進入老年代。
3.標記-整理算法
? 復制算法在對象存活率較高時就要進行較多的復制操作,效率將會變低。更關鍵的是,如果不想浪費 50% 的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都 100% 存活的極端情況,所以在老年代一般不能直接選用這種算法。
根據老年代的特點,有人提出了另外一種“標記-整理”(Mark-Compact)算法,標記過程仍然與“標記-清除”算法一樣,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存。
4.分代收集算法
? 當前商業虛擬機的垃圾收器都采用“分代收集”(Generational Collection)算法,根據對象存活周期的不同將內存劃分為幾塊并采用不用的垃圾收集算法。
? 一般是把 Java 堆分為**新生代和老年代,**這樣就可以根據各個年代的特點采用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”算法來進行回收。
四、Minor GC(新生代)和Full GC(老年代)觸發條件
1.Minor GC(新生代)觸發條件
? 當Eden區滿時,觸發MinorGC。
2.FullGC(老年代)觸發條件
- System.gc()方法的調用
- 老年代空間不足
- 方法區空間不足
- 通過Minor GC后進入老年代的平均大小大于老年代的可用內存
- 由Eden區、From Space區向To Space區復制時,對象大小大于To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小于該對象大小
3.Minor GC 和 Full GC 有什么不一樣
- 新生代 GC(Minor GC):指發生在新生代的垃圾收集動作,因為 Java 對象大多都具備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快。
- 老年代 GC(Major GC/Full GC):指發生在老年代的 GC,出現了 Major GC,經常會伴隨至少一次的 Minor GC(但非絕對的,在 Parallel Scavenge 收集器的收集策略里就有直接進行 Major GC 的策略選擇過程)。Major GC 的速度一般會比 Minor GC 慢 10 倍以上。
五、各垃圾收回器的特點及區別
? 如果說收集算法是內存回收的方法論,那么垃圾收集器就是內存回收的具體實現。具體有Serial收集器(串行收集器)、ParNew收集器、Parallel Scavenge收集器、Serial Old 收集器、Parallel Old收集器、CMS收集器、G1收集器。
? 下圖中7 種作用于不同分代的收集器,如果兩個收集器之間存在連線,就說明它們可以搭配使用。重點分析 CMS 和 G1 這兩款相對復雜的收集器,了解它們的部分運作細節。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-aldtTU5l-1609403845515)(D:\hsq\img\ljhs.jpg)]
1.Serial收集器(串行收集器)
? Serial 收集器,一個單線程的收集器,但它的“單線程”的意義并不僅僅說明它只會使用一個 CPU 或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。Serial 收集器對于運行在 Client 模式下的虛擬機來說是一個很好的選擇。
2.ParNew收集器
? ParNew 收集器其實就是 Serial 收集器的多線程版本,除了使用多條線程進行垃圾收集之外,其余行為包括 Serial 收集器可用的所有控制參數(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、對象分配規則、回收策略等都與 Serial 收集器完全一樣,在實現上,這兩種收集器也共用了相當多的代碼
? 注意,從 ParNew 收集器開始,后面還會接觸到幾款并發和并行的收集器。這里有必要先解釋兩個名詞:并發和并行。這兩個名詞都是并發編程中的概念,在談論垃圾收集器的上下文語境中,它們可以解釋如下。
- 并行(Parallel):指多條垃圾收集線程并行工作,但此時用戶線程仍然處于等待狀態。
- 并發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不一定是并行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行于另一個 CPU 上。
3.Parallel Scavenge收集器
? Parallel Scavenge 收集器的特點是它的關注點與其他收集器不同,CMS 等收集器的關注點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而 Parallel Scavenge 收集器的目標則是達到一個可控制的吞吐量(Throughput)。
? 所謂吞吐量就是 CPU 用于運行用戶代碼的時間與 CPU 總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了 100 分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99% 。停頓時間越短就越適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗,而高吞吐量則可以高效率地利用 CPU 時間,盡快完成程序的運算任務,主要適合在后臺運算而不需要太多交互的任務。
? Parallel Scavenge收集器提供了兩個參數用于精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數以及直接設置吞吐量大小的-XX:GCTimeRatio參數。
- MaxGCPauseMillis參數允許的值是一個大于 0 的毫秒數,收集器將盡可能地保證內存回收花費的時間不超過設定值。不要認為如果把這個參數的值設置得稍小一點就能使得系統的垃圾收集速度變得更快,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的:系統把新生代調小一些,收集 300MB 新生代肯定比收集 500MB 快吧,這也直接導致垃圾收集發生得更頻繁一些,原來10秒收集一次、每次停頓100毫秒,現在變成5秒收集一次、每次停頓70毫秒。停頓時間的確在下降,但吞吐量也降下來了。
- GCTimeRatio 參數的值應當是一個 0 到 100 的整數,也就是垃圾收集時間占總時間的比率,相當于是吞吐量的倒數。如果把此參數設置為 19,那允許的最大 GC 時間就占總時間的 5%(即 1/(1+19)),默認值為 99 ,就是允許最大 1%(即 1/(1+99))的垃圾收集時間。
4.Serial Old 收集器
? Serial Old 是 Serial 收集器的老年代版本,它同樣是一個單線程收集器,使用“標記-整理”算法。這個收集器的主要意義也是在于給 Client 模式下的虛擬機使用。如果在 Server 模式下,那么它主要還有兩大用途:一種用途是在 JDK 1.5 以及之前的版本中與 Parallel Scavenge 收集器搭配使用,另一種用途就是作為 CMS 收集器的后備預案,在并發收集發生 Concurrent Mode Failure 時使用。
5.Parallel Old收集器
? Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多線程和“標記-整理”算法。這個收集器是在 JDK 1.6 中才開始提供的,在此之前,新生代的 Parallel Scavenge 收集器一直處于比較尷尬的狀態。原因是:如果新生代選擇了 Parallel Scavenge 收集器,老年代除了 Serial Old(PS MarkSweep)收集器外別無選擇(Parallel Scavenge 收集器無法與 CMS 收集器配合工作)。
? 由于老年代 Serial Old 收集器在服務端應用性能上的“拖累”,使用了 Parallel Scavenge 收集器也未必能在整體應用上獲得吞吐量最大化的效果,由于單線程的老年代收集中無法充分利用服務器多 CPU 的處理能力,在老年代很大而且硬件比較高級的環境中,這種組合的吞吐量甚至還不一定有 ParNew 加 CMS 的組合“給力”。
直到 Parallel Old 收集器出現后,“吞吐量優先”收集器終于有了比較名副其實的應用組合,在注重吞吐量以及 CPU 資源敏感的場合,都可以優先考慮 Parallel Scavenge 加 Parallel Old 收集器。
6.CMS收集器
? CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的 Java 應用集中在互聯網站或者 B/S 系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。CMS 收集器就非常符合這類應用的需求。從名字(包含"Mark Sweep")上就可以看出,CMS 收集器是基于“標記—清除”算法實現的,它的運作過程相對于前面幾種收集器來說更復雜一些,整個過程分為4個步驟,包括:
- 初始標記(CMS initial mark)
- 并發標記(CMS concurrent mark)
- 重新標記(CMS remark)
- 并發清除(CMS concurrent sweep)
其中,初始標記、重新標記這兩個步驟仍然需要"Stop The World"。初始標記僅僅只是標記一下 GC Roots 能直接關聯到的對象,速度很快,并發標記階段就是進行 GC RootsTracing 的過程,而重新標記階段則是為了修正并發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比并發標記的時間短。
CMS 是一款優秀的收集器,它的主要優點在名字上已經體現出來了:并發收集、低停頓,但是 CMS 還遠達不到完美的程度,
它有以下 3 個明顯的缺點:
- 第一、導致吞吐量降低。
CMS 收集器對 CPU 資源非常敏感。其實,面向并發設計的程序都對 CPU 資源比較敏感。在并發階段,它雖然不會導致用戶線程停頓,但是會因為占用了一部分線程(或者說CPU資源)而導致應用程序變慢,總吞吐量會降低。CMS 默認啟動的回收線程數是(CPU數量+3)/4,也就是當 CPU 在4個以上時,并發回收時垃圾收集線程不少于 25% 的 CPU 資源,并且隨著 CPU 數量的增加而下降。但是當 CPU 不足 4 個(譬如2個)時,CMS 對用戶程序的影響就可能變得很大,如果本來 CPU 負載就比較大,還分出一半的運算能力去執行收集器線程,就可能導致用戶程序的執行速度忽然降低了 50%,其實也讓人無法接受。
- 第二、CMS 收集器無法處理浮動垃圾(Floating Garbage),可能出現"Concurrent Mode Failure"失敗而導致另一次 Full GC(新生代和老年代同時回收) 的產生。
由于 CMS 并發清理階段用戶線程還在運行著,伴隨程序運行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之后,CMS 無法在當次收集中處理掉它們,只好留待下一次 GC 時再清理掉。這一部分垃圾就稱為“浮動垃圾”。也是由于在垃圾收集階段用戶線程還需要運行,那也就還需要預留有足夠的內存空間給用戶線程使用,因此 CMS 收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供并發收集時的程序運作使用。
要是 CMS 運行期間預留的內存無法滿足程序需要,就會出現一次"Concurrent Mode Failure"失敗,這時虛擬機將啟動后備預案:臨時啟用 Serial Old 收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。所以說參數-XX:CM SInitiatingOccupancyFraction設置得太高很容易導致大量"Concurrent Mode Failure"失敗,性能反而降低。
- 第三、產生空間碎片。
CMS 是一款基于“標記—清除”算法實現的收集器,這意味著收集結束時會有大量空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,往往會出現老年代還有很大空間剩余,但是無法找到足夠大的連續空間來分配當前對象,不得不提前觸發一次 Full GC 。
7.G1收集器
? G1(Garbage-First)收集器是當今收集器技術發展的最前沿成果之一,G1 是一款面向服務端應用的垃圾收集器。HotSpot 開發團隊賦予它的使命是(在比較長期的)未來可以替換掉 JDK 1.5 中發布的 CMS 收集器。
與其他 GC 收集器相比,G1 具備如下特點:
- 并行與并發: G1 能充分利用多 CPU、多核環境下的硬件優勢,使用多個CPU(CPU或者CPU核心)來縮短 Stop-The-World 停頓的時間,部分其他收集器原本需要停頓 Java 線程執行的 GC 動作,G1 收集器仍然可以通過并發的方式讓 Java 程序繼續執行。
- 分代收集: 與其他收集器一樣,分代概念在 G1 中依然得以保留。雖然 G1 可以不需要其他收集器配合就能獨立管理整個 GC 堆,但它能夠采用不同的方式去處理新創建的對象和已經存活了一段時間、熬過多次 GC 的舊對象以獲取更好的收集效果。
- 空間整合: 與 CMS 的“標記—清理”算法不同,G1 從整體來看是基于“標記—整理”算法實現的收集器,從局部(兩個 Region 之間)上來看是基于“復制”算法實現的,但無論如何,這兩種算法都意味著 G1 運作期間不會產生內存空間碎片,收集后能提供規整的可用內存。這種特性有利于程序長時間運行,分配大對象時不會因為無法找到連續內存空間而提前觸發下一次 GC 。
- 可預測的停頓: 這是 G1 相對于 CMS 的另一大優勢,降低停頓時間是 G1 和 CMS 共同的關注點,但 G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時 Java(RTSJ)的垃圾收集器的特征了。
G1和CMS的比較
- CMS收集器是獲取最短回收停頓時間為目標的收集器,因為CMS工作時,GC工作線程與用戶線程可以并發執行,以此來達到降低停頓時間的目的(只有初始標記和重新標記會STW)。但是CMS收集器對CPU資源非常敏感。在并發階段,雖然不會導致用戶線程停頓,但是會占用CPU資源而導致引用程序變慢,總吞吐量下降。
- CMS僅作用于老年代,是基于標記清除算法,所以清理的過程中會有大量的空間碎片。
- **CMS收集器無法處理浮動垃圾,**由于CMS并發清理階段用戶線程還在運行,伴隨程序的運行自熱會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之后,CMS無法在本次收集中處理它們,只好留待下一次GC時將其清理掉。
- G1是一款面向服務端應用的垃圾收集器,適用于多核處理器、大內存容量的服務端系統。G1能充分利用CPU、多核環境下的硬件優勢,使用多個CPU(CPU或者CPU核心)來縮短STW的停頓時間,它滿足短時間停頓的同時達到一個高的吞吐量。
- 從JDK 9開始,G1成為默認的垃圾回收器。當應用有以下任何一種特性時非常適合用G1:Full GC持續時間太長或者太頻繁;對象的創建速率和存活率變動很大;應用不希望停頓時間長(長于0.5s甚至1s)。
- G1將空間劃分成很多塊(Region),然后他們各自進行回收。堆比較大的時候可以采用復制算法,碎片化問題不嚴重。整體上看屬于標記整理算法,局部(region之間)屬于復制算法。
- G1 需要記憶集 (具體來說是卡表)來記錄新生代和老年代之間的引用關系,這種數據結構在 G1 中需要占用大量的內存,可能達到整個堆內存容量的 20% 甚至更多。而且 G1 中維護記憶集的成本較高,帶來了更高的執行負載,影響效率。所以 CMS 在小內存應用上的表現要優于 G1,而大內存應用上 G1 更有優勢,大小內存的界限是6GB到8GB。
JVM常見的調優參數包括:
- -Xmx:指定java程序的最大堆內存, 使用java -Xmx5000M -version判斷當前系統能分配的最大堆內存
- -Xms:指定最小堆內存, 通常設置成跟最大堆內存一樣,減少GC
- -Xmn:設置年輕代大小。整個堆大小=年輕代大小 + 年老代大小。所以增大年輕代后,將會減小年老代大小。此值對系統性能影響較大,Sun官方推薦配置為整個堆的3/8。
- -Xss:指定線程的最大棧空間,此參數決定了java函數調用的深度,,值越大調用深度越深,,若值太小則容易出棧溢出錯誤(StackOverflowError)
- -XX:PermSize:指定方法區(永久區)的初始值,默認是物理內存的1/64, 在Java8永久區移除, 代之的是元數據區, 由-XX:MetaspaceSize指定
- -XX:MaxPermSize:指定方法區的最大值, 默認是物理內存的1/4, 在java8中由-XX:MaxMetaspaceSize指定元數據區的大小
- -XX:NewRatio=n:年老代與年輕代的比值,-XX:NewRatio=2, 表示年老代與年輕代的比值為2:1
- -XX:SurvivorRatio=n:Eden區與Survivor區的大小比值,-XX:SurvivorRatio=8表示Eden區與Survivor區的大小比值是8:1:1,因為Survivor區有兩個(from, to)
JVM性能監控有哪些?
JDK的命令行工具
- jps(虛擬機進程狀況工具):jps可以列出正在運行的虛擬機進程,并顯示虛擬機執行主類(Main Class,main()函數所在的類)名稱 以及這些進程的本地虛擬機唯一ID(Local Virtual Machine Identifier,LVMID)。
- jstat(虛擬機統計信息監視工具):jstat是用于監視虛擬機各種運行狀態信息的命令行工 具。它可以顯示本地或者遠程虛擬機進程中的類裝載、內存、垃圾收集、JIT編譯等運行數據。
- jinfo(Java配置信息工具):jinfo的作用是實時地查看和調整虛擬機各項參數。
- jmap(Java內存映像工具):命令用于生成堆轉儲快照(一般稱為heapdump或dump文 件)。如果不使用jmap命令,要想獲取Java堆轉儲快照,還有一些比較“暴力”的手段:譬如 在第2章中用過的-XX:+HeapDumpOnOutOfMemoryError參數,可以讓虛擬機在OOM異常出 現之后自動生成dump文件。jmap的作用并不僅僅是為了獲取dump文件,它還可以查詢finalize執行隊列、Java堆和永 久代的詳細信息,如空間使用率、當前用的是哪種收集器等。
- jhat(虛擬機堆轉儲快照分析工具):jhat命令與jmap搭配使用,來分析jmap生成的堆 轉儲快照。jhat內置了一個微型的HTTP/HTML服務器,生成dump文件的分析結果后,可以在 瀏覽器中查看。
- jstack(Java堆棧跟蹤工具):jstack命令用于生成虛擬機當前時刻的線程快照。線程快照就是當前虛擬機內每一條線程正在執行的方法堆棧 的集合,生成線程快照的主要目的是定位線程出現長時間停頓的原因,如線程間死鎖、死循 環、請求外部資源導致的長時間等待等都是導致線程長時間停頓的常見原因。線程出現停頓 的時候通過jstack來查看各個線程的調用堆棧,就可以知道沒有響應的線程到底在后臺做些 什么事情,或者等待著什么資源。
JDK的可視化工具
- JConsole
- VisualVM
總結
- 上一篇: Sketch学习
- 下一篇: notebook pip install