• 极客专栏正式上线!欢迎访问 https://www.jikewenku.com/topic.html
  • 极客专栏正式上线!欢迎访问 https://www.jikewenku.com/topic.html

如何让JVM按照预期GC

技术杂谈 勤劳的小蚂蚁 2个月前 (02-08) 56次浏览 已收录 0个评论 扫描二维码

让JVM按照预期GC(初级版)

如标题,是刚加入笨神(公众号:你假笨)的JVMPocket群时笨神给我们出的第一道题目,原题目是:写出一段JVM先3次YoungGC再1次CMS GC的代码。
JVM比较复杂,很少有人能深入了解它,对绝大部分程序员来说JVM都是黑盒子;那么对我们这些不是专门从事JVM工作的程序员来说,了解它的第一步就是:知道它大概怎么运行,让它按照自己的方式运行;所以笨神的这道题目很有价值,下面给出我当初的答案,源码如下:
/**
 * 说明,根据笔者的环境,即使空跑main方法都会分配1~2M内存
 * @author afei
 */
public class CmsGcTest {

    public static void main(String[] args) throws Exception {

        // list集合全局引入byte数组, 是为了每次YGC后,byte[]不被回收,直接进入Old区
        List<byte[]> holdList = new ArrayList<>();

        // 由于main方法允许肯定会有1~2M内存,所以为了触发第一次YGC,这里只需要分配7M即可
        for (int i=0; i<7; i++){
            holdList.add(new byte[1*1024*1024]);
        }

        // 为了触发第2,3次YGC,每次也只需要分配7M
        for (int i=0; i<7; i++){
            holdList.add(new byte[1*1024*1024]);
        }
        for (int i=0; i<7; i++){
            holdList.add(new byte[1*1024*1024]);
        }

        // sleep一下子为了让CMS GC线程能够有足够的时间检测到Old区达到了触发CMS GC的条件,
        // CMS GC线程默认2s扫描一次,可以通过参数CMSWaitDuration配置,例如-XX:CMSWaitDuration=3000
        Thread.sleep(1000);
    }

}

  • 配套的JVM参数:
java -verbose:gc -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xmx40m -Xms40m -Xmn10m -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=60 CmsGcTest
  • 说明
这样配置后,Eden区8M,S0/S1区各1M,old区30M,且当Old区占用60%即18M就达到触发CMS GC的条件。18M刚好在[14M, 21M]之间,即在第二次YGC后还没有达到触发CMS GC的条件,当第三次YGC后,21M左右的内存在Old区,从而触发CMS GC;

瑕疵

这段代码依然有一点点遗憾:
  1. 如果把Thread的时间调大,就会发现不停的CMS GC。因为分配的21M内存不能回收,所以导致Old区一直满足触发CMS GC的条件。
  2. 这段代码通用性不强,如果需求改成:让JVM先N次YoungGC再M次CMS GC,代码就会需要做出很大的改动。

让JVM按照预期GC(高级版)

这里利用一个JVM参数PretenureSizeThreshold实现更完美的方案。参数用法:-XX:PretenureSizeThreshold=2M,含义是:当分配的对象超过设定值时不在Eden区分配,直接在Old区分配,但是这个参数只能CMS前提下才生效,ParallelGC不生效;
public class CmsGcTest {

    private static final int _1M = 1*1024*1024;
    private static final int _2M = 2*1024*1024;

    public static void main(String[] args) {
        ygc(3);
        cmsGc(1);
        ygc(2);
        cmsGc(2);
        // 在这里想怎么触发GC就怎么调用ygc()和cmsGc()两个方法
    }

    /**
     * @param n 预期发生n次young gc
     */
    private static void ygc(int n){
        for (int i=0; i<n; i++){
            // 由于Eden区设置为8M, 所以分配8个1M就会导致一次YoungGC
            for(int j=0; j<8; j++){
                byte[] tmp = new byte[_1M];
            }
        }
    }

    /**
     * @param n 预期发生n次CMS gc
     */
    private static void cmsGc(int n){
        for (int i=0; i<n; i++){
            for(int j=0; j<3; j++) {
                // 由于设置了-XX:PretenureSizeThreshold=2M, 所以分配的2M对象不会在Eden区分配而是直接在Old区分配
                byte[] tmp = new byte[_2M];
            }
            try {
                // sleep10秒是为了让CMS GC线程能够有足够的时间检测到Old区达到了触发CMS GC的条件并完成CMS GC, CMS GC线程默认2s扫描一次,可以通过参数CMSWaitDuration配置,例如-XX:CMSWaitDuration=3000
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 配套的JVM参数:
-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xmx20m -Xms20m -Xmn10m -XX:PretenureSizeThreshold=2M -XX:+UseConcMarkSweepGC -XX:+UseParNewGC  -XX:CMSInitiatingOccupancyFraction=60 -XX:+UseCMSInitiatingOccupancyOnly
  • 说明
这样配置后,Eden区8M,S0/S1区各1M,old区10M,且当Old区占用60%就达到触发CMS GC的条件;

总结

这篇文章通过一道题目,正好一起总结了两个参数:
  • XX:PretenureSizeThreshold:分配的对象超过阈值,直接在old区分配。
  • XX:CMSWaitDuration:CMS线程扫描Old区是否满足触发CMS GC条件的扫描周期。
这道题目看似简单,但是却包含了JVM一些比较初级的知识点,例如JVM内存结构。如果没有掌握这些基本知识,是很难写出这个题目的答案来的。至于高级版本介绍的两个高级命令,提供了一种更优雅的解决办法。
另外,当你真正掌握这道题目后,一些初级的JVM GC问题也能够慢慢开始进行调优了。否则只能望洋兴叹,不知道怎么办!


丨极客文库, 版权所有丨如未注明 , 均为原创丨
本网站采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行授权
转载请注明原文链接:如何让JVM按照预期GC
喜欢 (0)
[247507792@qq.com]
分享 (0)
勤劳的小蚂蚁
关于作者:
温馨提示:本文来源于网络,转载文章皆标明了出处,如果您发现侵权文章,请及时向站长反馈删除。

您必须 登录 才能发表评论!

  • 精品技术教程
  • 编程资源分享
  • 问答交流社区
  • 极客文库知识库

客服QQ


QQ:2248886839


工作时间:09:00-23:00