垃圾回收算法到JVM垃圾回收器

232 阅读10分钟

垃圾回收算法到垃圾回收器

垃圾回收算法

早期的垃圾回收算法是引用计数法,简单来说就是一个对象如果被其他对象所引用,那么对象头中的引用计数就递增。在进行垃圾回收时,遍历所有对象,如果对象的引用计数为0,则可以进行回收。但是这将会出现循环依赖问题,进而导致垃圾无法回收。

因此后续使用可达性算法判断是否需要回收对象,具体来说就是从一个Root对象开始,遍历找到它可以找到的对象,这些对象标记为不可回收,除此之外的对象都会被回收。

标记-清理算法

双色标记清除算法:

  • 首先得知道白色代表不确定的状态,可能会被回收。黑色代表确定的状态,不会被回收。
  • 其次GC拥有所有对象的引用集合称之为heapSet,还拥有Root Object对象集合称之rootSet

标记阶段:

  • 将heapSet对象都染成白色
  • 然后根据rootSet递归地查询可达的对象,将可达对象染成黑色

清理阶段:

  • 将heapSet集合中为白色的对象进行回收

从Java的内存区域划分来看看哪些属于Root Object:

  • 堆区:
    • Java自带的类加载器对象:因为java中的类都是动态加载的,需要根据类加载器来获取对应的类对象,并且内部会缓存已加载的类对象,故类加载器不应该被GC回收,
    • 线程对象:线程是调度的最小单位,自然不该被回收
  • 虚拟机栈:
    • 局部变量引用的堆区对象:局部变量有可能引用堆区的对象,对应的引用对象不能回收。
  • 本地方法:
    • JNI引用的Java对象:如果本地方法的实现,例如c代码实现中需要引用Java对象,此时Java对象不存在那么就会造成程序异常,为了简化本地方法的开发,JNI引用的Java对象不被回收是比较合适的。除此之外还有本地方法栈中的局部变量也不该被回收。
  • 方法区:
    • 方法区引用的堆区对象:例如静态变量、常量

对于染色算法来说,实际上有三个阶段:标记阶段、清理阶段、修改阶段,其中前两个阶段是垃圾回收器在运行,而最后一个阶段是应用程序在运行。

但是一般来说我们允许标记的途中让出CPU给应用程序执行修改阶段,那么此时就有可能出现一些问题:

  • 修改阶段删除一个黑色对象的所有引用,唯独这个黑色对象没删除,那么此时该黑色对象就成了浮动垃圾,只能等待下一次标记阶段令其成为白色进行回收。
  • 修改阶段新增的对象如果是白色的,那么在下一次清理阶段时一定会被删除
  • 修改阶段新增的对象如果是黑色的,虽然保证了下一次清理阶段时一定会被删除,但还有一种情况,这个新增的黑色对象引用了一个白色对象,那么白色对象在下一次清理阶段时一定会被删除,此时再次使用这个黑色对象引用的白色对象时就会出现诡异的NPE。
  • 修改阶段修改对象的引用关系,实际表现可能是删除一个黑色对象的所有引用,也可能不会出现任何问题。

以上问题实际上属于并发问题,因为三个阶段,两个线程实际上共享了同一份资源


为了解决上述提到的并发问题,采用了三色染色算法作为目前的GC算法基础。

  • 首先明确三种颜色的定义:白色需要被回收、黑色不被回收、灰色增量对象

标记阶段:

  • 将未标记的对象染成白色
  • 然后根据rootSet递归地查询可达的对象,将可达对象染成灰色
  • 然后根据灰色对象递归查询可达的对象,如果这个灰色对象可达对象都属于灰色,那么该灰色对象染成黑色

清理阶段:

  • 将heapSet集合中为白色的对象进行回收

实际上三色染色算法只解决了上述提到的并发问题的一部分:

  • 对于删除问题,还是等到下一个标记阶段进行处理
  • 对于新增问题,则令新增染为灰色,恢复标记阶段时需要继续对这部分对象标记
  • 对于修改引用问题,同理新增处理,将修改的对象染为灰色,在标记阶段重新进行标记。

复制算法以及标记整理算法

为了解决内存碎片问题,提出了复制算法、标记-整理算法。

  • 复制算法将内存划分为两大块,每次只能使用一块,在垃圾回收之后,将存活对象都复制到另一块内存中,并保持整齐。
    • 缺点显而易见就是可用内存变为了一半;除此之外,由于复制不可避免,如果存活对象数量太多的话,那么内存的重新分配次数就会很多。
  • 标记-整理算法使用一整块内存进行分配,在垃圾回收之后,会将存活对象整理到一端保持整齐。
    • 首先解决了内存缩小的问题;其次,部分对象可能原本就是在一端对齐存放,故只需要对其他对象进行内存分配即可。

分代收集算法

通过整理内存,避免了内存碎片浪费空间的问题,并且标记-整理算法通过只将部分对象进行重新分配内存的方式减少了分配次数。那么还能不能进一步地降低内存分配次数呢?

我们可以注意到不同对象的生命周期是不同的,有的短有的长,如果生命长短不同的对象随意分配内存,那么在一次GC过后,生命周期短的对象回收,而生命周期长的对象依旧存活于内存中,因此不难想象内存碎片就会出现,而此时就不得不将这些存活对象整理到一端保持对齐。

一种很好的解决办法就是将同类型的对象统一存放,即划分新生代和老年代,那么就可以直接预防内存碎片的产生。

除此之外,我们还可以根据不同年代对象的特点来使用不同的垃圾回收算法,例如新生代的对象,由于每次存活下来的数量不多,故可以直接采用复制算法;而老年代的对象,由于存活下来的数量多,故采用标记整理算法来避免多次内存分配。

进一步地,根据内存的占用情况对不同年代区域进行回收——Partial GC和Full GC。前者可以分别对新生代、老年代进行回收,后者则是对整个内存进行回收。

full gc的触发时机:

  • System.gc()方法的调用
  • metaspace区内存达到阈值
  • Minor GC晋升到老年代的平均大小大于老年代的剩余空间
  • Minor GC大对象晋升到老年代时,老年代剩余空间不足

跨代引用、记忆集和卡表

跨代引用

跨代引用指的是不同年代对象之间的引用关系。此处一般指新生代和老年代之间的引用关系。

  • 如果是新生代引用老年代的对象,那么在进行Minor GC时,新生代对象可以安全的被回收,而老年代不受影响,整个应用可以正常进行。
  • 但如果是老年代引用新生代的对象,那么在进行Minor GC时,新生代对象的回收就必须额外进行判断处理,否则就可能出现老年代引用的新生代对象在Minor GC之后可能为null的情况。

而我们知道,目前主流的垃圾回收器都是根据可达性算法进行回收,故解决跨代引用问题的一个方法就是将引用的新生代对象的老年代对象加入到GC Root中,那么就可以保证被引用的新生代对象不会被回收。

记忆集

从节省空间的角度来看,可以采用每次Minor GC的时候遍历老年代区域,从而找出引用着新生代对象的老年代对象。但对应地,这将十分耗费时间和效率。

另一种做法则是以空间换时间,通过维护一个数据结构来降低时间的开销。记忆集就是这么一种解决跨代引用而提出来的概念。记忆集简单来说就是一个抽象的数据结构,该数据结构要求能够维护非回收区域对象/指针引用回收区域对象/指针的集合。例如Minor GC中,老年代对象引用新生代对象的集合。

卡表

卡表是记忆集的一种实现方式,卡表将非回收区域划分为一个个卡页,并且在回收区域或者全局维护一个卡表,用于记录卡页中元素/对象是否存在跨代引用的情况,这种情况我们可以称该卡页为脏页

那么在对回收区域进行垃圾回收前,可以遍历卡表中的脏页,将该页的所有对象都加入到GC Root中。

JVM垃圾回收器

GC实际上是一个内存管理器,不仅仅负责垃圾回收,还涉及到内存的分配。

image.png

  • Serial/Serial Old:只用一个GC线程处理垃圾。新生代采用标记-复制回收算法,老年代采用标记-整理回收算法
    • ParNew:可以看作是Serial回收器的多线程版本,采用多个GC线程处理垃圾
  • Parallel Scavenge/Parallel Old:在ParNew的基础上,更注重吞吐量(应用程序执行时间与总时间的占比)的提升。换个说法就是尽量减少STW的时间。
  • CMS:HotSpot第一款真正意义上的并发垃圾回收器。CMS属于老年代垃圾回收器。通过并发处理,尽可能地降低了STW的时间
    • CMS的工作阶段可以分为四步:
      • 初始标记:此时只有GC线程进行标记工作
      • 并发标记:GC线程和应用线程并发工作,GC线程用于标记应用线程修改的对象
      • 重新标记:只有GC线程进行标记工作,此时对增量的灰色对象进行重新标记
      • 并发清理:GC线程和应用线程并发工作,GC线程会对白色对象进行回收
    • 采用的是标记清理算法,因此会出现大量的内存碎片

在介绍G1垃圾回收器之前,需要先介绍一下JAVA堆区的划分。G1之前,整个堆区分为新生代和老年代,其中新生代又划分为Eden区与两个survivor区(一般是8:1:1,survivor区就是用于辅助实现复制算法)。而到了G1时期,整个堆区将划分为多个Region,但同时还保留有Eden、Survivor、Old这些概念,这些不同类型的Region就分散在整个堆区中,即它们并不是连续的。

image.png

  • G1:
    • 回收算法:标记-复制算法以及标记-整理算法
    • 分代回收:对整个堆区的Region进行垃圾回收
    • 并发回收:G1的工作阶段和CMS差不多,也分为四步。
    • 可预测的时间模型:G1有一个特点就是维护了一个优先回收列表,因此根据可执行GC的时间来优先回收价值最大的Region。