前面的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提倡的是小消息,大运算.
"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 Erlang. VM 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,耳机里面的歌声是来自罗大佑《沉默的表示》:
小心的问一声 亲爱的你
请问 有没有看到我沉默的脸
背影后的你是这般熟悉
是否是另一个沉默的你
脚步声去远后 眼睛睁开以后
所有的一切已沉默的人
风雨中的脸一样的孤单
奔向那千百个沉默夜晚
为何梦中清清楚楚我看到的你
简直像看到的我自己
轻轻问一声 是否还要我再等 因为夜已这样深
轻轻问一声 是否夜已这样深 是否还要我再等
夜已这样深 轻轻我想问一声 是否还要我再等
夜已这样深 是否还要我再等 轻轻我想问一声
是否我要等 因为夜已这样深 轻轻我想问一声
是否我要等 轻轻我想问一声 因为夜已这样深