Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript的垃圾回收机制 #36

Open
pfan123 opened this issue Apr 17, 2019 · 0 comments
Open

JavaScript的垃圾回收机制 #36

pfan123 opened this issue Apr 17, 2019 · 0 comments

Comments

@pfan123
Copy link
Owner

pfan123 commented Apr 17, 2019

前言

无论高级语言,还是低级语言。内存的管理都是:

  1. 内存分配:申明变量、函数、对象,系统会自动分配内存
  2. 内存使用:读写内存,使用变量、函数等
  3. 内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存

释放内存处理方式,各种语言都有自己的垃圾回收(garbage collection, 简称GC)机制。做GC的第一步是判断堆中存的是数据还是指针,是指针的话,说明它被指向活跃的对象。有3种判断方法:

  1. Conservative:存储格式是地址,C/C++有用到这种算法。
  2. Compiler hints:对于静态语言,比如Java,编译器是知道它是不是指针的,所以可以用这种。
  3. Tagged pointersJavaScript用的是这种,在字末位进行标识,1为指针。

JavaScript 内存问题

内存泄漏

什么情况下会内存泄漏 memory leak ?可以这么理解,就是有些代码本来应该要被回收的,但是没有被回收,所以一直占用着操作系统的内存,从而越积越多。一般的内存泄漏其实无关紧要,可怕的是内存泄漏引起的堆积,导致GC一直没办法使用所占用的内存给其他程序使用。

内存溢出

内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现 out of memory;比如申请了一个 integer, 但给它存了 long 才能存下的数,那就是内存溢出。

注意: memory leak 会最终会导致 out of memory

JavaScript 垃圾管理

JavaScript 有自动垃圾收集机制,找出不再继续使用的值,然后释放其占用的内存。垃圾收集器会每隔固定的时间段就执行一次释放操作。 在 JavaScript 中,最常用的是通过标记清除的算法来找到哪些对象是不再继续使用的,因此 a = null 其实仅仅只是做了一个释放引用的操作,让 a 原本对应的值失去引用,脱离执行环境,这个值会在下一次垃圾收集器执行操作时被找到并释放。

  • 在局部作用域中,当函数执行完毕,局部变量也就没有存在的必要了,因此垃圾收集器很容易做出判断并回收。但是全局变量什么时候需要自动释放内存空间则很难判断,因此在我们的开发中,需要尽量避免使用全局变量,以确保性能问题。
  • 以 Google 的 V8 引擎为例,在 V8 引擎中所有的 JAVASCRIPT 对象都是通过堆来进行内存分配的。当我们在代码中声明变量并赋值时,V8 引擎就会在堆内存中分配一部分给这个变量。如果已申请的内存不足以存储这个变量时,V8 引擎就会继续申请内存,直到堆的大小达到了 V8 引擎的内存上限为止(默认情况下,V8引擎的堆内存的大小上限在 64 位系统中为 1464MB,在32位系统中则为 732MB)。
  • 另外,V8 引擎对堆内存中的 JAVASCRIPT 对象进行分代管理。新生代:新生代即存活周期较短的 JAVASCRIPT 对象,如临时变量、字符串等; 老生代:老生代则为经过多次垃圾回收仍然存活,存活周期较长的对象,如主控制器、服务器对象等。

javascript回收方法

V8 引擎中使用两种优化方法:
  1. 分代回收;
  2. 增量 GC
  3. 目的是通过对象的使用频率、存在时长区分新生代与老生代对象。多回收新生代区(young generation),少回收老生代区(tenured generation),减少每次需遍历的对象,从而减少每次 GC 的耗时。
  4. 把需要长耗时的遍历、回收操作拆分运行,减少中断时间,但是会增大上下文切换开销.
回收算法:

大部分垃圾回收语言用的算法称之为 Mark-and-sweep

(1) 引用计数 (reference counting)

在内存管理环境中,对象 A 如果有访问对象 B 的权限,叫做对象 A 引用对象 B。引用计数的策略是将“对象是否不再需要 ”简化成“ 对象有没有其他对象引用到它,如果没有对象引用这个对象,那么这个对象将会被回收。

let obj1 = { a: 1 }; // 一个对象(称之为 A)被创建,赋值给 obj1,A 的引用个数为 1 ,很显然引用次数不为0, 无法垃圾收集
let obj2 = obj1; // A 的引用个数变为 2

obj1 = 0; // A 的引用个数变为 1
obj2 = 0; // A 的引用个数变为 0,此时对象 A 就可以被垃圾回收了

mozilla 文档 中很形象的一个例子:

var o = { 
  a: {
    b:2
  }
}; 
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集


var o2 = o; // o2变量是第二个对“这个对象”的引用

o = 1;      // 现在,“这个对象”只有一个o2变量的引用了,“这个对象”的原始引用o已经没有

var oa = o2.a; // 引用“这个对象”的a属性
               // 现在,“这个对象”有两个引用了,一个是o2,一个是oa

o2 = "yo"; // 虽然最初的对象现在已经是零引用了,可以被垃圾回收了
           // 但是它的属性a的对象还在被oa引用,所以还不能回收

oa = null; // a属性的那个对象现在也是零引用了
           // 它可以被垃圾回收了

当对象被引用次数为 0 时,就被回收。潜在的一个问题是:循环引用时,两个对象都至少被引用了一次,将不能自动被回收,导致内存泄露。

function func() {
    let obj1 = {};
    let obj2 = {};

    obj1.a = obj2; // obj1 引用 obj2
    obj2.a = obj1; // obj2 引用 obj1
}

当函数 func 执行结束后,返回值为 undefined,所以整个函数以及内部的变量都应该被回收,但根据引用计数方法,obj1 和 obj2 的引用次数都不为 0,所以他们不会被回收。

要解决循环引用的问题,最好是在不使用它们的时候手工将它们设为空。上面的例子可以这么做:

obj1 = null;
obj2 = null;

(2)标记清除 (mark and sweep)
这是当前主流的 GC 算法,从2012年起,所有现代浏览器都使用了** 标记-清除(Mark-Sweep)**垃圾回收算法。所有对 JavaScript 垃圾回收算法的改进都是基于 标记-清除算法 的改进,并没有改进 标记-清除算法 本身和它对“对象是否不再需要”的简化定义。

Mark-Sweep(标记清除)分为标记清除两个阶段,在标记阶段会遍历堆中的所有对象,然后标记活着的对象,在清除阶段中,会将死亡的对象进行清除。

当对象,无法从根对象沿着引用遍历到,即不可达(unreachable),进行清除。JAVASCRIPT 中有个全局对象,浏览器中是 window。定期的,垃圾回收期将从这个全局对象开始,找所有从这个全局对象开始引用的对象,再找这些对象引用的对象...对这些活着的对象进行标记,这是标记阶段清除阶段就是清除那些没有被标记的对象。

有了标记清除法,循环引用不再是问题了。在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。

常见 JavaScript 内存泄漏

1.意外的全局变量(全局变量不会被标记清除法清除)

JavaScript 处理未定义变量的方式比较宽松:未定义的变量会在全局对象创建一个新变量。在浏览器中,全局对象是 window

function foo(arg) {
    bar = "this is a hidden global variable";  // 意外挂在在 window 全局变量,导致内存泄漏
}

解决方法:

delete window.bar

2.被遗忘的计时器或回调函数

  • 计数器函数,一直占用内存
// 计数器一直存在会一直占用内存,计数器结束需要做释放处理
var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 处理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);
  • 事件回调函数(老式浏览器如 IE 6 无法做回收,新版浏览器已经可以处理)
var element = document.getElementById('button');
function onClick(event) {
    element.innerHTML = 'text';
}
element.addEventListener('click', onClick);  
//针对老式浏览器,回收需要移除事件回调
element.removeEventListener('click', onClick);  

3.闭包递归

闭包的特性:1.函数嵌套函数,2.函数内部可以引用外部的参数和变量,3.参数和变量不会被垃圾回收机制回收

闭包由于存在变量引用其返回的匿名函数,导致作用域无法得到释放。 最新版本的浏览器中,可以通过标记清除的方式处理掉闭包的作用域导致的内存泄漏,但闭包的变量(匿名函数、参数变量)会常驻内存,会造成一定的性能问题

let index = 0
function readData() {
  let buf = new Buffer(1024 * 1024 * 100)
  buf.fill('g')  

  return function fn() { // 此处会把 return 出来的函数挂在在 window 下,作用域无法清除
    index++   // 引入局外变量,内存无法清除
    if (index < buf.length) { 
      return buf[index-1]   // buf 不会被清除,需要手动清除
    } else {
      return ''
    } 
  }
}

const data = readData()
const next = data()

内存剖析工具方法

Chrome浏览器方法

Chrome 提供了一套很棒的检测 JavaScript 内存占用的工具 Memory , 提供 Heap snapshot (堆内存截图)、allocation instrumentation on timeline ( 内存timeline上的分配检测)、allocation sampling (内存分配抽样)

Node 命令行查看内存状态方法

console.log(process.memoryUsage());
// { rss: 27709440,
//  heapTotal: 5685248,
//  heapUsed: 3449392,
//  external: 8772 }

process.memoryUsage返回一个对象,包含了 Node 进程的内存占用信息。该对象包含四个字段,单位是字节,含义如下。

  • rss(resident set size)`:所有内存占用,包括指令区和堆栈。
  • heapTotal:"堆"占用的内存,包括用到的和没用到的。
  • heapUsed:用到的堆的部分。
  • external: V8 引擎内部的 C++ 对象占用的内存。

判断内存泄漏,以heapUsed字段为准。

WeakSet 和 WeakMap

通过前几个内存泄漏示例我们会发现如果我们一旦疏忽,就会容易地引发内存泄漏的问题。及时清除引用,回收内存非常重要。但是实际生产过程中,有可能不清楚上下文,导致内存泄漏。最好能有一种方法,在新建引用的时候就声明,哪些引用必须手动清除,哪些引用可以忽略不计,当其他引用消失以后,垃圾回收机制就可以释放内存。这样就能大大减轻程序员的负担,只要清除主要引用就可以了。

ES6 考虑到了这一点,推出了两种新的数据结构:WeakSetWeakMap。它们对于值的引用都是不计入垃圾回收机制的,弱引用。

const wm = new WeakMap();

const element = document.getElementById('example');

wm.set(element, 'some information');
wm.get(element) // "some information"

上面代码中,先新建一个 Weakmap 实例。然后,将一个 DOM 节点作为键名存入该实例,并将一些附加信息作为键值,一起存放在 WeakMap 里面。这时,WeakMap 里面对element的引用就是弱引用,不会被计入垃圾回收机制。

也就是说,DOM 节点对象的引用计数是1,而不是2。这时,一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放。Weakmap 保存的这个键值对,也会自动消失。

基本上,如果要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap

参考阅读:

JavaScript深入理解之垃圾收集

阮一峰-JavaScript 内存泄漏教程

阮一峰-JavaScript 运行机制详解:再谈Event Loop

4类 JavaScript 内存泄漏及如何避免

10分钟了解JS堆、栈以及事件循环的概念

(js队列,堆栈) (FIFO,LIFO)

从Promise到Event Loop

Node.js 内存管理和 V8 垃圾回收机制

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant