前面的Erlang杂记中我们简单提到过Erlang的垃圾回收机制:1.以进程为单位进行垃圾回收 2.ETS和原子不参与垃圾回收.今天我们继续这一话题,关注更多关于细节.

   在Erlang的官方文档中,关于垃圾回收的知识散见于各处,要把这些信息收集在一起还是要费些力气的,完全不像微软文档那样系统化,比如这是关于.net framework垃圾回收的文档:http://msdn.microsoft.com/en-us/library/ee787088.aspx ;好吧,有点耐心,还是可以从Erlang官方文档中发现好多宝的,回头再说,现在开始:

 

历史回顾

 简单回顾一下垃圾回收的知识,垃圾回收器的本质实际上是改变存活数据结构构成图的连通性.堆对象在图中的存活性是由指针的可到达性定义的.程序可以操作三种位置的数据:寄存器 程序栈(局部变量 临时变量) 全局变量.这些位置的变量有一部分保存了指向堆数据的引用,他们构成了应用程序的根(Root).对于用户程序动态分配的内存只能通过Root或者根发出的指针链访问,程序不应该访问其地址空间的随机位置.
   内存分配的解决方案有:
    [1] 静态分配 优点是:编译器知道所有的数据位置,实现效率高 缺点是:每一个数据结构的大小必须在编译时可知,方法调用不可以是递归的,因为同一个方法在内存中共享相同的位置. 无法动态创建数据结构
    [2] 栈分配 调用的时候入栈,调用结束出栈;同一个方法的不同调用不再共享地址,递归成为可能;只有大小能在编译时去递归大小的对象池啊能作为过程的结果返回.被调用者的生命周期不可能比调用方的生命周期更长.
    [3]堆分配 闭包成为可能 递归结构的表达式成为可能
 
   垃圾回收的经典算法有:引用计数 标记清除 节点复制
  [1] 引用计数方法是和程序执行同时进行,内存管理的开销比较均匀,这样进行没有长时间的挂起内存管理的时间比较稳定,可以获得比较平滑的响应时间;
  [2]标记清除 内存单元不会被立即回收,而是处于不可到达状态,直到所有的内存都被耗尽,进行全局级别的遍历来确定哪些单元可以回收.显然这种全局级别的中断在实时性要求较高的系统并不实用,甚至视频游戏都不可能接受在GC时有这么长的停顿.如果实时性方面要求不高,标记清除可以获得比引用计数更好的性能.标记清除的代价还是较高,标记是全局级别的,算法复杂度与整个堆大小成正比;标记清除使得内存空间倾向于碎片化.在物理存储器中碎片化的影响不大,但虚拟存储中会导致辅助存储器和主存之间频繁的交换页面,系统出现颠簸.
 [3]节点复制将堆分成两个半区,一个包含现有数据,另一个包含已经被废弃的数据,运行时两个半区的角色不断交换;这样做的优势在于内存分配的开销很小,只需要比较指针,不存在内存碎片的问题.但是内存浪费较大;
 [4]标记-整理缩并 标记所有的存活对象 通过重新调整存活对象位置来缩并对象图;更新指向被移动了位置的对象的指针
 [5] 分代回收 是基于统计学原理的:多数内存块的生存周期都比较短,垃圾收集器应当把更多的精力放在检查和清理新分配的内存块上

 IBM公司的David F.Bacon [7] 2004年发表了"A Unified Theory of Garbage Collection"论文,文中阐述了一种理论:任何一种GC算法都是跟踪回收和引用计数的两种回收思路的组合;
 

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参数,这个参数的意思是:最多经过多少代就可以强制进行充分垃圾回收了,不管旧数据堆是否有剩余空间.

 分代(generational )本身是基于统计学的:多数内存块的生存周期都比较短,最近创建的对象更容易变冷(不再被使用).基于上面的考量,Erlang对"年轻"一代对象的GC会更频繁,减少对常驻内存的对象GC次数.对于一个Erlang进程当没有足够Heap空间的时候就会触发GC.由于Heap是私有的所以进程销毁的时候内存可以直接回收.Erlang的GC可以分成两种:minor collection and major collection.
  Minor collection只对年轻一代(young generation)的对象进行GC,Major collection会进行整体GC. 在进行了一定次数Minor Collection后,或者Minor Collection没能释放足够的内存的时候就会触发Major collection.
 
 

我们看一下这个参数的默认值:
     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提倡的是小消息,大运算.

"The basic idea of the Private Heap (PH) architecture is that each process allocates and maintains its own, local, heap. The heap and the stack for each process are allocated in the same memory area and grow towards each other. The main advantage with a scheme like this is that processes’ heaps in general are small which usually makes garbage collection times fairly short.

   The garbage collector is a two generational stop-and-copy. It has two different modes of operation, corresponding to the minor and the major collection. The root set for each garbage collection consists of the process stack, the message queue and an optional vector of pointers sent to the garbage collector.

Note that the garbage collector does not have to scan the stack of any other process. Instead garbage collection happens locally. "

 Erlang Process的ordinary heap存放young generation的数据,历经2~3次Minor Collection的数据被提升为old generation,Erlang Process heap空间划分专门区域存放old generation 数据.Young generation的回收有一个水位线(high water mark)概念,凡是数据地址比水位线要低的都是较老的,比水位线地址高的是更年轻的数据.水位线一下的数据至少经历了一次minor collection或major collection.

 

 

下面的图片来自 论文  Characterizing the Scalability of ErlangVM on Many-core Processors


    上面提到进程创建伊始会分配很小的栈和堆资源,这并不是固定不变的,垃圾回收器会动态调整堆大小.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二进制数据通常数据量相当大,如果二进制数据<64 bytes会在进程内存储,如果超过64 bytes二进制数据是在进程以外的独立的堆分配.二进制数据占用一块数据区域,数据区域头信息包含指向数据区域的指针.当二进制数据分割成子二进制数据段的时候,会创建新的数据头信息但数据并没有被拷贝.二进制内存分配对节点内所有的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

[7] http://researcher.watson.ibm.com/researcher/files/us-bacon/Bacon04Unified.pdf 

 

现在时间2011-11-13 5:28:53,耳机里面的歌声是来自罗大佑《沉默的表示》:

小心的问一声 亲爱的你
请问 有没有看到我沉默的脸
背影后的你是这般熟悉
是否是另一个沉默的你

脚步声去远后 眼睛睁开以后
所有的一切已沉默的人
风雨中的脸一样的孤单
奔向那千百个沉默夜晚
为何梦中清清楚楚我看到的你
简直像看到的我自己

轻轻问一声 是否还要我再等 因为夜已这样深
轻轻问一声 是否夜已这样深 是否还要我再等
夜已这样深 轻轻我想问一声 是否还要我再等
夜已这样深 是否还要我再等 轻轻我想问一声
是否我要等 因为夜已这样深 轻轻我想问一声
是否我要等 轻轻我想问一声 因为夜已这样深