JavaScript 内存管理
Posted 陌路y
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript 内存管理相关的知识,希望对你有一定的参考价值。
javascript 具有垃圾自动回收机制(Garbage Collection)简称 GC。垃圾回收机制会中断整个代码执行,释放不可能再被使用的变量,释放内存,这个工作机制是周期性的,我们会在下文详细探讨。
可释放对象
function fn1() {
var obj1 = { name: \'xiaomuchen\', age: \'20\' }
}
function fn2() {
var obj2 = { name: \'xiaomuchen\', age: \'20\' }
return obj2
}
var a = fn1()
var b = fn2()
console.log(a, b) // undefined, {name: "xiaomuchen", age: "20"}
我们对比上面两个函数,fn1 在函数内声明变量 obj1 并且赋值,在函数执行后这个变量便不可再访问了,fn2 在最后把函数内的变量 obj2 返回到全局变量 b,所以 { name: \'xiaomuchen\', age: \'20\' } 这个对象(或者说 obj2)依然可被访问。
JavaScript 回收机制通过判断变量是否可被访问,来决定回收哪些变量。
标记清除和引用计数
那么 JavaScript 是如何判断变量是否可被访问?这就要提到标记清除和引用计数。
标记清除:标记清除是目前大部分 JavaScript 引擎使用的判断方式,通过标记变量的状态来确定是否可被回收。当变量在环境中被声明时标记进入环境,理论上永远不要释放进入环境的变量,因为它可以在环境中的任何位置、任何时刻被访问。当环境被销毁(如函数执行完),则变量被标记离开环境等待回收。
function fn(){
var a = { count: 10 } // 被标记,进入环境
var b = { count: 20 } // 被标记,进入环境
}
fn(); // 执行完毕之后 b 被标记,离开环境
引用计数:JavaScript 引擎维护一张引用表,保存内存中所有的资源的引用次数。资源被引用一次则引用 +1,资源被去掉引用或者退出变量的函数作用域时,则引用 -1,当资源的引用次数为0时,说明无法访问这个值,则等待回收。 (注:引用计数从 1 到 0 这个过程可能不执行,而是直接标记可被回收,不再进行加减运算节约开销)
function fn(){
var a = { count: 10 } // 资源 { count: 10 } 被引用次数为 1
a = { count: 20 } // 资源 { count: 20 } 被引用次数为 1,资源 { count: 10 } 被引用次数为 0,等待回收
// do someThing
}
fn(); // 资源 { count: 20 } 被释放
但是引用计数存在一种循环引用的情况,如下例子,两个对象之间相互引用,在离开环境后对象不可访问,但由于对象的引用次数为 1,则导致不会被回收。这个例子来自《JavaScript 高级程序设计》,但我思考良久,如果引用计数把 a.param 也作为一个变量来计数,那么就没有这个问题了,引用计数实现的方式不同,产生的结果也不一样。
function fn(){
var a = { count: 10 }
var b = { count: 20 }
a.param = b // b 的引用次数为 2
b.param = a // a 的引用次数为 2
}
fn(); // a、b 的引用次数为 1
GC 的缺陷、分代回收和增量 GC
和其他语言一样 GC 会中断代码执行,停止其他操作。因为要遍历所有对象,回收所有不可访问对象,这个操作的耗时可能有 100ms 以上。在 V8 引擎新版本中引入了两种优化方法:1. 分代回收(Generation GC),2. 增量 GC(increment GC)
分代回收:目的是通过对象的使用频率、存在时长区分新生代与老生代对象。多回收新生代区(young generation),少回收老生代区(tenured generation),减少每次需遍历的对象,从而减少每次GC的耗时
增量 GC:把需要长耗时的遍历、回收操作拆分运行,减少中断时间,但是会增大上下文切换开销
https://www.98891.com/article-13-1.html
Node.js 中的 GC 表现
当我们用 Node.js 搭建一个稳定的服务时,就需要考虑服务器内存的开销,下面一个 Node.js 内存回收执行的例子:
执行代码node --trace_gc --trace_gc_verbose test.js跟踪一个网络服务的 GC。
[41204:0x102001c00] Memory reducer: call rate 0.056, low alloc, foreground
[41204:0x102001c00] Memory reducer: started GC #1
[41204:0x102001c00] Heap growing factor 1.1 based on mu=0.970, speed_ratio=42956 (gc=675253, mutator=16)
[41204:0x102001c00] Grow: old size: 21382 KB, new limit: 33604 KB (1.1)
[41204:0x102001c00] Memory reducer: finished GC #1 (will do more)
[41204:0x102001c00] 156410 ms: Mark-sweep 27.7 (50.0) -> 21.0 (30.0) MB, 12.4 / 0.0 ms (+ 20.4 ms in 7 steps since start of marking, biggest step 4.8 ms) [Incremental marking task: finalize incremental marking] [GC in old space requested].
[41204:0x102001c00] Memory allocator, used: 30756 KB, available: 1435612 KB
[41204:0x102001c00] New space, used: 169 KB, available: 838 KB, committed: 1024 KB
[41204:0x102001c00] Old space, used: 16662 KB, available: 2417 KB, committed: 19412 KB
[41204:0x102001c00] Code space, used: 4078 KB, available: 178 KB, committed: 5120 KB
[41204:0x102001c00] Map space, used: 642 KB, available: 0 KB, committed: 2128 KB
[41204:0x102001c00] Large object space, used: 0 KB, available: 1434571 KB, committed: 0 KB
[41204:0x102001c00] All spaces, used: 21552 KB, available: 1438005 KB, committed: 27684 KB
[41204:0x102001c00] External memory reported: 1026 KB
[41204:0x102001c00] Total time spent in GC : 158.6 ms
[41204:0x102001c00] Memory reducer: call rate 0.003, low alloc, foreground
首先我们可以看到 Node.js 区分 New space、Old space 等来划分检索空间。而提示(+ 20.4 ms in 7 steps since start of marking, biggest step 4.8 ms) 告诉我们这个标记的步骤分 7 步进行,耗时最长的一次时 4.8ms。这使 JavaScript 可以很好的支持开发高实时应用。
总结
因为篇幅有限,留下一些小问题供大家思考:
- 闭包一定会导致内存不可被回收?
- 如何监控一个 Node.js 服务的内存开销,如何处理不可预知的内存泄漏?
以上是关于JavaScript 内存管理的主要内容,如果未能解决你的问题,请参考以下文章
VSCode自定义代码片段12——JavaScript的Promise对象