最近关注了一个国外技术博客RisingStack里面有很多高质量,且对新手也很friendly的文章。正好最近在学习Node.js的各种实现原理,在这里斗胆翻译一篇Node.js垃圾回收机制(原文链接)。
正文
在这篇文章中,你将会学习Node.js的垃圾回收(garbege collection)机制是如何工作的;即在你敲代码的时候,后台是怎么帮你清空内存里的垃圾的。
一、Node.js应用的内存管理
内存的适当分配,对于所有应用都至关重要。内存管理的任务,就是在程序请求内存的时候动态地为它们分配内存块;并在程序不再需要内存的时候释放掉。
应用级别的内存管理,有手动管理和自动管理两种模式。自动管理的机制中,通常都会包含垃圾回收机制。
The following code snippet shows how memory can be allocated in C, using manual memory management:
下面的代码片段展示了C语言中内存是如何分配的,这属于手动管理:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char name[20];
char *description;
strcpy(name, "RisingStack");
// memory allocation
description = malloc( 30 * sizeof(char) );
if( description == NULL ) {
fprintf(stderr, "Error - unable to allocate required memory\n");
} else {
strcpy( description, "Trace by RisingStack is an APM.");
}
printf("Company name = %s\n", name );
printf("Description: %s\n", description );
// release memory
free(description);
}
在手动内存管理机制中,释放无用内存的任务落在了程序猿身上。这样可能会给应用带来严重的问题:
内存泄露:可能某些占用的内存一直没有被释放。
当一个对象被删除(过早释放)的时候,可能会有指针不指向任何有效的对象,但仍然指向原来的内存。这种指针被称为“悬挂指针”。这个时候如果再去使用这段内存,就会产生严重的安全问题。
但幸运的是,Node.js是自带垃圾回收机制的,所以你不需要手动管理内存。
二、垃圾回收机制的概念
垃圾回收,是一种自动管理应用程序所占内存的机制,简称“GC”(方便起见,本文均采用此简写)。它的任务,就是回收无用对象(即垃圾)所占用的内存。它第一次出现,是在1959年的LISP
语言中,由John McCarthy发明。
GC判断一个对象为垃圾的标准是:是否还有其他对象引用它。
The way how the GC knows that objects are no longer in use is that no
other object has references to them.
如果没有GC
下图展示了没有垃圾管理机制的时候,内存的情况。可以看到有的对象与其余的对象之间,没有任何引用关系,但他们的内存也不会被回收。
有了GC之后
有了GC之后,没有引用关系的对象占用的内存,都会被GC悄然回收。
使用GC的优势
it prevents wild/dangling pointers bugs,
it won't try to free up space that was already freed up,
it will protect you from some types of memory leaks.
Of course, using a garbage collector doesn't solve all of your problems, and it’s not a silver bullet for memory management. Let's take a look at things that you should keep in mind!
避免了悬挂指针的出现。
它不会尝试去重复释放并没有被占用的内存。
它会防止某些类型的内存泄露。
当然了,GC并不能解决所有内存相关的问题,它不是解决内存管理问题的万金油。有些使用GC的注意事项还是需要开发者牢记:
performance impact - in order to decide what can be freed up, the GC consumes computing power
unpredictable stalls - modern GC implementations try to avoid "stop-the-world" collections
对性能的影响:在判断哪些内存要释放的时候,GC会占用CPU资源。
不可预测的中断:尽管现在的GC都会避免“停止一切”的情况发生,但是还是不可避免的会出现。
译注:“停止一切”(stop-the-world)是指当垃圾收集没有结束前,内存对于外部的请求是不会进行响应的,直到收集完毕应用才会继续响应请求。
三、Node.js垃圾回收&内存管理实践
学代码就是要写代码,下面就用几段代码展示本节的主题。首先介绍几个基本概念:
栈(Stack)
栈中存储着本地变量、指向堆中对象的指针、定义应用程序控制流的指针。
在下面的例子中,变量a、b都会存储在栈中。
function add (a, b) {
return a + b
}
add(4, 5)
堆(Heap)
堆专门用于存储“引用类型”的对象,例如字符串或对象。
下例中的Car
对象就是保存在堆中的。
function Car (opts) {
this.name = opts.name
}
const LightningMcQueen = new Car({name: 'Lightning McQueen'})
执行之后,内存看上去会是这个样子:
新建更多的Car
对象的话,内存会变成这样:
function Car (opts) {
this.name = opts.name
}
const LightningMcQueen = new Car({name: 'Lightning McQueen'})
const SallyCarrera = new Car({name: 'Sally Carrera'})
const Mater = new Car({name: 'Mater'})
如果这个时候执行垃圾回收,那么什么都不会发生,因为根对象(root)对每个对象都有引用。
那现在把上述例子再复杂化一点,给Car
对象添加点“部件”。
function Engine (power) {
this.power = power
}
function Car (opts) {
this.name = opts.name
this.engine = new Engine(opts.power)
}
let LightningMcQueen = new Car({name: 'Lightning McQueen', power: 900})
let SallyCarrera = new Car({name: 'Sally Carrera', power: 500})
let Mater = new Car({name: 'Mater', power: 100})
What would happen, if we no longer use Mater, but redefine it and assign some other value, like Mater = undefined?
现在,如果我们不想再使用Mater
这个实例,把他赋一个别的值,比如Mater = undefined
。这时会发生什么?
可以看到Mater
失去了root对他的引用。那么,在下次垃圾回收执行的时候,它的内存就会被释放。
好了,现在我们都理解了GC的基本原理和执行方式,来看看V8引擎中的GC是如何实现的吧!
垃圾回收方法
在我们之前的一篇文章中,我们介绍过Node.js的垃圾回收方法是如何工作的,我强烈建议阅读此文章。
这篇文章的要点如下:
1. 新生代空间 & 老生代空间
堆中存在两个“段”(segment
),新生代空间(New Space)和老生代空间(Old Space)。新的内存分配都发生在新建空间中,它只有1-8MBs左右大,但垃圾回收却很迅速和频繁。这里存储的对象称为“新生代”(Young Generation)。
老生代空间中,存储着那些新生代空间中未被回收,晋升至此的对象。它们被称为“老生代”(Old Generation)。这里内存分配非常频繁,但垃圾回收的成本却很高,因此执行地不那么频繁。
2. 新生代
通常只有20%左右的新生代会晋升为老生代。老生代空间只有在快被耗尽的时候,才会执行垃圾回收。V8引擎采用了两种回收算法来实现:Scavenge 和 Mark-Sweep 。
Scavenge回收算法运算速度很快,用于新生代;慢一些的Mark-Sweep算法用于老生代。
四、现实案例The Meteor Case-Study
2013年,Meteor的作者们发布了一个他们遇到的内存泄露的例子。出问题的代码段如下:
var theThing = null
var replaceThing = function () {
var originalThing = theThing
var unused = function () {
if (originalThing)
console.log("hi")
}
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log(someMessage)
}
};
};
setInterval(replaceThing, 1000)
Well, the typical way that closures are implemented is that every
function object has a link to a dictionary-style object representing
its lexical scope. If both functions defined insidereplaceThing
actually usedoriginalThing
, it would be important that they both get
the same object, even iforiginalThing
gets assigned to over and over,
so both functions share the same lexical environment. Now, Chrome's V8
JavaScript engine is apparently smart enough to keep variables out of
the lexical environment if they aren't used by any closures - from the
Meteor blog.通常来说,实现闭包的方式为:每个函数对象都链接到一个字典式的对象,此对象表现其词法作用域。如果
replaceThing
中两个函数都使用了变量originalThing
,那么即便originalThing
被多次赋值,也必须保证这两个函数得到的永远是同一个对象,才能保证两个函数共享一个词法作用域。那么问题来了,Chrome的V8
JavaScript引擎只有在一个变量没有被用在任何闭包中的时候,才会将其隔离在词法环境之外。 - Meteor blog.
更多相关阅读
Finding a memory leak in Node.js
JavaScript Garbage Collection Improvements - Orinoco
memorymanagement.org