All Articles

JavaScript底层原理之垃圾收集

Published 30 Mar 2020

JS自动管理内存, 不像C提供mallocfree可以自己控制内存的使用和释放, 因此掌握JS垃圾收集的原理才能让我们写出配合JS垃圾收集机制的代码, 实现更高效的垃圾回收.

在浏览器端的内存泄漏会造成页面卡, 刷新能解决问题, 但是对于服务端这种不重启就一直运行的东西来说, 内存泄漏时间久了就down了, 重启的代价是很大的.

本文主要研究V8内存管理, 下面开始吧!

V8内存管理

V8对内存有限制, 64bit系统可以用1.4G, 32bit系统可以用0.7G 内存为什么要限制大小?

房子越大, 打扫起好耗费的时间越多, 1.4G内存, GC收集1次要1秒以上

通过process.memoryUsage()可以看到V8内存结构

GM3fTU.png

我们可以看到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垃圾回收机制

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是怎么清除它的呢?

GM34kF.png

之前我们说过V8回收垃圾基于新生代, 老生代的机制, 老生代可能是新生代中存久了的变量, 也可能是新生代存不下的变量(大于16M), 那么我们上面写的代码明显是被存在新生代中, 从上图中我们可以看到新生代会被存储空间分为2个相等的空间: fromto, 新生代的垃圾清除相当于: 将活的对象放入to, 将死的对象留在from中, 引用的结构是一个树结构, 广度优先的一层一层扫描那些活跃的对象, 活跃对象都扫描完之后就格式化from, 最后交换fromto,如果交换5次发现这个对象还活跃, 那么就把它移动到老生代中.

新生代什么时候移动到老生代?

新生代空间存不下的, 或者在新生代中一直活跃的会被移到老生代中, 老生代执行GC很慢, 1.4G收集1次要1秒以上, 这时候系统很卡.

移动到老生代后使用哪种算法?

新生代转到老生代中如果内存够, 就采用mark-sweep算法管理, 就是当清除垃圾的时候, 直接就清掉了,不会移动内存块, 这样会碎片, 浪费内存, 但是速度快

新生代转到老生代中如果内存不够, 就采用mark-compact算法管理, 它会将活跃的对象往左移动, 不活跃的往右边移动, 最后清除这些不活跃的, 但是明显这样速度慢, 所以V8常用的是mark-sweep算法来管理老生代.

老生代的优化是怎样的?

V8在对老生代回收时候有优化, 将一次GC收集分解撑多个小收集, 也就是一个大暂停变成了多个小暂停, 不至于让系统一直卡着不动.

总结

以上就是V8内存收集的原理, 主要是3种算法, 新生代使用scavenge, 老生代看情况使用mark-compact和mark-sweep, 我们在写代码时候避免闭包的滥用, 避免对不被需要的变量保持引用, 才能让GC顺利的进行垃圾收集.