acm-header
登录

ACM通信

实践

垃圾收集作为合资企业


程式化的线圈、插图

信贷:Wacomka

回到顶部

许多流行的编程语言都是在虚拟机(vm)上执行的,虚拟机提供了关键的基础设施,比如使用垃圾收集的自动内存管理。例子包括动态类型编程语言,如JavaScript和Python,以及静态语言,如Java和c#。对于这种语言,垃圾收集器定期跟踪应用程序堆上的对象,以确定哪些对象是活的,应该保留,哪些对象是死的,可以回收。

据说垃圾收集器管理应用程序内存,这意味着要管理编程语言。托管语言的主要优点是开发人员不必手动推理对象的生命周期和释放对象。忘记释放对象会泄漏内存,过早释放会导致指针悬空。

托管语言的虚拟机可以嵌入到更大的软件系统中,这些软件系统是用不同的(有时是非托管的)编程语言实现的,程序员负责释放不再需要的内存。这种异构软件系统的一个例子是谷歌的Chrome Web浏览器,其中高性能的V8JavaScript VM (https://v8.dev/)被嵌入到负责渲染网站的Blink渲染引擎中。Blink通过解释文档对象模型(DOM;https://www.w3.org/TR/WD-DOM/introduction.html),它是通过HTML定义的树形结构的独立于语言的跨平台表示。

因为Blink是用c++编写的,所以它实现了一个抽象DOM,将HTML文档表示为c++对象。DOM c++对象被包装并作为对象公开给JavaScript,这允许脚本通过修改DOM对象直接操作Web页面内容。c++对象被调用wrappables,它们的JavaScript版本包装器,以及连接这些对象的引用跨组件引用。尽管c++是一种非托管语言,但Blink对DOM c++对象有自己的垃圾收集器。然后,跨组件内存管理处理此类异构环境中的内存回收。

V8和Blink使用mark-sweep-compact垃圾收集器,其中一个垃圾收集周期包括三个阶段:标记,识别活动物体;全面,那里会释放死去的物体;而且压实,其中活动对象被重新定位,以减少内存碎片。在标记期间,垃圾收集器从定义的根引用集合中查找所有可访问的对象,概念上遍历对象图,其中图的节点是对象,边是对象的字段。

跨组件引用在组件边界上表达活性,必须在图中显式建模。管理这些引用的最简单方法是将它们作为对应组件的根。换句话说,从Blink到V8的引用将被视为V8中的根,反之亦然。这就产生了跨组件引用循环的问题,这与常规引用循环类似1在单个垃圾收集系统中,其中的对象组成强连接组件组,否则无法从活动对象图中访问这些组件。

循环需要手动突破使用弱引用,或者使用一些能够通过检查系统作为一个整体来推断活性的托管系统。手动中断循环并不总是可行的,因为所涉及对象的语义可能要求所有引用通过强引用保持存活。另一种选择是以一种不能构建循环的方式来限制所涉及的组件。请注意,在Chrome和Web的情况下,这并不总是可能的,如后面所示。

虽然可以通过统一两个组件的内存管理系统来避免周期问题,但仍然需要独立管理两个组件的内存,以保持关注点分离,因为如果依赖关系较少,那么在另一个系统中重用一个组件会更简单。例如,V8不仅在Chrome中使用,也在Node.js的服务器端运行时中使用,因此不希望在V8中添加与blink相关的知识。


跨组件跟踪支持跨组件边界进行高效、有效和安全的垃圾收集。


假设组件不能统一,跨组件引用循环可能导致两者之一内存泄漏当涉及循环的图不能被组件的垃圾收集器回收,严重影响浏览器性能时,或者过早收集对象导致免费后使用的安全漏洞和程序崩溃,将用户置于风险之中。

本文描述了一种称为跨组件跟踪(CCT)的方法,3.它在V8和Blink中实现,以解决跨组件边界的内存管理问题。跨组件跟踪也很好地集成了现有的工具基础设施,并改进了Chrome Dev-Tools (https://developers.google.com/web/tools/chrome-devtools/).

回到顶部

DOM和JavaScript的世界是分开的

如前所述,Chrome用c++可包装对象对DOM进行编码,而HTML标准中指定的大多数功能都以c++代码的形式提供。相比之下,JavaScript是在V8中使用与c++不兼容的自定义对象模型实现的。当JavaScript应用程序代码访问JavaScript DOM包装器对象的属性时,V8在Blink中调用c++回调函数,对底层的c++ DOM对象进行更改。相反,Blink对象也可以直接引用JavaScript对象并根据需要修改它们。例如,Blink可以将JavaScript对象的字段绑定到c++回调中,以供其他JavaScript代码使用。

worldsDOM和javascript都由它们自己的基于跟踪的垃圾收集器管理,这些垃圾收集器能够回收仅在它们自己的堆中传递根的内存。剩下的工作就是定义这些垃圾收集器应该如何处理跨组件的引用,以使它们能够有效地跨组件收集垃圾。为了突出显示泄漏和悬浮指针的问题,有必要查看一个JavaScript代码的具体示例,以及如何使用它创建随时间变化的动态内容。

图1显示了一个创建临时对象(加载条)的示例。loadingBar),然后替换为实际内容(内容)异步构建并在它准备好后立即交换。注意,访问document元素或body元素,或创建div元素会在它们各自的世界中生成一对对象,这些对象包含对彼此的引用。虽然程序本身是用JavaScript编写的,但是属性查找(例如)到body元素并调用DOM方法列表末尾而且方法的在Blink中被转发到相应的c++实现。常规的JavaScript访问,比如设置父属性,是由V8在它自己的对象上执行的。正是JavaScript和DOM的这种无缝集成使开发人员能够创建丰富的Web应用程序。同时,这个概念允许跨组件边界创建任意对象图。

f1.jpg
图1。与DOM交互的JavaScript示例。

图2显示了由示例创建的对象图的简化版本,左侧的JavaScript对象连接到右侧DOM中的c++对象。Java-Script对象,例如body和div元素,在JavaScript中几乎没有任何引用,但大多用于引用它们对应的c++对象。因此,为组件本地垃圾收集器定义跨组件引用的语义以允许收集这些对象是至关重要的。例如,将从Blink到V8的传入引用作为V8垃圾收集器的根将始终保持进度条对象还活着。将此类引用视为一致弱引用将导致V8垃圾收集器回收body和div元素,这将为Blink留下悬浮指针。

f2.jpg
图2。跨越JavaScript和DOM的对象图。

除了正确性,在这样一个复杂的环境中,另一个挑战是开发人员的可调试性。虽然Web平台允许c++和JavaScript在底层松散耦合,但对于使用HTML和JavaScript的Web开发人员来说,对这些抽象的api进行适当封装是至关重要的,包括在正确使用时防止内存泄漏。为了研究Web页面中的内存泄漏,开发人员需要能够无缝推理V8和Blink堆中对象的连通性的工具。

回到顶部

跨组件跟踪

我们建议将CCT作为一种解决跨组件边界引用循环的普遍问题的方法。对于CCT,所有涉及组件的垃圾收集器都进行了扩展,以允许跟踪到不同的组件,管理可能不同编程语言的对象。CCT使用一个组件的垃圾收集器作为主示踪剂计算活动对象的完整传递闭包以打破循环。

其他组件通过提供远程示踪剂它可以在主跟踪程序请求时遍历组件的对象。然后可以将系统视为一个托管堆。因此,只需遵循现有的垃圾收集原则,就可以扩展简单的CCT算法,允许根据需要移动收集器和增量或并发标记。8主跟踪器和远程跟踪器算法的伪代码可以在我们的完整研究文章中找到。3.

对于Chrome,我们开发了一个跨组件跟踪的版本,其中JavaScript对象的主跟踪器和c++对象的远程跟踪器分别由V8和Blink提供。通过这种方式,V8可以在执行垃圾收集时跟踪c++ DOM,有效地打破V8和Blink边界上的循环。在这个系统中,Blink垃圾收集只处理c++对象,并将来自V8的传入的跨组件引用视为根。这样,V8和Blink的垃圾收集器的后续调用可以跨组件边界回收循环。

V8中的跟踪器利用了隐藏类的概念2描述JavaScript对象的主体,以找到对其他对象的引用,以及对Blink的引用。Blink中的跟踪程序要求每个垃圾收集的c++类都要用描述类主体(包括对其他托管对象的任何引用)的方法进行手动注释。因为Blink在引入CCT之前就已经被垃圾回收了,所以只需要在整个渲染代码库中对这个方法进行微小的调整。

Chrome努力提供流畅的用户体验,以60fps(帧/秒)的速度更新屏幕,让V8和Blink渲染一帧的时间约为16.6毫秒。由于标记大型堆可能需要数百毫秒,V8和Blink都采用了一种称为增量标记,这意味着标记被分为几个步骤,在这些步骤中对象只被标记了很短的时间(例如,1ms)。

应用程序可以自由更改步骤之间的对象引用。这意味着应用程序可能在已标记的对象中隐藏对未标记对象的引用,这将导致提前收集活动对象。增量标记需要垃圾收集器通过保留强三色标记不变量来保持标记状态一致。8这个不变量表示,完全标记的对象只允许指向同样被完全标记或存储在某处以供处理的对象。V8和Blink使用保守的dijkstra风格的写屏障来保持标记不变6这确保向对象写入值时也会标记该值。事实上,V8甚至以这种方式在后台线程上提供并发标记,同时依赖于Blink中的增量跟踪。5

为了使它具体化,图3说明了V8在JavaScript和c++中跟踪和标记对象的CCT。V8的根对象可传递访问的对象被标记为黑色。随后,任何不可访问的对象(loadingBar,在本例中)被垃圾收集器回收。注意,从V8的角度来看,div元素之间没有区别内容而且loadingBar,并且只有CCT明确了哪些对象可以被V8的垃圾收集器回收。一旦无法访问的V8对象消失,Blink中的任何后续垃圾收集都不会看到相应的根HTMLDivElement然后回收可包装对的另一半。

f3.jpg
图3。跨组件的垃圾收集。

在Chrome中,CCT取代了它的前身,称为对象分组版本57。对象分组基于跨组件边界的过度近似活动,只要通过JavaScript保持单个包装器是活动的,就保持给定DOM树中的所有包装器和可包装器都是活动的。这种假设在实现时是合理的,因为从包装器修改DOM的情况很少发生。然而,过度近似有两个主要缺点:它保留了比所需更多的内存,这在Web应用程序不断增长的时代增加了浏览器中本已强大的内存压力;并且,原始算法不是为增量处理而设计的,这与CCT相比,导致了更长的垃圾收集暂停时间。

如今在Chrome中实现的增量CCT通过通过可达性计算对象的活跃性和支持增量处理提供了更好的近似,从而消除了这些问题。详细的性能分析可以在主要的研究论文中找到。3.我们目前正在研究Blink c++堆的并发标记,并将CCT集成到这样的方案中。

回到顶部

调试

内存泄漏bug是当今Web应用程序普遍存在的问题。7闭包等功能强大的语言构造使Web开发人员很容易意外地延长JavaScript和DOM对象的生命周期,从而导致比必要的更高的内存使用量。作为一个具体的例子,让我们假设fetchContent函数图1保留对所提供回调函数的内部引用(可能是由于一个bug),如图4

f4.jpg
图4。漏水的回调。

不知道实现的fetchContent函数,Web开发人员注意到loadingBar垃圾收集器不会回收上一个示例中的元素。调试工具能帮助追踪元素泄漏的原因吗?

可以应用跨组件垃圾收集所需的跟踪基础结构来改进内存调试。Chrome DevTools使用基础设施来捕获和可视化跨越JavaScript和DOM对象的对象图。该工具允许Web开发人员查询为什么特定对象没有被垃圾收集器回收。它以a的形式表示答案保留路径,它从对象运行到垃圾收集根。图5显示泄漏的保留路径loadingBar元素。路径显示泄漏的DOM元素是由loadingBar环境中的变量(称为上下文)的一个匿名闭包,该闭包由内部状态场的fetchContent函数。通过检查路径的每个节点,Web开发人员可以确定泄漏的源头。由于跨组件跟踪,路径可以无缝地跨越DOM和JavaScript边界。4

f5.jpg
图5。泄漏DIV元件的保留路径。

回到顶部

回收其他异构系统中的内存

Web浏览器是特别有趣的系统,因为所有主要的浏览器引擎都以类似的方式分离DOM和JavaScript对象(即为这些对象提供不同的堆)。类似于Blink和V8,所有这些浏览器都用c++编码它们的DOM,并且必须依赖于JavaScript的自定义对象模型。所有派生自blink的系统(例如Chrome、Opera和Electron)都依赖于CCT来处理跨组件引用。支持Firefox的Gecko呈现引擎使用引用计数来管理DOM对象。一个额外的增量周期收集器1它定期被唤醒,以确保这些循环最终被收集。WebKit是Safari内部运行的引擎,它使用c++ DOM的引用计数和一个附加的系统,该系统在垃圾收集周期的最后暂停时计算跨包装器/可包装边界的活动。不出所料,所有主流浏览器都有处理这类周期的机制,因为在运行时间较长的网站中,内存泄漏是不可避免的,而且会显著影响浏览器性能。

但是,更有趣的是,我们不知道有其他集成vm的复杂系统提供跨组件内存管理。虽然vm通常为在其他系统(如Java Native Interface (JNI)和NativeScript)中集成提供桥梁,但跨组件引用需要在所有这些系统中进行手动管理。使用这些系统的开发人员必须手动创建和销毁可以形成循环的链接。这很容易出错,并可能导致前面提到的问题。

回到顶部

结论

跨组件跟踪是解决跨组件边界引用循环问题的一种方法。只要组件可以在API边界上形成具有非平凡所有权的任意对象图,这个问题就会出现。在V8和Blink中实现了CCT的增量版本,以一种安全的方式实现了有效和高效的内存回收,而不会引入悬浮指针,这会导致Chrome或Chrome派生浏览器的程序崩溃或安全漏洞。Chrome DevTools重用了相同的跟踪系统,以可视化保留对象的路径,而不管它们是用c++还是JavaScript管理的。

但是请注意,CCT带来了巨大的实现开销,因为它需要在每个组件中实现跟踪程序。最终,实现者需要权衡通过对其系统实施限制来避免周期的努力,或者实现回收周期的机制(如CCT)。Chrome已经在V8和Blink中配备了垃圾收集器,因此我们选择实现一个通用的解决方案,如CCT,它允许顶部的系统根据需要保持灵活。

CCT不仅在Chrome中实现,而且在使用V8和Chrome的其他软件系统中也实现,例如流行的Opera Web浏览器和Electron。Cobalt是一个高性能、占地面积小的平台,提供了用于电视等嵌入式设备的HTML5、CSS和JavaScript的子集,它实现了跨组件跟踪,以管理其内存,灵感来自于我们的系统。

ACM队列的q戳相关文章
queue.acm.org

空闲时间垃圾收集调度
Ulan Degenbaev等人。
https://queue.acm.org/detail.cfm?id=2977741

实时垃圾收集
大卫·f·培根
https://queue.acm.org/detail.cfm?id=1217268

泄漏的空间
尼尔·米切尔
https://queue.acm.org/detail.cfm?id=2538488

回到顶部

参考文献

1.参考计数系统的并行循环收集。在15国会议记录th面向对象程序设计。Springer-Verlag,伦敦,英国,2001,207235;https://doi.org/10.1007/3-540-45337-7_12

2.Chambers, C., Ungar, D.和Lee, E. SELF的高效实现,SELF是一种基于原型的动态类型面向对象语言。面向对象程序设计系统,语言和应用。美国建筑工程学报,1989,4970;https://dl.acm.org/citation.cfm?doid=74877.74884

3.Degenbaev, U.等。跨组件的垃圾收集。在ACM编程语言论文集2,OOPSLA第151条,2018;https://dl.acm.org/citation.cfm?doid=3288538.3276521

4.Degenbaev, U., Filippov, A., Lippautz, M.和Payer, H.从JS到DOM的跟踪和再返回。V8, 2018;https://v8.dev/blog/tracing-js-dom

5.Degenbaev, U., Lippautz, M. and Payer, H. V8中的并行标记。V8, 2018;https://v8.dev/blog/concurrent-marking

6.Dijkstra, e.w., Lamport, L., Martin, a.j., Scholten, C.S.和Steffens, E.F.M.。Commun。ACM 21, 11(1978年11月),966975;https://dl.acm.org/citation.cfm?doid=359642.359655

7.M. Hablich和H. Payer从记忆路演中学到的教训;https://bit.ly/2018-memory-roadshow

8.琼斯(R.)、霍斯金(A.)和莫斯(E.)垃圾收集手册:自动内存管理的艺术。查普曼和霍尔,2012年。

回到顶部

作者

枪骑士Degenbaev是谷歌的软件工程师,负责V8 JavaScript引擎的垃圾收集器。

迈克尔Lippautz是谷歌的一名软件工程师,他在那里为V8 JavaScript虚拟机和Blink渲染引擎进行垃圾收集。此前,他曾致力于谷歌的Dart虚拟机。

汉斯·付款人他是谷歌的软件工程师,在那里他从事V8 JavaScript虚拟机的工作。此前,他曾在谷歌的Dart虚拟机和各种Java虚拟机上工作。


版权由作者/所有者持有。
向所有者/作者请求(重新)发布权限

数字图书馆是由计算机协会出版的。版权所有©2019 ACM, Inc.


没有发现记录

Baidu
map