jvm-03-垃圾收集

Reference

垃圾回收区域

内存垃圾回收的目的是未必避免程序在运行过程中出现内存溢出和内存泄漏问题。

JVM内存划分中,程序计数器、本地方法栈、虚拟机栈所需要的内存大小在代码结构确定后就已知的,并且会随着线程的生命周期创建和销毁,所以这部分内存分配具有确定性,不需要过多关注。

而对于堆和方法区,需要多大内存是要在运行时才能知道的,因为方法的参数不同,调用次数不同,这些都是在运行期动态生成的,所以垃圾回收的职责就是对这部分垃圾对象进行回收,释放内存。

回收方法区:java虚拟机规范中,不要求方法区实现垃圾收集器,主要是对方法区进行垃圾收集的性价比不高,但是方法区也是可以在运行时产生新的字面量和类的。例如new String(“abc”)的时候,如果常量池中不存在“abc”这个对象,则会在常量池中创建。JSP的实现过程是将新的servlet类加载到jvm中。

所以方法区(HotSpot用永久代实现方法区)的垃圾回收主要包括:废弃常量和无用类。 是否对类进行回收HotSpott提供了-Xnoclassgc来控制

方法区内存的回收查看《深入理解Java虚拟机(第2版)》3.2.5章节

判断哪些对象可以回收

  • 引用计数法:为每个对象创建一个引用计数器,统计所有指向该对象的引用个数,当一个对象引用个数为0时,则说明对象没有引用可以回收 > 缺点:引用计数法无法处理循环引用的对象

  • 可达性分析法:通过“GC Roots”对象集往下搜索引用链,当一个引用链无法到达“GC Roots”时,则说明该引用链的所有对象可以回收(obj5、obj6、obj7无法到达GC Roots,所以可以被回收)。

可达性分析法

引用计数法是目前主流的垃圾标记算法,GC Roots包括 - 虚拟机栈中引用的对象 - 方法区中静态属性引用的对象 - 方法区中常量引用的对象 - 本地方法(JNI)栈中引用的对象

垃圾回收算法

  • 标记-清除算法(Mark-Sweep):最基本的垃圾收集算法,逻辑简单,缺点:效率偏低,容易产生内存碎片

    gc-mark-sweep

  • 复制算法(Copying):逻辑简单、效率高、没有内存碎片,但是内存使用率不高,适合存活对象少(需要复制的对象少)的情况

    gc-copying

  • 标记-整理算法(Mark-Compact):内存利用率高,没有内存碎片,适合存活对象多,垃圾少的情况(需要移动位置的对象少)

    gc-compact

分代垃圾回收

以上垃圾回收算法各自都有优缺点,为了针对不同场景,jvm开发者对内存做了划分,针对不同区域使用不同的回收算法。

  • 新生代
    • 大部分对象在gc时被回收,存活对象少,使用复制算法。为了解决内存占用过高问题,将新生代划分成了Eden, Survivors(s0, s1或者from, to),内存分配比例是Eden:from:to = 8:1:1
  • 老年代
    • 经过多次gc后依然存活的对象进入老年代,所以gc时存活对象多,使用“标记-清除”或者“标记-整理”算法

详细查看《深入理解Java虚拟机(第2版)》3.3章节

垃圾对象转移和收集过程

  1. 新对象在Eden区创建,当Eden区快满的时候,进行Minor GC(YGC),将Eden清空,把存活对象放到From区,然后新对象继续在Eden区创建。
  2. 当Eden区又满的时候,进行Minor GC(YGC),清理Eden区和From区垃圾对象,把存活对象放到To区,依次类推,From区和To区交替使用。
  3. 当对象经过多次Minor GC(YGC)后依然存活(超过回收年龄,不同垃圾收集器的阈值不一样,可以通过参数修改),进入老年代(Old区)。
  4. 如果存活对象太多,From区或者To区放不下,则直接进入Old区。
  5. Old区快满时(当内存使用比例达到一定阈值,不同收集器阈值不一样,cms收集器默认是92%),触发Major GC(FGC),同时对新生代和老年代进行垃圾回收。

垃圾收集器

各垃圾收集器类型和适用分代范围

JVM结构体系

Serial

单线程串行收集器,用于新生代垃圾收集。使用“复制算法”,多用于客户端程序。

Serial Old

Serial的老年代版本,同样是单线程收集器,使用“标记-整理”算法。多用于客户端程序,同时作为CMS担保失败后的备预案收集器。

ParNew

Serial的多线程版本,同时是使用“复制算法”,目的是为了充分利用cpu资源,缩短停顿时间。适用于多线程的服务器程序,配合CMS收集器一起使用

Parallel Scavenge

与ParNew一样,是使用复制算法的并发新生代收集器。不同之处在于Parallel Scavenge是吞吐量优先的,可以设置期望停顿时间(MaxGCPauseMillis)和吞吐量(GCTimeRatio),但是无法与CMS仪器配合使用。

Parallel Old

Parallel Scavenge的老年代版本,使用”标记-整理“算法。在JDK1.6之后提供,为了配合Parallel Scavenge实现高吞吐量的收集器组合。

CMS

低停顿的并发收集器,部分阶段能够与用户线程同时进行。采用“标记-清除”算法,多用于web等对延迟要求较高的系统。

随着垃圾收集器的发展,在JDK1.9中CMS已经被标记为废弃

垃圾收集过程

  • 初始标记(initial mark): stop the world,仅标记GC Roots能直接关联到的对象,速度很快;
  • 并发标记(concurrent mark): 进行GC Roots Tracing的过程;
  • 重新标记(remark): stop the world,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但比并发标记时间短;
  • 并发清除(concurrent sweep): 整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

优缺点

  • 优点:并发收集、低停顿
  • 缺点
    1. 对CPU资源非常敏感,会与用户线程抢占cpu资源,在cpu资源不足的情况下同样会影响程序执行
    2. 无法处理浮动垃圾、产生大量空间碎片
    3. 因为cms在垃圾收集时,用户线程同时运行,在这期间产生的垃圾超过可提供的内存空间时,cms将临时采用后预案,使用Serial Old对老年代重新进行垃圾收集。

-XX:CMSinitiatingoccupancyFraction可以设置老年代达到内存占用比例时触发cms,即预留多少空间给垃圾收集期间同时产生的对象。 JDK1.5 默认是68%,JDK1.6默认值调整到了92%

G1

同样是以低延迟为目标的并发收集器,在高并发和大内存下表现很好,在JDK1.9中成为了默认垃圾收集器。物理上已经没有严格的年代划分,将堆内存划分成多个region,针对每个region进行收集,没有垃圾碎片,可以设置期望停顿时间。如果最求高吞吐量,G1并不是一个好的选择。

详细可查看《深入理解java虚拟机(第2版)》3.5.7章节,或者这篇文章:不用找了,深入理解G1垃圾收集器和GC日志,都整理好了

常用GC收集器组合

垃圾收集器 回收算法 参数 说明
Serial
Serial Old
复制算法
标记-整理算法
-XX:+UseSerialGC
(Serial + Serial Old)
单线程,不能充分利用CPU,多用于client模式
Parallel Scavenge
Parallel Old
复制算法
标记-整理算法
-XX:+UseParallelOldGC
(Parallel Scavenge + Parallel Old)
多线程,并行收集,高吞吐量
ParNew
CMS
复制算法
标记-清除算法
-XX:+UseConcMarkSweepGC
(ParNew + CMS)
多线程,追求极致低延迟
G1 复制算法 +
标记整理
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
(可选,期望停顿时间ms)
多线程,低延迟,JDK1.9中默认的收集器