JVM:垃圾回收算法、垃圾回收器

705 阅读11分钟

垃圾回收算法

什么是垃圾?为什么要回收?

在java中,当一个对象没有被任何指针指向的时候,他就成为了一个垃圾,也就应该对其进行回收,如果不对其进行回收就会造成内存泄露,甚至是内存溢出。

垃圾回收分为两个阶段垃圾标记和垃圾回收。

垃圾标记

引用计算算法(java并未使用,不存在循环引用造成内存泄露)

对每一个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。当对象被任何一个对象引用就会对计数器加以,引用失效就减一,直到计数器为0,就把该对象视为垃圾,对其进行回收。

该算法具有效率高,没有回收延迟的特性,但是他却增大了开销,每次进行加减法也增大了时间复杂度,同时,他不能处理循环引用的问题。所以实际上java并未采用这样的算法。

可达性分析算法

以根对象集合为起始点,按照从上到下的方式搜索根对象集合所连接的目标对象是否可达。内存中存活的对象都直接或者间接同根对象连接在一起。否则,就认为该对象应该被回收。

那么根元素集合包括哪些?

  • 虚拟机栈中引用的对象。

    • 例如,各个线程调用方法中使用到的参数,局部变量等。
  • 本地方法栈内引用的对象。

  • 方法区中静态属性引用的对象。

  • 方法区中常量引用的对象。

    • 字符串常量池中的引用。
  • 所有被同步锁持有的对象。

  • java虚拟机内部的引用。

对象的finalization机制

一个对象在被回收之前会执行finalization()方法,来进行资源释放等操作。

虽然对象的finalization机制听起来很高大上,但是我们平时好像从来没有用过,没有用过就对了,因为这是给垃圾回收调用的,并不需要程序员调用,而且也不允许程序员调用。因为finalization可能会导致对象复活,其次该机制发生的时间是由GC线程决定的,如果不发生GC就永远不会发生这个机制,最后,如果这个机制使用不当,就会严重影响GC性能。

对象的三种状态
  • 可触及状态

    • 正常使用的对象
  • 可复活状态

    • 对象不可触及,但是在finalization中被复活
  • 不可触及状态

    • 对象不能被复活,真正要被回收之前的状态。

虚拟机在判定一个对象是否被回收的时候,需要进行两次标记,第一次,判断该对象从根对象集合开始有没有引用链,,然后判断对象是否需要执行finalization()方法,如果该对象没有重写finalization()方法,活着之前已经调用过该方法(这个方法只能被调用一次),那么就认为不需要执行,那么该对象也就达到了第三种状态。

如果该对象重写了finalization()方法,且未执行过,那么该对象就会被插入一个队列中,由虚拟机自动创建一个低优先级的finalizer线程触发其finalization()方法,进行第二次标记,如果方法中,对象突然复活,就会将其移除即将回收的集合,达到第二个复活状态。

垃圾清除

标记清除算法

将所有具有引用执行的对象进行标记,对未标记的的对象进行清除。

由于清除阶段需要对所有对象进行遍历,所以效率不高,而且清除的空间是碎片化的,需要维护一个空闲列表。

复制清除算法

对所有存活的对象不进行标记,而是直接将其复制到另外一块一模一样大小的空间中去,然后对原来空间进行整体回收。这样做的好处是没有标记和清除过程,使得清除十分高效,复制过去的内存空间是连续的,不需要维护空闲表。但是需要两倍的内存空间,而且对象的复制会到值栈中引用地址会发生变化。

标记压缩算法

复制算法建立在存活对象少,垃圾比较多的情况下。该算法就是在标记清除算法的基础上进行了碎片整理,这样就不需要维护空闲列表。但是效率比标记清除算法更低。

分代收集算法

按照不同的分区选择不同的算法。在新生区,由于对象具有朝生夕死,回收频率高的特性,所以可以采用回收效率较高的复制算法。而对于老年区,由于对象的生命周期较长,而且空间比较大,回收频率不高,我们就可以采用标记清除,标记压缩算法或者混合使用。

增量收集算法

仍是传统的标记-清除算法和复制算法,只是通过通过分阶段的方式处理线程冲突问题,允许垃圾回收线程分阶段完成清理和复制。但是频繁的进行线程切换会造成垃圾回收的总成本升高,系统的吞吐量下降。

分区算法

将堆空间划分为很多个小空间,根据目标的停顿时间,每次合理的回收若干个小区间,从而减少一次GC所产生的停顿。

内存泄露

当一个对象不被使用的时候,jvm会自动对其进行回收,但是如果出现一些意外情况导致垃圾回收器没有对其回收,这样的现象,我们就叫他内存泄露,简单来说,就是对象不能被使用了,但是还不能回收掉,长期占用一定的内存空间。那么在哪些情况下会产生内存泄露?

当存在一些复杂的指针指向,如果存在对象不使用了,于是很多指针都放开,但是某一个指针没有放开,导致这些对象中。仍然有部分指针没有断开,而造成对象不能被回收。

另外,在单例模式中,单例的生命周期和应用程序的周期是一样长的,所以单例程序中,如果持有对不对象的引用的情况,那么这个外部对象就不能被回收,就会导致内存泄露。

还有一些就是资源未关闭连接的行为,数据库连接,网络连接,io操作等,就会导致对象不会被回收(主要是一些和外部资源有连接的对象)。

引用

强引用

类似new出来的对象,只要引用还在就永远不会进行回收。

Object o = new Object();

软引用

如果出现内存不足就会对软引用对象进行回收。

SoftReferencre<User> usersoftRef = new SoftReference<User>(new User("zero",18));

弱引用

生命周期仅存在于下一次垃圾回收之前,也就是只要发生GC就一定会回收弱引用。

WeakReference<User> userweakreference = new WeakReference<User>(new User("zero",18));

虚引用(对象回收跟踪)

虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

垃圾回收器

serial回收器(串行回收)

应用场景是在单核CPU的情况下使用一条收集线程去完成清理工作,在垃圾回收期间,其他所有的工作线程都要停下来,直到垃圾回收线程完毕。在其特殊的工作场景下,他的回收效率是最高的,因为他不需要反复进行多线程的切换。通常和Serial Old GC搭配工作。

ParNew回收器(并行回收)

采用并行回收的方式,其他跟serial回收器十分类似。主要运用在多CPU的环境下。可以和CMS GC以及 Serial Old GC搭配工作。

Parallel Scavenge回收器(吞吐量优先)

一个采用了复制算法,并行回收和stw机制的垃圾回收器。由此可知,该回收器的目标是注重吞吐量,

他和采用了标记-压缩算法、并行回收、stw机制的Parallel Old 收集器搭配, 效果非常好。(是jdk8的默认垃圾回收器)

cms回收器(并发收集器)

第一款能让垃圾回收线程和用户线程同时进行工作的回收器。他的关注点是尽可能缩短垃圾回收时用户线程的停顿时间,提高响应速度。

采用标记-清除算法,所以在垃圾回收之后会产生一些碎片空间,只能引用空闲列表的方式来执行后续的内存分配。

对CPU的资源也很敏感,在并发阶段因为会占用一部分线程,从而使程序变慢,总吞吐量也会降低。

无法处理浮动垃圾。在第二阶段的并发标记过程完成后,因为用户线程的某些操作使得某些之前不是垃圾的对象变成了垃圾,但是第三阶段的重写标记只是进行对之前怀疑是垃圾的对象确认一下是否是真的垃圾,不会标记新产生的这部分浮动垃圾。也就不会回收这部分垃圾。

流程:

  • 初始标记

    • 所有的工作线程都会出现短暂的stw,这个阶段的工作目的是标记所有GC Root能直接关联的对象,一旦标记完成就会恢复所有暂停线程,由于关联的对象比较小,所以速度非常快
  • 并发标记

    • 从GC root直接关联的对象开始遍历整个关联对象图的过程,这个过程耗时较长,但是不需要暂停用户线程,可以与垃圾收集并发运行。
  • 重新标记

    • 修正标记期间,因为用户进程继续运行导致标记变动的那部分对象的标记记录,这个阶段的停顿时间会比初始阶段长,但远比并发标记阶段时间短。
  • 并发清除

    • 清理删除标记阶段判断的已经不使用的对象,释放内存空间。这个阶段不需要移动存活的对象,所以也是可以和用户线程并发执行的。

G1垃圾回收器(区域分代化)

一个并行回收器,将堆内存划分成多个不想关的区域(物理上不连续),不同的区域表示不同的堆分区(Eden,s0,s1,old),这样做的目的是避免在堆中做全区域的回收,通过跟踪每个区域的回收价值(回收获得的空间大小以及所需时间比例)维护一个优先级列表,根据允许回收的时间,选择回收价值大的区域进行回收。

特点:

  • 并行性

    • 在回收期间可有多个GC线程同时工作,利用多核的特性,减少用户线程的stw。
  • 并发性

    • G1拥有于用户线程交替执行的能力,所以整个回收阶段发生不会完全阻塞程序的执行。
  • 分代收集

    • 将整个堆空间分为若干个区域,每个区域都代表堆空间的原始分区的一部分,而且他的回收不像其他回收器一样,他是兼顾新生区和老年区的,
  • 空间整合

    • 区域之间采用的是复制算法,而整体是标记压缩算法。在进行垃圾回收之后,不会有碎片化的空间,避免了后续空间分配带来的许多问题。
  • 可预测的停顿时间模型

    • G1除了追求低停顿之外,还建立可预测的停顿时间模型,让使用者在M的时间片段内,让垃圾回收的时间不超过n。

回收环节:

  • 年轻代GC

    • 当Eden内存空间不足的时候,就会进行YGC,这个时候他是一个并行的独占式的收集器,所有的用户线程都会停下来,将Eden中的存活对象放到s区,大对象直接放到老年区。
  • 老年代并发标记

    • 和cms 的清理过程类似
  • 混合回收

    • 会同时回收新生区和老年区的垃圾。