壹 ❀ 引

从事计算机相关技术工作的同学,对于内存空间相关概念多少有所耳闻,毕竟像我这种非计算机科班出身的人,对于栈堆,垃圾回收都能简单说道几句;当我明白JS 基本类型与引用类型数据存储方式不同,才对于为何要使用深拷贝恍然大悟。只是知道和深入了解是两码事,那么这篇文章从内存空间说起。

 贰 ❀ 栈、堆与队列

与c语言这种底层语言不同,JavaScript并没有提供内存管理的接口,而是在创建变量时自动分配内存,当变量不再需要使用时自动释放,也就是我们所常说的垃圾回收机制。

但不管是什么程序语言,内存的声明周期都满足以下三个阶段:

a.分配你需要的内存空间

b.使用分配到的内存(读、写)

c.不需要时将其释放或归还

大部分语言对于第二步是明确的,但对于JavaScript而言三步都是隐含的,也正是因如此才让JavaScript开发者产生了不用关心内存管理的错觉。

JavaScript内存空间分为栈,堆,池,队列。其中栈存放变量,基本类型数据与指向复杂类型数据的引用指针;堆存放复杂类型数据;池又称为常量池,用于存放常量;而队列在任务队列也会使用。我们一一细说。

1.栈数据结构

栈数据结构具备FILO(first in last out)先进后出的特性,较为经典的就是乒乓球盒结构,先放进去的乒乓球只能最后取出来。我在 一篇文章看懂JS执行上下文 这篇文章中有提到执行上下文栈,它用于存放js代码在执行过程中创建的所有上下文,同样也具备FILO的特性。

图片来源

在js中数据类型一般分类基本数据类型(Number Boolean Null Undefined String Symbol)与引用数据类型(Object Array Function ...),其中栈一般用于存放基本类型数据,例如以下代码在栈内存中分布:

var a = 1;
var b = a;
a = 2;

可以看到基本类型数据的变量名与值都存放在栈内存中,当我们将变量a复制给b时,栈会新开内存用于存放变量b,且当我们修改变量a时对变量b不会造成任何影响,因为a与b是互不相关的两份数据。

2.堆数据结构

堆数据结构是一种无序的树状结构,同时它还满足key-value键值对的存储方式;我们只用知道key名,就能通过key查找到对应的value。比较经典的就是书架存书的例子,我们知道书名,就可以找到对应的书籍。

图片来源

在js中堆内存一般用于存储引用类型的数据,需要注意的是由于引用类型的数据一般可以拓展,数据大小可变,所以存放在堆内存中;但对引用类型数据的引用地址是固定的,所以地址指向还是会存放在栈内存中。

我们通过内存图来模拟以下代码:

var a = [1,2,3];
var b = a;
a.push(4);

当我们创建数组a时,栈内存中只保存了变量a与指向堆内存中数组的地址指针,而当我们将a复制给变量b时,其实只是复制了一份地址指针,两者还是指向同一数组,无论谁修改,都会影响彼此。

这便是我们熟知的浅拷贝,若想对浅拷贝与深拷贝有更深了解,欢迎阅读博主 深拷贝与浅拷贝的区别,实现深拷贝的几种方法这篇文章。

3.队列

队列具有FIFO(First In First Out)先进先出的特性,与栈内存不同的是,栈内存只存在一个出口用于数据进栈出栈;而队列有一个入口与一个出口,理解队列一个较为实际的例子就像我们排队取餐,先排队的永远能先取到餐。

图片来源

在js中使用队列较为突出的就是js执行机制中的event loop事件循环,如果大家对于js事件执行机制有兴趣,可以阅读博主 JS执行机制详解,定时器时间间隔的真正含义 这篇文章,一定会让你有所收获。

 叁 ❀ 垃圾回收机制

我们在前面已经说到JS内存分配回收由计算机自动完成,同时也提到了垃圾回收机制这个概念,这里来细说。

1.js中的内存回收

在js中,垃圾回收器每隔一段时间就会找出那些不再使用的数据,并释放其所占用的内存空间。

以全局变量和局部变量来说,函数中的局部变量在函数执行结束后这些变量已经不再被需要,所以垃圾回收器会识别并释放它们。而对于全局变量,垃圾回收器很难判断这些变量什么时候才不被需要,所以尽量少使用全局变量。

2.垃圾回收的两种模式

那么垃圾回收器是如何检测变量是否需要的呢,大体上分为两种检测手段,引用计数与标记清除。

引用计数

引用计数的判断原理很简单,就是看一份数据是否还有指向它的引用,如果没有任何对象再指向它,那么垃圾回收器就会回收,举个例子:

// 创建一个对象,由变量o指向这个对象的两个属性
var o = {
name: '听风是风',
handsome: true
};
// name虽然设置为了null,但o依旧有name属性的引用
o.name = null;
var s = o;
// 我们修改并释放了o对于对象的引用,但变量s依旧存在引用
o = null;
// 变量s也不再引用,对象很快会被垃圾回收器释放
s = null;

引用计数存在一个很大的问题,就是对象间的循环引用,比如如下代码中,对象o1与o2相互引用,即便函数执行完毕,垃圾回收器通过引用计数也无法释放它们。

function f() {
var o1 = {};
var o2 = {};
o1.a = o2; // o1 引用 o2
o2.a = o1; // o2 引用 o1
return;
};
f();

标记清除

标记清除的概念也好理解,从根部出发看是否能达到某个对象,如果能达到则认定这个对象还被需要,如果无法达到,则释放它,这个过程大致分为三步:

a.垃圾回收器创建roots列表,roots通常是代码中保留引用的全局变量,在js中,我们一般认定全局对象window作为root,也就是所谓的根部。

b.从根部出发检查所有 的roots,所有的children也会被递归检查,能从root到达的都会被标记为active。

c.未被标记为active的数据被认定为不再需要,垃圾回收器开始释放它们。

当一个对象零引用时,我们从根部一定无法到达;但反过来,从根部无法到达的不一定是严格意义上的零引用,比如循环引用,所以标记清除要更优于引用计数。

从2012年起,所有现代浏览器都使用了标记清除垃圾回收算法,但老版本的IE6除外。

 肆 ❀ 如何避免内存泄漏

我们已经知道了垃圾回收的原理,那么我们如何避免创建无法回收的对象,以至造成内存泄漏的尴尬呢?下面说说常见的四种js内存泄漏。

1.全局变量

尽可能少的去创建全局变量是js开发者的常识,但如下两种方式还是会意外的创建全局变量,第一是在函数中声明变量未使用var:

function fn() {
a = 1;
};
fn();
window.a //

上述代码中我们在函数体内声明了一个变量a,由于未使用var声明,即便在函数体内,但它依旧是一个全局变量。我们知道全局变量等同于在window上添加属性,所以在函数执行完毕,我们依旧可以访问到它。

第二种是在函数体内通过this来创建变量:

function fn() {
this.a = 1;
};
fn();
window.a //

我们知道,当直接调用函数fn时,等同于window.fn(),所以函数体内的this会指向window,所以本质上还是创建了一个全局变量。

当然上述问题也不是无法解决,我们可以使用严格模式来避免这个问题,试着在代码头部添加‘use strict’,你会发现a就无法访问了,因为严格模式下,全局对象指向undefined。

有时候我们无法避免使用全局变量,那么记得在使用完毕后手动释放它们,例如让变量指向null。

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

var serverData = loadData();
setInterval(function () {
var renderer = document.getElementById('renderer');
if (renderer) {
renderer.innerHTML = JSON.stringify(serverData);
}
}, 3000);

在上述代码中,当dom元素renderer被移除时,由于是周期定时器的缘故,定时器回调函数始终无法被回收,这也导致了定时器会一直对数据serverData保持引用,好的做法是在不需要时停止定时器。

在例如我们在使用事件监听时,如果不再需要监听记得移除监听事件。

var element = document.getElementById('button');

function onclick(event) {
element.innerHTML = 'text';
}; element.addEventListener('click', onclick);
// 移除监听
element.removeEventListener('click', onclick);

3.闭包

闭包在js开发中是极其常见的,我们来看个例子:

var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
//unused未执行,但一直保持对theThing的引用
if (originalThing)
console.log("hi");
};
//创建一个新对象
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log("message");
}
};
}; setInterval(replaceThing, 1000);

定时器每次调用replaceThing,theThing都会获得一个包含数组longStr与闭包someMethod的新对象。

闭包unused保持着对象originalThing的引用,因为theThing赋值的缘故,也保持了对theThing的引用。虽然unused没执行,但引用关系会导致originalThing一直无法被回收,那么theThing也一样。正确做法是在replaceThing 最后添加originalThing  = null;

所以我们常说,对于闭包中的变量,在不需要时一定记得手动释放。

4.DOM的引用

操作dom总是被认为是不好的,但一定得操作,我们的习惯是通过一个变量来存储它,这样就可以反复使用了,但这也会造成一个问题,dom会被引用2次。

var elements = document.getElementById('button')

function doStuff() {
elements.innerHTML = '听风是风';
};
// 清除引用
elements = null;
document.body.removeChild(document.getElementById('button'));

在上述代码中,一次引用是基于dom树的引用,第二是变量elements的引用,当我们不需要这个dom时,都做两次清除操作。

 伍 ❀ 参考

JavaScript深入之带你走进内存机制

MDN 内存管理

JS进阶系列之内存空间

JavaScript 工作原理(一)——内存管理与四种常见内存泄漏的处理方法

JS 从内存空间谈到垃圾回收机制的更多相关文章

  1. 【Java】关于JVM运行时内存空间、JVM垃圾回收机制

    参考的优秀文章 <深入理解Java虚拟机 JVM高级特性与最佳实线>(机械工业出版社) Java虚拟机的堆.栈.堆栈如何去理解? 聊聊JVM的年轻代 前言 本文是<深入理解Java虚 ...

  2. 浅谈 JavaScript 垃圾回收机制

    github 获取更多资源 https://github.com/ChenMingK/WebKnowledges-Notes 在线阅读:https://www.kancloud.cn/chenmk/w ...

  3. JVM内存管理和JVM垃圾回收机制

    JVM内存管理和JVM垃圾回收机制(1) 这里向大家描述一下JVM学习笔记之JVM内存管理和JVM垃圾回收的概念,JVM内存结构由堆.栈.本地方法栈.方法区等部分组成,另外JVM分别对新生代和旧生代采 ...

  4. JVM的内存区域划分以及垃圾回收机制详解

    在我们写Java代码时,大部分情况下是不用关心你New的对象是否被释放掉,或者什么时候被释放掉.因为JVM中有垃圾自动回收机制.在之前的博客中我们聊过Objective-C中的MRC(手动引用计数)以 ...

  5. JVM内存管理、JVM垃圾回收机制、新生代、老年代以及永久代

    内存模型 JVM运行时数据区由程序计数器.堆.虚拟机栈.本地方法栈.方法区部分组成,结构图如下所示. JVM内存结构由程序计数器.堆.栈.本地方法栈.方法区等部分组成,结构图如下所示: 1)程序计数器 ...

  6. 浅谈python垃圾回收机制

    引入 ​ 解释器在执行到定义变量的语法时,会申请内存空间来存放变量的值,而内存的容量是有限的,这就涉及到变量值所占用内存空间的回收问题,当一个变量值没有用了(简称垃圾)就应该将其占用的内存给回收掉,那 ...

  7. 浅谈java垃圾回收机制

    今天看thinking in java,里面很详细的谈到java垃圾回收器机制,看完后让我对这神秘的区域有一定的了解,特写一些小总结记录下来. 分两点来说. 第一点:Object.finalize() ...

  8. 浅谈c#垃圾回收机制(GC)

    写了一个window服务,循环更新sqlite记录,内存一点点稳步增长.三天后,内存溢出.于是,我从自己的代码入手,查找到底哪儿占用内存释放不掉,最终明确是调用servicestack.ormlite ...

  9. java中垃圾回收机制和引用类型

    在java中JDK1.2版本以后,对象的引用类型分为四种,从高到低依次为:强引用.软引用.弱引用.虚引用. ①强引用的特点:垃圾回收机制绝不会回收它,即使内存不足时,JVM宁愿抛出OutOfMemor ...

随机推荐

  1. 2019/12/16学习内容摘要(Vim)

    一,Vim与vi的区别 vim可以当作vi的升级版本,他可以用多种颜色的方式来显示一些特殊的信息. vim会依据文件扩展名或者是文件内的开头信息, 判断该文件的内容而自动的执行该程序的语法判断式,再以 ...

  2. 浅谈python中selenium库调动webdriver驱动浏览器的实现原理

    最近学web自动化时用到selenium库,感觉很神奇,遂琢磨了一下,写了点心得. 当我们输入以下三行代码并执行时,会发现新打开了一个浏览器窗口并访问了百度首页,然而这是怎么做到的呢? from se ...

  3. qq cookie

    qq cookie from selenium import webdriver from selenium.webdriver import ActionChains import time, re ...

  4. CMKAE简单实用指南

    CMake is an open-source, cross-platform family of tools designed to build, test and package software ...

  5. 运维工具ansible-使用与介绍(转)

    转自 Linux轻量级自动运维工具-Ansible浅析 - ~微风~ - 51CTO技术博客http://weiweidefeng.blog.51cto.com/1957995/1895261 Ans ...

  6. 【第二章】Zabbix3.4监控SQLServer数据库和H3C交换机思科Cisco防火墙交换机教程笔记

    监控SQLServer数据库 SSMS执行相关SQL SQL模板命名规则 Zabbix客户端导入模板 添加SQLServer监控图形 SQLServer服务器关联模板 监控思科Cisco防火墙交换机 ...

  7. Ubuntu安装Node和npm

    本文简单介绍在Ubuntu上安装最新版本的node和npm. 本次试验环境是Ubuntu 18.10. 安装nodejs root@ubuntu:~# cat /etc/issue Ubuntu 18 ...

  8. xlwings 操作 excel

    xlwings: xlwings是一个Python库,它使Python的一些数据分析特性可以在Excel实例中使用,包括对numpy数组.pandas Series和DataFrame的支持.与其他任 ...

  9. Java中间消息件——ActiveMQ入门级运用

    先来说一说我们为什么要用这个东西啊! 比如,我们现在有这样了个问题要解决: 这样,我们就要用到中间消息间了 然后我们就说一下什么是中间消息间吧. 采用消息传送机制/消息队列 的中间件技术,进行数据交流 ...

  10. [b0044] numpy_快速上手

    1 概念理清 2 创建数组 2.1 f1= np.array( [ [1,2,3,4], [2,3,4,5], [3,4,5,6] ]) 其他代码 a= np.array([ [ [3.4,5,6,8 ...