关于垃圾回收机制:JavaScript垃圾回收机制

10次阅读

共计 4970 个字符,预计需要花费 13 分钟才能阅读完成。

前言

咱们晓得,JavaScript 中的变量次要分为两种类型:根本类型和援用类型。根本类型的值存储在栈(stack)内存中,而援用类型值的存储须要用到栈内存和堆 (heap) 内存,栈内存保留着变量的堆内存地址,地址指向的堆内存空间保留着具体的值。栈中变量的值在应用完后会被立刻回收,而堆中变量的值不会立刻回收,须要手动回收或应用某种策略进行回收。

JavaScript 具备主动垃圾回收机制,不须要像 C /C++ 语言那样须要开发者手动跟踪内存的应用状况。原理很简略:找出那些不再持续应用的变量,而后开释其占用的内存。为此,垃圾收集器会依照固定的工夫距离(或代码执行中预约的收集工夫),周期性地执行这一操作。

那么,怎么判断哪些变量有用哪些变量没用呢?以函数来说,函数中的局部变量只在函数执行过程中存在,在这个过程中,会为局部变量在栈或堆内存上调配相应的空间,以便存储它们的值。而后在函数中应用这些变量,直到函数执行完结。这时,局部变量就没有存在的必要了,因而能够开释它们的内存以供未来应用。这种状况下很容易判断变量是否有还有存在的必要;但并非所有状况下(如闭包)都这么容易就能得出结论。垃圾收集器必须跟踪有用或无用的变量,对不再有用的变量打上标记,以备未来发出其占用的内存。用于标识无用变量的策略可能会因实现而不同,但具体到浏览器中但实现,通常有两个策略:“标记革除”和“援用计数”

注:以上内容摘自《JavaScript 高级程序设计(第 3 版)》

标记革除

“标记革除”算法是目前宽泛应用最宽泛的垃圾收集算法,它的策略是,用某种标记办法将有用变量和无用变量进行辨别,做好标记后,下一次垃圾收集器工作的时候,就将标记为“垃圾”的变量进行回收,开释其所占内存。

垃圾收集器如何给变量打上标记呢?要了解它的工作形式,咱们要了解一个 JavaScript 内存治理的重要的概念:可达性。

可达性

所谓“可达性”,是指变量能够由“根”登程,通过一层或多层能够被拜访到。如果一个变量从根登程能够被拜访到,那么它就是“可达”的。垃圾回收器将可达的变量视为有用的变量,将那些不可达的变量视为无用的变量,并给无用的变量打上“垃圾”的标记,便于之后的回收操作。

在 JavaScript 中,有一组根本的固有可达值,因为不言而喻的起因无奈删除。例如:

  • 本地函数的局部变量和参数
  • 以后嵌套调用链上的其余函数的变量和参数
  • 全局变量
  • 还有一些其余外部的值

咱们以一段代码为例:

function marry(man,woman){
  man.wife = woman
  woman.husban = man
}
var man = {name:'Tom'}
var woman = {name:'Mary'}
var family = marry(man,woman)

这段代码在浏览器中运行时,内存示意如下:

 

能够看到,manwomanfamily这三个全局变量都挂在到了 window 对象上,依据咱们对“可达性”的定义,这些变量的值都能够从 window 登程被拜访到,它们都是“可达”的,垃圾回收器不会对它们进行回收。

当初,咱们尝试让垃圾回收器回收 man 指向的对象

man = null

这时候的内存图变为

能够看到,此时之前创立的 man 对象的值仍然能够通过 window.woman.husbandwindow.family.father拜访到,因而这个对象仍被视为有用变量,不会被回收。接下来咱们“切断”所有拜访门路:

delete woman.husband delete family.father

此时内存图变为:

此时,因为 man 对象曾经无奈从“根”登程拜访到了,因而它未来要被垃圾回收器当成“垃圾”回收。

为了加深了解,咱们换一种操作,间接从根开始切断它们的拜访门路:

man = null 
woman = null 
family = null

此时很容易得出内存图示:

如图所见,尽管之前定义的 manwomanfamily 相互之间还有关联(援用),但它们曾经无奈从根登程拜访到了,成为了一座“孤岛”。垃圾回收器在运行的时候会将它们标记为“不可达”变量或“垃圾”,在回收的时候将它们“捡起来”。

这便是“可达性”。

说完了“可达性”,咱们来说说垃圾回收器是如何进行标记的。如上文所述,标记只是一种策略,如何标记要看各浏览器具体的实现。

上面咱们以《JavaScript 高级程序设计》中的一段话为例:

垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,能够应用任何标记形式)。而后,它会去掉环境中的变量以及被环境中的变量援用的变量的标记。而在此之后再被加上标记的变量将被视为筹备删除的变量,起因是环境中的变量曾经无法访问到这些变量来。最初,垃圾收集器实现内存革除工作,销毁那些带标记的值并回收它们所占用的内存空间。

标记革除算法分为两个阶段:标记阶段和革除阶段。

上文援用这段话咱们能够这么了解:在标记阶段,垃圾收集器先给内存中的所有变量都打上一个的标记,接着,从所有的“根”登程,沿着援用链路把沿途的所有变量的标记勾销;而那些无奈被拜访到的变量身上仍带有标记,这些变量就是之后要回收的“垃圾”,咱们将它们标记为“垃圾”;在革除阶段,垃圾回收器将那些被标记为“垃圾”的变量收集起来,在适当的机会将它们销毁,并回收它们占用的内存空间。

以上文彻底删除 man 为例形容这一过程。

首先,给内存中所有变量打上标记(途中黄色局部):

接着,沿着根登程,将沿途可拜访到的变量的标记去掉:

这时候能够很容易辨认到,之前 man 指向的对象是“垃圾”,咱们给他打个更显眼的标记:

这样垃圾回收器就很容易地找到这个“垃圾”将它回收了。

为了加深了解,咱们再以一段简略的闭包代码为例:

function makeGirlfriend(name){
  var girlfriend = {name:name}
  return function(newName){girlfriend.name = newName}
}
var makeAGirlfriend = makeGirlfriend('fanbingbing')
makeAGirlfriend('libingbing')

这段代码执行后,函数 makeGirlfriend 中的局部变量 girlfriend 并不会被立刻回收,因为它未来可能会被再次应用。换一种说法,代码 var myGirlfriend = makeGirlfriend('fanbingbing')返回了一个函数,这个函数的外部的作用域链上保留了 makeGirlfriend 函数的流动对象上的 girlfriend 的援用,也就是 JS 外部能够以 myGirlfriend->scopeChain->makeGirlfriend->girlfriend 的门路拜访到它,它是“可达”的, 它所占用的内存不会被回收。这也是咱们常说的闭包容易引起内存透露,要善用 / 慎用闭包的起因。

援用计数

另一种不太常见的垃圾回收策略叫 援用计数。援用计数的含意是跟踪记录每个援用类型值被援用的次数,每多 1 次援用,次数加 1,每勾销 1 次援用,次数减 1。当这个值的援用次数变成 0 时,阐明没方法再拜访这个值了,这个值将被视为“垃圾”并回收。

Netspace Navigator 是最早应用援用计数策略的浏览器,但它很快就遇到一个重大但问题:循环援用。循环援用是指对象之间相互援用的景象,使得它们的援用计数永远不可能变为 0,如以下代码:

function problem(){var boy = {}
    var girl = {}
    boy.sister = girl
    girl.brother = boy
}

boy 和 girl 别离通过各自的属性 sister、brother 援用对方,在函数执行完后,boy 和 girl 指向的对象依然存在,因为它们的援用次数永远不会是 0。如果这个函数被反复屡次调用,就会导致大量内存得不到回收。为此,Netspace 在 Navigator 的后续版本中弃用了援用计数的策略,改用标记革除来实现垃圾回收器。

另外,IE 的晚期版本(IE8 及以前)版本中,有一部分对象并不是原生 JavaScript 对象,如 BOM 和 DOM 对象。它们是应用 C ++ 以 COM(component Object Model, 组件对象模型)对象的模式实现的,而 COM 对象的收集机制采纳的就是援用计数策略,因而,当在这些版本的浏览器中,当呈现 JavaScript 对象与 DOM 或 BOM 对象互相援用的景象时,即便 IE 的 JS 引擎是用标记革除策略来实现的,此时这些 DOM 和 BOM 对象仍不会被回收。

性能问题

垃圾回收器是周期性运行的,而且如果为变量调配的内存数量很可观,那么回收工作量也将是相当大的。在这种状况下,确定垃圾回收器的运行距离 / 运行机会是一个十分重要的问题。IE6 也是因而而身败名裂的:IE6 的垃圾收集器是依据内存调配量运行的,具体来说就是,当内存调配量达到预设的临界值时,垃圾回收器就会运行。这导致的问题就是,一个利用中可能始终保有这么多变量,这样就会频繁地登程垃圾回收器的运行。咱们晓得,JS 引擎是单线程运行的,当垃圾回收器运行的时候,为了保障逻辑的正确性,其余的 JavaSript 脚本将被暂停执行。垃圾回收器频繁的运行将会导致失常的业务代码得不到无效执行,产生很“卡”的景象。

为了解决 IE6 卡顿的问题,IE7 扭转了垃圾回收器的工作形式:将触发垃圾回收器运行的临界值变为动静调整,从而无效防止了垃圾回收器的频繁触发,极大晋升了 IE 在运行蕴含大量 JavaScript 的页面的性能。

v8 引擎的优化

虽说目前支流浏览器都是应用的“标记革除”的垃圾回收策略,但单纯的标记革除还是有它的毛病。咱们晓得,援用类型的值存储在堆中,JS 引擎在创立这些值的时候会给它们别离开拓独立的堆内存空间。因为不是每个援用类型的值占用的内存大小都一样,因而在保留这些值时和垃圾收集器将它们回收后,都不可避免地在它们之间存在一些未应用的内存空间,也就是内存碎片,就像背包里装了很多货色但总有间隙一样。这些内存碎片有可能很小而不足以用于未来寄存新的对象,为此可能会提前触发垃圾回收,而这次回收本来是不必要的。如何防止这样的资源节约是一个待解决的问题。

上面咱们以 google 的 V8 引擎为例,说说它做了哪些优化。

分代回收

V8 引擎的垃圾回收基于分带回收机制,它将内存分为“新生代”和“老生代”。新生代,顾名思义,新生代中寄存的就是值那些存活工夫比拟短,来来即走的对象;相同的,老生代内存中寄存的是那些存活工夫比拟长,或者新生代中寄存不下的大对象。

新生代和老生代应用了不同的回收策略,并且它们被调配了不同大小的内存空间:

  • 新生代应用了较小的内存大小,用 Scavenge 算法将新生代内存一分为二:from 区和 to 区,分配内存时,对象存储在 from 区,进行垃圾回收时,将 from 区的存活对象复制到 to 区,非存活对象占用内存被开释,而后二者角色产生对换。
  • 老生代应用了较大的内存大小,而且老生代中的对象可能从新生代中“降职”过去。简略说就是,新生代中的存活对象在复制过程中,“年龄”会逐步增长,当它足够“老”时,就会“降职”为老对象,寄存到老生代内存中。老生代内存应用了 Mark-Sweep(标记革除)和 Mark-Compact(标记整顿)相结合的策略进行垃圾回收。Mark-Sweep 正如上文所述;Mark-Compact 是对 Mark-Sweep 的补充,次要解决内存碎片的问题,具体做法是在整顿过程中,将活着的对象往一边挪动,挪动实现后,活着对象那一侧之外的内存会被回收。

这部分更具体的内容请移步 https://blog.csdn.net/wu_xianqiang/article/details/90736087。

增量标记

进行垃圾回收时,JS 引擎会暂停其余代码的执行。如果垃圾回收的工夫过长,将会给应用程序带来显著的期待。V8 引擎为此做了“增量标记”的优化,即垃圾回收器进行垃圾回收时先标记一部分 / 一段时间,而后停下来让程序代码持续运行,之后垃圾回收器再次运行时持续标记。直到标记工作实现后,垃圾回收器再进行清理工作。

参考资料

  1. 《JavaScript 高级程序设计 第 3 版》
  2. https://blog.csdn.net/wu_xianqiang/article/details/90736087

正文完
 0