垃圾回收算法和垃圾收集器

对象是否可被回收

引用计数法

实现原理: 为每个对象配备一个整形的计数器,例如,对象A,只要有任何对象引用了A,则A的引用计数器就加1,当引用失效时,减1;当A的引用计数器为0时,对象A就不可能再被使用,即可以进行回收。

存在问题:

  1. 无法处理循环引用的问题。所以在Java垃圾回收器中,没有使用这种方法。

循环引用:有对象A和B,A中含有对象B的引用,对象B中含有A的引用;此时,A和B的计数器都不为0,即无法进行回收;但是在整个系统中,没有其他对象引用了A和B,A和B是应该被回收的对象,但由于垃圾对象之间的互相引用,从而使垃圾回收器无法识别,造成内存泄漏。

  1. 引用计数器要求每次引用产生和消除的时候,都要伴随一次加减操作,对系统性能有一定影响。

可达性分析法

实现原理: 通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链时(即不可达),则此对象不可用,可以判定为可回收对象。

可以作为 GC Roots 的对象:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(即一般说的Native方法)引用的对象

判断可触及性

通过可达性分析法可以判断哪些对象不可达。一般来说,不可达对象需要被回收。但事实上,该对象有可能在某一条件下“复活自己”,如果这样,再回收就不合理了。为此需要定义对象的可触及性状态,并规定在什么状态下,可以安全回收对象。

可触及性包含三种状态:

  • 可触及的:从根节点出发,可以到达这个对象。
  • 可复活的:对象的所有引用都被释放,但是对象有可能在 finalize() 函数中复活。
  • 不可触及的:对象的finalize()函数被调用,并且没有复活,或者 对象的 finalize() 没有重写(没必要执行), 那么就会进入不可触及状态,此时对象不可能被复活,因为 finalize() 只会被调用一次。

对象只有在不可触及状态时,才可以被回收。

注意:

  • finalize()函数是一个非常糟糕的模式,不推荐使用它释放资源。
  • finalize() 有可能发生引用外泄,在无意中复活对象。
  • finalize() 被系统调用,调用时间不明确。释放资源推荐在 try-catch-finally 中实现。

引用级别

Java 提供四个级别的引用,由强到弱依次是:强引用、软引用、弱引用、虚引用。除强引用外,其他都可以在 java.lanf.ref 包中找到。

不同引用级别出现的意义: 希望描述这样一类对象:当内存对象还足够时,则保留在内存之中;如果内存空间在进行垃圾回收后还是非常紧张,则可以抛弃这些对象。

  • 强引用:类似 Object object = new Object() 这类的引用,只要强引用还存在,垃圾回收器永远不会回收掉引用的对象。
  • 软引用:用来描述一些还有用但是非必须的对象。对于软引用关联的对象,系统将在发生内存溢出前,将这些对象列进回收范围进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用:用来描述非必须对象。它关联的对象只能拿生存到下一次垃圾收集发生之前,当垃圾回收器工作时,无论内存是否足够,都会回收被弱引用关联的对象。
  • 虚引用:一个对象是否有虚引用,完全不会影响其生存周期,也无法通过虚引用取得对象实例。为对象设置虚引用关联的唯一目的是能在这个对象被回收时得到系统通知。

软引用,弱引用非常适合保存可有可无的缓存数据,从而加速系统运行。

垃圾回收算法

标记清除算法

顾名思义:此算法分为两个阶段 标记 和 清除。

标记:标记的过程其实就是,遍历所有的 GC Roots,然后将所有GC Roots可达的对象标记为存活的对象。

清除:清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。

不足:

  1. 效率问题:标记和清除过程的效率都不高。
  2. 空间碎片问题:标记清除后会出现大量不连续的内存碎片,空间碎片太多会导致以后程序运行无法分配较大对象时,提前触发GC。

复制算法

将原有的内存空间分为两块,每次只是用其中一块,在GC时,将正在使用的内存中存活对象复制到未使用的内存块中,然后,清除正在使用内存的所有对象,交换两块内存的角色。

复制算法适合存活对象少、垃圾对象多的情况,所以复制算法很适合新生代(新生代中垃圾对象通常多于存活对象)。

新生代中的 Eden 存活下来的对象,Survivor区不能完全存放,那么这些对象会通过分配担保机制进入老年代。

标记整理算法

标记整理算法是一种老年代的回收算法。首先需要从根节点开始,对所有可达对象做一次标记,然后将所有存活对象整理到内存的一端,之后清理边界外所有的空间。

分代收集算法

将内存区间根据对象的特点分成几块,根据每块内存区间的特点,使用不同的回收算法,以提高垃圾回收的效率。

对于新生代,回收频率很高,但每次GC耗时很短,而老年代频率低,但会消耗更长的时间。为了支持高频率的新生代回收,虚拟机使用一种叫做卡表的数据结构。卡表为一个比特位集合,每一个比特位可以用来表示老年代的某一区域中的所有对象是否持有新生代对象的引用。

这样在新生代GC时,可以不用耗大量时间扫描所有老年代对象,来确定引用关系,可以先扫描卡表,卡表位为1时,才包含新生代引用,在新生代GC时,只需扫描卡表位为1所在的老年代空间,这样可大大加快新生代回收速度。

分区算法

分区算法将整个堆空间划分成连续的小区间。每个小区间都独立使用,独立回收。这种做法的好处是可以控制一次回收多少个小区间,从而很好的控制GC产生的停顿时间。

垃圾回收器

Serial收集器

新生代串行回收器:

  1. 它只使用单线程进行垃圾回收
  2. 它是独占式的垃圾回收

由于串行收集器只使用单线程回收,所以在垃圾回收时会出现“Stop The World”。这样会造成很槽糕的用户体验,在实时性要求较高的场景不适应。

由于新生代串行收集器使用复制算法,实现相对简单,逻辑处理特别高效,而且没有线程切换开销。在单CPU处理器等硬件平台不是特别优越的场合,它的性能可以超过并行回收器和并发回收器。

Serial Old 收集器

老年代串行回收器:

  1. 使用的是标记-整理算法
  2. 串行的,它是独占式的

可以作为CMS回收器的备用回收器

ParNew收集器

新生代并发回收器

  1. 回收策略,算法,参数和新生代串行回收器一样
  2. 回收过程进行了多线程化,但是回收过程应用程序依旧会全部暂停

Parallel Scavenge 收集器

同样也是新生代并发收集器,但是它却注重系统吞吐量。

Parallel Old 收集器

老年代并发回收器,使用标记-清除算法

  1. 多线程并发收集器,也是一种关注吞吐量的收集器

CMS收集器

老年代并发收集器,主要关注于系统停顿时间。

  1. 初始标记: STW(Stop The World)标记根对象
  2. 并发标记:标记所有对象
  3. 预清理:清理前准备以及控制停顿时间
  4. 重新标记:STW,修正并发标记数据
  5. 并发清理:清理垃圾
  6. 并发重置

初始标记和重新标记是独占系统资源的,而预清理、并发标记、并发清除和并发重置是可以和用户线程一起执行的。

G1收集器


分享到:


相關文章: