Skip to content

对于内存释放的例子,感觉不是很好理解 #7

Closed
@aogg

Description

@aogg

对于内存释放的例子,感觉不是很好理解(每次都是一个例子)。不知道下面理解是否正确?
运行时添加 -expose-gc的node命令行参数。

https://github.com/ElemeFE/node-interview/blob/master/sections/js-basic.md#内存释放

// 错误
var theThing = null;
var replaceThing = function () {
    let 泄漏变量 = theThing;
    let unused = function () {
        if (泄漏变量)
            console.log("hi")
    };
    // 不断修改引用
    theThing = {
        longStr: new Array(1000000).join('*'),
        someMethod: function () {
            console.log('a')
        }
    };

    global.gc();
    // 每次输出的值会越来越大
    console.log(process.memoryUsage().heapUsed);
};
setInterval(replaceThing, 100);

正确修改1

// 正确
var theThing = null;
var replaceThing = function () {
    let unused = function () {
        if (theThing)
            console.log("hi")
    };
    // 不断修改引用
    theThing = {
        longStr: new Array(1000000).join('*'),
        someMethod: function () {
            console.log('a')
        }
    };

    global.gc();
    // 每次输出的值会保持不变
    console.log(process.memoryUsage().heapUsed);
};
setInterval(replaceThing, 100);

正确修改2

// 正确
var theThing = null;
var replaceThing = function () {
    let 泄漏变量 = theThing;
    let unused = function () {
        if (泄漏变量)
            console.log("hi")
    };
    // 不断修改引用
    theThing = {
        longStr: new Array(1000000).join('*'),
        someMethod: function () {
            console.log('a')
        }
    };

    泄漏变量 = null;
    // unused = null; // 不行匿名函数依然存在

    global.gc();
    // 每次输出的值会保持不变
    console.log(process.memoryUsage().heapUsed);
};
setInterval(replaceThing, 100);

Activity

hyj1991

hyj1991 commented on Mar 1, 2017

@hyj1991

这个其实你从这方面理解:在V8当前版本中,闭包对象是当前作用域中的所有内部函数作用域共享的,并且这个当前作用域的闭包对象中除了包含一条指向上一层作用域闭包对象的引用外,其余的存储的变量引用一定是当前作用域中的所有内部函数作用域中使用到的变量

这样就可以解释泄漏的原因了:在 replaceThing 定义的函数作用域中,由于 unused 表达式定义的函数使用到了 泄漏变量,因此 泄漏变量replaceThing 函数作用域的闭包对象持有了,从而导致 theThing 对象中的 someMethod 函数隐式地持有了 泄漏变量 的引用。

这样就造成了 theThing->someMethod->泄漏变量->上一次theThing->... 的循环引用,因此产生了内存泄漏,而且 thThing. longStr 又是一个大字符串,所以每次访问后内存上升明显。

其实这里要解决也比较简单:一种就是你的第二种修改方式,手动在每次调用完成后把 泄漏变量 = null;,另一种是干掉 unused 函数和 泄漏变量 间的闭包引用,这样 someMethodreplaceThing 定义的函数作用域生成的闭包对象中就不会有 泄漏变量了,也就没有内存泄漏了。

aogg

aogg commented on Mar 1, 2017

@aogg
Author

@hyj1991 学到了,没留意到theThing.someMethod函数

根据你上面说的第二种取消必包引用,补充如下:(不知理解对不)

var theThing = '';
var replaceThing = function () {
    let 泄漏变量 = !!theThing;
    let unused = function () {
        if (泄漏变量)
            console.log("hi")
    };

    theThing = {
        longStr: new Array(1000000).join('*'),
        someMethod: function () {
            console.log('a')
        }
    };

    global.gc();
    console.log(process.memoryUsage().heapUsed);
};
setInterval(replaceThing, 100);

但是这样theThing.someMethod方法依然占用两个变量(但不会内存上升),是不是应该每次都要在最后调用

    泄漏变量 = null;
    unused = null;

1、这样看来是不是应该减少匿名函数使用,或有什么更好的方法?
2、如果是面向对象,都用类会不会就好点,将定义外部变量和定义类分开(减少副作用)?

然后不断的学习,又发现一个解决方案:(利用let的块级作用域)

var theThing = '';
var replaceThing = function () {
    {
        let 泄漏变量 = theThing; // 如果是var则依然会内存上升
        let unused = function () {
            if (泄漏变量)
                console.log("hi")
        };
    }

    // 不断修改引用
    theThing = {
        longStr: new Array(1000000).join('*'),
        someMethod: function () {
            console.log('a')
        }
    };

    global.gc();
    console.log(process.memoryUsage().heapUsed);
};
setInterval(replaceThing, 100);
Lellansin

Lellansin commented on Mar 1, 2017

@Lellansin
Contributor

Hi, 还可以:

let unused = function (内部变量) {
    if (内部变量)
        console.log("hi")
};

这样 immutable 一下貌似也可以了

hyj1991

hyj1991 commented on Mar 2, 2017

@hyj1991

@aogg ,你的两种修改方式其实和提问的本意有一些区别了。
首先你的第一种方式,其实相当于断掉了 泄漏变量theThing 的引用,所以不会泄露。我当时的意思其实是这样处理:

let unused = function (泄漏变量) {
    if (泄漏变量)
        console.log("hi")
};

这样 unused 表达式和外部的 泄漏变量 的闭包引用切断了,那么同一个作用域下的 someMethod 方法持有的指向 replaceThing 函数作用域的闭包对象中就不会有 泄露变量 了,从而一次interval中的引用链 theThing->someMethod 就结束了,内存不会泄露。

至于你写的第二种处理方式,是因为let的块级作用域,导致整个 replaceThing 中的作用域结构完全变掉了,即:

{
        let 泄漏变量 = theThing; // 如果是var则依然会内存上升
        let unused = function () {
            if (泄漏变量)
                console.log("hi")
        };
    }

这个一整个部分和 someMethod 的函数作用域共享同一个 replaceThing 函数作用域生成的闭包对象了,这里显然 unusedsomeMethod 不会再共享同一个闭包对象了,所以也不会泄露。

aogg

aogg commented on Mar 3, 2017

@aogg
Author

@hyj1991 对,应该通过传参来减少函数对外部变量的依赖。好像js对这种处理方式用的会比较多,再补充个完整的:

var theThing = '';
var replaceThing = function () {
    let 泄漏变量 = theThing;
    let unusedSync = function (泄漏变量) { // 同步执行
        if (泄漏变量)
            console.log("hii")
    };
    unusedSync(泄漏变量);
    
    
    let unused = 泄漏变量 => function () { // 异步执行,就多一层
        if (泄漏变量)
            console.log("hi")
    };
    setTimeout(unused(泄漏变量), 0);

    theThing = {
        longStr: new Array(1000000).join('*'),
        someMethod: function () {
            console.log('a')
        }
    };


    global.gc();
    console.log(process.memoryUsage().heapUsed);
};
setInterval(replaceThing, 100);
gjc9620

gjc9620 commented on Mar 3, 2017

@gjc9620

@hyj1991 hyj1991 也许可以这样? 但是为什么这样可以?

// 错误
var theThing = null;
var replaceThing = function () {
  let 泄漏变量 = theThing;
  let unused = function () {
    if (泄漏变量)
      console.log("hi")
  };
  theThing = theThing ||  {}
  theThing.longStr = new Array(1000000).join('*');
  theThing.someMethod = function () {
    // console.log('a')
  };
  // 不断修改引用
  // theThing = {
  //   // longStr: new Array(1000000).join('*')
  //   // ,
  //   someMethod: function () {
  //     // console.log('a')
  //   }
  // };
  
  global.gc();
  // 每次输出的值会越来越大
  console.log(process.memoryUsage().heapUsed);
};
setInterval(replaceThing, 100);

hyj1991

hyj1991 commented on Mar 6, 2017

@hyj1991

@gjc9620 你这个问题我觉得可以分两个方面讨论下:

  • 看到的回收一部分是由于:直接被持有无法释放的是 new Array().join("*") 后的字符串,所以 new Array() 得到的临时数组对象是可以被释放的。

  • 回收的另一核心部分:这里就算把 new Array().join("*") 改为 new Array() 后,即直接对每次生成的数组对象直接引用,但是按照你的修改方式后依旧能回收的原因在于你写的 thething = theing || {} ,实际上例子中的泄漏产生的原因是 thething 每次指向一个新生成的对象,并且这个对象中包含一个函数 someMethod 又指向上次循环前的 thething,导致的内存泄漏。而你修改后,只有第一次会生成新对象,后面的 thething 其实一直指向的是自己,那么就相当于每次循环只做了 thething 对象里面属性的重新赋值而已,自然重新赋值后之前的 new Array() 对象或者 new Array().join() 字符串就能被回收了,也就不会产生内存泄漏~

有误之处还请指正

gjc9620

gjc9620 commented on Mar 6, 2017

@gjc9620

@hyj1991
感谢你的回答

  • 实际上例子中的泄漏产生的原因是 thething 每次指向一个新生成的对象,并且这个对象中包含一个函数 someMethod 又指向上次循环前的 thething,导致的内存泄漏
    someMethod 又指向上次循环前的 thething 怎么理解? 每次都是新的对象好像和前几次的循环没有关系?请指教

  • 从而导致 theThing 对象中的 someMethod 函数隐式地持有了 泄漏变量 的引用。
    为什么someMethod会隐式持有呢?他只是很简单的函数并没有用到任何闭包的变量呀

gjc9620

gjc9620 commented on Mar 6, 2017

@gjc9620

@hyj1991

  let unused = function () {
    if (泄漏变量)
      console.log("hi")
  };

话说回来这个unused并没有使用 不会被GC清除吗?这个问题似乎很复杂和GC如何执行有关

hyj1991

hyj1991 commented on Mar 6, 2017

@hyj1991

@gjc9620
第一个问题,本来 theThing 每次tick时指向新对象,那么 theThing 指向的老对象因为没人持有它的引用了应该会被回收,但是这里恰恰因为 thething->新对象->someMethod->泄漏变量->老thething引用->老对象 的链式引用关系 导致老对象永远被持有所以无法释放掉。至于 someMethod 为什么会引用到 泄漏变量 就是你的第二个问题了。

第二个问题:你可以仔细看看我的第一个回答:实际上同一个作用域生成的闭包对象是被该作用域中所有下一级作用域共同持有的,正是因为 unused 使用到了 泄漏变量,所以导致 replaceThing这一级的函数作用域中的闭包对象包含了 泄漏变量 ,而 theThing 持有了 someMethodsomeMethod 定义的函数作用域由持有上述的闭包对象,所以虽然在 someMethod 中 根本没有使用到 泄漏变量,也会隐式地持有。

hyj1991

hyj1991 commented on Mar 6, 2017

@hyj1991

@gjc9620
至于你说的和GC机制有关,怎么回收肯定是GC机制,但是这里 泄漏变量 无法释放其实也是因为有对象一直在引用它,所以GC在这个问题上没有什么特殊处理的部分

aogg

aogg commented on Mar 6, 2017

@aogg
Author

@hyj1991 其实 隐式地持有变量显式地持有变量 会导致什么或者说有什么区别?

这个内存泄漏问题是因为同时存在两个函数与一个不断变化引用的新对象变量,其中两个函数一个显式持有一个隐式持有(满足这三个条件就会导致内存泄漏)。
而上面所有解决方案都是为了减少这个三个条件中的一个。假设新变量的逻辑不改,那就只有从两个函数入手,可为什么去掉unused函数的 显式持有变量 就可以解决内存泄漏这个问题呢?它还不是在 隐式地持有变量 吗?
难道当没有显式地持有变量时变量就会被回收吗?这种文档哪里有?

gjc9620

gjc9620 commented on Mar 6, 2017

@gjc9620

@hyj1991
请问隐式地持有变量 有相关引用或者代码吗 想了解一下相关

hyj1991

hyj1991 commented on Mar 6, 2017

@hyj1991

隐式还是显式只是一个说法罢了,实际上就是js闭包对象除了保持一条指向上一级作用域闭包对象的引用外,还会包含所有下面作一级域使用到的本作用域定义的变量,看这段吧:

In V8, once there is any closure in the context, the context will be attached to every function, even for those who don’t reference the context at all

gjc9620

gjc9620 commented on Mar 6, 2017

@gjc9620

@hyj1991 谢谢 请问文章出处?

11 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @Lellansin@aogg@gjc9620@guohaoyun@hyj1991

        Issue actions

          对于内存释放的例子,感觉不是很好理解 · Issue #7 · ElemeFE/node-interview