java基礎-jvm學習

 

 

參考文章:https://www.jianshu.com/p/82f27b4dd582

https://www.jianshu.com/p/c441cb07e79a

https://blog.csdn.net/xxxx3/article/details/81009524

https://blog.csdn.net/Lnho2015/article/details/77677869

jvm是每一位java開發人員都應該瞭解甚至是熟知的知識,最早接觸時覺得不實用,沒有進行過系統的學習,現在補上。

1.什麼是jvm?

JVM是Java Virtual Machine(Java虛擬機)的縮寫,是通過在實際的計算機上仿真模擬各種計算機功能來實現的。由一套字節碼指令集、一組寄存器、一個棧、一個垃圾回收堆和一個存儲方法域等組成。JVM屏蔽了與操作系統平臺相關的信息,使得Java程序只需要生成在Java虛擬機上運行的目標代碼(字節碼),就可在多種平臺上不加修改的運行,這也是Java能夠“一次編譯,到處運行的”原因。

2.JRE、JDK和JVM的關係

  • JRE(Java Runtime Environment, Java運行環境)是Java平臺,所有的程序都要在JRE下才能夠運行。包括JVM和Java核心類庫和支持文件。
  • JDK(Java Development Kit,Java開發工具包)是用來編譯、調試Java程序的開發工具包。包括Java工具(javac/java/jdb等)和Java基礎的類庫(java API)
  • JVM(Java Virtual Machine, Java虛擬機)是JRE的一部分。JVM主要工作是解釋自己的指令集(即字節碼)並映射到本地的CPU指令集和OS(操作系統)的系統調用。Java語言是跨平臺運行的,不同的操作系統會有不同的JVM映射規則,使之與操作系統無關,完成跨平臺性。

jdk是包含jre的,安裝的jdk,其實jre就不必安裝了,當然,如果你想裝jre的話也不會有什麼問題。

3.java 體系結構介紹

jvm體系總體分四大塊:

  • 類的加載機制
  • jvm內存結構
  • GC算法 垃圾回收
  • GC分析 命令調優

類加載機制之前的博客中有介紹過,主要流程就是將.class文件內容加載至內存中,存放到運行時方法區(static變量也在這裏),然後在堆上生成一個class對象,類的加載的最終產品是位於堆區中的Class對象,Class對象封裝了類在方法區內的數據結構,並且向Java程序員提供了訪問方法區內的數據結構的接口。

這裏再簡單複習一下

加載類的過程。

(1)裝載(loading)

 

負責找到二進制字節碼並加載至JVM中,JVM通過類名、類所在的包名、ClassLoader完成類的加載。
因此,標識一個被加載了的類:類名 + 包名 + ClassLoader實例ID。

(2)鏈接(linking)

 

負責對二進制字節碼的格式進行校驗、初始化裝載類中的靜態變量以及解析類中調用的接口。
鏈接又包含三塊內容:驗證、準備、初始化。
    1)驗證,文件格式、元數據、字節碼、符號引用驗證;
    2)準備,爲類的靜態變量分配內存,並將其初始化爲默認值;
    3)解析,把類中的符號引用轉換爲直接引用

(3)初始化(initializing)

 

負責執行類中的靜態初始化代碼、構造器代碼以及靜態屬性的初始化,以下六種情況初始化過程會被觸發。
    1. 創建類的實例,也就是new的方式
    2.訪問某個類或接口的靜態變量,或者對該靜態變量賦值
    3.調用類的靜態方法
    4.反射(如Class.forName(“com.shengsiyuan.Test”))
    5.初始化某個類的子類,則其父類也會被初始化
    6.Java虛擬機啓動時被標明爲啓動類的類(Java Test),直接使用java.exe命令來運行某個主類
  • 類加載器
    站在Java開發人員的角度來看,類加載器可以大致劃分爲以下三類:

(1).啓動類加載器:Bootstrap ClassLoader

 

負責加載存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,
或被-Xbootclasspath參數指定的路徑中的
並且能被虛擬機識別的類庫(如rt.jar,所有的java.開頭的類均被Bootstrap ClassLoader加載)。
啓動類加載器是無法被Java程序直接引用的。

(2).擴展類加載器:Extension ClassLoader

 

該加載器由sun.misc.Launcher$ExtClassLoader實現,
它負責加載JDK\jre\lib\ext目錄中,或者由java.ext.dirs系統變量指定的路徑中的所有類庫(如javax.開頭的類),
開發者可以直接使用擴展類加載器。

(3).應用程序類加載器:Application ClassLoader

 

該類加載器由sun.misc.Launcher$AppClassLoader來實現,
它負責加載用戶類路徑(ClassPath)所指定的類,開發者可以直接使用該類加載器,
如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

應用程序都是由這三種類加載器互相配合進行加載的,如果有必要,我們還可以加入自定義的類加載器。因爲JVM自帶的ClassLoader只是懂得從本地文件系統加載標準的java class文件,因此如果編寫了自己的ClassLoader,便可以做到如下幾點:
1、在執行非置信代碼之前,自動驗證數字簽名。
2、動態地創建符合用戶特定需要的定製化構建類。
3、從特定的場所取得java class,例如數據庫中和網絡中。

  • 雙親委託模式當類加載器收到了類加載的請求
    當JVM加載一個類的時候,下層的加載器會將任務給上一層類加載器,上一層加載檢查它的命名空間中是否已經加載這個類,如果已經加載,直接使用這個類。如果沒有加載,繼續往上委託直到頂部。檢查之後,按照相反的順序進行加載。如果Bootstrap加載器不到這個類,則往下委託,直到找到這個類。一個類可以被不同的類加載器加載。
    雙親委派機制:
1、當AppClassLoader加載一個class時,它首先不會自己去嘗試加載這個類,
而是把類加載請求委派給父類加載器`ExtClassLoader`去完成。
2、當ExtClassLoader加載一個class時,它首先也不會自己去嘗試加載這個類,
而是把類加載請求委派給`BootStrapClassLoader`去完成。
3、如果BootStrapClassLoader加載失敗(例如在$JAVA_HOME/jre/lib裏未查找到該class),
會使用`ExtClassLoader`來嘗試加載;
4、若ExtClassLoader也加載失敗,則會使用AppClassLoader來加載,
如果 `AppClassLoader`也加載失敗,則會報出異常ClassNotFoundException。

啓動類加載器負責加載存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被-Xbootclasspath參數指定的路徑中的,並且能被虛擬機識別的類庫(如rt.jar,所有的java.*開頭的類均被Bootstrap ClassLoader加載)。啓動類加載器是無法被Java程序直接引用的。擴展類加載器:Extension ClassLoader,該加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載DK\jre\lib\ext目錄中,或者由java.ext.dirs系統變量指定的路徑中的所有類庫(如javax.*開頭的類),開發者可以直接使用擴展類加載器。應用程序類加載器:Application ClassLoader,該類加載器由sun.misc.Launcher$AppClassLoader來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者可以直接使用該類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

雙親委派模型意義:

 

系統類防止內存中出現多份同樣的字節碼
保證Java程序安全穩定運行
可見性限制:下層的加載器能夠看到上層加載器中的類,反之則不行,委派只能從下到上。
不允許卸載類:類加載器可以加載一個類,但不能夠卸載一個類。但是類加載器可以被創建或者刪除。

這麼總結可能不太好理解,首先,如果你定義了類名、路徑和java中的List接口都一樣的接口會怎樣?運行main方法就會直接報錯,因爲啓動類加載器會發現兩個同樣的class,而我們定義的那個,是非法的,這麼一說,保證字節碼不重複就好理解的多了吧,而且由於啓動類加載器是最高的父類加載器,在jvm啓動時其類庫就會被加載,也就是說啓動後,我們就可以直接使用jdk提供的大量工具類庫,也保證了jdk類庫的優先加載。

自定義類加載器的總共分三步:1、繼承ClassLoader對象, 2、覆蓋findClass方法 3、發揮defineClass方法生成的class

關於類加載和自定義類加載器,上篇博客有展開說,見:https://blog.csdn.net/future_xiaowu/article/details/105268886

根據《Java 虛擬機規範(Java SE 7 版)》規定,Java 虛擬機所管理的內存如下圖所示。

方法區和堆是所有線程共享的內存區域;
而java棧、本地方法棧和程序計數器是運行是線程私有的內存區域。

詳細介紹每個區域的作用

堆:對於大多數應用來說。堆(java heap)是java虛擬機管理的內存中最大的一塊,java堆被所有線程共享,在虛擬機啓動時堆就完成初始化,它的意義就是爲了存放對象實體,注意,不是引用,對象實體內容就存在堆中。java最強大的功能之一就是垃圾回收機制,堆是存放對象的地方,那麼垃圾回收主要針對的自然也就是堆,所以堆又叫做"GC堆",如果從內存回收的角度看,現在回收期基本都採用分代收集算法,;再細緻一點的有Eden空間、From Survivor空間、To Survivor空間等。

根據Java虛擬機規範的規定,Java堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可,就像我們的磁盤空間一樣。在實現時,既可以實現成固定大小的,也可以是可擴展的,不過當前主流的虛擬機都是按照可擴展來實現的(通過-Xmx和-Xms控制)。如果在堆中沒有內存完成實例分配,並且堆也無法再擴展時,將會拋出OutOfMemoryError內存溢出異常。

方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼}等數據。雖然Java虛擬機規範把方法區描述爲堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是與Java堆區分開來。

java7之前,方法區位於永久代(PermGen),永久代和堆相互隔離,永久代的大小在啓動JVM時可以設置一個固定值,不可變;

java7中,存儲在永久代的部分數據就已經轉移到Java Heap或者Native memory。但永久代仍存在於JDK 1.7中,並沒有完全移除,譬如符號引用(Symbols)轉移到了native memory;字符串常量池(interned strings)轉移到了Java heap;類的靜態變量(class statics)轉移到了Java heap。

java8中,取消永久代,方法存放於元空間(Metaspace),元空間仍然與堆不相連,但與堆共享物理內存,邏輯上可認爲在堆中

根據Java虛擬機規範的規定,當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。

  • 程序計數器

程序計數器(Program Counter Register)是一塊較小的內存空間,它的作用可以看做是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型裏(僅是概念模型,各種虛擬機可能會通過一些更高效的方式去實現),字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。

由於Java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個內核)只會執行一條線程中的指令。因此,爲了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間的計數器互不影響,獨立存儲,我們稱這類內存區域爲“線程私有”的內存。

如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是Natvie方法(一個Native Method就是一個java調用非java代碼的接口。一個Native Method是這樣一個java的方法:該方法的實現由非java語言實現,比如C。),這個計數器值則爲空(Undefined)。

此內存區域是唯一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError情況的區域。

  • JVM棧

與程序計數器一樣,Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀(Stack Frame)用於存儲局部變量表、操作棧、動態鏈接、方法出口等信息。每一個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。

局部變量表存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它不等同於對象本身,根據不同的虛擬機實現,它可能是一個指向對象起始地址的引用指針,也可能指向一個代表對象的句柄或者其他與此對象相關的位置)和returnAddress類型(指向了一條字節碼指令的地址)。

其中64位長度的long和double類型的數據會佔用2個局部變量空間(Slot),其餘的數據類型只佔用1個。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。

在Java虛擬機規範中,對這個區域規定了兩種異常狀況:如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;如果虛擬機棧可以動態擴展(當前大部分的Java虛擬機都可動態擴展,只不過Java虛擬機規範中也允許固定長度的虛擬機棧),當擴展時無法申請到足夠的內存時會拋出OutOfMemoryError異常。

  • 本地方法棧

本地方法棧(Native Method Stacks)與虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的Native方法服務。虛擬機規範中對本地方法棧中的方法使用的語言、使用方式與數據結構並沒有強制規定,因此具體的虛擬機可以自由實現它。甚至有的虛擬機(譬如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二爲一。與虛擬機棧一樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。

一張圖表示如下

比較常見的問題圍繞棧和堆展開,以及jvm內存溢出如何處理的問題。

根據上面的介紹,處理內存溢出要按照具體的場景處理,棧不夠會溢出,堆不夠會溢出,方法區不夠也會溢出,而這些區域對應的都是內存溢出的問題,處理方式不同 ,可參考https://blog.csdn.net/xxxx3/article/details/81009524https://blog.csdn.net/Lnho2015/article/details/77677869

簡單來說,需要先定位是哪一種溢出,再對應處理,如果是方法區溢出則一般是方法區空間不夠用,而我們創建的class又太多引起的,修改方法區(java7中的永久區)參數 -XX:PermSize=5M -XX:MaxPermSize=7M,可以把這兩個參數改大些

第二種是堆引起的溢出,這種事比較常見的,創建了很多對象,而在使用之後這些對象沒有得到很好的回收導致堆滿溢出,場景也會多一些。這裏簡單羅列一下

單個對象內容過大,當對象屬性多,存儲數據大的時候可能會引起這樣的問題,對象運行時大小的獲取以後介紹,這種情況有限考慮對象瘦身,縮減對象內部存儲數據的量和屬性字段數,其次修改jvm啓動參數,增大堆內存的配置-Xms256m -Xmx1024m

程序內存在死循環不斷創建對象也可能引起這種情況,這樣的情況下代碼需要先優化代碼,再修改對應的配置參數,這裏需要注意,可以通過調用"System.gc()"來觸發垃圾回收,但並不保證會確實進行垃圾回收,JVM的垃圾回收只收集哪些由new關鍵字創建的對象。所以,如果不是用new創建的對象,你可以使用finalize函數來執行清理。,並不是強制執行回收,所以指望這種方法來解決內存溢出的問題是不可靠的。

第三種,棧內存溢出,這種一般與線程掛鉤,創建線程過多、線程超時均可能引起上述問題,可以修改棧配置,也需要進行代碼優化

 

上面簡單說了一下GC,下面,具體先介紹java的垃圾回收機制,以及該如何具體定位內存溢出的問題

JVM三大核心區域

JVMHeap區域(年輕代、老年代)和方法區(永久代)結構圖:

第一張圖片內容爲各個部分主要存儲內容和特性,圖二簡略說明了方法區和堆中的新生代和老年代

從Java GC的角度解讀代碼:new的對象會首先會進入年輕代的Eden中(如果對象太大可能直接進入年老代)。在GC之前對象是存在Eden和from中的,進行GC的時候Eden中的對象被拷貝到To這樣一個survive空間(survive倖存)空間:包括from和to,他們的空間大小是一樣的,又叫s1和s2)中(有一個拷貝算法),From中的對象(算法會考慮經過GC倖存的次數)到一定次數(閾值(如果說每次GC之後這個對象依舊在Survive中存在,GC一次他的Age就會加1,默認15就會放到OldGeneration。但是實際情況比較複雜,有可能沒有到閾值就從Survive區域直接到Old Generation區域。在進行GC的時候會對Survive中的對象進行判斷,Survive空間中有一些對象Age是一樣的,也就是經過的GC次數一樣,年齡相同的這樣一批對象的總和大於等於Survive空間一半的話,這組對象就會進入old Generation中,(是一種動態的調整))),會被複制到OldGeneration,如果沒到次數From中的對象會被複制到To中,複製完成後To中保存的是有效的對象,Eden和From中剩下的都是無效的對象,這個時候就把Eden和From中所有的對象清空。在複製的時候Eden中的對象進入To中,To可能已經滿了,這個時候Eden中的對象就會被直接複製到Old Generation中,From中的對象也會直接進入Old Generation中。就是存在這樣一種情況,To比較小,第一次複製的時候空間就滿了,直接進入old Generation中。複製完成後,To和From的名字會對調一下,因爲Eden和From都是空的,對調後Eden和To都是空的,下次分配就會分配到Eden。一直循環這個流程。好處:使用對象最多和效率最高的就是在Young Generation中,通過From to就避免過於頻繁的產生FullGC(Old Generation滿了一般都會產生FullGC)

 

虛擬機在進行MinorGC(新生代的GC)的時候,會判斷要進入OldGeneration區域對象的大小,是否大於Old Generation剩餘空間大小,如果大於就會發生Full GC。

剛分配對象在Eden中,如果空間不足嘗試進行GC,回收空間,如果進行了MinorGC空間依舊不夠就放入Old Generation,如果OldGeneration空間還不夠就OOM了。

比較大的對象,數組等,大於某值(可配置)就直接分配到老年代,(避免頻繁內存拷貝)

如果OldGeneration滿了就會產生FullGC

滿原因:
1,from survive中對象的生命週期到一定閾值

2,分配的對象直接是大對象

3、由於To 空間不夠,進行GC直接把對象拷貝到年老代(年老代GC時候採用不同的算法)

如果Young Generation大小分配不合理或空間比較小,這個時候導致對象很容易進入Old Generation中,而Old Generation中回收具體對象的時候速度是遠遠低於Young Generation回收速度。

因此實際分配要考慮年老代和新生代的比例,考慮Eden和survives的比例

Permanent Generation中發生GC的時候也對性能影響非常大,也是Full GC

-XX:SurvivorRatio新生代裏面Eden和一個Servive的比例,如果SurvivorRatio是5的話,也就是Eden區域是SurviveTo區域的5倍。Survive由From和To構成。結果就是整個Eden佔用了新生代5/7,From和To分別佔用了1/7,如果分配不合理,Eden太大,這樣產生對象很順利,但是進行GC有一部分對象倖存下來,拷貝到To,空間小,就沒有足夠的空間,對象會被放在old Generation中。如果Survive空間大,會有足夠的空間容納GC後存活的對象,但是Eden區域小,會被很快消耗完,這就增加了GC的次數。

二、 JVM的GC日誌Full GC日誌每個字段徹底詳解

[Full GC (Ergonomics) [PSYoungGen: 984K->425K(2048K)] [ParOldGen:7129K->7129K(7168K)] 8114K->7555K(9216K), [Metaspace:2613K->2613K(1056768K)], 0.1022588 secs] [Times: user=0.56 sys=0.02,real=0.10 secs]

[Full GC (Allocation Failure) [PSYoungGen: 425K->425K(2048K)][ParOldGen: 7129K->7129K(7168K)] 7555K->7555K(9216K), [Metaspace:2613K->2613K(1056768K)], 0.1003696 secs] [Times: user=0.64 sys=0.03,real=0.10 secs]

[Full GC(表明是Full GC) (Ergonomics) [PSYoungGen:FullGC會導致新生代Minor GC產生]984K->425K(2048K)][ParOldGen:(老年代GC)7129K(GC前多大)->7129K(GC後,並沒有降低內存佔用,因爲寫的程序不斷循環一直有引用)(7168K) (老年代總容量)] 8114K(GC前佔用整個Heap空間大小)->7555K (GC後佔用整個Heap空間大小) (9216K) (整個Heap大小,JVM堆的大小), [Metaspace: (java6 7是permanentspace,java8改成Metaspace,類相關的一些信息) 2613K->2613K(1056768K) (GC前後基本沒變,空間很大)], 0.1022588 secs(GC的耗時,秒爲單位)] [Times: user=0.56 sys=0.02, real=0.10 secs](用戶空間耗時,內核空間耗時,真正的耗時時間)

三、 Java8中的JVM的MetaSpace

Metaspace的使用C語言實現的,使用的是OS的空間,Native Memory Space可動態的伸縮,可以根據類加載的信息的情況,在進行GC的時候進行調整自身的大小,來延緩下一次GC的到來。

可以設置Metaspace的大小,如果超過最大大小就會OOM,不設置如果把整個操作系統的內存耗盡了出現OOM,一般會設置一個足夠大的初始值,安全其間會設置最大值。

永久代發生GC有兩種情況,類的所有的實例被GC掉,且class load不存。

對於元數據空間 簡化了GC, class load不存在了就需要進行GC。

三種基本的GC算法基石

一、 標記/清除算法

內存中的對象構成一棵樹,當有效的內存被耗盡的時候,程序就會停止,做兩件事,第一:標記,標記從樹根可達的對象(途中水紅色),第二:清除(清楚不可達的對象)。標記清除的時候有停止程序運行,如果不停止,此時如果存在新產生的對象,這個對象是樹根可達的,但是沒有被標記(標記已經完成了),會清除掉。

缺點:遞歸效率低性能低;釋放空間不連續容易導致內存碎片;會停止整個程序運行;

 

 

二、 複製算法

把內存分成兩塊區域:空閒區域和活動區域,第一還是標記(標記誰是可達的對象),標記之後把可達的對象複製到空閒區,將空閒區變成活動區,同時把以前活動區對象1,4清除掉,變成空閒區。

速度快但耗費空間,假定活動區域全部是活動對象,這個時候進行交換的時候就相當於多佔用了一倍空間,但是沒啥用。

三、 標記整理算法

平衡點

標記誰是活躍對象,整理,會把內存對象整理成一課樹一個連續的空間,

綜合了上述算法優略

1, 分代GC在新生代的算法:採用了GC的複製算法,速度快,因爲新生代一般是新對象,都是瞬態的用了可能很快被釋放的對象。

2, 分代GC在年老代的算法 標記/整理算法,GC後會執行壓縮,整理到一個連續的空間,這樣就維護着下一次分配對象的指針,下一次對象分配就可以採用碰撞指針技術,將新對象分配在第一個空閒的區域。

JVM垃圾回收器串行、並行、併發垃圾回收器概述

1, JVM中不同的垃圾回收器

2, 串行,並行,併發垃圾回收器(和JVM歷史有關係,剛開始串行)

Java中Stop-The-World機制簡稱STW,是在執行垃圾收集算法時,Java應用程序的其他所有線程都被掛起(除了垃圾收集幫助器之外)。Java中一種全局暫停現象,全局停頓,所有Java代碼停止,native代碼可以執行,但不能與JVM交互;這些現象多半是由於gc引起。

1、Serial 收集器

      Serial 收集器是新生代的單線程收集器,它“單線程”的意義體現在:       1.它只會使用一個CPU或一條收集線程去完成垃圾收集工作;       2.它在進行垃圾收集時,必須暫停其他所有的工作線程(既Stop The World),直到它收集結束。       Stop The World是由虛擬機在後臺自動發起和自動完成的,在用戶不可見的情況下把正常的工作線程全部停掉,這對很多應用來說都是難以接收的,應該儘量避免Stop The World。

2、Serial Old 收集器

      Serial Old 收集器是老年代的單線程收集器,使用“標記-整理”算法,除了與新生代的Serial收集器配合之外,Serial Old 收集器的另一種用途就是作爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。

3、Serial + Serial Old

      Serial 和 Serial Old雖然是單線程收集器,但它有着優於其他收集器的地方,簡單而高效(與其他收集器的單線程相比),對於限定單個CPU的環境來說,它們由於沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。 

4、ParNew 收集器

      ParNew收集器是新生代收集器,它是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集之外,其餘行爲包括Serial收集器可用的所有控制參數(例如:-XX:SurvivorRatio-XX:PretenureSizeThreshold-XX:HandlePromotionFailure等)、收集算法、Stop The World、對象分配規則、回收策略等都與Serial收集器完全一樣,在實現上,這兩種收集器也共用了相當多的代碼。        ParNew 收集器除了多線程收集之外,其他與Serial收集器相比沒有太多創新之處,但它卻是許多運行在Server模式下的虛擬機鍾首選的新生代收集器,其中一個與性能無關但很重要的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作。        ParNew 收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至由於存在線程交互的開銷,該收集器在通過超線程技術實現的兩個CPU的環境中都不能百分百地保證可以超越Serial收集器。當然,隨着可以使用的CPU的數量的增加,它對與GC時系統資源的有效利用還是很有好處的。它默認開啓的收集線程數與CPU的數量相同,在CPU非常多(譬如32個)的情況下,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。

5、並行(Parallel)和併發(Concurrent)

      **並行(Parallel)收集器:**指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態;       **併發(Concurrent)收集器:**指用戶線程與垃圾收集線程同時執行(但不一定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行於另一個CPU上。

JVM中CMS收集器解密

低延遲進行垃圾回收,在線服務和處理速度要求高的情況下很重要

配置:XX:UseConcMarkSweepGC

concurrence(併發) Mark(標記)Sweep(清理)

低延時

把垃圾回收分成四個階段

CMS-initial-mark初始標記階段會stop the world,短暫的暫停程序根據跟對象標記的對象所連接的對象是否可達來標記出哪些可到達

CMS-concurrent-mark併發標記,根據上一次標記的結果確定哪些不可到達,線程併發或交替之行,基本不會出現程序暫停。

CMS-remark再次標記,會出現程序暫停,所有內存那一時刻靜止,確保被全部標記,有可能第二階段之前有可能被標記爲垃圾的對象有可能被引用,在此標記確認。

CMS-concurrent-sweep併發清理垃圾,把標記的垃圾清理掉了,沒有壓縮,有可能產生內存碎片,不連續的內存塊,這時候就不能更好的使用內存,可以通過一個參數配置,根據內存的情況執行壓縮。

JVM中G1收集器

可以像CMS收集器一樣,GC操作與應用的現場一起併發執行

緊湊的空閒內存區域且沒有很長的GC停頓時間

需要可預測的GC暫停耗時

不想犧牲太多吞吐量性能

啓動後不需要請求更大的Java堆

通過案例瞬間理解JVM中PSYoungGen、ParOldGen、MetaSpace

 

Heap PSYoungGen      total 2560K, used 321K[0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000)  eden space 2048K, 15% used[0x00000007bfd00000,0x00000007bfd50568,0x00000007bff00000)  from space 512K, 0% used[0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)  to   space 512K, 0% used[0x00000007bff80000,0x00000007bff80000,0x00000007c0000000) ParOldGen       total 7168K, used 7097K[0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000) object space 7168K, 99%used [0x00000007bf600000,0x00000007bfcee7b8,0x00000007bfd00000) Metaspace       used 2647K, capacity 4486K, committed4864K, reserved 1056768K class space    used 289K, capacity 386K, committed 512K,reserved 1048576K

PSYoungGen是eden + from

使用MAT對Dump文件進行分析實戰

導出Dump文件

 

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