免费下载!由 O’Reilly 出版的《NGINX 完全指南》中文版已正式上线 了解详情
写点什么

理解垃圾回收算法

  • 2017-03-27
  • 本文字数:2992 字

    阅读完需:约 10 分钟

来自 Atomic Object 公司的 Ken Fox 为了解释各种垃圾回收算法,开发了一个小工具,用于对各种垃圾回收算法进行可视化演示。这个工具通过动画的形式展示了垃圾回收算法的执行过程,让人非常直观地了解这些复杂算法背后的原理。这篇文章最早由 Ken Fox 于 2014 年 9 月 3 号发表在 Atomic Spin 博客上,以下译文已获得源网站的翻译授权。原文链接“ Visualizing Garbage Collection Algorithms ”。

自动垃圾回收机制对于大部分开发人员来说已经见惯不怪了,它只不过是语言运行时为了简化我们的工作而提供的一种特性。

如果我们不了解垃圾回收器的原理,在选择垃圾回收器时,就很难做出决定。一个垃圾回收器涉及数百个实现细节,如果不清楚它们的作用和陷阱,就很容易陷入迷茫之中。

我开发了一个好玩的小工具,用于可视化 5 种垃圾回收算法。这个工具把运行时的垃圾回收行为创建成动画,相关代码可以在 github 上找到。这些垃圾回收算法可以通过简单的动画来演示,连我自己都感到很惊讶。

在结束时进行清理—无垃圾回收(No GC)

在任务处理结束时一次性清理所有的资源,这是最简单的垃圾清理方式。如果任务可以被拆分成更小的单元,那么使用这种方式来清理垃圾会很有效。举个例子,Apache 的 Web 服务器为每个请求创建了一个小型的内存池,请求被处理完之后,整个内存池会被清理掉。

下面的动画模拟了一个正在运行的程序,画布代表程序的整体内存。黑色表示未被使用的内存,闪烁的绿色表示内存读取,黄色表示内存写入。色块会随着时间衰退,我们不仅可以看到内存是如何被使用的,还能看到当前的活动情况。如果仔细看的话,我们会发现,程序会忽略掉一些内存区域。这些区域变成了垃圾,它们不会再被使用,程序也无法触及到这些区域。

这个程序在内存中运行,不需要担心垃圾的清理问题。在下面的其他例子中,我也将使用这个程序作为示例。

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

另一个简单的方案是使用计数器,计数器表示资源(内存中的对象)的使用次数,当计数器变成零的时候就可以将该资源销毁。在向已有的系统添加垃圾回收器时,开发人员通常会选择计数回收器,因为这种方式最容易与已有的资源管理器和代码集成在一起。Apple 最初在 Object-C 里使用了标记清理回收器,不过这种回收器给他们带来了很多困扰,后来他们不得不使用计数回收器替代它。而且事实证明,自动计数回收器比之前的标记清理回收器更适合在 Object-C 中使用。

下面的动画模拟的是之前的那个程序,不过这次是根据内存对象的引用次数来清理垃圾。闪烁的红色表示正在进行引用计数。引用计数的最大好处在于它可以很快地检测出垃圾,从动画中你会发现,有些红色闪过之后的区域立即变成黑色。

不过引用计数算法存在很多问题。最大的一个问题是,它无法解决循环引用问题。这种情况很常见,父对象和反向引用会形成引用环,造成内存泄露。除此之外,引用计数需要额外的开销。从动画中可以看到,在内存使用没有增长的情况下,红色区域也一直保持闪烁。虽然现代 CPU 的运算能力很强,但内存并不快,而计数器的运算需要频繁使用内存。因为计数器需要不断被更新,所以它们不是只读的,而且不能保证线程安全。

引用计数器算法是一种摊销算法(将开销分摊给了程序),不过它是偶然性的摊销,无法保证响应时间。例如,假设程序正在处理一个很大的树结构,最后一个使用这个树的程序会触发销毁操作,根据墨菲定律,其延迟会比期望的要高。不过,这篇文章所演示的其他算法也都不是完全的摊销算法,所以偶然性的摊销完全取决于所处理的数据(这些算法有些是并发的,有些是部分并发的,不过我开发的这个工具无法演示这些特性)。

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

标记清除回收算法解决了一些在引用计数算法中存在的问题。它解决了循环引用问题,而且开销要小得多,因为它不需要维护计数器。

不过,它无法立即检测到垃圾。从动画中可以看到,在一小段时间内没有出现红色闪烁,之后又突然出现了很多红色闪烁,说明它正在标记存活的对象。在完成标记之后,所有的垃圾被清除,释放出内存。我们还能从动画中看到,有些区域一下子全部转成黑色,而不是像引用计数算法那样慢慢地随时间扩散开来。

标记清除算法有更高的一致性要求,而且难以将其集成到已有的系统中。在标记阶段,回收器要求遍历所有的存活对象,包括封装在对象里的数据。如果某个对象不支持回收器的遍历访问,那么使用这种回收器就会存在风险。标记清除算法的另一个缺点是,在清除阶段,它会在整个内存范围内进行清除。如果系统的垃圾不多,那么问题不大,但是现代的函数式编程模型总是能产生大量的垃圾。

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

你可能会注意到,在之前的动画里,对象不会发生移动。一旦对象分配到了内存,它就会待在原地不动,即使内存出现了很多碎片。后面的两个算法将会改变这种状况,它们使用不同的方式来实现回收。

标记整理算法清理内存的方式不是通过清除,而是将对象移动到空余的内存空间。对象总是以不变的次序存留在内存里,先分配到内存的对象总处于较低的内存段,不过因为经过移动,对象间的内存间隙被消除。

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

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

标记整理是一种复杂的算法,需要多次遍历所有分配到内存的对象。从动画里我们可以看到,在红色闪烁之后,在计算对象的目标地址时发生了很多读写操作,然后对象被移动到目标地址上,对象的引用也被指向新的地址。这种复杂性所带来的主要好处就是极低的内存开销。Oracle 的 Hotspot 虚拟机使用了多种垃圾回收算法,其中老年代空间使用的是标记整理算法。

复制回收器(Copying Collector)

我要演示的最后一种算法是大部分高性能垃圾回收系统的基础。它有点类似标记整理算法,不过相比之下要简单很多。它把内存分为两个部分,然后在这两个内存空间之间移动对象。在实际应用当中,一般会有多个内存空间,每个空间分配给不同年代的对象。先是在其中一个内存空间创建新对象,如果它们存活下来,就把它们复制到另一个内存空间。最后,如果这些对象的寿命足够长,它们会被复制到老年代空间。如果一个垃圾回收器被贴上分代或者朝生夕灭的标签,那它极有可能是多空间的复制回收器。

除了简单性和灵活性,这种算法的最大优势在于,它只处理存活的对象。这种算法并不存在标记阶段,存活的对象直接被复制到新的地址上,对象引用也随之指向新的地址。

从动画中我们可以看到,有些对象集合几乎是整块地被复制到另一个内存空间里,这是比较糟糕的情况,这也就是为什么我们需要对垃圾回收器进行调优。如果我们能够通过调整内存大小和控制内存分配,确保大部分对象在垃圾回收开始之前死亡,那么,我们就得到了函数式编程和高性能的一个完美组合。


感谢郭蕾对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2017-03-27 19:006454
用户头像

发布了 321 篇内容, 共 127.4 次阅读, 收获喜欢 138 次。

关注

评论

发布
暂无评论
发现更多内容

Python 中的数组哪去了?

宇宙之一粟

Python 数组 2月月更

Nginx跨域解决配置示例

nginx 跨域

如何打造一个能自动回复的钉钉机器人

老表

Python 机器人 Linxu 跟老表学云服务器

大厂偏爱的Agent技术究竟是个啥

捉虫大师

架构 agent

[Python]第一章(建议收藏)

謓泽

Python 2月月更

eBPF 完美搭档:连接云原生网络的 Cilium

火山引擎边缘云

边缘计算 ebpf 云原生网络 cllium

工作想法小计(2):2/14 - 2/18

非晓为骁

个人成长

关于MVVM和MVC,面试看这篇就够了

山河已无恙

mvc 全栈 MVVM 2月月更

学生管理系统的架构设计

凌波微步

「架构实战营」

第三个模块作业

achilles

[Python]介绍

謓泽

Python 2月月更

数据库读写分离如何保证主从一致性?

蜜糖的代码注释

MySQL 数据库 2月月更

基于CC2530设计的智能风扇

DS小龙哥

2月月更 智能风扇

云原生时代,如何保证容器镜像安全?

极狐GitLab

DevSecOps 镜像安全 极狐GitLab

盘一盘常见的6种索引失效情况

华为云开发者联盟

MySQL 索引 字符串 查询 索引失效

凡泰极客成为W3C成员并加入MiniApps工作组,将积极参与小程序快应用技术标准化进程

FinClip

小程序

开源| 直播推拉流2.0升级了什么

anyRTC开发者

开源 音视频 屏幕共享 视频直播 美颜滤镜

Pulsar 职位广场 | 腾讯、华为云、虾皮、众安保险、StreamNative 等多个热招岗位

Apache Pulsar

开源 架构 云原生 招聘 Apache Pulsar

面试突击25:sleep和wait有什么区别?

王磊

java面试

OpenHarmony移植案例与原理:如何适配服务启动引导部件bootstrap_lite

华为云开发者联盟

OpenHarmony 移植 bootstrap_lite startup 系统服务

C++异常处理机制

正向成长

c++ 异常处理

上海市宝山区委书记陈杰一行参访旺链科技

旺链科技

区块链 产业区块链 Vone新闻

学生管理系统架构设计文档

阿卷

架构实战营

UMEM:友盟统计自定义事件多应用一键同步 & 批处理工具

SamgeApp

Docker Vue 友盟助手 友盟自定义事件批处理 友盟统计

鲲鹏DevKit & BoostKit直播解密:如何“做开发者的开发者”

科技热闻

好用不卡,这些插件和配置让你的 Webstorm 更牛逼!

前端下午茶

前端 工具 webstorm

用简单例子带你了解联合索引查询原理及生效规则

华为云开发者联盟

sql 索引 查询 联合索引

十年所学,梦想终至,不负时光 | 《云端架构》新书首推发布,来自极度努力的吕校长

博文视点Broadview

CSS实现阮大佬博文的阅读进度功能

战场小包

CSS css3 前端 2月月更

VIPKID基于Karmada的容器PaaS平台落地实践

华为云原生团队

开源 Kubernetes k8s多集群管理 混合云 分布式云

学生管理系统的架构文档

卡西毛豆静爸

「架构实战营」

理解垃圾回收算法_语言 & 开发_Ken Fox_InfoQ精选文章