1.總覽
這個教程我們將看看詳盡的Java垃圾回收。接下來,我們將介紹幾個不同的實例,我們將瞭解可用的不同配置選項。此外,我們還將重點介紹如何解釋詳細日誌的輸出。
要了解有關垃圾回收 (GC) 和可用不同實現的更多信息,請查看我們有關 Java 垃圾回收器的文章。
2.Verbose GC 簡介
在調試和分析許多問題(尤其是內存問題時),通常需要打開詳細的垃圾回收日誌記錄。事實上,有些人會認爲,爲了嚴格監視我們的應用程序運行狀況,我們應該始終監視 JVM 的垃圾回收性能。
正如我們將看到的,GC 日誌是一個非常重要的工具,用於揭示對應用程序的堆和 GC 配置的潛在改進。對於發生的每個 GC,GC 日誌都提供有關其結果和時間的精確數據。
隨着時間的推移,分析這些信息可以幫助我們更好地瞭解行爲或應用程序,並幫助我們調整應用程序的性能。此外,它還可以通過指定最佳堆大小、其他 JVM 選項和備用 GC 算法來幫助優化 GC 頻率和收集時間。
2.1 例子
public class Application {
private static Map<String, String> stringContainer = new HashMap<>();
public static void main(String[] args) {
System.out.println("Start of program!");
String stringWithPrefix = "stringWithPrefix";
// Load Java Heap with 3 M java.lang.String instances
for (int i = 0; i < 3000000; i++) {
String newString = stringWithPrefix + i;
stringContainer.put(newString, newString);
}
System.out.println("MAP size: " + stringContainer.size());
// Explicit GC!
System.gc();
// Remove 2 M out of 3 M
for (int i = 0; i < 2000000; i++) {
String newString = stringWithPrefix + i;
stringContainer.remove(newString);
}
System.out.println("MAP size: " + stringContainer.size());
System.out.println("End of program!");
}
}
如上例所示,此簡單程序將 300 萬個 String 實例加載到 Map 對象中。然後,我們使用 System.gc() 對垃圾回收器進行顯式調用。
最後,我們從地圖中刪除 200 萬個字符串實例。我們還顯式使用 System.out.println() 使解釋輸出更加容易。
在下一節中,我們將瞭解如何激活 GC 日誌記錄。
2.激活GC日誌
讓我們首先運行我們的程序,並通過我們的 JVM 啓動參數啓用verbose GC:
-XX:+UseSerialGC -Xms1024m -Xmx1024m -verbose:gc
這裏的重要參數是 -verbose:gc
,它以最簡單的形式激活垃圾回收信息的日誌記錄。默認情況下,GC 日誌寫入粗壯,應爲每個年輕代 GC 和每個完整 GC 輸出一行。
爲了我們的示例,我們通過參數-XX:_UseSerialGC
指定了串行垃圾回收器,即最簡單的 GC 實現。
我們還設置了最小和最大的堆大小 1024mb,但當然,我們可以調整更多的 JVM 參數。
3.1Verbose GC的輸出
Start of program!
[GC (Allocation Failure) 279616K->146232K(1013632K), 0.3318607 secs]
[GC (Allocation Failure) 425848K->295442K(1013632K), 0.4266943 secs]
MAP size: 3000000
[Full GC (System.gc()) 434341K->368279K(1013632K), 0.5420611 secs]
[GC (Allocation Failure) 647895K->368280K(1013632K), 0.0075449 secs]
MAP size: 1000000
End of program!
在上面的輸出中,我們已經看到了許多有關JVM內部情況的有用信息。
起初,這個輸出看起來相當令人害怕的,但現在讓我們一步一步地分析它。
首先,我們可以看到,四個集合發生, one Full GC and three cleaning Young generation 。
3.2 verbose GC更詳盡的輸出
讓我們更詳細地分解輸出行,以準確瞭解發生了什麼:
- GC or Full GC - 垃圾回收的類型,用於區分次要或完全垃圾回收的 GC 或完整 GC
- (Allocation Failure) or (System.gc()) - 集合的原因 - 分配失敗表示Egen去中沒有剩餘空間來分配我們的對象
- 279616K->146232K - GC 運行前後佔用的堆內存(用箭頭分隔)
- (1013632K) - 堆的當前容量
- 0.3318607 秒 - GC 事件的運行時間(以秒爲單位)
我們看輸出的第一行, 279616K->146232K(1013632K) 表明 GC 把佔用的堆內存從 279616K 減少到 146232K,釋放了1013632k的空間。GC的時候,堆空間是1013632K,GC耗費了 0.3318607 s。
儘管簡單的 GC 日誌記錄格式可能很有用,它提供了有限的詳細信息。但是,我們無法判斷 GC 是否將任何對象從新生代移動到老年代,或者每個回收前後新生代的容量是多少。
因此,詳細的 GC 日誌記錄比簡單的日誌記錄更有用。
4.詳細的GC日誌
-XX:+PrintGCDetails
,這個給我們更詳細的GC信息,比如:
- 每次GC前後,新生代和老年代的大小
- 在新生代和老年代,每次GC消耗的時間
- 每次GC,提升的對象大小
- 整個堆內存的大小
5.分析更詳盡的GC日誌
配置參數:
-XX:+UseSerialGC -Xms1024m -Xmx1024m -verbose:gc -XX:+PrintGCDetails
輸出如下:
Start of program!
[GC (Allocation Failure) [DefNew: 279616K->34944K(314560K), 0.3626923 secs] 279616K->146232K(1013632K), 0.3627492 secs] [Times: user=0.33 sys=0.03, real=0.36 secs]
[GC (Allocation Failure) [DefNew: 314560K->34943K(314560K), 0.4589079 secs] 425848K->295442K(1013632K), 0.4589526 secs] [Times: user=0.41 sys=0.05, real=0.46 secs]
MAP size: 3000000
[Full GC (System.gc()) [Tenured: 260498K->368281K(699072K), 0.5580183 secs] 434341K->368281K(1013632K), [Metaspace: 2624K->2624K(1056768K)], 0.5580738 secs] [Times: user=0.50 sys=0.06, real=0.56 secs]
[GC (Allocation Failure) [DefNew: 279616K->0K(314560K), 0.0076722 secs] 647897K->368281K(1013632K), 0.0077169 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
MAP size: 1000000
End of program!
Heap
def new generation total 314560K, used 100261K [0x00000000c0000000, 0x00000000d5550000, 0x00000000d5550000)
eden space 279616K, 35% used [0x00000000c0000000, 0x00000000c61e9370, 0x00000000d1110000)
from space 34944K, 0% used [0x00000000d3330000, 0x00000000d3330188, 0x00000000d5550000)
to space 34944K, 0% used [0x00000000d1110000, 0x00000000d1110000, 0x00000000d3330000)
tenured generation total 699072K, used 368281K [0x00000000d5550000, 0x0000000100000000, 0x0000000100000000)
the space 699072K, 52% used [0x00000000d5550000, 0x00000000ebcf65e0, 0x00000000ebcf6600, 0x0000000100000000)
Metaspace used 2637K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 283K, capacity 386K, committed 512K, reserved 1048576K
我們應該能夠識別簡單 GC 日誌中的所有元素。但有幾個新的點。
現在,讓我們考慮輸出中以藍色突出顯示的新項:
5.1 分析年輕代裏的Minor GC
- [GC (Allocation Failure) [DefNew: 279616K->34944K(314560K), 0.3626923 secs] 279616K->146232K(1013632K), 0.3627492 secs] [Times: user=0.33 sys=0.03, real=0.36 secs]
我們拆分一下:
- GC日誌開頭的“[GC”和“[Full GC”說明了這次垃圾收集的停頓類型,而不是用來區分新生代GC還是老年代GC的。如果有“Full”,說明這次GC是發生了Stop-The-World的。
- DefNew - 垃圾回收使用的名字,這是一個單線程的收集器,發生了 mark-copy stop-the-world ,清理新生代使用的空間。
- 279616K->34944K – GC前該內存區域已使用容量-> GC後該內存區域已使用容量 (該內存區域總容量)
- 0.3626923 secs – 表示該內存區域GC所佔用的時間,單位是秒
- [Times: user=0.33 sys=0.03, real=0.36 secs] – GC 持續的時間,從不同維度統計的。
介紹一下:
- user – 進程執行用戶態代碼(核心之外)所使用的時間。這是執行此進程所使用的實際 CPU 時間,其他進程和此進程阻塞的時間並不包括在內。在垃圾收集的情況下,表示 GC 線程執行所使用的 CPU 總時間。
- sys – 進程在內核態消耗的 CPU 時間,即在內核執行系統調用或等待系統事件所使用的 CPU 時間。
- real – 程序從開始到結束所用的時鐘時間。這個時間包括其他進程使用的時間片和進程阻塞的時間(比如等待 I/O 完成)。
5.2 分析 Full GC
在這個倒數第二個示例中,我們看到,對於由系統調用觸發的Full GC,使用的收集器是 Tenured。
我們看到的最後一條信息是元空間的相同模式的細分:
[Metaspace: 2624K->2624K(1056768K)], 0.5580738 secs]
Metaspace - JDK 8.HotSpot JVM開始使用本地化的內存存放類的元數據,這個空間叫做元空間
輸出的最後一部分包括堆的細目,包括內存每個部分的內存佔用量摘要。
Eden去佔用了 35%,Tenured佔用了52%. 元數據空間和類空間也被包括了。
從上面的輸出,我們能清楚的知道,在JVM內部,GC事件消耗了多少內存。
6.增加日期和時間
沒有日期和時間信息,任何好的日誌都是不完整的。
當我們需要將 GC 日誌數據與其他源的數據關聯時,這些額外信息非常有用,或者它只能幫助促進搜索。
當我們運行應用程序以獲取顯示日誌的日期和時間信息時,我們可以添加以下兩個參數:
-XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps
現在,每行都以絕對日期和時間開始,寫入時跟一個時間戳,反映自 JVM 啓動以來以秒爲單位的實時傳遞時間:
2018-12-11T02:55:23.518+0100: 2.601: [GC (Allocation ...
7.存入文件
默認的日誌輸出是控制檯,我們可以存入文件:
我們可以添加參數-Xloggc:<file>
-Xloggc:/path/to/file/gc.log
8.Java 9以上新增的功能
在GC日誌過程中,會遇到這麼個配置:-XX:+PrintGC
。在Java8裏面,-verbose:gc
和XX:+PrintGC
是一樣的。
-XX:+PrintGC
在Java9被棄用了。-Xlog:gc
在JDK9和10中仍然可以使用。
9.分析GC的工具
使用文本編輯器分析 GC 日誌可能非常耗時且非常繁瑣。根據使用的 JVM 版本和 GC 算法,GC 日誌格式可能會有所不同。
有一個非常好的免費圖形分析工具,用於分析垃圾回收日誌,提供有關潛在垃圾回收問題的許多指標,甚至爲這些問題提供潛在的解決方案。