GC算法,垃圾回收器

635 阅读10分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

GC算法 垃圾回收

主要关注点:

  • 对象存活判断
  • GC算法
  • 垃圾回收器

对象存活判断

判断对象是否存活一般有两种方式:

  • 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
  • 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。
    • 什么是GCroot:
      • main方法是java程序的入口,一个main方法就是一个main栈桢,这个栈桢中开始的对象都是根对象
      • 静态变量:一个class文件被加载进内存后,会对静态变量做一个初始化,静态变量能够访问的到的对象叫根对象
      • 方法区中的常量引用的对象
      • JNI指针,如果调用了C/C++本地方法所用到的类对象
      • 换言之当程序启动之后马上需要的对象就是根对象

GC算法

GC最基础的算法有三种:标记 -清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般都采用分代收集算法。

  • 标记 -清除算法,“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有可用的对象,在标记完成后再扫描一遍标记出可回收的对象并清除,适用于存活对象多的场景,会有内存碎片,会stop-the-world,因为在标记的过程中没办法防止并发,所以在标记的过程中整个jvm可能就会停止运行。
  • 复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉,缺点就是内存浪费,适用于存活对象少的情况,因为复制对象需要开销,复制算法也有stw,因为要根据gcroot去找到存活对象,然后把存活对象复制到空闲的另一个区域,只不过新生代的对象有一个特点 朝生夕死,所以存活的对象不多,所以复制算法在这种情况下很快,并且自带‘整理’,不会出现内存碎片。
  • 标记-整理算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。扫描两次,需要移动对象,效率偏低。
  • 分代收集算法(hotspot),“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,年轻代用复制算法,老年代用标记整理/标记清除算法,对于老年代而言,每次垃圾回收只能释放小部分空间,若使用复制算法,每次将需要做大量复制。

垃圾回收器

  • Serial收集器,单线程串行收集器是最古老,最稳定以及效率高的收集器(因为单线程减少了上下文切换,减少系统开销),可能会产生较长的停顿(stop-the-world),只使用一个线程去回收。新生代收集器,复制算法
  • ParNew收集器,ParNew收集器其实就是Serial收集器的多线程版本。新生代收集器
  • Parallel收集器,Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。新生代收集器,复制算法
  • Parallel Old 收集器,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
  • CMS收集器,CMS(Concurrent Mark Sweep)并发的标记与清除,通常用在老年代内,标记根对象和收集过程中堆又发生改变的时候会stop-the-world。
    • 初始标记:会造成STW,先简单标记出根节点处的垃圾,所以停顿时间短暂
    • 并发标记:垃圾线程和用户线程同时运行,标记的过程中,因为用户线程也在运行,可能会有新的垃圾产生,也有可能有垃圾会被重新引用。这个过程也是最耗时的
    • Precleaning(预清理)
      • 处理新生代已经发现的引用
      • 重新标记那些在并发标记阶段引用被更新的对象
    • AbortablePreclean(可中断的预清理)
      • 发生的前提新生代Eden区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold 默认是2M
      • 为什么需要这个阶段,存在的价值是什么?
        • 因为CMS GC的终极目标是降低垃圾回收时的暂停时间,所以在该阶段要尽最大的努力去处理那些在并发阶段被应用线程更新的老年代对象,这样在暂停的重新标记阶段就可以少处理一些,暂停时间也会相应的降低。
    • 重新标记:会造成STW,因为并发标记的过程中,程序还在运行,肯定会有新的垃圾产生或者原先是垃圾的对象又被重新引用了,所以还需要再标记一次
    • 并发清理:最后再对垃圾进行清理,这个过程也因为用户线程在运行,所以还是会产生新的浮动垃圾,这些垃圾就需要等待下一次G C的时候来清理了
    • CMS的问题
      • 内存碎片化:因为CMS使用的算法是标记清除,所以会产生内存的碎片化,当到最后内存碎片化严重,导致一个对象进入老年代,已经没有足够的连续内存可以存放它的时候,它就不得不使用serialOld标记整理,serialOld又是单线程串行的,就会导致STW的时间特别长
        • 解决方案:
          • 针对一些大的朝生熄灭的对象,Surivor空间存放不下,就会存放到老年代,针对这种情况,可以扩大Survivor空间。
          • 老年代空间碎片,无法放下大对象,就在进行一定次数的Full GC(标记清除)的时候进行一次标记整理算法。
      • 浮动垃圾:CMS并发清理阶段,会产生浮动垃圾,老年代的清理速度赶不上年轻代的提升速度,就会造成concurrent mode failure
        • 解决方案:
          • 一定次数的Full GC(标记清除)的时候进行一次标记整理算法。
          • 提升老年代空间

\

  • G1收集器:将内存堆区域分而治之划分多个(region)区域,整体采用标记-整理算法,region内局部采用复制算法,不会产生内存碎片,同时可以设置并行收集的最大暂停时间供jvm决策具体使用的算法控制停顿时间。GC算法和垃圾回收器算法图解以及更详细内容参考 jvm系列(三):GC算法 垃圾收集器
    • 每个分区都可能是年轻代也可能是老年代,但是在同一时刻只能属于某个代,所以分代属于逻辑上的概念
    • G1会优先回收垃圾特别多的分区,这也就是G1名字的由来,garbage FIrst
    • G1高效回收的关键

      • 记录了这个region中有哪个对象被引用了,这样就垃圾回收这个region的时候就不需要到整个堆中去扫描,判断这个region中的哪个对象被引用了,直接到Rset里面去看,有记录的说明被引用了,没记录的就可以直接回收了
      • Rset会影响赋值效率,当有一个引用指向一个对象的时候,它又一个额外的操作,就是记录在Rset中,这叫做写屏障,和内存当中的写屏障不一样

G1什么时候触发fullGC

G1 提供了两种 GC 模式,Young GC 和 Mixed GC。

  • Young GC:当eden区的内存空间不足的时候会触发,会根据响应时间动态的调整eden区的region占比,来减少Ygc的发生,多线程并行的;
  • Mixed GC:相当于一个CMS,当对象占有对内存的空间已经超过了45%(有个参数可以调整)这个默认值的时候会触发,Mixed GC就是一套完整的CMS,也包含了CMS的完整过程。如果垃圾产生的速度远大于回收的速度,就会使用serial Old,进行FGC,效率非常低
  • G1也会有FGC,G1和CMS调优目标其中之一就是FGC尽量别有(一次都没有)
    • 如果产生FGC怎么办
      • 扩内存
      • 提高CPU性能(垃圾回收速度快,就不怕新对象堆积导致FGC了)
      • 降低触发Mixed GC的阈值,提早触发Mixed GC,就不容易FGC了

三色标记

  1. 白色对象,表示自身未被标记;
  2. 灰色对象,表示自身被标记,但内部引用未被处理;
  3. 黑色对象,表示自身被标记,内部引用都被处理;

漏标是指,本来是live object但是由于没有遍历到,被当成垃圾回收了

漏标的条件

  • 黑色对象已经是被标记完了的对象,下一次不会再对它进行重新标记
  • 黑色对象指向白色对象
  • 灰色对象指向白色对象的引用没了

三色标记的问题

  1. 线程1和2开始三色标记,

  1. 此时B与C之间引用关系断开,添加A与C的引用关系

  1. 完成标记

问题:我们发现C虽然有引用关系但是并没有标记,这就是漏标问题

打破方式

  • 增量更新:关注引用的增加,把A从黑色重新标记为灰色,下次重新扫描属性 (CMS使用)
  • SATB:关注引用的删除,当B->D消失时,要把这个引用推到GC堆栈,保证D还能被GC扫描到 (G1使用)
    • 为什么G1使用SATB算法:配合Rset使用,B指向D的引用消失,然后A指向D的引用又不存在时,D会被重新push到堆中,由于Rset的存在,下次扫描时就不需要扫描整个堆了,只需要去rset中看看D有没有其他对象引用即可,效率比较高

\

特点:

    • 吞吐量和PS相比下降了大概15%~20%
    • 响应时间大概在200ms左右
    • 特点:
      • 并发标记
      • 压缩空心空间不会延长GC的暂停时间
      • 更易预测GC暂停时间
      • 适用于不需要实现很高吞吐量的场景,但是响应时间要求高的场景,吞吐量可以升级硬件来实现
      • 和CMS一样采用三色标记算法
  • ZGC:颜色指针算法

总结

    • serial随着JDK的诞生而诞生,它是单线程的串行收集器,清理过程中会STW,而后为了提升效率诞生Parallel Scavenge。然后因为实在受不了STW产生了一款里程碑式的收集器CMS,为了配合它呢就诞生了ParNew。
    • 目前还没有一款收集器不会造成STW
    • 常见的垃圾回收器组合:Serial+SerialOld、CMS+PN、PS+PO(没做任何设置默认就是这两款收集器)

逻辑分代

只是个概念,物理内存上并没有区分年轻代、老年代

G1是逻辑分代模型,JDK1.9后出现

除Epsilon、ZGC、shenandoan之外的GC都有逻辑分代模型

物理分代

除G1外,不仅逻辑分代、还物理分代

有对内存进行划分