从JS中的内存管理提及 —— JS中的弱引用

本文首发于公众号:符合预期的CoyPanjavascript

写在前面

在全部的编程语言中,咱们声明一个变量时,须要系统为咱们分配一块内存。当咱们再也不须要这个变量时,须要将内存进行回收(这个过程称之为垃圾回收)。在C语言中,有malloc和free来协助咱们进行内存管理。在JS中,开发者不须要手动进行内存管理,JS引擎会为咱们自动作这些事情。可是,这并不意味着咱们在使用JS进行编码时,不须要关心内存问题。java

JS中的内存分配与变量

内存声明周期以下:react

  1. 分配你所须要的内存
  2. 使用分配到的内存(读、写)
  3. 不须要时将其释放

在JS中,这三步都是对开发者无感的,不须要咱们过多的关心。面试

咱们须要注意的是,当咱们声明一个变量、获得一块内存时,须要正确区分一个变量究竟是一个基本类型仍是引用类型。算法

基本类型:String,Number,Boolean,Null,Undefined,Symbol编程

引用类型:Object,Array,Function数组

对于基本类型变量来讲,系统会为其分配一块内存,这块内存中保存的,就是变量的内容。浏览器

对于引用类型变量来讲,其存储的只是一个地址而已,这个地址指向的内存块才是是变量的真正内容。引用变量的赋值,也只是把地址进行传递(复制)。举个例子:markdown

// a 和 b 指向同一块内存
var a = [1,2,3];
var b = a;
a.push(4);
console.log(b); // [1,2,3,4]
复制代码

还有一点须要注意,JS中的函数传参,实际上是按值传递(按引用传递)。举个例子:react-router

// 函数f的入参,实际上是把 a 的值复制了一份。注意 a 是一个引用类型变量,其保存的是一个指向内存块的一个地址。
function f(obj) {
	obj.b = 1;
}
var a = { a : 1};
f(a);
console.log(a); // { a: 1, b: 1}
复制代码

在平时的开发中,彻底理解JS中变量的存储方式是十分重要的。对于我本身来讲,尽可能避免把引用类型变量处处传递,可能一不当心在某个地方修改了变量,另外一个地方逻辑没有判断好,很容易出Bug,特别是在项目复杂度较高,且多人开发时。这也是我比较喜欢使用纯函数的缘由。

另外,根据我以前的面试经验,有很多的小伙伴认为下面的代码会报错,这也是对JS中变量存储方式掌握不熟致使的。

// const 声明一个不可改变的变量。 
// a 存储的只是数组的内存地址而已,a.push 并不会改变 a 的值。
const a = [];
a.push('1'); 
console.log(a); // ['1']
复制代码

JS中的垃圾回收

垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象若是有访问另外一个对象的权限(隐式或者显式),叫作一个对象引用另外一个对象。例如,一个Javascript对象具备对它原型的引用(隐式引用)和对它属性的引用(显式引用)。

在这里,“对象”的概念不只特指 JavaScript 对象,还包括函数做用域(或者全局词法做用域)。当变量再也不须要时,JS引擎会把变量占用的内存进行回收。可是怎么界定【变量再也不须要】呢?主要有两种方法。

引用计数算法

把“对象是否再也不须要”简化定义为“对象有没有其余对象引用到它”。若是没有引用指向该对象(零引用),对象将被垃圾回收机制回收。MDN上的例子:

var o = { 
  a: {
    b:2
  }
}; 
// 两个对象被建立,一个做为另外一个的属性被引用,另外一个被分配给变量o
// 很显然,没有一个能够被垃圾收集


var o2 = o; // o2变量是第二个对“这个对象”的引用

o = 1;      // 如今,“这个对象”只有一个o2变量的引用了,“这个对象”的原始引用o已经没有

var oa = o2.a; // 引用“这个对象”的a属性
               // 如今,“这个对象”有两个引用了,一个是o2,一个是oa

o2 = "yo"; // 虽然最初的对象如今已是零引用了,能够被垃圾回收了
           // 可是它的属性a的对象还在被oa引用,因此还不能回收

oa = null; // a属性的那个对象如今也是零引用了
           // 它能够被垃圾回收了
复制代码

这种方法有一个局限性,那就是没法处理循环引用。在下面的例子中,两个对象被建立,并互相引用,造成了一个循环。它们被调用以后会离开函数做用域,因此它们已经没有用了,能够被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,因此它们不会被回收。

// 这种状况下,o和o2都没法被回收。
function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}
f();
复制代码

标记-清除算法

这个算法假定设置一个叫作根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将按期从根开始,找全部从根开始引用的对象,而后找这些对象引用的对象……从根开始,垃圾回收器将找到全部能够得到的对象和收集全部不能得到的对象。

关于JS中的垃圾回收算法,网上已经有不少的文章讲解,这里再也不进行赘述。

JS中的内存泄露

尽管JS为咱们自动处理内存的分配、回收问题,可是在某些特定的场景下,JS的垃圾回收算法并不能帮咱们去除已经再也不使用的内存。这种【因为疏忽或错误形成程序未能释放已经再也不使用的内存】的现象,被称做内存泄露。

内存占用愈来愈高,轻则影响系统性能,重则致使进程崩溃。

可能产生内存泄露的场景有很多,包括全局变量,DOM事件,定时器等等。

下面是一段存在内存泄露的示例代码:

class Page1 extends React.Component {

    events= []

    componentDidMount() {
        window.addEventListener('scroll', this.handleScroll.bind(this));
    }

    render() {
        return <div> <div><Link to={'/page2'}>前往Page2</Link></div> <p>page1</p> .... </div>
    }

    handleScroll(e) {
        this.events.push(e);
    }
}
复制代码

当咱们点击按钮跳转到Page2后,在page2不停进行滚动操做,咱们会发现内存占用不断的上涨:

产生这个内存泄露的缘由是:咱们在Page1被unmount的时候,尽管Page1被销毁了,可是Page1的滚动回调函数经过eventListener依然可“触达”,因此不会被垃圾回收。进入Page2后,滚动事件的逻辑依然生效,内部的变量没法被GC。若是用户在Page2进行长时间滑动等操做,页面会逐渐变得卡顿。

上述的例子,在咱们开发的过程当中,并很多见。不只仅是事件绑定,也有多是定时上报逻辑等等。如何解决呢?记得在unmount的时候,进行相应的取消操做便可。

在平时的项目开发中,内存泄露还有不少其余的场景。浏览器页面还好,毕竟一直开着某个页面的用户不算太多,刷新就好。而Node.js发生内存泄露的后果就比较严重了,可能服务就直接崩溃了。掌握JS的变量存储方式、内存管理机制,养成良好的编码习惯,能够帮助咱们减小内存泄露的发生。

JS中的弱引用

前面咱们讲到了JS的垃圾回收机制,若是咱们持有对一个对象的引用,那么这个对象就不会被垃圾回收。这里的引用,指的是强引用

在计算机程序设计中,还有一个弱引用的概念: 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并所以可能在任什么时候刻被回收。

在JS中,WeakMap 和 WeakSet 给咱们提供了弱引用的能力。

WeakMap 、WeakSet

要说WeakMap,先来讲一说Map。Map 对象保存键值对,而且可以记住键的原始插入顺序。任何值(对象或者原始值) 均可以做为一个键或一个值。

Map对对象是强引用:

const m = new Map();
let obj = { a: 1 };
m.set(obj, 'a');
obj = null; // 将obj置为null并不会使 { a: 1 } 被垃圾回收,由于还有map引用了 { a: 1 }
复制代码

WeakMap是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值能够是任意的。WeakMap是对对象的弱引用:

const wm = new WeakMap();
let obj = { b: 2 };
wm.set(obj, '2');
obj = null; // 将obj置为 null 后,尽管 wm 依然引用了{ b: 2 },可是因为是弱引用,{ b: 2 } 会在某一时刻被GC。
复制代码

正因为这样的弱引用,WeakMap 的 key 是不可枚举的 (没有方法能给出全部的 key)。若是key 是可枚举的话,其列表将会受垃圾回收机制的影响,从而获得不肯定的结果。

WeakSet能够视为 WeakMap 中全部值都是布尔值的一个特例,这里就再也不赘述了。

JavaScript 的 WeakMap 并不是真正意义上的弱引用:实际上,只要键仍然存活,它就强引用其内容。WeakMap 仅在键被垃圾回收以后,才弱引用它的内容。这种关系更准确地称为 ephemeron

WeakRef

WeakRef是一个更高级的API,它提供了真正的弱引用。咱们直接借助上文的内存泄露的例子来看一看WeakRef的效果:

import React from 'react';
import { Link } from 'react-router-dom';

// 使用WeakRef将回调函数“包裹”起来,造成对回调函数的弱引用。
function addWeakListener(listener) {
    const weakRef = new WeakRef(listener);
    const wrapper = e => {
        if (weakRef.deref()) {
            return weakRef.deref()(e);
        }
    }
    window.addEventListener('scroll', wrapper);
}

class Page1 extends React.Component {

    events= []

    componentDidMount() {
        addWeakListener(this.handleScroll.bind(this));
    }

    componentWillUnmount() {
        console.log(this.events);
    }

    render() {
        return <div> <div><Link to={'/page2'}>前往Page2</Link></div> <p>page1</p> .... </div>
    }

    handleScroll(e) {
        this.events.push(e);
    }
}


export default Page1;
复制代码

咱们再来看看点击按钮跳转到page2后的内存表现:

能够很直观的看到,在跳转到page2后,持续滚动一段时间后,内存平稳。这是由于随着page1被unmount,真正的滚动回调函数( Page1的 handleScroll 函数)被GC掉了。其内部的变量也最终被GC。

但其实,这里还有一个问题,虽然咱们经过weakRef.deref() 拿不到 handleScroll 滚动回调函数了(已被GC),可是咱们的包裹函数 wrapper 依然会执行。由于咱们没有执行removeEventListener。理想状况是:咱们但愿滚动监听函数也被取消掉。

能够借助FinalizationRegistry来实现这个功能。看下面的示例代码:

// FinalizationRegistry构造函数接受一个回调函数做为参数,返回一个示例。咱们把实例注册到某个对象上,当该对象被GC时,回调函数会触发。
const gListenersRegistry = new FinalizationRegistry(({ window, wrapper }) => {
    console.log('GC happen!!');
    window.removeEventListener('scroll', wrapper);
});

function addWeakListener(listener) {
    const weakRef = new WeakRef(listener);
    const wrapper = e => {
        console.log('scroll');
        if (weakRef.deref()) {
            return weakRef.deref()(e);
        }
    }
    // 新增这行代码,当listener被GC时,会触发回调函数。回调函数传参由咱们本身控制。
    gListenersRegistry.register(listener, { window, wrapper });
    window.addEventListener('scroll', wrapper);
}
复制代码

WeakRef 和 FinalizationRegistry 属于高级Api,在Chrome v84 和 Node.js 13.0.0 后开始支持。通常状况下不建议使用。由于容易用错,致使更多的问题。

写在后面

本文从JS中的内存管理讲起,说到了JS中的弱引用。虽然JS引擎帮咱们处理了内存管理问题,可是咱们在业务开发中并不能彻底忽视内存问题,特别是在Node.js的开发中。

关于V8的内存策略的更多细节,能够移步我以前翻译的一篇文章:

V8引擎的内存管理

参考资料:

一、www.youtube.com/watch?v=TPm…

二、www.infoq.cn/article/lKs…

三、developer.mozilla.org/zh-CN/docs/…

相关文章
相关标签/搜索