JVM調優之探索CMS和G1的實體記憶體歸還機制
前言:
公司有一個資產統計系統,使用頻率很低,但是要求在使用時查詢速度快,因此想到做一些快取放在記憶體中,在長時間沒有使用,持久化到磁碟中,並對垃圾進行回收,歸還實體記憶體給作業系統,從而節省寶貴資源給其它業務系統。當我做好快取時,卻發現了一個棘手的問題,通過程式釋放資源並通知GC回收資源後,堆記憶體的已用記憶體減少了,空閒記憶體增加了,可是程序佔用系統記憶體卻沒有減少。查閱了很多資料,也嘗試過很多次,都沒有完美解決問題。直到後來看到一段評論談及G1垃圾回收器,才恍然大悟。
接下來,通過一個小demo給大家演示一下兩種垃圾回收器對實體記憶體歸還的區別。如果有什麼不對的地方,希望大家能夠在評論裡面指正。
- 堆大小配置:
-Xms128M -Xmx2048M
先附上測試程式碼:
import org.junit.Test; import java.util.ArrayList; import java.util.List; public class MemoryRecycleTest { @Test public void testMemoryRecycle() throws InterruptedException { List list = new ArrayList(); //指定要生產的物件大小為512m int count = 512; //新建一條執行緒,負責生產物件 new Thread(() -> { try { for (int i = 1; i <= 10; i++) { System.out.println(String.format("第%s次生產%s大小的物件", i, count)); addObject(list, count); //休眠40秒 Thread.sleep(i * 10000); } } catch (InterruptedException e) { e.printStackTrace(); } }).start(); //新建一條執行緒,負責清理list,回收jvm記憶體 new Thread(() -> { for (;;) { //當list記憶體到達512m,就通知gc回收堆 if (list.size() >= count) { System.out.println("清理list.... 回收jvm記憶體...."); list.clear(); //通知gc回收 System.gc(); //列印堆記憶體資訊 printJvmMemoryInfo(); } } }).start(); //阻止程式退出 Thread.currentThread().join(); } public void addObject(List list, int count) { for (int i = 0; i < count; i++) { OOMobject ooMobject = new OOMobject(); //向list新增一個1m的物件 list.add(ooMobject); try { //休眠100毫秒 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } public static class OOMobject{ //生成1m的物件 private byte[] bytes=new byte[1024*1024]; } public static void printJvmMemoryInfo() { // 虛擬機器級記憶體情況查詢 long vmFree = 0; long vmUse = 0; long vmTotal = 0; long vmMax = 0; int byteToMb = 1024 * 1024; Runtime rt = Runtime.getRuntime(); vmTotal = rt.totalMemory() / byteToMb; vmFree = rt.freeMemory() / byteToMb; vmMax = rt.maxMemory() / byteToMb; vmUse = vmTotal - vmFree; System.out.println(""); System.out.println("JVM記憶體已用的空間為:" + vmUse + " MB"); System.out.println("JVM記憶體的空閒空間為:" + vmFree + " MB"); System.out.println("JVM總記憶體空間為:" + vmTotal + " MB"); System.out.println("JVM總記憶體最大堆空間為:" + vmMax + " MB"); System.out.println(""); } }
首先使用CMS垃圾回收器:
- 將jvm執行引數設定為如下:
-Xms128M -Xmx2048M -XX:+UseConcMarkSweepGC
- 執行程式後,使用JProfiler檢視堆記憶體情況:
- 檢視控制檯列印的內容:
第1次生產512大小的物件 清理list.... 回收jvm記憶體.... JVM記憶體已用的空間為:6 MB JVM記憶體的空閒空間為:936 MB JVM總記憶體空間為:942 MB JVM總記憶體最大堆空間為:1990 MB 第2次生產512大小的物件 清理list.... 回收jvm記憶體.... JVM記憶體已用的空間為:4 MB JVM記憶體的空閒空間為:1025 MB JVM總記憶體空間為:1029 MB JVM總記憶體最大堆空間為:1990 MB 第3次生產512大小的物件 清理list.... 回收jvm記憶體.... JVM記憶體已用的空間為:4 MB JVM記憶體的空閒空間為:680 MB JVM總記憶體空間為:684 MB JVM總記憶體最大堆空間為:1990 MB 第4次生產512大小的物件 清理list.... 回收jvm記憶體.... JVM記憶體已用的空間為:4 MB JVM記憶體的空閒空間為:119 MB JVM總記憶體空間為:123 MB JVM總記憶體最大堆空間為:1990 MB 第5次生產512大小的物件 清理list.... 回收jvm記憶體.... JVM記憶體已用的空間為:4 MB JVM記憶體的空閒空間為:119 MB JVM總記憶體空間為:123 MB JVM總記憶體最大堆空間為:1990 MB 第6次生產512大小的物件 清理list.... 回收jvm記憶體.... JVM記憶體已用的空間為:4 MB JVM記憶體的空閒空間為:119 MB JVM總記憶體空間為:123 MB JVM總記憶體最大堆空間為:1990 MB 第7次生產512大小的物件 清理list.... 回收jvm記憶體.... JVM記憶體已用的空間為:4 MB JVM記憶體的空閒空間為:119 MB JVM總記憶體空間為:123 MB JVM總記憶體最大堆空間為:1990 MB 第8次生產512大小的物件 清理list.... 回收jvm記憶體.... JVM記憶體已用的空間為:4 MB JVM記憶體的空閒空間為:119 MB JVM總記憶體空間為:123 MB JVM總記憶體最大堆空間為:1990 MB 第9次生產512大小的物件 清理list.... 回收jvm記憶體.... JVM記憶體已用的空間為:4 MB JVM記憶體的空閒空間為:119 MB JVM總記憶體空間為:123 MB JVM總記憶體最大堆空間為:1990 MB
- 檢視jmap heap 資訊:
C:\Users>jmap -heap 4716
Attaching to process ID 4716, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.161-b12
using parallel threads in the new generation.
using thread-local object allocation.
Concurrent Mark-Sweep GC
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 2122317824 (2024.0MB)
NewSize = 44695552 (42.625MB)
MaxNewSize = 348913664 (332.75MB)
OldSize = 89522176 (85.375MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
New Generation (Eden + 1 Survivor Space):
capacity = 280887296 (267.875MB)
used = 1629392 (1.5539093017578125MB)
free = 279257904 (266.3210906982422MB)
0.5800874668251284% used
Eden Space:
capacity = 249692160 (238.125MB)
used = 1629392 (1.5539093017578125MB)
free = 248062768 (236.5710906982422MB)
0.6525603366961942% used
From Space:
capacity = 31195136 (29.75MB)
used = 0 (0.0MB)
free = 31195136 (29.75MB)
0.0% used
To Space:
capacity = 31195136 (29.75MB)
used = 0 (0.0MB)
free = 31195136 (29.75MB)
0.0% used
concurrent mark-sweep generation:
capacity = 624041984 (595.1328125MB)
used = 4169296 (3.9761505126953125MB)
free = 619872688 (591.1566619873047MB)
0.6681114583470076% used
6718 interned Strings occupying 574968 bytes.
通過統計圖和控制檯日誌,可以看到在執行43秒左右前,使用記憶體呈直線平滑上升,開闢的記憶體呈階梯狀上升。當使用記憶體到達525m時,程式發起了System.gc(),此時垃圾被回收了,因此使用記憶體回到了10m,可是jvm開闢出來的記憶體空間卻沒有歸還給作業系統,導致程式一直霸佔著960m左右的記憶體資源。第二次生產物件時,可以看到在執行53秒至1分44秒時,不再開闢新空間,而是重複利用已開闢的記憶體繼續建立物件,當執行第二次System.gc()時,jvm又開闢了一小部分記憶體,這一次程式霸佔了1050m記憶體資源。第三次生產物件時,可以看到在執行2分05秒至2分55秒時,不再開闢新空間,而是重複利用已開闢的記憶體繼續建立物件,當執行到第三次System.gc()時,jvm歸還了一部分記憶體給作業系統,此時依然霸佔著700m記憶體。........迴圈執行10次......從總的情況,可以看出,隨著System.gc()次數逐漸增加和時間間隔逐漸拉大,從繼續開闢記憶體變成了慢慢歸還記憶體給了作業系統,直到後面將實體記憶體全部歸還給作業系統。
接下來使用G1垃圾回收器:
-Xms128M -Xmx2048M -XX:+UseG1GC
- 執行程式後,使用JProfiler檢視堆記憶體情況:
- 檢視控制檯列印的內容:
第1次生產512大小的物件
清理list.... 回收jvm記憶體....
JVM記憶體已用的空間為:5 MB
JVM記憶體的空閒空間為:123 MB
JVM總記憶體空間為:128 MB
JVM總記憶體最大堆空間為:2024 MB
第2次生產512大小的物件
清理list.... 回收jvm記憶體....
JVM記憶體已用的空間為:4 MB
JVM記憶體的空閒空間為:124 MB
JVM總記憶體空間為:128 MB
JVM總記憶體最大堆空間為:2024 MB
第3次生產512大小的物件
清理list.... 回收jvm記憶體....
JVM記憶體已用的空間為:4 MB
JVM記憶體的空閒空間為:124 MB
JVM總記憶體空間為:128 MB
JVM總記憶體最大堆空間為:2024 MB
第4次生產512大小的物件
清理list.... 回收jvm記憶體....
JVM記憶體已用的空間為:4 MB
JVM記憶體的空閒空間為:124 MB
JVM總記憶體空間為:128 MB
JVM總記憶體最大堆空間為:2024 MB
第5次生產512大小的物件
清理list.... 回收jvm記憶體....
JVM記憶體已用的空間為:4 MB
JVM記憶體的空閒空間為:124 MB
JVM總記憶體空間為:128 MB
JVM總記憶體最大堆空間為:2024 MB
第6次生產512大小的物件
清理list.... 回收jvm記憶體....
JVM記憶體已用的空間為:4 MB
JVM記憶體的空閒空間為:124 MB
JVM總記憶體空間為:128 MB
JVM總記憶體最大堆空間為:2024 MB
第7次生產512大小的物件
清理list.... 回收jvm記憶體....
JVM記憶體已用的空間為:4 MB
JVM記憶體的空閒空間為:124 MB
JVM總記憶體空間為:128 MB
JVM總記憶體最大堆空間為:2024 MB
第8次生產512大小的物件
清理list.... 回收jvm記憶體....
JVM記憶體已用的空間為:4 MB
JVM記憶體的空閒空間為:124 MB
JVM總記憶體空間為:128 MB
JVM總記憶體最大堆空間為:2024 MB
第9次生產512大小的物件
清理list.... 回收jvm記憶體....
JVM記憶體已用的空間為:4 MB
JVM記憶體的空閒空間為:124 MB
JVM總記憶體空間為:128 MB
JVM總記憶體最大堆空間為:2024 MB
- 檢視jmap heap 資訊:
C:\Users>jmap -heap 18112
Attaching to process ID 18112, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.161-b12
using thread-local object allocation.
Garbage-First (G1) GC with 4 thread(s)
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 2122317824 (2024.0MB)
NewSize = 1363144 (1.2999954223632812MB)
MaxNewSize = 1272971264 (1214.0MB)
OldSize = 5452592 (5.1999969482421875MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 1048576 (1.0MB)
Heap Usage:
G1 Heap:
regions = 2024
capacity = 2122317824 (2024.0MB)
used = 8336616 (7.950416564941406MB)
free = 2113981208 (2016.0495834350586MB)
0.39280714253663074% used
G1 Young Generation:
Eden Space:
regions = 2
capacity = 83886080 (80.0MB)
used = 2097152 (2.0MB)
free = 81788928 (78.0MB)
2.5% used
Survivor Space:
regions = 0
capacity = 0 (0.0MB)
used = 0 (0.0MB)
free = 0 (0.0MB)
0.0% used
G1 Old Generation:
regions = 11
capacity = 50331648 (48.0MB)
used = 6239464 (5.950416564941406MB)
free = 44092184 (42.049583435058594MB)
12.396701176961264% used
6706 interned Strings occupying 573840 bytes.
通過統計圖和控制檯日誌,可以看到在執行41秒左右前,使用記憶體呈直線平滑上升,開闢的記憶體也是呈直線平滑上升。當使用記憶體到達530m時,程式發起了System.gc(),垃圾被回收,因此使用記憶體回到了10m。此時會發現神奇的現象出來了,jvm之前開闢出來的剩餘記憶體空間全部歸還給了作業系統,記憶體回到了我們指定的初始jvm堆大小128m。通過多次執行生產物件對比發現,jvm都是在每一次呼叫System.gc()後全部歸還實體記憶體,不做任何保留。達到了我期望的效果!
總結:
CMS垃圾回收器,在記憶體開闢後,會隨著System.gc()執行次數逐漸增多和回收頻率逐漸拉長,從繼續開闢記憶體到慢慢歸還實體記憶體給作業系統,直到出現一次全部歸還,就會在每次呼叫System.gc()都歸還所有剩餘的實體記憶體給作業系統;G1恰恰相反,G1是在JVM每次回收垃圾後,主動歸還實體記憶體給作業系統,不做任何保留,大大降低了記憶體佔用。
另外,檢視java堆疊實時情況,推薦使用JProfiler和VisualVM。如果是本地推薦JProfiler,因為功能強大,不過遠端配置麻煩;如果是連遠端java程序,推薦VisualVM,功能夠用,連線遠端只需配置一些jvm引數。
其它說明
JDK 12將有G1收集器,將記憶體返回到作業系統(不呼叫System.gc)“應用程式空閒時”
jdk9 增加了這個jvm引數:
-XX:+ShrinkHeapInSteps
使Java堆漸進地縮小到目標大小,該選項預設開啟,經過多次GC後堆縮小到目標大小;如果關閉該選項,那麼GC後Java堆將立即縮小到目標大小。如果希望最小化Java堆大小,可以關閉改選項,並配合以下選項:
-XX:MaxHeapFreeRatio=10 -XX:MinHeapFreeRatio=5
這樣將保持Java堆空間較小,並減少程式的動態佔用空間,這對嵌入式應用非常有用,但對於一般應用,可能降低效能。
參考資料:
http://www.imooc.com/wenda/detail/574044
https://developer.ibm.com/cn/blog/2017/still-paying-unused-memory-java-app-idle/
https://gameinstitute.qq.com/community/detail/118528
https://www.zhihu.com/question/30813753
https://www.zhihu.com/question/29161