进程堆之浅显易懂

Erlang里的每一个事物都是一个term。一个term是Erlang里的任何值。在Erlang内部,一个term是一个保留一些最少有效位(根据值的不同有效位从2到6不等)的,这些有效位定义了它的类型。剩下的位包含它自己的值(比如立即数的值)或包含一个指向堆上数据(box 值)的指针。

一个进程的堆是一个由term组成的数组。而进程的栈则是另一个由term组成的数组。栈被分配在堆的里面。寄存器也是一个由term组成的数组。堆上的数据大多数都是由term组成的数组,除了用头部标签(详情参阅 数据类型内存布局)标记的数据。

堆载体

Erlang内的内存分配发生在所谓的“载体”内。它们看起来像游戏里使用的“区域内存”–事先分配的一大块系统堆。在载体内部,真正的内存分配在这里发生。至于它们是如何运作的,为简单起见你可以想象成简单的malloc/realloc。

克服内存碎片的复杂事情都被封装好了,并且不是我们要理解的重点。你可以看源码 erts/emulator/beam/erl_*alloc.c (有许多文件,每一个分配策略一个文件)。模拟器有命令行标志来控制分配策略(参阅 http://erlang.org/doc/man/erts_alloc.html 标志部分)。

堆内的内存分配

当一个进程需要一些内存,它的 heap_top 增大,堆顶下面的内存就准备好被使用。一些活动想在其他进程的堆上分配内存,例如发送一个消息将把一个消息的拷贝给接收进程。

在进程堆内部是没有记账本的。就是没有跟踪那一个字属于哪里,但是我们可能通过查看标签位是可以知道每一个内存单元存储了什么。

垃圾回收

垃圾回收跟踪从寄存器和栈知道的活数据并保存它们,然后将其他的数据都卸掉。

当一个堆达到它的容量阀值(比如75%),进程就触发垃圾回收。一个新的更大的堆可能被分配出来。扫描分代的垃圾回收算法运行在堆上。这个算法获取“roots”(垃圾回收期间,根是所有已知的活数据)并把它们移到新堆。然后扫描源堆的剩余部分,提取更多的值,由根引用。扫描后,源堆只剩下死数据接着算法把它们卸掉。

“扫描”的意思是,垃圾回收器将数据从头到尾过一遍,分析所有它遇到的数据。“分代的”的意思是,算法将数据分为新生代和老生代,并假设新数据经常是死掉的,老数据是不太可能被释放的。另外算法记住老的位置(成熟的),也就是上一次扫描结束的地方。这个位置下面的任何数据被保证自上次扫描以来有没有更新。这一招会减少扫描的量,并加速算法。

在现实中比较复杂一点。可能有一个或两个堆有不同的逻辑应用于它们。每一个进程都有自己的垃圾回收器这样就使得Erlang的垃圾回收延时低。另外它不会暂停或影响其他调度器上的其他进程。这不是一个简单的话题,不过原理都在这:http://gchandbook.org/

另外请参阅
BEAM 智慧: 深层次知识: 进程堆布局
BEAM Wisdoms: 深层次知识: 数据类型内存布局
Erlang里的垃圾回收器

原文链接: http://beam-wisdoms.clau.se/en/latest/eli5-process-heap.html