js 内存

378 阅读5分钟

为什么要关注内存

在程序运行和变量声明的过程都要用到内存,了解内存有利于写出更好的程序代码

内存溢出

简单来说内存溢出就是程序要运行的内存大与能提供的内存,这个时候就会造成程序崩溃,例如浏览器长时间无反应,浏览器崩溃。

数据类型和内存

数据内型

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中的对象直接晋升到老生代内存空间中。