前言:
公司有一個資產統計系統,使用頻率很低,但是要求在使用時查詢速度快,因此想到做一些緩存放在內存中,在長時間沒有使用,持久化到磁盤中,並對垃圾進行回收,歸還物理內存給操作系統,從而節省寶貴資源給其它業務系統。當我做好緩存時,卻發現了一個棘手的問題,通過程序釋放資源並通知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/29161424