公司网站上传文章,html5营销网站建设,html5 网站模板,平山县建设局网站前言
JavaScript提供了高效的内存管理机制#xff0c;它的垃圾回收功能是自动的。在我们创建新对象、函数、原始类型和变量时#xff0c;所有这些编程元素都会占用内存。那么JavaScript是如何管理这些元素并在它们不再使用时清理它们的呢#xff1f;
在本节中#xff0c;…前言
JavaScript提供了高效的内存管理机制它的垃圾回收功能是自动的。在我们创建新对象、函数、原始类型和变量时所有这些编程元素都会占用内存。那么JavaScript是如何管理这些元素并在它们不再使用时清理它们的呢
在本节中我们将讨论JavaScript中的内存管理的所有内部过程。我们将关注以下主题 JavaScript中的内存生命周期 JavaScript中的内存分配 当内存不再使用时的释放 JavaScript的垃圾回收 JavaScript垃圾回收中的引用概念 标记-清除算法
其他编程语言例如C语言支持手动内存管理方法比如malloc()和free()。相比之下JavaScript会自动为新对象分配内存并在不再使用时释放它们。因此JavaScript是web上速度最快的编程语言之一。
让我们了解JavaScript中的内存管理深入探讨JavaScript垃圾回收的主题。 内存生命周期
每种编程语言的内存生命周期都是相似的。它按照以下方式工作 为新实例分配所需的内存 在调用实例时使用已分配的内存读取、写入 当实例不再使用时释放分配的内存
但是第二部分可能在所有编程语言中有所不同具体取决于它们的架构和用法。在低级别的语言例如C语言中第一步和第三步是明确的但在高级语言例如JavaScript中它们通常是隐式的。 JavaScript中的内存分配
值的初始化
在JavaScript中当值被声明时它们会自动分配内存。我们不需要手动为创建的变量和对象分配内存。
让我们看下面的例子解释不同类型的变量和对象的内存分配 const a 200; // 为一个数字类型的变量分配内存
const a anystring; // 为一个字符串类型的变量分配内存const new_obj {a: 5,b: null,
}; // 为一个对象分配内存并包含值// 就像对象一样它也会为数组和包含的值分配内存
const my_array [5, null, anystring];
// JavaScript也会为函数分配内存
function my_function(a) {return a 5;
} // 它会分配一个函数可调用对象// JavaScript也会为函数表达式对象分配内存
anyHtmlElement.addEventListener(click, function() {htmlElement.style.backgroundColor green;
}, true);
从上面的例子中我们可以看到JavaScript为每个对象、变量和方法分配了值。
读取和写入值
当在JavaScript中使用值时可以从分配的内存中读取和写入这些值。我们可以从变量或对象属性的分配内存中读取和写入值甚至可以将参数传递给函数。
在Javascript中基本数据类型存储在栈内存中引用数据类型存储在堆内存中但是引用数据类型会在栈内存中存储一个实际对象的引用。 示例如下 当内存不再使用时的释放
当需要释放分配的内存时大多数内存管理问题都发生在这个阶段。在这个阶段最复杂的任务是确定特定变量、对象或函数何时不再需要。
低级别的编程语言要求开发人员手动指定他们希望释放的分配实例的内存的时间点。
但是像JavaScript这样的高级语言使用自动内存管理也就是垃圾回收。
垃圾回收的主要用途是自动监视并找到不再使用的特定块并将其回收。
然而内存管理的自动处理完全基于近似值。因此确定特定内存块是否仍然需要是无法确定的通用问题。这就是JavaScript垃圾回收器登场并高效地管理内存的原因。
现在我们已经讨论了JavaScript中的内存管理让我们继续我们的主题垃圾回收。 JavaScript垃圾回收
如前所述垃圾回收面临的主要问题是自动查找特定内存是否将被使用。在这种情况下垃圾回收器为解决这个问题实施了一种限制。为了解决这个问题JavaScript使用了引用的概念。让我们了解引用的概念 JavaScript垃圾回收中的引用概念
对于自动内存管理JavaScript依赖于引用的概念。在这个概念中在内存管理上下文中如果另一个对象在应用程序中的后续位置被访问无论是隐式还是显式访问则将一个对象视为对另一个对象的引用。例如对象将具有对其原型的引用这是一个隐式引用以及对其属性值的引用这是一个显式引用。
在这种情况下对象的概念将扩展到比JavaScript中的常规对象更广泛的范围。它还包含了对象的作用域。
引用计数垃圾回收
JavaScript的垃圾回收器遵循计算引用的算法。该算法确定一个对象是否还需要以确定一个对象是否仍然有其他对象引用它。如果一个对象没有引用指向它它将被视为垃圾。
让我们通过下面的例子来理解它 let a {x: {y: 5}
};
// 这里创建了两个对象x和y其中x被另一个y作为其属性之一引用。
// 另一个对象是通过分配给a变量来引用的。
// 在这里给定的对象没有一个可以垃圾回收的理由。let b a; // b变量是引用对象的第二个引用。
a 5; // 现在原来在a中的对象将有一个唯一的引用即b变量。
let z b.x; // 对象的x属性的引用。
// 现在z对象将有两个引用一个作为属性另一个作为z变量。
b chrome; // 原来在b中的对象现在没有引用指向它。
// 然而它的a属性仍然被z变量引用所以它不能被释放。
z null; // 原来在a中的对象的a属性现在没有引用指向它。它可以被垃圾回收。
限制循环引用
JavaScript的垃圾回收器在发现循环引用时存在限制。例如考虑以下例子 function myFunction() {const a {};const b {};a.x b; // a引用了bb.y a; // b引用了areturn someString;
}
myFunction();
从上面的例子中我们创建了两个相互引用的对象形成了一个循环引用的情况。当函数myFunction()完成执行时它将超出作用域。它们在那时不再需要但是它们分配的内存将被回收。因此垃圾收集器不会清理它们的分配。
然而垃圾收集器的引用计数算法将不会将它们识别为可回收对象但它们中的每一个都有一个引用这就是为什么它们都不会被标记为垃圾回收的原因。循环引用问题是应用程序中内存泄漏的原因之一。
IE 6和IE 7维护引用计数垃圾回收器但没有其他现代引擎使用引用计数垃圾回收器来防止由于循环引用而引发的内存泄漏。
垃圾回收的两种常用方法
标记-清除算法
标记-清除算法减少了“一个对象不再使用”算法的使用而是专注于“一个对象不可达”方法。
这种方法专注于根一组对象。在JavaScript中根是全局对象。在这种方法中垃圾收集器从根全局对象开始找到从这些根指向的对象。它引用了从这些根对象指向的所有对象。因此它找到了所有可达和不可达的对象。
这个算法是引用计数算法的改进。它将一个对象视为不可达如果它没有引用指向它。因此在这个算法中如果对象具有零引用那么它就不可达。所以在这个算法中如果对象具有循环引用该对象不再持有真实的零引用。
截至2012年所有现代浏览器都遵循标记-清除算法进行垃圾回收。过去几年中JavaScript的所有改进都采用了这种方法而不是其本地方法“一个对象不再需要”。
在第一个例子中在函数调用后没有两个对象引用任何资源。因此垃圾收集器将它们视为不可达的并且回收它们的内存。
这是目前浏览器大多基于标记清除法。我们可以分为两个阶段
标记从根节点遍历为每个可以访问到的对象都打上一个标记表示该对象可达。清除在没有可用分块时对堆内存遍历若没有被标记为可达对象就将其回收。
优点实现简单。缺点a 内存过于碎片化。 b 内存分配速度慢。解决方法标记-整理法 标记整理法和标记清除法标记阶段一致只是整理阶段是先将被引用的对象移动到一端然后清理掉标记的内存。
视图如下 2、引用计数法
引用计数法就是追踪每个变量被引用的次数当引用数为0将可以被回收掉。优点当引用数为0时会被立即回收缺点a 计数器的增减处理频繁会导致空间的使用效率降低。 b 循环引用无法收回导致内存泄漏。 若有一函数Person中a引用了bb引用了a。每次调用函数Person它们的引用计数都不为0则永远不能被回收。 示例如下
function Person(){let a{};let b{};a.prop b;b.prop a;
}限制
在应用程序中可能会有这样的时刻手动指定哪些内存将被释放会很方便。要释放对象的内存我们需要手动将其显式设置为不可达。
我们不能手动触发JavaScript垃圾收集器。 分代式垃圾回收机制
把分代式垃圾回收机制单独拎出来是因为涉及内容较多容易混淆。v8中将内存分成了两个区新生代和老生代。新生代对象存活时间较短内存通常支持1~8MB。而老生代存储存活时间较长或常驻内存的对象。对于新老两块内存区域的垃圾回收频率不同所以V8 采用了两个垃圾回收器来管控。 视图如下 1、 新生代垃圾回收
新生代垃圾回收通过Scavenge策略进行垃圾回收在具体实现中主要采用了一种复制式的方法Cheney算法。Cheney算法将堆内存也分为两个区一个使用状态的空间我们称为使用区。一个处于闲置状态的空间称为空闲区。新加入的对象都会被存放到使用区当使用区快被写满时就执行一次垃圾回收操作。
1垃圾回收流程
先对使用区中的活动做标记 标记完成后将使用区的活动对象复制进空闲区并进行排序 将原先使用区对象占用的空间释放 最后进行角色互换把空闲区变为使用区使用区变为空闲区
2新生代对象何时会到老生代
第一种情况经过多次复制后依然存活的对象会被认为是生命周期较强的对象会被移到老生代管理。 第二种情况如果复制一个对象到空闲区时空闲区空间占用超过25%那么这个对象将被移到老生代区域。原因是当完成Scavenge回收后空闲区转变成使用区需继续进行内存分配若占比过大将会影响后续内存的分配。
3并行回收
Javascript是一门单线程语言它是运行在主线程上的而在进行垃圾回收的时候就会阻塞Javascript脚本的执行需等待垃圾回收完毕后再恢复脚本执行这种行为叫全停顿。那当GC时间过长就会造成页面卡顿问题。那一个人干活慢n个人一起速度便会是一个人的n倍。程序也一样我们可以通过引入多个辅助线程来同时处理。因此V8引入了并行回收机制。 新生代对象空间就采用并行策略。在垃圾回收过程中启动多个线程来负责新生代中的垃圾清理这些线程同时将对象空间中的数据移到空闲区。由于这个过程中数据地址会发生改变所以还需要同步更新引用这些对象的指针。
2、老生代垃圾回收
老生代数据大多是存活的对象不需要时常清除更新所以采用上面提到的标记清除法来进行垃圾回收。因为上面也提到标记清除后会产生大量内存碎片所以V8就采用了上文所说的标记整理法来解决这个问题。
1增量标记
并行策略虽然可以增加垃圾回收的效率对于新生代这样存放较小对象的回收器能有很好的优化但其实还是全停顿式的。对于存放较大对象的老生代来说这些较大对象GC时哪怕使用并行策略依旧会消耗大量时间。所以V8对老生代进行了优化从全停顿标记切换到了增量标记。 增量标记就是将一次GC分成多步小GC标记让JS和小GC标记交替执行直到标记完成。 问题来了小GC标记执行完后是如何暂停执行JS任务而后又是如何进行下一次小GC 标记如果执行JS任务时刚被标记好的对象引用又被修改了该当如何V8解决这两个问题的方法分别是三色标记法和写屏障。
1解决问题一三色标记法
标记清理法区分是通过非黑即白的策略但这样便会出现在增量标记时内存中黑白都有我们无法区分下一步是什么所以采用了三色标记法使用每个对象的两个标记位和一个标记工作表来实现标记两个标记位编码三种颜色黑11白00灰10。
黑色表示对象自身及对象的引用都被标记已检查状态 白色表示未被标记的对象初始状态 灰色表示自身被标记自身的引用未被标记待检查状态 执行流程如下
初始将所有对象都是白色 从root对象开始先将root对象标记为灰色并推入标记工作表中 当收集器从标记工作表中弹出对象并访问他的所有引用对象时自身灰色就会变成黑色。 将自身的下一个引用对象标记为灰色 一直这样执行下去直到没有可以被标记为灰色的对象时剩下的白色对象都是不可达的进入清理阶段。恢复时从灰色标记对象开始执行。
2解决问题二写屏障
为了解决黑色对象在程序执行中被新添加引用或已经标记黑色的被引用对象不再被引用了。写屏障就有了以下两个变化
不对已标记的黑色对象做处理因为在之后的GC中也会被清理。 Write-barrier 机制强制不变黑的对象指向白色对象。这个也被称作强三色不变性。所以一旦有黑色对象引用白色对象该机制会强制将引用的白色对象改为灰色从而保证下一次增量 GC 标记阶段可以正确标记。
2懒性清理
增量标记完后如果当前内存足以支持代码的快速运行也没必要立即清理可让程序先运行也无需一次性清理完所有垃圾对象可以按需清理直到所有垃圾对象清理完后再继续增量标记。
并发回收
并发主要发生在工作线程上。当在工作线程辅助线程执行GC是应用程序可以继续在主线程运行并不会被挂起。 这也是有问题的因为GC也在进行应用程序也在执行此时对象的引用关系随时都有可能变化所以之前做的一些标记就需要改变所以需要读写锁机制来控制这一点。
总结
JavaScript提供了高效的内存管理机制。JavaScript自动支持内存管理对我们来说是透明的。我们不能手动触发JavaScript垃圾收集器。