从垃圾回收机制来分析内存泄漏

一、V8引擎简介

V8引擎是由Google开发的开源JavaScript引擎,以C++开发,初始版本发布于2008年9月2日,官网v8.dev。Chrome浏览器和NodeJs都是基于V8引擎。

二、V8引擎垃圾回收机制
2.1 垃圾回收

  • 垃圾回收的英文是Garbage Collection,简写GC。
  • 现代Javascript引擎,如V8引擎,会动态的为程序管理分配内存,无需开发人员处理。引擎会定期检查分配给应用程序的内存,辨别其中已经不再需要的数据,将其清除以释放空间,这个过程称为垃圾回收。
  • Chrome浏览器中,单个Javascript虚拟机实例能使用的内存被限制为最大1.4GB(32位系统则为0.7GB),垃圾回收是非常必要的,垃圾回收异常(如内存溢出)时,会导致内存占用升高,严重时会导致内存溢出浏览器崩溃。
  • 垃圾回收力求回收更多空间的同时还要确保只释放不再被需要的数据,这个过程还是比较耗费资源的,一个处理不好就会导致卡顿、甚至故障。好在V8引擎已经为我们做了足够多的工作,它在合适的时机触发垃圾回收,运用巧妙高效的垃圾回收策略,使得浏览器能够以60FPS的速度向用户呈现流畅快速的页面。

2.2 垃圾回收时机
2.2.1 无成本回收
https://v8.dev/blog/free-garbage-collection

  • Chrome中除了JavaScript引擎V8,还有一个渲染引擎Blink。Blink渲染引擎中包含任务调度程序,它会统一管理浏览器的整体繁忙程度和需要执行的任务,为了使Chrome保持敏捷的响应速度,Blink渲染引擎会对这些任务进行优先级排序,优先执行对延迟比较敏感的任务。
  • Chrome浏览器在渲染页面时,会以60FPS(每秒60帧)的速度刷新屏幕,折算下来留给浏览器的每一帧渲染时间约为16.6毫秒,如果浏览器在不到16.6毫秒的时间内完成了所有工作,那么在开始下一帧渲染之前就进入了空闲状态。空闲状态下Chrome会调度V8引擎执行一些低优先级的所谓空闲任务,其中就包含垃圾回收任务。
  • 空闲任务是特殊的低优先级任务,调度程序会在浏览器空闲状态下执行它们。空闲任务有一个截止时间,如果浏览器正在渲染动画,这个截止时间是渲染下个帧的预计开始时间,如果页面没有新的渲染任务,截止时间会长一些,但不会超过50毫秒的上限,以确保浏览器能及时响应用户事件。
    无成本回收即是指利用浏览器的空闲时间来执行垃圾回收,它会尽量降低垃圾回收对用户体验的影响。

2.2.2 垃圾回收策略

  • Javascript中,数据类型分为简单类型和引用类型两类,简单类型存放在栈(stack)中,引用类型存放在堆(heap)中。指向引用类型的的指针也存放在栈中。

  • 栈中的简单类型占用固定大小的内存空间,由系统在栈操作时自动分配和释放,而堆中的对象数据由于大小不固定,系统无法自动释放,需要Javascript引擎来处理。

  • V8引擎使用分代垃圾回收器(generational garbage collector),将堆中的内存分为新生代(young generation)和老生代(old generation),新生代存放新的、较小的对象,老生代存放旧的对象。

  • 事实上,V8堆中的内存区不止新生代内存区和老生代内存区,还有代码区、大对象区等,但内存回收主要发生在新生代和老生代,其他内存区篇幅所限不再展开。

2.2.3 新生代 Scavenger算法

  • 新生代使用半空间(semi-space)策略,意味着新生代内存空间中总有一半是空的,新对象一开始被分配到活动空间(From-Space)中,另一半为非活动空间(To-Space),当活动空间变满的时候,会使用Scavenger算法将存活的对象疏散(复制)到非活动空间中,疏散完毕后会将非活动空间激活为活动空间,同时清除原先活动空间中的所有数据并将其变为非活动空间。
  • 新生代的存储空间是比较小的,仅32MB(32位系统上更是只有16MB),由于半空间策略,实际能使用的空间只有16MB(32位系统仅8MB)。仅凭上述策略,这点内存很快就会被耗尽,所以在疏散存活对象到另一半空间时,已经被疏散过一次的对象会被晋升,存储到老生代当中去。

小结:由于新产生的变量中大部分其实都是会被很快回收,所以新生代设计的内存空间较小,而且非常适合使用Scavenger算法。
空间较小:更容易触发回收,并且每次回收要处理的数据量不大,利于利用零散的空闲状态来处理,对流畅性的影响非常小
Scavenger算法:大部分新变量都会被很快回收,仅需要疏散存活的对象,性能高效

  • 写屏障(write barriers)
    如果新生代中的一个对象被老生代中的对象引用了,要遍历整个老生代内存空间吗?

为了保证新生代垃圾回收效率,V8使用写屏障来维护了一个从老生代到新生代的引用列表。在执行Scavenger算法进行新生代垃圾回收时,V8通过写屏障可以快速了解老生代对新生代中的哪些对象有引用,从而避免遍历整个老生代内存空间。

2.2.4 老生代 Mark-Sweep & Mark-Compact算法

  • 老生代的内存空间比较大,一次GC所需花费的成本也比新生代要大得多,不过其GC频率相对新生代也低得多。
  • 老生代采用标记清除(Mark-Sweep)算法和标记压缩(Mark-Compact)算法,GC过程分为三步:
    marking,标记堆中的存活对象;
    sweeping,清除堆中的非存活对象;
    compacting,清理内存碎片,压缩空间;

Marking
类似于其他开发语言中的引用计数算法,V8通过对象的可访问性(reachablility)来判断堆中的的对象是否存活,并将其打上标记。GC时从一组已知的根集对象(root set,包含栈、全局对象)开始,跟踪遍历对象中的指针,根据指针找到对象后将其标记为可访问,如此递归下去,直到把所有被引用的对象都遍历完毕。

Sweeping
清除过程,V8将非存活对象留下的内存空间添加到称为空闲空间列表(free-list)的数据结构中。当下次需要分配内存时,只需要通过这个空闲空间列表就可以快速找到空闲的内存空间。

Compact

  • 经过Sweeping清理过后的内存空间,其中的空闲空间是非常零散的,称为内存碎片,不利于后续的空间利用。类似pc上的硬盘碎片整理,V8也需要对这些内存空间进行整理压缩,将零散的空间碎片合并成一个大的空间。
  • 由于移动大对象的成本比较大,所以V8只会对高度碎片化的内存分页(V8将堆内存划分为固定大小的内存分页)进行整理压缩

小结:与新生代相反,老生代的数据量较大,而且老生代中的数据大部分会继续存活,所以Mark-Sweeping算法只处理非存活的对象。

V8中GC项目的代号为Orinoco,它使用了很多优秀的技术,除了上面有讲到的,还有一些并行、并发、增量等技术。

三、通过快照分析问题
Chrome自带内存分析工具,按F12打开开发者工具,切换到内存面板。

  • 抓个快照
  • 在类过滤器中输入depached可以滤出已经被分离出DOM树但是尚未被内存回收的DOM对象。
  • 点击展开这个DOM数组,然后鼠标悬浮到占用内存最高的DOM对象上,可以看到没有被回收的 DOM。
  • 点击DOM对象,会在下方的对象保留器中显示该对象被哪些对象所引用。保留器会自动根据内存占用大小对这些对象倒序排序,并且会自动展开第一个对象,层层追溯,直到根集对象。
  • 通过分析引用路径,分析代码层定位并处理。

四、避免内存泄漏手段

  • 执行中的代码只保存必要的数据。代码执行过程中,一旦数据不再需要,应该及时将其赋值为null,这个做法叫做解除引用(dereferencing)。
  • 提供可供外部调用的变量清空方法。模块/组件中的数据如果需要提供给其他模块/组件使用,最好也同时提供数据清理方法,便于外部不再需要时可将其清理掉。let
    leakArray = []; export function clear() { leakArray = []; }
  • 业务不需要用到的内部函数,可以重构到函数外,实现解除闭包。
  • 避免滥用闭包。
  • 避免创建过多的生命周期较长的对象,或者将对象分解成多个子对象。
  • 注意清除定时器和事件监听器。
  • 注意清除DOM引用。
  • 利用弱引用。ES6中为了解决内存泄漏问题新增了两个数据结构WeakMap和WeakSet。GC过程判断对象可访问性时不会将弱引用考虑进去,只要对象没有被其他引用所引用,就会被回收释放内存。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值