JavaScript 中的垃圾回收机制

吐槽君 分类:javascript

一、内存管理

  • 内存:计算机中所有程序的运行都在内存中进行,由可读写单元组成,表示一片可操作空间
  • 管理: 人为的去操作一片空间的申请,使用,释放
  • 内存管理: 开发者主动去申请空间,使用空间,释放空间
  • 管理流程: 申请 -> 使用 -> 释放

JavaScript 不支持开发者进行手动的去对内存进行管理,当JS执行引擎执行代码的时候,遇见变量会自动的在内存中为该变量分配空间

// 申请内存空间
let obj = {}

// 使用内存空间
obj.name = 'reborn'

// 释放内存空间
obj = null 
 

二、JavaScript 中的垃圾回收

垃圾的定义

  • 程序中不被引用(不被需要)的变量就是垃圾
  • 程序中不能再访问到的是垃圾

当满足这两点的时候,JS 执行引擎会将内存中的垃圾(空间)进行释放和回收。

可达对象:从根上(全局作用域)出发,可以访问到的对象就是可达对象(作用域,引用)

// 申请内存空间,obj变量引用了这片空间
const obj = {name: 'rebornjiang'}

// 将 obj 的引用赋值给到 newObj 变量, 目前 newObj 也引用了这片内存空间。
const newObj = obj

// 将 obj 的引用给释放掉
obj = null

// {name: 'rebornjiang'} 这片内存空间还是可达对象,其还有被引用的空间,所以不能够释放掉。
 

三、GC算法

GC的认识与作用
认识: GC是垃圾回收机制的简写
作用:GC可以找到内存中的垃圾,并释放和回收空间。

常见的GC算法

  • 引用计数
  • 标记清除
  • 标记整理
  • 分代回收

1. 引用计数算法

核心思想:

  • 有专门的引用计数器用来计算对象(变量)的引用次数
  • 当引用关系发生改变的时候会修改引用数字
  • 判断当前引用数是否为0,如果为0 GC 将其进行作为垃圾进行释放和回收。

代码演示

// case 1
const user1 = { age: 18 }
const user2 = { age: 20 }
const user3 = { age: 21 }

const ageList = [user1.age, user2.age, user3.age]

// 当代码执行一轮之后,发现ageList这个数组中仍然还引用了 user1 , user2, user3
// 此时这些对象的引用计数就不是0,GC 就不会对其进行释放和回收。


// case 2
function fn() {
  const num1 = 1
  const num2 = 2
}

fn()

// 当fn()执行结束之后,num1 与 num2 不在被使用,其引用计数器为0, GC 就会对其进行回收。 

 

优点:

  • 发现可以立即回收,当监听到内存中空间的引用为0时可以立即释放回收。
  • 可最大程度的减少程序暂停的可能,因为引用计数GC会时刻监听到内存中空间的对象的引用是否为0,如果为0立即释放回收,这样就最大程度减少程序暂停的可能

缺点:

  • 无法将循环引用的对象进行回收释放。参考下面代码示例
  • 相对于其他的GC算法时间开销大。这是因为引用计数算法需要对内存空间对象维护一个引用计数,并监听引用的变化,当引用发生变化后,引用计数也要发生相应的改变。可内存中会有许多的空间对象要被维护和监听其引用计数,这样就会导致引用计数算法的时间开销大。
// 对象的循环引用
function fn() {
  const obj1 = { name: 'rebornjiang' }
  const obj2 = { name: 'molly' }

  obj1.name = obj2
  obj2.name = obj1
  return ''
}
fn()

// 即使fn执行结束之后,肯定会涉及垃圾回收,obj1 与 obj2 在全局也不会再有引用他们,但是此时obj1 与 obj2 的name属性相互引用了obj1 与 obj2这两个对象
// 其引用计数就不是0, 所以 采用 引用计数垃圾回收机制就不能将其进行释放与回收。
 

2. 标记清除算法

标记清除算法核心思想:

  • 分为标记和清除两个阶段
  • 遍历所有的对象,然后将其活动对象(可达对象)进行标记
  • 遍历所有的对象,然后将非标记的对象进行清除,释放回收相应的空间,将回收的空间加入空闲链表中,等待申请使用。
  • 同时将标记对象进行取消其标记。

优点

  • 相对引用计数算法 它可以清除循环引用对象

在这里插入图片描述

// 将上面图示转换为代码
const A = {
  D: {}
}

const B = {}

const C = {
  E: {}
}

function fn() {
  const a1 = {}
  const b1 = {}
  a1.obj1 = b1
  b1.obj2 = a1

  return ''
}

fn()

// 当fn执行完成之后,a1与b1就与全局没有什么关系了,a1 b1就编程了不可达对象。
// 在标记清除算法的第一个阶段,就不会对 a1 b1 进行标记,等待标记清除算法的第二个
// 阶段会对 a1 b1 进行清除,同时会回收空间,将回收空间添加到空闲链表中,等待申请使用。
 

缺点

  • 回收空间会是碎片化的空间,不能够使空间得到最大利用率。
  • 标记清除是不可以立即清除垃圾的
    • 当标记清除和标记整理GC运行时,程序会暂停执行

模拟内存存储请看

任何一个内存空间对象都会有两部分组成: 头 + 域
头: 存储空间源信息的,如空间的大小,空间的地址信息。
域: 专门用来存放数据的

在这里插入图片描述
示例图说明:

  • 标记清除算法在第一轮标记的阶段会将 A 标记为可达对象,BC不进行标记。
  • 标记清除算法的第二阶段会将未标记的BC不可达对象进行清除和回收其空间添加到空闲链表中,等待申请使用。
  • B 空间对象会有两个域存储空间,C空间对象会有 一个域存储空间。B C 加起来好像是释放了三个域存储空间,但是由于 B C 内存空间中多了一个 A空间,这就造成所回收的空间BC的空间地址不连续,这就造成内存空间的碎片化。
  • 碎片化空间不能够组合使用,这就导致申请使用的空间大小需要跟释放空间大小相匹配才能够达到空间的最大利用率。

3. 标记整理算法实现原理

核心思想:

  • 标记整理算法是标记清除算法的增强操作,解决标记清除算法带来的空间碎片化的问题。
  • 标记阶段的操作与 标记清除算法GC的标记阶段是一样的
  • 标记整理会在清除阶段之前会执行整理,将标记的可达对象与未标记的不可达对象进行分类分开,使即将要被回收的空间地址产生连续。

请查看下图:
在这里插入图片描述
在这里插入图片描述
优点

  • 与标记清除的优点相同
  • 弥补了标记清除所带来的空间碎片化的缺点

缺点

  • 当标记清除和标记整理GC运行时,程序会暂停执行
  • 不能够立即回收垃圾

4. 分代回收算法

V8 垃圾回收策略就是采取分代回收,参考下一节。

四、V8 JS执行引擎的认识

V8的认识

  • V8 是 JS执行引擎
  • V8 速度快 是因为采用即时编译。不像其他 JS 执行引擎需要将代码编译成字节码,并解释成机器码。V8引擎在执行 JS 代码的时候可以直接翻译成机器码进行执行。
  • V8 内存设有上限。

V8 垃圾回收策略
采用分代回收思想, 这个代分为新生代对象与老生代对象,对不同代对象采取不同的GC算法进行垃圾回收。

  • V8 执行引擎将内存存储空间一分为二,分为新生代存储空间与老生代存储空间
  • 针对不同代对象采用不同的GC算法,使垃圾回收效率达到最高。

在这里插入图片描述

新生代老生代对象的概念

  • 新生代对象:存活时间较短的对象,如函数内的局部作用域的成员,执行完成之后就会进行回收。
  • 老生代对象:存活时间长的对象,如全局作用域中的成员,闭包成员

V8常用的GC算法

  • 分代回收
  • 空间复制
  • 标记清除
  • 标记整理
  • 标记增量

1. V8 回收新生代对象

新生代对象被存储在新生代存储空间,在不同操作系统的存储空间大小不一样,在 OSx64 为32M,OSx32 为16M。

在这里插入图片描述

新生代对象回收实现的原理

  • 对新生代对象采用复制算法 + 标记整理算法进行垃圾回收
  • 将新生代内存空间分为两个相等的大小的空间,如上图的 From 与 To模块
  • 使用空间为From,空闲空间为To
  • 代码执行的时候需要申请空间进行使用,会将所有的活动对象都分配到到 From 空间内
  • 当From空间容量到达一定的程度之后会触发标记整理GC操作,将标记整理的活动对象拷贝到空闲空间To内
  • From空间和To空间完成一次角色互换,To空间会变为新的From空间,原来的From空间则变为To空间。

回收细节说明:

  • 拷贝过程可能会出现晋升
  • 晋升就是将新生代对象移动至老生代
  • 是否晋升取决于以下两个标准
  • 一轮GC还存货的新生代需要晋升
  • To 空间的使用率超过25%

2. V8 回收老生代对象

老生代对象被存储在老生代存储空间,在不同操作系统的存储空间大小不一样,在 OSx64 为1.4G,OSx32 为700M。
在这里插入图片描述

老生代对象回收实现原理

  • 主要采用标记清除,标记整理,增量标记算法
  • 首先使用标记清除完成垃圾空间的回收。对于碎片化存储空间等待标记整理算法进行处理。
  • 当新生代对象要往老生代对象中移动的时候,并且老生代对象的内存空间又不足以分配给移动过来的对象,此时会采用标记整理对碎片化空间进行优化。
  • 采用增量标记进行效率优化

增量标记算法
增量标记算法如图就可以知道,在对可达对象进行标记的时候不一次性标记完,乃是分段标记,提供垃圾回收的效率。
在这里插入图片描述

回复

我来回复
  • 暂无回复内容