为什么要关注内存
在程序运行和变量声明的过程都要用到内存,了解内存有利于写出更好的程序代码
内存溢出
简单来说内存溢出就是程序要运行的内存大与能提供的内存,这个时候就会造成程序崩溃,例如浏览器长时间无反应,浏览器崩溃。
数据类型和内存
数据内型
1.原始数据类型:字符串(string),数字(number),布尔(Boolean),空对象(null),未定义(undefined), symbol(唯一的值)
2.引用数据类型:object
3.内存空间: 栈内存(stack)堆内存(heap)
原始数据类型的值大小固定,系统分配存储空间,引用数据大小不固定
###栈内存 原始数据类型的变量,放在stack中,遵循先进后出的原则
let a = 1;
let b= 2;
let c = 3;
在变量入栈的时候a先进入,b再进入,c最后进入 在变量出栈的时候c先出,b再出,a最后出栈
堆内存
声明引用数据类型的变量的时候,js 会在堆内存中开辟一个空间,把变量内容转变成一个字符串存放起来(假如是函数,存放的就是函数体),还会生成一个对应的内存地址这个地址,会被存放在栈内存里。
function foo() {
return '函数'
}
foo;
// ƒ foo() {
return '函数'
}
foo();
// 函数
就如同上面的例子,直接打印foo得到的只是存在堆内存中的字符串,调用foo()的时候才会找堆内存中的字符串,并把字符串转换成js代码执行
js的垃圾回收机制
在js中声明变量运行程序都要用到内存,为了节省内存,就需要找出那些不在继续使用的变量,释放它占用的内存,垃圾回收器会按照固定的时间间隔周期性的执行这一操作。
js 采用自动的垃圾回收机制来管理内存,这一方法有利有弊:
优点:简化内存管理代码,降低开发人员工作量,减少长时间运行带来的内存泄漏问题 缺点:js没有暴露内存的api,所有的管理,开发人员无法主动操作,没法进行干预。开发人员就不能掌握内存。
浏览器常用的垃圾回收方法
1、引用计数
就是记录每个值被引用的次数
let obj = {a:1} // +1
let obj1 = {a: 1} // +1
let obj = {} // -1
let obj = null; // -1
- 在上面的例子中 声明了变量obj并且为他赋值了一个引用类型的值,这个值的引用次数+1
- 这个值又赋值了给另一个变量,引用次数再 +1
- 但是包含这个引用类型的值的变量被赋予了别的值,引用记述 -1
- 当引用计数变成0时,这个值也就不能访问了
- 垃圾收集器下次运行的时候,这个引用次数是0的值所占用的内存,会被释放
引用计数的回收方式,会有循环引用的bug
let objA = {a:1}
let objB = {b:1}
objA.c = objA;
objB.b = objB;
这个赋值方式,引用计数的方式就不起作用了,会浪费内存,但是现在主流浏览器,都会解决这个问题
2.标记清除
当变量进入执行环境是,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。
执行环境定义了变量或者函数有权访问其他数据,决定其行为,每一个执行环境都有和他相关联的变量对象,环境中定义的所有变量和函数有保存在这个对象里。
- 全局执行环境(windows)
- 局部执行环境 (function)
v8垃圾回收机制
v8做一次小的垃圾回收需要50ms以上,做一次非增量式的垃圾回收甚至要1秒以上,十分影响性能。
分式垃圾回收机制
v8 直接将内存分为 新生代(存放存活时间短的对象)和老生代(存活时间长的对象)
老生代空间 是连续的,采用标记清除的方式来释放内存,但是标记清除会导致,内存空间不连续,所以要使用标记合并的方式,来把标记的变量放在一边,没有标记的放在另一边,然后回收被标记的一边,这样就解决了,内存不连续的问题。
新生代算法,采用scavenge算法,会有from和to2个空间
像图片显示的这样,obj2要被回收的时候,obj1要被放入to中,然后反转,在回收to中的内容,最后把obj2 回收完毕。
新生代晋升
1、新生代在垃圾回收的过程中,当一个对象经过多次的复制之后,依然存活,就会被认为为是生命周期较长的对象,会被移动到老生代里面,采用新的算法管理。
2、在from空间和to空间,在反转的时候,如果to空间中的使用量已经超过25%,就将from中的对象直接晋升到老生代内存空间中。