一文弄懂JVM內存結構,垃圾回收器和垃圾回收算法

聲明:本文從知乎上部分熱門文章做二次整理,希望可以幫助更多的人,如有侵權,請聯繫刪除。

jvm

概述:

jvm: java virtual machine, 用於把我們寫的那些不能直接被程序識別的java代碼,翻譯給操作系統,告訴他我們要做的是什麼操作。

 

生命週期:

java程序開始執行的時候運行,程序結束後停止

機器上運行幾個java程序,就會相應的有幾個jvm進程

jvm中的線程分爲兩種: 守護線程和普通線程。守護線程是jvm自己使用的線程,比如垃圾回收。 普通線程一般指的就是java程序的線程,只要jvm中有普通線程在執行一般情況jvm就不會停止,除非強制調用exit();方法種植程序。

 

啓動過程:

根據本地配置的環境變量找到jvm, java.exe 通過LoadJavaVm 來裝入jvm文件,LoadLibrary裝載jvm動態連接庫,然後把JVM中的到處函數JNI_CreateJavaVM和JNI_GetDefaultJavaVMInitArgs 掛接到InvocationFunction 變量的CreateJavaVM和GetDafaultJavaVMInitArgs 函數指針變量上。JVM的裝載工作完成。

運行java程序: jvm運行java程序的方式有兩種,jar包和Class

運行jar的時候:java.exe調用GetMainClassName函數,該函數先活的JNIEnv實例然後調用JarFileJNIEnv類中的getMainfest(),從期返回的Mainifest對象中getArribittes("Main-Class")的值,即jar包中文件:META-INF/MANIFEST.MF指向的Main-Class的朱雷明作爲運行的主類。之後main函數會調用java.c中LoadClass方法狀態該主類(使用JNIEvn實例的FindClass)

 

運行Class的時候,main函數直接接調用Java.c中LoadClass方法裝在該類

 

類加載器:

jvm默認提供了三個類加載器:

  1. Bootstrap classLoader: 稱之爲啓動類加載器,是最頂層的類加載器,負責加載JDK中的核心類庫,jdk/lib目錄下的jar. 如rt.jar,resource.jar, charset.jar等

  2. Extension ClassLoader: 稱之爲擴展類加載器,負責加載java的擴展類庫,默認加載$JAVA_HOME中jre/lib/*.jar, 或-Djava.ext.dirs指定目錄下的jar包

  3. APP ClassLoader:稱之爲系統類加載器,負責加載應用程序classPath目錄下的所有jar和class文件。

    除了java默認的三個類加載之外,我們還可以根據自身需要自定義ClassLoader, 自定義的類加載器必須繼承ClassLoader類,除了Bootstrap ClassLoader,不是一個普通的java類,底層使用c++語言編寫的,已經嵌入到jvm內核中,當jvm啓動後,BootstrapClassLoader也隨之啓動,負責加載完核心類庫後,並構造ExtensionClassLoader和 App CLassLoader

    類加載的機制包括加載,連接(驗證,準備,解析),初始化

方法區:

在jvm中,類型信息和類靜態變量都保存在方法區中,類型信息是由類加載器在類加載的過程中從類文件中提取出來的,需要注意一點的是,常量池也存放於方法區中。

程序中所有的線程共享一個方法區,所以訪問方法區的信息必須確保線程是安全的。如果有兩個線程同時去加載一個類,那麼只能有一個線程被允許去加載這個類,另一個必須等待。

方法區也是可以被垃圾回收,但是條件肺炎嚴苛,必須在該類沒有任何引用的情況下。

類型信息包括:類型全名,類型的父類型全名,接口還是類,類型修飾符,父接口全名列表,類型的字段信息,類型的方法信息,所有的靜態變量,指向類加載器的引用,指向Class的引用,基本類型常量池

堆:

當java創建一個類的對象或者數組時,都在堆中爲新的對象分配內存,虛擬機中只有一個堆,程序中所有的線程都共享它。堆佔用的控件是最多的,堆的存取類型爲管道類型,先進先出。在程序運行中,可以動態分配堆內存的大小。

 

棧:

java棧中只保存基本數據類型和自定義對象的引用,注意只是對象的引用,而不是對象本身,對象本身保存在堆中。像String,Integer,等包裝類是存放於堆中的。棧是先進後出類型的,棧內的數據在超出其作用域後,會被自動釋放掉,不由JVM GC管理。每一個線程都包含一個棧區,每個棧中的數據都是私有的,其他棧不能訪問。每個線程都會創建一個操作棧,每個棧又包含了若干個棧幀,每個棧幀對應着每個方法的每次調用,每個棧幀包含了三個部分:

局部變量區(方法內基本類型變量、變量對象指針),操作數棧區(存放方法執行過程中產生的中間結果),運行環境區(動態連接、正確的方法返回相關信息、異常捕捉)

 

本地方法棧:

本地方法棧的功能和jvm棧得非常類似,用於存儲本地方法的局部變量表,本地方法的操作數棧等信息。本地方法棧是在程序調用或jvm調用本地方法接口(native)時候啓用。本地方法都不是使用java語言編寫的,比如使用C語言編寫的本地方法,本地方法也不由jvm去運行,所有本地方法的運行不受jvm的管理。hotspot vm將本地方法棧和jvm棧合併了。

 

程序計數器:

在jvm概念模型裏,字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令。分支,循環,跳轉,異常處理,線程恢復等基礎功能都需要依賴這個計數器完成。jvm的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,爲了各條線程之間的切換後計數器能恢復到正確的執行位置,所以每條線程都有一個獨立的程序計數器。程序計數器只佔很小的一塊內存空間。當線程正在執行一個java方法,程序計數器記錄的是正在執行的jvm字節碼指令的地址,如果正在執行的是一個native方法,那麼程序計數器的值則爲空(undefined). 程序計數器是唯一一個在jvm規範中沒有規定任何oom的區域。

 

JVM執行引擎:

Java虛擬機相當於一臺虛擬的“物理機”,這兩種機器都有代碼執行能力,其區別主要是物理機的執行引擎是直接建立在處理器、硬件、指令集和操作系統層面上的。而JVM的執行引擎是自己實現的,因此程序員可以自行制定指令集和執行引擎的結構體系,因此能夠執行那些不被硬件直接支持的指令集格式。

在JVM規範中制定了虛擬機字節碼執行引擎的概念模型,這個模型稱之爲JVM執行引擎的統一外觀。JVM實現中,可能會有兩種的執行方式:解釋執行(通過解釋器執行)和編譯執行(通過即時編譯器產生本地代碼)。有些虛擬機只採用一種執行方式,有些則可能同時採用兩種,甚至有可能包含幾個不同級別的編譯器執行引擎。

輸入的是字節碼文件、處理過程是等效字節碼解析過程、輸出的是執行結果。在這三點上每個JVM執行引擎都是一致的。

 

本地方法接口(JNI)

JAVA NATIVE INTERFACE: 提供了若干api實現java和其他語言的通信(主要是C和C++)

適用場景: 當我們有一些舊的庫,已經使用C語言編寫好了,如果要移植到java上來,非常浪費時間,而jni可以支持java程序與C語言編寫的庫進行交互,這要就不必要進行移植了。或者是與硬件,操作系統進行交互,提高程序的性能等,都可以使用JNI,需要注意的一點是需要保證本地代碼能工作在任何java虛擬機環境。副作用:一旦使用JNI,java將失去兩個優點,一個是不在跨平臺,一個是程序不在絕對安全。

 

JAVA 常量池

jvm常量池也稱之爲運行時常量池,他是方法區的一部分,用於存放編譯期間生成的各種字面量和符號引用。運行時常量池不要求一定只有在編譯器產生的才能進入,運行期間也可以將常量放入池中,這種特性被開發人員利用的比較多的是String.intern()方法。

由“用於存放編譯期間生成的各種字面量和符號引用” 這句話可見,常量池中存儲的是對象的引用而不是對象的本身。

常量池的好處: 爲了避免頻繁的創建和銷燬對象而影響系統性能,他也實現了對象的共享。

例如字符串常量池:在編譯階段就把所有的字符串文字放到一個常量池中。節省內存空間,節省運行時間。

 

GC-垃圾回收:

stop-the-world(stw): 他會在任何一種GC算法中發生。stw意味着jvm因爲需要執行GC而停止了應用程序的執行。當stw發生時,出GC所需的線程外,所有的線程都進入等待狀態,直到GC任務完成。GC優化的很多時候,就是減少stw的發生。

需要注意的是,jvm gc只回收堆和方法區內的對象,而棧區的數據,在超出作用域後會被jvm自動釋放掉,所有其不再jvm gc的管理範圍內。

jvm -gc 如何判斷對象可以被回收了?

  • 對象沒有應用

  • 作用域發生未捕獲異常

  • 程序在作用域正常執行完畢

  • 程序執行了System.exit();

  • 程序發生意外終止(被殺線程等)

在java程序中不能顯示的分配和註銷緩存,因爲這些事情jvm都幫我們做了,那就是GC.有些時候我們可以將相關對象設置成null來試圖顯示的清楚緩存,但是並不是設置成null就會一定被標記爲可回收,有可能會發生逃逸。將對象設置成null至少沒有什麼壞處,但是使用System.gc()便不可取了,使用System.gc()的時候並不是馬上執行GC操作,而是會等待一段時間,甚至不執行,而且System.gc()如果別執行,會出發Full GC,這費城影響性能。

 

GC什麼時候執行:

eden區空間不夠存放新對象的時候,執行minor gc。 升到年老代的對象大於老年代的剩餘空間時執行full gc,或者小於的時候,被 HandlePromotionFailure 參數強制Full GC。 調優主要是減少Full GC 的觸發次數,可以通過NewRatio 控制新生代轉老年代的比例,通過MaxTurningThreshold 設置對象進入老年代的年齡閥值。

 

按代的垃圾回收機制:

新生代(Young generation):絕大多數的最新被創建的對象都會被分配到這裏,由於大部分在創建後很快變得不可達,很多對象別創建在新生代,然後消失。對象從這個區域消失的過程,我們稱之爲 Minor GC

老年代(old generation): 對象沒有變得不可達,並且從新生代週期中存活了下來,會被拷貝到這裏。其區域分配的空間要比新生代多。也正是由於其相對較大的空間,發生在老年代的GC次數要比新生代少得多。對象從老年代消失的過程稱之爲: Major GC, 或者 Full GC.

持久代(Permanent generation):也稱之爲方法區,用於保存類常量以及字符串常量,注意,這個區域不是用於存儲那些從老年代存活下來的對象,這個區域也可能發生GC, 發生在這個區域的GC事件也被算作Major GC,只不過在這個區域發生GC 的條件非誠嚴苛,必須符合以下三種條件:

  1. 所有實例被回收

  2. 加載該類的ClassLoader被回收

  3. Class 對象無法通過任何途徑訪問(包括反射)

     

    如果老年代要引用新生代的對象,會發生什麼呢?

    爲了解決這個問題,老年代中存在一個 card table ,它是一個512byte大小的塊。所有老年代的對象指向新生代對象的引用都會被記錄在這個表中。當針對新生代執行GC的時候,只需要查詢 card table 來決定是否可以被回收,而不用查詢整個老年代。這個 card table 由一個write barrier 來管理。write barrier給GC帶來了很大的性能提升,雖然由此可能帶來一些開銷,但完全是值得的。

默認的新生代和老年代所佔空間的比例爲1:2

 

新生代空間的扣成和邏輯:

分爲三個部分: 一個伊甸園空間(eden), 兩個倖存者空間)(From Survivor, To Survivor)默認比例: Eden:From:to = 8:1:1

每個空間執行順序:

  1. 絕大多數剛剛被創建的對象會存放在伊甸園EDEN空間

  2. 在eden空間執行第一次gc(minor gc)後,存活的對象被移動到其中的一個倖存者區

  3. 此後,每次Eden空間執行gc後,存活的對象都會被堆積在同一個倖存者空間。

  4. 當一個倖存者空間飽和,還存在存活的對象會被移動到另一個倖存者空間,然後會清空已經飽和的那個倖存者空間

  5. 在以上步驟中重複N次(N=MAXTenuringThreshold(年齡閥值設定,默認15))依然存活的對象,就會別移動到老年代

從上面的步驟可以發現,兩個倖存者空間,必須有一個是保持空的,如果兩個倖存者空間都有數據,或者兩個都是空的,那一定是你的系統出現了某種錯誤。

我們需要重點記住的是,對象在剛剛被創建之後,是保存在Eden區的,哪些長期存活的對象會經由倖存者空間轉到老年代空間。也有例外的情況,對於一些比較大的對象(需要分配連續比較大的空間)則直接進入到老年代,一般在倖存者空間不足的情況下發生。

 

老年代空間的構成與邏輯:

老年代空間的構成其實很簡單,他不像新生代那樣劃分爲幾個區域,他只有一個區域,裏面存儲的對象並不像新生代空間絕大部分都是朝聞道,夕死矣。這裏的對象幾乎都是從Survivor空間中熬過來的,他們絕不會輕易狗帶。因此FULL GC 發生的次數不會有minor gc那麼頻繁,並且做一次full gc的時間比minor gc要更長(約10倍)

 

GC算法:

 1. 根搜索算法(可達性分析): 從GCROOT開始,尋找對應的引用節點,找到這個節點後,繼續尋找這個節點的引用節點。當所有的引用節點尋找完畢後,剩餘的節點則被認爲是沒有被引用到的節點,及無用的節點。目前java中可以作爲GCroot的對象有: 虛擬機棧中引用的對象(本地變量表),方法區中靜態屬性引用的對象,方法區中常量引用的對象,本地方法棧中引用的對象(native)

2. 標記-清除算法:

標記-清除算法採用從根集合進行掃描,對存活的對象進行標記,標記完畢後,在掃描整個空間中未標記的對象進行直接回收。標記-清除算法不需要進行對象的移動,並且僅對不存活的對象進行處理,在存活的對象比較多的情況下極爲高效,但是由於標記-清除算法直接回收不存活的對象,並沒有對存活的對象進行整理,因此會導致內存碎片。

3. 複製算法:

複製算法將內存劃分爲兩個區間,使用此算法時,所有的動態分配的對象都只能分配在其中一個區間,而另一個區間是閒置的。複製算法採用從根集合掃描,將存活對象複製到空閒區間,當掃描完畢活動區間後,會將活動區間一次性全部回收,此時原本的空閒區間變成了活動區間,下次gc的時候會重複剛纔的操作,以此循環。複製算法在存活對象較少的時候,極爲高效,但是帶來的成本是犧牲一半的內存空間用於對象的移動,所以複製算法使用的場景,必須是對象的存活率非常低纔行。

4. 標記-整理算法:

標記-整理算法採用和標記-清除算法一樣的方式進行對象的標記,清除,但是在回收不存活對象佔用的空間後,會見給所有的存活的對象往左端空閒空間移動,並更新對應的指針。標記-整理算法是在標記-清除算法之上,又進行了對象的移動排序整理,因此成本更高,但卻解決了內存碎片的問題,

JVM 爲了優化內存得回收,是用來分代回收的方式,對於新生代的內存回收,主要採用複製算法,而對於老年代的回收,大多采用標記整理算法。

 

垃圾回收器

需要注意的是,每一個回收器都存在stw的問題,只不過各個回收器在stw時間優化程度、算法的不同,可根據自身需求選擇適合的回收器。

1.Serial(-XX: + UseSerialGC)

從名字可以看出,這是一個串行的垃圾回收器,這也是java虛擬機中最基本,歷史最悠久的收集器,在jdk1.3之前是java虛擬機新生代收集器的唯一選擇,目前也是ClientVM 下ServerVM4核4gb以下機器的默認垃圾回收器,Serial收集器並不是只能使用一個CPU進行收集,而是當jvm需要進行垃圾回收的時候,需暫停所有的用戶線程,直到回收結束。

使用算法: 複製算法。

Serial收集器雖然是最老的,但是它對於限定單個CPU的環境來說,由於沒有線程交互的開銷,專心做垃圾收集,所以它在這種情況下是相對於其他收集器中最高效的。

2. SerialOld(-XX: + UseSerialGC)

SerialOld是Serial收集器的老年代收集器版本,它同樣是一個單線程收集器,這個收集器目前主要用於Client模式下使用。如果在Server模式下,它主要還有兩大用途:一個是在JDK1.5及之前的版本中與Parallel Scavenge收集器搭配使用,另外一個就是作爲CMS收集器的後備預案,如果CMS出現Concurrent Mode Failure,則SerialOld將作爲後備收集器。

使用算法:標記 - 整理算法

3. ParNew(-XX: +UseParNewGC)

ParNew其實就是Serial收集器的多線程版本。除了Serial收集器外,只有它能與CMS收集器配合工作。

使用算法: 複製算法

ParNew 是許多運行在Server模式下的JVM的首選的新生代收集器,但是在單cpu的情況下,他的效率遠遠低於Serial收集器,所以一定要注意使用場景。

 

4. ParallelScavenge(-XX:+UseParallelGC)

ParallelScavenge又被稱爲吞吐量優先收集器,和ParNew 收集器類似,是一個新生代收集器

使用算法: 複製算法

ParallelScavenge收集器的目的是打到一個可控的吞吐量,所謂吞吐量就是cpu用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)。如果虛擬機總共運行了100分鐘,其中垃圾回收用了1分鐘,那麼吞吐量就是99%

所以這個收集器適合在後臺運算而不需要很多交互的任務。接下來看看兩個用於準備控制吞吐量的參數 1,-XX:MaxGCPauseMills(控制最大垃圾收集的時間) 設置一個大於0的毫秒數,收集器儘可能地保證內存回收不超過設定值。但是並不是設置地越小就越快。GC停頓時間縮短是以縮短吞吐量和新生代空間來換取的。 2,-XX:GCTimeRatio(設置吞吐量大小) 設置一個0-100的整數,也就是垃圾收集時間佔總時間的比率,相當於吞吐量的倒數。

 

5. ParallelOld(-XX: + UseParallelOldGC)

ParallelOld 是並行收集器,和SerialOld 一樣,是一個老年代收集器,是老年代吞吐量優先的一個收集器,這個收集器在JDK1.6之後纔開始提供的,再次之前,ParallelScavenge只能選擇SerialOld來作爲其老年代的收集器,這嚴重拖累了ParallelScavenge的整體速度,而ParallelOld出現了之後,吞吐量有限收集器才名副其實

使用算法: 標記-整理算法

在注重吞吐量與CPU數量大於1 的情況下,都可以優先考慮ParallelScavenge + ParallelOld收集器

6. CMS(-XX:UseConcMarkSweepGC)

CMS是一個老年代收集器,全稱Concurrent Low Pause Collector, 是JDK1.4以後開始引用的心GC收集器,在jdk5,jdk6中得到了進一步的改進。他是對於響應時間的重要性需求大於吞吐量要求的收集器,對於要求服務器響應速度高的情況下,使用CMS非常合適。

CMS的一大特點,就是用兩次短暫的暫定來代替串行或者並行標記整理算法時候的長暫停

使用算法:標記-清理

執行過程如下:

初始標記(STW initial mark):在這個階段,需要虛擬機停頓在正在執行的應用線程,官方叫法叫做STW,這個過程從根對象掃描直接關聯的對象,並做標記,這個過程會很快完成。

併發標記(Concurrent marking) :這個階段緊隨初始標記階段,在初始標記的基礎上繼續向下追溯標記,注意這裏是併發標記,標識用戶線程可以和GC線程一起併發執行,這個階段不會暫停用戶線程

併發預清理(Concurrent precleaning):這個階段仍然是併發的,jvm查找正在執行併發標記階段時候進入老年代的對象(可能這是會有對象從新生代晉升到老年代,或被分配到老年代)通過重新掃描,減少在一個階段重新標記的工作,因爲下一個階段會stw

重新標記(stw remark): 這個階段會再次暫停正在執行的應用線程,重新從根對象開始查找並標記併發階段遺漏的對象(在併發標記階段結束後對象狀態的更新導致)並處理對象關聯,這一次耗時迴避“初始標記”更長,並且這個階段可以並行標記。

併發清理(Concurrent sweeping): 這個階段是併發的,應用程序和GC清理線程可以一起併發執行

併發重置(Concurrent reset):這個階段仍然是併發的,重置CMS收集器的數據結構,等待下一次垃圾回收

CMS:缺點:

  1. 內存碎片;由於使用了標記-清理算法,導致內存空間中會產生內存碎片,不過CMS收集器做了一些小的優化,就是把未分配的空間彙總成一個列表,當有JVM需要分配內存空間的時候,會搜索這個列表找到符合條件的空間來存儲這個對象,但是內存碎片的問題仍然存在,如果一個對象需要三塊連續的空間來存儲,因爲內存碎片的問題,找不到這樣的空間,就會導致full gc.

  2. 需要更多的CPU資源:由於使用了併發處理,很多情況下都是GC線程和用戶線程併發執行的,這樣就需要佔用更多的CPU資源,也是犧牲了一定吞吐量的原因。

  3. 需要更大的堆空間:因爲CMS標記階段用用程序的線程還是執行的,那麼就會有堆空間繼續分配的問題,爲了保障CMS在回收堆空間之前還有空間分配給新加入的對象,必須預留一部分空間,cms默認在老年代空間使用68%的時候啓動垃圾回收,可以通過-XX:CMSinitiatingOccupancyFraction=n來設置這個閥值。

 

7. garbageFirst(G1)

G1收集器是jdk1.7提供的一個新的收集器,是當今收集器技術發展的最前沿成果之一。G1是一款面向服務端應用的垃圾收集器,Hotspot開發團隊賦予它的使命是未來可以替換掉cms.

G1具備以下特點:

  1. 並行與併發: G1能充分利用多CPU,多核心環境下的硬件優勢,使用多個CPU來縮短STW停頓的時間,部分其他收集器原本需要停頓java線程執行的G1動作,G1收集器仍然可以通過併發的方式讓java程序繼續執行。

  2. 分代收集:與其他收集器一樣,分代概念在G1中仍然得以保留,雖然G1可以不需要其他收集器配合就能單獨管理整個GC堆,但他能夠採取不同的方式去處理新創建的對象和已經存活了一段時間。熬過多個gc的舊對象已獲得更好的收集效果

  3. 空間整合:與CMS的標記-清除算法不同,G1收集器從整體上看是基於標記-整理算法實現的,從局部(兩個region)上看是基於複製算法實現的,但無論如何,兩種算法都意味着g1運行期間不會產生內存空間碎片,收集後能夠提供規整的可用內存。這種特性有利於程序的長時間運行, 分配大對象時不會因爲無法找到連續的內存空間而提前觸發下一次GC

  4. 可預測的停頓:這是G1相比cms的另一大優勢,降低停頓時間是G1和cms的共同關注點,但是G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時java(RTSJ)的垃圾收集器的特徵了。

 

整理一下新生代和老年代的收集器。

新生代收集器:

Serial (-XX:+UseSerialGC)

ParNew(-XX:+UseParNewGC)

ParallelScavenge(-XX:+UseParallelGC)

G1 收集器

老年代收集器:

SerialOld(-XX:+UseSerialOldGC)

ParallelOld(-XX:+UseParallelOldGC)

CMS(-XX:+UseConcMarkSweepGC)

G1 收集器

 

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