1. 内存管理
像C语言这样的底层语言一般都有底层的内存管理接口,比如malloc()动态的分配内存空间
和free()释放动态分配的内存空间
。而JavaScript在创建变量(对象、字符串)时自动进行了内存分配,并且在不使用他们的时候自动进行释放。释放的过程称为垃圾回收。
1.1 为什么要关注内存
- 任何程序的运行都要分配运行空间
- 如果不再使用的内容得不到释放,不会返回到操作系统或空闲内存池,会导致内存泄漏
- 程序运行所需的内存空间大于当前的可用内存空间会引发内存溢出
- 内存溢出,程序占用的内存会越来越大,最终引起客户端卡顿,甚至无响应。
1.2 内存生命周期
不管什么程序语言,内存生命周期基本是一致的:
- 分配你所需要的内存
- 使用分配到的内存(进行读、写)
- 不需要时将内存进行释放
2. 内存分配
我们在开发过程中,很少意识到内存的分配,是因为JavaScript在定义变量时就完成了内存的分配。
2.1 JS数据类型
基础数据类型:String, Number, Boolean, Null, Undefined, Symbol
引用数据类型:Object
存放这些数据的内存又可以分为两部分:栈内存(Stack)和堆内存(Heap)。原始数据类型存在栈中,引用数据类型存在堆中。
2.2 栈内存
- 栈内存,只有一个口,内容存储类似于杯子中放入石块。先放入的石块在最下面,后放入的石块在最上面。
- 栈内存就是JavaScript中的调用栈,是用来存储执行上下文,以及存储执行上下文中的一些基本类型中的小数据。
- 特点:连续区域,容量较小,读取速度快。后入先出,读取栈底元素需要将上面的元素全部取出。
2.2.1 变量环境
存放var声明与函数声明的变量空间,编译时就能确定,不受块级作用域影响
2.2.2 词法环境
存放let与const声明的变量空间,编译时不能完全确定,受块级作用域影响
2.3 堆内存
- 引用数据类型,比如Object, Array,他们的大小不是固定的,所以是存在堆内存的。JS不允许直接操作堆内存,我们在操作对象时,操作的实际是对象的引用,而不是实际的对象。可以理解为对象在栈里面存了一个内存地址,这个地址指向了堆里面实际的对象。所以引用类型的值是一个指向堆内存的引用地址。
- 特点:不连续的内存区域,容量较大,读取速度慢(因为引用地址在堆中,多了一次中转,所以读取速度自然会比栈要慢)。随意读取,类似于图书馆书架上的书,喜欢哪本拿哪本。
3. 内存回收
3.1 栈内存回收
function fn1() {
function fn2() {
}
fn2()
}
fn1()
3.2 堆内存回收
内存垃圾回收领域中有个重要术语:代际假说,其有以下两个特点:
- 大部分对象在内存中存在的时间很短,很多对象一经分配内存,很快就变得不可访问
- 不死的对象,会活的更久
基于代际假说,JS把堆空间分成新生代和老生代两个区域,新生代中存放的是生存时间短的对象,通常只支持1~8M的容量;老生代中存放的是生存时间长的对象,一些大的数据也会被直接分配到老生区中。而针对这两个区域,JS存在两个垃圾回收器:主垃圾回收器和副垃圾回收器。他们之间的相同执行流程如下:
- 标记空间中活动对象和非活动对象
- 回收非活动对象所占据的内存
- 内存整理(可选)。有的垃圾回收器工作过程中会产生内存碎片,此时需要内存整理防止不够连续空间分配给大数据
3.2.1 副垃圾回收器
副垃圾回收器主要是采用 Scavenge 算法进行新生区的垃圾回收,它把新生区划分为两个区域:对象区域和空闲区域,新加入的对象都会存放到对象区域,当对象区域快被写满时,会对对象区域进行垃圾标记,把存活对象复制并有序排列至空闲区域,完成后让这两个区域角色互转,由此便能无限循环进行垃圾回收。同时存在对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,会被移动到老生区中。
3.2.2 主垃圾回收器
由于老生区空间大,数据大,所以不适用 Scavenge 算法,主要是采用标记-整理算法,其工作流程是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。接着让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。垃圾回收工作是需要占用主线程的,必须暂停JS脚本执行等待垃圾回收完成后恢复,这种行为称为全停顿。 由于老生代内存大,全停顿对性能的影响非常大,所以出现了增量标记的策略进行老生区的垃圾回收。
4. JS内存泄漏判定
- 本地打包一个去掉压缩、拥有sourcemap及没有任何console的生产版本(console会保留对象引用,阻碍销毁;去掉压缩和保留sourcemap有利于定位源码)
- 启动本地服务,打开控制台选择内存Memory,选择所需的快照场景,例如堆快照,点击左上角红色点点,拍快照
- 查看不同时间所查看的快照大小变化情况,conversation实例从443上升至1117,message实例从443上升至1287,而该用户实际只有221个会话
- 不断在会话间切换,通过timeline看到有内存不被释放,而且生成detached dom证明有内存泄漏存在
总结用法,希望可以帮助到你