每两周一篇的连续更新来啦,虽然主要是为了完成社区任务 😂
Erlang Process 内存分布
在了解 GC 之前,我们先来看看 Erlang 进程的内存分布是怎样的:
Shared Heap Erlang Process Memory Layout
+----------------------------------+ +----------------------------------+
| | | |
| | | PID / Status / Registered Name | Process
| | | | Control
| | | Initial Call / Current Call +----> Block
| | | | (PCB)
| | | Mailbox Pointers |
| | | |
| | +----------------------------------+
| | | |
| | | Function Parameters |
| | | | Process
| | | Return Addresses +----> Stack
| | | |
| +--------------+ | | Local Variables |
| | | | | |
| | +------------+--+ | +-------------------------------+--+
| | | | | | | |
| | | +-------------+--+ | | ^ v +----> Free
| | | | | | | | | Space
| | | | +--------------+-+ | +--+-------------------------------+
| +-+ | | | | | |
| +-+ | Refc Binary | | | Mailbox Messages (Linked List) |
| +-+ | | | |
| +------^---------+ | | Compound Terms (List, Tuples) | Process
| | | | +----> Private
| | | | Terms Larger than a word | Heap
| | | | |
| +--+ ProcBin +-------------+ Pointers to Large Binaries |
| | | |
+----------------------------------+ +----------------------------------+
- PCB: 进程控制块保存一些有关进程的信息,如进程标识符(PID)、当前状态(正在运行、等待,等等)、注册名称、初始时和当前正在调用的函数,等等。PCB 还保存了一些指向传入本进程的消息的指针,这些消息以链表的形式存储在堆中。
- Stack: 向下增长的内存区域,保存了函数入参、函数调用返回地址、以及本地变量等等。大一点的数据结构,比如 List 和 Tuple 这些是保存在 Heap 里的。
- Heap: 向上增长的内存区域, 保存了进程的消息邮箱、复合的数据类型比如 Lists, Tuples and Binaries 以及像浮点数这种超过一个机器字(word)长度的对象。注意超过 64 个字节长度的 Binary 不是存储在这个进程的私有 Heap 里的,这种情况下进程的私有 Heap 里面存的是指向另一个全局共享 Heap 里 Binary 对象的指针。这个共享 Heap 是可以被所有的 Process 访问的,保存在全局共享 Heap 里面的“大 Binary” 叫做 Refc Binary (Reference Counted Binary),而存在私有 Heap 里面的指针叫做 ProcBin。
每个 Erlang 进程都有自己的栈和堆,这些栈和堆分配在同一个内存块中,并"相向而行"🐶。当栈和堆相遇时(也就是说此进程的内存用尽时),将触发垃圾回收。如果没能回收足够的内存,进程将会去申请更多的内存。
我们来看一下进程的 PCB 里面的内容:
1> hipe_bifs:show_pcb(self()).
P: 0x00007f7f3cbc0400
---------------------------------------------------------------
Offset| Name | Value | *Value |
0 | id | 0x000001d0000003a3 | |
72 | htop | 0x00007f7f33f15298 | |
96 | hend | 0x00007f7f33f16540 | |
88 | heap | 0x00007f7f33f11470 | |
104 | heap_sz | 0x0000000000000a1a | |
80 | stop | 0x00007f7f33f16480 | |
592 | gen_gcs | 0x0000000000000012 | |
594 | max_gen_gcs | 0x000000000000ffff | |
552 | high_water | 0x00007f7f33f11c50 | |
560 | old_hend | 0x00007f7f33e90648 | |
568 | old_htop | 0x00007f7f33e8f8e8 | |
576 | old_head | 0x00007f7f33e8e770 | |
112 | min_heap_size | 0x00000000000000e9 | |
328 | rcount | 0x0000000000000000 | |
336 | reds | 0x0000000000002270 | |
16 | tracer | 0xfffffffffffffffb | |
24 | trace_fla.. | 0x0000000000000000 | |
344 | group_lea.. | 0x0000019800000333 | |
352 | flags | 0x0000000000002000 | |
360 | fvalue | 0xfffffffffffffffb | |
368 | freason | 0x0000000000000000 | |
320 | fcalls | 0x00000000000005a2 | |
384 | next | 0x0000000000000000 | |
48 | reg | 0x0000000000000000 | |
56 | nlinks | 0x00007f7f3cbc0750 | |
616 | mbuf | 0x0000000000000000 | |
640 | mbuf_sz | 0x0000000000000000 | |
464 | dictionary | 0x0000000000000000 | |
472 | seq..clock | 0x0000000000000000 | |
480 | seq..astcnt | 0x0000000000000000 | |
488 | seq..token | 0xfffffffffffffffb | |
496 | intial[0] | 0x000000000000320b | |
504 | intial[1] | 0x0000000000000c8b | |
512 | intial[2] | 0x0000000000000002 | |
520 | current | 0x00007f7f3be87c20 | 0x000000000000ed8b |
296 | cp | 0x00007f7f3d3a5100 | 0x0000000000440848 |
304 | i | 0x00007f7f3be87c38 | 0x000000000044353a |
312 | catches | 0x0000000000000001 | |
224 | arity | 0x0000000000000000 | |
232 | arg_reg | 0x00007f7f3cbc04f8 | 0x000000000000320b |
240 | max_arg_reg | 0x0000000000000006 | |
248 | def..reg[0] | 0x000000000000320b | |
256 | def..reg[1] | 0x0000000000000c8b | |
264 | def..reg[2] | 0x00007f7f33ec9589 | |
272 | def..reg[3] | 0x0000000000000000 | |
280 | def..reg[4] | 0x0000000000000000 | |
288 | def..reg[5] | 0x00000000000007d0 | |
136 | nsp | 0x0000000000000000 | |
144 | nstack | 0x0000000000000000 | |
152 | nstend | 0x0000000000000000 | |
160 | ncallee | 0x0000000000000000 | |
56 | ncsp | 0x0000000000000000 | |
64 | narity | 0x0000000000000000 | |
---------------------------------------------------------------
我们使用 hipe_bifs:show_pcb
来打印 PCB 的内容,请注意 OTP 23 似乎删掉了这个工具,所以尽量用 OTP 22 之前的版本。
其中 htop
和 stop
是分别指向 Heap 和 Stack 顶部的指针,“顶部” 的意思是堆或者栈上的下一个可用的内存片段。heap
和 hend
指针分别指向整个 Heap 的起始和终止位置。然后 heap_size
字段是 Heap 的大小,单位是字 (words)。就是说,在 64 位机器上:hend - heap = heap_sz * 8
,32 位机器上:hend - heap = heap_sz * 4
。最后要说的就是 min_heap_size = 0xe9
(words),这个指的是 Heap 的初始大小,同时也是当 Erlang 进程内存收缩的时候所允许的 Heap 最小值,默认值是 0xe9(233)
。
需要注意的是,前面我们介绍的 Heap 和 Stack 两个概念,其实在 Erlang 进程的内存里是同一块内存,heap
和 hend
两个指针分别指向了这块内存的开始和终止位置。这也是为什么上面 hipe_bifs:show_pcb/1
的结果里,对于堆栈只有个 stop
指针而没有对应的 stack
和 send
。基于这些我们再重新画一下文章开始的内存图:
+--+-------------------------------+ <--- *hend 高地址
| |
| Function Parameters |
| | Process
| Return Addresses +----> Stack
| |
| Local Variables |
| |
+-------------------------------+--+ <--- *stop
| | |
| ^ v +----> Free
| | | Space
+--+-------------------------------+
| |
| Mailbox Messages (Linked List) |
| |
| Compound Terms (List, Tuples) | Process
| +----> Private
| Terms Larger than a word | Heap
| |
---+ Pointers to Large Binaries |
| |
+----------------------------------+ <--- *heap 低地址
现在我们可以简单的介绍一下垃圾回收了。
简单地将,Erlang 的垃圾回收是一个分代的垃圾回收,并且是基于单进程的,所以它不会 Stop the world
。从上面我们了解到的 Erlang 进程内存结构来说,它是工作在进程的私有 Heap 之上(从 *heap 到 *hend),然后额外还要去清扫 ProcBin 指向的全局共享 Heap 中的那些内存。
关于 Erlang 垃圾回收的进一步的细节,我们将放到本系列的后续文章中讲解。
引用文献以及资料: