在上一篇文章中,介绍了在GC机制中,GC是以什么标准判定对象可以被标记的,以及最有效最常用的可达性分析法。
今天介绍另外一种非常常用的标记算法,它的应用面也相当广泛。这就是:
引用计数法 Reference Counting
  这个算法的本质,其实就是上篇文章中判断一个对象要被回收的另外一种思路,即如果没有其它对象调用当前对象,那么当前对象就可以被回收了。判断有多少调用当前对象有两种方法,一种是看看其它对象,有多少对象持有当前对象的引用。还有一种办法就是,当前对象自身实现一个计数机制。统计来自外界引用的调用。第一个办法就是上篇文章中可达性分析的最初思路。而第二个办法就是现在要介绍的引用计数法的最初思路:我们不关心谁保存了我们的引用,我们只关心保存我们引用的对象究竟有多少个。
在引用计数法中,每个对象拥有一个记录自身引用被持有的个数,当这个对象的计数器的值为0时,也就是不再有其它对象持有该对象的引用了,那么也就是不再有对象可以调用到当前对象的方法或者变量了。这一刻也就是当前对象可以被回收的时刻了。
  在《垃圾回收的算法与实现》这本书中,对于该算法又一个很有意思的描述;
  每个对象就像是一个明星。这个对象的引用计数的大小,就像是这个明星的人气指数。当明星的人气指数为0时,也就是这个明星黯然离场的时候了。
在引用计数法中,每个对象的引用计数器初始(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )值为0。没当有一个新对象持有当前对象的引用时,计数器就会加1。没当有一个已经持有引用的对象消失,或者抛弃持有的引用时。计数器就会-1。当计数器的值再次为0,这个计数器所代表的对象就会被回收掉。(更准确的说,是会让空闲链表持有自己的引用,将自己所占用的内存空间标记为可以重新分配的区域)。
接下来说说这个算法的优缺点:
优点:
1、随时随地的回收垃圾
  当计数器变为0的瞬间,当前对象就会被放置到空闲队列中,作为可以被重新分配的内存空间。而其他的GC算法,如之前讲到的可达性分析法,都需要进行一次全局的清理,才会统一的清理掉这个周期内的所有已知的垃圾空间。
2、最大暂停时间短
  引用计数法师在每次生成、或销毁对象或者是变更指针的时候进行一次计算的,因此对程序的影响时间是非常短暂的。
而其他的GC算法则由于需要统一的清除或者是复制等,所以暂停的时间会比较长,对程序的影响也比较长。有时这个时间长到性能上已经无法忍受时,就需要不断的调优,减短单次暂停的最大时长。
3、核心思路简单
  引用计数法。不需要从根节点依次开始进行遍历。每个对象只关心直接持有自己引用的对象是否发生了变化。这样当对象发生回收时,也只影响这个对象直接持有的引用对象,而不会直接影响到更深路径的对象。
  如A持有B/C,B持有D/E。当A被回收时,只会影响B,C对象的引用计数器。当B的计数器值因此降为0时,才会影响到D/E节点。整体的计算成本非常的低。而其他的GC方式需要从根依次进行遍历。整个过程非常复杂(如涉及到一个网状的引用关系,如何终止掉无意义的遍历就尤为重要)。
缺点:
1、计算的频率过快
  每次执行一条命令时,都可能会引起若干次的引用计数变化。尤其是对于一些根节点持有的对象(从根对象)的引用计数,其变化的速度更是惊人。因此计数器的工作量非常繁重。
2、计数器所占用的内存空间非常的大
  引用计数法中。每个对象都需要一个属于自己的引用计数器,尽管这个计数器使用无符号型来存储,但是也只能节约一个bit位的空间。由于可能会发生所有对象都持有一个对象的极端条件,所以计数器所允许的最大值一定是要非常大。(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )相应的所占用的控件也会非常的大。这是一种是典型的空间换取时间的算法。
3、实现非常复杂,一旦出错后果会非常的严重
  尽管该算法的优点是思路非常简单,但是实现起来却要复杂的多。每当一个对象回收时,都需要刷新每一处使用到的对象的计数器。一旦有一处错误,则可能会出现永远无法修复的内存泄露问题。
4、循环依赖
  这个问题可以是引用计数法被公认的最难处理问题:当两个(也可以使是多个)内存对象互相依赖,同时也不与外界有引用关系,从而形成一种类似孤岛链的关系。此时每一个对象计数器都不为0,GC也就无法回收掉这些内存了。这种情况下,典型的引用计数法是无法解决掉的。往往需要结合其他的回收算法,进行改良才能解决问题。


针对这些缺点。业界提供了很多的改良算法
1、延迟引用计数法 Deffered Reference Counting
deferred [dɪ'fɜ:d] adj. 延期的,缓召的; 
  针对从根对象的引用非常频繁的更新,从而导致其计数器的计算任务非常繁重的这个问题。有人提出了一种特别的思路:不维护从根引用对象的计数器。这些计数器的值始终为0。其他对象仍然正常采用引用计数器的方式。但是这就会有一个问题,GC无法判断哪些对象是可以回收的,哪些是不能回收。因此就需要把计数器降为0(decr_ref_cnt函数)的对象暂时先放置在一个容器中,延迟它的回收。这个容器称为ZCT(Zero Count Table)。它专门用来记录那些计数器经过减持计算而变为0的对象。

如下图:

那么什么时候开始真正的标记垃圾对象呢?
一般来说当我们创建(new_obj函数)对象时,发现已经没有空余的内存空间可以分配时。就会进行一次ZCT扫描(scan_zct函数)来清理掉这些对象。然后再次尝试分配内存,如果仍然不能成功,那么这时候就认为内存溢出了。
ZCT扫描(scan_zct函数)的步骤如下:
(1)首先将从根引用的计数器调整到正常的数值;
可见下图:

(2)然后遍历ZCT,将值为0的对象都清理掉(delete(obj)函数),放置到前文说的空闲队列中。(此时这些对象的空间就可以被用来分配新的对象了)
(3)然后将所有的从根引用的计数器再调整回去。

  这个算法的好处是,废弃掉从根引用对象的计数器被频繁刷新这些无意义的繁重耗时的操作,大大减轻了处理器的负担。
当然它的缺点也很明显,内存不再会被立即回收掉。只有当内存空间不够时(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ ),才开始扫描ZCT,统一进行回收。而扫描ZCT的耗时一般会随着ZCT的增大而增大,这样就导致了GC的最大的暂停时间变大。当然也可以通过调小ZCT来减小最大暂停时间,但是这样又会让GC更频繁的进行ZCT扫描(空间与时间不可兼得)。从而导致内存回收处理的吞吐量下降。2、Sticky引用计数法
sticky 英 [ˈstɪki] adj. 粘性的; 不动的; 
  前文有提到计数器的控件占用非常的大。这是为了保证极端场景计数下,计数器可以正常使用。但这种算法却恰恰是将计数器所占用的空间(计数上限)缩小。这是因为对于大部分对象来说,计数器中所能达到的最大值都不大,对象很快就会被回收了。为了保证计数器每一个对象的计数器都不会溢出,而给每一个对象都开辟一块非常大的空间来计数,这是一种非常愚蠢的行为。
对于个别计数器会溢出的对象来说:
(1)那么就让它溢出好了
反正它都被这么多对象引用了,概率上讲,基本也不太会被回收了,可以说默认它为永生对象了。
(2)如果认为计数器溢出不好,可以加入从根寻址的变相算法,大致思路是这个样子的:
  <1>计数器归0
  <2>从根依次寻址,增加引用计数值
  <3>清理掉引用计数器为0的对象
这个算法的好处是:
<1>降低计数器占用的空间
<2>清理掉循环引用的场景
3、1位引用计数算法
  这个算法可以说是Sticky引用计数法的一种极端体现,也就是计数器只有1位。一旦出现共有一个内存的场景下就“溢出”了。
尽管场景很极端,但是他代表了很多的内存:这些对象从创造出来之后,只被一个对象持有,不存在多个对象共同持有的情况。
这种算法中,计数器更多像是一个标记值,标记当前对象是被其他对象引用,而不再是一个对象的计数器。
4、部分标记清除算法
  这个算法可以说是纯粹就是为了解决循环依赖的。算法的大致思路如下:将内存对象标记为四种状态:
  A绝对不是垃圾的对象;
  B绝对是垃圾的对象;
  C可能存在循环引用的对象;
  D搜索完毕的对象。
这样就可以针对对象的状态采用不同的回收算法计算。由于内存中的大部分对象都处于循环引用的孤岛连之外(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )。因此大部分的对象仍然采用的是引用计数法进行计算。只有少部分的对象可能存在循环引用中,因此只对这部分对象进行进行根可达性的计算。
尽管引用计数法自身存在诸多的缺陷,但是仍然有很多地方采用了这种回收算法:如Python的GC、Flash player的内存管理等。可惜由于循环依赖等原因,到目前为止,主流的JVM还均没有将引用计数法作为GC的回收算法来使用。


  对于这篇文章中提到的这些GC标记算法,以及这些GC标记算法的实现,在这里推荐一本前文提到的书:《垃圾回收的算法与实现》。这本书写的非常详细,书中的插图也非常形象。有兴趣的同学可以找来阅读下。

JVM GC-----3、垃圾标记算法(二)的更多相关文章

  1. JVM GC-----2、垃圾标记算法(一)

    在上一篇文章中,我介绍了关于GC机制中,GC在确认垃圾对象后,是如何回收这些垃圾对象的几种算法.现在介绍下GC机制一般是如何定位(或者叫做标记)出这些垃圾对象的.我们先来问下自己,如何判介绍了断一个对 ...

  2. 【转载】Java性能优化之JVM GC(垃圾回收机制)

    文章来源:https://zhuanlan.zhihu.com/p/25539690 Java的性能优化,整理出一篇文章,供以后温故知新. JVM GC(垃圾回收机制) 在学习Java GC 之前,我 ...

  3. Java虚拟机(三)垃圾标记算法与Java对象的生命周期

    前言 这一节我们来简单的介绍垃圾收集器,并学习垃圾标记的算法:引用计数算法和根搜索算法,为了更好的理解根搜索算法,会在文章的最后介绍Java对象在虚拟机中的生命周期. 1.垃圾收集器概述 垃圾收集器( ...

  4. Java性能优化之JVM GC(垃圾回收机制)

    Java的性能优化,整理出一篇文章,供以后温故知新. JVM GC(垃圾回收机制) 在学习Java GC 之前,我们需要记住一个单词:stop-the-world .它会在任何一种GC算法中发生.st ...

  5. JVM——GC(垃圾回收)算法

    一.垃圾回收的基本概念 垃圾回收(GC,Garbage Collection),指内存中不会再被使用的对象清理掉. 垃圾回收有很多种算法:如引用计数法.标记压缩法.复制算法.分代/分区的思想 二.垃圾 ...

  6. JVM中的垃圾回收算法GC

    GC是分代收集算法:因为Young区,需要回收垃圾对象的次数操作频繁:Old区次数上较少收集:基本不动Perm区.每个区特点不一样,所以就没有通用的最好算法,只有合适的算法. GC的4大算法 1.引用 ...

  7. 小师妹学JVM之:GC的垃圾回收算法

    目录 简介 对象的生命周期 垃圾回收算法 Mark and sweep Concurrent mark sweep (CMS) Serial garbage collection Parallel g ...

  8. JVM学习--(四)垃圾回收算法

    我们都知道java语言与C语言最大的区别就是内存自动回收,那么JVM是怎么控制内存回收的,这篇文章将介绍JVM垃圾回收的几种算法,从而了解内存回收的基本原理. stop the world 在介绍垃圾 ...

  9. @JVM新一代的垃圾回收算法

    垃圾回收的瓶颈 传统分代垃圾回收方式,已经在一定程度上把垃圾回收给应用带来的负担降到了最小,把应用的吞吐量推到了一个极限.但是他无法解决的一个问题,就是Full GC所带来的应用暂停.在一些对实时性要 ...

随机推荐

  1. Postfix 邮件服务 - PostfixAdmin

    PostfixAdmin 基于web的postfix邮件发送服务器的管理工具,可以直接管理postfix的虚拟域名和邮件用户,前提是这些数据是存储在mysql或者是PostgreSQL数据库中. Po ...

  2. uboot 如何向内核传递参数

    a.uboot 向内核传递的参数有两种类型 1.一个是bootargs 2.一个是环境参数, 而环境参数的设置靠的是 Y:\junda\JdLinuxApp\A1801_uboot\source\u- ...

  3. 关于cc -o命令

    这个命令很灵活,格式是: cc -o 目标二进制可执行文件 文件1 文件2 文件3 ..... 其中目标文件后面的文件,可为源代码,也可为二进制文件,也可为库文件 比如: //a.c #include ...

  4. prim算法,克鲁斯卡尔算法---最小生成树

    最小生成树的一个作用,就是求最小花费.要在n个城市之间铺设光缆,主要目标是要使这 n 个城市的任意两个之间都可以通信,但铺设光缆的费用很高,且各个城市之间铺设光缆的费用不同,因此另一个目标是要使铺设光 ...

  5. Eclipse文件路径

    经常我们需要读取某个文件,一般情况下,在Eclipse工程中,路径为./src/....

  6. Java并发编程(1)-Java内存模型

    本文主要是学习Java内存模型的笔记以及加上自己的一些案例分享,如有错误之处请指出. 一 Java内存模型的基础 1.并发编程模型的两个问题 在并发编程中,需要了解并会处理这两个关键问题: 1.1.线 ...

  7. VMware12虚拟机中Ubuntu16.04安装CPU版本Caffe

    首先,可以自行下载VMware12进行安装,基本上都是直接点击‘下一步’直到安装完成,这里重点讲一下Ubuntu16及Caffe的安装步骤 第一步: 下载Ubuntu16.04版本的文件,这里给出链接 ...

  8. Informatic学习总结_day02

    1.sort组件(排序) 2.aggregater组件  聚集组件  sum   如果不排序,去做汇总的话,一个大的session 运行起来会很慢 3.join控件  连接组件 对于异构的数据源,一个 ...

  9. python 历险记(三)— python 的常用文件操作

    目录 前言 文件 什么是文件? 如何在 python 中打开文件? python 文件对象有哪些属性? 如何读文件? read() readline() 如何写文件? 如何操作文件和目录? 强大的 o ...

  10. 【bzoj1901】dynamic ranking(带修改主席树)

    传送门(权限) 传送门(非权限) 花了一晚上总算把代码调好了……才知道待修改主席树怎么操作…… 然而还是一知半解orz…… 先说说我的理解吧 我们一般建主席树的时候都是直接在序列上建的 但是如果有修改 ...