JS自动管理内存, 不像C提供malloc
和free
可以自己控制内存的使用和释放, 因此掌握JS垃圾收集的原理才能让我们写出配合JS垃圾收集机制的代码, 实现更高效的垃圾回收.
在浏览器端的内存泄漏会造成页面卡, 刷新能解决问题, 但是对于服务端这种不重启就一直运行的东西来说, 内存泄漏时间久了就down了, 重启的代价是很大的.
本文主要研究V8内存管理, 下面开始吧!
V8对内存有限制, 64bit系统可以用1.4G, 32bit系统可以用0.7G 内存为什么要限制大小?
房子越大, 打扫起好耗费的时间越多, 1.4G内存, GC收集1次要1秒以上
通过process.memoryUsage()
可以看到V8内存结构
我们可以看到stack
中存储的仅仅是一个指针, 实际分配的对象在heap
中存储,那么内存泄漏是什么呢?
内存泄漏就是heap中的对象太多了, 把heap撑爆了, 也就是term: OOM.
解决那些泄漏的问题就是让heap中那些不再使用的对象被GC回收.
heap中除了存对象外还存着闭包, 比如下面的Person将一直存在heap中
let personFactory = function(){
let p = new Person(name);
return function(){
console.log(p)
}
}
let p1 = personFactory('evle')
V8的回收机制, 根据垃圾的寿命长短分为新生代和老生代, 顾名思义, 新生代就是活不长的,老生代就是活很久的.
function Person(name){
this.name = name
}
let p1 = new Person('evle')
我们声明了一个对象, p1是指针存储在stack里, new Person(‘evle’)是对象, 存储在堆里, 现在打开Chrome dev tools我们可以在memory中看到Person x1
, 代表现在内存中有1个Person对象, 因为p1引用了这个对象, 下面让我们再引用一次这个对象.
let set = new Set();
set.add(p1);
这次我们用Set又引用了一次Person
这个对象, 这时我们在内存中会看到Person x2
, 代表这个Person对象被引用了2次, GC每隔几十毫秒执行一次, 一直在找不被引用的对象,如果找到就清除掉, 下面我们让p1和set取消对这个对象的引用.
set = null;
p1 = null; // 随便赋值什么都好, 只要没人引用Person
这时我们再看内存, 内存中Person已经消失了, 意味着被GC清掉了, 那么GC是怎么清除它的呢?
之前我们说过V8回收垃圾基于新生代, 老生代的机制, 老生代可能是新生代中存久了的变量, 也可能是新生代存不下的变量(大于16M), 那么我们上面写的代码明显是被存在新生代中, 从上图中我们可以看到新生代会被存储空间分为2个相等的空间: from
和to
, 新生代的垃圾清除相当于: 将活的对象放入to, 将死的对象留在from中, 引用的结构是一个树结构, 广度优先的一层一层扫描那些活跃的对象, 活跃对象都扫描完之后就格式化from, 最后交换from
和to
,如果交换5次发现这个对象还活跃, 那么就把它移动到老生代中.
新生代空间存不下的, 或者在新生代中一直活跃的会被移到老生代中, 老生代执行GC很慢, 1.4G收集1次要1秒以上, 这时候系统很卡.
新生代转到老生代中如果内存够, 就采用mark-sweep
算法管理, 就是当清除垃圾的时候, 直接就清掉了,不会移动内存块, 这样会碎片, 浪费内存, 但是速度快
新生代转到老生代中如果内存不够, 就采用mark-compact
算法管理, 它会将活跃的对象往左移动, 不活跃的往右边移动, 最后清除这些不活跃的, 但是明显这样速度慢, 所以V8常用的是mark-sweep
算法来管理老生代.
V8在对老生代回收时候有优化, 将一次GC收集分解撑多个小收集, 也就是一个大暂停变成了多个小暂停, 不至于让系统一直卡着不动.
以上就是V8内存收集的原理, 主要是3种算法, 新生代使用scavenge, 老生代看情况使用mark-compact和mark-sweep, 我们在写代码时候避免闭包的滥用, 避免对不被需要的变量保持引用, 才能让GC顺利的进行垃圾收集.