前言
本文討論的JVM以JDK1.8為基準點,附帶會橫向比較,往前推到JDK1.6。JVM是任何一個學習JAVA的程式設計師繞不開的核心,本文就會圍繞這個核心展開對它剖析,希望能給廣大的程式設計師帶來幫助。
一. 簡介
Java Virtual Machine(Java虛擬機器)的縮寫
JVM是一個標準,一套規範,規定了.class檔案在其內部執行的相關標準和規範,及其相關的內部構成。比如:所有的JVM都是基於棧結構的執行方式。那麼不符合這種要求的,不算是JVM( 如Android中所使用的Dalvik 虛擬機器就不能稱作是JAVA 虛擬機器, 因為它是基於暫存器(最新的Android系統據說已經放棄了Dalvik VM, 而是使用ART)。
JVM相關的產品有很多,現在最常用的是Oracle公司的HotSpot虛擬機器(還有Oracle的JRockit、IBM的J9也是非常有名的JVM),HotSpot是JVM的具體實現。因此, 這裡討論的都是HotSpot虛擬機器。
JVM是實現跨平臺的關鍵,JVM在執行位元組碼時,把位元組碼解釋成具體平臺上的機器指令執行。這就是Java的能夠“一次編譯,到處執行”的原因。
JDK: Java Development Kit
JRE: Java Runtime Environment
JVM主要由以下四部分構成:
類載入器子系統(Class Loader)、
執行時資料區/記憶體空間(Runtime Data Area)、
執行引擎(Execution Engine)
本地介面/本地方法介面(Native Interface)
二. 類載入器子系統(Class Loader)
2.1. 載入順序
類載入: 是透過JVM的類載入器從JVM外部以二進位制位元組流的方式載入到JVM中。
JVM本身有至少三種類載入器:
1). Bootstrap ClassLoader(根類載入器)C++實現, 載入位於jre/lib/下的jar;
2). Extension ClassLoader(擴充套件類載入器)主要用於載入jre/lib/ext/下的jar;
3). App ClassLoader(應用類載入器)載入classpath環境變數所指定的class;
4). Custom ClassLoader(自定義的類載入器)用於實現自己的類載入器, 如Tomcat中就實現多個類載入器,用來管理不同的jar。
類載入順序:Custom → App → Extension → Bootstrap 直到這個類被載入成功
如果一個類被不同的類載入器載入, 那麼就是兩個不同的類。
為了保證類載入的安全性,在Java 1.2後引入了雙親委派模型
雙親委派:
1). 當AppClassLoader載入一個class時,它首先不會自己去嘗試載入這個類,而是把類載入請求委派給父類載入器ExtensionClassLoader去完成。
2). 當ExtensionClassLoader載入一個class時,它首先也不會自己去嘗試載入這個類,而是把類載入請求委派給BootstrapClassLoader去完成。
3). 如果BootstrapClassLoader載入失敗(例如在$JAVA_HOME/jre/lib裡未查詢到該class),會使用ExtensionClassLoader來嘗試載入;
4). 若ExtensionClassLoader也載入失敗,則會使用AppClassLoader來載入,如果AppClassLoader也載入失敗,則會報出異常ClassNotFoundException。
雙親委派模型有效解決了以下問題:
1). 每一個類都只會被載入一次,避免了重複載入
2). 每一個類都會被儘可能的載入(從引導類載入器往下,每個載入器都可能會根據優先次序嘗試載入它)
3). 有效避免了某些惡意類的載入(比如自定義了Java。lang.Object類,一般而言在雙親委派模型下會載入系統的Object類而不是自定義的Object類)
2.2. 類載入過程
1). 載入: 首先,透過一個類的全類名來獲取此類的二進位制位元組流;其次,將類中所代表的靜態儲存結構轉換為執行時資料結構;最後,生成一個代表載入的類的java.lang.Class物件,作為方法區這個類的所有資料的訪問入口。
載入完成之後,虛擬機器外部的二進位制靜態資料結構就轉換成了虛擬機器所需要的結構儲存在方法區中(至於如何轉換,則由具體虛擬機器自己定義實現),而所生成的Class物件,則存放在方法區中,用來作為程式訪問方法區中資料的外部介面。
載入包括隱式載入(new方式建立物件)和顯式載入(反射建立物件)
2). 驗證: 其目的就是保證載入進來的.class檔案不會危害到虛擬機器本身,且內容符合當前虛擬機器的規範要求。
主要驗證的內容大致有:檔案格式驗證、後設資料驗證、位元組碼驗證、符號引用驗證。
檔案格式驗證: 主要確保符合class檔案格式規範(如文字字尾為.class的檔案將驗證不透過),以及主次版本號,驗證是否當前JVM可以處理等。
後設資料驗證: 主要驗證編譯後的位元組碼描述資訊是否符合java語法規範。
位元組碼驗證: 最為複雜,主要透過控制流和資料流確定語義是否合法、符合邏輯。
符號引用驗證: 可以看做是除自身以外(常量池中各種引用符號)的資訊匹配校驗,如透過持有的引用能否找到對應的例項。
3). 準備: 包括類初始化(clinit)和物件例項化(init)。正式為類變數分配記憶體,並設定類變數的初始值。這些變數都會在方法區中進行分配。
4). 解析: 將常量池內的符號引用替換為直接引用的過程。主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制程式碼等。
5). 初始化: 載入的最後階段,程式真正執行的開始。
6). 使用
7). 解除安裝
驗證,準備,解析合稱為連結
符號引用:
符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能夠無歧義的定位到目標即可。
例如,在Class檔案中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等型別的常量出現。
符號引用與虛擬機器的記憶體佈局無關,引用的目標並不一定載入到記憶體中。在Java中,一個Java類將會編譯成一個class檔案。在編譯時,Java類並不知道所引用的類的實際地址,因此只能使用符號引用來代替。比如org.simple.People類引用了org.simple.Language類,在編譯時People類並不知道Language類的實際記憶體地址,因此只能使用符號org.simple.Language(假設是這個,當然實際中是由類似於CONSTANT_Class_info的常量來表示的)來表示Language類的地址。各種虛擬機器實現的記憶體佈局可能有所不同,但是它們能接受的符號引用都是一致的,因為符號引用的字面量形式明確定義在Java虛擬機器規範的Class檔案格式中。
三. 執行時資料區/記憶體模型/記憶體空間(Runtime Data Area)
執行時資料區由五部分組成: 方法區、堆、虛擬機器棧、本地方法棧、程式計數器
3.1. 方法區
是執行緒共享,執行緒安全的。Java虛擬機器規範中定義方法區是堆的一個邏輯部分。
方法區儲存三種資料: 類資訊(類及父類的完全限定名,類的型別,訪問修飾符, Class引用、ClassLoader引用、實現的介面的全限定名的列表)、常量、靜態變數。舉例來說,如果兩個類同時要載入一個尚未被載入的類,那麼一個類會請求它的ClassLoader去載入需要的類,另一個類只能等待而不會重複載入。
3.1.1. 特點:
1>. 執行緒共享: 方法區是堆的一個邏輯部分,因此和堆一樣,都是執行緒共享的。整個虛擬機器中只有一個方法區。
2>. 永久代/元空間: 方法區中的資訊一般需要長期存在,而且它又是堆的邏輯分割槽,因此用堆的劃分方法,我們把方法區稱為老年代。
3>. 記憶體回收效率低: 方法區中的資訊一般需要長期存在,回收一遍記憶體之後可能只有少量資訊無效。對方法區的記憶體回收的主要目標是:對常量池的回收和對型別的解除安裝。
4>. Java虛擬機器規範對方法區的要求比較寬鬆: 和堆一樣,允許固定大小,也允許可擴充套件的大小,還允許不實現垃圾回收。
3.1.2. 常量池
常量池主要分為:Class檔案常量池、執行時常量池,字串常量池。
注:包裝型別常量池不屬於JVM層面,而是java層面的封裝。
Class檔案常量池、執行時常量池 儲存在方法區中
字串常量池:
JDK1.6: 儲存在持久代中,與方法區隔離
JDK1.7: 儲存在堆中
JDK1.8及以後: 儲存在元空間,與方法區隔離
3.2. 堆(Heap)
堆是所有執行緒共享的,用於儲存物件例項、陣列值、指向方法表的指標。
堆分為新生代(Minor)、舊生代(Major)、永久代(Permanent Space)[1.7及之前]、後設資料區/元空間(Meta Space)[1.8及之後]
新生代分為Eden Space(伊甸園區)和Survivor Space(倖存者區): 由From Space和To Space組成
預設比例: Young : Old = 1:2, Eden : Survivor From : Survivor To = 8:1:1
3.2.1. 永久代和後設資料區的區別:
元空間並不在虛擬機器中,而是使用本地記憶體
3.2.2. 特點:
1>. 執行緒共享: 整個Java虛擬機器只有一個堆,所有的執行緒都訪問同一個堆。而程式計數器、Java虛擬機器棧、本地方法棧都是一個執行緒對應一個的。
2>. 在虛擬機器啟動時建立
3>. 垃圾回收的主要場所。
4>. 不同的區域存放具有不同生命週期的物件。這樣可以根據不同的區域使用不同的垃圾回收演算法,從而更具有針對性,更高效。
5>. 堆的大小既可以固定也可以擴充套件,但主流的虛擬機器堆的大小是可擴充套件的,因此當執行緒請求分配記憶體,但堆已滿,且記憶體已滿無法再擴充套件時,就丟擲OutOfMemoryError。
3.2.3. 新生代:
新生區是類的誕生、成長、消亡的區域,一個類在這裡產生,應用,最後被垃圾回收器收集,結束生命。
新生區又分為兩部分:伊甸區(Eden space)和倖存者區(Survivor space),所有的類都是在伊甸區被new出來的。
倖存區有兩個:0區(Survivor 0 space)和1區(Survivor 1 space)。
年齡引數: MaxTenuringThreshold
當物件在Survivor區躲過一次GC的話,其物件年齡便會加1
預設情況下,如果物件年齡達到15歲,就會移動到老年代中
當伊甸區的空間用完時,程式又需要建立物件,JVM的垃圾回收器將對伊甸園進行垃圾回收Minor GC(又叫Young GC),將伊甸區中的剩餘物件移動到倖存0區。若倖存0區也滿了,移動到1區。那如果1區也滿了呢?再移動到老年代。若老年代也滿了,那麼這個時候將產生Major GC(又叫Old GC), 進行老年代的記憶體清理, Major GC後老年代仍然無法儲存,則產生Full GC。若執行Full GC 之後發現依然無法進行物件的儲存,就會產生OOM異常“OutOfMemoryError”。
如果出現java.lang.OutOfMemoryError: Java heap space異常,說明Java虛擬機器的堆記憶體不夠。原因有二:
a.Java虛擬機器的堆記憶體設定不夠,可以透過引數-Xms、-Xmx來調整。
b.程式碼中建立了大量大物件,並且長時間不能被垃圾收集器收集(存在被引用)。
分配擔保(老年代為新生代作擔保):
當JVM準備為一個物件分配記憶體空間時,發現此時Eden+Survior中空閒的區域無法裝下該物件,那麼就會觸發MinorGC,對該區域的廢棄物件進行回收。但如果MinorGC過後只有少量物件被回收,仍然無法裝下新物件,那麼此時需要將Eden+Survior中的所有物件都轉移到老年代中,然後再將新物件存入Eden區。這個過程就是“分配擔保”
3.2.4. 永久代
永久儲存區是一個常駐記憶體區域,用於存放JDK自身所攜帶的 Class,Interface 的後設資料,也就是說它儲存的是執行環境必須的類資訊,被裝載進此區域的資料是不會被垃圾回收器回收掉的,關閉 JVM 才會釋放此區域所佔用的記憶體。
如果出現java.lang.OutOfMemoryError: PermGen space,說明是Java虛擬機器對永久代Perm記憶體設定不夠。原因有二:
a. 程式啟動需要載入大量的第三方jar包。例如:在一個Tomcat下部署了太多的應用;
b. 大量動態反射生成的類不斷被載入,最終導致Perm區被佔滿。
3.2.5. 物件建立過程
1>. 檢查常量池中是否有即將要建立的這個物件所屬的類的符號引用:
若常量池中沒有這個類的符號引用,說明這個類還沒有被定義!丟擲ClassNotFoundException;
若常量池中有這個類的符號引用,則進行下一步工作
2>. 檢查這個符號引用所代表的類是否已經被JVM載入:
若該類還沒有被載入,就找該類的class檔案,並載入進方法區;
若該類已經被JVM載入,則準備為物件分配記憶體
3>. 根據方法區中該類的資訊確定該類所需的記憶體大小:
一個物件所需的記憶體大小是在這個物件所屬類被定義完就能確定的!且一個類所生產的所有物件的記憶體大小是一樣的!JVM在一個類被載入進方法區的時候就知道該類生產的每一個物件所需要的記憶體大小。
4>. 從堆中劃分一塊對應大小的記憶體空間給新的物件
5>. 為物件中的成員變數賦上初始值(預設初始化)
6>. 設定物件頭中的資訊: 物件頭(雜湊值、GC分代年齡、資料長度)
7>. 呼叫物件的建構函式進行初始化
3.3. 虛擬機器棧/VM棧/Java棧(VM Stack)
棧是執行緒私有的,執行緒安全的。它的生命期跟隨執行緒的生命期,執行緒結束棧記憶體也就釋放,對於棧來說不存在垃圾回收問題,只要執行緒一結束該棧就Over,生命週期和執行緒一致。
3.3.1. 棧執行原理
棧中的資料都是以棧幀(Stack Frame)的格式存在,棧幀是一個記憶體區塊,是一個資料集,是一個有關方法和執行期資料的資料集,當一個方法A被呼叫時就產生了一個棧幀F1,並被壓入到棧中,A方法又呼叫了B方法,於是產生棧幀F2也被壓入棧,B方法又呼叫了C方法,於是產生棧幀F3也被壓入棧…… 依次執行完畢後,先彈出後進......F3棧幀,再彈出F2棧幀,再彈出F1棧幀。遵循“先進後出”/“後進先出”(FILO)原則。
3.3.2. 棧儲存(棧幀中主要儲存以下資料):
棧幀資料(Frame Data): 包括類檔案、方法等等。
本地變數表/區域性變數表(Local Variables): 輸入引數和輸出引數以及方法內的變數;
棧操作/運算元棧(Operand Stack): 記錄出棧、入棧的操作;
動態連結: 在執行期間將符號引用轉化為直接引用,就稱為動態連結
方法出口資訊: 返回值
3.3.3. 特點:
1>. 區域性變數表的建立是在方法被執行的時候,隨著棧幀的建立而建立。而且,區域性變數表的大小在編譯時期就確定下來了,在建立的時候只需分配事先規定好的大小即可。此外,在方法執行的過程中區域性變數表的大小是不會發生改變的。
2>. 虛擬機器棧會出現兩種異常:StackOverFlowError和OutOfMemoryError。
a) StackOverFlowError: 當執行緒請求棧的深度超過當前虛擬機器棧的最大深度的時候,就丟擲StackOverFlowError異常。
b) OutOfMemoryError: 當執行緒請求棧時記憶體用完了,此時丟擲OutOfMemoryError異常。
3.4. 程式計數器(Program Counter Register)/PC暫存器(PC Register)
程式計數器是執行緒私有,執行緒安全的。程式計數器是一塊較小的記憶體空間,可以把它看作當前執行緒正在執行的位元組碼的行號指示器。也就是說,程式計數器裡面記錄的是當前執行緒正在執行的那一條位元組碼指令的地址。
3.4.1. 程式計數器的作用:
1>. 位元組碼直譯器透過改變程式計數器來依次讀取指令,從而實現程式碼的流程控制,如:順序執行、選擇、迴圈、異常處理。
2>. 在多執行緒的情況下,程式計數器用於記錄當前執行緒執行的位置,從而當執行緒被切換回來的時候能夠知道該執行緒上次執行到哪兒了
3.4.2. 特點:
1>. 是一塊較小的儲存空間
2>. 執行緒私有。每條執行緒都有一個程式計數器。
3>. 是唯一一個不會出現OutOfMemoryError的記憶體區域。
4>. 生命週期隨著執行緒的建立而建立,隨著執行緒的結束而死亡。
3.5. 本地方法棧(Native Method Stack)
本地方法棧類似於VM棧,主要儲存了本地方法呼叫的狀態。在Sun JDK中,本地方法棧和VM棧是同一個。
四. 執行引擎
執行引擎是JVM執行Java位元組碼的核心,執行方式主要分為解釋執行、編譯執行、自適應最佳化執行、硬體晶片執行方式。
解釋執行: 有三種最佳化方式(1: 棧頂快取; 2: 部分棧幀共享; 3: 執行機器指令)
編譯執行: 主要利用了JIT(Just-In-Time)編譯器在執行時進行編譯,它會在第一次執行時編譯位元組碼為機器碼並快取,之後就可以重複利用
自適應最佳化執行: 自適應最佳化執行的思想是程式中10%~20%的程式碼佔據了80%~90%的執行時間,所以透過將那少部分程式碼編譯為最佳化過的機器碼就可以大大提升執行效率。
五. 垃圾回收
5.1. 垃圾回收標誌
1>. 引用計數法: 每個物件都有一個計數器,當這個物件被一個變數或另一個物件引用一次,該計數器加一;若該引用失效則計數器減一。當計數器為0時,就認為該物件是無效物件。
2>. 可達性分析法: 所有和GC Roots直接或間接關聯的物件都是有效物件,和GC Roots沒有關聯的物件就是無效物件。
Java中可作為根集合(GC Root)的物件有:
1). 虛擬機器棧中引用的物件(本地變數表)
2). 方法區中靜態屬性引用的物件
3). 方法區中常量引用的物件
4). 本地方法棧中引用的物件(Native物件)
Java中的四種引用: 1.強引用;2:軟引用;3:弱引用;4:虛引用(幽靈/幻影引用)
引用計數法雖然簡單,但存在一個嚴重的問題,它無法解決迴圈引用的問題。 因此,目前主流語言均使用可達性分析方法來判斷物件是否有效。
5.2. 垃圾回收演算法
1). 引用計數器(Java1.2之前)
2). 標記-清除演算法: 採用從根集合進行掃描,對存活的物件物件標記,標記完畢後,再掃描整個空間中未被標記的物件,進行回收。不需要進行物件的移動,並且僅對不存活的物件進行處理,在存活物件比較多的情況下極為高效,但由於標記-清除演算法直接回收不存活的物件,因此會造成記憶體碎片。
3). 複製演算法: 採用從根集合掃描,並將存活物件複製到一塊新的,沒有使用過的空間中,這種演算法當控制元件存活的物件比較少時,極為高效,但是帶來的成本是需要一塊記憶體交換空間用於進行物件的移動。
4). 標記-整理演算法: 標記-整理演算法是在標記-清除演算法的基礎上,又進行了物件的移動,因此成本更高,但解決了記憶體碎片的問題。
六. 垃圾回收器
新生代的GC: 序列GC(Serial GC)、並行GC(ParNew GC)、並行回收GC(Parallel Scavenge GC)
舊生代的GC: 序列GC(Serial MSC)、並行GC(parallel MSC)、併發GC(CMS)
Serial: 序列收集器並不是只能使用一個CPU進行收集,而是當JVM需要進行垃圾回收的時候,需要中斷所有的使用者執行緒,直到它回收結束為止,因此又號稱“Stop The World” 的垃圾回收器。
ParNew: 多執行緒版本的Serial收集器
Parallel Scavenge: 又稱為是吞吐量優先的收集器,假設程式執行了100分鐘,JVM的垃圾回收佔用1分鐘,那麼吞吐量就是99%。
G1 GC,全稱Garbage-First Garbage Collector,透過-XX:+UseG1GC引數來啟用,G1垃圾收集器沒有新生代和老年代的概念了,而是將堆劃分為一塊塊獨立的Region。當要進行垃圾收集時,首先估計每個Region中的垃圾數量,每次都從垃圾回收價值最大的Region開始回收,因此可以獲得最大的回收效率
產生背景: 作為體驗版隨著JDK 6u14版本面世,在JDK 7u4版本發行時被正式推出,相信熟悉JVM的同學們都不會對它感到陌生。在JDK 9中,G1被提議設定為預設垃圾收集器(JEP 248)。
G1收集器的設計目標是取代CMS收集器,它同CMS相比,在以下方面表現的更出色:
1). G1是一個有整理記憶體過程的垃圾收集器,不會產生很多記憶體碎片。
2). G1的Stop The World(STW)更可控,G1在停頓時間上新增了預測機制,使用者可以指定期望停頓時間。
七. JVM記憶體調優
對JVM記憶體調優的主要目的是減少GC頻率和Full GC的次數,過多的GC和Full GC是會佔用很多的系統資源(主要是CPU),影響系統的吞吐量。特別要關注Full GC,因為它會對整個堆進行整理
導致Full GC一般由於以下幾種情況:
1). 舊生代空間不足: 調優時儘量讓物件在新生代GC時被回收、讓物件在新生代多存活一段時間和不要建立過大的物件及陣列避免直接在舊生代建立物件
2). Permanet Generation空間不足: 增大Perm Gen空間,避免太多靜態物件,控制好新生代和舊生代的比例
3). System.gc()被顯示呼叫: 垃圾回收不要手動觸發,儘量依靠JVM自身的機制
調優手段主要是透過控制堆記憶體的各個部分的比例和GC策略來實現,下面來看看各部分比例不良設定會導致什麼後果
1). 新生代設定過小: 一是新生代GC次數非常頻繁,增大系統消耗;二是導致大物件直接進入舊生代,佔據了舊生代剩餘空間,誘發Full GC
2). 新生代設定過大: 一是新生代設定過大會導致舊生代過小(堆總量一定),從而誘發Full GC;二是新生代GC耗時大幅度增加,一般說來新生代佔整個堆1/3比較合適
3). Survivor設定過小: 導致物件從eden直接到達舊生代,降低了在新生代的存活時間
4). Survivor設定過大: 導致eden過小,增加了GC頻率
JVM提供兩種較為簡單的GC策略:
1). 吞吐量優先: JVM以吞吐量為指標,自行選擇相應的GC策略及控制新生代與舊生代的大小比例,來達到吞吐量指標。這個值可由-XX:GCTimeRatio=n來設定
2). 暫停時間優先: JVM以暫停時間為指標,自行選擇相應的GC策略及控制新生代與舊生代的大小比例,儘量保證每次GC造成的應用停止時間都在指定的數值範圍內完成。這個值可由-XX:MaxGCPauseRatio=n來設定
JVM常見配置:
1). 堆設定
-Xmn: 新生代記憶體大小的最大值,包括E區和兩個S區的總和
-Xms: 初始堆大小
-Xmx: 最大堆大小
-Xss: 設定每個執行緒的棧記憶體,預設1M
-XX:NewSize=n: 設定年輕代大小
-XX:NewRatio=n: 設定年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代佔整個年輕代年老代和的1/4
-XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區佔整個年輕代的1/5
-XX:PermSize=n:設定持久代大小
-XX:MaxPermSize=n:設定持久代最大值
JDK1.8
-XX:MetaspaceSize=n:初始元空間大小
-XX:MaxMetaspaceSize=n:元空間最大值,預設是沒有限制的
2). 收集器設定
-XX:+UseSerialGC:設定序列收集器
-XX:+UseParallelGC:設定並行收集器
-XX:+UseParNewGC: 設定年輕代為並行收集
-XX:+UseParalledlOldGC:設定並行年老代收集器
-XX:+UseConcMarkSweepGC:設定併發收集器
-XX:+UseCompressedOops: 壓縮堆大小,JVM 會使用 32 位的 OOP,而不是 64 位的 OOP
3). 並行收集器設定
-XX:ParallelGCThreads=n:設定並行收集器收集時使用的CPU數。並行收集執行緒數。
-XX:MaxGCPauseMillis=n:設定並行收集最大暫停時間
-XX:GCTimeRatio=n:設定垃圾回收時間佔程式執行時間的百分比。公式為1/(1+n)
4). 併發收集器設定
-XX:+CMSIncrementalMode:設定為增量模式。適用於單CPU情況。
-XX:ParallelGCThreads=n:設定併發收集器年輕代收集方式為並行收集時,使用的CPU數。並行收集執行緒數。
5). CMS相關引數
-XX:+UseConcMarkSweepGC 使用CMS記憶體收集
6). 垃圾回收統計資訊
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
八. 垃圾回收監控
1). jstat命令列工具監控JVM記憶體和垃圾回收
2). Java VisualVM及Visual GC外掛
3). JConsole