前言:

公司有一个资产统计系统,使用频率很低,但是要求在使用时查询速度快,因此想到做一些缓存放在内存中,在长时间没有使用,持久化到磁盘中,并对垃圾进行回收,归还物理内存给操作系统,从而节省宝贵资源给其它业务系统。当我做好缓存时,却发现了一个棘手的问题,通过程序释放资源并通知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

JVM调优之探索CMS和G1的物理内存归还机制的更多相关文章

  1. JVM调优(二)经验参数设置

    调优设置具体解析 堆大小设置 JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制:系统的可用虚拟内存限制:系统的可用物理内存限制.32位系统下,一般限制在1.5 ...

  2. 深入理解JAVA虚拟机(内存模型+GC算法+JVM调优)

    目录 1.Java虚拟机内存模型 1.1 程序计数器 1.2 Java虚拟机栈 局部变量 1.3 本地方法栈 1.4 Java堆 1.5 方法区(永久区.元空间) 附图 2.JVM内存分配参数 2.1 ...

  3. jvm调优的分类

    本文部分内容出自https://blog.csdn.net/yang_net/article/details/5830820 调优步骤: 衡量系统现状. 设定调优目标. 寻找性能瓶颈. 性能调优. 衡 ...

  4. JVM调优之经验

    在生产系统中,高吞吐和低延迟一直都是JVM调优的最终目标,但这两者恰恰又是相悖的,鱼和熊掌不可兼得,所以在调优之前要清楚舍谁而取谁.一般计算任务和组件服务会偏向高吞吐,而web展示则偏向低延迟才会带来 ...

  5. 网络摘抄-深入浅出JVM调优

    基本概念: JVM把内存区分为堆区(heap).栈区(stack)和方法区(method).由于本文主要讲解JVM调优,因此我们可以简单的理解为,JVM中的堆区中存放的是实际的对象,是需要被GC的.其 ...

  6. JVM调优和深入了解性能优化

    JVM调优的本质: 并不是显著的提高系统性能,不是说你调了,性能就能提升几倍或者上十倍,JVM调优,主要调的是稳定.如果你的系统出现了频繁的垃圾回收,这个时候系统是不稳定的,所以需要我们来进行JVM调 ...

  7. JVM调优参数、方法、工具以及案例总结

    这种文章挺难写的,一是JVM参数巨多,二是内容枯燥乏味,但是想理解JVM调优又是没法避开的环节,本文主要用来总结梳理便于以后翻阅,主要围绕四个大的方面展开,分别是JVM调优参数.JVM调优方法(流程) ...

  8. 【JVM进阶之路】十:JVM调优总结

    1.调优原则 JVM调优听起来很高大上,但是要认识到,JVM调优应该是Java性能优化的最后一颗子弹. 比较认可廖雪峰老师的观点,要认识到JVM调优不是常规手段,性能问题一般第一选择是优化程序,最后的 ...

  9. JVM调优基础到进阶

    GC和GC Tuning GC的基础知识 1.什么是垃圾 C语言申请内存:malloc free C++: new delete c/C++ 手动回收内存 Java: new ? 自动内存回收,编程上 ...

随机推荐

  1. c# RedisHelper

    使用redis组件如下,至于为什么使用3.9版本,是因为4.0开始商业了,限制了次数 ServiceStack.Common" version="3.9.70"Servi ...

  2. Google Protocol Buffer 的使用和原理(无论对存储还是数据交换,都是个挺有用的东西,有9张图做说明,十分清楚)

    感觉Google Protocol Buffer无论对存储还是数据交换,都是个挺有用的东西,这里记录下,以后应该用得着.下文转自: http://www.ibm.com/developerworks/ ...

  3. java关键字之synchronized

    1.synchronized可以用了修饰一个普通方法,或者代码块,这个时候synchronized锁定的是当前对象,只要有一个线程在访问对应的方法或代码块,其他线程必须等待.2.synchronize ...

  4. SpringBoot实现文件上传

    前言参考:快速开发第一个SpringBoot应用 这篇文章会讲解如何使用SpringBoot完成一个文件上传的过程,并且附带一些SpringBoot开发中需要注意的地方 首先我们写一个文件上传的htm ...

  5. Windows下OSGEarth的编译过程

    目录 1. 依赖 1) OpenSceneGraph 2) GDAL 3) CURL 4) GEOS 5) 其他 2. 编译 1) 设置参数 2) 配置路径 3) 生成编译 3. 参考文献 1. 依赖 ...

  6. Metasploit实现木马生成、捆绑、免杀

    原创博客,转载请注出处! 我的公众号,正在建设中,欢迎关注: Meatsploit介绍 2018/01/03 更新 Metasploit是一款优秀的开源(!= 完全免费)渗透测试框架平台,在该平台下可 ...

  7. .Net将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),并使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA

    前言: 前段时间因为项目进度比较繁重所以一直都没有时间更新博客,内心深深的负重感,没有履行年初立下的flag.不过这个月会把上个月没有完成的任务补上来,咱们可不是喜欢拖欠任务的攻城狮.哈哈,废话不多说 ...

  8. 获取Class的实例

    //1.调用运行时类本身的.class属性 Class<Person> class1 = Person.class; System.out.println(class1); //2.通过运 ...

  9. 基于cxf开发的WebService

    Node.jshttps://www.cnblogs.com/goldlong/p/8027997.htmlQQ音乐apihttps://juejin.im/post/5a35228e51882506 ...

  10. CentOS 7更新系统时间

    Linux系统在安装的时候,总是会出现时区,时间的错误. 将Linux系统时间和本地区网络时间同步,ntpdate可以从网络同步时间, 需要安装sudo yum install ntp ntpdate ...