内存分类与回收

2020 M12 26

js中将内存划分为栈内存和堆内存。
栈内存除了存储基本类型和引用类型的堆内存地址外,还可以作为代码执行环境。
堆内存存储引用类型的值,如对象的属性名和属性值,函数体中的代码字符串。

内存开辟

//开辟栈内存,存储变量like,值为coding
var like="coding";
//开辟堆内存,存储对象的属性名 name 和属性值 冷月心
//开辟栈内存,存储变量obj,值为其堆内存地址
var obj={name:"冷月心"}
//开辟堆内存,存储函数体中的代码字符串 console.log(123)
//开辟栈内存,存储变量fn,值为其堆内存地址
function fn(){console.log(123)}

栈内存回收

全局作用域和私有作用域,都属于栈内存。 全局作用域在页面关闭时销毁。 函数执行产生私有作用域,一般而言,函数执行完对应的内存被销毁。 如果外部有变量保持其引用,则不销毁(闭包)。
function log(){alert(1)}
log()//执行完正常销毁
function f1(){return function(){}}
//f1 执行返回一个引用类型值,该值被外部变量f2接收,f1不销毁
var f2=f1();
//dom注册事件监听
//该函数被dom对象上的属性onclick接收,保持其引用,不销毁
dom.onclick=function(){}
//F形成的作用域不会立即销毁,执行后销毁
function F(){ return function(){}}
F()()

堆内存回收

代码示例

var obj={name:"冷月心"}
//外部有变量保持着对该堆内存的引用,这部分内存不会销毁
//手动去掉或更改引用,使得该堆内存无外部引用,js引擎会在空闲的时候将这部分内存回收
obj=null
obj=1;

v8堆内存空间分类

以chrome浏览器中使用的js引擎v8为例,v8将堆内存分成新生代内存和老生代内存。 新生代内存存放生存时间短的对象, 老生代内存中存放生存时间长的对象。 它们最终都会交给js的垃圾回收器处理,其中主垃圾回收器主要负责老生代的垃圾回收, 副垃圾回收器主要负责新生代的垃圾回收。

v8堆内存垃圾回收机制

不论什么类型的垃圾回收器,它们都有一套共同的执行流程。 首先标记空间中活动对象和非活动对象(可回收的对象)。 然后对被标记为非活动对象所占据的内存进行回收(其实就是标记清除法)。 一般而言,频繁回收对象后,内存中就会存在大量不连续空间,这些不连续的内存空间被称为内存碎片。 如果不做整理,即使剩余的非连续空间满足分配要求,也无法分配一个大的连续的内存空间。 所以最后需要进行内存整理。

v8新生代Scavenge算法和对象晋升策略

新生代中用Scavenge算法来处理。把新生代空间一分为二,一半是对象区域,一半是空闲区域。 在进行垃圾回收时,除了标记清除外,副垃圾回收器还会完成存活对象到空闲区域的复制转移和对象的有序排列。 这个有序排列过程就相当于完成了内存整理,不会出现内存碎片的情况。 最后,将对象区域与空闲区域进行翻转,循环利用,这样就完成了垃圾对象的回收操作。 为了保证复制转移效率,新生代内存往往会设置的很小,这也意味着新生代内存空间容易被填满。 如此,出现了对象晋升策略这一概念,经过两次垃圾回收依然还存活的对象,会被移动到老生区中。

全停顿和增量标记算法

实际上,js引擎的垃圾回收是和主线程执行的脚本相冲突的,不能同时进行。 一旦执行垃圾回收算法,正在执行的js脚本必须暂停,待垃圾回收完毕后再恢复执行, 这种行为叫做全停顿。由此,增量标记算法横空而出,它可以把一个完整的垃圾回收任务拆分为很多小的任务, 可理解为时间分片。执行时间比较短的小任务可与js逻辑交替进行,这样就不会有明显的卡顿感知。

推荐阅读

https://time.geekbang.org/column/intro/216

https://juejin.cn/post/6844903993420840967