v8引擎垃圾回收机制

205 阅读7分钟

v8引擎垃圾回收机制浅析

一、为什么要关注浏览器内存

  • 防止内存过大引起客户端卡顿甚至无响应
  • node使用的也是v8,内存对于后端服务的性能至关重要
  • 面试装*神技

(大文件上传 ps:可能是2g -> 切片 || 扩展c++空间)

二、v8由什么组成的

v8内存分配(一)

内存分配图👇 image.png 也会分为栈和堆,垃圾回收机制我们主要关注堆(Heap memory)这个部分,而负责垃圾回收机制的部分主要是NewSpace(Young generation) (新生代)and Oid space(generation) (老生代)

  1. 大对象空间:Large object space 指的是大于其他空间大小限制对象所存放的位置(你定义的东西如果超出了新老空间就会放在这个大对象空间)
  2. 代码空间:Code space 及时编译器(JIT)在此处存储已经编译的代码块(就是我们所有的js代码都会在这个环境里编译且运行)
  • 补充:我们定义的函数也会存放在堆空间,是以string形式存储的,调用的时候使用一个栈内存的方式去调用的,模式是和code space是一样的
  1. 单元空间&属性单元空间&地图空间:Cell space Property cell space Map space 都包含相同大小的空间并且对他们的指向对象有一定的限制从而这里也可以简化他们的收集,但是大部分的内容都是存在新老空间的

v8内存分配(二)

image.png

新生代:这两个空间是严格对半分的

老生代:是连续的空间,但是有两个区(如果一个对象它有指针、引用指向其他对象的话,多数内容会保存在Oid pointer space)(如果是一个原始对象,没有指针引用啥的,它就保存在Oid data space里)所有老生代的对象都会由新生代晋升来的

  • 拓展:知道新生代和老生代内存空间是多少么?(特指v8)
    • 和操作系统有关:64位操作系统为1.4G(1464MB),32位为0.7G(732MB)
      • 64位新生代的空间位64MB,老生代为1400MB
      • 32位新生代的空间为32MB,老生代为700MB
      • 目前nodev14.12.0内存为4GB,但是网上很多资料是按照1.4GB or2GB讲的

为什么这么设计呢?

为什么一定要把内存限制在1.4G呢?

  • 先有js后有node,js是为了浏览器渲染
  • js/node是异步单线程
  • node读写大文件,webpack编译大型项目/代码的时候,内存都有可能占用1.4G,而前端代码不具备持久化,来了销毁来了销毁,vite编译代码
  • 没有必要设计过大的内存空间(如果有需要,随时可以扩充c++)
  • 因为在进行垃圾回收的过程中,会停止线程,(v8写了1.5G要50ms)也就是说空间越大垃圾越多,回收的时间就会越长

三、垃圾回收算法

新生代与老生代的回收算法完全不相同

  • 新生代就是copy(复制) Scavenge算法(新生代互换)
  • 老生代标记整理清除:早期使用 Mark-Sweep(标记清除) 后来采用 Mark-Compact(标记整理)

新生代算法

image.png 新生代互换:数据进来后会先存到新生代的From空间,然后有就继续存,直到from空间满了(如图二),在往里面存的时候,才开始触发垃圾回收机制,然后开始清理垃圾,在把obj1和obj2从from空间copy到To空间,然后将两者身份对调

  • 扩展:为什么新生代要采用这种复制的形式呢?
    • 牺牲空间来换取时间
  • 为什么老生代不用这种复制方式?
    • 老生代有1400MB,如果等分的话就剩700MB,严重浪费空间
  • 新生代存满了有用的数据不能删的时候,就要存到老生代了,这个时候如果用copy的方式太浪费资源,要考虑存储空间,所以用了其他的方法(早期使用 Mark-Sweep(标记清除) 后来采用 Mark-Compact(标记整理)

老生代算法

image.pngimage.png

广度扫描、全停顿标记、增量标记&三色标记法(IE:以前用引用计数,后来不用了)

标记清除(早期) :根结点(指的是GC根节点),它会先广度扫描(就是做标记),然后对中间有引用的变量进行标记,然后当空间要满了的时候开始清理,就会清理掉没有引用的垃圾

标记整理(后来) :也是先进行广度扫描,然后标记,然后开始第三件事情,就是整理空间,使其变成连续的空间,第四步直接清除

  • 提问:先清除在整理和先整理在清除有区别么?

    1. 有区别的

    2. 仔细观察图标就会发现,如果我先整理在清除,是做了两件事,第一件事是用有引用的变量覆盖掉没有引用的变量,第二件事是清除掉没有引用的变量

    3. 如果是先清除在整理,就需要找到所有没有引用的数据,然后清除,在整理

    • 实际上是先整理在清除
  • 提问:为什么要整理?

    1. 因为对象or数组存储在堆,而堆是一个连续的线性的存储空间
    2. 零碎的空间没有办法存储连续的大的数据,就好像你带对象看电影,但是所有的单号都被单身土豪买走了,你和你对象就是坐不进去一样,而这个时候你就很想拥有标记整理

早期的用的是全停顿标记法,后来用的是增量标记加上三色标记法

全停顿标记法:js是单线程的,所以运行的时候是先运行主线程 -> 垃圾回收 -> 主线程 -> 回收,回收的时候会做广度扫描,是一次性扫描所有的的进行标记,一次性操作完,就是全停顿标记法

  • 扩展:三色标记法将对象标记为黑、白、灰三种颜色,黑色对象为可达对象,应该保留,白色对象为不可达对象,应该被清除,灰色作为中间过度态。

增量标记&三色标记法:就是让主线程与GC之间切换的速度更快,但是标记的更少,来换取速度,每次以上一次标记的变量作为这次开始的节点来标记,然后是通过黑白灰三色来标记的

四、新生代如何晋升到老生代

image.png

新生代晋升到老生代需要两个条件:

  1. 在From空间有没有被Scavenge复制过一次(只能是一次,只针对node)
  2. 你被复制过一次,同时To空间被用了超过25%

满足这两个条件才能正儿八经的晋升到老生代,晋升到老生代就很稳定了,一般情况下就不会被清除了

五、v8是如何处理变量的

通过process.memoryUsage()

const os = require('os')
function getMemory() {
  let memory = process.memoryUsage()//内存使用情况
  let format = function (bytes) {
    return `${(bytes / 1024 / 1024).toFixed(2)}MB`
  }
  let totalM = os.totalmem
  let freeM = os.freemem
  console.log(`总空间:${format(totalM)}\t使用:${format(freeM)}`)
  console.log(`heapTotal:${format(memory.heapTotal)}\theapUsed:${format(memory.heapUsed)}`)
}

// 炸掉v8
let count = 0
let useMem = () => {
  let size = 20 * 1024 * 1024
  let arr = new Array(size)
  console.log(count++)
  return arr
}

let total = []
for (let i = 0; i < 50; i++){
  getMemory()
  total.push(useMem())
}

console.log('success')


//浏览器查看内存:window.performance => 看里面的memory
//- jsHeapSizeLimit:限制内存
//- usedJSHeapSize:使用的内存
//	> process.memoryUsage()
//	{
//  	rss: 24178688,//当前内存占用
//  	heapTotal: 6037504,//堆内存总量
//  	heapUsed: 3850752,//使用的堆内存
//  	external: 1581665,//额外使用的内存
//  	arrayBuffers: 9479//数组(阵列)缓冲区
//	}

//通过上面的代码可得出全局变量不能被清除

//扩展内存命令
//max-old-space-size=2048(单位是MB | 默认值,你可以改成4096,前提是你的电脑还有空间)
//max-new-space-size=102400(单位是KB	| 默认值,你可以改成4096,前提是你的电脑还有空间)
  • 内存主要就是存储变量等数据
  • 局部变量当程序执行结束,且没有引用的时候就消失
  • 全局对象会始终活到程序结束

六、验证整理清除的代码

function getMemory() {
  let memory = process.memoryUsage()//内存使用情况
  let format = function (bytes) {
    return `${(bytes / 1024 / 1024).toFixed(2)}MB`
  }
  console.log(`heapTotal:${format(memory.heapTotal)}\theapUsed:${format(memory.heapUsed)}`)
}

// 炸掉v8
let size = 20 * 1024 * 1024
let total = []
function fn() {
  let arr1 = new Array(size)
  let arr2 = new Array(size)
  let arr3 = new Array(size)
  let arr4 = new Array(size)
  let arr5 = new Array(size)
}
fn()
for (let i = 0; i < 25; i++){
  getMemory()
  total.push(new Array(size))
}

console.log('success')

点赞吧👍

如果有不对的地方,还请大佬指出,感谢大佬们的点赞