Erlang 是一种具有动态类型的函数式编程语言。它的主要特点是在独立进程级别工作。CPU几乎已经达到饱和状态,现在主要通过增加内核数量来发展。
使用 Erlang 编写的应用程序可以充分利用多线程 CPU 的优势,处理大量并发活动。
Erlang 的用例包括为抗错网络应用程序、消息应用程序、任务管理器和服务器监控系统开发后端。Erlang 最大的优点之一是其垃圾回收机制。
Erlang 的可扩展性
当前实现代码架构的基础是调度器,它在每个 CPU 内核上都有自己的进程队列。每个进程都被分配了一定数量的函数调用,在这些函数调用用完后,队列中的下一个进程就会启动。
在 Erlang 虚拟机中,进程是一个完全隔离的实体,它有自己的内存栈(进程指令)和堆(进程所需的数据)。进程由两部分组成:
- 包含进程一般信息、内存空间引用、统计信息和计数器的独立控制器块
- 控制器引用的内存空间
何时需要垃圾回收
如果内存中没有足够的空间存放新信息,就会启动垃圾回收。它会删除应用程序不再需要的数据,然后应用程序会继续运行。
Erlang 使用 GC 将堆中的对象分为两类:长期和短期。第一类包含至少经历过一次垃圾回收的数据,因此被认为是应用程序工作所必需的。因此,GC 每隔几个周期才会返回这些对象。
标准周期
minor 垃圾回收器循环会剔除不再被引用的对象,这些对象也不是进程运行所必需的。它分为三个步骤:
- GC 会找到所谓的根对象(Root Objects),即引用进程内存中数据的对象(进程字典、消息队列、错误数据等)。然后挑选出它们所引用的数据,并将其放入新的内存区域,剩下的则删除。
- 然后,收集器会查看新内存区域中第一批被移动的对象,并只移动它们所引用的数据。
- 堆栈被移动到新内存区域后,垃圾回收周期结束,不再有任何引用的对象会被删除。在该过程中存活下来的数据会被标记,以便在下一个 GC 周期中将至少存活一个周期的数据转移到旧堆区域。
值得注意的是,堆栈(执行指令)和堆(工作数据)都位于进程内存中,它们位于可用内存段的边缘,并相互增长。
当没有可用空间时,调度程序会中断代码的执行,并使用垃圾回收器分配一个新的内存区域,然后只将相关对象转移到该区域。在此之后,如果初始分配的内存大小过大而无法缩小,则可能会再次出现复制。
如何处理旧对象
Erlang 的一个特点是,收集器每 65000 个标准周期才进行一次 major 收集过程,返回旧堆中的对象。在 major 收集过程中,仍被引用的对象会被移动到一个新的区域,而不相关的数据则会被移除。
但在此期间,进程内存肯定会增长,而且早期的 Erlang 有一个 bug,如果进程执行时间过长,可能会崩溃,原因可能是大量的垃圾回收或调度程序没有考虑到的 NIF 调用。
为了解决这些问题,我们添加了第二种调度器类型–脏调度器(Dirty scheduler)。它仍然在每个 CPU 内核上运行,但所有调度器都在一个共享队列中工作。预计会运行很长时间,超过超时的进程会被放在这里。如果系统检测到没有足够的内存供进程运行,而且下一次 GC 调用可能需要很长时间,那么它稍后就会在脏工作队列中运行。
由于进程仍有执行超时,为了向其提供所需的内存,我们采用了延迟 GC 技术。该技术会将当前堆标记为 “废弃”,并分配一个新的内存区域,其中包含足够的空闲内存,以满足进程的需要,另外还会多分配一点。
如果仍然没有足够的内存,就会分配另一个区域,与前一个区域链接,并标记为 “堆碎片”。如此反复,直到进程满足要求为止。
内存增长
一个进程需要更多内存的速度有多快?在 21 世纪的版本中是这样的:
- 数字 0-22: 根据斐波那契数列为 12 和 38 23 或更多: 增加 20%。
- 23 或更多: 增加 20% 一个标准的 GC 循环需要以下两个数字中较大的一个: 旧堆大小的 1/8 或所需内存的 300%。
对于长期对象 GC,仍会分配所需的内存,但如果内存是相关对象总大小的 4 倍,则内存会减半。通常,这是在 Erlang 虚拟机决定是否需要更改已分配内存区域时最后完成的。
Erlang 的优势
可以检查对象的相关性,而无需像 Ruby 或 Python 等其他流行编程语言那样暂停整个系统。之所以能做到这一点,是因为 Erlang 中的进程是作为系统的孤立部分工作的。
Erlang 虚拟机是一种相当成熟的软件产品,已在软实时系统中得到验证。在 Evrone,我们在电信、视频流、移动互联网、消息系统和各种网络应用程序中使用 Erlang创建可靠、可扩展的应用程序。
原文链接:Garbage Collection in Erlang Helps Create Scalable Applications