| 本文主要针对 Hotspot VM 中“CMS + ParNew”组合的一些使用场景进行总结。重点经过部分源码对根因进行分析以及对排查方法进行总结,排查过程会省略较多,另外本文专业术语较多,有必定的阅读门槛,如未介绍清楚,还请自行查阅相关材料。html
| 总字数 2 万左右(不包含代码片断),总体阅读时间约 30min ,文章较长,能够选择你感兴趣的场景进行研究。java
自 Sun 发布 Java 语言以来,开始使用 GC 技术来进行内存自动管理,避免了手动管理带来的悬挂指针(Dangling Pointer)问题,很大程度上提高了开发效率,今后 GC 技术也一鸣惊人。GC 有着很是悠久的历史,1960 年有着“Lisp 之父”和“人工智能之父”之称的 John McCarthy 就在论文中发布了 GC 算法,60 年以来, GC 技术的发展也日新月异,但无论是多么前沿的收集器也都是基于三种基本算法的组合或应用,也就是说 GC 要解决的根本问题这么多年一直都没有变过。笔者认为,在不太远的未来, GC 技术依然不会过期,比起突飞猛进的新技术,GC 这门古典技术更值得咱们学习。c++
目前,互联网上 Java 的 GC 资料要么是主要讲解理论,要么就是针对单一场景的 GC 问题进行了剖析,对整个体系总结的资料少之又少。前车可鉴,后事之师,美团的几位工程师搜集了内部各类 GC 问题的分析文章,并结合我的的理解作了一些总结,但愿能起到“抛砖引玉”的做用,文中如有错误之处,还请你们不吝指正。程序员
GC 问题处理能力能不能系统性掌握?一些影响因素都是互为因果的问题该怎么分析?好比一个服务 RT 忽然上涨,有 GC 耗时增大、线程 Block 增多、慢查询增多、CPU 负载高四个表象,到底哪一个是诱因?如何判断 GC 有没有问题?使用 CMS 有哪些常见问题?如何判断根因是什么?如何解决或避免这些问题?阅读完本文,相信你将会对 CMS GC 的问题处理有一个系统性的认知,更能游刃有余地解决这些问题,下面就让咱们开始吧!面试
想要系统性地掌握 GC 问题处理,笔者这里给出一个学习路径,总体文章的框架也是按照这个结构展开,主要分四大步。算法
在正式开始前,先作些简要铺垫,介绍下 JVM 内存划分、收集算法、收集器等经常使用概念介绍,基础比较好的同窗能够直接跳过这部分。spring
GC: GC 自己有三种语义,下文须要根据具体场景带入不一样的语义:shell
从 JCP(Java Community Process)的官网中能够看到,目前 Java 版本最新已经到了 Java 16,将来的 Java 17 以及如今的 Java 11 和 Java 8 是 LTS 版本,JVM 规范也在随着迭代在变动,因为本文主要讨论 CMS,此处仍是放 Java 8 的内存结构。数据库
GC 主要工做在 Heap 区和 MetaSpace 区(上图蓝色部分),在 Direct Memory 中,若是使用的是 DirectByteBuffer,那么在分配内存不够时则是 GC 经过 Cleaner#clean
间接管理。编程
任何自动内存管理系统都会面临的步骤:为新对象分配空间,而后收集垃圾对象空间,下面咱们就展开介绍一下这些基础知识。
Java 中对象地址操做主要使用 Unsafe 调用了 C 的 allocate 和 free 两个方法,分配方法有两种:
备注:引用计数法是能够处理循环引用问题的,下次面试时不要再这么说啦~ ~
自从有自动内存管理出现之时就有的一些收集算法,不一样的收集器也是在不一样场景下进行组合。
三种算法在是否移动对象、空间和时间方面的一些对比,假设存活对象数量为 L、堆空间大小为 H,则:
把 mark、sweep、compaction、copying 这几种动做的耗时放在一块儿看,大体有这样的关系:
虽然 compaction 与 copying 都涉及移动对象,但取决于具体算法,compaction 可能要先计算一次对象的目标地址,而后修正指针,最后再移动对象。copying 则能够把这几件事情合为一体来作,因此能够快一些。另外,还须要留意 GC 带来的开销不能只看 Collector 的耗时,还得看 Allocator 。若是能保证内存没碎片,分配就能够用 pointer bumping 方式,只须要挪一个指针就完成了分配,很是快。而若是内存有碎片就得用 freelist 之类的方式管理,分配速度一般会慢一些。
目前在 Hotspot VM 中主要有分代收集和分区收集两大类,具体能够看下面的这个图,不过将来会逐渐向分区收集发展。在美团内部,有部分业务尝试用了 ZGC(感兴趣的同窗能够学习下这篇文章 新一代垃圾回收器ZGC的探索与实践),其他基本都停留在 CMS 和 G1 上。另外在 JDK11 后提供了一个不执行任何垃圾回收动做的回收器 Epsilon(A No-Op Garbage Collector)用做性能分析。另一个就是 Azul 的 Zing JVM,其 C4(Concurrent Continuously Compacting Collector)收集器也在业内有必定的影响力。
备注:值得一提的是,早些年国内 GC 技术的布道者 RednaxelaFX (江湖人称 R 大)也曾就任于 Azul,本文的一部分材料也参考了他的一些文章。
-XX:ParallelGCThreads
参数来控制收集的线程数,整个过程都是 STW 的,常与 CMS 组合使用。目前使用最多的是 CMS 和 G1 收集器,两者都有分代的概念,主要内存结构以下:
以上仅列出常见收集器,除此以外还有不少,如 Metronome、Stopless、Staccato、Chicken、Clover 等实时回收器,Sapphire、Compressor、Pauseless 等并发复制/整理回收器,Doligez-Leroy-Conthier 等标记整理回收器,因为篇幅缘由,不在此一一介绍。
工欲善其事,必先利其器,此处列出一些笔者经常使用的工具,具体状况你们能够自由选择,本文的问题都是使用这些工具来定位和分析的。
命令行推荐 arthas ,可视化界面推荐 JProfiler,此外还有一些在线的平台 gceasy、heaphero、fastthread ,美团内部的 Scalpel(一款自研的 JVM 问题诊断工具,暂时未开源)也比较好用。
在作 GC 问题排查和优化以前,咱们须要先来明确下究竟是不是 GC 直接致使的问题,或者应用代码致使的 GC 异常,最终出现问题。
评判 GC 的两个核心指标:
目前各大互联网公司的系统基本都更追求低延时,避免一次 GC 停顿的时间过长对用户体验形成损失,衡量指标须要结合一下应用服务的 SLA,主要以下两点来判断:
简而言之,即为一次停顿的时间不超过应用服务的 TP9999,GC 的吞吐量不小于 99.99%。举个例子,假设某个服务 A 的 TP9999 为 80 ms,平均 GC 停顿为 30 ms,那么该服务的最大停顿时间最好不要超过 80 ms,GC 频次控制在 5 min 以上一次。若是知足不了,那就须要调优或者经过更多资源来进行并联冗余。(你们能够先停下来,看看监控平台上面的 gc.meantime 分钟级别指标,若是超过了 6 ms 那单机 GC 吞吐量就达不到 4 个 9 了。)
备注:除了这两个指标以外还有 Footprint(资源量大小测量)、反应速度等指标,互联网这种实时系统追求低延迟,而不少嵌入式系统则追求 Footprint。
拿到 GC 日志,咱们就能够简单分析 GC 状况了,经过一些工具,咱们能够比较直观地看到 Cause 的分布状况,以下图就是使用 gceasy 绘制的图表:
如上图所示,咱们很清晰的就能知道是什么缘由引发的 GC,以及每次的时间花费状况,可是要分析 GC 的问题,先要读懂 GC Cause,即 JVM 什么样的条件下选择进行 GC 操做,具体 Cause 的分类能够看一下 Hotspot 源码:src/share/vm/gc/shared/gcCause.hpp 和 src/share/vm/gc/shared/gcCause.cpp 中。
const char* GCCause::to_string(GCCause::Cause cause) { switch (cause) { case _java_lang_system_gc: return "System.gc()"; case _full_gc_alot: return "FullGCAlot"; case _scavenge_alot: return "ScavengeAlot"; case _allocation_profiler: return "Allocation Profiler"; case _jvmti_force_gc: return "JvmtiEnv ForceGarbageCollection"; case _gc_locker: return "GCLocker Initiated GC"; case _heap_inspection: return "Heap Inspection Initiated GC"; case _heap_dump: return "Heap Dump Initiated GC"; case _wb_young_gc: return "WhiteBox Initiated Young GC"; case _wb_conc_mark: return "WhiteBox Initiated Concurrent Mark"; case _wb_full_gc: return "WhiteBox Initiated Full GC"; case _no_gc: return "No GC"; case _allocation_failure: return "Allocation Failure"; case _tenured_generation_full: return "Tenured Generation Full"; case _metadata_GC_threshold: return "Metadata GC Threshold"; case _metadata_GC_clear_soft_refs: return "Metadata GC Clear Soft References"; case _cms_generation_full: return "CMS Generation Full"; case _cms_initial_mark: return "CMS Initial Mark"; case _cms_final_remark: return "CMS Final Remark"; case _cms_concurrent_mark: return "CMS Concurrent Mark"; case _old_generation_expanded_on_last_scavenge: return "Old Generation Expanded On Last Scavenge"; case _old_generation_too_full_to_scavenge: return "Old Generation Too Full To Scavenge"; case _adaptive_size_policy: return "Ergonomics"; case _g1_inc_collection_pause: return "G1 Evacuation Pause"; case _g1_humongous_allocation: return "G1 Humongous Allocation"; case _dcmd_gc_run: return "Diagnostic Command"; case _last_gc_cause: return "ILLEGAL VALUE - last gc cause - ILLEGAL VALUE"; default: return "unknown GCCause"; } ShouldNotReachHere(); }
重点须要关注的几个GC Cause:
什么时机使用这些 Cause 触发回收,你们能够看一下 CMS 的代码,这里就不讨论了,具体在 /src/hotspot/share/gc/cms/concurrentMarkSweepGeneration.cpp 中。
bool CMSCollector::shouldConcurrentCollect() { LogTarget(Trace, gc) log; if (_full_gc_requested) { log.print("CMSCollector: collect because of explicit gc request (or GCLocker)"); return true; } FreelistLocker x(this); // ------------------------------------------------------------------ // Print out lots of information which affects the initiation of // a collection. if (log.is_enabled() && stats().valid()) { log.print("CMSCollector shouldConcurrentCollect: "); LogStream out(log); stats().print_on(&out); log.print("time_until_cms_gen_full %3.7f", stats().time_until_cms_gen_full()); log.print("free=" SIZE_FORMAT, _cmsGen->free()); log.print("contiguous_available=" SIZE_FORMAT, _cmsGen->contiguous_available()); log.print("promotion_rate=%g", stats().promotion_rate()); log.print("cms_allocation_rate=%g", stats().cms_allocation_rate()); log.print("occupancy=%3.7f", _cmsGen->occupancy()); log.print("initiatingOccupancy=%3.7f", _cmsGen->initiating_occupancy()); log.print("cms_time_since_begin=%3.7f", stats().cms_time_since_begin()); log.print("cms_time_since_end=%3.7f", stats().cms_time_since_end()); log.print("metadata initialized %d", MetaspaceGC::should_concurrent_collect()); } // ------------------------------------------------------------------ // If the estimated time to complete a cms collection (cms_duration()) // is less than the estimated time remaining until the cms generation // is full, start a collection. if (!UseCMSInitiatingOccupancyOnly) { if (stats().valid()) { if (stats().time_until_cms_start() == 0.0) { return true; } } else { if (_cmsGen->occupancy() >= _bootstrap_occupancy) { log.print(" CMSCollector: collect for bootstrapping statistics: occupancy = %f, boot occupancy = %f", _cmsGen->occupancy(), _bootstrap_occupancy); return true; } } } if (_cmsGen->should_concurrent_collect()) { log.print("CMS old gen initiated"); return true; } CMSHeap* heap = CMSHeap::heap(); if (heap->incremental_collection_will_fail(true /* consult_young */)) { log.print("CMSCollector: collect because incremental collection will fail "); return true; } if (MetaspaceGC::should_concurrent_collect()) { log.print("CMSCollector: collect for metadata allocation "); return true; } // CMSTriggerInterval starts a CMS cycle if enough time has passed. if (CMSTriggerInterval >= 0) { if (CMSTriggerInterval == 0) { // Trigger always return true; } // Check the CMS time since begin (we do not check the stats validity // as we want to be able to trigger the first CMS cycle as well) if (stats().cms_time_since_begin() >= (CMSTriggerInterval / ((double) MILLIUNITS))) { if (stats().valid()) { log.print("CMSCollector: collect because of trigger interval (time since last begin %3.7f secs)", stats().cms_time_since_begin()); } else { log.print("CMSCollector: collect because of trigger interval (first collection)"); } return true; } } return false; }
究竟是结果(现象)仍是缘由,在一次 GC 问题处理的过程当中,如何判断是 GC 致使的故障,仍是系统自己引起 GC 问题。这里继续拿在本文开头提到的一个 Case:“GC 耗时增大、线程 Block 增多、慢查询增多、CPU 负载高等四个表象,如何判断哪一个是根因?”,笔者这里根据本身的经验大体整理了四种判断方法供参考:
不一样的根因,后续的分析方法是彻底不一样的。若是是 CPU 负载高那可能须要用火焰图看下热点、若是是慢查询增多那可能须要看下 DB 状况、若是是线程 Block 引发那可能须要看下锁竞争的状况,最后若是各个表象证实都没有问题,那可能 GC 确实存在问题,能够继续分析 GC 问题了。
Mutator 的类型根据对象存活时间比例图来看主要分为两种,在弱分代假说中也提到相似的说法,以下图所示 “Survival Time” 表示对象存活时间,“Rate” 表示对象分配比例:
固然,除了两者以外还有介于二者之间的场景,本篇文章主要讨论第一种状况。对象 Survival Time 分布图,对咱们设置 GC 参数有着很是重要的指导意义,以下图就能够简单推算分代的边界。
笔者选取了九种不一样类型的 GC 问题,覆盖了大部分场景,若是有更好的场景,欢迎在评论区给出。
Unexpected GC: 意外发生的 GC,实际上不须要发生,咱们能够经过一些手段去避免。
Partial GC: 部分收集操做的 GC,只对某些分代/分区进行回收。
Young GC: 分代收集里面的 Young 区收集动做,也能够叫作 Minor GC。
Old GC: 分代收集里面的 Old 区收集动做,也能够叫作 Major GC,有些也会叫作 Full GC,但其实这种叫法是不规范的,在 CMS 发生 Foreground GC 时才是 Full GC,CMSScavengeBeforeRemark 参数也只是在 Remark 前触发一次Young GC。
一个问题的解决难度跟它的常见程度成反比,大部分咱们均可以经过各类搜索引擎找到相似的问题,而后用一样的手段尝试去解决。当一个问题在各类网站上都找不到类似的问题时,那么可能会有两种状况,一种这不是一个问题,另外一种就是遇到一个隐藏比较深的问题,遇到这种问题可能就要深刻到源码级别去调试了。如下 GC 问题场景,排查难度从上到下依次递增。
服务刚刚启动时 GC 次数较多,最大空间剩余不少可是依然发生 GC,这种状况咱们能够经过观察 GC 日志或者经过监控工具来观察堆的空间变化状况便可。GC Cause 通常为 Allocation Failure,且在 GC 日志中会观察到经历一次 GC ,堆内各个空间的大小会被调整,以下图所示:
在 JVM 的参数中 -Xms
和 -Xmx
设置的不一致,在初始化时只会初始 -Xms 大小的空间存储信息,每当空间不够用时再向操做系统申请,这样的话必然要进行一次 GC。具体是经过 ConcurrentMarkSweepGeneration::compute_new_size()
方法计算新的空间大小:
void ConcurrentMarkSweepGeneration::compute_new_size() { assert_locked_or_safepoint(Heap_lock); // If incremental collection failed, we just want to expand // to the limit. if (incremental_collection_failed()) { clear_incremental_collection_failed(); grow_to_reserved(); return; } // The heap has been compacted but not reset yet. // Any metric such as free() or used() will be incorrect. CardGeneration::compute_new_size(); // Reset again after a possible resizing if (did_compact()) { cmsSpace()->reset_after_compaction(); } }
另外,若是空间剩余不少时也会进行缩容操做,JVM 经过 -XX:MinHeapFreeRatio
和 -XX:MaxHeapFreeRatio
来控制扩容和缩容的比例,调节这两个值也能够控制伸缩的时机,例如扩容即是使用 GenCollectedHeap::expand_heap_and_allocate()
来完成的,代码以下:
HeapWord* GenCollectedHeap::expand_heap_and_allocate(size_t size, bool is_tlab) { HeapWord* result = NULL; if (_old_gen->should_allocate(size, is_tlab)) { result = _old_gen->expand_and_allocate(size, is_tlab); } if (result == NULL) { if (_young_gen->should_allocate(size, is_tlab)) { result = _young_gen->expand_and_allocate(size, is_tlab); } } assert(result == NULL || is_in_reserved(result), "result not in heap"); return result; }
整个伸缩的模型理解能够看这个图,当 committed 的空间大小超过了低水位/高水位的大小,capacity 也会随之调整:
定位:观察 CMS GC 触发时间点 Old/MetaSpace 区的 committed 占比是否是一个固定的值,或者像上文提到的观察总的内存使用率也能够。
解决:尽可能将成对出现的空间大小配置参数设置成固定的,如 -Xms
和 -Xmx
,-XX:MaxNewSize
和 -XX:NewSize
,-XX:MetaSpaceSize
和 -XX:MaxMetaSpaceSize
等。
通常来讲,咱们须要保证 Java 虚拟机的堆是稳定的,确保 -Xms
和 -Xmx
设置的是一个值(即初始值和最大值一致),得到一个稳定的堆,同理在 MetaSpace 区也有相似的问题。不过在不追求停顿时间的状况下震荡的空间也是有利的,能够动态地伸缩以节省空间,例如做为富客户端的 Java 应用。
这个问题虽然初级,可是发生的几率还真不小,尤为是在一些规范不太健全的状况下。
除了扩容缩容会触发 CMS GC 以外,还有 Old 区达到回收阈值、MetaSpace 空间不足、Young 区晋升失败、大对象担保失败等几种触发条件,若是这些状况都没有发生却触发了 GC ?这种状况有多是代码中手动调用了 System.gc 方法,此时能够找到 GC 日志中的 GC Cause 确认下。那么这种 GC 到底有没有问题,翻看网上的一些资料,有人说能够添加 -XX:+DisableExplicitGC
参数来避免这种 GC,也有人说不能加这个参数,加了就会影响 Native Memory 的回收。先说结论,笔者这里建议保留 System.gc,那为何要保留?咱们一块儿来分析下。
找到 System.gc 在 Hotspot 中的源码,能够发现增长 -XX:+DisableExplicitGC
参数后,这个方法变成了一个空方法,若是没有加的话便会调用 Universe::heap()::collect
方法,继续跟进到这个方法中,发现 System.gc 会引起一次 STW 的 Full GC,对整个堆作收集。
JVM_ENTRY_NO_ENV(void, JVM_GC(void)) JVMWrapper("JVM_GC"); if (!DisableExplicitGC) { Universe::heap()->collect(GCCause::_java_lang_system_gc); } JVM_END
void GenCollectedHeap::collect(GCCause::Cause cause) { if (cause == GCCause::_wb_young_gc) { // Young collection for the WhiteBox API. collect(cause, YoungGen); } else { #ifdef ASSERT if (cause == GCCause::_scavenge_alot) { // Young collection only. collect(cause, YoungGen); } else { // Stop-the-world full collection. collect(cause, OldGen); } #else // Stop-the-world full collection. collect(cause, OldGen); #endif } }
保留 System.gc
此处补充一个知识点,CMS GC 共分为 Background 和 Foreground 两种模式,前者就是咱们常规理解中的并发收集,能够不影响正常的业务线程运行,但 Foreground Collector 却有很大的差别,他会进行一次压缩式 GC。此压缩式 GC 使用的是跟 Serial Old GC 同样的 Lisp2 算法,其使用 Mark-Compact 来作 Full GC,通常称之为 MSC(Mark-Sweep-Compact),它收集的范围是 Java 堆的 Young 区和 Old 区以及 MetaSpace。由上面的算法章节中咱们知道 compact 的代价是巨大的,那么使用 Foreground Collector 时将会带来很是长的 STW。若是在应用程序中 System.gc 被频繁调用,那就很是危险了。
去掉 System.gc
若是禁用掉的话就会带来另一个内存泄漏问题,此时就须要说一下 DirectByteBuffer,它有着零拷贝等特色,被 Netty 等各类 NIO 框架使用,会使用到堆外内存。堆内存由 JVM 本身管理,堆外内存必需要手动释放,DirectByteBuffer 没有 Finalizer,它的 Native Memory 的清理工做是经过 sun.misc.Cleaner
自动完成的,是一种基于 PhantomReference 的清理工具,比普通的 Finalizer 轻量些。
为 DirectByteBuffer 分配空间过程当中会显式调用 System.gc ,但愿经过 Full GC 来强迫已经无用的 DirectByteBuffer 对象释放掉它们关联的 Native Memory,下面为代码实现:
// These methods should be called whenever direct memory is allocated or // freed. They allow the user to control the amount of direct memory // which a process may access. All sizes are specified in bytes. static void reserveMemory(long size) { synchronized (Bits.class) { if (!memoryLimitSet && VM.isBooted()) { maxMemory = VM.maxDirectMemory(); memoryLimitSet = true; } if (size <= maxMemory - reservedMemory) { reservedMemory += size; return; } } System.gc(); try { Thread.sleep(100); } catch (InterruptedException x) { // Restore interrupt status Thread.currentThread().interrupt(); } synchronized (Bits.class) { if (reservedMemory + size > maxMemory) throw new OutOfMemoryError("Direct buffer memory"); reservedMemory += size; } }
HotSpot VM 只会在 Old GC 的时候才会对 Old 中的对象作 Reference Processing,而在 Young GC 时只会对 Young 里的对象作 Reference Processing。Young 中的 DirectByteBuffer 对象会在 Young GC 时被处理,也就是说,作 CMS GC 的话会对 Old 作 Reference Processing,进而能触发 Cleaner 对已死的 DirectByteBuffer 对象作清理工做。但若是很长一段时间里没作过 GC 或者只作了 Young GC 的话则不会在 Old 触发 Cleaner 的工做,那么就可能让原本已经死亡,但已经晋升到 Old 的 DirectByteBuffer 关联的 Native Memory 得不到及时释放。这几个实现特征使得依赖于 System.gc 触发 GC 来保证 DirectByteMemory 的清理工做能及时完成。若是打开了 -XX:+DisableExplicitGC
,清理工做就可能得不到及时完成,因而就有发生 Direct Memory 的 OOM。
经过上面的分析看到,不管是保留仍是去掉都会有必定的风险点,不过目前互联网中的 RPC 通讯会大量使用 NIO,因此笔者在这里建议保留。此外 JVM 还提供了 -XX:+ExplicitGCInvokesConcurrent
和 -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
参数来将 System.gc 的触发类型从 Foreground 改成 Background,同时 Background 也会作 Reference Processing,这样的话就能大幅下降了 STW 开销,同时也不会发生 NIO Direct Memory OOM。
不止 CMS,在 G1 或 ZGC中开启 ExplicitGCInvokesConcurrent
模式,都会采用高性能的并发收集方式进行收集,不过仍是建议在代码规范方面也要作好约束,规范好 System.gc 的使用。
P.S. HotSpot 对 System.gc 有特别处理,最主要的地方体如今一次 System.gc 是否与普通 GC 同样会触发 GC 的统计/阈值数据的更新,HotSpot 里的许多 GC 算法都带有自适应的功能,会根据先前收集的效率来决定接下来的 GC 中使用的参数,但 System.gc 默认不更新这些统计数据,避免用户强行 GC 对这些自适应功能的干扰(能够参考 -XX:+UseAdaptiveSizePolicyWithSystemGC 参数,默认是 false)。
JVM 在启动后或者某个时间点开始,MetaSpace 的已使用大小在持续增加,同时每次 GC 也没法释放,调大 MetaSpace 空间也没法完全解决。
在讨论为何会 OOM 以前,咱们先来看一下这个区里面会存什么数据,Java7 以前字符串常量池被放到了 Perm 区,全部被 intern 的 String 都会被存在这里,因为 String.intern 是不受控的,因此 -XX:MaxPermSize
的值也不太好设置,常常会出现 java.lang.OutOfMemoryError: PermGen space
异常,因此在 Java7 以后常量池等字面量(Literal)、类静态变量(Class Static)、符号引用(Symbols Reference)等几项被移到 Heap 中。而 Java8 以后 PermGen 也被移除,取而代之的是 MetaSpace。
在最底层,JVM 经过 mmap 接口向操做系统申请内存映射,每次申请 2MB 空间,这里是虚拟内存映射,不是真的就消耗了主存的 2MB,只有以后在使用的时候才会真的消耗内存。申请的这些内存放到一个链表中 VirtualSpaceList,做为其中的一个 Node。
在上层,MetaSpace 主要由 Klass Metaspace 和 NoKlass Metaspace 两大部分组成。
-XX:-UseCompressedClassPointers
,或者 -Xmx
设置大于 32 G,就不会有这块内存,这种状况下 Klass 都会存在 NoKlass Metaspace 里。具体的定义均可以在源码 shared/vm/memory/metaspace.hpp 中找到:
class Metaspace : public AllStatic { friend class MetaspaceShared; public: enum MetadataType { ClassType, NonClassType, MetadataTypeCount }; enum MetaspaceType { ZeroMetaspaceType = 0, StandardMetaspaceType = ZeroMetaspaceType, BootMetaspaceType = StandardMetaspaceType + 1, AnonymousMetaspaceType = BootMetaspaceType + 1, ReflectionMetaspaceType = AnonymousMetaspaceType + 1, MetaspaceTypeCount }; private: // Align up the word size to the allocation word size static size_t align_word_size_up(size_t); // Aligned size of the metaspace. static size_t _compressed_class_space_size; static size_t compressed_class_space_size() { return _compressed_class_space_size; } static void set_compressed_class_space_size(size_t size) { _compressed_class_space_size = size; } static size_t _first_chunk_word_size; static size_t _first_class_chunk_word_size; static size_t _commit_alignment; static size_t _reserve_alignment; DEBUG_ONLY(static bool _frozen;) // Virtual Space lists for both classes and other metadata static metaspace::VirtualSpaceList* _space_list; static metaspace::VirtualSpaceList* _class_space_list; static metaspace::ChunkManager* _chunk_manager_metadata; static metaspace::ChunkManager* _chunk_manager_class; static const MetaspaceTracer* _tracer; }
MetaSpace 的对象为何没法释放,咱们看下面两点:
-XX:MinMetaspaceFreeRatio
和 -XX:MaxMetaspaceFreeRatio
两个参数动态控制整个 MetaSpace 的大小,具体使用能够看 MetaSpaceGC::compute_new_size()
方法(下方代码),这个方法会在 CMSCollector 和 G1CollectorHeap 等几个收集器执行 GC 时调用。这个里面会根据 used_after_gc
,MinMetaspaceFreeRatio
和 MaxMetaspaceFreeRatio
这三个值计算出来一个新的 _capacity_until_GC
值(水位线)。而后根据实际的 _capacity_until_GC
值使用 MetaspaceGC::inc_capacity_until_GC()
和 MetaspaceGC::dec_capacity_until_GC()
进行 expand 或 shrink,这个过程也能够参照场景一中的伸缩模型进行理解。void MetaspaceGC::compute_new_size() { assert(_shrink_factor <= 100, "invalid shrink factor"); uint current_shrink_factor = _shrink_factor; _shrink_factor = 0; const size_t used_after_gc = MetaspaceUtils::committed_bytes(); const size_t capacity_until_GC = MetaspaceGC::capacity_until_GC(); const double minimum_free_percentage = MinMetaspaceFreeRatio / 100.0; const double maximum_used_percentage = 1.0 - minimum_free_percentage; const double min_tmp = used_after_gc / maximum_used_percentage; size_t minimum_desired_capacity = (size_t)MIN2(min_tmp, double(max_uintx)); // Don't shrink less than the initial generation size minimum_desired_capacity = MAX2(minimum_desired_capacity, MetaspaceSize); log_trace(gc, metaspace)("MetaspaceGC::compute_new_size: "); log_trace(gc, metaspace)(" minimum_free_percentage: %6.2f maximum_used_percentage: %6.2f", minimum_free_percentage, maximum_used_percentage); log_trace(gc, metaspace)(" used_after_gc : %6.1fKB", used_after_gc / (double) K); size_t shrink_bytes = 0; if (capacity_until_GC < minimum_desired_capacity) { // If we have less capacity below the metaspace HWM, then // increment the HWM. size_t expand_bytes = minimum_desired_capacity - capacity_until_GC; expand_bytes = align_up(expand_bytes, Metaspace::commit_alignment()); // Don't expand unless it's significant if (expand_bytes >= MinMetaspaceExpansion) { size_t new_capacity_until_GC = 0; bool succeeded = MetaspaceGC::inc_capacity_until_GC(expand_bytes, &new_capacity_until_GC); assert(succeeded, "Should always succesfully increment HWM when at safepoint"); Metaspace::tracer()->report_gc_threshold(capacity_until_GC, new_capacity_until_GC, MetaspaceGCThresholdUpdater::ComputeNewSize); log_trace(gc, metaspace)(" expanding: minimum_desired_capacity: %6.1fKB expand_bytes: %6.1fKB MinMetaspaceExpansion: %6.1fKB new metaspace HWM: %6.1fKB", minimum_desired_capacity / (double) K, expand_bytes / (double) K, MinMetaspaceExpansion / (double) K, new_capacity_until_GC / (double) K); } return; } // No expansion, now see if we want to shrink // We would never want to shrink more than this assert(capacity_until_GC >= minimum_desired_capacity, SIZE_FORMAT " >= " SIZE_FORMAT, capacity_until_GC, minimum_desired_capacity); size_t max_shrink_bytes = capacity_until_GC - minimum_desired_capacity; // Should shrinking be considered? if (MaxMetaspaceFreeRatio < 100) { const double maximum_free_percentage = MaxMetaspaceFreeRatio / 100.0; const double minimum_used_percentage = 1.0 - maximum_free_percentage; const double max_tmp = used_after_gc / minimum_used_percentage; size_t maximum_desired_capacity = (size_t)MIN2(max_tmp, double(max_uintx)); maximum_desired_capacity = MAX2(maximum_desired_capacity, MetaspaceSize); log_trace(gc, metaspace)(" maximum_free_percentage: %6.2f minimum_used_percentage: %6.2f", maximum_free_percentage, minimum_used_percentage); log_trace(gc, metaspace)(" minimum_desired_capacity: %6.1fKB maximum_desired_capacity: %6.1fKB", minimum_desired_capacity / (double) K, maximum_desired_capacity / (double) K); assert(minimum_desired_capacity <= maximum_desired_capacity, "sanity check"); if (capacity_until_GC > maximum_desired_capacity) { // Capacity too large, compute shrinking size shrink_bytes = capacity_until_GC - maximum_desired_capacity; shrink_bytes = shrink_bytes / 100 * current_shrink_factor; shrink_bytes = align_down(shrink_bytes, Metaspace::commit_alignment()); assert(shrink_bytes <= max_shrink_bytes, "invalid shrink size " SIZE_FORMAT " not <= " SIZE_FORMAT, shrink_bytes, max_shrink_bytes); if (current_shrink_factor == 0) { _shrink_factor = 10; } else { _shrink_factor = MIN2(current_shrink_factor * 4, (uint) 100); } log_trace(gc, metaspace)(" shrinking: initThreshold: %.1fK maximum_desired_capacity: %.1fK", MetaspaceSize / (double) K, maximum_desired_capacity / (double) K); log_trace(gc, metaspace)(" shrink_bytes: %.1fK current_shrink_factor: %d new shrink factor: %d MinMetaspaceExpansion: %.1fK", shrink_bytes / (double) K, current_shrink_factor, _shrink_factor, MinMetaspaceExpansion / (double) K); } } // Don't shrink unless it's significant if (shrink_bytes >= MinMetaspaceExpansion && ((capacity_until_GC - shrink_bytes) >= MetaspaceSize)) { size_t new_capacity_until_GC = MetaspaceGC::dec_capacity_until_GC(shrink_bytes); Metaspace::tracer()->report_gc_threshold(capacity_until_GC, new_capacity_until_GC, MetaspaceGCThresholdUpdater::ComputeNewSize); } }
由场景一可知,为了不弹性伸缩带来的额外 GC 消耗,咱们会将 -XX:MetaSpaceSize
和 -XX:MaxMetaSpaceSize
两个值设置为固定的,可是这样也会致使在空间不够的时候没法扩容,而后频繁地触发 GC,最终 OOM。因此关键缘由就是 ClassLoader 不停地在内存中 load 了新的 Class ,通常这种问题都发生在动态类加载等状况上。
了解大概什么缘由后,如何定位和解决就很简单了,能够 dump 快照以后经过 JProfiler 或 MAT 观察 Classes 的 Histogram(直方图) 便可,或者直接经过命令便可定位, jcmd 打几回 Histogram 的图,看一下具体是哪一个包下的 Class 增长较多就能够定位了。不过有时候也要结合InstBytes、KlassBytes、Bytecodes、MethodAll 等几项指标综合来看下。以下图即是笔者使用 jcmd 排查到一个 Orika 的问题。
jcmd <PID> GC.class_stats|awk '{print$13}'|sed 's/\(.*\)\.\(.*\)/\1/g'|sort |uniq -c|sort -nrk1
若是没法从总体的角度定位,能够添加 -XX:+TraceClassLoading
和 -XX:+TraceClassUnLoading
参数观察详细的类加载和卸载信息。
原理理解比较复杂,但定位和解决问题会比较简单,常常会出问题的几个点有 Orika 的 classMap、JSON 的 ASMSerializer、Groovy 动态加载类等,基本都集中在反射、Javasisit 字节码加强、CGLIB 动态代理、OSGi 自定义类加载器等的技术点上。另外就是及时给 MetaSpace 区的使用率加一个监控,若是指标有波动提早发现并解决问题。
这种场景主要发生在分代的收集器上面,专业的术语称为“Premature Promotion”。90% 的对象朝生夕死,只有在 Young 区经历过几回 GC 的洗礼后才会晋升到 Old 区,每经历一次 GC 对象的 GC Age 就会增加 1,最大经过 -XX:MaxTenuringThreshold
来控制。
过早晋升通常不会直接影响 GC,总会伴随着浮动垃圾、大对象担保失败等问题,但这些问题不是马上发生的,咱们能够观察如下几种现象来判断是否发生了过早晋升。
分配速率接近于晋升速率,对象晋升年龄较小。
GC 日志中出现“Desired survivor size 107347968 bytes, new threshold 1(max 6)”等信息,说明此时经历过一次 GC 就会放到 Old 区。
Full GC 比较频繁,且经历过一次 GC 以后 Old 区的变化比例很是大。
好比说 Old 区触发的回收阈值是 80%,经历过一次 GC 以后降低到了 10%,这就说明 Old 区的 70% 的对象存活时间其实很短,以下图所示,Old 区大小每次 GC 后从 2.1G 回收到 300M,也就是说回收掉了 1.8G 的垃圾,只有 300M 的活跃对象。整个 Heap 目前是 4G,活跃对象只占了不到十分之一。
过早晋升的危害:
主要的缘由有如下两点:
同时没法 GC 掉对象还会带来另一个问题,引起动态年龄计算:JVM 经过 -XX:MaxTenuringThreshold
参数来控制晋升年龄,每通过一次 GC,年龄就会加一,达到最大年龄就能够进入 Old 区,最大值为 15(由于 JVM 中使用 4 个比特来表示对象的年龄)。设定固定的 MaxTenuringThreshold 值做为晋升条件:
相同应用在不一样时间的表现不一样,特殊任务的执行或者流量成分的变化,都会致使对象的生命周期分布发生波动,那么固定的阈值设定,由于没法动态适应变化,会形成和上面问题,因此 Hotspot 会使用动态计算的方式来调整晋升的阈值。
具体动态计算能够看一下 Hotspot 源码,具体在 /src/hotspot/share/gc/shared/ageTable.cpp 的 compute_tenuring_threshold
方法中:
uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) { //TargetSurvivorRatio默认50,意思是:在回收以后但愿survivor区的占用率达到这个比例 size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100); size_t total = 0; uint age = 1; assert(sizes[0] == 0, "no objects with age zero should be recorded"); while (age < table_size) {//table_size=16 total += sizes[age]; //若是加上这个年龄的全部对象的大小以后,占用量>指望的大小,就设置age为新的晋升阈值 if (total > desired_survivor_size) break; age++; } uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; if (PrintTenuringDistribution || UsePerfData) { //打印指望的survivor的大小以及新计算出来的阈值,和设置的最大阈值 if (PrintTenuringDistribution) { gclog_or_tty->cr(); gclog_or_tty->print_cr("Desired survivor size " SIZE_FORMAT " bytes, new threshold %u (max %u)", desired_survivor_size*oopSize, result, (int) MaxTenuringThreshold); } total = 0; age = 1; while (age < table_size) { total += sizes[age]; if (sizes[age] > 0) { if (PrintTenuringDistribution) { gclog_or_tty->print_cr("- age %3u: " SIZE_FORMAT_W(10) " bytes, " SIZE_FORMAT_W(10) " total", age, sizes[age]*oopSize, total*oopSize); } } if (UsePerfData) { _perf_sizes[age]->set_value(sizes[age]*oopSize); } age++; } if (UsePerfData) { SharedHeap* sh = SharedHeap::heap(); CollectorPolicy* policy = sh->collector_policy(); GCPolicyCounters* gc_counters = policy->counters(); gc_counters->tenuring_threshold()->set_value(result); gc_counters->desired_survivor_size()->set_value( desired_survivor_size*oopSize); } } return result; }
能够看到 Hotspot 遍历全部对象时,从全部年龄为 0 的对象占用的空间开始累加,若是加上年龄等于 n 的全部对象的空间以后,使用 Survivor 区的条件值(TargetSurvivorRatio / 100,TargetSurvivorRatio 默认值为 50)进行判断,若大于这个值则结束循环,将 n 和 MaxTenuringThreshold 比较,若 n 小,则阈值为 n,若 n 大,则只能去设置最大阈值为 MaxTenuringThreshold。动态年龄触发后致使更多的对象进入了 Old 区,形成资源浪费。
知道问题缘由后咱们就有解决的方向,若是是 Young/Eden 区太小,咱们能够在总的 Heap 内存不变的状况下适当增大 Young 区,具体怎么增长?通常状况下 Old 的大小应当为活跃对象的 2~3 倍左右,考虑到浮动垃圾问题最好在 3 倍左右,剩下的均可以分给 Young 区。
拿笔者的一次典型过早晋升优化来看,原配置为 Young 1.2G + Old 2.8G,经过观察 CMS GC 的状况找到存活对象大概为 300~400M,因而调整 Old 1.5G 左右,剩下 2.5G 分给 Young 区。仅仅调了一个 Young 区大小参数(-Xmn
),整个 JVM 一分钟 Young GC 从 26 次下降到了 11 次,单次时间也没有增长,总的 GC 时间从 1100ms 下降到了 500ms,CMS GC 次数也从 40 分钟左右一次下降到了 7 小时 30 分钟一次。
若是是分配速率过大:
过早晋升问题通常不会特别明显,但日积月累以后可能会爆发一波收集器退化之类的问题,因此咱们仍是要提早避免掉的,能够看看本身系统里面是否有这些现象,若是比较匹配的话,能够尝试优化一下。一行代码优化的 ROI 仍是很高的。
若是在观察 Old 区先后比例变化的过程当中,发现能够回收的比例很是小,如从 80% 只回收到了 60%,说明咱们大部分对象都是存活的,Old 区的空间能够适当调大些。
关于在调整 Young 与 Old 的比例时,如何选取具体的 NewRatio 值,这里将问题抽象成为一个蓄水池模型,找到如下关键衡量指标,你们能够根据本身场景进行推算。
Old 区频繁的作 CMS GC,可是每次耗时不是特别长,总体最大 STW 也在可接受范围内,但因为 GC 太频繁致使吞吐降低比较多。
这种状况比较常见,基本都是一次 Young GC 完成后,负责处理 CMS GC 的一个后台线程 concurrentMarkSweepThread 会不断地轮询,使用 shouldConcurrentCollect()
方法作一次检测,判断是否达到了回收条件。若是达到条件,使用 collect_in_background()
启动一次 Background 模式 GC。轮询的判断是使用 sleepBeforeNextCycle()
方法,间隔周期为 -XX:CMSWaitDuration
决定,默认为2s。
具体代码在: src/hotspot/share/gc/cms/concurrentMarkSweepThread.cpp。
void ConcurrentMarkSweepThread::run_service() { assert(this == cmst(), "just checking"); if (BindCMSThreadToCPU && !os::bind_to_processor(CPUForCMSThread)) { log_warning(gc)("Couldn't bind CMS thread to processor " UINTX_FORMAT, CPUForCMSThread); } while (!should_terminate()) { sleepBeforeNextCycle(); if (should_terminate()) break; GCIdMark gc_id_mark; GCCause::Cause cause = _collector->_full_gc_requested ? _collector->_full_gc_cause : GCCause::_cms_concurrent_mark; _collector->collect_in_background(cause); } verify_ok_to_terminate(); }
void ConcurrentMarkSweepThread::sleepBeforeNextCycle() { while (!should_terminate()) { if(CMSWaitDuration >= 0) { // Wait until the next synchronous GC, a concurrent full gc // request or a timeout, whichever is earlier. wait_on_cms_lock_for_scavenge(CMSWaitDuration); } else { // Wait until any cms_lock event or check interval not to call shouldConcurrentCollect permanently wait_on_cms_lock(CMSCheckInterval); } // Check if we should start a CMS collection cycle if (_collector->shouldConcurrentCollect()) { return; } // .. collection criterion not yet met, let's go back // and wait some more } }
判断是否进行回收的代码在:/src/hotspot/share/gc/cms/concurrentMarkSweepGeneration.cpp。
bool CMSCollector::shouldConcurrentCollect() { LogTarget(Trace, gc) log; if (_full_gc_requested) { log.print("CMSCollector: collect because of explicit gc request (or GCLocker)"); return true; } FreelistLocker x(this); // ------------------------------------------------------------------ // Print out lots of information which affects the initiation of // a collection. if (log.is_enabled() && stats().valid()) { log.print("CMSCollector shouldConcurrentCollect: "); LogStream out(log); stats().print_on(&out); log.print("time_until_cms_gen_full %3.7f", stats().time_until_cms_gen_full()); log.print("free=" SIZE_FORMAT, _cmsGen->free()); log.print("contiguous_available=" SIZE_FORMAT, _cmsGen->contiguous_available()); log.print("promotion_rate=%g", stats().promotion_rate()); log.print("cms_allocation_rate=%g", stats().cms_allocation_rate()); log.print("occupancy=%3.7f", _cmsGen->occupancy()); log.print("initiatingOccupancy=%3.7f", _cmsGen->initiating_occupancy()); log.print("cms_time_since_begin=%3.7f", stats().cms_time_since_begin()); log.print("cms_time_since_end=%3.7f", stats().cms_time_since_end()); log.print("metadata initialized %d", MetaspaceGC::should_concurrent_collect()); } // ------------------------------------------------------------------ if (!UseCMSInitiatingOccupancyOnly) { if (stats().valid()) { if (stats().time_until_cms_start() == 0.0) { return true; } } else { if (_cmsGen->occupancy() >= _bootstrap_occupancy) { log.print(" CMSCollector: collect for bootstrapping statistics: occupancy = %f, boot occupancy = %f", _cmsGen->occupancy(), _bootstrap_occupancy); return true; } } } if (_cmsGen->should_concurrent_collect()) { log.print("CMS old gen initiated"); return true; } // We start a collection if we believe an incremental collection may fail; // this is not likely to be productive in practice because it's probably too // late anyway. CMSHeap* heap = CMSHeap::heap(); if (heap->incremental_collection_will_fail(true /* consult_young */)) { log.print("CMSCollector: collect because incremental collection will fail "); return true; } if (MetaspaceGC::should_concurrent_collect()) { log.print("CMSCollector: collect for metadata allocation "); return true; } // CMSTriggerInterval starts a CMS cycle if enough time has passed. if (CMSTriggerInterval >= 0) { if (CMSTriggerInterval == 0) { // Trigger always return true; } // Check the CMS time since begin (we do not check the stats validity // as we want to be able to trigger the first CMS cycle as well) if (stats().cms_time_since_begin() >= (CMSTriggerInterval / ((double) MILLIUNITS))) { if (stats().valid()) { log.print("CMSCollector: collect because of trigger interval (time since last begin %3.7f secs)", stats().cms_time_since_begin()); } else { log.print("CMSCollector: collect because of trigger interval (first collection)"); } return true; } } return false; }
分析其中逻辑判断是否触发 GC,分为如下几种状况:
触发 CMS GC: 经过调用 _collector->collect_in_background()
进行触发 Background GC 。
-XX:CMSInitiatingOccupancyFraction
的值进行判断,须要设置参数 -XX:+UseCMSInitiatingOccupancyOnly
。-XX:UseCMSInitiatingOccupancyOnly
参数,判断当前 Old 区使用率是否大于阈值,则触发 CMS GC,该阈值能够经过参数 -XX:CMSInitiatingOccupancyFraction
进行设置,若是没有设置,默认为 92%。-XX:+CMSClassUnloadingEnabled
。触发 Full GC: 直接进行 Full GC,这种状况到场景七中展开说明。
_full_gc_requested
为真,说明有明确的需求要进行 GC,好比调用 System.gc。GenCollectorPolicy
类的 satisfy_failed_allocation()
方法中进行判断。你们能够看一下源码中的日志打印,经过日志咱们就能够比较清楚地知道具体的缘由,而后就能够着手分析了。
咱们这里仍是拿最多见的达到回收比例这个场景来讲,与过早晋升不一样的是这些对象确实存活了一段时间,Survival Time 超过了 TP9999 时间,可是又达不到长期存活,如各类数据库、网络连接,带有失效时间的缓存等。
处理这种常规内存泄漏问题基本是一个思路,主要步骤以下:
Dump Diff 和 Leak Suspects 比较直观就不介绍了,这里说下其它几个关键点:
通过整个流程下来基本就能定位问题了,不过在优化的过程当中记得使用控制变量的方法来优化,防止一些会加重问题的改动被掩盖。
CMS GC 单次 STW 最大超过 1000ms,不会频繁发生,以下图所示最长达到了 8000ms。某些场景下会引发“雪崩效应”,这种场景很是危险,咱们应该尽可能避免出现。
CMS 在回收的过程当中,STW 的阶段主要是 Init Mark 和 Final Remark 这两个阶段,也是致使 CMS Old GC 最多的缘由,另外有些状况就是在 STW 前等待 Mutator 的线程到达 SafePoint 也会致使时间过长,但这种状况较少,咱们在此处主要讨论前者。发生收集器退化或者碎片压缩的场景请看场景七。
想要知道这两个阶段为何会耗时,咱们须要先看一下这两个阶段都会干什么。
核心代码都在 /src/hotspot/share/gc/cms/concurrentMarkSweepGeneration.cpp 中,内部有个线程 ConcurrentMarkSweepThread 轮询来校验,Old 区的垃圾回收相关细节被彻底封装在 CMSCollector
中,调用入口就是 ConcurrentMarkSweepThread 调用的 CMSCollector::collect_in_background
和 ConcurrentMarkSweepGeneration
调用的 CMSCollector::collect
方法,此处咱们讨论大多数场景的 collect_in_background
。整个过程当中会 STW 的主要是 initial Mark 和 Final Remark,核心代码在 VM_CMS_Initial_Mark
/ VM_CMS_Final_Remark
中,执行时须要将执行权交由 VMThread 来执行。
CMSCollector::checkpointRootsInitialWork()
和 CMSParInitialMarkTask::work
中,总体步骤和代码以下:void CMSCollector::checkpointRootsInitialWork() { assert(SafepointSynchronize::is_at_safepoint(), "world should be stopped"); assert(_collectorState == InitialMarking, "just checking"); // Already have locks. assert_lock_strong(bitMapLock()); assert(_markBitMap.isAllClear(), "was reset at end of previous cycle"); // Setup the verification and class unloading state for this // CMS collection cycle. setup_cms_unloading_and_verification_state(); GCTraceTime(Trace, gc, phases) ts("checkpointRootsInitialWork", _gc_timer_cm); // Reset all the PLAB chunk arrays if necessary. if (_survivor_plab_array != NULL && !CMSPLABRecordAlways) { reset_survivor_plab_arrays(); } ResourceMark rm; HandleMark hm; MarkRefsIntoClosure notOlder(_span, &_markBitMap); CMSHeap* heap = CMSHeap::heap(); verify_work_stacks_empty(); verify_overflow_empty(); heap->ensure_parsability(false); // fill TLABs, but no need to retire them // Update the saved marks which may affect the root scans. heap->save_marks(); // weak reference processing has not started yet. ref_processor()->set_enqueuing_is_done(false); // Need to remember all newly created CLDs, // so that we can guarantee that the remark finds them. ClassLoaderDataGraph::remember_new_clds(true); // Whenever a CLD is found, it will be claimed before proceeding to mark // the klasses. The claimed marks need to be cleared before marking starts. ClassLoaderDataGraph::clear_claimed_marks(); print_eden_and_survivor_chunk_arrays(); { if (CMSParallelInitialMarkEnabled) { // The parallel version. WorkGang* workers = heap->workers(); assert(workers != NULL, "Need parallel worker threads."); uint n_workers = workers->active_workers(); StrongRootsScope srs(n_workers); CMSParInitialMarkTask tsk(this, &srs, n_workers); initialize_sequential_subtasks_for_young_gen_rescan(n_workers); // If the total workers is greater than 1, then multiple workers // may be used at some time and the initialization has been set // such that the single threaded path cannot be used. if (workers->total_workers() > 1) { workers->run_task(&tsk); } else { tsk.work(0); } } else { // The serial version. CLDToOopClosure cld_closure(¬Older, true); heap->rem_set()->prepare_for_younger_refs_iterate(false); // Not parallel. StrongRootsScope srs(1); heap->cms_process_roots(&srs, true, // young gen as roots GenCollectedHeap::ScanningOption(roots_scanning_options()), should_unload_classes(), ¬Older, &cld_closure); } } // Clear mod-union table; it will be dirtied in the prologue of // CMS generation per each young generation collection. assert(_modUnionTable.isAllClear(), "Was cleared in most recent final checkpoint phase" " or no bits are set in the gc_prologue before the start of the next " "subsequent marking phase."); assert(_ct->cld_rem_set()->mod_union_is_clear(), "Must be"); // Save the end of the used_region of the constituent generations // to be used to limit the extent of sweep in each generation. save_sweep_limits(); verify_overflow_empty(); }
void CMSParInitialMarkTask::work(uint worker_id) { elapsedTimer _timer; ResourceMark rm; HandleMark hm; // ---------- scan from roots -------------- _timer.start(); CMSHeap* heap = CMSHeap::heap(); ParMarkRefsIntoClosure par_mri_cl(_collector->_span, &(_collector->_markBitMap)); // ---------- young gen roots -------------- { work_on_young_gen_roots(&par_mri_cl); _timer.stop(); log_trace(gc, task)("Finished young gen initial mark scan work in %dth thread: %3.3f sec", worker_id, _timer.seconds()); } // ---------- remaining roots -------------- _timer.reset(); _timer.start(); CLDToOopClosure cld_closure(&par_mri_cl, true); heap->cms_process_roots(_strong_roots_scope, false, // yg was scanned above GenCollectedHeap::ScanningOption(_collector->CMSCollector::roots_scanning_options()), _collector->should_unload_classes(), &par_mri_cl, &cld_closure, &_par_state_string); assert(_collector->should_unload_classes() || (_collector->CMSCollector::roots_scanning_options() & GenCollectedHeap::SO_AllCodeCache), "if we didn't scan the code cache, we have to be ready to drop nmethods with expired weak oops"); _timer.stop(); log_trace(gc, task)("Finished remaining root initial mark scan work in %dth thread: %3.3f sec", worker_id, _timer.seconds()); }
整个过程比较简单,从 GC Root 出发标记 Old 中的对象,处理完成后借助 BitMap 处理下 Young 区对 Old 区的引用,整个过程基本都比较快,不多会有较大的停顿。
CMSCollector::checkpointRootsFinalWork()
中,总体代码和步骤以下:void CMSCollector::checkpointRootsFinalWork() { GCTraceTime(Trace, gc, phases) tm("checkpointRootsFinalWork", _gc_timer_cm); assert(haveFreelistLocks(), "must have free list locks"); assert_lock_strong(bitMapLock()); ResourceMark rm; HandleMark hm; CMSHeap* heap = CMSHeap::heap(); if (should_unload_classes()) { CodeCache::gc_prologue(); } assert(haveFreelistLocks(), "must have free list locks"); assert_lock_strong(bitMapLock()); heap->ensure_parsability(false); // fill TLAB's, but no need to retire them // Update the saved marks which may affect the root scans. heap->save_marks(); print_eden_and_survivor_chunk_arrays(); { if (CMSParallelRemarkEnabled) { GCTraceTime(Debug, gc, phases) t("Rescan (parallel)", _gc_timer_cm); do_remark_parallel(); } else { GCTraceTime(Debug, gc, phases) t("Rescan (non-parallel)", _gc_timer_cm); do_remark_non_parallel(); } } verify_work_stacks_empty(); verify_overflow_empty(); { GCTraceTime(Trace, gc, phases) ts("refProcessingWork", _gc_timer_cm); refProcessingWork(); } verify_work_stacks_empty(); verify_overflow_empty(); if (should_unload_classes()) { CodeCache::gc_epilogue(); } JvmtiExport::gc_epilogue(); assert(_markStack.isEmpty(), "No grey objects"); size_t ser_ovflw = _ser_pmc_remark_ovflw + _ser_pmc_preclean_ovflw + _ser_kac_ovflw + _ser_kac_preclean_ovflw; if (ser_ovflw > 0) { log_trace(gc)("Marking stack overflow (benign) (pmc_pc=" SIZE_FORMAT ", pmc_rm=" SIZE_FORMAT ", kac=" SIZE_FORMAT ", kac_preclean=" SIZE_FORMAT ")", _ser_pmc_preclean_ovflw, _ser_pmc_remark_ovflw, _ser_kac_ovflw, _ser_kac_preclean_ovflw); _markStack.expand(); _ser_pmc_remark_ovflw = 0; _ser_pmc_preclean_ovflw = 0; _ser_kac_preclean_ovflw = 0; _ser_kac_ovflw = 0; } if (_par_pmc_remark_ovflw > 0 || _par_kac_ovflw > 0) { log_trace(gc)("Work queue overflow (benign) (pmc_rm=" SIZE_FORMAT ", kac=" SIZE_FORMAT ")", _par_pmc_remark_ovflw, _par_kac_ovflw); _par_pmc_remark_ovflw = 0; _par_kac_ovflw = 0; } if (_markStack._hit_limit > 0) { log_trace(gc)(" (benign) Hit max stack size limit (" SIZE_FORMAT ")", _markStack._hit_limit); } if (_markStack._failed_double > 0) { log_trace(gc)(" (benign) Failed stack doubling (" SIZE_FORMAT "), current capacity " SIZE_FORMAT, _markStack._failed_double, _markStack.capacity()); } _markStack._hit_limit = 0; _markStack._failed_double = 0; if ((VerifyAfterGC || VerifyDuringGC) && CMSHeap::heap()->total_collections() >= VerifyGCStartAt) { verify_after_remark(); } _gc_tracer_cm->report_object_count_after_gc(&_is_alive_closure); // Change under the freelistLocks. _collectorState = Sweeping; // Call isAllClear() under bitMapLock assert(_modUnionTable.isAllClear(), "Should be clear by end of the final marking"); assert(_ct->cld_rem_set()->mod_union_is_clear(), "Should be clear by end of the final marking"); }
Final Remark 是最终的第二次标记,这种状况只有在 Background GC 执行了 InitialMarking 步骤的情形下才会执行,若是是 Foreground GC 执行的 InitialMarking 步骤则不须要再次执行 FinalRemark。Final Remark 的开始阶段与 Init Mark 处理的流程相同,可是后续多了 Card Table 遍历、Reference 实例的清理并将其加入到 Reference 维护的 pend_list
中,若是要收集元数据信息,还要清理 SystemDictionary、CodeCache、SymbolTable、StringTable 等组件中再也不使用的资源。
知道了两个 STW 过程执行流程,咱们分析解决就比较简单了,因为大部分问题都出在 Final Remark 过程,这里咱们也拿这个场景来举例,主要步骤:
-XX:+PrintReferenceGC
参数开启。基本在日志里面就能定位到大概是哪一个方向出了问题,耗时超过 10% 的就须要关注。2019-02-27T19:55:37.920+0800: 516952.915: [GC (CMS Final Remark) 516952.915: [ParNew516952.939: [SoftReference, 0 refs, 0.0003857 secs]516952.939: [WeakReference, 1362 refs, 0.0002415 secs]516952.940: [FinalReference, 146 refs, 0.0001233 secs]516952.940: [PhantomReference, 0 refs, 57 refs, 0.0002369 secs]516952.940: [JNI Weak Reference, 0.0000662 secs] [class unloading, 0.1770490 secs]516953.329: [scrub symbol table, 0.0442567 secs]516953.373: [scrub string table, 0.0036072 secs][1 CMS-remark: 1638504K(2048000K)] 1667558K(4352000K), 0.5269311 secs] [Times: user=1.20 sys=0.03, real=0.53 secs]
【根因】 有了具体的方向咱们就能够进行深刻的分析,通常来讲最容易出问题的地方就是 Reference 中的 FinalReference 和元数据信息处理中的 scrub symbol table 两个阶段,想要找到具体问题代码就须要内存分析工具 MAT 或 JProfiler 了,注意要 dump 即将开始 CMS GC 的堆。在用 MAT 等工具前也能够先用命令行看下对象 Histogram,有可能直接就能定位问题。
java.lang.ref.Finalizer
对象的 dominator tree,找到泄漏的来源。常常会出现问题的几个点有 Socket 的 SocksSocketImpl
、Jersey 的 ClientRuntime
、MySQL 的 ConnectionImpl
等等。_should_unload_classes
被设置为 true 时在 CMSCollector::refProcessingWork()
中与 Class Unload、String Table 一块儿被处理。if (should_unload_classes()) { { GCTraceTime(Debug, gc, phases) t("Class Unloading", _gc_timer_cm); // Unload classes and purge the SystemDictionary. bool purged_class = SystemDictionary::do_unloading(_gc_timer_cm); // Unload nmethods. CodeCache::do_unloading(&_is_alive_closure, purged_class); // Prune dead klasses from subklass/sibling/implementor lists. Klass::clean_weak_klass_links(purged_class); } { GCTraceTime(Debug, gc, phases) t("Scrub Symbol Table", _gc_timer_cm); // Clean up unreferenced symbols in symbol table. SymbolTable::unlink(); } { GCTraceTime(Debug, gc, phases) t("Scrub String Table", _gc_timer_cm); // Delete entries for dead interned strings. StringTable::unlink(&_is_alive_closure); } }
【策略】 知道 GC 耗时的根因就比较好处理了,这种问题不会大面积同时爆发,不过有不少时候单台 STW 的时间会比较长,若是业务影响比较大,及时摘掉流量,具体后续优化策略以下:
-XX:+ParallelRefProcEnabled
对 Reference 进行并行处理。-XX:-CMSClassUnloadingEnabled
来避免 MetaSpace 的处理,JDK8 会默认开启 CMSClassUnloadingEnabled,这会使得 CMS 在 CMS-Remark 阶段尝试进行类的卸载。正常状况进行的 Background CMS GC,出现问题基本都集中在 Reference 和 Class 等元数据处理上,在 Reference 类的问题处理方面,无论是 FinalReference,仍是 SoftReference、WeakReference 核心的手段就是找准时机 dump 快照,而后用内存分析工具来分析。Class 处理方面目前除了关闭类卸载开关,没有太好的方法。
在 G1 中一样有 Reference 的问题,能够观察日志中的 Ref Proc,处理方法与 CMS 相似。
并发的 CMS GC 算法,退化为 Foreground 单线程串行 GC 模式,STW 时间超长,有时会长达十几秒。其中 CMS 收集器退化后单线程串行 GC 算法有两种:
CMS 发生收集器退化主要有如下几种状况:
晋升失败(Promotion Failed)
顾名思义,晋升失败就是指在进行 Young GC 时,Survivor 放不下,对象只能放入 Old,但此时 Old 也放不下。直觉上乍一看这种状况可能会常常发生,但其实由于有 concurrentMarkSweepThread 和担保机制的存在,发生的条件是很苛刻的,除非是短期将 Old 区的剩余空间迅速填满,例如上文中说的动态年龄判断致使的过早晋升(见下文的增量收集担保失败)。另外还有一种状况就是内存碎片致使的 Promotion Failed,Young GC 觉得 Old 有足够的空间,结果到分配时,晋级的大对象找不到连续的空间存放。
使用 CMS 做为 GC 收集器时,运行过一段时间的 Old 区以下图所示,清除算法致使内存出现多段的不连续,出现大量的内存碎片。
碎片带来了两个问题:
增量收集担保失败
分配内存失败后,会判断统计获得的 Young GC 晋升到 Old 的平均大小,以及当前 Young 区已使用的大小也就是最大可能晋升的对象大小,是否大于 Old 区的剩余空间。只要 CMS 的剩余空间比前二者的任意一者大,CMS 就认为晋升仍是安全的,反之,则表明不安全,不进行Young GC,直接触发Full GC。
显式 GC
这种状况参见场景二。
并发模式失败(Concurrent Mode Failure)
最后一种状况,也是发生几率较高的一种,在 GC 日志中常常能看到 Concurrent Mode Failure 关键字。这种是因为并发 Background CMS GC 正在执行,同时又有 Young GC 晋升的对象要放入到了 Old 区中,而此时 Old 区空间不足形成的。
为何 CMS GC 正在执行还会致使收集器退化呢?主要是因为 CMS 没法处理浮动垃圾(Floating Garbage)引发的。CMS 的并发清理阶段,Mutator 还在运行,所以不断有新的垃圾产生,而这些垃圾不在此次清理标记的范畴里,没法在本次 GC 被清除掉,这些就是浮动垃圾,除此以外在 Remark 以前那些断开引用脱离了读写屏障控制的对象也算浮动垃圾。因此 Old 区回收的阈值不能过高,不然预留的内存空间极可能不够,从而致使 Concurrent Mode Failure 发生。
分析到具体缘由后,咱们就能够针对性解决了,具体思路仍是从根因出发,具体解决策略:
-XX:UseCMSCompactAtFullCollection=true
来控制 Full GC的过程当中是否进行空间的整理(默认开启,注意是Full GC,不是普通CMS GC),以及 -XX: CMSFullGCsBeforeCompaction=n
来控制多少次 Full GC 后进行一次压缩。-XX:CMSInitiatingOccupancyFraction
的值,让 CMS GC 尽早执行,以保证有足够的连续空间,也减小 Old 区空间的使用大小,另外须要使用 -XX:+UseCMSInitiatingOccupancyOnly
来配合使用,否则 JVM 仅在第一次使用设定值,后续则自动调整。-XX:+CMSScavengeBeforeRemark
在过程当中提早触发一次 Young GC,防止后续晋升过多对象。正常状况下触发并发模式的 CMS GC,停顿很是短,对业务影响很小,但 CMS GC 退化后,影响会很是大,建议发现一次后就完全根治。只要能定位到内存碎片、浮动垃圾、增量收集相关等具体产生缘由,仍是比较好解决的,关于内存碎片这块,若是 -XX:CMSFullGCsBeforeCompaction
的值很差选取的话,能够使用 -XX:PrintFLSStatistics
来观察内存碎片率状况,而后再设置具体的值。
最后就是在编码的时候也要避免须要连续地址空间的大对象的产生,如过长的字符串,用于存放附件、序列化或反序列化的 byte 数组等,还有就是过早晋升问题尽可能在爆发问题前就避免掉。
内存使用率不断上升,甚至开始使用 SWAP 内存,同时可能出现 GC 时间飙升,线程被 Block 等现象,经过 top 命令发现 Java 进程的 RES 甚至超过了 -Xmx
的大小。出现这些现象时,基本能够肯定是出现了堆外内存泄漏。
JVM 的堆外内存泄漏,主要有两种的缘由:
UnSafe#allocateMemory
,ByteBuffer#allocateDirect
主动申请了堆外内存而没有释放,常见于 NIO、Netty 等相关组件。哪一种缘由形成的堆外内存泄漏?
首先,咱们须要肯定是哪一种缘由致使的堆外内存泄漏。这里能够使用 NMT(NativeMemoryTracking) 进行分析。在项目中添加 -XX:NativeMemoryTracking=detail
JVM参数后重启项目(须要注意的是,打开 NMT 会带来 5%~10% 的性能损耗)。使用命令 jcmd pid VM.native_memory detail
查看内存分布。重点观察 total 中的 committed,由于 jcmd 命令显示的内存包含堆内内存、Code 区域、经过 Unsafe.allocateMemory
和 DirectByteBuffer
申请的内存,可是不包含其余 Native Code(C 代码)申请的堆外内存。
若是 total 中的 committed 和 top 中的 RES 相差不大,则应为主动申请的堆外内存未释放形成的,若是相差较大,则基本能够肯定是 JNI 调用形成的。
缘由一:主动申请未释放
JVM 使用 -XX:MaxDirectMemorySize=size
参数来控制可申请的堆外内存的最大值。在 Java8 中,若是未配置该参数,默认和 -Xmx
相等。
NIO 和 Netty 都会取 -XX:MaxDirectMemorySize
配置的值,来限制申请的堆外内存的大小。NIO 和 Netty 中还有一个计数器字段,用来计算当前已申请的堆外内存大小,NIO 中是 java.nio.Bits#totalCapacity
、Netty 中 io.netty.util.internal.PlatformDependent#DIRECT_MEMORY_COUNTER
。
当申请堆外内存时,NIO 和 Netty 会比较计数器字段和最大值的大小,若是计数器的值超过了最大值的限制,会抛出 OOM 的异常。
NIO 中是:OutOfMemoryError: Direct buffer memory
。
Netty 中是:OutOfDirectMemoryError: failed to allocate capacity byte(s) of direct memory (used: usedMemory , max: DIRECT_MEMORY_LIMIT )
。
咱们能够检查代码中是如何使用堆外内存的,NIO 或者是 Netty,经过反射,获取到对应组件中的计数器字段,并在项目中对该字段的数值进行打点,便可准确地监控到这部分堆外内存的使用状况。
此时,能够经过 Debug 的方式肯定使用堆外内存的地方是否正确执行了释放内存的代码。另外,须要检查 JVM 的参数是否有 -XX:+DisableExplicitGC
选项,若是有就去掉,由于该参数会使 System.gc 失效。(场景二:显式 GC 的去与留)
缘由二:经过 JNI 调用的 Native Code 申请的内存未释放
这种状况排查起来比较困难,咱们能够经过 Google perftools + Btrace 等工具,帮助咱们分析出问题的代码在哪里。
gperftools 是 Google 开发的一款很是实用的工具集,它的原理是在 Java 应用程序运行时,当调用 malloc 时换用它的 libtcmalloc.so,这样就能对内存分配状况作一些统计。咱们使用 gperftools 来追踪分配内存的命令。以下图所示,经过 gperftools 发现 Java_java_util_zip_Inflater_init
比较可疑。
接下来能够使用 Btrace,尝试定位具体的调用栈。Btrace 是 Sun 推出的一款 Java 追踪、监控工具,能够在不停机的状况下对线上的 Java 程序进行监控。以下图所示,经过 Btrace 定位出项目中的 ZipHelper
在频繁调用 GZIPInputStream
,在堆外内存分配对象。
最终定位到是,项目中对 GIPInputStream
的使用错误,没有正确的 close()。
除了项目自己的缘由,还可能有外部依赖致使的泄漏,如 Netty 和 Spring Boot,详细状况能够学习下这两篇文章,Spring Boot引发的“堆外内存泄漏”排查及经验总结、Netty堆外内存泄露排查盛宴。
首先能够使用 NMT + jcmd 分析泄漏的堆外内存是哪里申请,肯定缘由后,使用不一样的手段,进行缘由定位。
在 GC 日志中,出现 GC Cause 为 GCLocker Initiated GC。
2020-09-23T16:49:09.727+0800: 504426.742: [GC (GCLocker Initiated GC) 504426.742: [ParNew (promotion failed): 209716K->6042K(1887488K), 0.0843330 secs] 1449487K->1347626K(3984640K), 0.0848963 secs] [Times: user=0.19 sys=0.00, real=0.09 secs] 2020-09-23T16:49:09.812+0800: 504426.827: [Full GC (GCLocker Initiated GC) 504426.827: [CMS: 1341583K->419699K(2097152K), 1.8482275 secs] 1347626K->419699K(3984640K), [Metaspace: 297780K->297780K(1329152K)], 1.8490564 secs] [Times: user=1.62 sys=0.20, real=1.85 secs]
JNI(Java Native Interface)意为 Java 本地调用,它容许 Java 代码和其余语言写的 Native 代码进行交互。
JNI 若是须要获取 JVM 中的 String 或者数组,有两种方式:
因为 Native 代码直接使用了 JVM 堆区的指针,若是这时发生 GC,就会致使数据错误。所以,在发生此类 JNI 调用时,禁止 GC 的发生,同时阻止其余线程进入 JNI 临界区,直到最后一个线程退出临界区时触发一次 GC。
GC Locker 实验:
public class GCLockerTest { static final int ITERS = 100; static final int ARR_SIZE = 10000; static final int WINDOW = 10000000; static native void acquire(int[] arr); static native void release(int[] arr); static final Object[] window = new Object[WINDOW]; public static void main(String... args) throws Throwable { System.loadLibrary("GCLockerTest"); int[] arr = new int[ARR_SIZE]; for (int i = 0; i < ITERS; i++) { acquire(arr); System.out.println("Acquired"); try { for (int c = 0; c < WINDOW; c++) { window[c] = new Object(); } } catch (Throwable t) { // omit } finally { System.out.println("Releasing"); release(arr); } } } }
#include <jni.h> #include "GCLockerTest.h" static jbyte* sink; JNIEXPORT void JNICALL Java_GCLockerTest_acquire(JNIEnv* env, jclass klass, jintArray arr) { sink = (*env)->GetPrimitiveArrayCritical(env, arr, 0); } JNIEXPORT void JNICALL Java_GCLockerTest_release(JNIEnv* env, jclass klass, jintArray arr) { (*env)->ReleasePrimitiveArrayCritical(env, arr, sink, 0); }
运行该 JNI 程序,能够看到发生的 GC 都是 GCLocker Initiated GC,而且注意在 “Acquired” 和 “Released” 时不可能发生 GC。
GC Locker 可能致使的不良后果有:
-XX+PrintJNIGCStalls
参数,能够打印出发生 JNI 调用时的线程,进一步分析,找到引起问题的 JNI 调用。JNI 产生的 GC 问题较难排查,须要谨慎使用。
在这里,咱们把整个文章内容总结一下,方便你们总体地理解回顾。
下图为总体 GC 问题普适的处理流程,重点的地方下面会单独标注,其余的基本都是标准处理流程,此处再也不赘述,最后在整个问题都处理完以后有条件的话建议作一下复盘。
送上一张问题根因鱼骨图,通常状况下咱们在处理一个 GC 问题时,只要能定位到问题的“病灶”,有的放矢,其实就至关于解决了 80%,若是在某些场景下不太好定位,你们能够借助这种根因分析图经过排除法去定位。
-XX:+HeapDumpOnOutOfMemoryError
等一些参数就再也不提了,笔者建议添加如下参数,能够提升咱们分析问题的效率。其余建议: 上文场景中没有提到,可是对 GC 性能也有提高的一些建议。
-XX:-UseBiasedLocking
来提升性能。-XX:+AlwaysPreTouch
参数,让 VM 在 commit 内存时跑个循环来强制保证申请的内存真的 commit,避免运行时触发缺页异常。在一些大内存的场景下,有时候能将前几回的 GC 时间降一个数量级,可是添加这个参数后,启动的过程可能会变慢。最后,再说笔者我的的一些小建议,遇到一些 GC 问题,若是有精力,必定要探本穷源,找出最深层次的缘由。另外,在这个信息泛滥的时代,有一些被“奉为圭臬”的经验可能都是错误的,尽可能养成看源码的习惯,有一句话说到“源码面前,了无秘密”,也就意味着遇到搞不懂的问题,咱们能够从源码中一窥究竟,某些场景下确有奇效。但也不是只靠读源码来学习,若是硬啃源码但不理会其背后可能蕴含的理论基础,那很容易“捡芝麻丢西瓜”,“只见树木,不见森林”,让“了无秘密”变成了一句空话,咱们仍是要结合一些实际的业务场景去针对性地学习。
你的时间在哪里,你的成就就会在哪里。笔者也是在前两年才开始逐步地在 GC 方向上不断深刻,查问题、看源码、作总结,每一个 Case 造成一个小的闭环,目前初步摸到了 GC 问题处理的一些门道,同时将经验总结应用于生产环境实践,慢慢地造成一个良性循环。
本篇文章主要是介绍了 CMS GC 的一些常见场景分析,另一些,如 CodeCache 问题致使 JIT 失效、SafePoint 就绪时间长、Card Table 扫描耗时等问题不太常见就没有花太多篇幅去讲解。Java GC 是在“分代”的思想下内卷了不少年才突破到了“分区”,目前在美团也已经开始使用 G1 来替换使用了多年的 CMS,虽然在小的堆方面 G1 还略逊色于 CMS,但这是一个趋势,短期没法升级到 ZGC,因此将来遇到的 G1 的问题可能会逐渐增多。目前已经收集到 Remember Set 粗化、Humongous 分配、Ergonomics 异常、Mixed GC 中 Evacuation Failure 等问题,除此以外也会给出 CMS 升级到 G1 的一些建议,接下来笔者将继续完成这部分文章整理,敬请期待。
“防火”永远要胜于“救火”,不放过任何一个异常的小指标(通常来讲,任何不平滑的曲线都是值得怀疑的) ,就有可能避免一次故障的发生。做为 Java 程序员基本都会遇到一些 GC 的问题,独立解决 GC 问题是咱们必须迈过的一道坎。开篇中也提到过 GC 做为经典的技术,很是值得咱们学习,一些 GC 的学习材料,如《The Garbage Collection Handbook》《深刻理解Java虚拟机》等也是常读常新,赶忙动起来,苦练 GC 基本功吧。
最后的最后,再多啰嗦一句,目前全部 GC 调优相关的文章,第一句讲的就是“不要过早优化”,使得不少同窗对 GC 优化望而却步。在这里笔者提出不同的观点,熵增定律(在一个孤立系统里,若是没有外力作功,其总混乱度(即熵)会不断增大)在计算机系统一样适用,若是不主动作功使熵减,系统终究会脱离你的掌控,在咱们对业务系统和 GC 原理掌握得足够深的时候,能够放心大胆地作优化,由于咱们基本能够预测到每个操做的结果,放手一搏吧,少年!
美团到店事业群住宿门票数据智能组诚招小伙伴,从供、控、选、售等层面全方位提高业务竞争力,十万级 QPS 处理,亿级数据分析,完整业务闭环,目前有海量 HC,有兴趣的请将邮件发送至 hezhiming@meituan.com,咱们会在第一时间与你联系。
想阅读更多技术文章,请关注美团技术团队(meituantech)官方微信公众号。