前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一次帮助云上客户 JVM 调优的记录

一次帮助云上客户 JVM 调优的记录

原创
作者头像
鲍远林
修改2021-01-04 16:52:07
2.4K0
修改2021-01-04 16:52:07
举报
文章被收录于专栏:泛互云原生泛互云原生

年前帮一个网约车客户的系统做了次 JVM 参数调优,优化后的GC性能有较大提升,GC 次数和耗时都降了超 33%。优化无止境,对于文中的部分参数虽然可以更激进些,但毕竟是在帮客户做优化,没有之前做 2C 的方便。下面是优化后的客户反馈截图:

该客户用的腾讯云 4C8G 的 CVM,Java 8 跑的应用。虽然时下 Java 15 都出来了,但 Oracle 的商业策略,使得 Java 8 仍旧是大家的最爱。回顾优化前的 JVM 的配置,当时的配置是这样的:

第一眼看到上面的配置时,就感到惊讶,除了不合理的参数外,还有些过时的。问客户哪里来的这份参数时,得到的回复是从网上的 JVM 优化文章中摘取的。JVM 的优化文章非常的多,多到让人看花眼,所以大家一定要区别对待,不要全盘接受!下面针对此次优化,详细描述下调整的参数,以及调整的原由。

1. JVM 参数及默认值

Java 每个大版本间,都会有些参数变动,所以在写 JVM 配置前,一定要先看下你选择的参数是否还支持。Oracle 官网文档肯定是个途径,但不是最便捷的。下面这条命令可以立马查看所有参数及其默认值,而且还不用运行应用。

代码语言:shell
复制
java -XX:+PrintFlagsFinal -version

加上 grep 立马能看到参数的默认值,比如在我的机器上 ParallelGCThreads 默认值为 4。

Java 8 使用 Metaspace 替代了 PermGen,所以 PermGen 相关的参数要么不再支持要么被替换了。所以客户配置中的两个参数 CMSPermGenSweepingEnabledCMSInitiatingPermOccupancyFraction 是有问题的,用上面说的方法肯定是在 JVM 参数集中找不到的。其实,带上这两个参数运行下 java -version立即就能看到一个被替换了,一个不再支持。因而,作为优化的第一步,就是要去除这两个过时参数。

2. 根据应用特性调整新生代大小

由于业务数据动态性太强,每隔一段时间司机APP就会上报一次位置坐标信息,因而对于GPS上报后端服务来说,昙花一现的短寿对象占比很高。所以新生代和对象晋升的优化对性能的提升是非常有效的。

2.1 优化新生代大小

在GPS上报这种业务场景下,新生代设置小的话就会加大YGC的频次,但需要注意的是,加大新生代也会加大每次GC的停顿时间。从客户的配置 -Xms4608M -Xmx4608M -Xmn1536M 来看,4G 的堆划分了 1.5G 的新生代,显然有调整空间。优化时,去除了 -Xmn 使用-XX:NewRatio=1让新生代和老年代平分堆大小。

2.2 优化对象晋升

-XX:MaxTenuringThreshold 该参数是控制对象从新生代晋升到老年代所要经过的 Young GC 最大次数(JVM会动态决策,比如 survivor 大小、内存使用情况等,因而有可能小于该值也晋升了),即对象在 Survivor 区最多熬过多少次 Young GC 后晋升到年老代。JVM 中,在对象头里用4个bit来存储,因而最大值是15。但使用了 CMS 后默认值为 6,Java 8 的官方文档有说明,当然也可以使用前面所述的方法进行验证。

回头看客户配置 -XX:MaxTenuringThreshold=9 比使用了 CMS 下的默认值大。 优化时改成了默认值 6 加快对象晋升,提升 survivor 区利用,开启 -XX:+PrintTenuringDistribution 对比变更前后变化比较明显。

3. GC线程数

3.1 ParallelGCThreads

2013年 Google, Twitter, Oracle 一帮大神们对 ParallelGCThreads 和 ConcGCThreads 做了深入优化,对细节感兴趣的,可翻下大神们当初的讨论邮件:Better default for ParallelGCThreads and ConcGCThreads by using number of physical cores and CPU mask.

ParallelGCThreads 是使用 ParNew 收集器时,使用的并行GC线程数。在 JDK 的源码中,ParallelGCThreads 是在 Abstract_VM_Version::calc_parallel_worker_threads 进行初始化的。从下面的OpenJdk 8 源码可以看出,CPU数量决定了ParallelGCThreads 的取值(如果CPU开启了超线程,ncpus 实际上是逻辑CPU数):

  • 当CPU数量小于8时,ParallelGCThreads = ncpus
  • 当CPU数量大于8时,ParallelGCThreads = 8 + (ncpus - 8 ) ( 5/8 )
代码语言:c++
复制
unsigned int Abstract_VM_Version::nof_parallel_worker_threads(
                                                      unsigned int num,
                                                      unsigned int den,
                                                      unsigned int switch_pt) {
  if (FLAG_IS_DEFAULT(ParallelGCThreads)) {
    assert(ParallelGCThreads == 0, "Default ParallelGCThreads is not 0");
    // For very large machines, there are diminishing returns
    // for large numbers of worker threads.  Instead of
    // hogging the whole system, use a fraction of the workers for every
    // processor after the first 8.  For example, on a 72 cpu machine
    // and a chosen fraction of 5/8
    // use 8 + (72 - 8) * (5/8) == 48 worker threads.
    unsigned int ncpus = (unsigned int) os::active_processor_count();
    return (ncpus <= switch_pt) ?
           ncpus :
          (switch_pt + ((ncpus - switch_pt) * num) / den);
  } else {
    return ParallelGCThreads;
  }
}

unsigned int Abstract_VM_Version::calc_parallel_worker_threads() {
  return nof_parallel_worker_threads(5, 8, 8);
}

3.2 ConcGCThreads

CMS默认启动的并发线程数 ConcGCThreads =(ParallelGCThreads + 3)/4,具体可见OpenJdk 8 源码 concurrentMarkSweepGeneration.cpp

代码语言:txt
复制
// Support for multi-threaded concurrent phases
if (CMSConcurrentMTEnabled) {
	if (FLAG_IS_DEFAULT(ConcGCThreads)) {
	  // just for now
	  FLAG_SET_DEFAULT(ConcGCThreads, (ParallelGCThreads + 3)/4);
	}
	……
}

对于 ParallelGCThreads 和 ConcGCThreads 虽然有很多优化文章,很多优化其实用的都是 JVM 同样的线程计算方法,因而保持默认值就好。比如 Hbase, Spark 的官方优化中都建议的优化设置是:-XX:ParallelGCThreads=8+(logical processors-8)(5/8)。还有提供 Java 性能优化的公司 Tier1 App 在其 facebook 的文章 THREAD DUMP ANALYSIS PATTERN – SEVERAL SCAVENGERS 中提供的优化方法也一样,不同的是对 ConcGCThreads 做了小变动,由于文中没说具体场景,本次优化没有使用,有兴趣的可以验证下。

4. CMS 相关优化

4.1 DisableExplicitGC 后 ExplicitGCInvokesConcurrent 还香吗?

ExplicitGCInvokesConcurrent 表示因 System.gc() 触发的 Full GC 时,是否以并发方式进行收集。下面是 OpenJdk 8 源码 globals.hpp 中关于此参数的定义,其默认值 false。

代码语言:c++
复制
product(bool, ExplicitGCInvokesConcurrent, false,                 \
      "A System.gc() request invokes a concurrent collection; "   \
      "(effective only when UseConcMarkSweepGC)")

打开此参数后,在遇到 System.gc() 时,会开启 background 模式,即并行 FULL GC,从而提高 FULL GC 的效率。这里需要注意的是 System.gc() 不能禁用,JDK 源码中GenCollectedHeap::should_do_concurrent_full_gc(GCCause::Cause cause) 里有具体的校验逻辑:

代码语言:c++
复制
// genCollectedHeap.cpp
bool GenCollectedHeap::should_do_concurrent_full_gc(GCCause::Cause cause) {
  return UseConcMarkSweepGC &&
         ((cause == GCCause::_gc_locker && GCLockerInvokesConcurrent) ||
         // System.gc() 且开启 ExplicitGCInvokesConcurrent
          (cause == GCCause::_java_lang_system_gc && ExplicitGCInvokesConcurrent)); 
}

// gcCause.cpp
const char* GCCause::to_string(GCCause::Cause cause) {
  switch (cause) {
    case _java_lang_system_gc:
      return "System.gc()";
      ……
}

回过头看下优化前的客户设置:-XX:+ExplicitGCInvokesConcurrent -XX:+DisableExplicitGC,显然有失偏颇。优化时,去除了 -XX:+DisableExplicitGC,保留 -XX:+ExplicitGCInvokesConcurrent。只要自己的代码里面没有使用 System.gc(),保持 DisableExplicitGC 默认值 false 并开启 ExplicitGCInvokesConcurrent,可以防止依赖包里使用了 System.gc() 引发的其它问题。比如 1.0.0 后的 Kafka 为了避免 direct memory 引发的 OOM,也是去除了 -XX:+DisableExplicitGC,并开启 -XX:+ExplicitGCInvokesConcurrent。

-XX:+DisableExplicitGC was replaced by -XX:+ExplicitGCInvokesConcurrent in the default JVM settings. This helps avoid out of memory exceptions during allocation of native memory by direct buffers in some cases.

4.2 CMSFullGCsBeforeCompaction 已被弃用

客户配置的参数 -XX:CMSFullGCsBeforeCompaction=9 其实已被 JVM 弃用,启动日志也说的很明白:

Java HotSpot(TM) 64-Bit Server VM warning: CMSFullGCsBeforeCompaction is deprecated and will likely be removed in a future release.

CMSFullGCsBeforeCompaction 默认值为0,即每次 FGC 时都对老年代内存进行压缩,整理碎片。设置成 9,意味着每隔10次 FGC 才做一次压缩,很容易使得 old gen 因内存碎片问题,导致 promotion failure,所以在优化时去除了。

代码语言:c++
复制
product(uintx, CMSFullGCsBeforeCompaction, 0,                      \
    "Number of CMS full collection done before compaction if > 0") 

JDK 源码中有两个方法:Arguments::check_deprecated_gcs()Arguments::check_deprecated_gc_flags() 专门用于检查被弃用的 JVM 参数,Java 8 下被弃用的参数还包括:UseCMSCompactAtFullCollection、CMSIncrementalMode、MaxGCMinorPauseMillis、DefaultMaxRAMFraction、UseCMSCollectionPassing。

4.3 其它优化的参数

  • CMSInitiatingOccupancyFraction

客户的设置是 -XX:CMSInitiatingOccupancyFraction=60,小了点,根据之前的经验改成了 75。将 CMS 启动时机延后点,避免 CMS 来的太频繁。当然该参数需要结合 -XX:+UseCMSInitiatingOccupancyOnly 一块使用,防止JVM基于运行时收集的数据来启动 CMS,而非这里指定的内存使用比例。

代码语言:txt
复制
product(bool, UseCMSInitiatingOccupancyOnly, false,                     \
       "Only use occupancy as a criterion for starting a CMS collection")
  • -XX:+CMSClassUnloadingEnabled

Stack overflow 上的讨论 What does JVM flag CMSClassUnloadingEnabled actually do? 已经说的很清楚。通常 class 占用的空间并不是很大,所以被加载后就一直躺在 PermGen 或 Metaspace。但在 groovy 这类语言出现后,大量动态生成和加载 class,会导致 OOM。所以开启 CMSClassUnloadingEnabled 主要用于减轻 OOM 错误,不过没开启的话,PermGen / Metaspace 空间耗尽时也会触发 FGC,但不是并行收集)。考虑到客户的应用基本没有动态生成的类,所以优化时也就关闭了。

  • -XX:SoftRefLRUPolicyMSPerMB=0

早期的 JVM,GC 时遇到 Soft Reference 就会清楚掉,具体见hotspot faq。缺省值为 1000,即堆中每M空闲空间的soft reference 缓存 1s。考虑到有些框架类的对象创建开销较大,此外 hprof 看了下 软引用占比也不是很高,所以优化下去除了该参数,采用默认 1s 缓存方式。

代码语言:txt
复制
product(intx, SoftRefLRUPolicyMSPerMB, 1000,                            \
        "Number of milliseconds per MB of free space in the heap")

5. 一点感受

以上就是帮助客户优化的所有参数,为了客户业务稳定起见,有些参数没有设置的激进,采用默认值已经跑的很好。如果经常翻翻 JVM 的 mailing list,会发现维护 JVM 的大佬们在性能上考虑的很多。所以看到网上的各种优化帖子(包括本贴)时,先鉴别下不要盲目接受,否则可能还没有不优化的好。此外,没有通用的一成不变的最优参数,根据自己的业务场景进行优化才能取得想要的效果。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. JVM 参数及默认值
  • 2. 根据应用特性调整新生代大小
    • 2.1 优化新生代大小
      • 2.2 优化对象晋升
      • 3. GC线程数
        • 3.1 ParallelGCThreads
          • 3.2 ConcGCThreads
          • 4. CMS 相关优化
            • 4.1 DisableExplicitGC 后 ExplicitGCInvokesConcurrent 还香吗?
              • 4.2 CMSFullGCsBeforeCompaction 已被弃用
                • 4.3 其它优化的参数
                • 5. 一点感受
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档