阿菜

阿菜 查看完整档案

杭州编辑杭州电子科技大学  |  计算机科学与技术 编辑  |  填写所在公司/组织 www.jianshu.com/u/e283168f6a52 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

阿菜 赞了文章 · 2019-01-24

为什么不建议在for循环中使用"+"进行字符串拼接

摘要: 学习阿里巴巴Java开发手册。

Fundebug经授权转载,版权归原作者所有。

字符串,是Java中最常用的一个数据类型了。关于字符串的知识,作者已经发表过几篇文章介绍过很多,如:

本文,也是对于Java中字符串相关知识的一个补充,主要来介绍一下字符串拼接相关的知识。本文基于jdk1.8.0_181。

字符串拼接

字符串拼接是我们在Java代码中比较经常要做的事情,就是把多个字符串拼接到一起。

我们都知道,String是Java中一个不可变的类,所以他一旦被实例化就无法被修改。

不可变类的实例一旦创建,其成员变量的值就不能被修改。这样设计有很多好处,比如可以缓存hashcode、使用更加便利以及更加安全等。

但是,既然字符串是不可变的,那么字符串拼接又是怎么回事呢?

字符串不变性与字符串拼接

其实,所有的所谓字符串拼接,都是重新生成了一个新的字符串。下面一段字符串拼接代码:

String s = "abcd";
s = s.concat("ef");

其实最后我们得到的s已经是一个新的字符串了。如下图

s中保存的是一个重新创建出来的String对象的引用。

那么,在Java中,到底如何进行字符串拼接呢?字符串拼接有很多种方式,这里简单介绍几种比较常用的。

使用+拼接字符串

在Java中,拼接字符串最简单的方式就是直接使用符号+来拼接。如:

String wechat = "Hollis";
String introduce = "每日更新Java相关技术文章";
String hollis = wechat + "," + introduce;

这里要特别说明一点,有人把Java中使用+拼接字符串的功能理解为运算符重载。其实并不是,Java是不支持运算符重载的。这其实只是Java提供的一个语法糖。后面再详细介绍。

运算符重载:在计算机程序设计中,运算符重载(英语:operator overloading)是多态的一种。运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。

语法糖:语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。

concat
除了使用+拼接字符串之外,还可以使用String类中的方法concat方法来拼接字符串。如:

String wechat = "Hollis";
String introduce = "每日更新Java相关技术文章";
String hollis = wechat.concat(",").concat(introduce);

StringBuffer

关于字符串,Java中除了定义了一个可以用来定义字符串常量String类以外,还提供了可以用来定义字符串变量StringBuffer类,它的对象是可以扩充和修改的。

使用StringBuffer可以方便的对字符串进行拼接。如:

StringBuffer wechat = new StringBuffer("Hollis");
String introduce = "每日更新Java相关技术文章";
StringBuffer hollis = wechat.append(",").append(introduce);

StringBuilder
除了StringBuffer以外,还有一个类StringBuilder也可以使用,其用法和StringBuffer类似。如:

StringBuilder wechat = new StringBuilder("Hollis");
String introduce = "每日更新Java相关技术文章";
StringBuilder hollis = wechat.append(",").append(introduce);

StringUtils.join
除了JDK中内置的字符串拼接方法,还可以使用一些开源类库中提供的字符串拼接方法名,如apache.commons中提供的StringUtils类,其中的join方法可以拼接字符串。

String wechat = "Hollis";
String introduce = "每日更新Java相关技术文章";
System.out.println(StringUtils.join(wechat, ",", introduce));

这里简单说一下,StringUtils中提供的join方法,最主要的功能是:将数组或集合以某拼接符拼接到一起形成新的字符串,如:

String []list  ={"Hollis","每日更新Java相关技术文章"};
String result= StringUtils.join(list,",");
System.out.println(result);
//结果:Hollis,每日更新Java相关技术文章

并且,Java8中的String类中也提供了一个静态的join方法,用法和StringUtils.join类似。

以上就是比较常用的五种在Java种拼接字符串的方式,那么到底哪种更好用呢?为什么阿里巴巴Java开发手册中不建议在循环体中使用+进行字符串拼接呢?

使用+拼接字符串的实现原理

前面提到过,使用+拼接字符串,其实只是Java提供的一个语法糖, 那么,我们就来解一解这个语法糖,看看他的内部原理到底是如何实现的。

还是这样一段代码。我们把他生成的字节码进行反编译,看看结果。

String wechat = "Hollis";
String introduce = "每日更新Java相关技术文章";
String hollis = wechat + "," + introduce;

反编译后的内容如下,反编译工具为jad。

String wechat = "Hollis";
String introduce = "\u6BCF\u65E5\u66F4\u65B0Java\u76F8\u5173\u6280\u672F\u6587\u7AE0";//每日更新Java相关技术文章
String hollis = (new StringBuilder()).append(wechat).append(",").append(introduce).toString();

通过查看反编译以后的代码,我们可以发现,原来字符串常量在拼接过程中,是将String转成了StringBuilder后,使用其append方法进行处理的。

那么也就是说,Java中的+对字符串的拼接,其实现原理是使用StringBuilder.append

concat是如何实现的

我们再来看一下concat方法的源代码,看一下这个方法又是如何实现的。

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

这段代码首先创建了一个字符数组,长度是已有字符串和待拼接字符串的长度之和,再把两个字符串的值复制到新的字符数组中,并使用这个字符数组创建一个新的String对象并返回。

通过源码我们也可以看到,经过concat方法,其实是new了一个新的String,这也就呼应到前面我们说的字符串的不变性问题上了。

StringBuffer和StringBuilder

接下来我们看看StringBufferStringBuilder的实现原理。

String类类似,StringBuilder类也封装了一个字符数组,定义如下:

char[] value;

String不同的是,它并不是final的,所以他是可以修改的。另外,与String不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数,定义如下:

int count;

其append源码如下:

public StringBuilder append(String str) {
    super.append(str);
    return this;
}

该类继承了AbstractStringBuilder类,看下其append方法:

public AbstractStringBuilder append(String str) {
    if (str == null)
        return appendNull();
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}

append会直接拷贝字符到内部的字符数组中,如果字符数组长度不够,会进行扩展。

StringBufferStringBuilder类似,最大的区别就是StringBuffer是线程安全的,看一下StringBufferappend方法。

public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

该方法使用synchronized进行声明,说明是一个线程安全的方法。而StringBuilder则不是线程安全的。

StringUtils.join是如何实现的

通过查看StringUtils.join的源代码,我们可以发现,其实他也是通过StringBuilder来实现的。

public static String join(final Object[] array, String separator, final int startIndex, final int endIndex) {
    if (array == null) {
        return null;
    }
    if (separator == null) {
        separator = EMPTY;
    }

    // endIndex - startIndex > 0:   Len = NofStrings *(len(firstString) + len(separator))
    //           (Assuming that all Strings are roughly equally long)
    final int noOfItems = endIndex - startIndex;
    if (noOfItems <= 0) {
        return EMPTY;
    }

    final StringBuilder buf = new StringBuilder(noOfItems * 16);

    for (int i = startIndex; i < endIndex; i++) {
        if (i > startIndex) {
            buf.append(separator);
        }
        if (array[i] != null) {
            buf.append(array[i]);
        }
    }
    return buf.toString();
}

效率比较

既然有这么多种字符串拼接的方法,那么到底哪一种效率最高呢?我们来简单对比一下。

long t1 = System.currentTimeMillis();
//这里是初始字符串定义
for (int i = 0; i < 50000; i++) {
    //这里是字符串拼接代码
}
long t2 = System.currentTimeMillis();
System.out.println("cost:" + (t2 - t1));

我们使用形如以上形式的代码,分别测试下五种字符串拼接代码的运行时间。得到结果如下:

+ cost:5119
StringBuilder cost:3
StringBuffer cost:4
concat cost:3623
StringUtils.join cost:25726

从结果可以看出,用时从短到长的对比是:

StringBuilder`<`StringBuffer`<`concat`<`+`<`StringUtils.join

StringBufferStringBuilder的基础上,做了同步处理,所以在耗时上会相对多一些。

StringUtils.join也是使用了StringBuilder,并且其中还是有很多其他操作,所以耗时较长,这个也容易理解。其实StringUtils.join更擅长处理字符串数组或者列表的拼接。

那么问题来了,前面我们分析过,其实使用+拼接字符串的实现原理也是使用的StringBuilder,那为什么结果相差这么多,高达1000多倍呢?

我们再把以下代码反编译下:

long t1 = System.currentTimeMillis();
String str = "hollis";
for (int i = 0; i < 50000; i++) {
    String s = String.valueOf(i);
    str += s;
}
long t2 = System.currentTimeMillis();
System.out.println("+ cost:" + (t2 - t1));

反编译后代码如下:

long t1 = System.currentTimeMillis();
String str = "hollis";
for(int i = 0; i < 50000; i++)
{
    String s = String.valueOf(i);
    str = (new StringBuilder()).append(str).append(s).toString();
}

long t2 = System.currentTimeMillis();
System.out.println((new StringBuilder()).append("+ cost:").append(t2 - t1).toString());

我们可以看到,反编译后的代码,在for循环中,每次都是new了一个StringBuilder,然后再把String转成StringBuilder,再进行append

而频繁的新建对象当然要耗费很多时间了,不仅仅会耗费时间,频繁的创建对象,还会造成内存资源的浪费。

所以,阿里巴巴Java开发手册建议:循环体内,字符串的连接方式,使用 StringBuilderappend 方法进行扩展。而不要使用+

总结

本文介绍了什么是字符串拼接,虽然字符串是不可变的,但是还是可以通过新建字符串的方式来进行字符串的拼接。

常用的字符串拼接方式有五种,分别是使用+、使用concat、使用StringBuilder、使用StringBuffer以及使用StringUtils.join

由于字符串拼接过程中会创建新的对象,所以如果要在一个循环体中进行字符串拼接,就要考虑内存问题和效率问题。

因此,经过对比,我们发现,直接使用StringBuilder的方式是效率最高的。因为StringBuilder天生就是设计来定义可变字符串和字符串的变化操作的。

但是,还要强调的是:

  • 如果不是在循环体中进行字符串拼接的话,直接使用+就好了。
  • 如果在并发场景中进行字符串拼接的话,要使用StringBuffer来代替StringBuilder
查看原文

赞 22 收藏 9 评论 4

阿菜 发布了文章 · 2018-09-17

JVM GC 之「AdaptiveSizePolicy」实战

转载请注明原文链接:https://www.jianshu.com/p/741...

一、AdaptiveSizePolicy简介

AdaptiveSizePolicy(自适应大小策略) 是 JVM GC Ergonomics(自适应调节策略) 的一部分。

如果开启 AdaptiveSizePolicy,则每次 GC 后会重新计算 Eden、From 和 To 区的大小,计算依据是 GC 过程中统计的 GC 时间、吞吐量、内存占用量

开启 AdaptiveSizePolicy 的参数为:

-XX:+UseAdaptiveSizePolicy

JDK 1.8 默认使用 UseParallelGC 垃圾回收器,该垃圾回收器默认启动了 AdaptiveSizePolicy。

AdaptiveSizePolicy 有三个目标:

  1. Pause goal:应用达到预期的 GC 暂停时间。
  2. Throughput goal:应用达到预期的吞吐量,即应用正常运行时间 / (正常运行时间 + GC 耗时)。
  3. Minimum footprint:尽可能小的内存占用量。

AdaptiveSizePolicy 为了达到三个预期目标,涉及以下操作:

  1. 如果 GC 停顿时间超过了预期值,会减小内存大小。理论上,减小内存,可以减少垃圾标记等操作的耗时,以此达到预期停顿时间。
  2. 如果应用吞吐量小于预期,会增加内存大小。理论上,增大内存,可以降低 GC 的频率,以此达到预期吞吐量。
  3. 如果应用达到了前两个目标,则尝试减小内存,以减少内存消耗。

注:AdaptiveSizePolicy 涉及的内容比较广,本文主要关注 AdaptiveSizePolicy 对年轻代大小的影响,以及随之产生的问题。

AdaptiveSizePolicy 看上去很智能,但有时它也很调皮,会引发 GC 问题。


二、由 AdaptiveSizePolicy 引发的 GC 问题

某一天,有一位群友在群里发来一张 jmap -heap 内存使用情况图。

说 Survivor 区占比总是在 98% 以上。

jmap -heap 内存情况

仔细观察这张图,其中包含几个重要信息:

  1. From 和 To 区都比较小,只有 10M。容量比较小,才显得占比高。
  2. Old 区的占比和使用量(两个多 G)都比较高。

此外,还可以看到 Eden、From、To 之间的比例不是默认的 8:1:1。

于是,立马就想到 AdaptiveSizePolicy。

经群友的确认,使用的是 JDK 1.8 的默认回收算法。

JVM 参数配置如下:

JVM 参数配置

参数中没有对 GC 算法进行配置,即使用默认的 UseParallelGC。

用默认参数启动一个基于 JDK 1.8 的应用,然后使用 jinfo -flags pid 即可查看默认配置的 GC 算法。

默认使用 UseParallelGC

上文提到,该算法默认开启 AdaptiveSizePolicy。

即使 SurvivorRatio 的默认值是 8,但年轻代三个区域之间的比例仍会变动。

这个问题,可以参考来自R大的回答:

http://hllvm.group.iteye.com/...

HotSpot VM里,ParallelScavenge系的GC(UseParallelGC / UseParallelOldGC)默认行为是SurvivorRatio如果不显式设置就没啥用。显式设置到跟默认值一样的值则会有效果。

因为ParallelScavenge系的GC最初设计就是默认打开AdaptiveSizePolicy的,它会自动、自适应的调整各种参数。

在群友的截图中,From 区只有 10M,Eden 区占用了却超过年轻代八成的空间。

其原因是 AdaptiveSizePolicy 为了达到期望的目标而进行了调整。


大概定位了 Survivor 区小的原因,还有一个问题:

为什么老年代的占比和使用量都比较高?

于是群友使用 jmap -histo 查看堆中的实例。

jmap -histo 结果

可以看出,其中有两个类的实例比较多,分别是:

  1. LinkedHashMap$Entry
  2. ExpiringCache$Entry

于是,搜索关键类 ExpiringCache。

可以看出在 ExpiringCache 的构造函数中,初始化了一个 LinkedHashMap。

怀疑 LinkedHashMap&dollar;Entry 数量多的原因和 ExpiringCache&dollar;Entry 直接有关。

ExpiringCache(long millisUntilExpiration) {
    this.millisUntilExpiration = millisUntilExpiration;
    map = new LinkedHashMap<String,Entry>() {
        protected boolean removeEldestEntry(Map.Entry<String,Entry> eldest) {
          return size() > MAX_ENTRIES;
        }
      };
}

注:该 map 用于保存缓存数据,设置了淘汰机制。当 map 大小超过 MAX_ENTRIES = 200 时,会开始淘汰。

接着查看 ExpiringCache&dollar;Entry 类。

这个类的主要属性是「时间戳」和「值」,时间戳用于超时淘汰(缓存常用手法)。

static class Entry {
    private long   timestamp;
    private String val;
    ……
}

接着查看哪里使用到了这个缓存。

于是找到 get 方法,定位到只有一个类的一个方法使用到了这个缓存。

缓存 get 方法

使用到缓存的函数

接着往上层找,看到了一个熟悉的类:File,它的 getCanonicalPath() 方法使用到了这个缓存。

File 类的 getCanonicalPath 方法

该方法用于获取文件路径。

于是,询问群友,是否在项目中使用了 getCanonicalPath() 方法。

得到的回答是肯定的。

当项目中使用 getCanonicalPath() 方法获取文件路径时,会发生以下的事情:

  1. 首先从缓存中读取,取不到则需要生成缓存。
  2. 生成缓存需要新建 ExpiringCache&dollar;Entry 对象用于保存缓存值,这些新建的对象都会被分配到 Eden 区
  3. 大量使用 getCanonicalPath() 方法时,缓存数量超过 MAX_ENTRIES = 200 开启淘汰策略。原来 map 中的 ExpiringCache&dollar;Entry 对象变成垃圾对象,真正存活的 Entry 只有 200 个。
  4. 当发生 YGC 时,理论上存活的 200 个 Entry 会去往 To 区,其他被淘汰的垃圾 Entry 对象会被回收。
  5. 但由于 AdaptiveSizePolicy 将 To 区调整到只有 10MB,装不下本该移动到 To 区的对象,只能直接移动到老年代
  6. 于是,在每次 YGC 时,会有接近 200 个存活的 ExpiringCache&dollar;Entry 对象进入到老年代。随着缓存淘汰机制的运行,这些 Entry 对象立马又变成垃圾。
  7. 当对象进入老年代,即使变成了垃圾,也需要等到老年代 GC 或者 FGC 才能将其回收。由于老年代容量较大,可以承受多次 YGC 给予的 200 个 ExpiringCache&dollar;Entry 对象。
  8. 于是,老年代使用量逐渐变高。

老年代内存占用量高的问题也定位到了。

因为每次 YGC 只有 200 个实例进入到老年代,问题显得比较温和。

只是隔一段时间触发 FGC,应用运行看似正常。


接着使用 jstat -gcutil 查看 GC 情况。

可以看到从应用启动,一共发生了 15654 次 YGC。

jstat -gcutil 结果

推算每次 YGC 有 200 个 ExpiringCache&dollar;Entry 对象进入老年代。

那么,老年代中大约存在 3130800 个 ExpiringCache&dollar;Entry 对象。

从之前的 jmap -histo 结果中看到,ExpiringCache&dollar;Entry 对象的数量是 6118824 个。

两个数目都为百万级。其余约 300W 个实例应该都在 Eden 区。

每一次 YGC 后,都会有大量的 ExpiringCache&dollar;Entry 对象被回收。

从群友截取的 GC log 中可以看出,YGC 的频率大概为 23 秒一次。

GC log

假设运行的 jmap -histo 命令是在即将触发 YGC 之前。

那么,应用大概在 20s 的事件内产生了 300W 个 ExpiringCache&dollar;Entry 实例,1s 内产生约 15W 个。

假设单机 QPS = 300,一次请求产生的 ExpiringCache&dollar;Entry 实例数约为 500 个。

猜测是在循环体中使用了 getCanonicalPath() 方法。

至此可以得出 Survior 区变小,老年代占比变高的原因:

  1. 在默认 SurvivorRatio = 8 的情况下,没有达到吞吐量的期望,AdaptiveSizePolicy 加大了 Eden 区的大小。From 和To 区被压缩到只有 10M。
  2. 在项目中大量使用 getCanonicalPath() 方法,产生大量ExpiringCache&dollar;Entry 实例。
  3. 当 YGC 发生时候,由于 To 区太小,存活的 Entry 对象直接进入到老年代。老年代占用量逐渐变大。

从群友的 jstat -gcutil 截图中还可以看出,应用从启动到使用该命令,触发了 19 次 FGC,一共耗时 9.933s,平均每次 FGC 耗时为 520ms。

这样的停顿时间,对于一个高 QPS 的应用是无法忍受的。


定位到了问题的原因,解决方案比较简单。

解决的思路有两个:

  1. 不使用缓存,就不会生成大量 ExpiringCache&dollar;Entry 实例。
  2. 阻止 AdaptiveSizePolicy 缩小 To 区。让 YGC 时存活的 ExpiringCache&dollar;Entry 对象都能顺利进入 To 区,保留在年轻代,而不是进入老年代。

解决方案一:

不使用缓存。

使用 -Dsun.io.useCanonCaches = false 参数即可关闭缓存。

sun.io.useCanonCaches 参数

这种方案解决比较方便,但这个参数并非常规参数,慎用。

解决方案二:

保持使用 UseParallelGC,显式设置 -XX:SurvivorRatio=8。

配置参数进行测试:

默认配置

看到默认配置下,三者之间的比例不是 8:1:1。

加上参数 -Xmn100m -XX:SurvivorRatio=8

可以看到,加上参数 -Xmn100m -XX:SurvivorRatio=8 参数后,固定了 Eden 和 Survivor 之间的比例。

解决方案三:

使用 CMS 垃圾回收器。

CMS 默认关闭 AdaptiveSizePolicy。

配置参数 -XX:+UseConcMarkSweepGC,通过 jinfo 命令查看,可以看到 CMS 默认减去/不使用 AdaptiveSizePolicy。

jinfo 结果

群友也是采用了这个方法:

使用 CMS 之后的 jmap -heap 结果

可以看出,Eden 和 Survivor 之间的比例被固定,To 区没有被缩小。老年代的使用量和使用率也都很正常。


三、源码层面了解 AdaptiveSizePolicy

注:以下源码均主要基于 openjdk 8,不同 jdk 版本之间会有区别。

对源码的理解程度有限,对源码的理解也一直在路上。

有任何错误,还请各位指正,谢谢。

首先解释,为什么在 UseParallelGC 回收器的前提下,显式配置 SurvivorRatio 即可固定年轻代三个区域之间的比例。

在 arguments.cpp 类中有一个 set_parallel_gc_flags() 方法。

从方法命名来看,是为了设置并行回收器的参数。

// If InitialSurvivorRatio or MinSurvivorRatio were not specified, but the
  // SurvivorRatio has been set, reset their default values to SurvivorRatio +
  // 2.  By doing this we make SurvivorRatio also work for Parallel Scavenger.
  // See CR 6362902 for details.
  if (!FLAG_IS_DEFAULT(SurvivorRatio)) {
    if (FLAG_IS_DEFAULT(InitialSurvivorRatio)) {
       FLAG_SET_DEFAULT(InitialSurvivorRatio, SurvivorRatio + 2);
    }
    if (FLAG_IS_DEFAULT(MinSurvivorRatio)) {
      FLAG_SET_DEFAULT(MinSurvivorRatio, SurvivorRatio + 2);
    }
  }

当显式设置 SurvivorRatio,即 !FLAG_IS_DEFAULT(SurvivorRatio),该方法会设置别的参数。

方法注释上写着:

make SurvivorRatio also work for Parallel Scavenger
通过显式设置 SurvivorRatio 参数,SurvivorRatio 就会在 Parallel Scavenge 回收器中生效。

至于为何会生效,还有待进一步学习。

而默认是会被 AdaptiveSizePolicy 调整的。


接着查看 AdaptiveSizePolicy 动态调整内存大小的代码。

JDK 1.8 默认的 UseParallelGC 回收器,其对应的年轻代回收算法是 Parallel Scavenge。

触发 GC 的原因有多种,最普通的一种是在年轻代分配内存失败。

UseParallelGC 分配内存失败引发 GC 的入口位于
vmPSOperations.cpp 类的 VM_ParallelGCFailedAllocation::doit() 方法。

之后依次调用了以下方法:

parallelScavengeHeap.cpp 类的 failed_mem_allocate(size_t size) 方法。

psScavenge.cpp 类的 invoke()、invoke_no_policy() 方法。

invoke_no_policy() 方法中有一段代码涉及 AdaptiveSizePolicy。

if (UseAdaptiveSizePolicy) {
  ……
  size_policy->compute_eden_space_size(young_live,
                                               eden_live,
                                               cur_eden,
                                               max_eden_size,
                                               false /* not full gc*/);
  ……
}

在 GC 主过程完成后,如果开启 UseAdaptiveSizePolicy 则会重新计算 Eden 区的大小。

在 compute_eden_space_size 方法中,有几个判断。

对应 AdaptiveSizePolicy 的三个目标:

  1. 与预期 GC 停顿时间对比。
  2. 与预期吞吐量对比。
  3. 如果达到预期,则调整内存容量。
if ((_avg_minor_pause->padded_average() > gc_pause_goal_sec()) ||
      (_avg_major_pause->padded_average() > gc_pause_goal_sec())) {
    adjust_eden_for_pause_time(is_full_gc, &desired_promo_size, &desired_eden_size);
  } else if (_avg_minor_pause->padded_average() > gc_minor_pause_goal_sec()) {
    adjust_eden_for_minor_pause_time(is_full_gc, &desired_eden_size);
  } else if(adjusted_mutator_cost() < _throughput_goal) {
    assert(major_cost >= 0.0, "major cost is < 0.0");
    assert(minor_cost >= 0.0, "minor cost is < 0.0");
    adjust_eden_for_throughput(is_full_gc, &desired_eden_size);
  } else {
    if (UseAdaptiveSizePolicyFootprintGoal &&
        young_gen_policy_is_ready() &&
        avg_major_gc_cost()->average() >= 0.0 &&
        avg_minor_gc_cost()->average() >= 0.0) {
      size_t desired_sum = desired_eden_size + desired_promo_size;
      desired_eden_size = adjust_eden_for_footprint(desired_eden_size, desired_sum);
    }
  }

详细看其中一个判断。

if ((_avg_minor_pause->padded_average() > gc_pause_goal_sec()) ||
      (_avg_major_pause->padded_average() > gc_pause_goal_sec()))

如果统计的 YGC 或者 Old GC 时间超过了目标停顿时间,则会调用 adjust_eden_for_pause_time 调整 Eden 区大小。

gc_pause_goal_sec() 方法获取预期停顿时间,在 ParallelScavengeHeap::initialize() 方法中,通过读取 JVM 参数 MaxGCPauseMillis 获取。

gc_pause_goal_sec() 来自 JVM 参数


接下来,再看 CMS 回收器。

CMS 初始化分代位于 cmsCollectorPolicy.cpp 类的 initialize_generations() 方法。

if (UseParNewGC) {
  if (UseAdaptiveSizePolicy) {
    _generations[0] = new GenerationSpec(Generation::ASParNew,
                                         _initial_gen0_size, _max_gen0_size);
  } else {
    _generations[0] = new GenerationSpec(Generation::ParNew,
                                         _initial_gen0_size, _max_gen0_size);
  }
} else {
  _generations[0] = new GenerationSpec(Generation::DefNew,
                                       _initial_gen0_size, _max_gen0_size);
}
if (UseAdaptiveSizePolicy) {
  _generations[1] = new GenerationSpec(Generation::ASConcurrentMarkSweep,
                          _initial_gen1_size, _max_gen1_size);
} else {
  _generations[1] = new GenerationSpec(Generation::ConcurrentMarkSweep,
                          _initial_gen1_size, _max_gen1_size);
}

其中 _generations[0] 代表年轻代特征,_generations[1] 代表老年代特征。

如果设置不同的 UseParNewGC 、UseAdaptiveSizePolicy 参数,会对年轻代和老年代使用不同的策略。

CMS 垃圾回收入口位于 genCollectedHeap.cpp 类的 do_collection 方法。

在 do_collection 方法中,GC 主过程完成后,会对每个分代进行大小调整。

for (int j = max_level_collected; j >= 0; j -= 1) {
  // Adjust generation sizes.
  _gens[j]->compute_new_size();
}

使用 compute_new_size() 方法

本文主要讨论 AdaptiveSizePolicy 对年轻代的影响,主要看 ASParNewGeneration 类,其中的 AS 前缀就是 AdaptiveSizePolicy 的意思。

如果设置 -XX:+UseAdaptiveSizePolicy 则年轻代对应 ASParNewGeneration 类,否则对应 ParNewGeneration 类。

在 ASParNewGeneration 类中 compute_new_size() 方法中,调用了另一个方法调整 Eden 区大小。

size_policy->compute_eden_space_size(eden()->capacity(), max_gen_size());

该方法与 Parallel Scavenge 的 compute_eden_space_size 方法类似,也从三个方面对内存大小进行调整,分别是:

  • adjust_eden_for_pause_time
  • adjust_eden_for_throughput
  • adjust_eden_for_footprint

接着进行测试,设置参数 -XX:+UseAdaptiveSizePolicy、
-XX:+UseConcMarkSweepGC。

期望 CMS 会启用 AdaptiveSizePolicy,但根据 jmap -heap 结果查看,并没有启动,年轻代三个区域之间的比例为 8:1:1。

从 jinfo 命令结果也可以看出,即使设置了 -XX:+UseAdaptiveSizePolicy,仍然关闭了 AdaptiveSizePolicy。

jinfo 结果

因为在 JDK 1.8 中,如果使用 CMS,无论 UseAdaptiveSizePolicy 如何设置,都会将 UseAdaptiveSizePolicy 设置为 false。

查看 arguments.cpp 类中的 set_cms_and_parnew_gc_flags 方法,其调用了 disable_adaptive_size_policy 方法将 UseAdaptiveSizePolicy 设置成 false。

static void disable_adaptive_size_policy(const char* collector_name) {
  if (UseAdaptiveSizePolicy) {
    if (FLAG_IS_CMDLINE(UseAdaptiveSizePolicy)) {
      warning("disabling UseAdaptiveSizePolicy; it is incompatible with %s.",
              collector_name);
    }
    FLAG_SET_DEFAULT(UseAdaptiveSizePolicy, false);
  }
}

如果是在启动参数中设置了,则会打出提醒。

提醒 UseAdaptiveSizePolicy 参数和 CMS 不搭

但在 JDK 1.6 和 1.7 中,set_cms_and_parnew_gc_flags 方法的逻辑和 1.8 中的不同。

如果 UseAdaptiveSizePolicy 参数是默认的,则强制设置成 false。

如果显式设置(complete),则不做改变。

// Turn off AdaptiveSizePolicy by default for cms until it is
// complete.
if (FLAG_IS_DEFAULT(UseAdaptiveSizePolicy)) {
  FLAG_SET_DEFAULT(UseAdaptiveSizePolicy, false);
}

于是尝试使用 JDK 1.6 搭建 web 应用,加上 -XX:+UseAdaptiveSizePolicy、-XX:+UseConcMarkSweepGC 两个参数。

再用 jinfo -flag 查看,看到两个参数都被置为 true。

jinfo -flag 结果

接着,使用 jmap -heap 查看堆内存使用情况,发现展示不了信息。

jmap -heap 结果

这其实是 JDK 低版本的一个 Bug。

1.6.30以上到1.7的全部版本已经确认有该问题,jdk8修复。

参考:UseAdaptiveSizePolicy与CMS垃圾回收同时使用导致的JVM报错https://www.cnblogs.com/moona...


四、问题小结

  1. 现阶段大多数应用使用 JDK 1.8,其默认回收器是 Parallel Scavenge,并且默认开启了 AdaptiveSizePolicy。
  2. AdaptiveSizePolicy 动态调整 Eden、Survivor 区的大小,存在将 Survivor 区调小的可能。当 Survivor 区被调小后,部分 YGC 后存活的对象直接进入老年代。老年代占用量逐渐上升从而触发 FGC,导致较长时间的 STW。
  3. 建议使用 CMS 垃圾回收器,默认关闭 AdaptiveSizePolicy。
  4. 建议在 JVM 参数中加上 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution,让 GC log 更加详细,方便定位问题。

五、参考资料

  1. Garbage Collector Ergonomics
  2. File,file.getPath(), getAbsolutePath(), getCanonicalPath()区别
  3. UseAdaptiveSizePolicy与CMS垃圾回收同时使用导致的JVM报错
  4. JVM分析工具概述
查看原文

赞 0 收藏 0 评论 1

阿菜 提出了问题 · 2018-08-27

如何获取JVM核心dump(core dump)?

想通过sa-jdi.jar中的ClassDump将虚拟机加载的类都dump下来。

ClassDump支持从jvm进程或者jvm core dump中dump类。

线上应用,不该直接对pid进行dump,想要先获取一个core dump。

在visual vm中,可以读取core dump文件。

clipboard.png

但是不知道如何获取core dump。

关注 3 回答 1

阿菜 提出了问题 · 2018-08-27

如何获取JVM核心dump(core dump)?

想通过sa-jdi.jar中的ClassDump将虚拟机加载的类都dump下来。

ClassDump支持从jvm进程或者jvm core dump中dump类。

线上应用,不该直接对pid进行dump,想要先获取一个core dump。

在visual vm中,可以读取core dump文件。

clipboard.png

但是不知道如何获取core dump。

关注 3 回答 1

阿菜 提出了问题 · 2018-08-27

如何获取JVM核心dump(core dump)?

想通过sa-jdi.jar中的ClassDump将虚拟机加载的类都dump下来。

ClassDump支持从jvm进程或者jvm core dump中dump类。

线上应用,不该直接对pid进行dump,想要先获取一个core dump。

在visual vm中,可以读取core dump文件。

clipboard.png

但是不知道如何获取core dump。

关注 3 回答 1

阿菜 发布了文章 · 2018-08-16

由「Metaspace容量不足触发CMS GC」从而引发的思考

转载请注明原文链接:https://www.jianshu.com/p/468...

某天早上,毛老师在群里问「cat 上怎么看 gc」。

好好的一个群

看到有 GC 的问题,立马做出小鸡搓手状。


之后毛老师发来一张图。

老年代内存占用情况

图片展示了老年代内存占用情况。

第一个大陡坡是应用发布,老年代内存占比下降,很正常。

第二个小陡坡,老年代内存占用突然下降,应该是发生了老年代 GC。

但奇怪的是,此时老年代内存占用并不高,发生 GC 并不是正常现象。

于是,毛老师查看了 GC log。

GC log

从 GC log 中可以看出,老年代发生了一次 CMS GC。

但此时老年代内存使用占比 = 234011K / 2621440k ≈ 9%。

而 CMS 触发的条件是:

老年代内存使用占比达到 CMSInitiatingOccupancyFraction,默认为 92%,

毛老师设置的是 75%。

-XX:CMSInitiatingOccupancyFraction = 75

于是排除老年代占用过高的可能。

接着分析内存状况。

Metaspace 内存占用情况

毛老师发现在老年代发生 GC 时,Metaspace 的内存占用也一起下降。

于是怀疑是 Metaspace 占用达到了设置的参数 MetaspaceSize,发生了 GC。

查看 JVM 参数设置,MetaspaceSize 参数被设置为128m。

-XX:MetaspaceSize = 128m -XX:MaxMetaspaceSize = 256m

问题的原因被集中在 Metaspace 上。

毛老师查看另外一个监控工具,发生小陡坡的纵坐标的确接近 128m。

此时,引发出另一个问题:

Metaspace 发生 GC,为何会引起老年代 GC。

于是,想到之前看过 阿飞Javaer 的文章 《JVM参数MetaspaceSize的误解》

其中有几个关键点:

Metaspace 在空间不足时,会进行扩容,并逐渐达到设置的 MetaspaceSize。

Metaspace 扩容到 -XX:MetaspaceSize 参数指定的量,就会发生 FGC。

如果配置了 -XX:MetaspaceSize,那么触发 FGC 的阈值就是配置的值。

如果 Old 区配置 CMS 垃圾回收,那么扩容引起的 FGC 也会使用 CMS 算法进行回收。

其中的关键点是:

如果老年代设置了 CMS,则 Metasapce 扩容引起的 FGC 会转变成一次 CMS。

查看毛老师配置的 JVM 参数,果然设置了 CMS GC。

-XX:+UseConcMarkSweepGC

于是,解决问题的方法是调整 -XX:MetaspaceSize = 256m。

从监控来看,设置 -XX:MaxMetaspaceSize = 256m 已经足够。

因为后期并不会引发 CMS GC。


GC 的问题算是解决了,但同时引发了以下几点思考:

  1. Metaspace 分配和扩容有什么规律?
  2. JDK 1.8 中的 Metaspace 和 JDK 1.7 中的 Perm 区有什么区别?
  3. 老年代回收设置成非 CMS 时,Metaspace 占用到达 -XX:MetaspaceSize 会引发什么 GC?
  4. 如何制造 Metasapce 内存占用上升?

关于这个问题一和问题二,阿飞Javaer 已经解释的比较清楚。

对于 Metaspce,其初始大小并不等于设置的 -XX:MetaspaceSize 参数。

随着类的加载,Metaspce 会不断进行扩容,直到达到 -XX:MetaspaceSize 触发 GC。

而至于如何设置 Metaspace 的初始大小,目前的确没有办法。

在 openjdk 的 bug 列表中,找到一个 关于 Metaspace 初始大小的 bug,并且尚未解决。

Add JVM option to set initial Metaspace size

对于问题二, 阿飞Javaer 在文章中也进行了说明。

Perm 的话,我们通过配置 -XX:PermSize 以及 -XX:MaxPermSize 来控制这块内存的大小。

JVM 在启动的时候会根据 -XX:PermSize 初始化分配一块连续的内存块。

这样的话,如果 -XX:PermSize 设置过大,就是一种赤果果的浪费。

关于 Metaspace,JVM 还提供了其余一些设置参数。

可以通过以下命令查看。

java -XX:+PrintFlagsFinal -version | grep Metaspace

关于 Metaspace 更多的内容,可以参考笨神的文章:《JVM源码分析之Metaspace解密》

问题三

Metaspace 占用到达 -XX:MetaspaceSize 会引发什么?

已经知道,当老年代回收设置成 CMS GC 时,会触发一次 CMS GC。

那么如果不设置为 CMS GC,又会发生什么呢?

使用以下配置进行一个小尝试,然后查看 GC log。

-Xmx2048m -Xms2048m -Xmn1024m 
-XX:MetaspaceSize=40m -XX:MaxMetaspaceSize=128m
-XX:+PrintGCDetails -XX:+PrintGCDateStamps 
-XX:+PrintHeapAtGC -Xloggc:d:/heap_trace.txt

该配置并未设置 CMS GC,JDK 1.8 默认的老年代回收算法为 ParOldGen。

本文测试的应用在启动完成后,占用 Metaspace 空间约为 63m,可通过 jstat 命令查看。

于是,设置 -XX:MetaspaceSize = 40m,期望发生一次 GC。

从 GC log 中,可以找到以下关键日志。

[GC (Metadata GC Threshold) 
[PSYoungGen: 360403K->47455K(917504K)] 360531K->47591K(1966080K), 0.0343563 secs] 
[Times: user=0.08 sys=0.00, real=0.04 secs] 

[Full GC (Metadata GC Threshold) 
[PSYoungGen: 47455K->0K(917504K)] 
[ParOldGen: 136K->46676K(1048576K)] 47591K->46676K(1966080K), 
[Metaspace: 40381K->40381K(1085440K)], 0.1712704 secs] 
[Times: user=0.42 sys=0.02, real=0.17 secs] 

可以看出,由于 Metasapce 到达 -XX:MetaspaceSize = 40m 时候,触发了一次 YGC 和一次 Full GC。

一般而言,我们对 Full GC 的重视度比对 YGC 高很多。

所以一般都会直描述,当 Metasapce 到达 -XX:MetaspaceSize 时会触发一次 Full GC。

问题四

如何人工模拟 Metaspace 内存占用上升?

Metaspace 是 JDK 1.8 之后引入的一个区域。

有一点可以肯定的,Metaspace 会保存类的描述信息。

JVM 需要根据 Metaspace 中的信息,才能找到堆中类 java.lang.Class 所对应的对象。(有点绕)

既然 Metaspace 中会保存类描述信息,可以通过新建类来增加 Metaspace 的占用。

于是想到,使用 CGlib 动态代理,生成被代理类的子类。

简单的 SayHello 类。

public class SayHello {
    public void say() {
        System.out.println("hello everyone");
    }
}

简单的代理类,使用 CGlib 生成子类。

public class CglibProxy implements MethodInterceptor {

    public Object getProxy(Class clazz) {
        Enhancer enhancer = new Enhancer();
        // 设置需要创建子类的类
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        enhancer.setUseCache(false);
        // 通过字节码技术动态创建子类实例
        return enhancer.create();
    }

    // 实现MethodInterceptor接口方法
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("前置代理");
        // 通过代理类调用父类中的方法
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("后置代理");
        return result;
    }
}

简单新建一个 Controller 用于测试生成 10000 个 SayHello 子类。

@RequestMapping(value = "/getProxy", method = RequestMethod.GET)
@ResponseBody
public void getProxy() {
    CglibProxy proxy = new CglibProxy();
    for (int i = 0; i < 10000; i++) {
        //通过生成子类的方式创建代理类
        SayHello proxyTmp = (SayHello) proxy.getProxy(SayHello.class);
        proxyTmp.say();
    }
}

应用启动完毕后,请求 /getProxy 接口,发现 Metaspace 空间占用上升。

CGlib 动态代理生成子类

从堆 Dump 中也可以发现,有很多被 CGlib 所代理的 SayHello 类对象。

堆 Dump 分析

代理类对应的 java.lang.Class 对象分配在堆内,类的描述信息在 Metaspace 中。

堆中有多个 Class 对象,可以推断出 Metasapce 需要装下很多类描述信息。

最后,当 Metaspace 使用空间超过设置的 -XX:MaxMetaspaceSize=128m 时,就会发生 OOM。

Exception in thread "http-nio-8080-exec-6" java.lang.OutOfMemoryError: Metaspace

从 GC log 中可以看到,JVM 会在 Metaspace 占用满之后,尝试 Full GC。

但会出现以下字样。

Full GC (Last ditch collection)

此外,还有一个问题。

当 Metaspace 内存占用达到 -XX:MetaspaceSize 时,Metaspace 只扩容,不会引起 Full GC。

当 Metaspace 内存占用达到 -XX:MetaspaceSize 时,会发生 Full GC。

在发生第一次 Full GC 之后,Metaspace 依然会扩容。

那么,第二次触发 Full GC 的条件是?

有文章说,在触发第一次F Full GC 后,之后 Metaspace 的每次扩容,都会引起 Full GC。

但观察本文测试的 GC log 和 jstat 命令查看 Metasapce 扩容状况,可以看出:

在第一次 Full GC 之后,之后 Metaspace 的扩容,并不一定会引起 Full GC。

触发一次 Full GC

从 jstat 输出可以看到,在触发一次 Full GC 之后,Metaspace 依旧发生了扩容,但未发生 Full GC。

jstat FGC 次数一直都是 1。

此外,使用 GClib 动态生成类,Metaspace 继续扩容,到一定程度,触发了 Full GC。

但触发 FGC 时,Metaspace 占比并没用明显的规律。

Metaspace 持续扩容再次触发 FGC

尝试了几次,由于 jstat 设置了 1s 钟输出一次,所以每次触发 Full GC 时候,MC 的数据都不一样,但基本是相同。

猜测在第一次 Full GC 之后,之后再次触发 Full GC 的阈值是有一定的计算公式的。

但具体如何计算,估计是需要深入源码了。


此外可以看到,每次 Metaspace 扩容时,都伴随着一次 YGC 或者 Full GC,不知道是否是巧合。

接着看到 占小狼 的文章 《JVM源码分析之垃圾收集的执行过程》

文章有一句话:

从上述分析中可以发现,gc操作的入口都位于GenCollectedHeap::do_collection方法中。
不同的参数执行不同类型的gc。

打开 openjdk 8 中的 GenCollectedHeap 类,查看 do_collection 方法。

可以看到,在 do_collection 方法中,有这个一段代码。

if (complete) {
  // Delete metaspaces for unloaded class loaders and clean up loader_data graph
  ClassLoaderDataGraph::purge();
  MetaspaceAux::verify_metrics();
  // Resize the metaspace capacity after full collections
  MetaspaceGC::compute_new_size();
  update_full_collections_completed();
}

其中最主要的是 MetaspaceGC::compute_new_size();

得出,YGC 和 Full GC 的确会重新计算 Metaspace 的大小。

至于是否进行扩容和缩容,则需要根据 compute_new_size() 方法的计算结果而定。

得出,Metasapce 扩容导致 GC 这个说法,其实是不准确的。

正确的过程是:新建类导致 Metaspace 容量不够,触发 GC,GC 完成后重新计算 Metaspace 新容量,决定是否对 Metaspace 扩容或缩容。


参考资料

  1. JVM参数MetaspaceSize的误解 https://www.jianshu.com/p/b44...
  2. JVM源码分析之垃圾收集的执行过程 https://www.jianshu.com/p/04e...
  3. JVM源码分析之Metaspace解密 http://lovestblog.cn/blog/201...
查看原文

赞 2 收藏 2 评论 0

阿菜 发布了文章 · 2018-08-16

由「Metaspace容量不足触发CMS GC」从而引发的思考

转载请注明原文链接:https://www.jianshu.com/p/468...

某天早上,毛老师在群里问「cat 上怎么看 gc」。

好好的一个群

看到有 GC 的问题,立马做出小鸡搓手状。


之后毛老师发来一张图。

老年代内存占用情况

图片展示了老年代内存占用情况。

第一个大陡坡是应用发布,老年代内存占比下降,很正常。

第二个小陡坡,老年代内存占用突然下降,应该是发生了老年代 GC。

但奇怪的是,此时老年代内存占用并不高,发生 GC 并不是正常现象。

于是,毛老师查看了 GC log。

GC log

从 GC log 中可以看出,老年代发生了一次 CMS GC。

但此时老年代内存使用占比 = 234011K / 2621440k ≈ 9%。

而 CMS 触发的条件是:

老年代内存使用占比达到 CMSInitiatingOccupancyFraction,默认为 92%,

毛老师设置的是 75%。

-XX:CMSInitiatingOccupancyFraction = 75

于是排除老年代占用过高的可能。

接着分析内存状况。

Metaspace 内存占用情况

毛老师发现在老年代发生 GC 时,Metaspace 的内存占用也一起下降。

于是怀疑是 Metaspace 占用达到了设置的参数 MetaspaceSize,发生了 GC。

查看 JVM 参数设置,MetaspaceSize 参数被设置为128m。

-XX:MetaspaceSize = 128m -XX:MaxMetaspaceSize = 256m

问题的原因被集中在 Metaspace 上。

毛老师查看另外一个监控工具,发生小陡坡的纵坐标的确接近 128m。

此时,引发出另一个问题:

Metaspace 发生 GC,为何会引起老年代 GC。

于是,想到之前看过 阿飞Javaer 的文章 《JVM参数MetaspaceSize的误解》

其中有几个关键点:

Metaspace 在空间不足时,会进行扩容,并逐渐达到设置的 MetaspaceSize。

Metaspace 扩容到 -XX:MetaspaceSize 参数指定的量,就会发生 FGC。

如果配置了 -XX:MetaspaceSize,那么触发 FGC 的阈值就是配置的值。

如果 Old 区配置 CMS 垃圾回收,那么扩容引起的 FGC 也会使用 CMS 算法进行回收。

其中的关键点是:

如果老年代设置了 CMS,则 Metasapce 扩容引起的 FGC 会转变成一次 CMS。

查看毛老师配置的 JVM 参数,果然设置了 CMS GC。

-XX:+UseConcMarkSweepGC

于是,解决问题的方法是调整 -XX:MetaspaceSize = 256m。

从监控来看,设置 -XX:MaxMetaspaceSize = 256m 已经足够。

因为后期并不会引发 CMS GC。


GC 的问题算是解决了,但同时引发了以下几点思考:

  1. Metaspace 分配和扩容有什么规律?
  2. JDK 1.8 中的 Metaspace 和 JDK 1.7 中的 Perm 区有什么区别?
  3. 老年代回收设置成非 CMS 时,Metaspace 占用到达 -XX:MetaspaceSize 会引发什么 GC?
  4. 如何制造 Metasapce 内存占用上升?

关于这个问题一和问题二,阿飞Javaer 已经解释的比较清楚。

对于 Metaspce,其初始大小并不等于设置的 -XX:MetaspaceSize 参数。

随着类的加载,Metaspce 会不断进行扩容,直到达到 -XX:MetaspaceSize 触发 GC。

而至于如何设置 Metaspace 的初始大小,目前的确没有办法。

在 openjdk 的 bug 列表中,找到一个 关于 Metaspace 初始大小的 bug,并且尚未解决。

Add JVM option to set initial Metaspace size

对于问题二, 阿飞Javaer 在文章中也进行了说明。

Perm 的话,我们通过配置 -XX:PermSize 以及 -XX:MaxPermSize 来控制这块内存的大小。

JVM 在启动的时候会根据 -XX:PermSize 初始化分配一块连续的内存块。

这样的话,如果 -XX:PermSize 设置过大,就是一种赤果果的浪费。

关于 Metaspace,JVM 还提供了其余一些设置参数。

可以通过以下命令查看。

java -XX:+PrintFlagsFinal -version | grep Metaspace

关于 Metaspace 更多的内容,可以参考笨神的文章:《JVM源码分析之Metaspace解密》

问题三

Metaspace 占用到达 -XX:MetaspaceSize 会引发什么?

已经知道,当老年代回收设置成 CMS GC 时,会触发一次 CMS GC。

那么如果不设置为 CMS GC,又会发生什么呢?

使用以下配置进行一个小尝试,然后查看 GC log。

-Xmx2048m -Xms2048m -Xmn1024m 
-XX:MetaspaceSize=40m -XX:MaxMetaspaceSize=128m
-XX:+PrintGCDetails -XX:+PrintGCDateStamps 
-XX:+PrintHeapAtGC -Xloggc:d:/heap_trace.txt

该配置并未设置 CMS GC,JDK 1.8 默认的老年代回收算法为 ParOldGen。

本文测试的应用在启动完成后,占用 Metaspace 空间约为 63m,可通过 jstat 命令查看。

于是,设置 -XX:MetaspaceSize = 40m,期望发生一次 GC。

从 GC log 中,可以找到以下关键日志。

[GC (Metadata GC Threshold) 
[PSYoungGen: 360403K->47455K(917504K)] 360531K->47591K(1966080K), 0.0343563 secs] 
[Times: user=0.08 sys=0.00, real=0.04 secs] 

[Full GC (Metadata GC Threshold) 
[PSYoungGen: 47455K->0K(917504K)] 
[ParOldGen: 136K->46676K(1048576K)] 47591K->46676K(1966080K), 
[Metaspace: 40381K->40381K(1085440K)], 0.1712704 secs] 
[Times: user=0.42 sys=0.02, real=0.17 secs] 

可以看出,由于 Metasapce 到达 -XX:MetaspaceSize = 40m 时候,触发了一次 YGC 和一次 Full GC。

一般而言,我们对 Full GC 的重视度比对 YGC 高很多。

所以一般都会直描述,当 Metasapce 到达 -XX:MetaspaceSize 时会触发一次 Full GC。

问题四

如何人工模拟 Metaspace 内存占用上升?

Metaspace 是 JDK 1.8 之后引入的一个区域。

有一点可以肯定的,Metaspace 会保存类的描述信息。

JVM 需要根据 Metaspace 中的信息,才能找到堆中类 java.lang.Class 所对应的对象。(有点绕)

既然 Metaspace 中会保存类描述信息,可以通过新建类来增加 Metaspace 的占用。

于是想到,使用 CGlib 动态代理,生成被代理类的子类。

简单的 SayHello 类。

public class SayHello {
    public void say() {
        System.out.println("hello everyone");
    }
}

简单的代理类,使用 CGlib 生成子类。

public class CglibProxy implements MethodInterceptor {

    public Object getProxy(Class clazz) {
        Enhancer enhancer = new Enhancer();
        // 设置需要创建子类的类
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        enhancer.setUseCache(false);
        // 通过字节码技术动态创建子类实例
        return enhancer.create();
    }

    // 实现MethodInterceptor接口方法
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("前置代理");
        // 通过代理类调用父类中的方法
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("后置代理");
        return result;
    }
}

简单新建一个 Controller 用于测试生成 10000 个 SayHello 子类。

@RequestMapping(value = "/getProxy", method = RequestMethod.GET)
@ResponseBody
public void getProxy() {
    CglibProxy proxy = new CglibProxy();
    for (int i = 0; i < 10000; i++) {
        //通过生成子类的方式创建代理类
        SayHello proxyTmp = (SayHello) proxy.getProxy(SayHello.class);
        proxyTmp.say();
    }
}

应用启动完毕后,请求 /getProxy 接口,发现 Metaspace 空间占用上升。

CGlib 动态代理生成子类

从堆 Dump 中也可以发现,有很多被 CGlib 所代理的 SayHello 类对象。

堆 Dump 分析

代理类对应的 java.lang.Class 对象分配在堆内,类的描述信息在 Metaspace 中。

堆中有多个 Class 对象,可以推断出 Metasapce 需要装下很多类描述信息。

最后,当 Metaspace 使用空间超过设置的 -XX:MaxMetaspaceSize=128m 时,就会发生 OOM。

Exception in thread "http-nio-8080-exec-6" java.lang.OutOfMemoryError: Metaspace

从 GC log 中可以看到,JVM 会在 Metaspace 占用满之后,尝试 Full GC。

但会出现以下字样。

Full GC (Last ditch collection)

此外,还有一个问题。

当 Metaspace 内存占用达到 -XX:MetaspaceSize 时,Metaspace 只扩容,不会引起 Full GC。

当 Metaspace 内存占用达到 -XX:MetaspaceSize 时,会发生 Full GC。

在发生第一次 Full GC 之后,Metaspace 依然会扩容。

那么,第二次触发 Full GC 的条件是?

有文章说,在触发第一次F Full GC 后,之后 Metaspace 的每次扩容,都会引起 Full GC。

但观察本文测试的 GC log 和 jstat 命令查看 Metasapce 扩容状况,可以看出:

在第一次 Full GC 之后,之后 Metaspace 的扩容,并不一定会引起 Full GC。

触发一次 Full GC

从 jstat 输出可以看到,在触发一次 Full GC 之后,Metaspace 依旧发生了扩容,但未发生 Full GC。

jstat FGC 次数一直都是 1。

此外,使用 GClib 动态生成类,Metaspace 继续扩容,到一定程度,触发了 Full GC。

但触发 FGC 时,Metaspace 占比并没用明显的规律。

Metaspace 持续扩容再次触发 FGC

尝试了几次,由于 jstat 设置了 1s 钟输出一次,所以每次触发 Full GC 时候,MC 的数据都不一样,但基本是相同。

猜测在第一次 Full GC 之后,之后再次触发 Full GC 的阈值是有一定的计算公式的。

但具体如何计算,估计是需要深入源码了。


此外可以看到,每次 Metaspace 扩容时,都伴随着一次 YGC 或者 Full GC,不知道是否是巧合。

接着看到 占小狼 的文章 《JVM源码分析之垃圾收集的执行过程》

文章有一句话:

从上述分析中可以发现,gc操作的入口都位于GenCollectedHeap::do_collection方法中。
不同的参数执行不同类型的gc。

打开 openjdk 8 中的 GenCollectedHeap 类,查看 do_collection 方法。

可以看到,在 do_collection 方法中,有这个一段代码。

if (complete) {
  // Delete metaspaces for unloaded class loaders and clean up loader_data graph
  ClassLoaderDataGraph::purge();
  MetaspaceAux::verify_metrics();
  // Resize the metaspace capacity after full collections
  MetaspaceGC::compute_new_size();
  update_full_collections_completed();
}

其中最主要的是 MetaspaceGC::compute_new_size();

得出,YGC 和 Full GC 的确会重新计算 Metaspace 的大小。

至于是否进行扩容和缩容,则需要根据 compute_new_size() 方法的计算结果而定。

得出,Metasapce 扩容导致 GC 这个说法,其实是不准确的。

正确的过程是:新建类导致 Metaspace 容量不够,触发 GC,GC 完成后重新计算 Metaspace 新容量,决定是否对 Metaspace 扩容或缩容。


参考资料

  1. JVM参数MetaspaceSize的误解 https://www.jianshu.com/p/b44...
  2. JVM源码分析之垃圾收集的执行过程 https://www.jianshu.com/p/04e...
  3. JVM源码分析之Metaspace解密 http://lovestblog.cn/blog/201...
查看原文

赞 2 收藏 2 评论 0

阿菜 关注了问题 · 2018-08-13

CMS GC的初始标记阶段到底标记了哪些对象?

最近在看CMS GC,发现不同文章对CMS GC的初始标记阶段(initial mark)阶段的描述不一。

文章一

https://plumbr.io/handbook/ga...

clipboard.png

mark all the objects in the Old Generation that are either direct GC roots or are referenced from some live object in the Young Generation

按该文章阐述:
初始标记阶段,标记所有GCRoot直达的老年代对象和被年轻代对象直接引用的老年代对象。
说明初始标记阶段只标记老年代对象。


文章二

http://www.cnblogs.com/little...

clipboard.png

按该文章阐述:
初始标记阶段,标记所有GCRoot直达的对象,GCRoot可达的对象包括年轻代和老年代对象。


文章三

https://blogs.oracle.com/poon...

clipboard.png

按该文章阐述:
标记的也是所有GCRoot直达的对象。


请问:
CMS GC的初始标记阶段到底标记了哪些对象?
求推荐高质量的关于CMS GC的文章和论文。

关注 1 回答 0

阿菜 关注了问题 · 2018-08-13

CMS GC的初始标记阶段到底标记了哪些对象?

最近在看CMS GC,发现不同文章对CMS GC的初始标记阶段(initial mark)阶段的描述不一。

文章一

https://plumbr.io/handbook/ga...

clipboard.png

mark all the objects in the Old Generation that are either direct GC roots or are referenced from some live object in the Young Generation

按该文章阐述:
初始标记阶段,标记所有GCRoot直达的老年代对象和被年轻代对象直接引用的老年代对象。
说明初始标记阶段只标记老年代对象。


文章二

http://www.cnblogs.com/little...

clipboard.png

按该文章阐述:
初始标记阶段,标记所有GCRoot直达的对象,GCRoot可达的对象包括年轻代和老年代对象。


文章三

https://blogs.oracle.com/poon...

clipboard.png

按该文章阐述:
标记的也是所有GCRoot直达的对象。


请问:
CMS GC的初始标记阶段到底标记了哪些对象?
求推荐高质量的关于CMS GC的文章和论文。

关注 1 回答 0

阿菜 赞了文章 · 2018-08-13

如何成为一位「不那么差」的程序员

前言

已经记不清有多少读者问过:

博主,你是怎么学习的?像我这样的情况有啥好的建议嘛?

也不知道啥时候我居然成人生导师了。当然我不排斥这些问题,和大家交流都是学习的过程。

因此也许诺会准备一篇关于学习方面的文章;所以本文其实准备了很久,篇幅较长,大家耐心看完希望能有收获。

以下内容仅代表我从业以来所积累的相关经验,我会从硬技能、软实力这些方面尽量阐述我所认为的 “不那么差的程序员” 应当做到哪些技能。

技能树

作为一名码代码的技术工人,怎么说干的还是技术活。

既然是技术活那专业实力就得过硬,下面我会按照相关类别谈谈我们应该掌握哪些。

计算机基础

一名和电脑打交道的工种,计算机是我们赖以生存的工具。所以一些基础技能是我们应该和必须掌握的。

比如网络相关的知识。

其中就包含了 TCP 协议,它和 UDP 的差异。需要理解 TCP 三次握手的含义,拆、粘包等问题。

当然上层最常见的 HTTP 也需要了解,甚至是熟悉。

这块推荐《图解 HTTP》一书。

接着是操作系统相关知识。

由于工作后你写的大部分代码都是运行在 Linux 服务器上,所以对于这个看它脸色行事主你也得熟悉才行。

比如进程、线程、内存等概念;服务器常见的命令使用,这个没啥窍门就是得平时多敲敲多总结。

我也是之前兼职了半年运维才算是对这一块比较熟悉。

Linux 这个自然是推荐业界非常出名的《鸟哥的 Linux 私房菜》

当作为一个初学者学习这些东西时肯定会觉得枯燥乏味,大学一般在讲专业课之前都会有这些基础学科。我相信大部分同学应该都没怎么仔细听讲,因为确实这些东西就算是学会了记熟了也没有太多直接的激励。

但当你工作几年之后会发现,只要你还在做计算机相关的工作,这些都是绕不开的,当哪天这些知识不经意的帮助到你时你会庆幸当初正确的选择。

数据结构与算法

接下来会谈到另一门枯燥的课程:数据结构。

这块当初在大学时也是最不受待见的一门课程,也是我唯一挂过的科目。

记得当时每次上课老师就让大家用 C 语言练习书上的习题,看着一个个拆开都认识的字母组合在一起就六亲不认我果断选择了放弃。

这也造成现在的我每隔一段时间就要看二叉树、红黑树、栈、队列等知识,加深印象。

算法这个东西我确实没有啥发言权,之前坚持刷了部分 LeetCode 的题目也大多停留在初中级。

但像基本的查找、排序算法我觉得还是要会的,不一定要手写出来但要理解其思路。

所以强烈建议还在大学同学们积极参与一些 ACM 比赛,绝对是今后的加分利器。

这一块内容可能会在应届生校招时发挥较大作用,在工作中如果你的本职工作是 Java Web 开发的话,这一块涉猎的几率还是比较低。

不过一旦你接触到了模型设计、中间件、高效存储、查询等内容这些也是绕不过的坎。

这块内容和上面的计算机基础差不多,对于我们 Java 开发来说我觉得平时除了多刷刷 LeetCode 加深印象之外,在日常开发中每选择一个容器存放数据时想想为什么选它?有没有更好的存储方式?写入、查询效率如何?

同样的坚持下去,今后肯定收货颇丰。

同时推荐《算法(第4版)》

Java 基础

这里大部分的读者都是 Java 相关,所以这个强相关的技能非常重要。

Java 基础则是走向 Java 高级的必经之路。

这里抛开基本语法不谈,重点讨论实际工作中高频次的东西。

  • 基本容器,如:HashMap、ArrayList、HashSet、LinkedList 等,不但要会用还得了解其中的原理。这样才能在不同的场景选择最优的设计。
  • IO、NIO 也是需要掌握。日常开发中大部分是在和磁盘、网络(写日志、数据库、Redis)打交道,这些都是 IO 的过程。
  • 常见的设计模式如:代理、工厂、回调、构建者模式,这对开发灵活、扩展性强的应用有很大帮助。
  • Java 多线程是非常重要的特性,日常开发很多。能理解线程模型、多线程优缺点、以及如何避免。
  • 良好的单测习惯,很多人觉得写单测浪费时间没有意义。但正是有了单测可以提前暴露出许多问题,减少测试返工几率,提高代码质量。
  • 良好的编程规范,这个可以参考《阿里巴巴 Java 开发手册》以及在它基础上优化的《唯品会 Java 手册》
 《Java核心技术·卷 I》值得推荐。

多线程应用

有了扎实的基础之后来谈谈多线程、并发相关的内容。

想让自己的 title 里加上“高级”两字肯定得经过并发的洗礼。

这里谈论的并发主要是指单应用里的场景,多应用的可以看后文的分布式内容。

多线程的出现主要是为了提高 CPU 的利用率、任务的执行效率。但并不是用了多线程就一定能达到这样的效果,因为它同时也带来了一些问题:

  • 上下文切换
  • 共享资源
  • 可见性、原子性、有序性等。

一旦使用了多线程那肯定会比单线程的程序要变得复杂和不可控,甚至使用不当还会比单线程慢。所以要考虑清楚是否真的需要多线程。

会用了之后也要考虑为啥多线程会出现那样的问题,这时就需要理解内存模型、可见性之类的知识点。

同样的解决方式又有哪些?各自的优缺点也需要掌握。

谈到多线程就不得不提并发包下面的内容 java.util.concurrent

最常用及需要掌握的有:

这一块的内容可以然我们知道写 JDK 大牛处理并发的思路,对我们自己编写高质量的多线程程序也有很多帮助。

推荐《Java 并发编程的艺术》很好的并发入门书籍。

JVM 虚拟机

想要深入 Java ,JVM 是不可或缺的。对于大部分工作 1~3 年的开发者来说直接接触这一些内容是比较少的。

到了 3~5 年这个阶段就必须得了解了,以下内容我觉得是必须要掌握的:

掌握这些内容真的对实际分析问题起到巨大帮助。

 对此强力推荐《深入理解Java虚拟机》,这本书反反复复看过好几遍,每个阶段阅读都有不同的收获。

数据库

做 WEB 应用开发的同学肯定要和数据库打不少交道,而且通常来说一个系统最先出现瓶颈往往都是数据库,说数据库是压到系统的最后一根稻草一点也不为过。

所以对数据库的掌握也是非常有必要。拿互联网用的较多的 MySQL 数据库为例,一些必须掌握的知识点:

  • 索引的数据结构及原理、哪些字段应当创建索引。
  • 针对于一个慢 SQL 的优化思路。
  • 数据库水平垂直拆分的方案,需要了解业界常用的 MyCAT、sharding-sphere 等中间件。

常规使用可以参考《阿里巴巴 Java 开发手册》中的数据库章节,想要深入了解 MySQL 那肯定得推荐经典的《高性能 MySQL》一书了。

分布式技术

随着互联网的发展,传统的单体应用越来越不适合现有场景。

因此分布式技术出现了,这块涵盖的内容太多了,经验有限只能列举我日常使用到的一些内容:

  • 首先是一些基础理论如:CAP 定理,知道分布式系统会带来的一些问题以及各个应用权衡的方式。
  • 了解近些年大热的微服务相关定义、来源以及对比,有条件的可以阅读 martin fowler 的原文 Microservices,或者也可以搜索相关的国内翻译。
  • 对 Dubbo、SpringCloud 等分布式框架的使用,最好是要了解原理。
  • 接着要对分布式带来的问题提出解决方案。如分布式锁分布式限流、分布式事务、分布式缓存、分布式 ID、消息中间件等。
  • 也要了解一些分布式中的负载算法:权重、Hash、一致性 Hash、故障转移、LRU 等。
  • 最好能做一个实践如:[秒杀架构实践

](https://crossoverjie.top/%2F2...

之前有开源一个分布式相关解决组件:

https://github.com/crossoverJie/distributed-redis-tool

同时推荐一本入门科普《大型网站技术架构》,出版时间有点早,从中可以学习一些思路。

懂点架构

相信大家都有一个架构师的梦想。

架构师给人的感觉就是画画图纸,搭好架子,下面的人员来添砖加瓦最终产出。

但其实需要的内功也要非常深厚,就上面列举的样样需要掌握,底层到操作系统、算法;上层到应用、框架都需要非常精通。(PPT 架构师除外)

我自身参与架构经验也不多,所以只能提供有限的建议。

首先分布式肯定得掌握,毕竟现在大部分的架构都是基于分布式的。

这其中就得根据 CAP 理论结合项目情况来选择一致性还是可用性,同时如何做好适合现有团队的技术选型。

这里推荐下开涛老师的《亿级流量网站架构核心技术》,列举了很多架构实例,不过网上褒贬不一,但对于刚入门架构的能科普不少知识。

如何学习

谈完了技能树,现在来聊聊如何学习,这也是被问的最多的一个话题。

而关于学习讨论的最多的也是看视频还是看书?

视频

不得不承认视频是获取知识最便捷的来源,毕竟包含了图、文、声。

大学几年时间其实我也没好好上专业课,我记得真正入门 Java 还是一个暑假花了两个月的时间天天在家里看 ”马士兵“ 老师的视频教程,当时的资源也很老了,记得好像是 07 年出的视频(用的还是 Google )。

那段时间早起晚睡,每天学到东西之后马上实践,心里也很有成就感。后来开学之后一度成为同学们眼中的”学霸“人物。

 现在打开我 12 年的电脑,硬盘里还躺着好几十 G 的教学视频。

看书

工作后时间真的很宝贵,完全没有了学生生涯的想学就学的自由。所以现在我主要知识来源还是书籍。

这些是我最近看的书:

IMG_2387.JPG

看书又会涉及到电子书和纸质书的区别,我个人比较喜欢纸质书。毕竟我可以方便的记笔记以及可以随时切换章节。最主要的还是从小养成的闻书香的习惯。

知识付费

近几年知识付费越来越流行,许多大佬也加入了这个行列,人们也逐渐在习惯为知识去付费。

说实话写一好篇文章出一份视频都非常不容易,能有正向的激励,作者才能持续输出更好的内容。

这块我觉得国内做的比较好我也为之付费的有极客时间、大佬的知识星球等。

这三点没有绝对的好坏之分,其实可以看出我刚入门的时候看视频,工作之后看书及知识付费内容。

视频的好处是可以跟着里面老师的思路一步一步往下走,比较有音视频代入感强,就像学校老师讲课一样。

但由于内容较长使读者没法知晓其中的重点,甚至都不敢快进生怕错过了哪个重要知识,现在由于 IT 越来越火,网上的视频也很多导致质量参差不齐也不成体系。

而看书可以选择性的浏览自己感兴趣的章节,费解的内容也方便反复阅读

所以建议刚入门的同学可以看看视频跟着学,参与工作一段时间后可以尝试多看看书。

当然这不是绝对的,找到适合自己的学习方式就好。但不管是视频还是看书都要多做多实践。

打造个人品牌

个人品牌看似很程序员这个职业不怎么沾边,但在现今的互联网时代对于每个人来说都很重要。

以往我们在写简历或是评估他人简历的时候往往不会想到去网络搜索他的个人信息,但在这个信息爆炸的时代你在网上留下的一点印记都能被发现。

博客

因此我们需要维护好自己的名片,比如先搭建自己的个人博客。

博客的好处我也谈过几次了,前期关注人少没关系,重要的是坚持,当你写到 50、100篇文章后你会发现自己在这过程中一定是的到了提高。

GitHub

第二点就和技术人比较相关了:参与维护好自己的 GitHub。

由于 GitHub 的特殊属性,维护好后可以更好的打造个人品牌。

Talk is cheap. Show me the code 可不是随便说说的。

想要维护好可以从几个方面着手:

  • 参与他人的项目,不管是代码库还是知识库都可以,先融入进社区。
  • 发起自己的开源项目,不管是平时开发过程中的小痛点,还是精心整理的知识点都可以。

但这过程中有几点还是要注意:

  • 我们需要遵守 GitHub 的社交礼仪。能用英文尽量就用英文,特别是在国外厂库中。
  • 尽量少 push 一些与代码工作无关的内容,我认为这并不能提高自己的品牌。
  • 别去刷 star。这也是近期才流行起来,不知道为什么总有一些人会钻这种空子,刷起来的热度对自己并没有任何提高。

这里有一篇国外大佬写的 How to build your personal brand as a new developer :

https://medium.freecodecamp.org/building-your-personal-brand-as-a-new-web-developer-f6d4150fd217

English 挺重要

再来谈谈英语的重要性,我记得刚上大学时老师以及一些培训机构都会说:

别怕自己英语差就学不了编程,真正常用的就那些词语。

这句话虽没错,但英语在对 IT 这行来说还是有着极大的加分能力。

拿常见的 JDK 里的源码注释也是纯英文的,如果英语还不错的话,一些 Spring 的东西完全可以自学,直接去 Spring 官网就可以查看,甚至后面出的 SpringCloud,官方资料就是最好的教程。

再有就是平时查资料时,有条件的可以尝试用 Google + 英文 搜索,你会发现新的世界。

不然也不会有面向 Google/Stack Overflow 编程。

对于英语好的同学自然不怕,那不怎么好的咋办呢?

比如我,但我在坚持以下几点:

  • 所有的手机、电脑系统统统换成英语语言,养成习惯(不过也有尴尬的连菜单都找不到的情况)。
  • 订阅一些英语周刊,比如 ”湾区日报“。
  • 定期去类似于 https://medium.com/ 这样具有影响力的国外社区阅读文章。

虽然现在我也谈不上多好,但目前我也在努力,希望大家也一起坚持。

推荐一本近期在看的书《程序员的英语》。

保持竞争力

技术这个行业发展迅速、变化太快,每年也都有无数相关行业毕业生加入竞争,稍不留神就会被赶上甚至超越。

所以我们无时无刻都得保持竞争力。

多的谈不上,我只能谈下目前我在做的事情:

  • 打好基础。不是学了之后就忘了,需要不停的去看,巩固,基础是万变不离其宗的。
  • 多看源码,了解原理,不要停留在调参侠的境界。
  • 关注行业发展、新技术、新动态至少不能落伍了。
  • 争取每周产出一篇技术相关文章。
  • 积极参与开源项目。

思维导图

结合上文产出了一个思维导图更直观些。

总结

本文结合了自身的一些经验列举了一些方法,不一定对每位都有效需要自行判断。

也反反复复写了差不多一周的时间,希望对在这条路上和正在路上的朋友们起到一些作用。

大部分都只是谈了个思路,其实每一项单聊都能写很多。每个点都有推荐一本书籍,有更好建议欢迎留言讨论。

上文大部分的知识点都有维护在 GitHub 上,感兴趣的朋友可以自行查阅:

https://github.com/crossoverJie/Java-Interview

文中有贴了一些站外链接,阅读原文可以方便查看

你的点赞与转发是最大的支持。

查看原文

赞 156 收藏 126 评论 12

认证与成就

  • 获得 10 次点赞
  • 获得 74 枚徽章 获得 2 枚金徽章, 获得 24 枚银徽章, 获得 48 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-07-27
个人主页被 601 人浏览