【一看就懂】关于浏览器垃圾回收你了解哪些

188 阅读7分钟

原文链接》》

前言

很多人聊起浏览器的垃圾回收机制都处于一知半解的状态。垃圾回收不就是帮你释放一些不会再用到的内存吗?还知道一点的人可能会说浏览器引擎很智能地帮你做了垃圾回收,在开发层面我们已经不需要过多地去关注它。确实Javascript是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。在C和C++等语言中,跟踪内存使用对开发者来说是个很大的负担,也是很多问题的来源。Javascript为开发者卸下了这个负担,通过自动内存管理实现内存分配和闲置资源回收。但是作为一个优秀的程序员,我们显然是不会满足于仅仅知道一个东西是怎么用的,所谓知其然知其所以然,尽管实际业务开发中不太可能会用到,但是其中的思想对于我们的进阶是百害无一利的,只有真正地理解了这些东西,我们才能写出质量更高的代码。

垃圾回收三个问

如果你看过比较多问题的解决方案你就会发现很多东西都会有一些固定的套路。就比如将这个问题抛给你,让你来实现一个浏览器的垃圾回收机制,你会怎么做。基本方案很简单:你想那不就是确定哪个变量不会再使用,然后释放它占用的内存不就好了吗? 确实是这样,然后根据这个方案会衍生出一些问题,比如怎么判断什么样的变量是不会再使用的呢? 什么时候进行这样一个判断呢? **具体到内存层面应该怎么样释放内存呢?**等等,当你把这些衍生出来的问题都解决掉之后,你会惊讶地发现,我已经把它实现出来了!接下来我们将围绕这个基本方案来解决它的衍生问题。

第一问:什么样的变量浏览器能将它的内存回收?

我们以函数中局部变量的正常生命周期为例。函数中的局部变量会在函数执行的时存在。此时,栈(或堆)内存会分配空间以保存相应的值。函数在内部使用了变量,然后退出。此时,就不再需要那个局部变量了,它占用的内存可以释放,供后面使用。这种情况下显然不再需要局部变量了,但并不是所有时候都会这么明显。垃圾回收程序必须跟踪哪个变量还会使用,以及哪个变量不会再使用,以便回收内存。如何标记未使用的变量也许有不同的实现方式。不过,在浏览器的发展史上,用到过主要两种的标记策略:标记清理和引用计数

标记清理

Javascript最常用的标记策略是标记清理。垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。

引用计数

另一种没那么常用的标记策略是引用计数。其思路是对每个值都记录它被引用的次数。声明变量并赋给它一个引用值时,这个值的引用数为1。如果同一个值又被赋值给另外一个变量,那么引用数加1.类似地,如果保存对该引用值的变量被其他值给覆盖了,那么引用数减1。当一个值的引用数为0时,就说明没办法再访问到这个值了,因此可以安全地回收其内存了。垃圾回收程序下次运行的时候就会释放引用数为0的值的内存。

引用计数有一个很严重的问题就是循环引用,所谓循环引用就是对象A有一个指针指向对象B,而对象B也引用了对象A。比如:

function problem() {
	const objectA  = {}
    const objectB = {}
    
    objectA.someOtherObject = objectB
    objectB.anotherObject = objectA
}

在这个例子当中,objectA和objecB通过各自的属性循环引用,意味着它们的引用数都是2。在标记清理策略下,这不是问题,因为函数结束后,这两个对象都不在作用域中。而在引用计数策略下,函数执行完,变量objectA和objectB会指向null,当时由于它们所指向的对象之间相互引用,所以它们的引用数为1,永远不会变成0.如果函数被多次调用,则会导致大量内存永远不会被释放。因此现在浏览的标记策略都是采用的标记清理。

第二问:什么时候执行一次垃圾回收程序呢?

垃圾回收程序会周期性地执行,如果内存中分配了很多变量,则可能造成性能损失,因此垃圾回收的时间调度很重要。尤其是在内存有限的移动设备上,垃圾回收有可能会明显拖慢渲染的速度和帧速率。开发者并不知道什么时候程序会收集垃圾,因此最好的办法就是在写代码时就要做到:无论什么时候开始收集垃圾,都能让他尽快结束工作。

现代垃圾回收程序会基于对JavaScript运行时环境的探测来决定何时运行。探测机制因引擎而异,但基本上都是根据已分配对象的大小和数量来判断的。比如,根据V8团队2016年的一篇博文的说法:在一次完整的垃圾回收之后,V8的堆增长策略会根据活跃对象的数量外加一些余量来确定何时再次垃圾回收。

由于调度垃圾回收程序方面的问题会导致性能下降,IE曾经饱受诟病。它的策略是根据分配数,比如分配了256个变量、4096个对象/数组字面量和数组槽位(slot),或者64KB字符串。只要满足其中某个条件,垃圾回收程序就会运行。这样实现的问题在于,分配那么多变量的脚本,很可能在其整个生命周期内始终需要那么多变量,结果就会导致垃圾回收程序过于频繁地运行。由于对性能的严重影响,IE7最终更新了垃圾回收程序。

IE7发布后,JavaScript引擎的垃圾回收程序被调优为动态改变分配变量、字面量或数组槽位等会触发垃圾回收的阈值。IE7的起始阈值都与IE6相同。如果垃圾回收程序回收的内存不到已分配的15%,这些变量、字面量或数组槽位的阈值就会翻倍。如果有一次回收的内存达到已分配的85%,则阈值重置为默认值。 这么一个简单的修改,极大地提升了重度依赖JavaScript的网页在浏览器中的性能。

第三问:垃圾回收机制具体到内存中是如何进行操作的呢?

由于本文的篇幅问题,笔者不在此详述,有兴趣的可以自行查阅资料,附上一篇笔者之前看过的文章: 深入理解Chrome V8垃圾回收机制

写在后面

本文是笔者在阅读JavaScript高级程序设计第四版后整理出来的概括文章,如有任何问题欢迎在下方评论区交流。