暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

JVM调优之探索CMS和G1的物理内存归还机制

熊峰的博客 2019-07-24
400

熊峰的博客

Java技术点滴分享

关注


前言:

公司有一个资产统计系统,使用频率很低,但是要求在使用时查询速度快,因此想到做一些缓存放在内存中,在长时间没有使用,就持久化到磁盘中,并对垃圾进行回收,归还物理内存给操作系统,从而节省宝贵资源给其它业务系统。当我做好缓存时,却发现了一个棘手的问题,通过程序释放资源并通知GC回收资源后,堆内存的已用内存减少了,空闲内存增加了,可是进程占用系统内存却没有减少。查阅了很多资料,也尝试过很多次,都没有完美解决问题。直到后来看到一段评论谈及G1垃圾回收器,才恍然大悟。

接下来,通过一个小demo给大家演示一下两种垃圾回收器对物理内存归还的区别。如果有什么不对的地方,希望大家能够在评论里面指正。

  • 堆大小配置:

  1. -Xms128M -Xmx2048M

先附上测试代码:

  1. import org.junit.Test;


  2. import java.util.ArrayList;

  3. import java.util.List;


  4. public class MemoryRecycleTest {


  5. @Test

  6. public void testMemoryRecycle() throws InterruptedException {


  7. List list = new ArrayList();


  8. //指定要生产的对象大小为512m

  9. int count = 512;


  10. //新建一条线程,负责生产对象

  11. new Thread(() -> {

  12. try {

  13. for (int i = 1; i <= 10; i++) {

  14. System.out.println(String.format("第%s次生产%s大小的对象", i, count));

  15. addObject(list, count);

  16. //休眠40秒

  17. Thread.sleep(i * 10000);

  18. }

  19. } catch (InterruptedException e) {

  20. e.printStackTrace();

  21. }

  22. }).start();


  23. //新建一条线程,负责清理list,回收jvm内存

  24. new Thread(() -> {

  25. for (;;) {

  26. //当list内存到达512m,就通知gc回收堆

  27. if (list.size() >= count) {

  28. System.out.println("清理list.... 回收jvm内存....");

  29. list.clear();

  30. //通知gc回收

  31. System.gc();

  32. //打印堆内存信息

  33. printJvmMemoryInfo();

  34. }

  35. }

  36. }).start();


  37. //阻止程序退出

  38. Thread.currentThread().join();

  39. }


  40. public void addObject(List list, int count) {

  41. for (int i = 0; i < count; i++) {

  42. OOMobject ooMobject = new OOMobject();

  43. //向list添加一个1m的对象

  44. list.add(ooMobject);

  45. try {

  46. //休眠100毫秒

  47. Thread.sleep(100);

  48. } catch (InterruptedException e) {

  49. e.printStackTrace();

  50. }

  51. }

  52. }


  53. public static class OOMobject{

  54. //生成1m的对象

  55. private byte[] bytes=new byte[1024*1024];

  56. }


  57. public static void printJvmMemoryInfo() {

  58. // 虚拟机级内存情况查询

  59. long vmFree = 0;

  60. long vmUse = 0;

  61. long vmTotal = 0;

  62. long vmMax = 0;

  63. int byteToMb = 1024 * 1024;

  64. Runtime rt = Runtime.getRuntime();

  65. vmTotal = rt.totalMemory() / byteToMb;

  66. vmFree = rt.freeMemory() / byteToMb;

  67. vmMax = rt.maxMemory() / byteToMb;

  68. vmUse = vmTotal - vmFree;

  69. System.out.println("");

  70. System.out.println("JVM内存已用的空间为:" + vmUse + " MB");

  71. System.out.println("JVM内存的空闲空间为:" + vmFree + " MB");

  72. System.out.println("JVM总内存空间为:" + vmTotal + " MB");

  73. System.out.println("JVM总内存最大堆空间为:" + vmMax + " MB");

  74. System.out.println("");

  75. }


  76. }

首先使用CMS垃圾回收器:

  • 将jvm运行参数设置为如下:

  1. -Xms128M -Xmx2048M -XX:+UseConcMarkSweepGC

  • 运行程序后,使用JProfiler查看堆内存情况:

  • 查看控制台打印的内容:

  1. 1次生产512大小的对象

  2. 清理list.... 回收jvm内存....


  3. JVM内存已用的空间为:6 MB

  4. JVM内存的空闲空间为:936 MB

  5. JVM总内存空间为:942 MB

  6. JVM总内存最大堆空间为:1990 MB


  7. 2次生产512大小的对象

  8. 清理list.... 回收jvm内存....


  9. JVM内存已用的空间为:4 MB

  10. JVM内存的空闲空间为:1025 MB

  11. JVM总内存空间为:1029 MB

  12. JVM总内存最大堆空间为:1990 MB


  13. 3次生产512大小的对象

  14. 清理list.... 回收jvm内存....


  15. JVM内存已用的空间为:4 MB

  16. JVM内存的空闲空间为:680 MB

  17. JVM总内存空间为:684 MB

  18. JVM总内存最大堆空间为:1990 MB


  19. 4次生产512大小的对象

  20. 清理list.... 回收jvm内存....


  21. JVM内存已用的空间为:4 MB

  22. JVM内存的空闲空间为:119 MB

  23. JVM总内存空间为:123 MB

  24. JVM总内存最大堆空间为:1990 MB


  25. 5次生产512大小的对象

  26. 清理list.... 回收jvm内存....


  27. JVM内存已用的空间为:4 MB

  28. JVM内存的空闲空间为:119 MB

  29. JVM总内存空间为:123 MB

  30. JVM总内存最大堆空间为:1990 MB


  31. 6次生产512大小的对象

  32. 清理list.... 回收jvm内存....


  33. JVM内存已用的空间为:4 MB

  34. JVM内存的空闲空间为:119 MB

  35. JVM总内存空间为:123 MB

  36. JVM总内存最大堆空间为:1990 MB


  37. 7次生产512大小的对象

  38. 清理list.... 回收jvm内存....


  39. JVM内存已用的空间为:4 MB

  40. JVM内存的空闲空间为:119 MB

  41. JVM总内存空间为:123 MB

  42. JVM总内存最大堆空间为:1990 MB


  43. 8次生产512大小的对象

  44. 清理list.... 回收jvm内存....


  45. JVM内存已用的空间为:4 MB

  46. JVM内存的空闲空间为:119 MB

  47. JVM总内存空间为:123 MB

  48. JVM总内存最大堆空间为:1990 MB


  49. 9次生产512大小的对象

  50. 清理list.... 回收jvm内存....


  51. JVM内存已用的空间为:4 MB

  52. JVM内存的空闲空间为:119 MB

  53. JVM总内存空间为:123 MB

  54. JVM总内存最大堆空间为:1990 MB

  • 查看jmap heap 信息:

  1. C:\Users>jmap -heap 4716

  2. Attaching to process ID 4716, please wait...

  3. Debugger attached successfully.

  4. Server compiler detected.

  5. JVM version is 25.161-b12


  6. using parallel threads in the new generation.

  7. using thread-local object allocation.

  8. Concurrent Mark-Sweep GC


  9. Heap Configuration:

  10. MinHeapFreeRatio = 40

  11. MaxHeapFreeRatio = 70

  12. MaxHeapSize = 2122317824 (2024.0MB)

  13. NewSize = 44695552 (42.625MB)

  14. MaxNewSize = 348913664 (332.75MB)

  15. OldSize = 89522176 (85.375MB)

  16. NewRatio = 2

  17. SurvivorRatio = 8

  18. MetaspaceSize = 21807104 (20.796875MB)

  19. CompressedClassSpaceSize = 1073741824 (1024.0MB)

  20. MaxMetaspaceSize = 17592186044415 MB

  21. G1HeapRegionSize = 0 (0.0MB)


  22. Heap Usage:

  23. New Generation (Eden + 1 Survivor Space):

  24. capacity = 280887296 (267.875MB)

  25. used = 1629392 (1.5539093017578125MB)

  26. free = 279257904 (266.3210906982422MB)

  27. 0.5800874668251284% used

  28. Eden Space:

  29. capacity = 249692160 (238.125MB)

  30. used = 1629392 (1.5539093017578125MB)

  31. free = 248062768 (236.5710906982422MB)

  32. 0.6525603366961942% used

  33. From Space:

  34. capacity = 31195136 (29.75MB)

  35. used = 0 (0.0MB)

  36. free = 31195136 (29.75MB)

  37. 0.0% used

  38. To Space:

  39. capacity = 31195136 (29.75MB)

  40. used = 0 (0.0MB)

  41. free = 31195136 (29.75MB)

  42. 0.0% used

  43. concurrent mark-sweep generation:

  44. capacity = 624041984 (595.1328125MB)

  45. used = 4169296 (3.9761505126953125MB)

  46. free = 619872688 (591.1566619873047MB)

  47. 0.6681114583470076% used


  48. 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垃圾回收器:

  1. -Xms128M -Xmx2048M -XX:+UseG1GC

  • 运行程序后,使用JProfiler查看堆内存情况:

  • 查看控制台打印的内容:

  1. 1次生产512大小的对象

  2. 清理list.... 回收jvm内存....


  3. JVM内存已用的空间为:5 MB

  4. JVM内存的空闲空间为:123 MB

  5. JVM总内存空间为:128 MB

  6. JVM总内存最大堆空间为:2024 MB


  7. 2次生产512大小的对象

  8. 清理list.... 回收jvm内存....


  9. JVM内存已用的空间为:4 MB

  10. JVM内存的空闲空间为:124 MB

  11. JVM总内存空间为:128 MB

  12. JVM总内存最大堆空间为:2024 MB


  13. 3次生产512大小的对象

  14. 清理list.... 回收jvm内存....


  15. JVM内存已用的空间为:4 MB

  16. JVM内存的空闲空间为:124 MB

  17. JVM总内存空间为:128 MB

  18. JVM总内存最大堆空间为:2024 MB


  19. 4次生产512大小的对象

  20. 清理list.... 回收jvm内存....


  21. JVM内存已用的空间为:4 MB

  22. JVM内存的空闲空间为:124 MB

  23. JVM总内存空间为:128 MB

  24. JVM总内存最大堆空间为:2024 MB


  25. 5次生产512大小的对象

  26. 清理list.... 回收jvm内存....


  27. JVM内存已用的空间为:4 MB

  28. JVM内存的空闲空间为:124 MB

  29. JVM总内存空间为:128 MB

  30. JVM总内存最大堆空间为:2024 MB


  31. 6次生产512大小的对象

  32. 清理list.... 回收jvm内存....


  33. JVM内存已用的空间为:4 MB

  34. JVM内存的空闲空间为:124 MB

  35. JVM总内存空间为:128 MB

  36. JVM总内存最大堆空间为:2024 MB


  37. 7次生产512大小的对象

  38. 清理list.... 回收jvm内存....


  39. JVM内存已用的空间为:4 MB

  40. JVM内存的空闲空间为:124 MB

  41. JVM总内存空间为:128 MB

  42. JVM总内存最大堆空间为:2024 MB


  43. 8次生产512大小的对象

  44. 清理list.... 回收jvm内存....


  45. JVM内存已用的空间为:4 MB

  46. JVM内存的空闲空间为:124 MB

  47. JVM总内存空间为:128 MB

  48. JVM总内存最大堆空间为:2024 MB


  49. 9次生产512大小的对象

  50. 清理list.... 回收jvm内存....


  51. JVM内存已用的空间为:4 MB

  52. JVM内存的空闲空间为:124 MB

  53. JVM总内存空间为:128 MB

  54. JVM总内存最大堆空间为:2024 MB

  • 查看jmap heap 信息:

  1. C:\Users>jmap -heap 18112

  2. Attaching to process ID 18112, please wait...

  3. Debugger attached successfully.

  4. Server compiler detected.

  5. JVM version is 25.161-b12


  6. using thread-local object allocation.

  7. Garbage-First (G1) GC with 4 thread(s)


  8. Heap Configuration:

  9. MinHeapFreeRatio = 40

  10. MaxHeapFreeRatio = 70

  11. MaxHeapSize = 2122317824 (2024.0MB)

  12. NewSize = 1363144 (1.2999954223632812MB)

  13. MaxNewSize = 1272971264 (1214.0MB)

  14. OldSize = 5452592 (5.1999969482421875MB)

  15. NewRatio = 2

  16. SurvivorRatio = 8

  17. MetaspaceSize = 21807104 (20.796875MB)

  18. CompressedClassSpaceSize = 1073741824 (1024.0MB)

  19. MaxMetaspaceSize = 17592186044415 MB

  20. G1HeapRegionSize = 1048576 (1.0MB)


  21. Heap Usage:

  22. G1 Heap:

  23. regions = 2024

  24. capacity = 2122317824 (2024.0MB)

  25. used = 8336616 (7.950416564941406MB)

  26. free = 2113981208 (2016.0495834350586MB)

  27. 0.39280714253663074% used

  28. G1 Young Generation:

  29. Eden Space:

  30. regions = 2

  31. capacity = 83886080 (80.0MB)

  32. used = 2097152 (2.0MB)

  33. free = 81788928 (78.0MB)

  34. 2.5% used

  35. Survivor Space:

  36. regions = 0

  37. capacity = 0 (0.0MB)

  38. used = 0 (0.0MB)

  39. free = 0 (0.0MB)

  40. 0.0% used

  41. G1 Old Generation:

  42. regions = 11

  43. capacity = 50331648 (48.0MB)

  44. used = 6239464 (5.950416564941406MB)

  45. free = 44092184 (42.049583435058594MB)

  46. 12.396701176961264% used


  47. 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)“应用程序空闲时”

  1. jdk9 增加了这个jvm参数:


  2. -XX:+ShrinkHeapInSteps

  3. 使Java堆渐进地缩小到目标大小,该选项默认开启,经过多次GC后堆缩小到目标大小;如果关闭该选项,那么GCJava堆将立即缩小到目标大小。如果希望最小化Java堆大小,可以关闭改选项,并配合以下选项:


  4. -XX:MaxHeapFreeRatio=10 -XX:MinHeapFreeRatio=5


  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


想了解更多?
那就赶紧来关注我

长按二维码 关注我们

推送java优质文章、视频教程
微信公众号:熊峰的博客


文章转载自熊峰的博客,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论