Java虛擬機(jvm)

前言

在本文將深入討論 Java 虛擬機相關核心知識

參考書籍:

  • 《深入理解 Java 虛擬機》周志明,機械工業出版社

學習課程:

  • 【煉數成金】深入 JVM 內核—原理、診斷與優化
  • 【龍果學院】深入理解 Java 虛擬機( JVM 性能調優+內存模型+虛擬機原理)
  • 【尚學堂】白鶴翔 JVM 虛擬機優化

核心知識

1. 運行時數據區域

 

1. 程序計數器(線程私有)

記錄正在執行的虛擬機字節碼指令的地址(如果正在執行的是本地方法則爲空)。

  • 多個線程競爭時被掛起,程序計數器記錄執行到哪裏
  • 唯一一個在 Java 虛擬機規範中沒有規定任何 OutOfMemoryError 情況的區域

2. 虛擬機棧(線程私有)

每個 Java 方法在執行的同時會創建一個棧幀用於存儲局部變量表、操作數棧、常量池引用等信息,從調用直至執行完成的過程,就對應着一個棧幀在 Java 虛擬機棧中入棧和出棧的過程。

 

  • 補充:棧幀中還存在動態鏈接、出口(返回地址)等。

可以通過 -Xss 這個虛擬機參數來指定一個程序的 Java 虛擬機棧內存大小:

java -Xss=512M HackTheJava

該區域可能拋出以下異常:

  • 當線程請求的棧深度超過最大值,會拋出 StackOverflowError 異常;

  • 棧進行動態擴展時如果無法申請到足夠內存,會拋出 OutOfMemoryError 異常。

3. 本地方法棧(線程私有)

本地方法一般是用其它語言(C、C++ 或彙編語言等)編寫的,並且被編譯爲基於本機硬件和操作系統的程序,對待這些方法需要特別處理。

本地方法棧與 Java 虛擬機棧類似,它們之間的區別只不過是本地方法棧爲本地方法服務。

 

4. 堆

所有對象實例都在這裏分配內存。

是垃圾收集的主要區域("GC 堆")。現代的垃圾收集器基本都是採用分代收集算法(因爲對象的生命週期不一樣),主要思想是針對不同的對象採取不同的垃圾回收算法。虛擬機把 Java 堆分成以下三塊:

新生代 (Young Generation)

  • 在方法中去 new 一個對象,那這方法調用完畢後,對象就會被回收,這就是一個典型的新生代對象。

老年代 (Old Generation)

  • 在新生代中經歷了 N 次垃圾回收後仍然存活的對象就會被放到老年代中。而且大對象直接進入老年代
  • 當 Survivor 空間不夠用時,需要依賴於老年代進行分配擔保,所以大對象直接進入老年代

永久代 (Permanent Generation)

  • 即方法區。

當一個對象被創建時,它首先進入新生代,之後有可能被轉移到老年代中。

新生代存放着大量的生命很短的對象,因此新生代在三個區域中垃圾回收的頻率最高。爲了更高效地進行垃圾回收,把新生代繼續劃分成以下三個空間:

  • Eden(伊甸園)
  • From Survivor(倖存者)
  • To Survivor

 

Java 堆不需要連續內存,並且可以動態增加其內存,增加失敗會拋出 OutOfMemoryError 異常。

可以通過 -Xms 和 -Xmx 兩個虛擬機參數來指定一個程序的 Java 堆內存大小,第一個參數設置初始值,第二個參數設置最大值。

java -Xms=1M -Xmx=2M HackTheJava
  • 思考:爲什麼是 8:1:1

5. 方法區

用於存放已被加載的類信息(包含:類版本、字段、方法、接口)、常量 (final)、靜態變量 (static)、即時編譯器 (JIT) 編譯後的代碼等數據。因爲都是共享的數據,所有要放在方法區。

和 Java 堆一樣不需要連續的內存,並且可以動態擴展,動態擴展失敗一樣會拋出 OutOfMemoryError 異常。

對這塊區域進行垃圾回收的主要目標是對常量池的回收和對類的卸載,但是一般比較難實現。

JDK 1.7 之前,HotSpot 虛擬機把它當成永久代來進行垃圾回收,JDK 1.8 之後,取消了永久代,用 metaspace(元數據)區替代。

6. 運行時常量池

運行時常量池是方法區的一部分。

Class 文件中的常量池(編譯器生成的各種字面量和符號引用)會在類加載後被放入這個區域。

除了在編譯期生成的常量,還允許動態生成,例如 String 類的 intern()。

在TLAB空間中存在

// 字節碼常量
String s1 = "123";
String s2 = "123";
System.out.println(s1 == s1);  //  true

7. 直接內存

在 JDK 1.4 中新加入了 NIO 類,它可以使用 Native 函數庫直接分配堆外內存,然後通過一個存儲在 Java 堆裏的 DirectByteBuffer 對象作爲這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因爲避免了在 Java 堆和 Native 堆中來回複製數據。

 

2. 判斷一個對象是否可被回收

程序計數器、虛擬機棧和本地方法棧這三個區域屬於線程私有的,只存在於線程的生命週期內,線程結束之後也會消失,因此不需要對這三個區域進行垃圾回收。垃圾回收主要是針對 Java 堆和方法區進行。

1. 引用計數算法

**描述:**給對象中添加一個引用計數器每當有一個地方引用它時,計數器就加1;當引用失效時,計數器值就減1;任何時刻計數器爲0的對象就是不可能在被使用的。

**缺陷:**很難解決對象間相互循環引用的問題

2. 可達性分析算法

通過 GC Roots 作爲起始點進行搜索,能夠到達到的對象都是存活的,不可達的對象可被回收。

 

★ GC用的引用可達性分析算法中,哪些對象可作爲GC Roots對象?【阿里面經】

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  • 方法區中靜態屬性引用的對象。
  • 方法區中常量引用的對象。
  • 本地方法棧中 JNI (即一般說的 Native 方法)引用的對象。

3. 引用類型

無論是通過引用計算算法判斷對象的引用數量,還是通過可達性分析算法判斷對象是否可達,判定對象是否可被回收都與引用有關。

在 JDK 1.2 之後,Java 對引用的概念進行了擴充,將引用分爲 強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。

1. 強引用(Strong Reference)

被強引用關聯的對象不會被回收。

使用 new 一個新對象的方式來創建強引用。

Object obj = new Object();

2. 軟引用(Soft Reference)

被軟引用關聯的對象只有在內存不夠的情況下才會被回收。

使用 SoftReference 類來創建軟引用。

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使對象只被軟引用關聯

3. 弱引用(Weak Reference)

被弱引用關聯的對象一定會被回收,也就是說它只能存活到下一次垃圾回收發生之前。

使用 WeakReference 類來實現弱引用。

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;

4. 虛引用(Phantom Reference)

又稱爲幽靈引用或者幻影引用。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用取得一個對象。

爲一個對象設置虛引用關聯的唯一目的就是能在這個對象被回收時收到一個系統通知。

使用 PhantomReference 來實現虛引用。

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;

4. 方法區的回收

Java虛擬機規範中確實說過可以不要求虛擬機在方法區實現垃圾收集,而且在方法區中進行垃圾收集的 “性價比” 一般比較低:在堆中,尤其在新生代中,常規的應用一次垃圾收集一般可以回收 70% ~ 95%的空間,而永久代的垃圾收集效率遠低於此。

永久代的垃圾收集主要回收兩部分:廢棄常量 和 無用的類

  • 回收廢棄常量與回收 Java 堆中的對象非常類似。
  • 要判定一個類是否是 “無用的類” 的條件相對苛刻許多。類需要同時滿足下面3個條件才能算 “無用的類”
    • 該類的所有實例都已經被回收。
    • 加載該類的 ClassLoader 已經被回收。
    • 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

在大量使用反射、動態代理、GGLib 等 ByteCode 框架、動態生成 Jsp 以及 OSGI 這類頻繁自定義 ClassLoader 的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。

5. finalize()

finalize() 類似 C++ 的析構函數,用來做關閉外部資源等工作。但是 try-finally 等方式可以做的更好,並且該方法運行代價高昂,不確定性大,無法保證各個對象的調用順序,因此最好不要使用。

當一個對象可被回收時,如果需要執行該對象的 finalize() 方法,那麼就有可能通過在該方法中讓對象重新被引用,從而實現自救。自救只能進行一次,如果回收的對象之前調用了 finalize() 方法自救,後面回收時不會調用 finalize() 方法。

3. 垃圾收集算法(垃圾處理方法)

1. 標記 - 清除

 

首先標記出所有需要回收的對象,在標記完成後統一回收所有標記的對象。

不足:

  • 效率問題:標記和清除的效率都不高
  • 空間問題:標記清除之後會產生大量不連續的內存碎片,導致以後需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另外一次垃圾收集。

2. 標記 - 整理

 

複製收集算法在對象存活率較高時就要進行較多的複製操作,效率會變低。更關鍵的是,如果不想浪費 50% 的空間,就需要有額外的空間進行分配擔保,所以老年代一般不能直接選用這種算法。

讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。

3. 複製回收

 

將內存劃分爲大小相等的兩塊,每次只使用其中一塊,當這一塊內存用完了就將還存活的對象複製到另一塊上面,然後再把使用過的內存空間進行一次清理。

主要不足是隻使用了內存的一半。

現在的商業虛擬機都採用這種收集算法來回收新生代,但是並不是將新生代劃分爲大小相等的兩塊,而是分爲一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 空間和其中一塊 Survivor。在回收時,將 Eden 和 Survivor 中還存活着的對象一次性複製到另一塊 Survivor 空間上,最後清理 Eden 和使用過的那一塊 Survivor。

HotSpot 虛擬機的 Eden 和 Survivor 的大小比例默認爲 8:1,保證了內存的利用率達到 90%。如果每次回收有多於 10% 的對象存活,那麼一塊 Survivor 空間就不夠用了,此時需要依賴於老年代進行分配擔保,也就是借用老年代的空間存儲放不下的對象。

★ 分代收集

現在的商業虛擬機採用分代收集算法,它根據對象存活週期將內存劃分爲幾塊,不同塊採用適當的收集算法。

一般將堆分爲新生代和老年代。

  • 新生代使用:複製算法
  • 老年代使用:標記 - 清除 或者 標記 - 整理 算法

4. 垃圾收集器

 

以上是 HotSpot 虛擬機中的 7 個垃圾收集器,連線表示垃圾收集器可以配合使用。

  • 單線程與多線程:單線程指的是垃圾收集器只使用一個線程進行收集,而多線程使用多個線程;
  • 串行與並行:串行指的是垃圾收集器與用戶程序交替執行,這意味着在執行垃圾收集的時候需要停頓用戶程序;並形指的是垃圾收集器和用戶程序同時執行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式執行。

1. Serial

Serial 翻譯爲串行,也就是說它以串行的方式執行。

它是單線程的收集器,只會使用一個線程進行垃圾收集工作。

它的優點是簡單高效,對於單個 CPU 環境來說,由於沒有線程交互的開銷,因此擁有最高的單線程收集效率。

它是 Client 模式下的默認新生代收集器,因爲在用戶的桌面應用場景下,分配給虛擬機管理的內存一般來說不會很大。Serial 收集器收集幾十兆甚至一兩百兆的新生代停頓時間可以控制在一百多毫秒以內,只要不是太頻繁,這點停頓是可以接受的。

2. ParNew

它是 Serial 收集器的多線程版本。

是 Server 模式下的虛擬機首選新生代收集器,除了性能原因外,主要是因爲除了 Serial 收集器,只有它能與 CMS 收集器配合工作。

在JDK1.5 時期,HotSpot 推出了 CMS 收集器(Concurrent Mark Sweep),它是 HotSpot 虛擬機中第一款真正意義上的併發收集器。不幸的是,CMS 作爲老年代的收集器,卻無法與 JDK1.4.0 中已經存在的新生代收集器 Parallel Scavenge 配合工作,所以在 JDK1.5中使用 CMS 來收集老年代的時候,新生代只能選擇 ParNew 或者 Serial 收集器中的一個

默認開啓的線程數量與 CPU 數量相同,可以使用 -XX:ParallelGCThreads 參數來設置線程數。

Parallel Scavenge 收集器以及後面提到的 G1 收集器都沒有使用傳統的 GC 收集器代碼框架,而另外獨立實現,其餘集中收集器則共用了部分的框架代碼。

3. Parallel Scavenge

Parallel Scavenge 收集器是一個新生代收集器,它也是使用複製算法的收集器,又是並行的多線程收集器。

與 ParNew 的不同之處:

其它收集器關注點是儘可能縮短垃圾收集時用戶線程的停頓時間(響應時間),而它的目標是達到一個可控制的吞吐量,它被稱爲 吞吐量優先收集器

吞吐量指 CPU 用於運行用戶代碼的時間佔總時間的比值

吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)

停頓時間越短就越適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗。而高吞吐量則可以高效率地利用 CPU 時間,儘快完成程序的運算任務,主要適合在後臺運算而不需要太多交互的任務。

縮短停頓時間是以犧牲吞吐量和新生代空間來換取的:新生代空間變小,垃圾回收變得頻繁,導致吞吐量下降。

可以通過一個開關參數打卡 GC 自適應的調節策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 區的比例、晉升老年代對象年齡等細節參數了。虛擬機會根據當前系統運行情況收集性能監控信息,動態調整這些參數,以提供最合適的停頓時間或最大的吞吐量,這種調節方式稱爲GC自適應的調節策略(GC Ergonomiscs) 。

Parallel Scavenge 收集器氣提供了兩個參數用於精確控制吞吐量

  • 最大垃圾收集停頓時間: -XX:MaxGCPauseMills
  • 吞吐量大小:-XX:GCTimeRatio

MaxGCPauseMills 參數允許的值是一個大於0的毫秒數,收集器將儘可能地保證內存回收所花費的時間不超過設定值。但 GC 的停頓時間縮短是以犧牲吞吐量和新生代空間來換取的。停頓時間下降,但吞吐量也降下來了。

GCTimeRatio 參數的值是一個大於0且小於100的整數,也就是垃圾收集時間佔總時間的比例,相當於吞吐量的倒數。區間 1/(1+99) ~ 1/(1+1),即 1% ~ 50%。

由於與吞吐量關係密切,Parallel Scavenge 收集器也經常稱爲 “吞吐量優先“ 收集器。

-XX:+UserAdaptiveSizePolicy: GC 自適應調節策略(GC Ergonomics),打開參數後,就不需要手工指定新生代的大小(-Xmn)、Eden 與 Survivor 區的比例(-XX:SurvivorRatio)、晉升老年代對象的年齡(-XX:PretenureSizeThreshold)等細節參數了。

4. Serial Old

Serial Old 是 Serial 收集器的老年代版本,它同樣是一個單線程收集器,使用 ”標記-整理“ 算法。

這個收集器的主要意義也是在於給 Client 模式下的虛擬機使用。如果在 Server 模式下,那麼它主要還有兩大用途:

  • 在 JDK 1.5 以及之前版本(Parallel Old 誕生以前)中與 Parallel Scavenge 收集器搭配使用。

  • 作爲 CMS 收集器的後備預案,在併發收集發生 Concurrent Mode Failure 時使用。

5. Parallel Old

Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多線程和 ”標記-整理“ 算法。

在注重吞吐量以及 CPU 資源敏感的場合,都可以優先考慮 Parallel Scavenge 加 Parallel Old 收集器。

6. CMS

CMS(Concurrent Mark Sweep),Mark Sweep 指的是 標記 - 清除 算法。CMS 是一款優秀的收集器,主要優點:併發收集、低停頓,Sun公司也稱之爲併發低停頓收集器(Concurrent Low Pause Collection)。

特點:併發收集、低停頓。

分爲以下四個流程:

  • 初始標記:僅僅只是標記一下 GC Roots 能直接關聯到的對象,速度很快,需要停頓。
  • 併發標記:進行 GC Roots Tracing 的過程,它在整個回收過程中耗時最長,不需要停頓。
  • 重新標記:爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,需要停頓。
  • 併發清除:不需要停頓。

在整個過程中耗時最長的併發標記和併發清除過程中,收集器線程都可以與用戶線程一起工作,不需要進行停頓。

具有以下缺點:

  • 吞吐量低:低停頓時間是以犧牲吞吐量爲代價的,導致 CPU 利用率不夠高。

  • 無法處理浮動垃圾,可能出現 Concurrent Mode Failure。浮動垃圾是指併發清除階段由於用戶線程繼續運行而產生的垃圾,這部分垃圾只能到下一次 GC 時才能進行回收。由於浮動垃圾的存在,因此需要預留出一部分內存,意味着 CMS 收集不能像其它收集器那樣等待老年代快滿的時候再回收。如果預留的內存不夠存放浮動垃圾,就會出現 Concurrent Mode Failure,這時虛擬機將臨時啓用 Serial Old 來替代 CMS。

  • 標記 - 清除算法導致的空間碎片,往往出現老年代空間剩餘,但無法找到足夠大連續空間來分配當前對象,不得不提前觸發一次 Full GC。

    • CMS 提供了一個開關參數 -XX:+UseCMSCompactAtFullCollection(默認開啓),用於在 CMS 收集器頂不住要進行 Full GC 時開啓內存碎片的合併整理過程,內存整理的過程是無法併發的。
    • 參數 -XX:CMSFullGCsBeforeCompaction 用於設置執行多少次不壓縮的 Full GC後,跟着來以此帶壓縮的,(默認值爲0)

7. G1

G1的第一篇paper(附錄1)發表於2004年,在2012年纔在jdk1.7u4中可用。oracle官方計劃在jdk9中將G1變成默認的垃圾收集器,以替代CMS。

  • 爲何oracle要極力推薦G1呢,G1有哪些優點?
    • 首先,G1的設計原則就是簡單可行的性能調優
    • 其次,G1將新生代,老年代的物理空間劃分取消了**。**

G1(Garbage-First),它是一款面向服務端應用的垃圾收集器,在多 CPU 和大內存的場景下有很好的性能。HotSpot 開發團隊賦予它的使命是未來可以替換掉 CMS 收集器。

堆被分爲新生代和老年代,其它收集器進行收集的範圍都是整個新生代或者老年代,而 G1 可以直接對新生代和老年代一起回收。

 

G1 把堆劃分成多個大小相等的獨立區域(Region),新生代和老年代不再物理隔離。

 

通過引入 Region 的概念,從而將原來的一整塊內存空間劃分成多個的小空間,使得每個小空間可以單獨進行垃圾回收。這種劃分方法帶來了很大的靈活性,使得可預測的停頓時間模型成爲可能。通過記錄每個 Region 垃圾回收時間以及回收所獲得的空間(這兩個值是通過過去回收的經驗獲得),並維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的 Region。

每個 Region 都有一個 Remembered Set,用來記錄該 Region 對象的引用對象所在的 Region。通過使用 Remembered Set,在做可達性分析的時候就可以避免全堆掃描。

 

如果不計算維護 Remembered Set 的操作,G1 收集器的運作大致可劃分爲以下幾個步驟:

  • 初始標記
  • 併發標記
  • 最終標記:爲了修正在併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程的 Remembered Set Logs 裏面,最終標記階段需要把 Remembered Set Logs 的數據合併到 Remembered Set 中。這階段需要停頓線程,但是可並行執行。
  • 篩選回收:首先對各個 Region 中的回收價值和成本進行排序,根據用戶所期望的 GC 停頓時間來制定回收計劃。此階段其實也可以做到與用戶程序一起併發執行,但是因爲只回收一部分 Region,時間是用戶可控制的,而且停頓用戶線程將大幅度提高收集效率。

具備如下特點:

  • 空間整合:整體來看是基於“標記 - 整理”算法實現的收集器,從局部(兩個 Region 之間)上來看是基於“複製”算法實現的,這意味着運行期間不會產生內存空間碎片。
  • 可預測的停頓:能讓使用者明確指定在一個長度爲 M 毫秒的時間片段內,消耗在 GC 上的時間不得超過 N 毫秒。

詳情請參考:

8. 比較

收集器 單線程/並行 串行/併發 新生代/老年代 收集算法 目標 適用場景
Serial 單線程 串行 新生代 複製 響應速度優先 單 CPU 環境下的 Client 模式
Serial Old 單線程 串行 老年代 標記-整理 響應速度優先 單 CPU 環境下的 Client 模式、CMS 的後備預案
ParNew 並行 串行 新生代 複製算法 響應速度優先 多 CPU 環境時在 Server 模式下與 CMS 配合
Parallel Scavenge 並行 串行 新生代 複製算法 吞吐量優先 在後臺運算而不需要太多交互的任務
Parallel Old 並行 串行 老年代 標記-整理 吞吐量優先 在後臺運算而不需要太多交互的任務
CMS 並行 併發 老年代 標記-清除 響應速度優先 集中在互聯網站或 B/S 系統服務端上的 Java 應用
G1 並行 併發 新生代 + 老年代 標記-整理 + 複製算法 響應速度優先 面向服務端應用,將來替換 CMS

參考:Java GC | Pandora

5. 內存分配與回收策略

1. 什麼時候進行MinGC,FullGC

  • Minor GC:發生在新生代上,因爲新生代對象存活時間很短,因此 Minor GC 會頻繁執行,執行的速度一般也會比較快。
    • 新生代中的垃圾收集動作,採用的是複製算法
    • 對於較大的對象,在 Minor GC 的時候可以直接進入老年代
  • Full GC:發生在老年代上,老年代對象其存活時間長,因此 Full GC 很少執行,執行速度會比 Minor GC 慢很多。
    • Full GC 是發生在老年代的垃圾收集動作,採用的是 標記-清除/整理 算法。
    • 由於老年代的對象幾乎都是在 Survivor 區熬過來的,不會那麼容易死掉。因此 Full GC 發生的次數不會有 Minor GC 那麼頻繁,並且 Time(Full GC)>Time(Minor GC)

2. 內存分配策略

1. 對象優先在 Eden 分配

大多數情況下,對象在新生代 Eden 區分配,當 Eden 區空間不夠時,發起 Minor GC。

2. 大對象直接進入老年代

大對象是指需要連續內存空間的對象,最典型的大對象是那種很長的字符串以及數組。

經常出現大對象會提前觸發垃圾收集以獲取足夠的連續空間分配給大對象。

-XX:PretenureSizeThreshold,大於此值的對象直接在老年代分配,避免在 Eden 區和 Survivor 區之間的大量內存複製。

3. 長期存活的對象進入老年代

爲對象定義年齡計數器,對象在 Eden 出生並經過 Minor GC 依然存活,將移動到 Survivor 中,年齡就增加 1 歲,增加到一定年齡則移動到老年代中。

-XX:MaxTenuringThreshold 用來定義年齡的閾值。

4. 動態對象年齡判定

虛擬機並不是永遠地要求對象的年齡必須達到 MaxTenuringThreshold 才能晉升老年代,如果在 Survivor 中相同年齡所有對象大小的總和大於 Survivor 空間的一半,則年齡大於或等於該年齡的對象可以直接進入老年代,無需等到 MaxTenuringThreshold 中要求的年齡。

5. 空間分配擔保

在發生 Minor GC 之前,虛擬機先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果條件成立的話,那麼 Minor GC 可以確認是安全的。

如果不成立的話虛擬機會查看 HandlePromotionFailure 設置值是否允許擔保失敗,如果允許那麼就會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試着進行一次 Minor GC;如果小於,或者 HandlePromotionFailure 設置不允許冒險,那麼就要進行一次 Full GC。

3. Full GC 的觸發條件

對於 Minor GC,其觸發條件非常簡單,當 Eden 空間滿時,就將觸發一次 Minor GC。而 Full GC 則相對複雜,有以下條件:

1. 調用 System.gc()

只是建議虛擬機執行 Full GC,但是虛擬機不一定真正去執行。不建議使用這種方式,而是讓虛擬機管理內存。

2. 老年代空間不足

老年代空間不足的常見場景爲前文所講的大對象直接進入老年代、長期存活的對象進入老年代等。

爲了避免以上原因引起的 Full GC,應當儘量不要創建過大的對象以及數組。除此之外,可以通過 -Xmn 虛擬機參數調大新生代的大小,讓對象儘量在新生代被回收掉,不進入老年代。還可以通過 -XX:MaxTenuringThreshold 調大對象進入老年代的年齡,讓對象在新生代多存活一段時間。

3. 空間分配擔保失敗

使用複製算法的 Minor GC 需要老年代的內存空間作擔保,如果擔保失敗會執行一次 Full GC。具體內容請參考上面的第五小節。

4. JDK 1.7 及以前的永久代空間不足

在 JDK 1.7 及以前,HotSpot 虛擬機中的方法區是用永久代實現的,永久代中存放的爲一些 Class 的信息、常量、靜態變量等數據。

當系統中要加載的類、反射的類和調用的方法較多時,永久代可能會被佔滿,在未配置爲採用 CMS GC 的情況下也會執行 Full GC。如果經過 Full GC 仍然回收不了,那麼虛擬機會拋出 java.lang.OutOfMemoryError。

爲避免以上原因引起的 Full GC,可採用的方法爲增大永久代空間或轉爲使用 CMS GC。

5. Concurrent Mode Failure

執行 CMS GC 的過程中同時有對象要放入老年代,而此時老年代空間不足(可能是 GC 過程中浮動垃圾過多導致暫時性的空間不足),便會報 Concurrent Mode Failure 錯誤,並觸發 Full GC。

6. 類加載機制

虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的 Java 類型,這就是虛擬機類加載機制。(類是在運行期間動態加載的)

懶加載:要用的時候再去加載

類的生命週期

 

包括以下 7 個階段:

  • 加載(Loading)
  • 驗證(Verification)
  • 準備(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 卸載(Unloading)

其中解析過程在某些情況下可以在初始化階段之後再開始,這是爲了支持 Java 的動態綁定。

這7個階段中的:加載、驗證、準備、初始化、卸載的順序是固定的。但它們並不一定是嚴格同步串行執行,它們之間可能會有交叉,但總是以 “開始” 的順序總是按部就班的。至於解析則有可能在初始化之後纔開始,這是爲了支持 Java 語言的運行時綁定(也稱爲動態綁定或晚期綁定)。

類初始化時機

1. 主動引用

虛擬機規範中並沒有強制約束何時進行加載,但是規範嚴格規定了有且只有下列五種情況必須對類進行初始化(加載、驗證、準備都會隨之發生):

  • 遇到 new、getstatic、putstatic、invokestatic 這四條字節碼指令時,如果類沒有進行過初始化,則必須先觸發其初始化。最常見的生成這 4 條指令的場景是:使用 new 關鍵字實例化對象的時候;讀取或設置一個類的靜態字段(被 final 修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候;以及調用一個類的靜態方法的時候。
  • 使用 java.lang.reflect 包的方法對類進行反射調用的時候,如果類沒有進行初始化,則需要先觸發其初始化。
  • 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  • 當虛擬機啓動時,用戶需要指定一個要執行的主類(包含 main() 方法的那個類),虛擬機會先初始化這個主類;
  • 當使用 JDK 1.7 的動態語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最後的解析結果爲 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化;

2. 被動引用

以上 5 種場景中的行爲稱爲對一個類進行主動引用。除此之外,所有引用類的方式都不會觸發初始化,稱爲被動引用。被動引用的常見例子包括:

  • 通過子類引用父類的靜態字段,不會導致子類初始化。
System.out.println(SubClass.value);  // value 字段在 SuperClass 中定義
  • 通過數組定義來引用類,不會觸發此類的初始化。該過程會對數組類進行初始化,數組類是一個由虛擬機自動生成的、直接繼承自 Object 的子類,其中包含了數組的屬性和方法。
SuperClass[] sca = new SuperClass[10];
  • 常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。
System.out.println(ConstClass.HELLOWORLD);

類加載過程

包含了加載、驗證、準備、解析和初始化這 5 個階段。

1. 加載

加載是類加載的一個階段,注意不要混淆。

加載過程完成以下三件事:

  • 通過一個類的全限定名來獲取定義此類的二進制字節流。
  • 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時存儲結構。
  • 在內存中生成一個代表這個類的 Class 對象,作爲方法區這個類的各種數據的訪問入口。

加載源(其中二進制字節流可以從以下方式中獲取):

  • 文件:從 ZIP 包讀取,這很常見,最終成爲日後 JAR、EAR、WAR 格式的基礎。
  • 網絡:從網絡中獲取,這種場景最典型的應用是 Applet。
  • 計算生成一個二進制流:運行時計算生成,這種場景使用得最多得就是動態代理技術,在 java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass 的代理類的二進制字節流。
  • 由其他文件生成:由其他文件生成,典型場景是 JSP 應用,即由 JSP 文件生成對應的 Class 類。
  • 數據庫:從數據庫讀取,這種場景相對少見,例如有些中間件服務器(如 SAP Netweaver)可以選擇把程序安裝到數據庫中來完成程序代碼在集羣間的分發。 ...

2. 驗證

目的:確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

  • 文件格式驗證:驗證字節流是否符合 Class 文件格式的規範,並且能被當前版本的虛擬機處理。
    • 是否以0xCAFEBABE開頭
    • 版本號是否合理
  • 元數據驗證:對字節碼描述的信息進行語義分析,以保證其描述的信息符合 Java 語言規範的要求。
    • 是否有父類-繼
    • 承了final類?
    • 非抽象類實現了所有的抽象方法
  • 字節碼驗證(很複雜):通過數據流和控制流分析,確保程序語義是合法、符合邏輯的。
    • 運行檢查
    • 棧數據類型和操作碼數據參數吻合
    • 跳轉指令指定到合理的位置
  • 符號引用驗證:發生在虛擬機將符號引用轉換爲直接引用的時候,對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗。
    • 常量池中描述類是否存在
    • 訪問的方法或字段是否存在且有足夠的權限

3. 準備

類變量是被 static 修飾的變量,準備階段爲類變量分配內存並設置初始值,使用的是方法區的內存。

實例變量不會在這階段分配內存,它將會在對象實例化時隨着對象一起分配在堆中。

注意,實例化不是類加載的一個過程,類加載發生在所有實例化操作之前,並且類加載只進行一次,實例化可以進行多次。

初始值一般爲 0 值,例如下面的類變量 value 被初始化爲 0 而不是 123,在初始化的中才會被設置爲1。

  • 默認值:int 0, boolean false, float 0.0, char '0', 抽象數據類型 null
public static int value = 123;

對於static final類型,在準備階段就會被賦上正確的值

public static final int value = 123;

4. 解析

什麼是符號引用和直接引用?

  • 符號引用:符號引用是一組符號來描述所引用的目標對象,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存佈局無關,引用的目標對象並不一定已經加載到內存中。
  • 直接引用:直接引用可以是直接指向目標對象的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機內存佈局實現相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同,如果有了直接引用,那引用的目標必定已經在內存中存在。

將常量池的符號引用替換爲直接引用的過程

  • 類或接口的解析
  • 字段解析
  • 類方法解析
  • 接口方法解析

更多詳情轉向:【必讀】JVM 類加載機制 - 掘金

5. 初始化

初始化階段才真正開始執行類中定義的 Java 程序代碼。初始化階段即虛擬機執行類構造器 () 方法的過程。

在準備階段,類變量已經賦過一次系統要求的初始值,而在初始化階段,根據程序員通過程序制定的主觀計劃去初始化類變量和其它資源。

() 方法具有以下特點:

  • 是由編譯器自動收集類中所有類變量的賦值動作和靜態語句塊中的語句合併產生的,編譯器收集的順序由語句在源文件中出現的順序決定。特別注意的是,靜態語句塊只能訪問到定義在它之前的類變量,定義在它之後的類變量只能賦值,不能訪問。例如以下代碼:
public class Test {
    static {
        i = 0;                // 給變量賦值可以正常編譯通過
        System.out.print(i);  // 這句編譯器會提示“非法向前引用”
    }
    static int i = 1;
}
  • 與類的構造函數(或者說實例構造器 ())不同,不需要顯式的調用父類的構造器。虛擬機會自動保證在子類的 () 方法運行之前,父類的 () 方法已經執行結束。因此虛擬機中第一個執行 () 方法的類肯定爲 java.lang.Object。
  • 由於父類的 () 方法先執行,也就意味着父類中定義的靜態語句塊要優先於子類的變量賦值操作。例如以下代碼:
static class Parent {
    public static int A = 1;
    static {
        A = 2;
    }
}

static class Sub extends Parent {
    public static int B = A;
}

public static void main(String[] args) {
     System.out.println(Sub.B);  // 2
}
  • () 方法對於類或接口不是必須的,如果一個類中不包含靜態語句塊,也沒有對類變量的賦值操作,編譯器可以不爲該類生成 () 方法。
  • 接口中不可以使用靜態語句塊,但仍然有類變量初始化的賦值操作,因此接口與類一樣都會生成 () 方法。但接口與類不同的是,執行接口的 () 方法不需要先執行父接口的 () 方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也一樣不會執行接口的 () 方法。
  • 虛擬機會保證一個類的 () 方法在多線程環境下被正確的加鎖和同步,如果多個線程同時初始化一個類,只會有一個線程執行這個類的 () 方法,其它線程都會阻塞等待,直到活動線程執行 () 方法完畢。如果在一個類的 () 方法中有耗時的操作,就可能造成多個線程阻塞,在實際過程中此種阻塞很隱蔽。

類加載器

虛擬機設計團隊把類加載階段中的 “通過一個類的全限定名來獲取描述此類的二進制字節流(即字節碼)” 這個動作放到 Java 虛擬機外部去實現,以便讓應用程序自己決定如何去獲取所需要的類(通過一個類的全限之名獲取描述此類的二進制字節流)。實現這個動作的代碼模塊稱爲 “類加載器”

1. 類與類加載器

兩個類相等:只有被同一個類加載器加載的類纔可能會相等。相同的字節碼被不同的類加載器加載的類不相等。

這裏的相等,包括類的 Class 對象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回結果爲 true,也包括使用 instanceof 關鍵字做對象所屬關係判定結果爲 true。

2. 類加載器分類

從 Java 虛擬機的角度來講,只存在以下兩種不同的類加載器:

  • 啓動類加載器(Bootstrap ClassLoader),這個類加載器用 C++ 實現,是虛擬機自身的一部分;
  • 所有其他類的加載器,這些類由 Java 實現,獨立於虛擬機外部,並且全都繼承自抽象類 java.lang.ClassLoader。

從 Java 開發人員的角度看,類加載器可以劃分得更細緻一些:

  • 啓動類加載器(Bootstrap ClassLoader)此類加載器負責將存放在 <JAVA_HOME>\lib 目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中的,並且是虛擬機識別的(僅按照文件名識別,如 rt.jar,名字不符合的類庫即使放在 lib 目錄中也不會被加載)類庫加載到虛擬機內存中。啓動類加載器無法被 Java 程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給啓動類加載器,直接使用 null 代替即可。
  • 擴展類加載器(Extension ClassLoader)這個類加載器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它負責將 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系統變量所指定路徑中的所有類庫加載到內存中,開發者可以直接使用擴展類加載器。
  • 應用程序類加載器(Application ClassLoader)這個類加載器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。由於這個類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般稱爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
  • 自定義類加載器
    • 載器步驟:
      • 定義一個類,繼承ClassLoader
      • 重寫 loadClass 方法
      • 實例化 Class 對象
    • 自定義類加載器的優勢
      • 類加載器是java語言的一項創新,也是java語言流行的重要原因之一,它最初的設計是爲了滿足java applet 的需求而開發出來的
      • 高度的靈活性
      • 通過自定義類加載器可以實現熱部署
      • 代碼加密

參考資料:

3. 雙親委派模型

JVM 如何加載一個類的過程,雙親委派模型中有哪些方法有沒有可能父類加載器和子類加載器,加載同一個類?如果加載同一個類,該使用哪一個類?

  • 雙親委派機制圖

 

  • 雙親委派概念

    • 如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的加載器都是如此,因此所有的類加載請求都會傳給頂層的啓動類加載器,只有當父加載器反饋自己無法完成該加載請求(該加載器的搜索範圍中沒有找到對應的類)時,子加載器纔會嘗試自己去加載。
  • 加載器

    • 啓動(Bootstrap)類加載器:是用本地代碼實現的類裝入器,它負責將 <Java_Runtime_Home>/lib下面的類庫加載到內存中(比如rt.jar)。由於引導類加載器涉及到虛擬機本地實現細節,開發者無法直接獲取到啓動類加載器的引用,所以不允許直接通過引用進行操作。
    • 標準擴展(Extension)類加載器:是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它負責將< Java_Runtime_Home >/lib/ext或者由系統變量 java.ext.dir指定位置中的類庫加載到內存中。開發者可以直接使用標準擴展類加載器。
    • 系統(System)類加載器:由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。它負責將系統類路徑(CLASSPATH)中指定的類庫加載到內存中。開發者可以直接使用系統類加載器。除了以上列舉的三種類加載器,還有一種比較特殊的類型 — 線程上下文類加載器。
  • 如果加載同一個類,該使用哪一個類?

    • 父類的

7. Student s = new Student(); 在內存中做了哪些事情

  1. 加載 Student.class 文件進內存
  2. 在棧內存爲 s 開闢空間
  3. 在堆內存爲 Student 對象開闢空間
  4. 對 Student 對象的成員變量進行默認初始化
  5. 對 Student 對象的成員變量進行顯示初始化
  6. 通過構造方法對 Student 對象的成員變量賦值
  7. Student 對象初始化完畢,把對象地址賦值給 s 變量

8. Java虛擬機工具

JDK 本身提供了很多方便的 JVM 性能調優監控工具,除了 jps、jstat、jinfo、jmap、jhat、jstack 等小巧的工具,還有集成式的 jvisualvm 和 jconsole。

(1)jps

jps(JVM Process Status Tool,虛擬機進程監控工具),這個命令可以列出正在運行的虛擬機進程,並顯示虛擬機執行主類名稱,以及這些進程的本地虛擬機唯一 ID。這個 ID 被稱爲本地虛擬機唯一 ID(local virtual Machine Identifier,簡寫爲LVMID)。如果你在 linux 的一臺服務器上使用 jps 得到的 LVMID 其實就是和 ps 命令得到的 PID 是一樣的。

語法格式如下:

jps [options] [hostid]

如果不指定hostid就默認爲當前主機或服務器。

options參數選項說明如下:

-q 不輸出類名、Jar名和傳入main方法的參數
-m 輸出傳入main方法的參數
-l 輸出main類或Jar的全限名
-v 輸出傳入JVM的參數

使用(查看所有java進程)

jps -lv

示例:

[root@chengchi ~]# jps
24804 Jps
1862 mango.jar

[root@chengchi ~]# jps -lv
24787 sun.tools.jps.Jps -Dapplication.home=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.171-8.b10.el7_5.x86_64 -Xms8m
1862 /home/www/api.chengchijinfu.com/mango_server/target/mango.jar -Dserver.port=8080 -Dspring.profiles.active=prod

(2)jstat

jstat(JVM Statistics Monitoring Tool,虛擬機統計信息監視工具),這個命令用於監視虛擬機各種運行狀態信息。它可以顯示本地或者遠程虛擬機進程中的類裝載、內存、垃圾收集、JIT編譯等運行數據,雖然沒有GUI圖形界面,只是提供了純文本控制檯環境的服務器上,但它是運行期間定位虛擬機性能問題的首選工具。

語法格式如下:

jstat [option vmid [interval [s | ms] [count ] ] ]

例如:需要每 1000 毫秒查詢一次進程 16418 垃圾收集狀況,一共查詢 10 次,那命令如下:

參考:jstat命令詳解 - CSDN博客

(3)jinfo

jinfo (Configuration Info for Java,配置信息工具) 這個命令可以實時地查看和調整虛擬機各項參數。

查看2788的MaxPerm大小可以用

[root@Bill-8 bin]# jinfo -flag MaxPermSize 2788
-XX:MaxPermSize=134217728

(4)jmap

jmap(Memory Map for Java,內存映像工具),用於生成堆轉存的快照,一般是 heapdump 或者 dump 文件。如果不適用 jmap 命令,可以使用 -XX:+HeapDumpOnOutOfMemoryError 參數,當虛擬機發生內存溢出的時候可以產生快照。或者使用kill -3 pid也可以產生。jmap 的作用並不僅僅是爲了獲取 dump 文件,它可以查詢 finalize 執行隊列,java 堆和永久代的詳細信息,如空間使用率,當前用的哪種收集器。

jmap的命令格式:

jmap [option] vmid
jmap -J-d64 -heap 16418

(5)jhat

jhat(虛擬機堆轉儲快照分析工具),這個工具是用來分析 jmap dump 出來的文件。 由於這個工具功能比較簡陋,運行起來也比較耗時,所以這個工具不推薦使用,推薦使用MAT。

例如分析dump 出來的 test.bin,命令如下:

jhat test.bin 

它會在本地啓動一個web服務,端口是7000,這樣直接訪問 127.0.0.1:7000就能看到分析結果了。

(6)jstack【阿里實習】

jstack(Java Stack Trace,Java堆棧跟蹤工具),這個命令用於查看虛擬機當前時刻的線程快照(一般是threaddump 或者 javacore文件)。線程快照就是當前虛擬機內每一條線程正在執行的方法堆棧的集合。**生成線程快照的主要目的是:**定位線程出現長時間停頓的原因,入線程間死鎖、死循環、請求外部資源導致的長時間等待都是導致線程長時間停頓的常見原因。線程出現停頓的時候通過jstack來查看各個線程的調用堆棧,就可以知道沒有響應的線程到底在後臺做些什麼事情。

命令格式:

jstack [option] vmid

使用:查看進程2849 的堆棧信息

[root@Bill-8 yrd_soft]# jstack 2849

(7)jconsole【阿里面經】

JConsole 中,您將能夠監視 JVM 內存的使用情況、線程堆棧跟蹤、已裝入的類和 VM 信息以及 CE MBean。

jconsole:一個 java GUI 監視工具,可以以圖表化的形式顯示各種數據。並可通過遠程連接監視遠程的服務器VM。用 Java 寫的 GUI 程序,用來監控 VM,並可監控遠程的 VM,非常易用,而且功能非常強。命令行裏打 jconsole,選則進程就可以了。

(8)jvisualvm

jvisualvm 同 jconsole 都是一個基於圖形化界面的、可以查看本地及遠程的 JAVA GUI 監控工具,Jvisualvm 同 jconsole 的使用方式一樣,直接在命令行打入 jvisualvm 即可啓動,jvisualvm 界面更美觀一些,數據更實時:

參考資料:

9. 瞭解過JVM調優沒,基本思路是什麼

詳情轉向:美團技術:從實際案例聊聊Java應用的GC優化

10. JVM線程死鎖,你該如何判斷是因爲什麼?如果用VisualVM,dump線程信息出來,會有哪些信息

  • 常常需要在隔兩分鐘後再次收集一次thread dump,如果得到的輸出相同,仍然是大量thread都在等待給同一個地址上鎖,那麼肯定是死鎖了。

11. 什麼是內存泄露?用什麼工具可以查出內存泄漏

在 Java 中,內存泄漏就是存在一些被分配的對象,這些對象有下面兩個特點

  • 這些對象是可達的,即在有向圖中,存在通路可以與其相連;
  • 這些對象是無用的,即程序以後不會再使用這些對象。

如果對象滿足這兩個條件,這些對象就可以判定爲 Java 中的內存泄漏,這些對象不會被 GC 所回收,然而它卻佔用內存。

在 C++ 中,內存泄漏的範圍更大一些。有些對象被分配了內存空間,然後卻不可達,由於 C++ 中沒有 GC,這些內存將永遠收不回來。在 Java 中,這些不可達的對象都由 GC 負責回收,因此程序員不需要考慮這部分的內存泄露。

通過分析,我們得知,對於C++,程序員需要自己管理邊和頂點,而對於 Java 程序員只需要管理邊就可以了(不需要管理頂點的釋放)。通過這種方式,Java 提高了編程的效率。

 

同樣給出一個 Java 內存泄漏的典型例子,

Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
    Object o = new Object();
    v.add(o);
    o = null;   
}

在這個例子中,我們循環申請Object對象,並將所申請的對象放入一個 Vector 中,如果我們僅僅釋放引用本身,那麼 Vector 仍然引用該對象,所以這個對象對 GC 來說是不可回收的。因此,如果對象加入到Vector 後,還必須從 Vector 中刪除,最簡單的方法就是將 Vector 對象設置爲 null。

內存泄露查詢工具

  • MemoryAnalyzer:一個功能豐富的 JAVA 堆轉儲文件分析工具,可以幫助你發現內存漏洞和減少內存消耗
  • EclipseMAT:是一款開源的JAVA內存分析軟件,查找內存泄漏,能容易找到大塊內存並驗證誰在一直佔用它,它是基於Eclipse RCP(Rich Client Platform),可以下載RCP的獨立版本或者Eclipse的插件
  • JProbe:分析Java的內存泄漏。

參考資料:Java中關於內存泄漏出現的原因以及如何避免內存泄漏(超詳細版彙總上)

* 虛擬機參數

大多數的配置都是爲堆服務的

(1)-XX 對於系統級別的(JVM)的配置

  • 比如配置日誌信息或者配置JVM使用什麼樣的垃圾回收器

(2)非-XX 配置基本都是對應用層面上的配置

+ 表示啓動- 表示禁用

附錄:參考資料

更新說明

v1.0 2018/7/21 初版完成

v2.4 2018/8/18 基礎初版

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