可视化垃圾回收算法

链接:blog.jobbole.com/77280/

github.com/kenfox/gc-viz

大部分开发者都认为自动垃圾回收器是理所当然的。实际上,这只是语言运行时提供的一项实用功能,旨在简化我们的开发工作。

但是如果尝试着了解垃圾回收器的内部原理,你会发现很难弄明白。除非熟悉它的工作流程和错误处理方式,否则内部成千上万的实现细节会让你不知所措。

我编译了一个有五种不同的垃圾回收算法工具。程序运行时会创建一个动画界面。你可以从github.com/kenfox/gc-viz上获取动画和代码来实现。非常让我惊讶的是,这个简单的动画显现出这些重要的算法。

任务完成后清理: aka No GC

清理垃圾最简单可行的方法就是等一项任务完成之后,一次性处理所有的垃圾。这项技术非常有用,特别是如果能将一项任务分解成许多小任务。例如,Apache网络服务器在每次请求时创建一个小内存池并在请求完成后将创建的整个内存池完全释放。

上图的动画显示了一个正在运行的程序。整张图片代表程序的内存区。内存区在开始时是黑色,黑色表明内存尚未被使用。闪着鲜绿色和黄色的区域表明该内存区域正在读写。颜色随着时间变化,你可以观察内存的使用情况,也可以看到当前的活动情况。如果仔细观察,你会发现内存区域中开始出现一些程序执行过程中会忽略的区域。这些区域就成了所谓的垃圾——程序不能访问和使用。垃圾区域之外的的内存区域是可用的。

该程序的内存充足,所以不必担心程序运行时垃圾的清理。在后面的例子中我将一直使用这个简单的程序。

引用计数回收器

 

另一个简单的解决方案是对你使用的资源(此处指内存中的对象)进行计数,当计数值变为0时,对其进行处理。这是一项广泛使用的技术,当开发者将垃圾回收添加到现有系统中时——这是唯一一个容易与其他资源管理器和现有代码库集成的垃圾回收器。苹果在为Objective-C发布了标志-擦除垃圾回收器后明白这个事实。发布产品出现很多问题以致于他们不得不废弃该项特性,取而代之的是性能良好的自动引用计数回收器。

上面的动画显示了相同的程序,但是此时它将通过对内存中每一对象引用计数来处理垃圾。红色闪烁表示引用计数行为。引用计数的优势在于垃圾会被很快检测到——你可以看到红色闪烁过后紧接着该区域变黑。

遗憾的是引用计数存在诸多问题。最糟糕的是,它不能处理循环结构。而循环结构非常常见——继承或反向引用都将建立一个循环,该结构将造成内存泄露。引用计数的开销也很大 ——从动画中可以看到即使当内存使用不在增长时,红色闪烁一直持续。CPU运算速度很快,但内存读写很慢,而计数器不断被加载并保存至内存。所有这些计数器的更新很难保证数据的只读或线程安全。

引用计数是一种分摊算法(开销遍布整个程序运行时),但这是种分摊算法具有偶然性,不能保证反应时间。例如,程序中存在一个很大的树型结构。最后一段使用树的程序将触发对整个树的处理,墨菲说过事情如果有变坏的可能,不管这种可能性有多小,它总会发生。这里没有其他的分摊算法,所以分摊的偶然特征可能取决于数据。(所有这些算法有并发或部分并发的命令,但这些都是超出了程序可演示的范围。)

标记-擦除回收器

标记-擦除消除了引用计数存在的一些问题。它能够轻松解决循环结构在引用技术中存在的问题,由于不需维持计数,系统开销比较低。

该算法舍弃垃圾检测的实时性。动画中,有一段运行时间没有任何红色的闪烁,然后突然出现许多红色闪烁表明当前正在标记活动对象。在标记完成后,程序要遍历整个内存空间并处理垃圾。在动画中你还将注意到—— 许多区域立刻变黑而不像引用计数方式那样随着时间慢慢变黑。

标记-擦除比引用计数要求更高的一致性实现,而且很难移植到现有系统中。在标记阶段需要遍历所有活动数据,甚至是封装在对像中的数据。如果一个对象不支持遍历,那么尝试将标记-擦除移植到代码中风险太大。标记-擦除的另一个不足之处在于擦除阶段必须遍历整个内存来查找垃圾。对于一个产生垃圾较少的系统,这不是问题,但现在的函数式编程风格产生了大量的垃圾。

标记-压缩回收器

 

在前面的动画中你可能注意到一点,对象从不移动。一旦对象在内存中分配,该对象的存储位置就不会再改变,即使被散步在黑色区域的内存碎片包围。下面两种算法用完全不同的方式改变了这种现象。

标记-压缩算法不是仅通过标记内存区域是否空闲来处理内存,而是通过将对象移动到空闲表来实现。对象通常按照内存顺序存储,先分配的对象在内存的低地址空间——但是处理对象造成的空缺将随着对象的移动变大。

移动对象意味着新对象只能在已使用内存的末尾创建。这就是所谓的“bunp”分配器,和栈分配器一样,但不限制栈空间。有些使用bump分配器的系统甚至不用调用栈存储数据,他们只在堆中分配调用帧,像其他对象一样对待。

有时理论高于实践,另一个优势是当对象被压缩后,程序能够像访问硬件高速缓存一样访问内存。不确定你能否看到这个好处——尽管引用计数和标记-擦除使用的内存分配器很复杂,但调试效果很好,效率也很高。

标记-压缩是算法很复杂,需要多次遍历所有分配对象。在动画中可以看到紧随红色闪烁的活动对象其后的是大量读和写标记为目的地计算,对象被移动,最终引用固定指向移动后的对象。这个复杂程序背后最大的优点是内存开销非常小。Oracle的Hotspot JVM使用了多种不同垃圾回收算法。而全局对象空间使用标记-压缩回收算法。

拷贝回收器

最后使用动画显示的算法是大多数高性能垃圾收集系统的基础。它和标记-压缩是一样的移动回收器,但是相比之下实现却非常简单。它使用两块内存空间,在两个内存间交替复制活动对象。实际上,空间不止两块,这些空间用于不同代对象,新的对象在一个空间中创建,如果生命周期没有结束就会被复制到另一个空间,如果长期存在就会被复制到一个永久性空间。如果你听说一个垃圾收集器是分代的或短暂的,通常是多空间拷贝回收器。

除了简单性和灵活性,该算法的主要优势在于只要在活动对象上花时间。没有独立的标记阶段必须被擦除或压缩。在遍历活动对象期间,对象会被立即复制,弥补了以往对象在引用计数时的不足。

在动画中,你可以看到回收过程中乎所有的数据从一个空间复制到另一个空间。对该算法来说是个糟糕的情况,这是人们谈论优化垃圾收集器的一个原因。如果你能调整内存并有优化分配,使得在回收开始前大部分对象都废弃了,那么你就能兼顾安全函数式编程风格和高性能。

(注:限于译者水平有限,不足之处恳请指正。)

时间: 2024-08-06 18:01:43

可视化垃圾回收算法的相关文章

JVM优化 垃圾回收 算法 垃圾收集器 GC日志可视化查看

今日内容了解什么是垃圾回收掌握垃圾会回收的常见算法学习串行.并行.并发.G1垃圾收集器学习GC日志的可视化查看 1.什么是垃圾回收?程序的运行必然需要申请内存资源,无效的对象资源如果不及时处理就会一直占有内存 资源,最终将导致内存溢出,所以对内存资源的管理是非常重要了. 1.1.C/C++语言的垃圾回收在C/C++语言中,没有自动垃圾回收机制,是通过new关键字申请内存资源,通过delete 关键字释放内存资源.如果,程序员在某些位置没有写delete进行释放,那么申请的对象将一直占用内存资源,

java----java垃圾回收算法

1.引用计数法(Reference Counting Collector) 1.1算法分析 引用计数是垃圾收集器中的早期策略.在这种方法中,堆中每个对象实例都有一个引用计数.当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1.当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1.任何引用计数器为0的对象实例可以被当作垃圾收集.当一个对象实例被垃圾收

垃圾回收算法(5)分代回收

分代垃圾回收,基于的是“大部分的对象,在生成后马上就会变成垃圾”这一经验上的事实为设计出发点.此前讨论过基于引事实的另一个垃圾回收算法,引用计数出的一些优化思路. 分代的关键是: 给对象记录下一个age,随着每一次垃圾回收,这个age会增加: 给不同age的对象分配不同的堆内内存空间,称为某一代: 对某一代的空间,有适合其的垃圾回收算法: 对每代进行不同垃圾回收,一般会需要一个额外的信息:即每代中对象被其他代中对象引用的信息.这个引用信息对于当前代来说,扮演与"root"一样的角色,也

常用垃圾回收算法

今天我关于常见的垃圾回收算法来做个总结,我们最常听到的是Java虚拟机里的垃圾回收机制,其实垃圾回收的概念最先并不是Java里首先提出来的, 垃圾回收这个概念很早就已经被提出来了,并且已经在其他语言中得到了应用. 关于垃圾回收的机制,这里不再解释,这篇文章我主要介绍常见的垃圾回收算法,当然还有其他的. 算法一:引用计数法. 这个方法是最经典点的一种方法.具体是对于对象设置一个引用计数器,每增加一个变量对它的引用,引用计数器就会加1,没减少一个变量的引用, 引用计数器就会减1,只有当对象的引用计数

常用的垃圾回收算法

引用计数法 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1. 只要A对象的引用计数器的值为0,则对象A就不可能再被使用. 实现也很简单,只需要为每个对象配备一个整型的计数器即可. 缺点: 1.无法处理循环引用的情况 2.引用计算器要求在每次引用产生和消除的时候,需要伴随一个加法操作和减法操作,对系统性能有一定影响. A引用了B,B又引用了A,因此A和B的引用计数器都不为0,但是系统中却 不存在任何第3个对象引用了A和B. 这种情况下,A和B是无

JAVA虚拟机垃圾回收算法原理

除了释放不再被引用的对象外,垃圾收集器还要处理堆碎块.新的对象分配了空间,不再被引用的对象被释放,所以堆内存的空闲位置介于活动的对象之间.请求分配新对象时可能不得不增大堆空间的大小,虽然可以使用的总空闲空间是足够的.这是因为,堆中没有连续的空闲空间放得下新的对象. 垃圾收集器算法 任何垃圾回收算法都必须做两件事,首先,它必须检测出垃圾对象.其次,它必须回收垃圾对象所使用的堆空间并还给程序.从根对象开始,任何可以被触及的对象都被认为是“活动的”对象(如果正在运行的程序可以访问到根对象和某个对象之间

JVM垃圾回收算法 总结及汇总

先看一眼JVM虚拟机运行时的内存模型: 1.方法区 Perm(永久代.非堆) 2.虚拟机栈 3.本地方法栈 (Native方法) 4.堆 5.程序计数器 1 首先的问题是:jvm如何知道那些对象需要回收 ? 目前两种标识算法.三种回收算法.两种清除算法.三种收集器 引用计数法 每个对象上都有一个引用计数,对象每被引用一次,引用计数器就+1,对象引用被释放,引用计数器-1,直到对象的引用计数为0,对象就标识可以回收 这个可以用数据算法中的图形表示,对象A-对象B-对象C 都有引用,所以不会被回收,

JVM垃圾回收算法(最全)

JVM垃圾回收算法(最全) 下面是JVM虚拟机运行时的内存模型: 1.方法区 Perm(永久代.非堆) 2.虚拟机栈 3.本地方法栈 (Native方法) 4.堆 5.程序计数器 1 首先的问题是:jvm如何知道那些对象需要回收 ? 目前两种标识算法.三种回收算法.两种清除算法.三种收集器 引用计数法 每个对象上都有一个引用计数,对象每被引用一次,引用计数器就+1,对象引用被释放,引用计数器-1,直到对象的引用计数为0,对象就标识可以回收 这个可以用数据算法中的图形表示,对象A-对象B-对象C

JVM常见垃圾回收算法

jdk1.7.0_79 众所周知,Java是一门不用程序员手动管理内存的语言,全靠JVM自动管理内存,既然是自动管理,那必然有一个垃圾内存的回收机制或者回收算法.本文将介绍几种常见的垃圾回收(下文简称GC)算法. 在Java堆上分配一个内存给实例对象时,此时在虚拟机栈上引用型变量就会存放这个实例对象的起始地址. Object obj = new Object();  现在如果我们将变量赋值为null. obj = null; 此时可以看到Java堆上的实例对象无法再次引用它,那么它就是被GC的对