python简史_垃圾收集简史

垃圾回收的好处是无可争辩的-提高可靠性,将内存管理与类接口设计分离,并减少开发人员追寻内存管理错误的时间。 Java程序根本不会发生众所周知的指针悬空和内存泄漏的问题。 (Java程序可能表现出某种形式的内存泄漏,更准确地称为无意对象保留,但这是一个不同的问题。)但是,垃圾回收并非没有代价,其中包括性能影响,暂停,配置复杂性和不确定的终结处理。

理想的垃圾收集实现将是完全不可见的-不会有垃圾收集暂停,不会浪费CPU时间进行垃圾收集,垃圾收集器不会与虚拟内存或缓存进行负面交互,并且堆不会需要大于应用程序的驻留时间 (堆占用)。 当然,没有完美的垃圾收集器,但是在过去十年中,垃圾收集器有了很大的改进。

选项-和选择

1.3 JDK包含三种不同的垃圾回收策略; 1.4.1 JDK包含六个和十多个命令行选项,用于配置和调整垃圾回收。 它们有何不同? 为什么我们需要那么多?

各种垃圾收集实现使用不同的策略来标识和回收无法访问的对象,并且它们与用户程序和调度程序的交互方式也不同。 不同种类的应用程序对垃圾收集的要求不同-实时应用程序将需要短暂且有限的收集暂停,而企业应用程序可能会容忍更长或更难以预测的暂停,以支持更高的吞吐量。

垃圾收集如何工作?

垃圾回收有几种基本策略:引用计数,标记清除,标记紧凑和复制。 此外,某些算法可以递增地执行其工作(不需要立即收集整个堆,从而缩短了收集暂停),而某些算法可以在用户程序运行时运行( 并发收集器)。 其他用户必须在用户程序挂起时立即执行整个收集(所谓的世界停止收集器)。 最后,还有混合收集器,例如1.2和更高版本的JDK使用的分代收集器,它们在堆的不同区域使用不同的收集算法。

在评估垃圾收集算法时,我们可能会考虑以下任何或所有标准:

  • 暂停时间。 收藏家会阻止世界进行收藏吗? 多长时间? 暂停可以及时进行吗?
  • 暂停可预测性。 可以在用户程序方便的时间安排垃圾收集暂停时间,而不是在垃圾收集器的方便时间安排垃圾暂停时间吗?
  • CPU使用率。 垃圾回收花费了总可用CPU时间的百分之多少?
  • 内存占用量。 许多垃圾回收算法需要将堆划分为单独的内存空间,其中某些内存在某些时候可能无法被用户程序访问。 这意味着堆的实际大小可能比用户程序的最大堆驻留时间大几倍。
  • 虚拟内存交互。 在物理内存有限的系统上,完整的垃圾回收可能会将非驻留页面故障转移到内存中,以便在收集过程中对其进行检查。 因为页面错误的代价很高,所以希望垃圾回收器适当地管理引用的位置。
  • 缓存交互。 即使在整个堆都可以放入主内存的系统上(几乎所有Java应用程序都这样),垃圾回收通常会起到将用户程序使用的数据从缓存中清除的作用,从而给用户程序带来性能损失。 。
  • 对程序局部性的影响。 尽管有些人认为垃圾收集器的工作仅仅是回收不可访问的内存,但另一些人认为垃圾收集器也应尝试改善用户程序的引用局部性。 压缩和复制收集器会在收集期间重新放置对象,这有可能改善位置。
  • 编译器和运行时的影响。 某些垃圾回收算法需要编译器或运行时环境的大力配合,例如每执行一次指针分配便更新参考计数。 这既为必须生成这些簿记指令的编译器创建了工作,又为必须执行这些附加指令的运行时环境创建了开销。 这些要求对性能有何影响? 它会干扰编译时优化吗?

无论选择哪种算法,硬件和软件的趋势都使垃圾回收变得更加实用。 1970年代和1980年代的经验研究表明,在大型Lisp程序中,垃圾回收消耗了运行时间的25%至40%。 尽管垃圾回收可能还不是完全看不见的,但是它肯定已经走了很长一段路。

基本算法

所有垃圾回收算法面临的问题都是相同的-标识分配器分配的内存块,但用户程序无法访问这些内存块。 不可达是什么意思? 可以通过以下两种方式之一访问内存块:如果用户程序在根目录中拥有对该块的引用,或者在另一个可访问的块中具有对该块的引用。 在Java程序中,根是对保持在活动堆栈框架上的静态变量或局部变量中的对象的引用。 可达对象集是指对关系下根集的传递闭包。

参考计数

最简单的垃圾回收策略是引用计数。 引用计数很简单,但是需要编译器的大力帮助,并且在mutator (从垃圾回收器的角度来看,是用户程序的术语)上增加了开销。 每个对象都有一个关联的引用计数-该对象的活动引用数。 如果对象的引用计数为零,则它是垃圾(用户程序无法访问)并且可以回收。 每次修改指针引用时(例如通过赋值语句),或者当引用超出范围时,编译器都必须生成代码以更新引用对象的引用计数。 如果对象的引用计数为零,则运行时可以立即回收该块(并减少回收的块引用的任何块的引用计数),或将其放置在队列中以进行延迟收集。

许多ANSI C ++库类(例如string )都使用引用计数来提供垃圾回收的外观。 通过重载赋值运算符并利用C ++作用域提供的确定性终结,C ++程序可以使用string类,就好像它是垃圾回收一样。 引用计数很简单,很适合进行增量收集,并且收集过程倾向于具有良好的引用位置,但是出于多种原因,它很少用于生产垃圾收集器,例如,它无法回收无法访问的循环结构(对象直接或间接相互引用的对象,例如循环链接列表或包含指向父节点的反向指针的树)。

追踪收集者

JDK中没有标准的垃圾收集器使用引用计数。 相反,它们都使用某种形式的跟踪收集器 。 跟踪收集器停止了整个世界(尽管不一定在整个收集期间内)并开始跟踪对象,从根集开始并遵循引用,直到检查了所有可到达的对象。 根可以在程序寄存器中,每个线程的堆栈中的局部(基于堆栈)变量以及静态变量中找到。

扫号收集器

跟踪收集器的最基本形式是标记扫描收集器,它是Lisp发明者John McCarthy于1960年首次提出的,在这种扫描收集器中,世界停止了,收集器从根开始访问每个活动节点,并标记它访问的每个节点。 当没有更多的引用要遵循时,收集完成,然后清除堆(即检查堆中的每个对象),并将所有未标记的对象回收为垃圾并返回到空闲列表。 图1说明了垃圾回收之前的堆。 阴影块是垃圾,因为用户程序无法访问它们:

图1.可到达和不可到达的对象
可到达和不可到达的物体

Mark-sweep易于实现,可以轻松回收循环结构,并且不会像引用计数那样给编译器或mutator带来任何负担。 但是它有缺陷-收集暂停可能会很长,并且在清除阶段会访问整个堆,这会对可能分页堆的虚拟内存系统产生非常负面的性能影响。

标记清除的最大问题是在清除阶段访问了每个活动(即已分配)的对象(无论是否可达)。 因为很大一部分对象可能是垃圾,所以这意味着收集器正在花费大量精力检查和处理垃圾。 标记清除收集器还倾向于使堆碎片化,这可能导致局部性问题,并且即使有足够的可用内存,也可能导致分配失败。

复制收集器

在复制收集器 (另一种形式的跟踪收集器)中,堆被分为两个大小相等的半空间,其中一个包含活动数据,另一个未使用。 当活动空间填满时,世界将停止,并将活动对象从活动空间复制到非活动空间。 然后翻转空间的角色,将旧的非活动空间变为新的活动空间。

复制收集的优点是仅访问活动对象,这意味着将不检查垃圾对象,也无需将其分页到内存或带入缓存。 复制收集器中收集周期的持续时间取决于活动对象的数量。 但是,复制收集器要增加将数据从一个空间复制到另一个空间,调整所有引用以指向新副本的成本。 特别是,寿命长的对象将在每个集合上来回复制。

堆压实

复制收集器还有另一个好处,那就是将活动对象集压缩到堆的底部。 这不仅改善了用户程序引用的局部性并消除了堆碎片,而且还大大降低了对象分配的成本-对象分配成为堆顶部指针上的简单指针添加。 无需维护空闲列表或后备列表,也无需执行最佳拟合或首次拟合算法-分配N个字节就像将N添加到堆顶部指针并返回其先前值一样简单,例如在清单1中建议:

清单1.复制收集器中的廉价内存分配
void *malloc(int n) { 
    if (heapTop - heapStart < n)
        doGarbageCollection();

    void *wasStart = heapStart;
    heapStart += n;
    return wasStart;
}

对于非垃圾收集语言实现了复杂内存管理方案的开发人员,可能会对复制收集器中便宜的分配(简单的指针添加)如此便宜而感到惊讶。 这可能是普遍认为对象分配昂贵的原因之一-早期的JVM实现未使用复制收集器,并且开发人员仍隐含地假设分配成本与其他语言(例如C)相似,而实际上可能是Java运行时便宜很多。 分配的成本不仅较小,而且对于在下一个收集周期之前变为垃圾的对象,释放成本为零,因为垃圾对象既不会被访问也不会被复制。

紧凑型收藏家

复制算法具有出色的性能特征,但是它的缺点是需要的存储量是标记清除收集器的两倍。 标记紧凑算法以避免出现此问题的方式将标记清除和复制结合在一起,但代价是增加了收集的复杂性。 像标记扫描一样,标记紧凑是一个分为两个阶段的过程,其中每个活动对象都在标记阶段被访问和标记。 然后,复制标记的对象,以便将所有活动对象压缩到堆的底部。 如果在每个集合上执行完全压缩,那么生成的堆将类似于复制收集器的结果-堆的活动部分与可用区域之间有明确的界限,因此分配成本与复制相当集电极。 寿命长的对象往往会堆积在堆的底部,因此不会像在复制收集器中那样反复复制它们。

好吧,哪一个?

好的,那么JDK采取哪种方法进行垃圾回收? 从某种意义上说,它们都是。 早期的JDK使用单线程mark-sweep或mark-sweep-compact收集器。 JDK 1.2及更高版本采用了一种称为世代收集的混合方法,该堆根据对象的年龄将堆划分为几个部分,并使用不同的收集算法分别收集不同的世代。

尽管分代垃圾收集在运行时引入了一些额外的簿记要求,但事实证明它非常有效。 在下个月的Java理论和实践中 ,除了1.4.1 JVM提供的所有其他垃圾收集选项之外,我们还将探索分代垃圾收集的工作方式以及1.4.1 JVM如何使用它。 在接下来的文章中,我们将研究垃圾收集对性能的影响,包括揭穿与内存管理有关的一些性能神话。


翻译自: https://www.ibm.com/developerworks/java/library/j-jtp10283/index.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值