blog
blog copied to clipboard
Javascript GC杂谈
前言
Javascript
是一门具有垃圾回收机制的编程语言,程序员大部分情况下不用手动去操心内存管理的问题。
和EvemtLoop
类似,虽然我们不能直接地去操控垃圾清理的执行与停止,但充分了解了V8
的垃圾回收机制后吗,在编码上能够有意识地去较少GC
的影响。相当于武林高手虽然绑上了手脚,但仍然能够识破别人的招式,以守为攻。
本文主要分为两个部分,① 先了解V8
与内存的关系,② 再延展开去了解V8
对内存的管理(清理)。
V8 与内存
V8的初始内存限制
- V8启动后只能够使用物理机的部分内存,(64位系统下1.4GB,32位系统下0.7GB)
-
V8
创始之初是设计为新一代Chrome
的内核,作为浏览器所需要的内存当然就不需要太大,这从系统安全
和浏览器本身需求
两个角度去考虑的。 - 基于
V8
自身的垃圾回收机制,若分配的内存过大,单次的垃圾回收时间则会过长,这是在服务端Node.js
和前端浏览器都不可以接受的。
内存占用情况
-
heapTotal
和heapUsed
分别代表已申请到的堆内存大小,和当前的使用量。 -
external
代表 V8 管理的,但绑定到C++
对象的内存使用情况。 -
rss
: 全称是resident set size
,是驻留集大小, 是给这个进程分配了多少物理内存(占总分配内存的一部分),包含所有的 C++ 和 JavaScript 对象与代码。
'os.freemem()': os.freemem(), // 返回系统空闲内存的大小, 单位是字节
'os.totalmen': os.totalmem(), // 返回系统总共内存的大小
堆外内存
堆外内存指的是那些OS
分配给Node.js
进程使用的内存资源,但却不是通过V8
内核进行分配的内存,称之为堆外内存
。
这种情况是因为在浏览器Api
中,对于大多数场景不需要使用过度的内存,而Node.js
在服务端的场景,需要处理网络I/O流,则需要更大的内存,常见的有Buffer
API。
GC清除算法
内存分代
-
V8
的内存分为老生代
和新生代
,新生代
较小,用于保存新产生的小的对象,老生代
用于保存大对象和经历多次回收仍未回收的对象。 - 新生代和老生代加起来总共的就是
V8
可支配堆内存大小。
新生代的清除算法
新生代使用Scavenge
算法进行回收。其实现核心是Cheney
算法。
执行过程
- 首先将
From
空间中所有能从GC Root
对象到达的对象(说明仍该对象仍存活)复制到To
区。 - 复制过去之前会进行检查,若出现以下两种情况之一,则会触发
对象晋升机制
,直接移动到老生代
中。- 此时
To
空间的使用率已经超过了25%。 - 已经经历过了一次
Scavenge
回收,却没有被淘汰掉的
- 此时
- 非活动对象的
semispace
内存会被释放掉。 - 两个
semispace
空间对调。
老生代的清除算法
老生代中GC
使用的是标记清除(Mark Sweep)
策略 和 标记整理(Mark Compact)
策略。
Mark Sweep
执行过程
- 标记阶段将老生代中仍然存活的对象做上标记
- 直接清除回收未被标记的对象
- 但可能会产生大量的内存碎片
Mark Compact
执行过程
- 标记阶段将老生代中仍然存活的对象做上标记
- 将仍然存活的对象往内存的一端进行移动
- 直接清理掉边界外的非占用内存
- 但是在内存中移动对象的过程十分耗时
V8
中会将这两种方法进行结合,在大多数情况下使用的是Mark Sweep
,而在发生对象晋升
而老生代空间连续不足的情况下,Mark Compact
才会被触发。
incremental marking
与lazy swaeeping
为了减低全堆垃圾回收带来的全停顿
,V8
从标记到清除也都进行了改进,使得原本需要长时间的GC
工作,得以分段实行。
常见内存问题
内存泄漏
- 闭包 与 全局变量 闭包的原理这里不再赘述,当闭包的引用挂载在全局下,而闭包本身没有被释放的情况下,则也可能会引起内存泄漏。
-
DOM
节点内存泄漏 只有同时满足DOM
树和JavaScript
代码都不引用某个DOM
节点,该节点才会被作为垃圾进行回收。 - 定时器、事件使用后未移除 定时器 和 时间监听基本是JavaScript的常用工具,但常常是只记得用,不记得销毁。也可能导致内存泄漏。
内存膨胀
- 缓存的使用
前端开发时,经常使用内存将计算过程进行,缓存的变量又绑定在顶级属性上,也就导致了长期存活的对象越来越多。特别是在
Node.js
服务端端开发时,尽可能减少内存进行的使用。
频繁垃圾回收
- 使用大量的临时空间,导致垃圾回收频繁触发。
如何对应避免内存泄漏
V8
- 坏习惯之王:
定时器
和事件监听
要记得及时清除。 - 对象之间尽量减少交叉,可以达到拆分对象,不创造大生命周期对象的效果。
Node.js环境下
-
Node.js
中尽量使用stream
和buffer
来操作大文件,而不是使用内存 -
慎用内存当做缓存
不要像页面开发一样,想当然地大量使用内存保存临时变量,使用场景允许的情况下,使用外部有着完整过期机制的缓存工具,来缓存大数据对象。
主动释放
赋值null以释放
一旦数据不再使用,最好通过将其值设置为null
来释放其引用,这个做法叫做解除引用(dereferencing)
一般会针对全局对象的属性进行解除引用操作,局部变量会在离开执行环境的时候自动被解除引用。
解除引用后并不会马上释放内存,只是相当于打上了一个标记,本轮GC回收
的时候,会直接将指向null
的数据原所占内存释放掉。
不推荐使用 delete 释放空间
已知delete
关键字也可以删除对象的一个属性,但不推荐使用
- 常常用于删除对象上的一个属性,无论对应的属性值是一个对象,还是函数或是其他
- 任何使用
let
const
var
fun
声明的量,都不能都够使用delete
去删除 - 删除不能删除的量,执行器并不会报错,会返回一个
false
,然后相当无操作 - 对于对象属性,若该属性配置了
configurable:false
,那么也是不能够被删除的。 -
delete
相对于null
操作范围基本限定在对象的属性上,能够实现资源回收的优化也仅限于对象的属性上。
结语
无论是V8
下的浏览器
还是Node.js
其内存分配制度是极其相似的,Node.js
仅仅在其使用场景需要处理大文件I/O
的情况下增加了堆外内存的相关API
。
因为没有实际去排查过V8
在生产环境下的内存问题,接下来的文章就去看看Node.js
和浏览器下如何排查内存隐患
吧。
参考资料
[1]《Javascript 高级程序设计 第三版》
[2] 浅谈Chrome中的垃圾回收 -博客园
[3]《Nodejs深入浅出》
[4] javascript 中的 delete