JavaScript垃圾回收机制(引用计数,标记清除,性能优化)

目录
  • 一、前言
  • 二、何为垃圾
  • 三、垃圾回收
  • 四、可达性(Reachability)
  • 五、可达性举例
    • 层次关联
    • 相互关联
    • 可达孤岛
  • 六、垃圾回收算法
    • 引用计数
    • 标记清除
  • 七、性能优化
    • 分代回收
    • 增量收集
    • 空闲收集
  • 八、总结

一、前言

垃圾回收是JavaScript的隐藏机制,我们通常无需为垃圾回收劳心费力,只需要专注功能的开发就好了。但是这并不意味着我们在编写JavaScript的时候就可以高枕无忧了,伴随着我们实现的功能越来越复杂,代码量越积越大,性能问题就变的越来越突出。如何写出执行速度更快,而且占用内存更小的代码是程序员永无止歇的追求。一个优秀的程序员总是能在极其有限的资源下,实现惊人的效果,这也正式芸芸众生和高高在上的神祗之间的区别。

二、何为垃圾

代码执行在计算机的内存中,我们在代码中定义的所有变量、对象、函数都会在内存中占用一定的内存空间。在计算机中,内存空间是非常紧张的资源,我们必须时时刻刻注意内存的占用量,毕竟内存条非常贵!如果一个变量、函数或者对象在创建之后不再被后继的代码执行所需要,那么它就可以被称作垃圾。

虽然从直观上理解垃圾的定义非常容易,但是对于一个计算机程序来说,我们很难在某一时刻断定当前存在的变量、函数或者对象在未来不再使用。为了降低计算机内存的开销,同时又保证计算机程序正常执行,我们通常规定满足以下任一条件的对象或者变量为垃圾:

  • 没有被引用的对象或者变量;
  • 无法访问到的对象(多个对象之间循环引用);

没有被引用的变量或者对象相当于一座没有门的房子,我们永远都无法进入其中,因此不可能在用到它们。无法访问到的对象之间虽然具备连通性,但是仍然无法从外部进入其中,因此也无法再次被利用。满足以上条件的对象或者变量,在程序未来执行过程中绝对不会再次被采用,因此可以放心的当作垃圾回收。

当我们通过以上定义明确了需要丢弃的对象,是否就意味着剩余的变量、对象中就没有垃圾了呢?

不是的!我们当前分辨出的垃圾只是所有垃圾的一部分,仍然会有其他垃圾不满足以上条件,但是也不会再次被使用了。

这是否可以说满足以上定义的垃圾是“绝对垃圾”,其他隐藏在程序中的为“相对垃圾”呢?

三、垃圾回收

垃圾回收机制(GC,Garbage Collection)负责在程序执行过程中回收无用的变量和内存占用的空间。一个对象虽然没有再次使用的可能,但是仍然存在于内存中的现象被称为内存泄漏。内存泄漏是非常危险的现象,尤其在长时间运行的程序中。如果一个程序出现了内存泄漏,它占用的内存空间就会越来越多,直至耗尽内存。

字符串、对象和数组没有固定的大小,所以只有当它们大小已知时才能对它们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都要分配内存才存储这个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便它们能够被再次利用;否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。

JavaScript的垃圾回收机制会间歇性的检查没有用途的变量、对象(垃圾),并释放条它们占用的空间。

四、可达性(Reachability)

不同的编程语言采用不同的垃圾回收策略,例如C++就没有垃圾回收机制,所有的内存管理靠程序员本身的技能,这也就造成了C++比较难以掌握的现状。JavaScript采用可达性管理内存,从字面意思上看,可达的意思是可以到达,也就是指程序可以通过某种方式访问、使用的变量和对象,这些变量所占用的内存是不可以被释放的。

JavaScript规定了一个固有的可达值集合,集合中的值天生就是可达的:

当前正在执行的函数上下文(包括函数内的局部变量、函数的参数等);当前嵌套调用链上的其他函数、它们的局部变量和参数;全局变量;其他内部的变量;

以上变量称为,是可达性树的顶层节点。

如果一个变量或则对象,直接或者间接的被根变量应用,则认为这个变量是可达的。

换一个说法,如果一个值能够通过根访问到(例如,A.b.c.d.e),那么这个值就是可达的。

五、可达性举例

层次关联

let people = {
    boys:{
        boys1:{name:'xiaoming'},
        boys2:{name:'xiaojun'},
    },
    girls:{
        girls1:{name:'xiaohong'},
        girls2:{name:'huahua'},
    }
};

以上代码创建了一个对象,并赋值给了变量people,变量people中包含了两个对象boysgirlsboysgirls中又分别包含了两个子对象。这也就创建了一个包含了3层引用关系的数据结构(不考虑基础类型数据的情况下),

如下图:

其中,people节点由于是全局变量,所以天然可达。boysgirls节点由于被全局变量直接引用,构成间接可达。boys1boys2girls1girls2由于被全局变量间接应用,可以通过people.boys.boys访问,因此也属于可达变量。

如果我们在以上代码的后面加上以下代码:

people.girls.girls2 = null;
people.girls.girls1 = people.boys.boys2;

那么,以上引用层次图将会变成如下形式:

其中,girls1girls2由于和grils节点断开连接,从而变成了不可达节点,意味着将被垃圾回收机制回收。

而如果此时,我们再执行以下代码:

people.boys.boys2 = null;

那么引用层次图将变成如下形式:

此时,虽然boys节点和boys2节点断开了连接,但是由于boys2节点和girls节点之间存在引用关系,所以boys2仍然属于可达的,不会被垃圾回收机制回收。

以上关联关系图证明了为何称全局变量等值为,因为在关联关系图中,这一类值通常作为关系树的根节点出现。

相互关联

let people = {
    boys:{
        boys1:{name:'xiaoming'},
        boys2:{name:'xiaojun'},
    },
    girls:{
        girls1:{name:'xiaohong'},
        girls2:{name:'huahua'},
    }
};
people.boys.boys2.girlfriend = people.girls.girls1;	//boys2引用girls1
people.girls.girls1.boyfriend = people.boys.boys2;	//girls1引用boys2

以上代码在boys2girls1之间创建了一个相互关联的关系,关系结构图如下:

此时,如果我们切断boysboys2之间的关联:

delete people.boys.boys2;

对象之间的关联关系图如下:

显然,并没有不可达的节点出现。

此时,如果我们切断boyfriend关系连接:

delete people.girls.girls1;

关系图变为:

此时,虽然boys2girls1之间还存在girlfriend关系,但是,boys2以及变为不可达节点,将被垃圾回收机制收回。

可达孤岛

let people = {
    boys:{
        boys1:{name:'xiaoming'},
        boys2:{name:'xiaojun'},
    },
    girls:{
        girls1:{name:'xiaohong'},
        girls2:{name:'huahua'},
    }
};
delete people.boys;
delete people.girls;

以上代码形成的引用层次图如下:

此时,虽然虚线框内部的对象之间仍然存在相互引用的关系,但是这些对象同样是不可达的,并会被垃圾回收机制删除。这些节点已经和脱离了关系,变的不可达。

六、垃圾回收算法

引用计数

所谓引用计-数,顾名思义,就是每次对象被引用时都进行计数,增加引用就加一,删除引用就减一,如果引用数变为0,那么就被认定为垃圾,从而删除对象回收内存。

举个例子:

let user = {username:'xiaoming'};//对象被user变量引用,计数+1
let user2 = user;//对象被新的变量引用,计数+1
user = null;//变量不再引用对象,计数-1
user2 = null;//变量不再引用对象,奇数-1
//此时,对象引用数为0,会被删除

虽然看起来引用计数方法非常合理,实际上,采用引用计数方法的内存回收机制存在明显的漏洞。

例如:

let boy = {};
let girl = {};
boy.girlfriend = girl;
girl.boyfriend = boy;
boy = null;
girl = null;

以上代码在boygirl之间存在相互引用,计数删掉boygirl内的引用,二者对象并不会被回收。由于循环引用的存在,两个匿名对象的引用计数永远不会归零,也就产生了内存泄漏。

C++中存在一个智能指针shared_ptr)的概念,程序员可以通过智能指针,利用对象析构函数释放引用计数。但是对于循环引用的状况就会产生内存泄漏。

好在JavaScript已经采用了另外一种更为安全的策略,更大程度上避免了内存泄漏的风险。

标记清除

标记清除mark and sweep)是JavaScript引擎采取的垃圾回收算法,其基本原理是从出发,广度优先遍历变量之间的引用关系,对于遍历过的变量打上一个标记(优秀员工徽章),最后删除没有标记的对象。

算法基本过程如下:

  • 垃圾收集器找到所有的,并颁发优秀员工徽章(标记);
  • 然后它遍历优秀员工,并将优秀员工引用的对象同样打上优秀员工标记;
  • 反复执行第2步,直至无新的优秀员工加入;
  • 没有被标记的对象都会被删除。

举个栗子:

如果我们程序中存在如下图所示的对象引用关系:

我们可以清晰的看到,在整个图片的右侧存在一个“可达孤岛”,从出发,永远无法到达孤岛。但是垃圾回收器并没有我们这种上帝视角,它们只会根据算法会首先把根节点打上优秀员工标记。

然后从优秀员工出发,找到所有被优秀员工引用的节点,如上图中虚线框中的三个节点。然后把新找到的节点同样打上优秀员工标记。

反复执行查找和标记的过程,直至所有能找到的节点都被成功标记。

最终达到下图所示的效果:

由于在算法执行周期结束之后,右侧的孤岛仍然没有标记,因此会被垃圾回收器任务无法到达这些节点,最终被清除。

如果学过数据结构和算法的童鞋可能会惊奇的发现,这不就是图的遍历吗,类似于连通图算法。

七、性能优化

垃圾回收是一个规模庞大的工作,尤其在代码量非常大的时候,频繁执行垃圾回收算法会明显拖累程序的执行。JavaScript算法在垃圾回收上做了很多优化,从而在保证回收工作正常执行的前提下,保证程序能够高效的执行。

性能优化采取的策略通常包括以下几点:

分代回收

JavaScript程序在执行过程中会维持相当量级的变量数目,频繁扫描这些变量会造成明显的开销。但是这些变量在生命周期上各有特点,例如局部变量会频繁的创建,迅速的使用,然后丢弃,而全局变量则会长久的占据内存。JavaScript把两类对象分开管理,对于快速创建、使用并丢弃的局部变量,垃圾回收器会频繁的扫描,保证这些变量在失去作用后迅速被清理。而对于哪些长久把持内存的变量,降低检查它们的频率,从而节约一定的开销。

增量收集

增量式的思想在性能优化上非常常见,同样可以用于垃圾回收。在变量数目非常大时,一次性遍历所有变量并颁发优秀员工标记显然非常耗时,导致程序在执行过程中存在卡顿。所以,引擎会把垃圾回收工作分成多个子任务,并在程序执行的过程中逐步执行每个小任务,这样就会造成一定的回收延迟,但通常不会造成明显的程序卡顿。

空闲收集

CPU即使是在复杂的程序中也不是一直都有工作的,这主要是因为CPU工作的速度非常快,外围IO往往慢上几个数量级,所以在CPU空闲的时候安排垃圾回收策略是一种非常有效的性能优化手段,而且基本不会对程序本身造成不良影响。这种策略就类似于系统的空闲时间升级一样,用户根本察觉不到后台的执行。

八、总结

本文的主要任务是简单的结束垃圾回收的机制、常用的策略和优化的手段,并不是为了让大家深入了解引擎的后台执行原理。

通过本文,你应该了解:

垃圾回收是JavaScript的特性之一,执行在后台,无需我们操心;垃圾回收的策略是标记清除,按照可达性理论筛选并清除垃圾;标记清楚策略可以避免可达孤岛带来的内存泄漏

到此这篇关于JavaScript垃圾回收机制(引用计数,标记清除,性能优化)的文章就介绍到这了,更多相关JS垃圾回收机制 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 跟我学习javascript的垃圾回收机制与内存管理

    一.垃圾回收机制-GC Javascript具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存. 原理:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存. JavaScript垃圾回收的机制很简单:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是实时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行. 不再使用的变量也就是生命周期结束的变量,当然只可能是局部变量,全局变量的生

  • js闭包和垃圾回收机制示例详解

    前言 闭包和垃圾回收机制常常作为前端学习开发中的难点,也经常在面试中遇到这样的问题,本文记录一下在学习工作中关于这方面的笔记. 正文 1.闭包 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现.作为一个JavaScript开发者,理解闭包十分重要. 1.1闭包是什么? 闭包就是一个函数引用另一个函数的变量,内部函数被返回到外部并保存时产生,(内部函数的作用域链AO使用了外层函数的AO) 因为变量被引用着所以不会被回收,因此可以用来封装一个私有

  • 谈谈JavaScript中的垃圾回收机制

    JavaScript 具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存. 在编写 JavaScript 程序时,开发人员不用再关心内存使用问题,所需内存的分配以及无用内存的回收完全实现了自动管理. 这种垃圾收集机制的原理其实很简单:找出那些不再继续使用的变量,然后释放其占用的内存.为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间), 周期性地执行这一操作. 具体到浏览器中的实现,则通常有两个策略,分别为标记清除和引用计数. 一.标记清除 JavaScri

  • 对于js垃圾回收机制的理解

    原理 找到不再被使用的变量,然后释放其占用的内存,但这个过程不是时时的,因为其开销比较大, 所以垃圾回收器会按照固定时间间隔周期性的执行 回收方式 a.标记清除 当变量进入环境时,将这个变量标记为"进入环境";当变量离开环境时,则将其标记为"离开环境". 标记"离开环境"的就回收内存 b.引入计数(低级浏览器) 当变量声明,第一次赋值时记为1,然后当这个变量值改变时,记录为0,将计数为0的回收 内存泄露 a.意外的全局变量引起的内存泄露 原因:

  • 一文带你了解JavaScript垃圾回收机制

    目录 1. 概述 2. 内存管理 3. 垃圾回收 4. GC算法介绍 5. 引用计数算法 1. 引用计数优缺点 6. 标记清除算法 1. 标记清除算法优缺点 7. 标记整理算法 8. 执行时机 9. V8引擎 1. 垃圾回收策略 2. 回收新生代对象 3. 回收老生代对象 4. V8垃圾回收总结 10. Performance工具介绍 1. 内存问题的体现 2. 监控内存的几种方式 3. 任务管理器监控内存 4. TimeLine记录内容 5. 堆快照查找分离DOM 6. 判断是否存在频繁GC

  • 详解JavaScript的垃圾回收机制

    目录 为什么需要垃圾回收(GC) 什么是垃圾回收 垃圾产生 垃圾回收策略 引用计数标记 循环引用引发的问题 解决方法 引用计数算法的优缺点 标记清除算法 核心思想 标记清除算法优缺点 标记整理算法 V8引擎的垃圾回收 回收新生代对象 对象晋升机制 回收老生代对象 参考文档: 总结 为什么需要垃圾回收(GC) 程序和人一样,生活时间长了会产生垃圾,程序在运行过程中也会产生垃圾,垃圾积攒过多后,会导致程序运行速度变慢. 在JavaScript中的字符串.对象.数组等数据的内存是不固定的,只有真正使用

  • JavaScript的垃圾回收机制与内存管理

    如果我们想要优化性能,首先我们必须得了解JavaScript中的垃圾回收机制,这样可以将很多没有被使用到的变量从内存中清除掉,腾出更多的内存空间,给别的变量分配内存空间. JavaScript中的垃圾回收机制 引言 本篇文章将讲解一下javascript的垃圾回收机制.同时,我们必须先具备作用域链的概念,不懂的小伙伴可以先花5分钟观看一下这篇文章,简单了解一下作用域链的知识--从零开始讲解JavaScript中作用域链的概念及用途 正文 一.垃圾回收机制 在JavaScript中,具有自动垃圾回

  • JavaScript 垃圾回收机制分析

    在公司经常会听到大牛们讨论时说道内存泄露神马的,每每都惊羡不已,最近精力主要用在了Web 开发上,读了一下<JavaScript高级程序设计>(书名很唬人,实际作者写的特别好,由浅入深)了解了一下JavaScript垃圾回收机制,对内存泄露有了一定的认识. 和C#.Java一样JavaScript有自动垃圾回收机制,也就是说执行环境会负责管理代码执行过程中使用的内存,在开发过程中就无需考虑内存分配及无用内存的回收问题了.JavaScript垃圾回收的机制很简单:找出不再使用的变量,然后释放掉其

  • JavaScript垃圾回收机制(引用计数,标记清除,性能优化)

    目录 一.前言 二.何为垃圾 三.垃圾回收 四.可达性(Reachability) 五.可达性举例 层次关联 相互关联 可达孤岛 六.垃圾回收算法 引用计数 标记清除 七.性能优化 分代回收 增量收集 空闲收集 八.总结 一.前言 垃圾回收是JavaScript的隐藏机制,我们通常无需为垃圾回收劳心费力,只需要专注功能的开发就好了.但是这并不意味着我们在编写JavaScript的时候就可以高枕无忧了,伴随着我们实现的功能越来越复杂,代码量越积越大,性能问题就变的越来越突出.如何写出执行速度更快,

  • PHP垃圾回收机制引用计数器概念分析

    如果你安装了xdebug,就可以用xdebug_debug_zval()显示"zval"的信息了.如下: 复制代码 代码如下: <?php$str = "jb51.net";xdebug_debug_zval('str'); 结果: str:(refcount=1, is_ref=0),string 'jb51.net' (length=10) 只有当变量容器在"refcount"变成0时就被销毁.当你unset()一个变量时,想要的&qu

  • Python的垃圾回收机制深入分析

    一.概述: Python的GC模块主要运用了"引用计数"(reference counting)来跟踪和回收垃圾.在引用计数的基础上,还可以通过"标记-清除"(mark and sweep)解决容器对象可能产生的循环引用的问题.通过"分代回收"(generation collection)以空间换取时间来进一步提高垃圾回收的效率. 二.引用计数 在Python中,大多数对象的生命周期都是通过对象的引用计数来管理的.从广义上来讲,引用计数也是一种垃

  • JVM的垃圾回收机制详解和调优

    文章来源:matrix.org.cn 作者:ginger547 1.JVM的gc概述 gc即垃圾收集机制是指jvm用于释放那些不再使用的对象所占用的内存.java语言并不要求jvm有gc,也没有规定gc如何工作.不过常用的jvm都有gc,而且大多数gc都使用类似的算法管理内存和执行收集操作. 在充分理解了垃圾收集算法和执行过程后,才能有效的优化它的性能.有些垃圾收集专用于特殊的应用程序.比如,实时应用程序主要是为了避免垃圾收集中断,而大多数OLTP应用程序则注重整体效率.理解了应用程序的工作负荷

  • 浅谈Java垃圾回收机制

    一.什么是垃圾 java中,什么样的对象是垃圾?有人说:没有被引用的对象就是垃圾对象.我一开始对此也是深信不疑的,但是当年我这么回答面试官的时候,得到的是一个大大的白眼. 判断一个对象是否是垃圾,有两种算法,一种是引用计数法,但是,这种方法解决不了循环引用的问题. /**循环问题*/ public class Demo{ public Demo instance; public static void main(String[] args) { Demo a=new Demo(); Demo b

  • javascript 内存回收机制理解

    1.唠叨 javascript语言是一门优秀的脚本语言.其中包含脚本语言的灵活性外还拥有许多高级语言的特性.例如充许构建和实例化一个对象,垃圾回收机制(GC:Garbage Collecation).通常我们使用new创建对象,GC负责回收对象占用内存区域.因此了解GC,可以加深对javascript垃圾回收机制的理解. 2.用局部变量和全局变量解释GC GC在回收内存时,首先会判断该对象是否被其它对象引用.在确定没有其它对象引用便释放该对象内存区域.因此如何确定对象不再被引用是GC的关键所在.

  • 简单谈谈PHP的垃圾回收机制

    1.每一个变量定义时都保存在一个叫zval的容器里面,这里面包含了数量的类型和和值,还包含了一个refcount(理解为存在几个变量个数)和is_ref(理解为是否为引用变量)两个额外信息,当变量被引用一次refcount就会+1,当你unset一下之后这个值就会减1直到为0就会从内存中删除 2.定义一个变量的时候并不是每次都会扩大预定于值,因为PHP会在内存中先预占用一个空间,等你声明变量的时候就会分配给你,但是当你超出这个预占用空间之后,那么它就会增加空间,但是等你删除变量时候这个空间容量不

随机推荐