1. 程式人生 > >JVM調優之探索CMS和G1的實體記憶體歸還機制

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