Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >javascript中的内存管理

javascript中的内存管理

原创
作者头像
程序那些事
修改于 2021-03-02 02:01:29
修改于 2021-03-02 02:01:29
59900
代码可运行
举报
文章被收录于专栏:程序那些事程序那些事
运行总次数:0
代码可运行

简介

在c语言中,我们需要手动分配和释放对象的内存,但是在java中,所有的内存管理都交给了java虚拟机,程序员不需要在手动进程内存的分配和释放,大大的减少了程序编写的难度。

同样的,在javascript中,内存管理也是自动进行的,虽然有自动的内存管理措施,但是这并不意味着程序员就不需要关心内存管理了。

本文将会进行详细的介绍javascript中的内存管理策略。

内存生命周期

对于任何程序来说,内存的生命周期通常都是一样的。

可以分为三步:

  1. 在可用空间分配内存
  2. 使用该内存空间
  3. 在使用完毕之后,释放该内存空间

所有的程序都需要手动执行第二步,对于javascript来说,第1,3两步是隐式实现的。

我们看下javascript中分配内存空间的例子。

通过初始化分配内存空间:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var n = 123; // 为数字分配内存
var s = 'azerty'; // 为String分配内存

var o = {
  a: 1,
  b: null
}; // 为对象分配内存

// 为数组分配内存
var a = [1, null, 'abra']; 

function f(a) {
  return a + 2;
} // 为函数分配内存

通过函数调用分配内存空间:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var d = new Date(); // 通过new分配date对象

var e = document.createElement('div'); // 分配一个DOM对象

var s = 'azerty';
var s2 = s.substr(0, 3); // 因为js中字符串是不可变的,所以substr的操作将会创建新的字符串

var a = ['ouais ouais', 'nan nan'];
var a2 = ['generation', 'nan nan'];
var a3 = a.concat(a2); 
// 同样的,concat操作也会创建新的字符串

释放空间最难的部分就是需要判断空间什么时候不再被使用。在javascript中这个操作是由GC垃圾回收器来执行的。

垃圾回收器的作用就是在对象不再被使用的时候进行回收。

JS中的垃圾回收器

判断一个对象是否可以被回收的一个非常重要的标准就是引用。

如果一个对象被另外一个对象所引用,那么这个对象肯定是不能够被回收的。

引用计数垃圾回收算法

引用计数垃圾回收算法是一种比较简单和简洁的垃圾回收算法。他把对象是否能够被回收转换成了对象是否仍然被其他对象所引用。

如果对象没有被引用,那么这个对象就是可以被垃圾回收的。

我们举一个引用计数的例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var x = { 
  a: {
    b: 2
  }
}; 
//我们创建了两个对象,a对象和a外面用大括号创建的对象。
// 我们将大括号创建的对象引用赋值给了x变量,所以x拥有大括号创建对象的引用,该对象不能够被回收。
// 同时,因为a对象是创建在大括号对象内部的,所以大括号对象默认拥有a对象的引用
// 因为两个对象都有引用,所以都不能够被垃圾回收

var y = x;  //我们将x赋值给y,大括号对象现在拥有两个引用

x = 1;   // 我们将1赋值给x,这样只有y引用了大括号的对象

var z = y.a;  // 将y中的a对象引用赋值给z,a对象拥有两个引用

y = 'flydean';  // 重新赋值给y,大括号对象的引用数为0,大括号对象可以被回收了,但是因为其内部的a对象还有一个z在被引用
                // 所以暂时不能被回收

z = null;       // z引用也被重新赋值,a对象的引用数为0,两个对象都可以被回收了

引用计数的一个缺点就是可能会出现循环引用的情况。

考虑下面的一个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function f() {
  var x = {};
  var y = {};
  x.a = y;        // x references y
  y.a = x;        // y references x

  return 'flydean';
}

f();

在上面的例子中,x中的a属性引用了y。而y中的a属性又引用了x。

从而导致循环引用的情况,最终导致内存泄露。

在实际的应用中,IE6 和IE7 对DOM对象使用的就是引用计数的垃圾回收算法,所以可能会出现内存泄露的情况。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var div;
window.onload = function() {
  div = document.getElementById('myDivElement');
  div.circularReference = div;
  div.lotsOfData = new Array(10000).join('*');
};

上面的例子中,DOM中的myDivElement元素使用circularReference引用了他本身,如果在引用计数的情况下,myDivElement是不会被回收的。

当myDivElement中包含了大量的数据的时候,即使myDivElement从DOM tree中删除了,myDivElement也不会被垃圾回收,从而导致内存泄露。

Mark-and-sweep回收算法

讲到这里,大家是不是觉得JS的垃圾回收算法和java中的很类似,java中也有引用计数和mark-and-sweep清除算法。

这种回收算法的判断标准是对象不可达。

在javascript中,通过扫描root对象(JS中的root对象那些全局对象),然后找到这些root对象的引用对象,然后再找到这些被引用对象的引用对象,一层一层的往后查找。

最后垃圾回收器会找到所有的可达的对象和不可达的对象。

使用不可达来标记不再被使用的对象可以有效的解决引用计数法中出现的循环引用的问题。

事实上,现在基本上所有的现代浏览器都支持Mark-and-sweep回收算法。

调试内存问题

如果发送了内存泄露,我们该怎么调试和发现这个问题呢?

在nodejs中我们可以添加–inspect,然后借助Chrome Debugger来完成这个工作:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
node --expose-gc --inspect index.js

上面的代码将会开启nodejs的调试功能。

我们看下输出结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Debugger listening on ws://127.0.0.1:9229/88c23ae3-9081-41cd-98b0-d0f7ebceab5a
For help, see: https://nodejs.org/en/docs/inspector

结果告诉了我们两件事情,第一件事情就是debugger监听的端口。默认情况下将会开启127.0.0.1的9229端口。并且分配了一个唯一的UUID以供区分。

第二件事情就是告诉我们nodejs使用的调试器是Inspector。

使用Chrome devTools进行调试的前提是我们已经开启了 –inspect模式。

在chrome中输入chrome://inspect:

我们可看到chrome inspect的界面,如果你本地已经有开启inspect的nodejs程序的话,在Remote Target中就可以直接看到。

选中你要调试的target,点击inspect,即可开启Chrome devTools调试工具:

你可以对程序进行profile,也可以进行调试。

闭包Closures中的内存泄露

所谓闭包就是指函数中的函数,内部函数可以访问外部函数的参数或者变量,从而导致外部函数内部变量的引用。

我们看一个简单闭包的例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 function parentFunction(paramA)
 {
 var a = paramA;
 function childFunction()
 {
 return a + 2;
 }
 return childFunction();
 }

上面的例子中,childFunction引用了parentFunction的变量a。只要childFunction还在被使用,a就无法被释放,从而导致parentFunction无法被垃圾回收。事实上Closure默认就包含了对父function的引用。

我们看下面的例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 <html>
 <body>
 <script type="text/javascript">
 document.write("Program to illustrate memory leak via closure");
 window.onload=function outerFunction(){
 var obj = document.getElementById("element");
 obj.onclick=function innerFunction(){
 alert("Hi! I will leak");
 };
 obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
 // This is used to make the leak significant
 };
 </script>
 <button id="element">Click Me</button>
 </body>
 </html>

上面的例子中,obj引用了 DOM 对象element,而element的onclick是outerFunction的内部函数,从而导致了对外部函数的引用,从而引用了obj。

这样最终导致循环引用,造成内存泄露。

怎么解决这个问题呢?

一个简单的办法就是在使用完obj之后,将其赋值为null,从而中断循环引用的关系:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 <html>
 <body>
 <script type="text/javascript">
 document.write("Avoiding memory leak via closure by breaking the circular
 reference");
 window.onload=function outerFunction(){
 var obj = document.getElementById("element");
 obj.onclick=function innerFunction()
 {
 alert("Hi! I have avoided the leak");
 // Some logic here
 };
 obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
 obj = null; //This breaks the circular reference
 };
 </script>
 <button id="element">"Click Here"</button>
 </body>
 </html>

还有一种很简洁的办法就是不要使用闭包,将其分成两个独立的函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 <html>
 <head>
 <script type="text/javascript">
 document.write("Avoid leaks by avoiding closures!");
 window.onload=function()
 {
 var obj = document.getElementById("element");
 obj.onclick = doesNotLeak;
 }
 function doesNotLeak()
 {
 //Your Logic here
 alert("Hi! I have avoided the leak");
 }
 </script>
 </head>
 <body>
 <button id="element">"Click Here"</button>
 </body>
 </html>

本文作者:flydean程序那些事 本文链接:http://www.flydean.com/js-memory-management/ 本文来源:flydean的博客 欢迎关注我的公众号:「程序那些事」最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
线性代数--MIT18.06(十五)
投影我们已经知道它的定义了,那么我们为什么要投影呢?这就和我们之前的章节联系起来了,对于
fireWang
2019/03/13
4150
线性代数--MIT18.06(十六)
对于求解最佳的拟合直线,我们自然是希望直线离三个点的距离之和是最小的,这个距离实际上就是
fireWang
2019/03/13
5390
线性代数--MIT18.06(十四)
在之前的章节中我们讨论了四个子空间各自的性质,包括他们的维数,构成他们的基,他们对于线性方程组的解释性和可解性,对于实际问题的应用性。
fireWang
2019/03/13
8180
线性代数--MIT18.06(二十四)
马尔科夫矩阵的稳态问题就是有关特征值为 1 的对应特征向量,并且其他的特征值的绝对值都是小于 1 (可有其他特征值也为 1 的例外)。为什么呢?
fireWang
2019/04/24
6640
线性代数--MIT18.06(二十四)
线性代数--MIT18.06(二十五)
是秩 1 矩阵,因此秩为 1 ,也就说明在零空间是二维平面,即有两个特征值为 0 ,根据迹即为特征值相加之和,即可得到另一个特征值为 1 。其特征向量就是
fireWang
2019/03/13
7110
线性代数--MIT18.06(三十)
奇异值分解(SVD,singular value decomposition),也是对矩阵进行分解,但是和特征分解不同,SVD 并不要求要分解的矩阵为方阵。假设我们的矩阵
fireWang
2019/04/24
3970
线性代数--MIT18.06(三十)
线性代数--MIT18.06(三十二)
会有很多的数据冗余,并且数据量太大,系统会无法承载,数据的传输也是一个很大的问题。因此,会对图像进行压缩,常用的图像压缩技术有 JPEG,本质上就是基变换,也就是使用更好的基来重现图像。
fireWang
2019/04/24
3640
线性代数--MIT18.06(三十二)
线性代数--MIT18.06(三)
,我们依然可以使用矩阵消元的形式来求解,只不过要比我们之前提到的矩阵消元多做一些消元而已,这就是Gauss-Jordan法。
fireWang
2019/03/13
6810
线性代数--MIT18.06(三十二)
会有很多的数据冗余,并且数据量太大,系统会无法承载,数据的传输也是一个很大的问题。因此,会对图像进行压缩,常用的图像压缩技术有 JPEG,本质上就是基变换,也就是使用更好的基来重现图像。
fireWang
2019/03/13
5980
线性代数--MIT18.06(一)
从行的角度来看,三个三元一次方程表示三维空间中的三个平面,如果三个平面相交于一点,那么交点的坐标即为方程组的解。
fireWang
2019/03/13
1.1K0
线性代数--MIT18.06(三十五)
行不满秩,因此其不满秩,那么它不可能为正定矩阵,可以为半正定矩阵。 于是我们也就知道
fireWang
2019/03/13
6540
线性代数--MIT18.06(三十)
奇异值分解(SVD,singular value decomposition),也是对矩阵进行分解,但是和特征分解不同,SVD 并不要求要分解的矩阵为方阵。假设我们的矩阵
fireWang
2019/03/13
4150
线性代数--MIT18.06(二十四)
马尔科夫矩阵的稳态问题就是有关特征值为 1 的对应特征向量,并且其他的特征值的绝对值都是小于 1 (可有其他特征值也为 1 的例外)。为什么呢?
fireWang
2019/03/13
9430
线性代数--MIT18.06(三十三)
对应于课本(Introduction to Linear Algebra)第六章内容的习题课
fireWang
2019/03/13
8060
线性代数--MIT18.06(三十一)
考虑空间中的所有向量,都需要做线性变换,我们不可能对向量一个一个进行变换,然后得到变换后的空间。 此时就可以利用空间的基,我们对空间的一组基都得到它们变换后的结果,那么对于空间中的任意向量,因为我们都可以用基向量来将其表示出来,那么对任意向量的线性变换,都可以用基向量的线性变换的线性组合来表示,即对于空间的一组基
fireWang
2019/03/13
9650
线性代数--MIT18.06(二十七)
傅里叶矩阵(Fourier Matrix)是一个特殊的复矩阵,同时也是一个酉矩阵。它来源于傅里叶转换,其矩阵的特殊性质,通过矩阵分解,可以将计算量从
fireWang
2019/03/13
9410
线性代数--MIT18.06(三)
,我们依然可以使用矩阵消元的形式来求解,只不过要比我们之前提到的矩阵消元多做一些消元而已,这就是Gauss-Jordan法。
fireWang
2019/03/13
6710
线性代数--MIT18.06(三十四)
上图既包含了四个基本子空间的性质以及相对关系,还给出了伪逆的解释,伪逆就是列空间到行空间的映射(投影)。
fireWang
2019/03/13
7720
线性代数--MIT18.06(九)
上述两个描述是等价的,为什么呢?实际上我们可以将第一个定义的描述写成矩阵形式,就是
fireWang
2019/03/13
6060
线性代数--MIT18.06(二十三)
在上一讲我们已经介绍了特征值和特征向量的一种应用,那就是求解差分方程,这一讲,讲解其另一个应用——求解微分方程,当然,首先从一阶常系数微分方程开始讲解。
fireWang
2019/03/13
5620
相关推荐
线性代数--MIT18.06(十五)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验