虽然前端有垃圾回收机制,但当某块无用的内存,却无法被垃圾回收机制认为是垃圾时,也就发生内存泄漏了
而垃圾回收机制通常是使用标志清除策略,简单说,也就是引用从根节点开始是否可达来判定是否是垃圾。

一、浏览器的垃圾回收机制

1.垃圾回收算法

  • 引用计数法

    • 原理:机制就是跟踪一个值的引用次数,当声明一个变量并将一个引用类型赋值给该变量时该值引用次数加1,当这个变量指向其他一个时该值的引用次数便减一。当该值引用次数为0时就会被回收
    • 问题:该方式会引起内存泄漏的原因是它不能解决循环引用的问题,一般现代浏览器都不使用改种方式(IE6)
      1. var a={};
      2. var b={};
      3. a.prop = b;
      4. b.prop = a;
  • 标记清除法(Mark-and-sweep)

    • 大部分浏览器以此方式进行垃圾回收,当变量进入执行环境(函数中声明变量)的时候,垃圾回收器将其标记为“进入环境”,当变量离开环境的时候(函数执行结束)将其标记为“离开环境”,在离开环境之后还有的变量则是需要被删除的变量。

image.png

2.GC根:

一般指全局且不会被垃圾回收的对象,比如:window、document或者是页面上存在的dom元素。JavaScript的垃圾回收算法会判断某块对象内存是否是GC根可达(存在一条由GC根对象到该对象的引用),任何无法从根到达的对象都会被 GC 回收。

demo.webp


3. 内存支配

image.png

  • 节点 1 支配节点 2
  • 节点 2 支配节点 3 、4 和 6
  • 节点 3 支配节点 5
  • 节点 5 支配节点 8
  • 节点 6 支配节点 7

二、内存泄漏的常见场景

1. 意外的全局变量

  1. function foo(arg) {
  2. bar = "this is a hidden global variable with a large of data";
  3. }
  4. // 等价于
  5. function foo(arg) {
  6. window.bar = "this is an explicit global variable with a large of data";
  7. }
  8. function foo() {
  9. this.variable = "potential accidental global";
  10. }
  11. foo();
  12. //等价于
  13. function foo(arg) {
  14. window.bar = "this is a explicit global variable with a large of data";
  15. }
  16. // 之前哲文代码review的时候探讨问过这样的写法
  17. (function(w,$){
  18. // doSomething
  19. })(window, jQuery)
  20. //1.显示注明了依赖,可维护性大大提高
  21. //2.避免了闭包直接引用全局外部变量

函数里引用全局变量的问题,需要尽量避免,一般我们都可以使用严格模式来写代码,还有eslint来保证,所以现在知道了eslint的重要性。

2.遗忘的定时器

setTimeout 和 setInterval 是由浏览器专门线程来维护它的生命周期,所以当在某个页面使用了定时器,当该页面销毁时,没有手动去释放清理这些定时器的话,那么这些定时器还是存活着的。也就是说,定时器的生命周期并不挂靠在页面上,所以当在当前页面的 js 里通过定时器注册了某个回调函数,而该回调函数内又持有当前页面某个变量或某些 DOM 元素时,就会导致即使页面销毁了,由于定时器持有该页面部分引用而造成页面无法正常被回收,从而导致内存泄漏了。

  1. // 5秒拉一次数据
  2. var serverData = new Array(1000000).join('*')
  3. setInterval(function() {
  4. var renderer = document.getElementById('renderer');
  5. if(renderer) {
  6. renderer.innerHTML = JSON.stringify(serverData);
  7. }
  8. }, 5000);

3.使用不当的闭包

函数本身会持有它定义时所在的词法环境的引用,但通常情况下,使用完函数后,该函数所申请的内存都会被回收了
但当函数内再返回一个函数时,由于返回的函数持有外部函数的词法环境,而返回的函数又被其他生命周期东西所持有,导致外部函数虽然执行完了,但内存却无法被回收

建议:在传递给console.log的对象是不能被垃圾回收 ♻️,因为在代码运行之后需要在开发工具能查看对象信息。所以最好不要在生产环境中console.log任何对象。

4.遗漏的 DOM 元素

DOM 元素的生命周期正常是取决于是否挂载在 DOM 树上,当从 DOM 树上移除时,也就可以被销毁回收了
但如果某个 DOM 元素,在 js 中也持有它的引用时,那么它的生命周期就由 js 和是否在 DOM 树上两者决定了,记得移除时,两个地方都需要去清理才能正常回收它

2.内存泄漏的判断方法

  1. 通过性能监控工具排查(判断有无)
    1. performace monitor
    2. performance memory视图
  • Performance面板记录性能时,勾选memory即可在分析结果中看到 memory 占用情况

js内存泄漏分析 - 图4

js内存泄漏分析 - 图5

  1. Devtools Memory 面板(精细排查)
    js内存泄漏分析 - 图6