几种垃圾回收GC概述

垃圾回收机制

引用计数回收器(Reference Counting Collector)

  原理是在每个对象内部维护一个整数值,叫做这个对象的引用计数,当对象被引用时引用计数加一,当对象不被引用时引用计数减一。当引用计数为 0 时,自动销毁对象。

目前引用计数法主要用在 c++ 标准库的 std::shared_ptr 、微软的 COM 、Objective-C 和 PHP 中。

  计数器表示资源(内存中的对象)的使用次数,当计数器变成零的时候就可以将该资源销毁。在向已有的系统添加垃圾回收器时,开发人员通常会选择计数回收器,因为这种方式最容易与已有的资源管理器和代码集成在一起

  不过引用计数算法存在很多问题。最大的一个问题是,它无法解决循环引用问题。

循环引用是指对象 A 和对象 B 互相持有对方的引用。这样两个对象的引用计数都不是 0 ,因此永远不能被收集,造成内存泄露。除此之外,引用计数需要额外的开销。

  虽然现代CPU的运算能力很强,但内存并不快,而计数器的运算需要频繁使用内存。因为计数器需要不断被更新,所以它们不是只读的,而且不能保证线程安全。
  引用计数器算法是一种摊销算法(将开销分摊给了程序),不过它是偶然性的摊销,无法保证响应时间。例如,假设程序正在处理一个很大的树结构,最后一个使用这个树的程序会触发销毁操作,根据墨菲定律,其延迟会比期望的要高。

标记清除回收器(Mark Sweep Collector)

  标记清除回收算法解决了一些在引用计数算法中存在的问题。它解决了循环引用问题,而且开销要小得多,因为它不需要维护计数器。
  这个算法分为两步,标记和清除。
    1. 标记:从程序的根节点开始, 递归地 遍历所有对象,将能遍历到的对象打上标记。
    2. 清除:讲所有未标记的的对象当作垃圾销毁。

Animation_of_the_Naive_Mark_and_Sweep_Garbage_Collector_Algorithm.gif-143.9kB
181024382398115

  不过,它无法立即检测到垃圾。这个算法还有一个缺陷,就是人们常常说的 STW 问题(Stop The World)。因为算法在标记时必须暂停整个程序,否则其他线程的代码可能会改变对象状态,从而可能把不应该回收的对象当做垃圾收集掉。
  标记清除算法有更高的一致性要求,而且难以将其集成到已有的系统中。在标记阶段,回收器要求遍历所有的存活对象,包括封装在对象里的数据。如果某个对象不支持回收器的遍历访问,那么使用这种回收器就会存在风险。

复制回收器(Copying Collector)

  节点复制也是基于追踪的算法。其将整个堆等分为两个半区(semi-space),一个包含现有数据,另一个包含已被废弃的数据。节点复制式垃圾收集从切换(flip)两个半区的角色开始,然后收集器在老的半区,也就是 Fromspace 中遍历存活的数据结构,在第一次访问某个单元时把它复制到新半区,也就是 Tospace 中去。在 Fromspace 中所有存活单元都被访问过之后,收集器在 Tospace 中建立一个存活数据结构的副本,用户程序可以重新开始运行了。

优点

  1. 所有存活的数据结构都缩并地排列在 Tospace 的底部,这样就不会存在内存碎片的问题。
  2. 获取新内存可以简单地通过递增自由空间指针来实现。

缺点

  1. 内存得不到充分利用,总有一半的内存空间处于浪费状态。
  2. Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。 181041528488728

标记整理回收器(Mark Compact Collector)

  为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
  标记整理算法清理内存的方式不是通过清除,而是将对象移动到空余的内存空间。对象总是以不变的次序存留在内存里,先分配到内存的对象总处于较低的内存段,不过因为经过移动,对象间的内存间隙被消除。

  新创建的对象总是处于内存的高段。这种内存分配器被称为“bump”,类似于栈的分配,只是没有栈那样的大小限制。有些使用bump分配器的系统甚至不用调用栈来存储数据,它们直接在堆里分配调用帧,并把它们看成对象。

  从理论上看,这种回收算法的另一个优势在于,程序具有了更好的内存访问模型,这种模型对现代硬件的内存缓存更加友好。不过,相比其他算法,我们很难直观地感受到这种优势,因为引用计数和标记清除算法里所使用的内存分配器虽然很复杂,但它们很健壮,很高效。

  标记整理是一种复杂的算法,需要多次遍历所有分配到内存的对象。这种复杂性所带来的主要好处就是极低的内存开销。Oracle的Hotspot虚拟机使用了多种垃圾回收算法。
181100129575916

三色标记法

三色标记算法是对标记阶段的改进,原理如下:
  1. 起初所有对象都是白色。
  2. 从根出发扫描所有可达对象,标记为灰色,放入待处理队列。
  3. 从队列取出灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色。
  4. 重复 3,直到灰色对象队列为空。此时白色对象即为垃圾,进行回收。
Animation_of_tri-color_garbage_collection.gif-94kB

  这个算法可以实现 "on-the-fly",也就是在程序执行的同时进行收集,并不需要暂停整个程序。
但是也会有一个缺陷,可能程序中的垃圾产生的速度会大于垃圾收集的速度,这样会导致程序中的垃圾越来越多无法被收集掉。

分代收集

  分代收集也是传统 Mark-Sweep 的一个改进。这个算法是基于一个经验:绝大多数对象的生命周期都很短。所以按照对象的生命周期长短来进行分代。
  一般 GC 都会分三代,在 java 中称之为新生代(Young Generation)、年老代(Tenured Generation)和永久代(Permanent Generation);在 .NET 中称之为第 0 代、第 1 代和第2代。
  原理如下:
    1. 新对象放入第 0 代
    2. 当内存用量超过一个较小的阈值时,触发 0 代收集
    3. 第 0 代幸存的对象(未被收集)放入第 1 代
    4. 只有当内存用量超过一个较高的阈值时,才会触发 1 代收集
    5. 2 代同理
  因为 0 代中的对象十分少,所以每次收集时遍历都会非常快(比 1 代收集快几个数量级)。只有内存消耗过于大的时候才会触发较慢的 1 代和 2 代收集。

因此,分代收集是目前比较好的垃圾回收方式。使用的语言(平台)有 jvm、.NET 。

  基于追踪的垃圾回收算法(标记-清扫、节点复制)一个主要问题是在生命周期较长的对象上浪费时间(长生命周期的对象是不需要频繁扫描的)。同时,内存分配存在这么一个事实 “most object die young”。基于这两点,分代垃圾回收算法将对象按生命周期长短存放到堆上的两个(或者更多)区域,这些区域就是分代(generation)。对于新生代的区域的垃圾回收频率要明显高于老年代区域
  分配对象的时候从新生代里面分配,如果后面发现对象的生命周期较长,则将其移到老年代,这个过程叫做 promote。随着不断 promote,最后新生代的大小在整个堆的占用比例不会特别大。收集的时候集中主要精力在新生代就会相对来说效率更高,STW 时间也会更短。
  目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。
533622b0-268e-3968-acbb-07b26a1dba2a
  而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。
  注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

优点

性能更优。

缺点

实现复杂

http://www.frankyang.cn/2017/08/30/gc/

原文地址:https://www.cnblogs.com/yangjiannr/p/GC.html