一、问题现象
在EC2上运行良好的程序迁移到k8s托管后,内存会持续增长,最后被容器kill,然后pod 重启。该现象一开始未被注意到,而是因为另外一个现象被深究出来:在重启的过程中,cassandra client链接cassandra host失败导致应用一直启动失败。
二、问题分析
定位原因是老年代内存一直涨,但是没有回收的动作,也就是触发old gc的条件没有达到。当前docker内jvm实例的Max内存设置,为${upper-limit} - constant 字节量。constant为常量,比如300Mb,即这300兆留给了docker 系统及其他监控进程。每次pod被kill时,是因为总内存使用超过了upper-limit, 即jvm heap内存 + jvm非heap 内存 + docker系统占用 > upper-limit。从资源使用看有三种情况可以触发内存超限:heap使用超限、非heap超限,docker系统超限。从监控来看是heap内老年代的回收问题,老年代一直不进行GC回收,认为还有足够的内存可以容纳minor gc的新生代幸存者,但是pod容器不这么认为。
三、问题解决
有两种解决方案,一是更改GC策略,二是调整Max堆内存设置。从我们的使用场景来看更倾向于第一种。从目前的-XX:+UseParallelGC调整至-XX:+UseConcMarkSweepGC,并设置old gc的触发阈值-XX:CMSInitiatingOccupancyFraction=70 为70%,当老年代内存空间使用水位达到70%并进行gc 回收。第二种堆内存设置,不设置-Xmx项,添加-XX:MaxRAMPercentage=75.0选项,jvm的最大内存使用为RAM的75%。老年代使用CMS gc策略后,线上问题解决。
四、笔记小摘
- UseParallelGC vs UseParallelOldGC
In Java 7 update release 4 (also referred to as Java 7u4, or JDK 7u4), - XX:+UseParallelOldGC was made the default GC
and the normal mode of operation for Parallel GC. As of Java 7u4, specifying -XX:+UseParallelGC also enables -XX:+UseParallelOldGC,
and likewise specifying -XX:+UseParallelOldGC also enables -XX:+UseParallelGC
从Java 7u4(含)之后UseParallelGC 同 UseParallelOldGC 可以相互等价,但是如要设置建议明确设置为UseParallelOldGC。UseParallelGC 是面向新生代的回收算法,UseParallelOldGC既可作用于新生代也作用于老年代,在老年代的gc sweep阶段后还会进行内存压缩。
- GC 日志字段说明
引用自 https://plumbr.io/blog/garbage-collection/understanding-garbage-collection-logs
总体上是GC的类型,回收前后新生代内存变化,jvm heap总内存前后变化,从不同的视角GC所消耗的时间。
- GC 日志打印时间戳的配置项
-XX:+PrintGCDateStamps 示例:2019-12-23T07:59:33.102+0000
- 查看jvm 支持的选项列表
java -XX:+PrintFlagsFinal -version
- cms gc的六个阶段
引用自: http://www.javaperformancetuning.com/news/qotm026.shtml
The concurrent collector (Enabled using -XX:+UseConcMarkSweepGC). This collector tries to allow application processing to continue as much as possible during the collection.
Splitting the collection into six phases described shortly, four are concurrent while two are stop-the-world: 1. the initial-mark phase (stop-the-world, snapshot the old generation so that we can run most of the rest of the collection concurrent to the application threads); 2. the mark phase (concurrent, mark the live objects traversing the object graph from the roots); 3. the pre-cleaning phase (concurrent); 4. the re-mark phase (stop-the-world, another snapshot to capture any changes to live objects since the collection started); 5. the sweep phase (concurrent, recycles memory by clearing unreferenced objects); 6. the reset phase (concurrent). If "the rate of creation" of objects is too high, and the concurrent collector is not able to keep up with the concurrent collection,
it falls back to the traditional mark-sweep collector.
cms gc的时候会和应用的线程并存,其中有两个阶段会发生STW,初始标记阶段和再标记阶段。如果“创建对象的速率”太高,超过了gc的回收能力,cms会退化到传统的mark-sweep 收集器,即单线程的 & STW & one CPU used。
- 官方gc简要手册
https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html#t5
描述了jvm的主要组件构成:类加载子系统、运行时数据域、执行引擎及 jvm heap的分代说明。注意jdk8以后,不再有permanent域,而为Metaspace,jvm参数设置最好要加上-XX:MaxMetaspaceSize,避免由于业务代码classloader不断new造成的内存泄漏,默认MaxMetaspaceSize是不受限的,`By default Metaspace in Java VM 8 is not limited, though for the sake of system stability it makes sense to limit it with some finite value`。
以上。