咱们如今经常使用的计算机都属于 冯·诺依曼体系计算机, 计算机硬件由 控制器、运算器、存储器、输入设备、输出设备 五大部分组成。git
咱们一般所说的内存就是 存储器。github
经常使用的内存都是易失性存储器(须要经过不断加电刷新来保持数据,一旦断电就会致使数据丢失),因此须要一种容量大、低成本的非易失性存储器来进行数据的存储,这就是外存,例如磁带、软盘、硬盘、光盘、闪存卡、U盘等。能够将外存理解为输入输出设备,由于外存是须要经过I/O接口进行数据存取的,而内存是由CPU直接寻址的。外存中的程序须要经过I/O接口调入内存中才能够运行。算法
内存就是程序运行的地方,其实程序本质上就是指令和数据的集合。因此说内存是指令和数据的临时存储器,而后CPU对内存中的指令和数据进行处理。编程
无论什么程序语言,其运行都依赖内存,内存生命周期基本是一致的:segmentfault
在JavaScript中,第一步和第三步由js引擎完成的,对于编程人员是隐藏的。可是这并不意味着咱们不须要了解JavaScript中的内存机制,了解内存机制有助于咱们写出更优雅、性能更好的代码。浏览器
JavaScript数据类型有基本类型和引用类型两大类,基本类型有Undefined、Null、Boolean、Number、String、Symbol六中,引用类型有Object,全部的JavaScript变量值将会是七种的其中之一。这些数据类型在内存中是怎样存储的?咱们来看一下JavaScript的内存模型。闭包
说是
JavaScript的内存模型
其实不太准确,只是便于理解。因为JavaScript中的内存分配是由js引擎完成的,因此更准确的描述是js引擎的内存模型
。ide
一个运行中的程序老是与内存中的一部分空间相对应。这部分空间叫作 Resident Set (驻留集)。V8(一种JS引擎) 组织内存的方式以下图:函数
各部分做用以下:工具
为何内存要如此分配?
看到有些文章中说基本类型变量复制按值传递,引用类型变量复制按引用传递,又有的说引用类型变量复制按共享传递。总之对新手不太友好,这里咱们站在内存层面来解释就比较好解释了。
咱们能够理解为JavaScript变量的拷贝都是按栈内存内的值传递,这里栈内存内的值对于基本类型变量来讲就是其值,对于引用类型来讲就是一个指向堆内存中实际值的地址。
咱们来看一个简单的例子理解一下:
let p1 = {name: 'logan'}
let p2 = p1
// p1 和 p2 在栈内存中存放的引用地址相同,都指向堆内存中存放对象 {name: 'logan'}
// 可是这两个引用地址倒是相互独立的,并不存在引用关系
复制代码
// 本质上是对堆内存中的对象进行修改,因此会同时影响p1和p2
p2.name = 'jason'
console.log(p1) // 输出:{name: 'jason'}
console.log(p2) // 输出:{name: 'jason'}
复制代码
// 这一步是直接修改了栈内存内标识符p2对应值,并不会影响p1
p2 = 3
console.log(p1) // 输出:{name: 'jason'}
复制代码
函数的参数传递与变量复制传递表现一致,也是按栈内存内的值进行传递,由于本质上来讲,函数传参就是把传入的实参拷贝赋值给形参。
垃圾回收是一种内存管理机制,就是将再也不用到的内存及时释放,以防内存占用愈来愈高,致使卡顿甚至进程崩溃。
在JavaScript中内存垃圾回收是由js引擎自动完成的。实现垃圾回收的关键在于如何肯定内存再也不使用,也就是肯定对象是否无用。主要有两种方式:引用计数 和 标记清除。
这是IE六、7采用的一种比较老的垃圾回收机制。引用计数肯定对象是否无用的方法是对象是否被引用。若是没有引用指向对象,对象就能够被回收。咱们结合代码来理解:
// 堆内存建立了一个对象{a: 1},咱们记为ObjA,变量obj1指向ObjA,ObjA引用次数为1
let obj1 = {
a: 1
}
// obj2 拷贝 obj1 的地址,也指向ObjA,ObjA引用次数为2
let obj2 = obj1
// 解除obj1对ObjA的引用,ObjA引用次数减一,为1
obj1 = 3
// 解除obj2对ObjA的引用,ObjA引用次数减一,为0,能够被回收
obj2 = 'logan'
复制代码
缺点:没法处理循环引用
什么意思呢,咱们结合代码理解,先看正常状况下引用计数的工做:
function func() {
// 堆内存建立对象{a: 1},记为ObjA,变量foo指向ObjA,ObjA引用次数为1
let foo = {a: 1}
// 堆内存建立空对象,记为ObjB,变量bar指向ObjB,ObjB引用次数为1
let bar = {}
// 其属性x指向ObjA,ObjA引用次数为2
bar.x = foo
// 当函数执行完毕返回时
// 变量bar生命周期结束,ObjB引用次数减一,为0,可被回收,故对其内部进行回收
// bar.x生命周期结束,ObjA引用次数减一,为1
// 变量foo生命周期结束,ObjA引用次数减一,为0,可被回收
}
复制代码
可是若是两个对象之间存在循环引用,引用计数就会没法处理:
function func() {
// 堆内存建立对象{a: 1},记为ObjA,变量foo指向ObjA,ObjA引用次数为1
let foo = {a: 1}
// 堆内存建立空对象,记为ObjB,变量bar指向ObjB,ObjB引用次数为1
let bar = {}
// 变量foo属性x指向ObjB,ObjB引用次数为2
foo.x = bar
// 变量bar属性x指向ObjA,ObjA引用次数为2
bar.x = foo
// 当函数执行完毕返回时
// 变量bar生命周期结束,ObjB引用次数减一,为1,不可被回收
// 变量foo生命周期结束,ObjA引用次数减一,为1,不可被回收
}
复制代码
优势:肯定性
引用计数其实也是有优势的,那就是对象必定会在最后一个引用失效的时候销毁,也就是说垃圾回收的时机在代码内是可控的,因此对于对延时比较敏感的场合比较适用。
从 2012 年起,全部现代浏览器都使用了标记清除的垃圾回收方法。
标记清除的工做原理简化后就是:从垃圾收集根(root)对象(在JavaScript中为全局环境记录)开始,标记出全部能够得到的对象,而后清除掉全部未标记的不可得到的对象。
也就是说,标记清除肯定对象是否无用的方法是对象是否能够被得到。
现代浏览器对JavaScript垃圾回收算法的改进都是基于标记清除算法的改进,并无改进标记清除算法自己和它对“对象是否能够被得到”的简化定义。
关于垃圾回收的更多内容,可阅读浅谈V8引擎中的垃圾回收机制
内存泄漏(Memory Leak) 是指程序中己动态分配的堆内存因为某种缘由程序未释放或没法释放,形成系统内存的浪费,致使程序运行速度减慢甚至系统崩溃等严重后果。
说到内存泄漏,不得不提一些文章内说闭包会形成内存泄漏,要尽可能少用。其实这个观点是错误的,咱们运用闭包说到底就两点目的:一是变量私有化,二是延长变量生命周期。 因此说 闭包并不会形成内存泄漏,而是正常的内存使用。
如何避免内存泄漏?一句话:及时解除无用引用。 例如再也不须要的闭包、定时器及全局变量等。说到底仍是我的编程习惯的好坏,多说无益,列太多的条条框框反而显得繁琐。
识别内存泄漏
Memory
选项Stop
,观察面板上的数据如图所示,内存占用若是总体平稳,说明不存在内存泄漏。
若是内存占用只升不降,或者总体呈一直升高的趋势,说明存在内存泄漏。
内存泄漏定位
若是发现页面存在内存泄漏,咱们能够在下方内存图点击对应的内存异常处,而后点击下方面板内的Event Log
面板,能够查看代码内具体发生了什么,见下图:
咱们发现原来是调用了grow
函数
let x = []
function grow() {
x.push(new Array(1000000).join('x'))
}
document.getElementsByClassName('title-h2')[0].addEventListener('click', grow)
复制代码
固然,上面的代码只是为了模拟,到底是否为内存泄漏要看变量x咱们是否须要用到,一旦不须要,咱们应该解除其引用。
欢迎前往阅读系列文章,若是喜欢或者有所启发,欢迎 star,对做者也是一种鼓励。
菜鸟一枚,若是有疑问或者发现错误,能够在相应的 issues 进行提问或勘误,与你们共同进步。