V8-学习5-垃圾回收与内存

101 阅读5分钟

1.垃圾回收的机制

垃圾回收算法

可访问性(reachability)算法

  1. 通过GCRoot 标记活动和非活动对象
    • 从GC Root 顶节点开始去递归整颗内存树
    • GC Root 能到达的是 标记为可访问对象,活动对象
    • GC Root 不能到达的是 标记为不可访问对象,非活动
  2. 内存整理,由于内存存在大量不连续的空间(内存碎片),需要重新整理才能正常使用大空间

代际假说

  1. 大部分活动的对象都是 创建的快销毁的快,如果函数内定义的变量,块级作用域变量等
  2. 不死的对象会活的更久,window,API

垃圾回收器

  1. 主垃圾回收
    • 老生代
      • 更大的数据
      • 不死的
    • Major GC
    1. 标记-清除 mark-sweep算法
      1. 标记
        • 从一个根节点开始递归遍历,能到达的对象就是活动对象,没到达就是垃圾数据
      2. 清除
        • 把标记为垃圾的全部遍历清理
      • 缺点:容易产生不连续的碎片
    2. 标记-整理 mark compact算法
      1. 标记
        • 从一个根节点开始递归遍历,能到达的对象就是活动对象,没到达就是垃圾数据
      2. 整理+清除
        • 让活动对象移动到另外一端,然后直接清空掉另外一段以外的所有内存
  2. 副垃圾回收
    • 新生代
      • 1-8m的
      • 活动的,函数或块级作用域的变量
    • Minor GC (Scavenger)
    • scavenge算法
      • 对象区域
      • 空闲区域
      • 流程
        1. 当对象区域快满的时候,先标记垃圾数据
        2. 把活动对象复制到空闲区域,同时顺便进行了排序,所以粘贴后是有序的,复制完把当前活动区直接清空
        3. 把空闲区域和对象区域 做互换,互换后就把刚刚整理好了活动对象放在了对象区域
      • 对象晋升策略
        • 如果连续两次垃圾回收依然存活,对象就被移到老生区中

2.优化垃圾回收

主要针对主垃圾回收器的效率,副垃圾回收器空间占用不大,可以忽略

早期的垃圾回收

全停顿stop the world

  1. 在主线程执行,造成阻塞
  2. 由于标记,整理十分耗时,进行垃圾回收大内存时候,会出现严重的卡顿

现代的垃圾回收器

  1. 并行回收
    • 使用多个辅助线程执行一个完整的垃圾回收,标记和整理
    • 缺点:必须全停顿,主线程先停止,才能回收
  2. 增量式垃圾回收
    • 把一个大的回收任务里的标记任务拆解成多个小任务,并在浏览器空闲时执行一部分小任务
    • 优点
      • 可以随时被打断,和重新继续执行
    • 难点
      • 暂停需要保存当前的扫描结果,等待下一次继续执行
      • 当有js代码改变了回收的对象,回收器能正常识别
    • 实现
      • 三色标记法(黑色+白色+ 灰色方案)
        1. 默认所有节点白色
          • 没有被访问到,可以被回收
        2. 当GC Root能到达标记为黑色
          • 已标记完成,待回收
        3. GC Root 引用着,但是子节点还没被垃圾回收器标记处理,也表明目前正在处理这个节点
      • 判断逻辑
        • 当没有灰色节点
          • 没有,正面已经标记完成,可以回收
        • 有灰色节点
          • 下次继续从灰色做起始开始位置
      • 写屏障 (Write-barrier) 机制
        • 把黑色指向白色的引用 -> 改为黑色指向灰色
        • 强三色不变性
    • 流程图 www.processon.com/diagraming/…
  3. 并发回收
    • 通过辅助线程在后台执行回收动作
    • 主线程在执行 JavaScript 的过程中,辅助线程能够在后台完成执行垃圾回收的操作。
    • 难点
      • 由于是同时操作,对象的变化可能导致的标记好的逻辑全部失效
      • 由于同时操作,会出现跟数据库一样的同时读写问题
    • concurrent
  4. v8实际使用的方案
    1. 使用并发利用辅助线程进行标记
    2. 使用并行进行清理和整理
    3. 使用增量进行清理的任务,穿插着浏览器空闲时

3.常见的内存问题

1. 内存泄露(memory leak)

  • 已经不再需要使用的数据,没有被正常释放
  • 是一个缓慢上传过程
  1. 避免定义了全局的变量,如在方法里,定义了全部变量
function foo() {
    //创建一个临时的temp_array
    this.temp_array = new Array(200000)
   /**
    * this.temp_array
    */
}
//使用严格模式 use strict解决
  1. 防止闭包的引用的数据
function fun(){  
   var obj = new Object()
   obj.arr = new Array(200000)
   obj.info = 'xxx'
   return showInfo(){
      return  obj.info
   }
}
let myShowInfo = fun()
myShowInfo()
//应该把大对象改为小对象


obj.info = 'xxx'
let info = obj.info
return showInfo(){
   return  info
}
//虽然也产生闭包,但是闭包的大小只是字符串变量

  1. 游离dom
// 动态在js创建的dom,如果被js的变量引用则无法释放
var ul;
function createNode() {
    var ul = document.createElement('ul'); 
    document.body.append(ul)
}
createNode()
//这里的全局ul会一直指向ul,导致即使removeChild了ul ,也引入被全局window.ul引用 

2. 内存膨胀(memory bloat)

  • 是一个快速上升的过程,然后达到一个平衡,或下降的状态
  • 可能是代码里面没有正确的创建使用内存,导致瞬间飙升

3. 频繁的垃圾回收

大量的临时变量重复创建销毁,多复用变量,常用的变量可提升到全局常用

function foo() {
  for (let i = 0; i < 10000; i++){
    let a = new Array(1000)
  }
}
foo()