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小于Xms设置

图中,发现RES=391M,实际上是小于设置的Xms的512M的。

原因在于:xms表示的是java进程向操作系统申请的虚拟内存值,而RES代表的是实际使用的内存值。

  1. 申请并不一定意味着要实际使用。

    只是向系统提前申请,系统会为其分配虚拟地址空间,但该空间并未直接映射到真实的物理内存地址。

  2. 只有当实际使用时,才会分配物理内存。

    实际使用时,只有当出现了缺页中断时,才会生成真正的物理内存地址映射,此时才是真正的物理内存占用。

所以这就是我们看到的,刚启动不久的java进程的RES值小于xms的主要原因。

关于虚拟内存和物理内存的关系的一篇介绍:https://blog.csdn.net/u012861978/article/details/53048077

为什么RES会大于xms

java进程运行一段时间之后,我们会发现此时的RES可能已经大于xms了。比如阿里云服务器上的监控程序,设定的xms是16M,xmx是32M。

java进程的xms和xmx设置

通过top查看RES使用情况,发现RES为75M,是大于xmx和xms的。

RES大于xms和xmx

原因在于RES表示的是java进程实际使用的物理内存值,而xms和xmx表示的仅是JVM的堆内存大小。

Java进程实际使用内存和什么有关

Java进程内存主要包含以下几点(PS:包含但不仅限于这几部分)

  1. 堆内存(新生代、老年代)
  2. JDK8之前永久代PermGen/JDK8之后元空间Metaspace
  3. 线程栈内存占用(Java线程栈内存和非java线程栈内存)
  4. 直接内存(Direct Memory)(Java NIO)
  5. JVM程序自身占用内存(Native Memory)
  6. 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不归还内存给操作系统。主要原因有以下几个

  1. 进程向操作系统申请内存是一个昂贵的过程。

    (存疑)申请内存时,申请的是虚拟内存地址,使用的时候,需要操作系统将虚拟内存地址映射到物理内存地址上。

    占用大量内存之后,立即释放掉内存意义不大,因为进程不知道何时需要更多的内存,所以最安全的做法是保留内存,以备不时之需。

  2. 性价比不高且风险大(无内存可用)

    因为不确定进程什么时候会再使用内存,如果归还掉使用时再申请的时候,不一定可以申请的到内存。

  3. 耗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

执行程序之后的堆内存变化情况

image-20200415220752386

从图中堆内存的情况可以看出,JDK8+CMS的配置下,JVM并不是很乐意立马归还内存给到OS,而是随着FullGC次数的增多逐渐归还,最终会全部归还。

JDK11

同样,我们放到JDK11下做下测试。

JVM参数:

-verbose:gc 
-XX:+PrintGCDetails 
-Xmx1g 
-Xms64m 
-Xmn32m 
-XX:PretenureSizeThreshold=3M 
-XX:+UseConcMarkSweepGC

执行之后堆内存的变化情况:

image-20200415231959742

基本上和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 

执行情况 如下:

image-20200415231932502

会发现第一次FullGC的时候,内存快速缩小到了最小值。

TOP-RES效果

过程中,我们去看top的话,也能看出来RES是实际在减少的,如下图:

启动不久后,内存达到峰值

image-20200415222851250

随着FullGC的增多,最终的RES降低到了最小值

image-20200415222937054

PS:

可以通过java -XX:+PrintFlagsFinal -version | grep ShrinkHeapInSteps 来查看当前JVM的下ShrinkHeapInSteps的默认值。

比如上面JDK11的情况下,默认值如下图:

image-20200415222117973

默认是开启的,而JDK8下,是没有此参数配置的。

image-20200415222211567

所以JDK8下,在CMS的配置下,是没有办法快速缩小内存到最小值的。

G1
JDK8

JVM参数配置如下:

-verbose:gc
-XX:+PrintGCDetails
-Xmx1g
-Xms64m
-Xmn32m
-XX:PretenureSizeThreshold=3M
-XX:+UseG1GC

执行之后,堆内存变化如下:

image-20200415231959742

内存会快速收缩到最小值,归还内存给OS。

JDK11

JVM参数如下:

-verbose:gc 
-XX:+PrintGCDetails 
-Xmx1g 
-Xms64m 
-Xmn32m 
-XX:PretenureSizeThreshold=3M 
-XX:+UseG1GC

执行之后,堆内存变化如下:

image-20200415232015376

基本上和JDK8下的类似,fullgc之后,内存被快速收缩到最小值,归还给OS。

有两个点值得注意:

  1. JDK11默认的ShrinkHeapInSteps是默认开启的,但这里看堆内存变化并不是渐进的缩小的。 所以在G1回收器下,ShrinkHeapInSteps是无效的。 如果我们手动关闭ShrinkHeapInSteps参数,发现堆内存变化和上面这个类似。

  2. JDK11下的G1和JDK8下的G1对内存的响应是不一样的。 从堆内存变化来看, JDK11下G1更加倾向于尽可能的利用内存,不着急回收。 而JDK8下G1则是倾向于尽可能的先回收内存。 从图中看,JDK8下G1的实际使用的堆内存大小基本是JDK11下G1的一半。

备注

只有FullGC的时候才能真正触发堆内存收缩归还OS。YGC是不能使JVM主动归还内存给OS的。

如果代码保持不变,但是JVM参数中设置xmsxmx相同的话,会发现不管是否有FullGC,堆内存大小都不发生变化,也就不释放内存给操作系统。如下图,最后面的堆内存使用基本为0了,但是HeapSize依旧没有变化,同样,观察top中的RES也是一样,此处可以自行尝试。

触发FullGC后RES

有些文章提到可以通过GCTimeRatio参数和MinHeapFreeRatioMaxHeapFreeRatio参数来促使JVM归还内存给OS,实测中,没什么很大的作用,内存收缩归还OS主要还是靠xmx != Xms这个配置。

PS:

MinHeapFreeRatio:GC后如果发现空闲堆内存占到整个预估堆内存的N%(百分比), 则放大堆内存的预估最大值。当设置xms=xmx的时候,此参数无效

MaxHeapFreeRatio:GC后如果发现空闲堆内存占到整个预估堆内存的N%(百分比),则收缩堆内存的预估最大值。当设置xms=xmx的时候,此参数无效

代码部分参考: https://juejin.im/post/5d35ae896fb9a07ed8428046

小结

JDK8及以上,可以实现归还内存给OS,但实际情况和JDK版本及垃圾回收器有关。

关于MinHeapFreeRatio 和 MaxHeapFreeRatio:

这俩参数,实测中对内存收缩没啥用途,将两个参数设置较低值的时候,堆内存的申请会更紧凑些,且能尽量少的减少堆内存申请,可以对比下面两个图:

image-20200415225324902 image-20200415225436317

补充

尽量保持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://docs.oracle.com/en/java/javase/11/gctuning/garbage-first-garbage-collector.html#GUID-6D6B18B1-063B-48FF-99E3-5AF059C43CE8

https://docs.oracle.com/en/java/javase/11/gctuning/factors-affecting-garbage-collection-performance.html#GUID-7FB2D1D5-D75F-4AA1-A3B1-4A17F8FF97D0

https://blog.csdn.net/zhanglong_4444/article/details/100033969