JVM垃圾收集器

本文主要介绍JVM垃圾回收机制以及实际遇到的内存收缩能力相关的问题以及解决方案。

这里的内存收缩能力主要是指:JVM在向操作系统申请内存使用之后,在进行JVM垃圾回收并且释放一定内存后是否一直占用操作系统的内存。若JVM一直占用,将导致ACS容器平台中的内存利用率较低,很多空余的内存没有得到实际的使用。

前提都是在Xms和Xmx不一致并且相差不接近的情况。

Java堆结构

在JVM中,一般存在方法区、堆区、程序计数器,本地方法站,java虚拟机栈。
一般对象的创建和销毁都发生在堆中,内存溢出等现象大多数情况都发生在堆空间,所以本文指的内存伸缩能力都指的是堆空间的内存申请和释放。

在JVM中,一般把堆分为两个区,年轻代和老年代。年轻代占堆的1/3,老年代占2/3。

年轻代

年轻代分为Eden区,Survivor 0区和Survivor 1区。
年轻代的垃圾回收称之为Minor GC,Eden区满,垃圾回收,放到S1,S1满垃圾回收,放到S2,S1和S2循环,达到晋升年龄,转移到老年代。通过复制法清理
Eden:s1:s2 = 8:1:1

老年代

老年代一般用标记-清理法进行垃圾清理。老年代的垃圾回收一般称之为Major GC(Full GC)。

常见的垃圾收集算法

在介绍垃圾收集算法之前,首先要明确什么是垃圾。

确定垃圾

一般有两种办法确定垃圾。

  1. 引用计数法,通过引用次数来判断是否回收,注意:循环引用不能回收
  2. 可达性分析,GC Roots作为起始节点,看是否可达。

垃圾收集算法

标记清除,复制算法,标记清理,标记压缩(往一端移动),分代收集,分区收集。

各个垃圾收集器介绍

新生代:
Serial 标记-复制,适用于单CPU环境
Parallel ParNew 标记-复制 client模式下的JVM
Parallel Scavenge 标记-复制,又称为吞吐量优先收集器,达到可控吞吐量,JDK1.7,JDK1.8默认新生代收集器
吞吐量 = 运行用户代码时间/(运行用户代码时间+垃圾回收时间)
系统会根据系统的当前GC情况自适应调节MaxGCPauseMillis、GCTimeRatio参数,来保证系统最合适的停顿时间和吞吐量。
老年代:
Serial Old 标记压缩 单线程 CMS后备方案
Parallel Old 标记压缩 多线程,JDK1.8默认老年代收集器
CMS 标记清除 会产生碎片,基本被G1取代
G1 标记压缩 多核 并行 高回收率

垃圾收集器组合及选择

UseSerialGC(串行GC): Serial(新生代)+ Serial Old(老年代)
UseParallelGC(并行GC):arallel Scavenge(新生代)+ Parallel Old(老年代)
UseConcMarkSweepGC: Parallel ParNew(新生代) + CMS收集器(老年代)+ Serial Old, Serial Old作为CMS出错后的垃圾收集。
UseG1GC(并发GC): 都使用G1。

  1. 如果应用程序很小(最多大约100M),则选择UseSerialGC
  2. 如果应用程序将在单个处理器上运行并且没有暂停时间要求,那么让VM自己选择,或者选择带有选项的则选择UseSerialGC
  3. 如果应用程序性能是第一优先级且没有暂停时间要求或者1秒或者更长的暂停是可以接受的,那么就让VM选择,或者使用UseParallelGC
  4. 如果响应时间比总吞吐时间更重要并且垃圾收集暂停必需保持短于大约1秒,则用UseConcMarkSweepGC或者UseG1GC

四种组合对比

参数 新生代收集器 新生代GC算法 老年代收集器 老年代GC算法 是否具有伸缩能力 优点 缺点 适用场景
UseSerialGC Serial 复制算法 Serial Old 标记整理 稳定,GC效率高 单线程,GC时间长,不适合大内存的服务 适用于Client段,小内存,单CPU场景
UseParallelGC arallel Scavenge 复制算法 Parallel Old 标记整理 是,但无法模拟 多线程并行回收,吞吐量高 停顿时间较长 适用于多CPU,在后台处理而不需要过多交互的应用
UseConcMarkSweepGC Parallel ParNew 复制算法 CMS + Serial Old 标记清除 并发执行,停顿时间段,响应快 会产生大量内存碎片,对CPU压力大 适用于内存大,CPU核数多的服务端,适合应用在互联网网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统的响应时间最短
UseG1GC G1 标记整理,局部复制 G1 标记整理,局部复制 能满足高吞吐和暂停时间,能并发执行,能更快整理空闲内存,可预测GC时间 调优复杂 适合内存大,CPU核数多的服务端,可替换CMS

ACS云Java应用内存占用高实例

JVM存在申请了堆内存后,并未将未使用的内存返回给操作系统。
JDK1.8分为两种情况:
若采用JDK1.8的默认垃圾收集器(UseParallelGC:Parallel Scavenge(新生代) + Parallel Old (老年代) ):
在启动应用时,一般会配置-Xms和-Xmx参数。一般在JVM启动后,回向操作系统申请应用需要的内存大小。
当应用由于并发高或需要大内存分析任务等情况时,此时JVM内存不足,JVM会不断向操作系统申请内存直到最大堆内存。
当应用由于并发减少或占用的内存不断减少时,JVM会触发GC,JVM堆内存空间总大小会在峰值的大小,内存未释放给操作系统,
所以一直处在峰值
通过内存占用指标,不一定能真实反馈JVM的内存情况,需要查看JVM中实际的内存占用情况。
若采用JDK1.8的G1垃圾收集器时:
在应用申请JVM内存之后,在应用无需过多JVM内存,GC后堆内存减少,此时内存占用率会下降。
在JDK1.9已经将G1作为默认垃圾收集器。

演示方式:

  1. 在应用中,会模拟一个缓存,并在缓存中创建一个数组对象。
  2. 通过接口不断创建缓存,模拟JVM堆内存增长
  3. 通过接口不断删除缓存,模拟JVM堆内存减少
  4. 通过接口清空JVM堆内存
  5. 通过接口手动gc触发JVM的GC

模拟过程:

  1. -Xloggc:/tmp/gc.log 将GC日志打印到日志文件
    -XX:PrintGCDetails: 打印JVM的GC详细信息
    
  2. 应用启动时状态 top
  3. 增加JVM内存,增加4次后,手动gc,发现内存占用上升
  4. 再次申请直至峰值
  5. 删除堆内存,并触发gc,top发现内存占用率不变
  6. 清空堆内存,内存占用率不变,要看gc日志中对内存空间的实际占用情况

JVM内存占用率与容器平台内存占用率

从上述示例可以看出,容器重的JVM内存占用率和容器平台内存占用率并不完全相等。

当容器中的-Xmx和-Xms不同,并且JVM内存具有伸缩能力的时候,容器平台中的内存占用率大致可以反映出JVM内存的占用情况。
当容器中的-Xmx和-Xms相同,JVM不具备内存伸缩能力时,容器平台中的内存占用率不能体现JVM内存占用情况,体现不了JVM堆内存的空闲或是占用高。

第二种情况,需要开发者通过:

  1. Java分析工具
  2. JDK自带的分析工具 jmap -hep xxx[可通过jps命令查看]
  3. 显式调用Java的Runtime.getRuntime().totalMemory()、Runtime.getRuntime().maxMemory(), Runtime.getRuntime().freeMemory()方法,查看JVM内存占用情况。

上述问题解决方案

优先通过JVM参数控制内存资源,控制堆内存在垃圾回收后将内存释放给操作系统,并通过实际JVM内存占用率辅助决策(可能需要容器平台提供Java应用的实际内存占用情况),评估JVM最大内存用量以及容器内存limit,让内存利用率更高。
若堆内存在垃圾回收之后无法将内存释放给操作系统,通过JVM实际堆内存占用率和应用各方面情况,进行人工流程去评估内存资源的上下限。