声明:本片文章是由Hackernews上的[Erlang Garbage Collection Details and Why It
Matters][1]编译而来,本着学习和研究的态度,进行的编译,转载请注明出处。html
Erlang须要解决的重要问题之一就是为实现极高响应能力的软实时系统建立平台。这样的系统须要一个快速的垃圾回收机制,而这个机制不会阻止系统及时的响应。另外一方面,当咱们把Erlang看做一种用无损更新属性的不可改变语言时,这个垃圾回收机制就显得更加剧要了,由于这种语言有很高的概率产生垃圾。golang
在深刻了解GC以前,有一个很重要的事,就是检查Erlang过程的内存布局的三个重要的点:进程控制模块,栈和堆。它和Unix的内存布局很是的相像。
进程控制模块:进程控制模块会保存一些关于进程的信息好比它在进程表中的标识符(PID)、当前状态(运行、等待)、它的注册名、初始和当前调用,同时PCB也会保存一些指向传入消息的指针,这些传入消息是存储在堆中链接表中的。express
栈:它是一个向下生长的存储区,这个存储区保存输入和输出参数、返回地址、本地变量和用于evaluating expressions的临时空间。编程
堆:它是一个向上生长的存储区,这个存储区保存进程邮箱的物理消息,像列表、元组和Binaries这种的复合项以及比像浮点数这种一个机器字更大的对象。超过64机器字的二进制项不会存储在进程私有堆里。他们被称做Refc Binary (Reference Counted Binary)并被存储在一个大的共享堆里,只要有那个Refc Binary指针的进程均可以访问这个堆。这个储存在进程私有堆中的指针叫做ProcBin。segmentfault
为告终实当前默认Erlang的GC机制,简单的说,它是一个分代复制的垃圾回收,独立运行在每一个Erlang进程私有堆的内部,并且它也是发生在全球共享堆中的引用计数垃圾回收。安全
私有堆的GC是分代的。分代GC把堆分为了新生和老年代两个部分。若是一个对象在GC循环生存下来,那么它在短时间内成为垃圾的概率将会很低,这也是这个划分的依据所在。所以,新生代是为新分配的数据准备的,老年代是为了在数次GC启动后生存下来数据的。这个分代帮助了GC减小在尚未成为垃圾数据上的没必要要的循环。对于Erlang垃圾回收有两个策略:Generational (Minor)和Fullsweep (Major)。分代的GC只收集新生的堆,而fullsweep的堆新老都会收集。如今,让咱们回顾一个新开始Erlang进程私有堆的GC步骤:ide
场景1:函数
Spawn > No GC > Terminate布局
若是一个短暂的进程没有使用超过min_heap_size的堆就结束了,GC是不会发生的。这种状况下全部被进程使用过的内存会被收集。学习
场景2:
Spawn > Fullsweep > Generational > Terminate
若是一个新生产的进程的数据增加超过min_heap_size,那么会使用fullsweep GC,显然这是由于没有GC发生,那么也不会有新生代和老年代之分。在第一次fullsweep GC后,堆就会被分代成这两部分,以后GC策略会转化到分代并保持到进程结束。
场景3:
Spawn > Fullsweep > Generational > Fullsweep > Generational > ... >
Terminate
有几种状况,GC策略在进程过程当中由分代转化回到fullsweep。第一种状况是进过必定次数的分代GC。这个数量能够是特定全局的或者是每一个有fullsweep_after flag的进程。同时在fullsweep GC以前每一个的进程和它的上限的分代GC计数器分别是minor_gcs 和 fullsweep_after特性,并在process_info(PID, garbage_collection)返回值中可见。第二种状况是当分代GC不能收集到足够的内存,最后一种状况是garbage_collect(PID)函数被手动调用。在这些状况后,GC策略会回复到从fullsweep到分代而后保持直到上述情形发生。
场景4:
Spawn > Fullsweep > Generational > Fullsweep > Increase Heap >
Fullsweep > ... > Terminate
在场景3中,若是第二fullsweep GC不能收集到足够内存,堆的大小会增长,GC策略又会转化成fullsweep,就像新生成的进程同样,这四种场景能够不断的出现。
如今的问题是为何在像Erlang这种自动垃圾收集语言这么重要。首先这些知识能够帮助你经过调整GC的发生和策略使你的系统运行更快。其次,这是咱们明白从垃圾回收的角度使Erlang变成软件实时平台的重要缘由的地方。这是由于每一个进程都有它本身的堆和它本身的GC,因此每次GC出如今一个进程中的时候,只是中止正在收集过程当中的Erlang进程,但不会中止其余的进程,而这正是一个软实时系统所须要的。
共享堆的GC是参考计数。每一个在共享堆(Refc)的对象都有与存储的其余对象(ProcBin)相对的参考计数器,这些其余对象(ProcBin)都存储在Erlang进程私有堆内部。若是一个对象参考计数器达到0,这个对象会变得没法访问并将销毁。参考计数器很廉价而且能够帮助系统避免意外长时的暂停并且提升体统的响应速度。可是在设计你的actor模型系统时,不了解一些著名的反模式会致使一些问题,好比内存泄漏。
当Refc第一次分红一个Sub-Binary。为了下降成本,一个sub-binary不是一个原binary分裂部分的新副本,仅仅是那个部分的一个参考。然而这个sub-binary会被看成加入到原binary的的一个新的参考,你知道,当原binary必须挂在它的sub-binary上时,这可能会引发一些问题。
其余已知的问题会发生在当一种生命周期很长的中间件看成控制和传递大型Refc binary消息的请求控制器或消息路由器时。当这个进程接触到每一个Refc消息时,它们的计数器会递增。所以收集这些Refc消息依靠于收集全部ProcBin对象,即便它们在中间件进程中。不幸的是,由于ProcBin仅仅只是个指针,所以它们成本很低并且在中间件进程中须要花很长的时间去触发GC。因此即便已经从除了中间件其余全部进程中收集了Refc消息,它们也须要保留在共享堆里。
共享堆之因此重要是由于它减小了因为在进程之间传递大量binary消息的IO。因为sub-binaries仅仅是其余binary的指针,他们能够快速的建立。可是做为一种经验法则,使用变得更快的捷径会产生成本,这个成本会以一种不会在恶劣条件下困住方式去构建你的系统。同时也有不少应对Refc binary泄露的著名方法,好比Fred Hebert在他的ebook发表的Erlang in Anger。我认为我不能解释的比他更好,因此强烈推荐你去阅读。
即便咱们使用像Erlang这种自我管理内存的语言,了解内存是如何分配和释放也是很必要的。不像Go的内存模型文档建议你“若是你必需要经过阅读剩下的文档去了解你的编程的行为,那么你太聪明了。不要这么聪明”,我相信咱们必需要足够的聪明去让咱们的系统运行得更快更安全,但作到这一点,深刻了解它的原理是必不可少的。
• Academic and Historical Questions about Erlang
• Implementation of FPL & Concurrency
• Efficient Memory Management for Message-Passing Concurrency Paper
• Programming the Parallel World by Erlang Paper
关于Erlang内存泄漏的问题的一些分析能够参见云巴以前的一篇Erlang内存泄漏分析的文章有什么问题欢迎留言交流。