xms/xmx和RES关系
几个名词解释
xms : JVM初始堆内存大小。
xmx : JVM最大堆内存大小。
xmn : JVM新生代内存大小。
RES : Linux下TOP命令的RES内存占用,为常驻内存,或者叫做物理内存。
VIRT : Linux下TOP命令的VIRT内存占用,为虚拟内存。
SHR : Linux下TOP命令的SHR内存占用,为共享内存
为什么建议xms=xmx
目的是获得一个稳定的堆大小。当xms=xmx的时候,运行时,理论上堆大小恒定不变。稳定的堆空间,可以减少GC次数。但相应的,会增加GC时间。一般情况下,GC次数越少,系统受到的影响越小,因为减少了STW的次数。
为什么RES会小于xms
这种一般发生在java进行启动不久的时候(当然也可能一直小于xms),
比如我们以下面这个命令启动halo这个博客:Xms我们指定的是512M。
/opt/soft/jdk18/jdk1.8.0_101/bin/java -Xms512m -Xmx512m -jar halo-1.2.0.jar
然后我们top看下这个进程的RES内存占用:
图中,发现RES=391M
,实际上是小于设置的Xms的512M
的。
原因在于:xms表示的是java进程向操作系统申请的虚拟内存值,而RES代表的是实际使用的内存值。
-
申请并不一定意味着要实际使用。
只是向系统提前申请,系统会为其分配虚拟地址空间,但该空间并未直接映射到真实的物理内存地址。
-
只有当实际使用时,才会分配物理内存。
实际使用时,只有当出现了缺页中断时,才会生成真正的物理内存地址映射,此时才是真正的物理内存占用。
所以这就是我们看到的,刚启动不久的java进程的RES值小于xms的主要原因。
关于虚拟内存和物理内存的关系的一篇介绍:https://blog.csdn.net/u012861978/article/details/53048077
为什么RES会大于xms
java进程运行一段时间之后,我们会发现此时的RES可能已经大于xms了。比如阿里云服务器上的监控程序,设定的xms是16M,xmx是32M。
通过top查看RES使用情况,发现RES为75M,是大于xmx和xms的。
原因在于RES表示的是java进程实际使用的物理内存值,而xms和xmx表示的仅是JVM的堆内存大小。
Java进程实际使用内存和什么有关
Java进程内存主要包含以下几点(PS:包含但不仅限于这几部分)
- 堆内存(新生代、老年代)
- JDK8之前永久代PermGen/JDK8之后元空间Metaspace
- 线程栈内存占用(Java线程栈内存和非java线程栈内存)
- 直接内存(Direct Memory)(Java NIO)
- JVM程序自身占用内存(Native Memory)
- codecache
PS:
直接内存不受Java堆内存大小限制,受本机总内存及处理器寻址空间限制。
codecache一般占用很少,可以不考虑。(JVM生成的native code存放的内存空间称作code cache, JIT编译和JNI编译的代码都放在这里)
Java线程栈内存=java线程数 * Xss
非java线程栈内存=非java线程数*线程栈大小
所以,这就可以解释为什么很多时候RES是大于Xms值了。
为何GC后不释放RES
一般情况下,JVM使用过的内存是不会归还给系统的。目的是牺牲内存来提高稳定性。 这是个大前提。
比如,我们查看java的堆内存占用的时候,因为GC的存在,所以堆内存占用其实一直在变,但是实际上RES并没有因为GC而减少。
GC后不释放RES,换句话说是 GC不归还内存给操作系统。主要原因有以下几个
-
进程向操作系统申请内存是一个昂贵的过程。
(存疑)申请内存时,申请的是虚拟内存地址,使用的时候,需要操作系统将虚拟内存地址映射到物理内存地址上。
占用大量内存之后,立即释放掉内存意义不大,因为进程不知道何时需要更多的内存,所以最安全的做法是保留内存,以备不时之需。
-
性价比不高且风险大(无内存可用)
因为不确定进程什么时候会再使用内存,如果归还掉使用时再申请的时候,不一定可以申请的到内存。
-
耗CPU且易产生内存碎片
如果归还内存给系统,意味着需要调整堆大小(因为xms申请了一段内存,这段内存会被保留不被它用),之后把空闲的内存块归还给系统,这个过程会占用cpu;另外一方面,频繁的操作会导致内存地址不连续,即产生内存碎片,因为进程之间的内存是相互隔离的,所以当申请大的内存空间时操作系统也不好利用这些内存。
讲个段子: 这年头借钱的都是大爷,比如我凭本事借的钱,凭什么要还你,虽然无赖,但对于借钱人来说是最有利的(虽然长期看可能借不到钱了,这点不再考虑范围之内)。放到JVM这里就是,我凭本事申请到的内存,为啥要还给你!
GC后如何归还内存给系统
并不是一定不能归还内存给操作系统,只是出于系统稳定性的考虑下,不建议归还内存给操作系统。
强制要求JVM归还空闲内存给操作系统,依赖于JVM的实现及其版本。JDK8及以上是可以支持的。
先说结论:
- 能不能归还,主要依赖于Xms和Xmx是否相等。
- 何时归还,主要依赖于JDK版本和垃圾回收器类型
示例代码
之后的JDK8和JDK11下的测试均以此代码为例。
public class JVMGCDemo2 {
private static final int ONE_MB = 1024*1024;
static volatile List list = new ArrayList();
public static void main(String[] args) {
//指定要生产的对象大小为512m
int count = 256;
//新建一条线程,负责生产对象
new Thread(() -> {
try {
for (int i = 1; i <= 3; i++) {
System.out.println(String.format("第%s次生产%s大小的对象", i, count));
addObject(list, count);
//休眠40秒
TimeUnit.SECONDS.sleep(15);
}
//最后list清空,释放内存。
list.clear();
System.out.println("调用system.gc触发FullGc");
//等待2秒后,调用System.gc触发一次FullGC。触发收缩
System.gc();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
//新建一条线程,负责清理list,回收jvm内存
new Thread(() -> {
for (;;) {
if (list.size() >= count) {
System.out.println("清理list.... ");
list.clear();
//打印堆内存信息
printJvmMemoryInfo();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//阻止程序退出
try {
Thread.currentThread().join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void addObject(List list,int count){
for (int i = 0; i < count; i++) {
OOMobject ooMobject = new OOMobject(1);
//向list添加一个1m的对象
list.add(ooMobject);
try {
//休眠100毫秒
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static class OOMobject{
//生成1m的对象
private byte[] bytes=null;
public OOMobject(){
bytes = new byte[ONE_MB];
}
public OOMobject(int size){
bytes = new byte[size*ONE_MB];
}
}
public static void printJvmMemoryInfo() {
// 虚拟机级内存情况查询
Runtime rt = Runtime.getRuntime();
long vmTotal = rt.totalMemory() / ONE_MB;
long vmFree = rt.freeMemory() / ONE_MB;
long vmMax = rt.maxMemory() / ONE_MB;
long 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("");
}
}
运行时日志如下:
第1次生产256大小的对象
清理list....
JVM内存已用的空间为:264 MB
JVM内存的空闲空间为:256 MB
JVM总内存空间为:520 MB
JVM总内存最大堆空间为:1024 MB
第2次生产256大小的对象
清理list....
JVM内存已用的空间为:263 MB
JVM内存的空闲空间为:260 MB
JVM总内存空间为:523 MB
JVM总内存最大堆空间为:1024 MB
第3次生产256大小的对象
清理list....
JVM内存已用的空间为:262 MB
JVM内存的空闲空间为:366 MB
JVM总内存空间为:628 MB
JVM总内存最大堆空间为:1024 MB
调用system.gc触发FullGc
CMS
JDK8
JVM参数配置
-verbose:gc
-XX:+PrintGCDetails
-Xmx1g
-Xms64m
-Xmn32m
-XX:PretenureSizeThreshold=3M
-XX:+UseConcMarkSweepGC
执行程序之后的堆内存变化情况
从图中堆内存的情况可以看出,JDK8+CMS的配置下,JVM并不是很乐意立马归还内存给到OS,而是随着FullGC次数的增多逐渐归还,最终会全部归还。
JDK11
同样,我们放到JDK11下做下测试。
JVM参数:
-verbose:gc
-XX:+PrintGCDetails
-Xmx1g
-Xms64m
-Xmn32m
-XX:PretenureSizeThreshold=3M
-XX:+UseConcMarkSweepGC
执行之后堆内存的变化情况:
基本上和JDK8下的情况一致,都是渐进式的归还内存给操作系统
但是,JDK11相对JDK8相对更灵活些,提供了更多的归还内存的选项(JDK9以上都有,此处我们以JDK11为例)
JDK11提供了一个JVM参数ShrinkHeapInSteps
通过这个参数,可以在GC之后渐进式的归还内存给到操作系统。JDK11下,此参数默认开启。所以这也就是为什么上面这个堆内存变化是渐进式收缩的原因。
可以把此参数关闭,看下堆内存的变化情况。
JVM参数如下:
-verbose:gc
-XX:+PrintGCDetails
-Xmx1g
-Xms64m
-Xmn32m
-XX:PretenureSizeThreshold=3M
-XX:+UseConcMarkSweepGC
-XX:-ShrinkHeapInSteps
执行情况 如下:
会发现第一次FullGC的时候,内存快速缩小到了最小值。
TOP-RES效果
过程中,我们去看top的话,也能看出来RES是实际在减少的,如下图:
启动不久后,内存达到峰值
随着FullGC的增多,最终的RES降低到了最小值
PS:
可以通过java -XX:+PrintFlagsFinal -version | grep ShrinkHeapInSteps
来查看当前JVM的下ShrinkHeapInSteps
的默认值。
比如上面JDK11的情况下,默认值如下图:
默认是开启的,而JDK8下,是没有此参数配置的。
所以JDK8下,在CMS的配置下,是没有办法快速缩小内存到最小值的。
G1
JDK8
JVM参数配置如下:
-verbose:gc
-XX:+PrintGCDetails
-Xmx1g
-Xms64m
-Xmn32m
-XX:PretenureSizeThreshold=3M
-XX:+UseG1GC
执行之后,堆内存变化如下:
内存会快速收缩到最小值,归还内存给OS。
JDK11
JVM参数如下:
-verbose:gc
-XX:+PrintGCDetails
-Xmx1g
-Xms64m
-Xmn32m
-XX:PretenureSizeThreshold=3M
-XX:+UseG1GC
执行之后,堆内存变化如下:
基本上和JDK8下的类似,fullgc之后,内存被快速收缩到最小值,归还给OS。
有两个点值得注意:
-
JDK11默认的ShrinkHeapInSteps是默认开启的,但这里看堆内存变化并不是渐进的缩小的。 所以在G1回收器下,ShrinkHeapInSteps是无效的。 如果我们手动关闭ShrinkHeapInSteps参数,发现堆内存变化和上面这个类似。
-
JDK11下的G1和JDK8下的G1对内存的响应是不一样的。 从堆内存变化来看, JDK11下G1更加倾向于尽可能的利用内存,不着急回收。 而JDK8下G1则是倾向于尽可能的先回收内存。 从图中看,JDK8下G1的实际使用的堆内存大小基本是JDK11下G1的一半。
备注
只有FullGC的时候才能真正触发堆内存收缩归还OS。YGC是不能使JVM主动归还内存给OS的。
如果代码保持不变,但是JVM参数中设置xms
和xmx
相同的话,会发现不管是否有FullGC,堆内存大小都不发生变化,也就不释放内存给操作系统。如下图,最后面的堆内存使用基本为0了,但是HeapSize依旧没有变化,同样,观察top中的RES也是一样,此处可以自行尝试。
有些文章提到可以通过GCTimeRatio
参数和MinHeapFreeRatio
及MaxHeapFreeRatio
参数来促使JVM归还内存给OS,实测中,没什么很大的作用,内存收缩归还OS主要还是靠xmx != Xms这个配置。
PS:
MinHeapFreeRatio:GC后如果发现空闲堆内存占到整个预估堆内存的N%(百分比), 则放大堆内存的预估最大值。当设置xms=xmx的时候,此参数无效
MaxHeapFreeRatio:GC后如果发现空闲堆内存占到整个预估堆内存的N%(百分比),则收缩堆内存的预估最大值。当设置xms=xmx的时候,此参数无效
小结
JDK8及以上,可以实现归还内存给OS,但实际情况和JDK版本及垃圾回收器有关。
关于MinHeapFreeRatio 和 MaxHeapFreeRatio:
这俩参数,实测中对内存收缩没啥用途,将两个参数设置较低值的时候,堆内存的申请会更紧凑些,且能尽量少的减少堆内存申请,可以对比下面两个图:
补充
尽量保持xms和xmx一致,这样可以减少堆内存调整带来的性能损耗,也可以减少堆内存调整带来的无内存风险。
参考:
https://www.zhihu.com/question/271877603
https://www.jianshu.com/p/0cbc4e44c596
https://www.jianshu.com/p/80ae667e1b7f
https://www.ibm.com/developerworks/cn/java/j-lo-jvm-optimize-experience/index.html
https://stackoverflow.com/questions/30458195/does-gc-release-back-memory-to-os
https://community.oracle.com/message/6440432
http://www.imooc.com/wenda/detail/574044
https://www.geekyhacker.com/2019/01/04/jvm-does-not-release-memory/
https://developer.ibm.com/javasdk/2017/09/25/still-paying-unused-memory-java-app-idle/
https://blog.csdn.net/zhanglong_4444/article/details/100033969
TODO
-
done 直接内存/NativeMemory 含义及原因 https://www.cnblogs.com/guozp/p/7845605.html https://www.jianshu.com/p/0cbc4e44c596
-
永久代和元空间关系及区别 done
-
codecache https://juejin.im/post/5c9ef81151882567e7055260 https://juejin.im/post/5aebf997f265da0ba76f99db
-
Native Memory Tracking, NMT和pmap, VIRT和NMT关系 https://www.cnblogs.com/duanxz/p/3738858.html https://blog.csdn.net/jicahoo/article/details/50933469 https://juejin.im/post/5c986e48f265da60cc02b83c
-
done JCMD使用 https://www.jianshu.com/p/388e35d8a09b http://fengfu.io/2016/12/14/jcmd命令详解/
-
虚拟内存和物理内存,新建一个对象的过程 https://www.ibm.com/developerworks/cn/java/j-nativememory-linux/index.html https://juejin.im/entry/5bd95604f265da391b0a4ea4 https://blog.csdn.net/yusiguyuan/article/details/12041317
-
内存碎片和操作系统关系 https://blog.csdn.net/SnailCpp/article/details/91344670 https://juejin.im/entry/5bd95604f265da391b0a4ea4 https://blog.csdn.net/yusiguyuan/article/details/12041317 http://c.biancheng.net/view/1264.html
-
吞吐量、GCTimeRatio:https://www.javatt.com/p/47695 http://ifeve.com/useful-jvm-flags-part-6-throughput-collector/
-
Linux监控工具: https://www.jianshu.com/p/a6b073738352 https://www.freebuf.com/sectool/100484.html
-
-XX:PretenureSizeThreshold https://blog.csdn.net/w605283073/article/details/94363110
-
稳定堆和动荡的堆, MinHeapFreeRatio https://www.ibm.com/developerworks/cn/java/j-lo-jvm-optimize-experience/index.html https://blog.csdn.net/claram/article/details/104635114
-
g1 https://tech.meituan.com/2016/09/23/g1.html https://juejin.im/post/5d33be9d5188253a2e1b8fa6 https://houbb.github.io/2018/10/08/jvm-21-gc-g1 https://blog.cloudera.com/cdh6-3-hbase-g1-gc-tuning-with-jdk11/
-
逃逸分析
为何GC之后不归还: https://www.zhihu.com/question/30813753
JVM GC优化 https://tech.meituan.com/2017/12/29/jvm-optimize.html