一.前端为什么要关注内存
- 防止页面占用内存过大,引起客户端卡顿,甚至无响应
- Node.JS使用V8引擎,内存对于后端服务的性能至关重要,因为后端服务的持久性,后端更容易造成内存泄漏
二.js数据类型与js内存机制
- 原始数据类型:字符串(String)、数字(Number)、布尔(Boolean)、空对象(Null)、未定义(undefined)、Symbol
- 引用数据类型:Object
- 内存空间:栈内存(stack)、堆内存(heap)
栈内存
// 定义变量
var a = 10;
var b = 'hello world';
var c = true;
栈内存可以理解为一个玻璃瓶,定义变量时依次执行入栈操作,以定义变量a为例,首先将10存入内存空间,
之后,在我们的作用域中声明了一个遍历a,此时a的值位undefined。声明之后,将a与10关联,关联之后定义完成。
栈内存中实现的是先进后出,图中先出栈的就是变量c,接着是b最后才是a。
堆内存
js不允许直接访问堆内存,我们在操作对象时,实际上是操作对象的引用,而不是直接操作实际对象。因此,引用类型的值都是按‘引用’访问的。这里的‘引用’可以粗浅的理解为保存在栈内存的一个地址,该地址与堆内存的实际值相关联。
函数也是一个引用类型的值,他的函数体以字符串的形式存在于堆内存中。
三.垃圾回收
找出那些不再继续使用的变量,然后释放其所占用的内存,垃圾回收器会按照固定的时间间隔周期性的执行这一操作。
JavaScript使用垃圾回收机制来自动管理内存,垃圾回收是一把双刃剑:
- 优势:可以大幅简化程序的内存管理代码,降低程序员的负担,减少因长时间运转而带来的内存泄露问题。
- 不足:意味着程序员将无法掌控内存。JavaScript没有暴露任何关于内存的API。我们无法强迫其进行垃圾回收,更无法干预内存管理。
JavaScript中如何利用垃圾回收管理内存
- 引用计数
跟踪记录每个值被引用的次数,如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。
- 原理
每次引用加一,被释放时减一,当这个值的引用次数变成0时,就可以将其内存空间回收
const obj = {a:10}; // 引用+1
const obj1 = obj; // 引用+1
const obj = {}; // 引用-1
const obj1 = null; // 引用为0
- 声明了一个变量并将一个引用类型的值赋值给这个变量,这个引用类型的值引用次数时1
- 同一个值又被赋值给另一个变量,这个引用类型值的引用次数加1
- 当包含这个引用类型值的变量又被赋值给另一个值了,那么这个引用类型值的引用次数减1
- 当引用次数变成0时,说明没办法访问这个值了
- 当垃圾收集器下一次执行时,它就会释放引用次数是0的值所占的内存
// 循环引用
function fn(){
var objA = {a:10};
var objB = {b:10};
objA.c = objA;
objB.d = objB;
}
循环引用会导致旧版本浏览器无法回收,新版浏览器会采用标记清除方法回收。
标记清除
标记清除指的是当变量进入环境时,这个变量标记为“进入环境”;当变量离开环境时,则将其标记为“离开环境”,最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
function foo(){
var a =10; // 被标记进入环境
var b = 'hello' // 被标记进入环境
}
foo() // 执行完毕,a和b被标记离开环境,内存被回收
四.V8内存管理机制
限制内存的原因
- V8最初为浏览器而设计,不太可能遇到大量内存的使用场景
- 防止因为垃圾回收所导致的线程暂停执行的时间过长
V8的回收策略
- V8采用了一种分代回收的策略,将内存分为两个生代:新生代和老生代。
- V8分别对新生代和老生代使用不同的垃圾回收算法来提升垃圾回收的效率。
新生代垃圾回收
新生代对象的晋升
- 在新生代垃圾回收的过程中,当一个对象经过多次复制后依然存活,他将会被认为是生命周期较长的对象,随后会被移动到老生代中,采用新的算法进行管理
- 在From空间和To空间进行反转的过程中,如果To空间中的使用量已经超过25%,那么就将From中的对象直接晋升到老生代的内存空间中