JavaScript 引擎中的内存管理
简介
本文档介绍 QML 中 JavaScript 引擎的动态内存管理。这是一个相当技术性的深入描述。只有当你关心 QML 中 JavaScript 内存管理的确切特性时,才需要阅读本文。特别是,如果你想优化你的应用程序以获得最高性能,它可能会有所帮助。
注意: 通过使用 Qt Quick Compiler将 QML 代码编译成 C++,就能避免 JavaScript 堆的大量使用。生成的 C++ 代码使用熟悉的 C++ 栈和堆来存储对象和值。然而,JavaScript 主环境总会使用一些 JavaScript 管理的内存,无论你是否使用它。如果使用了无法编译为 C++ 的功能,引擎会退回到解释或 JIT 编译,并使用 JavaScript 堆中存储的 JavaScript 对象。
基本原则
QML 中的 JavaScript 引擎有一个专用的内存管理器,它以多个页面为单位向操作系统请求地址空间。然后,JavaScript 创建的对象、字符串和其他管理值,会使用 JavaScript 引擎自己的分配方案,放置在这个地址空间中。JavaScript 引擎不使用 C 库的 malloc() 和 free(),也不使用 C++ 的 new 和 delete 的默认实现来为 JavaScript 对象分配内存。
在类 Unix 系统中,通常使用 mmap(),而在 windows 系统中则使用 VirtualAlloc()。这些原语有几种特定平台的实现方式。以这种方式预留的地址空间不会立即存入物理内存。相反,操作系统会在内存页面被实际访问时注意到,然后才将其提交。因此,地址空间实际上是空闲的,拥有大量的地址空间可以为 JavaScript 内存管理器提供所需的有利条件,以便在 JavaScript 堆中高效地放置对象。此外,还有一些特定平台的技术可以告诉操作系统,某块地址空间虽然仍被保留,但暂时不必映射到物理内存中。这样,操作系统就可以根据需要解除对内存的承诺,并将其用于其他任务。最重要的是,大多数操作系统并不保证对这种退约请求立即采取行动。只有当内存确实需要用于其他用途时,操作系统才会将其解约。在类 Unix 系统中,我们通常使用 madvise() 来处理。Windows 系统中的 VirtualFree() 有特定的标志,可以实现相同的功能。
注意: 有些内存分析工具并不了解这种机制,因此会过度报告 JavaScript 的内存使用情况。
JavaScript 堆上存储的所有值都会被垃圾回收。当值超出作用域或被 "丢弃 "时,不会立即被 "删除"。只有垃圾回收器才能从 JavaScript 堆中删除值并返回内存(有关工作原理,请参阅下面的垃圾回收)。
基于 QObject 的类型
QObject-基于 QObject 的类型,尤其是你可以用 QML 元素来表述的所有内容,都是在 C++ 堆上分配的。当从 JavaScript 访问QObject 时,JavaScript 堆上只会有一个围绕指针的小封装。不过,这样一个封装器可以拥有它指向的QObject 。请参见QJSEngine::ObjectOwnership 。如果封装器拥有该对象,那么封装器被垃圾回收时,该对象就会被删除。您也可以通过调用 destroy() 方法手动触发删除。destroy() 内部调用QObject::deleteLater()。因此,它不会立即删除对象,而是等待下一次事件循环迭代。
QML 声明的对象属性存储在 JavaScript 堆中。只要它们所属的对象存在,它们就存在。之后,它们会在垃圾回收器下次运行时被删除。
对象分配
在 JavaScript 中,任何结构化类型都是对象。这包括函数对象、数组、正则表达式、日期对象等。QML 有许多内部对象类型,如上面提到的QObject wrapper。每当创建一个对象,内存管理器就会在 JavaScript 堆中为其找到一些存储空间。
JavaScript 字符串也是受管理的值,但其字符串数据不在 JavaScript 堆上分配。与QObject 封装类似,字符串的堆对象只是字符串数据指针的薄层封装。
为对象分配内存时,首先将对象的大小四舍五入到 32 字节对齐方式。每个 32 字节的地址空间称为一个 "槽"。对于小于 "巨大大小 "阈值的对象,内存管理器会进行一系列尝试,将对象放入内存:
- 内存管理器会保存先前释放的堆的链接列表,称为 "仓"。每个 "仓 "中都有堆的碎片,每个 "仓 "中的插槽大小固定。如果大小合适的分区不是空的,内存管理器就会选择第一个分区并将对象放入其中。
- 尚未使用的内存通过缓冲区分配器进行管理。缓冲区指针指向已占用地址空间之外的字节。如果仍有足够的未使用地址空间,缓冲区就会相应增加,并将对象放入未使用空间。
- 对于先前释放的不同大小、大于上述特定大小的堆,会保留一个单独的缓冲区。内存管理器会遍历该列表,并尝试找到可以分割的堆块,以容纳新对象。
- 内存管理器会搜索比要分配的对象更大的特定大小的堆列表,并尝试分割其中一块。
- 最后,如果上述方法都不奏效,内存管理器会预留更多地址空间,并使用缓冲区分配器分配对象。
巨型对象由自己的分配器处理。每个对象都会从操作系统获取一个或多个独立的内存页,并进行单独管理。
此外,内存管理器从操作系统获取的每块新地址空间都会有一个标头,标头中包含每个槽的若干标志:
- 对象:对象占用的第一个插槽将被标记为该位。
- 扩展:对象占用的任何其他插槽都会标记该位。
- 标记:垃圾回收器运行时,如果对象仍在使用,则会设置该位。
内部类
为了尽量减少对象成员元数据所需的存储空间,JavaScript 引擎会为每个对象指定一个 "内部类"。其他 JavaScript 引擎称之为 "隐藏类 "或 "形状"。内部类会被复制并保存在树状结构中。如果给对象添加了一个属性,就会检查当前内部类的子类,看看之前是否出现过相同的对象布局。如果是,我们就可以立即使用生成的内部类。否则,我们必须创建一个新的内部类。
内部类存储在 JavaScript 堆的独立区域中,其他工作方式与上述一般对象分配方式相同。这是因为在收集使用内部类的对象时,内部类必须保持活力。内部类的收集需要单独进行。
不过,存储在内部类中的实际属性不会保留在 JavaScript 堆中,而是使用 new 和 delete 进行管理。
垃圾回收
JavaScript 引擎中使用的垃圾收集器是一种非移动、标记和清扫设计。自 Qt 6.8 起,它默认以增量方式运行(除非QV4_GC_TIMELIMIT设置为 0)。在标记阶段,我们会遍历所有可以找到对象实时引用的已知位置。特别是
- JavaScript 全局
- QML 和 JavaScript 编译单元中不可删除的部分
- JavaScript 堆栈
- 持久值存储。这是QJSValue 和类似类保存 JavaScript 对象引用的地方。
对于在这些地方找到的任何对象,都会对其引用的任何内容递归设置标记位。
在清扫阶段,垃圾回收器会遍历整个堆,并释放之前未标记的任何对象。释放后的内存会被分类到不同的文件夹中,以用于进一步的分配。如果某块地址空间完全清空,它就会被退订,但地址空间仍会保留(见上文的基本原则)。如果内存使用量再次增长,则会重新使用相同的地址空间。
垃圾回收器可以通过调用gc() 函数手动触发,也可以通过启发式方法触发,启发式方法会考虑以下方面:
- JavaScript 堆上由对象管理但未在 JavaScript 堆上直接分配的内存量,如字符串和内部类成员数据。我们会为这些内存设置一个动态阈值。如果超过阈值,垃圾回收器就会运行,并提高阈值。如果托管的外部内存远远低于阈值,则会降低阈值。
- 保留的总地址空间。只有在至少预留了一些地址空间后,才会考虑在 JavaScript 堆上分配内部内存。
- 上次垃圾回收器运行后额外预留的地址空间。如果上次运行垃圾回收器后的地址空间量是已用内存量的两倍以上,我们就会再次运行垃圾回收器。
分析内存使用情况
为了观察地址空间和分配对象数量的发展情况,最好使用专门的工具。该工具提供的 QML Profiler提供了一个可视化工具。更通用的工具无法看到 JavaScript 内存管理器在其保留的地址空间内做了什么,甚至可能不会注意到地址空间的一部分没有被提交到物理内存中。
调试内存使用情况的另一种方法是logging categories qt.qml.gc.statistics和qt.qml.gc.allocatorStats。如果启用了 qt.qml.gc.statistics 的调试级别,垃圾收集器每次运行时都会打印一些信息:
- 总共保留了多少地址空间
- 垃圾回收前后使用了多少内存
- 到目前为止分配了多少不同大小的对象
qt.qml.gc.allocatorStats 的调试级别会打印更详细的统计信息,其中还包括垃圾收集器的触发方式、标记和清扫阶段的时序,以及按字节和地址空间块分列的内存使用明细。
© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.