若是你关注过 elasticsearch 的日志,可能会看到以下相似的内容:java
[2018-06-30T17:57:23,848][WARN ][o.e.m.j.JvmGcMonitorService] [qoo--eS] [gc][228384] overhead, spent [2.2s] collecting in the last [2.3s] [2018-06-30T17:57:29,020][INFO ][o.e.m.j.JvmGcMonitorService] [qoo--eS] [gc][old][228385][160772] duration [5s], collections [1]/[5.1s], total [5s]/[4.4d], memory [945.4mb]->[958.5mb]/[1007.3mb], all_pools {[young] [87.8mb]->[100.9mb]/[133.1mb]}{[survivor] [0b]->[0b]/[16.6mb]}{[old] [857.6mb]->[857.6mb]/[857.6mb]}
看到其中的[gc]
关键词你也猜到了这是与 GC 相关的日志,那么你了解每一部分的含义吗?若是不了解,你能够继续往下看了。算法
咱们先从最简单的看起:编程
日志发生的时间
日志级别
,这里分别是WARN
和INFO
输出日志的类
,咱们后面也会讲到这个类当前 ES 节点名称
gc
关键词,咱们就从这个关键词聊起。友情提示:
对 GC 已经了如指掌的同窗,能够直接翻到最后看答案。数据结构
GC,全称是 Garbage Collection
(垃圾收集)或者 Garbage Collector
(垃圾收集器)。多线程
在使用 C语言编程的时候,咱们要手动的经过 malloc
和 free
来申请和释放数据须要的内存,若是忘记释放内存,就会发生内存泄露的状况,即无用的数据占用了宝贵的内存资源。而Java 语言编程不须要显示的申请和释放内存,由于 JVM 能够自动管理内存,这其中最重要的一部分就是 GC
,即 JVM 能够自主地去释放无用数据(垃圾)占用的内存。并发
咱们研究 GC 的主要缘由是 GC 的过程会有 Stop The World
(STW)的状况发生,即此时用户线程会中止工做,若是 STW 的时间过长,则应用的可用性、实时性等就降低的很厉害。jvm
GC
主要解决以下3个问题:elasticsearch
咱们一个个来看下。spa
所谓垃圾,指的是再也不被使用(引用)的对象。Java 的对象都是在堆(Heap)上建立的,咱们这里默认也只讨论堆。那么如今问题就变为如何断定一个对象是否还有被引用,思路主要有以下两种:线程
方法1简单粗暴效率高,但准确度不行,尤为是面对互相引用的垃圾对象时无能为力。
方法2是目前经常使用的方法,这里有一个关键是 GC Roots
,它是断定的源头,感兴趣的同窗能够本身去研究下,这里就不展开讲了。
垃圾找到了,该怎么回收呢?看起来彷佛是个很傻的问题。直接收起来扔掉不就行了?!对应到程序的操做,就是直接将这些对象占用的空间标记为空闲不就行了吗?那咱们就来看一下这个基础的回收算法:标记-清除(Mark-Sweep)算法。
该算法很简单,使用经过可达性分析分析方法标记出垃圾,而后直接回收掉垃圾区域。它的一个显著问题是一段时间后,内存会出现大量碎片,致使虽然碎片总和很大,但没法知足一个大对象的内存申请,从而致使 OOM,而过多的内存碎片(须要相似链表的数据结构维护),也会致使标记和清除的操做成本高,效率低下,以下图所示:
为了解决上面算法的效率问题,有人提出了复制算法。它将可用内存一分为二,每次只用一块,当这一块内存不够用时,便触发 GC,将当前存活对象复制(Copy)到另外一块上,以此往复。这种算法高效的缘由在于分配内存时只须要将指针后移,不须要维护链表等。但它最大的问题是对内存的浪费,使用率只有 50%。
但这种算法在一种状况下会很高效:Java 对象的存活时间极短。据 IBM 研究,Java 对象高达 98% 是朝生夕死的,这也意味着每次 GC 能够回收大部分的内存,须要复制的数据量也很小,这样它的执行效率就会很高。
该算法解决了第1中算法的内存碎片问题,它会在回收阶段将全部内存作整理,以下图所示:
但它的问题也在于增长了整理阶段,也就增长了 GC 的时间。
既然大部分 Java 对象是朝生夕死的,那么咱们将内存按照 Java 生存时间分为 新生代(Young)
和 老年代(Old)
,前者存放短命僧,后者存放长寿佛,固然长寿佛也是由短命僧升级上来的。而后针对二者能够采用不一样的回收算法,好比对于新生代
采用复制算法会比较高效,而对老年代
能够采用标记-清除或者标记-整理算法。这种算法也是最经常使用的。JVM Heap 分代后的划分通常以下所示,新生代通常会分为 Eden、Survivor0、Survivor1区,便于使用复制算法。
将内存分代后的 GC 过程通常相似下图所示:
Eden
区建立Eden
区满,触发 Young GC,此时将 Eden
中还存活的对象复制到 S0
中,并清空 Eden
区后继续为新的对象分配内存Eden
区再次满后,触发又一次的 Young GC,此时会将 Eden
和S0
中存活的对象复制到 S1
中,而后清空Eden
和S0
后继续为新的对象分配内存Old
区Old
区也会在合适的时机进行本身的 GC前面咱们讲了众多的垃圾收集算法,那么其具体的实现就是垃圾收集器,也是咱们实际使用中会具体用到的。现代的垃圾收集机制基本都是分代收集算法,而 Young
与 Old
区分别有不一样的垃圾收集器,简单总结以下图:
从上图咱们能够看到 Young
与 Old
区有不一样的垃圾收集器,实际使用时会搭配使用,也就是上图中两两连线的收集器是能够搭配使用的。这些垃圾收集器按照运行原理大概能够分为以下几类:
咱们下面简单看一下他们的运行机制。
该类 Young区
的为 Serial GC
,Old区
的为Serial Old GC
。执行大体以下所示:
该类Young 区
的有 ParNew
和 Parallel Scavenge
,Old 区
的有Parallel Old
。其运行机制以下,相比 Serial GC ,其最大特色在于 GC 线程是并行的,效率高不少:
该类目前只是针对 Old 区
,最多见就是CMS GC
,它的执行分为多个阶段,只有部分阶段须要中止用户进程,这里不详细介绍了,感兴趣能够去找相关文章来看,大致执行以下:
目前最新的 GC 有G1GC
和ZGC
,其运行机制与上述均不相同,虽然他们也是分代收集算法,但会把 Heap 分红多个 region 来作处理,这里不展开讲,感兴趣的能够参看最后参考资料的内容。
Elasticsearch 默认的 GC 配置是CMS GC
,其 Young 区
用 ParNew
,Old 区
用CMS
,你们能够在 config/jvm.options
中看到以下的配置:
## GC configuration -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly
如今咱们已经知道如何找到和回收垃圾了,那么何时回收呢?简单总结以下:
Young 区
的GC 都是在 Eden 区
满时触发Old 区
是在 Young GC 时预测Old 区是否能够为 young 区 promote 到 old 区 的 object 分配空间,若是不可用则触发 Old GC。这个也能够理解为是 Old区
满时。Old 区
大小超过必定比例后触发,而不是 Old 区满。这个缘由在于 CMS GC 是并发的算法,也就是说在 GC 线程收集垃圾的时候,用户线程也在运行,所以须要预留一些 Heap 空间给用户线程使用,防止因为没法分配空间而致使 Full GC 发生。前面讲了这么多,终于能够回到开篇的问题了,咱们直接来看答案
[2018-06-30T17:57:23,848][WARN ][o.e.m.j.JvmGcMonitorService] [qoo--eS] [gc][228384] overhead, spent [2.2s] collecting in the last [2.3s]
[gc][这是第228384次GC 检查] 在最近 2.3 s 内花了 2.2s 用来作垃圾收集,这占比彷佛有些过了,请抓紧来关注下。
[2018-06-30T17:57:29,020][INFO ][o.e.m.j.JvmGcMonitorService] [qoo--eS] [gc][old][228385][160772] duration [5s], collections [1]/[5.1s], total [5s]/[4.4d], memory [945.4mb]->[958.5mb]/[1007.3mb], all_pools {[young] [87.8mb]->[100.9mb]/[133.1mb]}{[survivor] [0b]->[0b]/[16.6mb]}{[old] [857.6mb]->[857.6mb]/[857.6mb]}
咱们直接来看具体的含义好了,相信有了前面的 GC 基础知识,你们在看这里解释的时候就很是清楚了。
从日志中咱们能够看到输出这些日志的类名叫作JvmGcMonitorService
,咱们去源码中搜索很快会找到它/Users/rockybean/code/elasticsearch/core/src/main/java/org/elasticsearch/monitor/jvm/JvmGcMonitorService.java
,这里就不详细展开讲解源码了,它执行的内容大概以下图所示:
关于打印日志的格式在源码也有,以下所示:
private static final String SLOW_GC_LOG_MESSAGE = "[gc][{}][{}][{}] duration [{}], collections [{}]/[{}], total [{}]/[{}], memory [{}]->[{}]/[{}], all_pools {}"; private static final String OVERHEAD_LOG_MESSAGE = "[gc][{}] overhead, spent [{}] collecting in the last [{}]";
另外细心的同窗会发现输出的日志中 gc 只分了 young 和 old ,缘由在于 ES 对 GC Name 作了封装,封装的类为:org.elasticsearch.monitor.jvm.GCNames
,相关代码以下:
public static String getByMemoryPoolName(String poolName, String defaultName) { if ("Eden Space".equals(poolName) || "PS Eden Space".equals(poolName) || "Par Eden Space".equals(poolName) || "G1 Eden Space".equals(poolName)) { return YOUNG; } if ("Survivor Space".equals(poolName) || "PS Survivor Space".equals(poolName) || "Par Survivor Space".equals(poolName) || "G1 Survivor Space".equals(poolName)) { return SURVIVOR; } if ("Tenured Gen".equals(poolName) || "PS Old Gen".equals(poolName) || "CMS Old Gen".equals(poolName) || "G1 Old Gen".equals(poolName)) { return OLD; } return defaultName; } public static String getByGcName(String gcName, String defaultName) { if ("Copy".equals(gcName) || "PS Scavenge".equals(gcName) || "ParNew".equals(gcName) || "G1 Young Generation".equals(gcName)) { return YOUNG; } if ("MarkSweepCompact".equals(gcName) || "PS MarkSweep".equals(gcName) || "ConcurrentMarkSweep".equals(gcName) || "G1 Old Generation".equals(gcName)) { return OLD; } return defaultName; }
在上面的代码中,你会看到不少咱们在上一节中提到的 GC 算法的名称。
至此,源码相关部分也讲解完毕,感兴趣的你们能够自行去查阅。
讲解 GC 的文章已经不少,本文又唠唠叨叨地讲一遍基础知识,是但愿对于第一次了解 GC 的同窗有所帮助。由于只有了解了这些基础知识,你才不至于被这些 GC 的输出吓懵。但愿本文对你理解 ES 的 GC 日志 有所帮助。
若是你想深刻的了解 JAVA GC 的知识,能够关注 ElasticTalk
公众号,回复 GC
关键词后便可获取做者推荐的电子书等资料。