blog icon indicating copy to clipboard operation
blog copied to clipboard

Javascript GC杂谈

Open HXWfromDJTU opened this issue 4 years ago • 0 comments

前言

Javascript是一门具有垃圾回收机制的编程语言,程序员大部分情况下不用手动去操心内存管理的问题。

EvemtLoop类似,虽然我们不能直接地去操控垃圾清理的执行与停止,但充分了解了V8的垃圾回收机制后吗,在编码上能够有意识地去较少GC的影响。相当于武林高手虽然绑上了手脚,但仍然能够识破别人的招式,以守为攻。

本文主要分为两个部分,① 先了解V8与内存的关系,② 再延展开去了解V8对内存的管理(清理)。

V8 与内存

V8的初始内存限制

  • V8启动后只能够使用物理机的部分内存,(64位系统下1.4GB,32位系统下0.7GB)
  • V8创始之初是设计为新一代Chrome的内核,作为浏览器所需要的内存当然就不需要太大,这从系统安全浏览器本身需求两个角度去考虑的。
  • 基于V8自身的垃圾回收机制,若分配的内存过大,单次的垃圾回收时间则会过长,这是在服务端Node.js和前端浏览器都不可以接受的。

内存占用情况

  • heapTotalheapUsed 分别代表已申请到的堆内存大小,和当前的使用量。
  • 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流,则需要更大的内存,常见的有BufferAPI。

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 markinglazy swaeeping

为了减低全堆垃圾回收带来的全停顿V8从标记到清除也都进行了改进,使得原本需要长时间的GC工作,得以分段实行。

常见内存问题

内存泄漏

  • 闭包 与 全局变量 闭包的原理这里不再赘述,当闭包的引用挂载在全局下,而闭包本身没有被释放的情况下,则也可能会引起内存泄漏。
  • DOM节点内存泄漏 只有同时满足 DOM 树和 JavaScript 代码都不引用某个 DOM 节点,该节点才会被作为垃圾进行回收。
  • 定时器、事件使用后未移除 定时器 和 时间监听基本是JavaScript的常用工具,但常常是只记得用,不记得销毁。也可能导致内存泄漏。

内存膨胀

  • 缓存的使用 前端开发时,经常使用内存将计算过程进行,缓存的变量又绑定在顶级属性上,也就导致了长期存活的对象越来越多。特别是在Node.js服务端端开发时,尽可能减少内存进行的使用。

频繁垃圾回收

  • 使用大量的临时空间,导致垃圾回收频繁触发。

如何对应避免内存泄漏

V8

  1. 坏习惯之王:定时器事件监听 要记得及时清除。
  2. 对象之间尽量减少交叉,可以达到拆分对象,不创造大生命周期对象的效果。

Node.js环境下

  1. Node.js中尽量使用 streambuffer来操作大文件,而不是使用内存
  2. 慎用内存当做缓存
    不要像页面开发一样,想当然地大量使用内存保存临时变量,使用场景允许的情况下,使用外部有着完整过期机制的缓存工具,来缓存大数据对象。

主动释放

赋值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

HXWfromDJTU avatar Aug 05 '20 17:08 HXWfromDJTU