Closed
Description
对于内存释放的例子,感觉不是很好理解(每次都是一个例子)。不知道下面理解是否正确?
运行时添加 -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 commentedon Mar 1, 2017
这个其实你从这方面理解:在V8当前版本中,闭包对象是当前作用域中的所有内部函数作用域共享的,并且这个当前作用域的闭包对象中除了包含一条指向上一层作用域闭包对象的引用外,其余的存储的变量引用一定是当前作用域中的所有内部函数作用域中使用到的变量。
这样就可以解释泄漏的原因了:在
replaceThing
定义的函数作用域中,由于unused
表达式定义的函数使用到了泄漏变量
,因此泄漏变量
被replaceThing
函数作用域的闭包对象持有了,从而导致theThing
对象中的someMethod
函数隐式地持有了泄漏变量
的引用。这样就造成了
theThing->someMethod->泄漏变量->上一次theThing->...
的循环引用,因此产生了内存泄漏,而且thThing. longStr
又是一个大字符串,所以每次访问后内存上升明显。其实这里要解决也比较简单:一种就是你的第二种修改方式,手动在每次调用完成后把
泄漏变量 = null;
,另一种是干掉unused
函数和泄漏变量
间的闭包引用,这样someMethod
和replaceThing
定义的函数作用域生成的闭包对象中就不会有泄漏变量
了,也就没有内存泄漏了。aogg commentedon Mar 1, 2017
@hyj1991 学到了,没留意到theThing.someMethod函数
根据你上面说的第二种取消必包引用,补充如下:(不知理解对不)
但是这样theThing.someMethod方法依然占用两个变量(但不会内存上升),是不是应该每次都要在最后调用
1、这样看来是不是应该减少匿名函数使用,或有什么更好的方法?
2、如果是面向对象,都用类会不会就好点,将定义外部变量和定义类分开(减少副作用)?
然后不断的学习,又发现一个解决方案:(利用let的块级作用域)
Lellansin commentedon Mar 1, 2017
Hi, 还可以:
这样 immutable 一下貌似也可以了
hyj1991 commentedon Mar 2, 2017
@aogg ,你的两种修改方式其实和提问的本意有一些区别了。
首先你的第一种方式,其实相当于断掉了
泄漏变量
和theThing
的引用,所以不会泄露。我当时的意思其实是这样处理:这样
unused
表达式和外部的泄漏变量
的闭包引用切断了,那么同一个作用域下的someMethod
方法持有的指向replaceThing
函数作用域的闭包对象中就不会有泄露变量
了,从而一次interval中的引用链theThing->someMethod
就结束了,内存不会泄露。至于你写的第二种处理方式,是因为let的块级作用域,导致整个
replaceThing
中的作用域结构完全变掉了,即:这个一整个部分和
someMethod
的函数作用域共享同一个replaceThing
函数作用域生成的闭包对象了,这里显然unused
和someMethod
不会再共享同一个闭包对象了,所以也不会泄露。aogg commentedon Mar 3, 2017
@hyj1991 对,应该通过传参来减少函数对外部变量的依赖。好像js对这种处理方式用的会比较多,再补充个完整的:
gjc9620 commentedon Mar 3, 2017
@hyj1991 hyj1991 也许可以这样? 但是为什么这样可以?
hyj1991 commentedon Mar 6, 2017
@gjc9620 你这个问题我觉得可以分两个方面讨论下:
看到的回收一部分是由于:直接被持有无法释放的是
new Array().join("*")
后的字符串,所以new Array()
得到的临时数组对象是可以被释放的。回收的另一核心部分:这里就算把
new Array().join("*")
改为new Array()
后,即直接对每次生成的数组对象直接引用,但是按照你的修改方式后依旧能回收的原因在于你写的thething = theing || {}
,实际上例子中的泄漏产生的原因是thething
每次指向一个新生成的对象,并且这个对象中包含一个函数someMethod
又指向上次循环前的thething
,导致的内存泄漏。而你修改后,只有第一次会生成新对象,后面的thething
其实一直指向的是自己,那么就相当于每次循环只做了thething
对象里面属性的重新赋值而已,自然重新赋值后之前的new Array()
对象或者new Array().join()
字符串就能被回收了,也就不会产生内存泄漏~有误之处还请指正
gjc9620 commentedon Mar 6, 2017
@hyj1991
感谢你的回答
实际上例子中的泄漏产生的原因是 thething 每次指向一个新生成的对象,并且这个对象中包含一个函数 someMethod 又指向上次循环前的 thething,导致的内存泄漏
someMethod 又指向上次循环前的 thething 怎么理解? 每次都是新的对象好像和前几次的循环没有关系?请指教
从而导致 theThing 对象中的 someMethod 函数隐式地持有了 泄漏变量 的引用。
为什么someMethod会隐式持有呢?他只是很简单的函数并没有用到任何闭包的变量呀
gjc9620 commentedon Mar 6, 2017
@hyj1991
话说回来这个unused并没有使用 不会被GC清除吗?这个问题似乎很复杂和GC如何执行有关
hyj1991 commentedon Mar 6, 2017
@gjc9620
第一个问题,本来
theThing
每次tick时指向新对象,那么theThing
指向的老对象因为没人持有它的引用了应该会被回收,但是这里恰恰因为thething->新对象->someMethod->泄漏变量->老thething引用->老对象
的链式引用关系 导致老对象永远被持有所以无法释放掉。至于someMethod
为什么会引用到泄漏变量
就是你的第二个问题了。第二个问题:你可以仔细看看我的第一个回答:实际上同一个作用域生成的闭包对象是被该作用域中所有下一级作用域共同持有的,正是因为
unused
使用到了泄漏变量
,所以导致replaceThing
这一级的函数作用域中的闭包对象包含了泄漏变量
,而theThing
持有了someMethod
,someMethod
定义的函数作用域由持有上述的闭包对象,所以虽然在someMethod
中 根本没有使用到泄漏变量
,也会隐式地持有。hyj1991 commentedon Mar 6, 2017
@gjc9620
至于你说的和GC机制有关,怎么回收肯定是GC机制,但是这里
泄漏变量
无法释放其实也是因为有对象一直在引用它,所以GC在这个问题上没有什么特殊处理的部分aogg commentedon Mar 6, 2017
@hyj1991 其实 隐式地持有变量 和 显式地持有变量 会导致什么或者说有什么区别?
这个内存泄漏问题是因为同时存在两个函数与一个不断变化引用的新对象变量,其中两个函数一个显式持有一个隐式持有(满足这三个条件就会导致内存泄漏)。
而上面所有解决方案都是为了减少这个三个条件中的一个。假设新变量的逻辑不改,那就只有从两个函数入手,可为什么去掉
unused
函数的 显式持有变量 就可以解决内存泄漏这个问题呢?它还不是在 隐式地持有变量 吗?难道当没有显式地持有变量时变量就会被回收吗?这种文档哪里有?
gjc9620 commentedon Mar 6, 2017
@hyj1991
请问隐式地持有变量 有相关引用或者代码吗 想了解一下相关
hyj1991 commentedon Mar 6, 2017
隐式还是显式只是一个说法罢了,实际上就是js闭包对象除了保持一条指向上一级作用域闭包对象的引用外,还会包含所有下面作一级域使用到的本作用域定义的变量,看这段吧:
gjc9620 commentedon Mar 6, 2017
@hyj1991 谢谢 请问文章出处?
11 remaining items