Skip to content

内存管理

内存为什么需要管理? 如果我们在写代码的过程中,不够了解内存的管理机制,写出不容易被察觉的内存问题性代码,就会给程序带来意想不到的 BUG。

内存:由可读写单元组成,表示一片可操作空间

管理:人为的去操作一片空间的申请、使用和释放。

内存管理:开发者主动申请空间、使用空间、释放空间。

管理流程:申请一使用一释放

// 申请空间(由于JavaScript没有提供相关api,我们只能通过JS执行引擎,遇到变量定义的时候,自动去申请空间)
let obj = {};
// 使用空间
obj.name = 1;
// 释放空间
obj = null;

JS 中的垃圾回收

对象在什么情况下会被视为垃圾?

JS 中的垃圾回收是自动的

当对象不再被引用时,被视为垃圾

对象不能从根(全局执行上下文)上访问到时是垃圾

let obj = { name: 11 };
let ali = obj;
obj = null; // {name: 11} 被ali引用,不是一个垃圾

GC 算法

GC:垃圾回收机制;它可以找到内存中的垃圾、并释放和回收空间。 那么什么样的东西可以被当作垃圾呢?

程序中不再使用的对象

程序中能再访问的对象

GC 算法:算法就是工作时查找和回收所遵循的规则。 常见的 GC 算法有:

引用计数

判断对象的引用来决定是不是垃圾

标记清除

  • 给活动对象添加标记,来判断他是不是垃圾

标记整理

类似标记清除

分代回收

引用计数算法实现原理

核心思想:内部通过一个计数器来维护当前对象的引用数,从而判断当前对象的引用数是否为 0,来决定它是不是一个垃圾对象。当这个对象的引用为 0 的时候,GC 就开始工作,将其所在的对象空间进行回收和释放,然后再使用。

当某个对象的引用关系发生改变时,引用计数器就会主动去修改当前对象所对应的引用数值。当引用为 0 时,GC 就会将其所在的空间回收。 引用关系发生改变:假设我们的代码里面有一个对象空间,如果有一个变量名指向它,这时就把对象空间的引用加 1,如果又有一个变量指向它,那就再加 1;如果是减少的情况,例如取消引用,那就减 1。为 0 时,GC 就会立即将其回收。

引用计数算法的优缺点

优点

发现垃圾时立即回收

  • 如果引用为 0 就会立即进行回收、释放

最大限度减少程序暂停

  • 由于引用计数算法会时刻监控着那么引用为 0 的对象,如果在栈满时,会以最快的速度释放空间。程序就不会因为栈满而停止运行了。

缺点

  • 无法回收循环引用的对象
function fn() {
  const obj1 = {};
  const obj2 = {};
  // 在全局上下文中,没有使用到这两个对象了,他们已经可以被回收了
  // 但是由于这两个对象相互循环引用,在使用引用计数算法时,他仍然还是有引用并且不为0,因此GC无法对其回收。
  obj1.name = obj2;
  obj2.name = obj1;

  return "ok";
}
fn(); // 当函数执行完毕,会释放里面的空间

时间开销大

  • 需要时刻监控对象是否修改,如果有很多的对象需要修改,需要的时间就会更久一点

标记清除算法实现原理

标记清除算法相较于引用计数算法,他能解决更多的问题,因此它在 V8 中被大量使用。

核心思想:将整个垃圾回收操作分为两个阶段;第一个阶段,遍历所有对象,然后找到这些活动的对象(可达对象)并进行标记,如果不可达就不会标记并且会在第二个阶段被回收掉。第二个阶段,再次遍历所有对象,然后把那些没有被标记的对象进行清除操作,并把第一阶段中设置的标记抹掉,便于下次 GC 还能够正常工作。最后就可以把相应的垃圾进行回收,然后再把回收的空间交给空闲链表进行维护,下次程序执行就可以向空闲链表申请空间进行使用。

标记清除算法的优缺点

优点

解决了引用计数算法中对象循环引用无法回收的问题

  • 标记清除算法会递归遍历所有对象,然后将可达对象进行标记;如果两个对象相互循环引用,一旦对象不可达,它始终也能清除标记,GC 也能正常回收。

缺点

不会立即回收对象

  • 当遍历对象的时候,即使发现了不可达对象,它也不会立即去进行回收,只有等到最后才会去回收,而且其实这个时候程序是停止工作的。

空间碎片化

  • 回收后的空闲地址有可能不是连续的,使用空间时很容易造成空间上的浪费。

标记整理算法实现原理

和标记清除算法一样,标记整理算法在 V8 也被频繁使用

标记整理算法其实可以看做是标记清除算法的增强版,因为它们在第一个阶段是相同的,都会先遍历所有对象,然后对当前活动对象(可达对象)进行标记操作。

但是不同的是,标记清除算法是直接对没有标记的进行清除,而标记整理算法会在清除之前进行整理操作,移动对象的位置,并让他们在地址上产生连续。

这样连续的可用空间,能够最大化的让程序使用到内存释放出来的空闲空间,避免空间碎片化造成空间浪费。

标记整理算法优缺点

优点

减少碎片化空间

缺点

不会立即回收垃圾对象

V8

V8 是一款主流的 JavaScript 执行引擎。V8 之所以有优越的性能,是因为它优秀的内存管理机制以及 V8 采用的即时编译机制。 V8 内存的上限设定:64 位 1.5G 32 位 800M

V8 垃圾回收策略

采用分代回收的思想,把内存空间按照一定的规则分为两类,新生代存储区和老生代存储区。针对不同代采用最高效的 GC 算法,从而对不同的对象进行回收操作。

V8 中常用的 GC 算法

分代回收

空间复制

标记清除

标记整理

标记增量

V8 如何回收新生代对象

![image.png](JS内存机制+30743352-7897-4c39-a51b-58e5beb5f481/image 1.png)

如图所示,V8 内部将空间分为了两部分,左侧(From To)小空间专门用于存储新生代对象,在 64 位操作系统中,它的大小为 32M,32 位系统中,它的大小是 16M;

新生代指的是存活时间较短的对象。比如局部作用域(函数)中定义的变量,当函数执行完,函数出栈时函数中的变量就会被回收。 那么,V8 是如何完成新生代对象回收的呢?

新生代对象回收实现

回收过程采用复制算法+标记整理算法。首先它会将左侧的小空间也会分成两个部分(From、To),而且这两个空间是相同大小的。其中我们将 From 称为使用空间,将 To 称为空闲空间。当程序要申请空间时,它会将新申请的活动对象存储于 From 空间,这个时候 To 空间是空闲的没有使用。当 From 使用到一定程度后就会触发 GC 操作,它将 From 空间中的活动对象进行标记,然后对空间整理为连续的,便于后续不会产生碎片化空间,这些操作完成后再将这些活动对象拷贝至 To 空间,From 空间里的对象就有一份备份,这就意味着可以对它进行回收,因为活动对象都在 To 空间里有所体现,所以会直接把 From 空间中的对象进行回收。

回收细节

如果我们在拷贝时,发现某一个变量对象所指向的空间,在我们老生代存储区也存在,这个时候就会发生晋升的现象。这里的晋升指的就是将新生代的对象移动至老生代中进行存储。 判断是否晋升的条件有以下几个:

  • 经过一轮 GC 还存活的新生代对象需要晋升

如果新生代中的对象经过一轮 GC 还存活的,我们就可以把它拷贝至老生代存储区进行存储操作。

  • 在拷贝的过程中,发现 To 空间的使用率超过 25%,也需要把这次的活动对象都移动至老生代存储区中进行存储。

To 空间的使用率如果超过了限制,那么新进来的对象空间好像就存放不进去了,所以在这里有 25%的限制操作。

V8 如何回收老生代对象

如上图所示,老生代对象存放在右侧老生代区域。同样针对于老生代存储区也有大小限制,64 位的操作系统为 1.4G,32 位的操作系统为 700M。老生代对象就是指存活时间较长的对象,例如在全局上下文中存放的变量、闭包中存放的变量数据等。

老生代对象回收实现

主要采用标记清除、标记整理、增量标记算法。其实首先使用的是标记清除算法完成垃圾空间的释放和回收,因为它执行的速度是比较快的。当把新生代区域中的内容往老生代存储区域中移动(也就是晋升)的时候,而且老生代的空间又不足以存放所移过来的这些对象,就会触发标记整理,把之前的碎片空间进行整理回收,让我们有更多的空间进行使用。最后会采用增量标记算法对回收效率进行提升。

新老代细节对比

新生代区域垃圾回收使用空间换时间,因为他采用的是复制算法,这也就意味着每时每刻都会有空闲的空间存在。但是新生代存储区本身的存储就很小,那么优化出来的空间就更小,所以相对于它带来的时间效率上的提升是微不足道的。老生代区域垃圾回收不适合复制算法,因为他的空间很大,复制很多对象时也会非常消耗时间。

标记增量如何优化垃圾回收

当垃圾回收机制工作的时候,是会阻塞我们的程序运行的,程序执行完成后,会暂停下来进行回收操作。标记增量其实就是将一整段的垃圾回收操作拆分成多个小步骤组合着去完成,从而替换掉我们之前一口气去做完的垃圾回收操作。这样做的好处是可以让程序和垃圾回收机制交替着去执行,而不是执行程序时不能进行垃圾回收,垃圾回收时不能执行程序,这样带来的时间消耗也是非常合理的。而且 GC 执行的效率非常快,给用户带来的体验也更加友好了。