【JVM实战】再谈GC算法

JVM GC算法,对于了解JVM的程序员,应该都不会陌生,在这篇文章中,整理一下常见的GC算法分享给大家。

GC算法

  • 复制算法
  • 标记清除算法(Mark-Sweep)
  • 标记压缩算法(Mark-Compact)
  • 分代算法(Genarational Collection)
  • 分区算法(Region 主要在G1垃圾收集器中使用)

复制算法

其核心思想是:将内存划分为两块,每次只使用其中的一块,在垃圾回收的时候,回收其中的一块,将不需要回收的内存复制到另一块空间中,交换两个空间,此后往复回收垃圾。

从上面我们可以看出对于复制算法,如果需要回收的对象很多,那么每次只需要复制少量的对象到另一块空间即可,因此其效率很高,这也是新生代中使用该算法的原因,由于该算法的代价是牺牲一块内存进行交换,如果每次GC后存活的对象很多,需要复制大量的对象,所以其效率低下,再者空间利用率也不高,会发生频繁的GC,导致STM(Stop-The-World),所以这也是为什么老年代中不使用复制算法的原因。

在现代的java串行垃圾回收器中,使用了复制算法的思想,新生代被分为eden空间和两个survivor空间,一般称为from区和to区,其中的一块用于存放每次GC存活的对象。

在新生代垃圾回收的时候,会将Eden区和其中一块survivor区(假设from)中存活的对象复制到to区中(大对象或者老年对象直接进入老年代),这样,Eden和from中全是垃圾,直接回收,即保证了空间的连续性,也避免了大量的空间浪费。

标记清除算法

标记清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。

在回收时,该算法在标记阶段会首先通过根节点,标记所有从根节点开始的可达对象,没有被标记的就是未被引用的垃圾对象,然后在清除阶段就会清除所有未被标记的对象。但是该算法最可能产生的问题就是空间碎片问题。

标记整理算法

该算法建立在标记清除算法的基础上,避免了碎片的问题,它在标记完对象后,不单单只进行垃圾回收,同时还会将存活的对象进行压缩整理到内存的一端,这样避免了大量的碎片的存在,而且也不需要使用两块内存相同的空间,所以性价比比较高,也正好符合老年代对象存活时间长和多的现象。

分代算法

上面简单介绍了几种垃圾回收算法,我们可以看到每种算法都有自己的优缺点,适用于不同的场景下,那么如何高效的回收内存呢?现代java虚拟机一般都采用分代回收的算法,将内存划分为几个部分,根据每块内存空间的特点,选择合适的GC算法。

新生代中,一般都是一些朝生夕灭的对象,大多对象需要被回收,因此,适用复制算法回收效率更加高效。
老年代中,一般回收的垃圾不多,而且都是一些大对象,不适合继续使用复制算法,所以可以使用标记清除或者标记整理算法来回收,具体选择哪种需要根据具体的垃圾回收器,是否需要进行空间整理来决定。

对于新生代来说,回收的频率很高,每次回收的耗时也很短,而老年代回收的频率比较低,但是很耗时。为了支持高频率的新生代回收,虚拟机可能会使用了一种叫卡表(Card Table)的数据结构,卡表是一个比特位的集合,每一个比特位可以用来表示老年代的某一区域中所有对象是否持有新生代对象的引用。这样新生代GC的时候,就不需要花费大量的时间去扫描所有老年代对象来确定每个对象的引用关系,而是先扫描卡表,只有当卡表为1的时候,需要扫描给定区域的老年代对象,而卡表为0的所在区域的老年代对象中,一定不包含新生代对象的引用。如下图 所示,卡表中每一位表示老年代中的4KB的空间,卡表记录为0的老年代区域中没有任何对象指向新生代,卡表记录为1的区域才有对象包含新生代引用,所以在新生代GC时,只需要扫描卡表位为1的老年代空间即可,这样加快了回收的速度。
【JVM实战】再谈GC算法_第1张图片
分区算法

分代算法将整个内存空间按照对象的生命周期长短分为了不同的区域,而分区算法是将整个堆空间划分为连续的不同小区间,每一个小区间都独立使用,独立回收,这种算法的好处是可以控制一次回收多少个区间(即可以控制回收时间)。

一般来说,相同的条件下,堆空间越大,一次GC时所需要的时间就越长,从而产生的停顿时间就越长,即GC时间越长,为了更好的控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,合理地回收若干小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。

你可能感兴趣的:(JVM)