JVM 常见GC算法以及GC的时机

各种GC算法

前言

  • 书接上文,前面已经介绍过一些常见的GC算法的名称。

标记-清除算法(Mark-Sweep)

算法分为两个阶段

  • 算法分为"标记(Mark)"和"清除(Sweep)"两个阶段,首先标记出所有需要回收的对象,然后回收所有需要回收的对象。
  • 缺点
    • 效率问题,标记和清理两个过程效率都不高,需要扫描所有对象。
    • 空间问题,标记清理之后会产生大量不连续的内存碎片,空间碎片太多可能会导致后续使用中无法找到足够的连续内存而提前触发另一次的垃圾搜集动作。堆越大,GC次数越多,碎片越严重动画6.gif

复制(Copying)收集算法

将内存分成两块

  • 将可用内存划分为两块,每次只使用其1的一块,当半区内存用完了,仅将还存活的对象复制到另外一块上面,然后就把原来整块内存空间一次性清理掉。
  • 这样使得每次内存回收都是对整个半区的回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存就可以了,实现简单,运行高效。
  • 缺点:
    • 只是这种算法的代价是将内存缩小为原来的一半,代价高昂。
    • 复制收集算法在对象存活率高的时候,效率有所下降。
  • 现在的商业虚拟机中都是用了这一种收集算法来回收【新生代】
  • 将内存分为一块较大的eden空间和2块较少的survivor空间,每次使用eden和其中一块survivor,当回收时将eden和survivor还存活的对象一次性拷贝到另外一块survivor空间上,然后清理掉eden和用过的survivor。
  • Oracle Hotspot虚拟机默认eden和survivor的大小比例是8:1,也就是每次只有10%的内存是“浪费”的。
  • 如果不想浪费50%的空间,就需要有额外的空间进行分配担保用于应付半区内存中所有对象都100%存活的极端情况,所以在【老年代】一般不能直接选用这种算法。

动画6.gif

  • 只需要扫描存活的对象,效率更高不会产生碎片。
  • 需要浪费额外的内存作为复制区。
  • 复制算法非常适合生命周期比较短的对象,因为每次GC总能回收大部分的对象,复制的开销比较小
  • 根据IBM的专门研究,98%的Java对象只会存活1个GC周期(用完即弃的对象),对这些对象很适合用复制算法。而且不用1:1的划分工作区和复制区的空间。

标记-整理(Mark-Compact)算法

移动存活对象

  • 复制收集算法在对象存活率较高时,效率就会变低。更关键的是,如果不想浪费 50%的空间, 就需要有额外的空间进行分配担保,所以老年代一般不能直接选用这种算法
  • 根据老年代的特点,有人提出一种标记-整理算法(Mark-Compact),标记过程仍然与标记-清除算法一样后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
  • 没有内存碎片。
  • 比Mark-Sweep耗费更多的时间进行compact。
  • 移动后。 image.png

分代收集(Generational Collecting)算法

主流的GC算法

  • 当前商业虚拟机的垃圾收集都是采用“分代收集”(Generational Collecting)算法
  • 根据对象不同的存活周期将内存划分为几块
  • 一般是把Java堆分作新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,譬如新生代每次GC都有大批对象死去,只有少量存活,那就选用复制算法只需要付出少量存活对象的复制成本就可以完成收集。 image.png
  • 综合前面几种GC算法的优缺点,针对不同生命周期的对象采用不同的GC算法。
    • Hotspot JVM中共划分为三个代:年轻代(Young Generation)老年代(Old Generation)和元空间(Mate Space) image.png

  • 年轻代(Young Generation)
    • 新生成的对象都放在新生代年轻代用复制算法进行GC(理论年轻代对象的生命周期非常短,所以适合复制算法)
    • 年轻代分三个区。一个Eden区,两个Survivor区(一般称为s0,s1,可以通过参数设置Survivor个数)。对象在Eden区中生成。 当Eden区满时,还存活的对象将被复制到一个Survivor(s0)区,当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor(s1)区。
    • 当第二个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制到老年代。
    • 2个Survivor是完全对称,轮流替换。
    • Eden和2个Survivor的缺省比例是8:1:1,也就是10%的空间会被浪费。可以根据GC log的信息调整大小的比例。
  • 老年代(Old Generation)
    • 存放了经过一次或多次GC还存活的对象。
    • 一般采用Mark-Sweep或者Mark-Compact算法进行GC。
    • 如果有多种垃圾收集器可以选择。每种垃圾收集器可以看作一个GC算法的具体实现。可以根据体应用的需求选用合适的垃圾收集器(追求吞吐量?追求最短的响应时间?)。
  • 当老年代占用了堆内存的45%时,就会触发混合回收,也就是对新生代和老年代同时进行垃圾回收。

内存回收

要回收的目标

  • GC要做的是将那些消亡(dead)的对象所占用的内存回收掉。
  • Hotspot认为没有引用的对象是消亡的。
  • Hotspot将引用分为四种:Strong、Soft、Weak、Phantom:
    • Strong即默认通过Object o=new Object()这种方式`赋值的引用。
    • Soft、Weak、Phantom这三种则都是继承Reference。
  • 在Full GC时会对Reference类型的引用进行特殊处理:
    • Soft:内存不够时一定会被GC、长期不用也会被GC。
    • Weak:一定会被GC,当被mark为消亡时,会在ReferenceQueue中通知。
    • Phantom:本来就没引用,当从jvm heap中释放时会通知。
      ## GC的时机
      ### 何时会触发GC
  • 在分代模型的基础上,GC从时机上分为两种: Scavenge GC和Full GC
  • Scavenge GC (Minor GC)
    • 触发时机:
      • 新对象生成时,
      • Eden空间满了
    • 理论上Eden区大多数对象会在Scavenge GC回收,复制算法的执行效率会很高,Scavenge GC时间比较短。
  • Full GC
    • 整个JVM内存进行整理,包括Young、Old和 Perm(Matespace)。
    • 主要的触发时机:
      • Old满了
      • Perm满了,没有永久代,Matespace理论上没有上限,如果设置了阈值,到达阈值就会触发。
      • system.gc()
      • 效率很低,尽量减少Full GC。

分类:
后端
标签: