【本周主题】第三期 - JavaScript 内存机制

时间:2022-06-16
本文章向大家介绍【本周主题】第三期 - JavaScript 内存机制,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

一、js中的内存空间(堆和栈是啥)?

以下用一段代码说明堆和栈的区别:

栈(Stack)空间:

后进先出结构

早高峰的电梯,挤满了人,先进去的要想出来,后进去的是不是要先出来让路?就是这个道理吧。。。

这样,要获取其中一个,是不是很费性能。

存放的数据类型:

String、Number、Boolean、Null、Undefined 这五种基础数据类型。

拷贝这些类型的数据就是拷贝一个副本

以及:

Object、Array、Function等引用类型的指针。

拷贝这些类型的数据是拷贝了指针一个副本,新指针和原指针还是指向堆内存里的同一个地址。

栈跟队列区分:

队列是先进先出结构,它两边都有口。就像去火车站排队买票。第一个人先排队的,业务员第一个接待他。(业务员就是js主线程)

堆(Heap)空间:

树状结构

可以随时获取,就像书架上的书,也像苹果树上的每一个苹果,想摘那个摘哪个。就可以省点力气(不像栈,想摘最高的那个,还得把最底下的摘完才能摘。。。)

存放的数据类型:

Object、Array、Function等引用类型、闭包的变量。

池(常量池):

一般归类到栈中,存放常量。

二、stack的三种理解方式:

和学到这里的你一样,我经常搞不懂栈和调用栈到底是真么关系?栈空间和栈内存又是不是一回事儿?

直到我拜读了阮一峰老师的博文,才渐渐清醒起来。

这里记录自己的读书笔记。想观摩原作的请跳转至:Stack的三种含义

“栈”在不同的空间就有不同的含义,主要有三种含义:

a、数据结构 - 后进先出 || Last in,First out || LIFO

作为一种数据的存放方式,特点是后进先出。

上边电梯的例子在这里不够更加形象,

可以想象成一摞叠在一起的一元硬币,假如一共十个,想要拿出最下边那个,计算机的处理方式就是,把上边九个依次拿开,

才能把第十个拿出来给你(不要想一起把前九个抬起来拿最后一个的想法,那是你不是计算机。。。)。

这种做法,在js中有些方法组合使用就是了: push + pop:从最后边依次推进去,再从最后边拿。上边的1就是push(),2就是pop()的做法了。

b、代码运行方式 - 调用栈/执行栈(call stack)、出栈、入栈

调用栈,每一个函数在调用的时候,都堆叠到一起(和上边的硬币一样),需要把上边的先调用了,然后销毁、出栈之后,再把下边的暴露出来进行。

比如下边这段代码的执行:

这里,foo()和bar()【包括window在js代码】运行时,js引擎就会生成执行上下文(就是咱们常说的函数作用域了),这一段理解了,就能绕过很多面试题的障碍。

后期会整理到这里。等不及的你赶紧去看看吧。

c、内存空间 - 栈内存

 存放数据的一种内存区域。

系统划分出来的两个内存空间:栈和堆

区别如上所说,

栈有结构,所以要按次序存放,也可以知道每个区的大小,超过大小就是栈溢出错误。一个线程分配一个stack,线程独占。运行结束栈被清空

堆没有结构,可以任意存放,大小也不能确定,可以按需要增加。每个进程分配一个heap,线程共用。运行结束对象实例继续存在,直到垃圾回收。如果没被回收,就是内存泄漏。

(stack的寻址速度快于heap?)

三、内存声明周期

1、内存分配

就是我们声明一个变量、对象等的时候,系统就会自动给我们的变量分配内存。

比如执行下边的代码:

var a = 10;

事实上,编译器会这么处理:(节选自《你不知道的js(上)》第一章 1.2.2  p7)

遇到 var a,编译器会先像作用域(因为这里是在全局作用域执行的,你可以理解为window)寻找是否已经有一个a存在同一个作用域集合(window对象)中。

如果有a,则忽略该声明,继续进行编译。

否则没有a,会要求作用域在当前作用域集合(即window对象)中声明一个新的变量,并命名为a。这个过程,就是内存分配。

2、内存使用

就是编译器读、写内存,调取变量/对象等的值的时候。

读就是获取变量值,写入就是赋值或修改变量的值。这里引入两个《你不知道的js(上)》介绍的名词

比如:

console.log(a)

引擎在这里会有两段RHS查找(得到某某的值):

1、查找console

2、查找a的值

首先,console这个对象是在window对象上的一个属性。log是他上边的一个方法。

 然后查找a的值,因为a被创建到了栈内存,对a的取值就是内存使用

3、内存回收 && 内存释放

内存回收:

当我们使用完一个函数,该函数就会被自动销毁。(不考虑闭包的情况)

js中有垃圾回收机制,会自动回收不再使用的内存。

内存释放:

var a = null;//使用完毕,自动释放内存空间

四、垃圾回收 (内存回收)

1、垃圾回收机制

很多语言,在使用完毕后需要程序员手动释放内存,我们应该庆幸的是,js引擎有自动的垃圾回收机制。可以自动进行内存管理,减轻了我们的工作负担。

自动垃圾收集机制:

垃圾收集器每隔固定的时间就执行一次检查,不再使用的值就释放其占用的内存。

那么问题是,垃圾回收机制怎么知道哪些内存需要回收了呢?

2、垃圾回收算法

a. 引用计数

b. 标记清除

引用计数 方法: 原理,就是引擎记录所有对象值的引用次数,如果引用次数是0,就表示没用了可以“删除”

那什么样才算没有引用了呢? 比如这里js文件中只有一行代码:

var a = [123,2];

你说a有引用吗? 我第一感觉是没有的,但是看阮一峰大神的讲解,这里是还有引用的。 数组还在占用内存,变量a是一个引用。所以数组[123,2]这个变量值的引用次数是1,不能被清除

要想解除他的引用,需要执行 a = null;这样,a变成了一个null值,而数组[123,2]没有人引用他了,下一轮垃圾清理器过来的时候就会把他清除了。

所以,这么看来,我们写完程序后还要检查有哪些值是不再使用的,就给他指向null,剪断引用的线,释放内存空间。 尤其是在全局作用域。因为局部函数作用域有可能在没有闭包的情况下,函数执行完毕就会被自动消除。但是全局的变量,系统很难判断还有没有用,就像上边的a一样,所以我们除了避免使用全局变量外,还有记得及时释放不得已建立的全局变量。

但是他也有他的缺点,我上边列了脑图。 出现循环引用的情况:

var div = document.createElement("div");
div.onclick = function() {
  console.log("click");
};

变量div有事件处理函数的引用,同时事件处理函数也有div的引用!(div变量可在函数内被访问)。一个循序引用出现了,按ie中用的引用计数算法,该部分内存无可避免地泄露了。

扩展: ie8中,COM对象,用c++实现的组件对象模型,使用的就是引用计数方法。常常因为循环引用发生内存泄漏

标记清除 方法:(常用) 原理:对象是否可达。否,则被回收 从window全局对象根对象开始遍历,定期向下查找,找所有从根开始引用的对象、这些对象引用的对象。然后就知道哪些是可达到的,哪些是不可达到的(我的理解是和其他人没有联系的) 能达到的添加标识,最后没有标识的就会被内存回收,并且将之前的标记清除,下一次重新标记

这样,在循环引用的情况中,即使二者彼此互帮互助循环引用防止垃圾清除,但是,标记清除法则从根元素开始找,找不到他俩,他俩就都被清除了。

五、内存泄漏

2018-12-07 23:40:48 

前文说道,如果咱们建立的变量对象在不使用时没有及时被回收,就会造成内存泄漏。

内存泄漏究竟是个啥?是不是油箱被戳了个洞漏了?

就是动态分配的空间,在使用完毕后没有被释放,就会导致该内存空间一直被白白占用,直到程序结束。 危害: 想想,电脑的空间就那么多,一直有人“占着茅坑不拉*”,内存空间就会越来越少,程序运行就会越来越慢,造成程序的卡顿效果,严重的甚至还可能导致系统的崩溃。

具体表现整理如下:(参见搜狗百科) 1、cpu资源耗尽

鼠标键盘等操作无反应、页面假死

2、进程耗尽 没有新的进程可以用了,放到浏览器里有Browser进程、插件进程、GPU进程、内核渲染进程等,如果进程耗尽,其中想开一个是不是就不可能了。

3、硬盘耗尽 机器崩溃

4、内存泄漏或者内存耗尽 很麻烦而且不好用工具定位和跟踪 - 隐式内存泄漏

内存泄漏的分类:

常发性 偶发性 一次性 隐式: 说说这个和我们前端有关系的隐式内存泄漏,就是程序自动给我们的变量分配了内存空间,但是直到程序结束,他们才被释放。虽然程序结束,释放了所有内存,但是如果长时期的不结束这段程序,内存也就不能及时释放。

六、项目中造成你内存泄漏的几种情况

高级前端进阶公众号文章阅读笔记

目录:

1、意外的全局变量

2、被遗忘的定时器或回调函数

3、脱离DOM的引用

4、闭包

1、意外的全局变量

在函数作用域中,未使用var定义的变量会在全局创建一个新的全局变量:

function foo(){
  a = 2;
 var b = '局部变量'
}

此时,a没有使用var定义,就会在全局对象中创建一个a属性。

或者,在普通函数中使用this时,也就在全局对象window上创建一个属性:

function bar(){
  this.a = 2;
}

这种做法和上边定义那个a没什么区别(当然,在不使用new bar构造函数的情况下),this指向window(非严格模式下);

因为如果使用严格模式,this指向的是undefiend;

解决方法就是: 尽量变量都定义在局部函数作用域中,并且记得使用var等变量声明一下。

另外,如果不是使用构造函数,普通函数内部也记得使用this的时候,js的文件头部加上"use strict"字样,表示使用严格模式编译。

如果必须使用全局变量,那么要确保使用完以后将该变量指向null或重定义。

2、被遗忘的计时器或回调函数

定时器setInterval

var a = fun();
setInterval(function(){
  var node = document.getElementById('node');
  if(node){
     node.innerHTML = 'test';
  }
},1000);

上例:节点node或数据不需要时,定时器setInterval依然指向这些数据,所以即使node节点被溢出,interval仍旧存活并且垃圾回收期没法回收。

解决是终止定时器。

这种循环定时器,是一定要设置关闭条件,然后将其clear并且将timer指向null

3、脱离DOM的引用:

如果把DOM存成字典(键值对)或者数组,此时,同样的dom元素存在两个引用:

一个在DOM树中,另一个在字典中,那么将来需要把两个引用都清除。

比如:

var elements ={
  btn: document.getElementById('button'),
  img: document.getElementById('image'),
};
function doS(){
 elements.img.src = 'test/img.png';
 elements.btn.click();
}
function removeBtn(){
  document.body.removeChild(document.getElementById('button'));
}
//此时,仍旧存在一个全局的#button的引用,
//elements字典,btn元素仍旧在内存中,不能被回收

如果代码中保存了表格某一个<td>的引用,将来决定删除整个表格的时候,,你会认为回收器会回收除了已保存的<td>以外的其他节点?

事实上:<td>是表格的子节点,子元素和父元素是引用关系,由于代码保留了<td>的应用

导致整个表格仍待在内存中,所以保存DOM元素引用的时候要小心谨慎。

4、闭包

闭包的关键是匿名函数可以访问父级作用域的变量。

我们知道,函数在调用完毕之后,会被抛出执行栈进行销毁,且函数内部的局部变量也就不存在的。

但是如果有闭包的存在,函数被抛出执行栈以后,由于闭包内部引用了父级函数作用域内部的局部变量,

这些变量就不会被销毁,而是继续占据着内存空间,严重时造成泄漏。这是闭包的特性,但也是他的缺点。

同样的,可以不用的时候指向null

七、性能优化 ——管理内存

怎么避免/处理内存泄漏?

 (高程3)一旦确定数据不再使用,可以手动将其值设置为null来释放其引用。 —— 解除引用。

此做法适用于全局变量和全局对象的属性。因为局部变量大多会在他们离开执行环境时自动被解除引用。

 其他的对照第六点中的对应情况寻找对应解决方法吧。