然后我赶紧看了下监控,一切正常,距离上次发版好几天了,FULL GC 一次没有,YoungGC,十分钟一次,堆空闲也很充足。
不过说完之后,内心还是自我质疑了一下:会不会真有什么bug?难道是堆外泄露?线程没销毁?导致内存泄露了???
迅速上登上服务器又仔细的查看了各种指标,Heap/GC/Thread/Process 之类的,发现一切正常,并没有什么“泄漏”的迹象。
和运维的“沟通”
我心想这运维怕不是个der,JVM GC 回收和进程内存又不是一回事,不过还是和得他解释一下,不然一直baba个没完
虽然我内心是拒绝的,但得罪谁也不能得罪运维啊,想想还是给大哥解释解释,“增进下感情”
操作系统 与 JVM的内存分配
JVM 的自动内存管理,其实只是先向操作系统申请了一大块内存,然后自己在这块已申请的内存区域中进行“自动内存管理”。JAVA 中的对象在创建前,会先从这块申请的一大块内存中划分出一部分来给这个对象使用,在 GC 时也只是这个对象所处的内存区域数据清空,标记为空闲而已
为什么不把内存归还给操作系统?
JVM 还是会归还内存给操作系统的,只是因为这个代价比较大,所以不会轻易进行。而且不同垃圾回收器 的内存分配算法不同,归还内存的代价也不同。
比如在清除算法(sweep)中,是通过空闲链表(free-list)算法来分配内存的。简单的说就是将已申请的大块内存区域分为 N 个小区域,将这些区域同链表的结构组织起来,就像这样:
每个 data 区域可以容纳 N 个对象,那么当一次 GC 后,某些对象会被回收,可是此时这个 data 区域中还有其他存活的对象,如果想将整个 data 区域释放那是肯定不行的。
所以这个归还内存给操作系统的操作并没有那么简单,执行起来代价过高,JVM 自然不会在每次 GC 后都进行内存的归还。
怎么归还?
虽然代价高,但 JVM 还是提供了这个归还内存的功能。JVM 提供了-XX:MinHeapFreeRatio
和-XX:MaxHeapFreeRatio
两个参数,用于配置这个归还策略。
- MinHeapFreeRatio 代表当空闲区域大小下降到该值时,会进行扩容,扩容的上限为
Xmx
- MaxHeapFreeRatio 代表当空闲区域超过该值时,会进行“缩容”,缩容的下限为
Xms
不过虽然有这个归还的功能,不过因为这个代价比较昂贵,所以 JVM 在归还的时候,是线性递增归还的,并不是一次全部归还。
但是但是但是,经过实测,这个归还内存的机制,在不同的垃圾回收器,甚至不同的 JDK 版本中还不一样!
不同版本&垃圾回收器下的表现不同
下面是我之前跑过的测试结果:
public static void main(String[] args) throws IOException, InterruptedException {
List<Object> dataList = new ArrayList<>();
for (int i = 0; i < 25; i++) {
byte[] data = createData(1024 * 1024 * 40);// 40 MB
dataList.add(data);
}
Thread.sleep(10000);
dataList = null; // 待会 GC 直接回收
for (int i = 0; i < 100; i++) {
// 测试多次 GC
System.gc();
Thread.sleep(1000);
}
System.in.read();
}
public static byte[] createData(int size){
byte[] data = new byte[size];
for (int i = 0; i < size; i++) {
data[i] = Byte.MAX_VALUE;
}
return data;
}
JAVA 8 | UseParallelGC(ParallerGC + ParallerOld) | -Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40 | 否 |
JAVA 8 | CMS+ParNew | -Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC | 是 |
JAVA 8 | UseG1GC(G1) | -Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40 -XX:+UseG1GC | 是 |
JAVA 11 | UseG1GC(G1) | -Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40 | 是 |
JAVA 16 | UseZGC(ZGC) | -Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40 -XX:+UseZGC | 否 |
测试结果刷新了我的认知。,MaxHeapFreeRatio 这个参数好像并没有什么用,无论我是配置40,还是配置90,回收的比例都有和实际的结果都有很大差距。
但是文档中,可不是这么说的……
而且 ZGC 的结果也是挺意外的,JEP 351 提到了 ZGC 会将未使用的内存释放,但测试结果里并没有。
除了以上测试结果,stackoverflow 上还有一些其他的说法,我就没有再一一测试了
- JAVA 9 后
-XX:-ShrinkHeapInSteps
参数,可以让 JVM 已非线性递增的方式归还内存 - JAVA 12 后的 G1,再应用空闲时,可以自动的归还内存
所以,官方文档的说法,也只能当作一个参考,JVM 并没有过多的透露这个实现细节。
不过这个是否归还的机制,除了这位“热情”的运维老哥,一般人也不太会去关心,巴不得 JVM 多用点内存,少 GC 几回……
而且别说空闲自动归还了,我们希望的是一启动就分配个最大内存,避免它运行中扩容影响服务;所以一般 JAVA 程序还会将 Xms
和Xmx
配置为相等的大小,避免这个扩容的操作。
Xms6G,为什么启动之后 used 才 200M?
进程在申请内存时,并不是直接分配物理内存的,而是分配一块虚拟空间,到真正堆这块虚拟空间写入数据时才会通过缺页异常(Page Fault)处理机制分配物理内存,也就是我们看到的进程 Res 指标。
可以简单的认为操作系统的内存分配是“惰性”的,分配并不会发生实际的占用,有数据写入时才会发生内存占用,影响 Res。
所以,哪怕配置了Xms6G
,启动后也不会直接占用 6G 内存,只是 JVM 在启动后会malloc
6G 而已,但实际占用的内存取决于你有没有往这 6G 内存区域中写数据的。
总结
对于大多数服务端场景来说,并不需要JVM 这个手动释放内存的操作。至于 JVM 是否归还内存给操作系统这个问题,我们也并不关心。而且基于上面那个测试结果,不同 JAVA 版本,不同垃圾回收器版本区别这么大,更是没必要去深究了。
综上,JVM 虽然可以释放空闲内存给操作系统,但是不一定会释放,在不同 JAVA 版本,不同垃圾回收器版本下表现不同,知道有这个机制就行。