在查看系统内存监控的过程中,发现有几台机器的内存使用率一直很高,而且是呈现一个不太正常的高度,初始以为是 GC 不完全,也就是 JVM 内有大量对象不能回收,于是采用 Arthas 诊断查看一下机器的 JVM 使用情况。
这是挑选的一台机器查看的 JVM 使用情况,上图截图部分为内存使用情况。主要看第一个 HEAP-MEMORY-USAGE。这是 jvm 中堆的使用情况,init 为堆初始化 3.0 GiB,used 为已经使用了 598 MiB,committed 为已提交内存,即空闲内存+使用内存,但是一直被 JVM 占用,并没有归还给操作系统,这就造成了我们在监控上看的时候这台机器的内存占用率一直高居不下的主要原因之一。
按照正常业务理解,Jvm 在触发 GC 后回收的空闲内存应该会释放一部分给操作系统,但实际上并没有这么做,下面通过一段代码测试验证 CMS 和 G1 的物理内存归还机制。
测试
在测试中,需要至少两个线程,一个用来不断的创建大对象,一个用来手动触发系统 GC。同时采用 JProfiler 监控 JVM 堆内存变化。
测试代码如下:
1 import java.util.ArrayList; 2 import java.util.List; 3 4 /** 5 * @Author: Li.Jincheng 6 * @Date: 2022/1/24 18:36 7 * @Description: 8 */ 9 public class JvmMemoryTest { 10 static volatile List<BigObject> list = new ArrayList<>(); 11 12 public static void main(String[] args) { 13 int count = 512; 14 Thread createObjectThread = new Thread(() -> { 15 try { 16 for (int i = 1; i <= 10; i++) { 17 System.out.println(String.format("第%s次生产%s大小的对象", i, count)); 18 add(list, count); 19 //休眠10秒 20 Thread.sleep(10000); 21 } 22 } catch (InterruptedException e) { 23 e.printStackTrace(); 24 } 25 }); 26 27 Thread clearThread = new Thread(() -> { 28 while (true) { 29 if (list.size() < count) { 30 continue; 31 } 32 //当List内存到达512M,就通知GC 33 System.out.println("清理list.... 回收jvm内存...."); 34 list.clear(); 35 //GC 36 System.gc(); 37 //打印堆内存信息 38 printJvmMemoryInfo(); 39 } 40 }); 41 42 // 启动线程 43 createObjectThread.start(); 44 clearThread.start(); 45 46 try { 47 Thread.currentThread().join(); 48 } catch (InterruptedException e) { 49 e.printStackTrace(); 50 } 51 } 52 53 /** 54 * 打印Jvm内存情况 55 */ 56 public static void printJvmMemoryInfo() { 57 //虚拟机级内存情况查询 58 int byteToMb = 1024 * 1024; 59 Runtime runtime = Runtime.getRuntime(); 60 long vmTotal = runtime.totalMemory() / byteToMb; 61 long vmFree = runtime.freeMemory() / byteToMb; 62 long vmMax = runtime.maxMemory() / byteToMb; 63 long vmUse = vmTotal - vmFree; 64 System.out.println("##############"); 65 System.out.println("JVM内存已用的空间为:" + vmUse + " MB"); 66 System.out.println("JVM内存的空闲空间为:" + vmFree + " MB"); 67 System.out.println("JVM总内存空间为:" + vmTotal + " MB"); 68 System.out.println("JVM总内存最大堆空间为:" + vmMax + " MB"); 69 System.out.println("##############"); 70 } 71 72 /** 73 * 创建大对象列表 74 * 75 * @param list 76 * @param count 77 */ 78 public static void add(List<BigObject> list, int count) { 79 for (int i = 0; i < count; i++) { 80 BigObject bigObject = new BigObject(); 81 list.add(bigObject); 82 try { 83 Thread.sleep(50); 84 } catch (InterruptedException e) { 85 e.printStackTrace(); 86 } 87 } 88 } 89 90 public static class BigObject { 91 //生成5M的对象 92 private byte[] bytes = new byte[1024 * 1024 * 5]; 93 } 94 }
CMS 垃圾回收器
配置:
-Xms128M -Xmx2048M -XX:+UseConcMarkSweepGC
结果:
如上图所示,蓝色部分为实际使用内存,绿色部分为空闲内存,从图上可以看出在开始的时候即使触发了 GC 操作,JVM 回收了内存,但是并没有立即将空闲内存归还操作系统。在第三次 gc 后,可以明显看到 JVM 的空闲空间明显下降,这表明 JVM 已经归还了一部分内存,后面,随着 GC 次数增加慢慢的将内存归还。也就是说,CMS 垃圾回收器最终也会将申请的内存归还操作系统。
G1 垃圾回收器
配置:
-Xms128M -Xmx2048M -XX:+UseG1GC
结果:
从上图可以看出,每触发一次 GC,JVM 的使用内存和空闲内存总和都降到了初始值 128M,也就是说在使用 G1 垃圾回收器时,每次 GC 都会将 JVM 新申请开辟的空间归还给操作系统。这也是一开始我们理解的垃圾回收机制以及预期结果。
总结
CMS 垃圾回收器,在 JVM 申请内存后,会随着 GC 次数增加和频率足见拉长,从继续申请内存到慢慢归还给操作系统,知道如图所示出现一次全部归还后,每一次的 GC 都会将剩余空间归还操作系统;
G1 垃圾回收器与之相反,每次 GC 后都会将内存全部归还操作系统,大大降低了机器的内存占用。