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
image
  • 运行程序后,使用JProfiler查看堆内存情况:
image
  • 查看控制台打印的内容:
第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
image
  • 运行程序后,使用JProfiler查看堆内存情况:
image
  • 查看控制台打印的内容:
第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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容