Erlang垃圾回收机制

来源:互联网 发布:长得像混血儿 知乎 编辑:程序博客网 时间:2024/05/16 23:34
前面的Erlang杂记中我们简单提到过Erlang的垃圾回收机制:1.以进程为单位进行垃圾回收 2.ETS和原子不参与垃圾回收.今天我们继续这一话题,关注更多关于细节.   在Erlang的官方文档中,关于垃圾回收的知识散见于各处,要把这些信息收集在一起还是要费些力气的,完全不像微软文档那样系统化,比如这是关于.net framework垃圾回收的文档:http://msdn.microsoft.com/en-us/library/ee787088.aspx ;好吧,有点耐心,还是可以从Erlang官方文档中发现好多宝的,回头再说,现在开始:

Erlang垃圾回收机制
     "The current default GC is a "stop the world" generational mark-sweep collector. "文档中这样描述Erlang垃圾回收器,点出了其垃圾回收器的特点:1."stop the world" 2.generational  3.mark-sweep . 看到这个定义,问题就来了:既然进行垃圾回收的时候会导致进程挂起("stop the world"),那不会影响性能么?
    先说分代(generational),Erlang使用旧数据栈'old heap'来存储存至少经历了一次垃圾回收的数据.当旧数据栈'old heap'没有足够的空间的时候就会进行一次充分的垃圾回收(fullsweep).创建进程的时候我们可以通过使用spawn_opt/4来设置fullsweep_after参数,这个参数的意思是:最多经过多少代就可以强制进行充分垃圾回收了,不管旧数据堆是否有剩余空间.我们看一下这个参数的默认值:
     erlang:system_info(fullsweep_after).   
     {fullsweep_after,65535}
    65535是一个不小的数值,但是现在内存已经不再是稀缺资源,这个值还是可以接受的.如果你希望尽快回收内存的话,这个参数可以适当调整一下.把这个参数设置成0实际上是关闭了数据逐代回收算法,每一次垃圾回收都会拷贝所有livedata.很少有场景需要调整这个值,一般需要调整它的两个场景是:1.需要快速的抛弃掉不用的二进制数据就把这个值设置为0; 2.进程使用的数据生命周期都很短,短,旧数据堆会堆积很多垃圾数据;这时可以调小fullsweep_after为10或20,尽快触发充分垃圾回收.
   我们启动Erlang节点,一个节点(node)就是一个Erlang runtime的实例,对应操作系统的一个进程.比如在windows里面,打开进程管理器会看到erl.exe.在Eralng节点内部动态创建Erlang进程.
     每一个Erlang进程创建之后都会有自己的PCB,栈,私有堆.Erlang进程结束的时候,内存资源理解被释放便于资源复用.这样做背后的思想是:每一个进程都只有一小部分活跃数据(live data),所以垃圾回收将会是一个很快的操作.换句话说Erlang的垃圾回收是以进程为单位的,虽然GC过程会进程挂起但是由于回收速度快,影响很小.垃圾回收使用的是generational stop-and-copy回收器.从Erlang进程终止到其释放的内存被重用中间是没有延迟的.由于GC回收是以进程为单位,垃圾回收器的一个不便之处就是不能跨进程处理进程堆.同样的,由于进程间数据独立没有数据共享,消息发送实际上就是数据复制来实现的,如果复制的数据量很大也是会影响效率的,所以Erlang提倡的是小消息,大运算.
    上面提到进程创建伊始会分配很小的栈和堆资源,这并不是固定不变的,垃圾回收器会动态调整堆大小.Erlang节点创建进程速度超快,这个大家估计已经看过Joe Armstrong在书中创建进程的实验,这里不再赘述.那么一个Erlang进程创建之初到底会占用多少内存呢?我们可以用下面的例子做一下检查:
    Fun= fun-> receive after infinity -> ok end end.   %创建一个无限等待的Fun
    {_,Bytes}=process_info(spawn(Fun),memory).     %创建一个进程并查看其内存信息
    Bytes div erlang:system_info(wordsize).               %计算下这个进程占用多少字(word) 32系统为4 64位系统为8
  
  我们平常用的ETS(Erlang Term Strorage)是一个全局数据库,可以被节点内的所有进程共享访问.ETS也是由进程实现,所以存储和查询数据和消息发送一样都是通过复制实现.Erlang二进制数据通常数据量相当大.二进制数据是在进程以外的独立的堆分配.二进制数据占用一块数据区域,数据区域头信息包含指向数据区域的指针.当二进制数据分割成子二进制数据段的时候,会创建新的数据头信息但数据并没有被拷贝.二进制内存分配对节点内所有的Erlang进程可见.发送消息的时候,二进制数据发送的是引用.尽管垃圾回收器是基于拷贝的,二进制数据是走的标记-清除(mark-sweep)的路子.我们知道,标记-清除已经是面向全局的垃圾回收机制了.

查看垃圾回收状态
   说到这里,我们就和上次的内容续上了,[Erlang 0013]抓取Erlang进程运行时信息 提到了如何抓取Erlang进程的运行时信息,这些信息其中也包括了GC的信息:
{garbage_collection, GCInfo}
GCInfo is a list which contains miscellaneous information about garbage collection for this process. The content of GCInfo may be changed without prior notice.
下面是一段采样数据:
  {reductions,41087},
   {garbage_collection,[{min_bin_vheap_size,10946},
                        {min_heap_size,10946},
                        {fullsweep_after,65535},
                        {minor_gcs,18}]},

{suspending,[]}]

控制垃圾回收
我们可以主动控制垃圾回收,使用的方法是erlang:garbage_collect(PID);除此之外我们可以通过调整进程初始堆大小来实现min_heap_size,就是说进程的堆大小不再走自增长的过程,一开始就分配给它足够的大小.调整这个配置有两种方法:
    1.erl +h选项可以调整全局的min_heap_size
    2.针对某个进程可以在创建的时候使用spawn_opt/4 来指定min_heap_size 注意:该参数使用的单位是字word
    通过spawn_opt设定进程初始堆大小会有两个影响:1.进程创建之初就有较大的堆空间,不必经历自增长的过程 2.即使存储的数据小于堆大小,垃圾回收时也不再压缩堆大小;类似的参数还有{min_bin_vheap_size, VSize},可以使用下面的语句查看默认值:
erlang:system_info(min_heap_size).
{min_heap_size,233}
erlang:system_info(min_bin_vheap_size).
{min_bin_vheap_size,46368}
  
何时动手调参数?
什么时候来调整这些参数呢?你是不是跃跃欲试了?记得一个原则,东西没有坏的时候不要去修它;用Erlang文档中反复出现的一段话来做回答:
This option is only useful for performance tuning. In general, you should not use this option unless you know that there is problem with execution times and/or memory consumption, and you should measure to make sure that the option improved matters.

参考资料
[1] http://www.erlang.org/faq/academic.html 
[2] http://www.erlang.org/doc/man/erlang.html#process_info-2
[3] http://prog21.dadgum.com/16.html
[4] http://www.erlang.org/doc/efficiency_guide/processes.html
[5] http://amiest-devblog.blogspot.com/2008/05/forcing-process-to-garbage-collect-in.html
[6] http://www.lshift.net/blog/2009/12/01/garbage-collection-in-erlang