本页内容

JavaScript 引擎中的内存管理

简介

本文档描述了 QML 中 JavaScript 引擎的动态内存管理机制。这是一篇技术性较强且深入的说明。只有当您关注 QML 中 JavaScript 内存管理的具体特性时,才需要阅读本文。特别是,如果您正在尝试优化应用程序以获得最佳性能,本文将有所帮助。

注意:通过 使用 Qt Quick Compiler ,可以避免大量 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 ` 包装器。每当创建一个对象时,内存管理器都会在 JavaScript 堆上为其分配存储空间。

JavaScript 字符串也是受管理的值,但其字符串数据并非分配在 JavaScript 堆上。与QObject 包装器类似,字符串的堆对象只是对指向字符串数据的指针的轻量级包装。

为对象分配内存时,会先将对象的大小向上舍入至 32 字节对齐。每个 32 字节的地址空间片段被称为“槽”(slot)。对于小于“巨大大小”阈值的对象,内存管理器会进行一系列尝试将其放置在内存中:

  • 内存管理器维护着先前已释放的堆内存片段的链表,称为“桶”(bins)。每个桶包含固定大小的堆内存片段,这些片段以槽为单位存储。如果对应大小的桶不为空,则选择第一个条目并将对象放置于此。
  • 尚未使用的内存通过“缓冲区分配器”进行管理。一个缓冲区指针指向已占用地址空间之外的下一个字节。如果仍有足够的未使用地址空间,则相应地扩大缓冲区,并将对象放置在未使用的空间中。
  • 对于之前已释放的、大小大于上述特定尺寸且尺寸各异的堆内存块,会保留一个单独的桶。内存管理器会遍历该列表,尝试找到可拆分以容纳新对象的内存块。
  • 内存管理器会搜索那些大小大于待分配对象的固定尺寸桶列表,并尝试将其中一个进行拆分。
  • 最后,如果上述方法均无效,内存管理器将预留更多地址空间,并使用缓冲区分配器为该对象分配内存。

超大对象由其专属的分配器处理。对于每个超大对象,会从操作系统获取一个或多个独立的内存页,并分别进行管理。

此外,内存管理器从操作系统获取的每个新地址空间块都会有一个头部,其中包含每个槽位的若干标志位:

  • object:对象占用的第一个槽位会设置此位。
  • extends:该对象占用的后续任何槽位均会设置此位。
  • mark:当垃圾回收器运行时,若对象仍在使用中,则会设置此位。

内部类

为了最大限度地减少记录对象所包含成员的元数据所需的存储空间,JavaScript 引擎会为每个对象分配一个“内部类”。其他 JavaScript 引擎将此称为“隐藏类”或“形状”。 内部类经过去重处理并以树形结构存储。若向对象添加属性,系统会检查当前内部类的子节点,以确认是否曾出现过相同的对象布局。若存在,则可直接使用生成的内部类;否则,则需创建一个新的内部类。

内部类存储在 JavaScript 堆的专用区域中,该区域的工作原理与上述常规对象分配机制相同。这是因为在使用内部类的对象被回收时,内部类必须保持存活。随后,内部类会在单独的回收轮次中被回收。

不过,存储在内部类中的实际属性并不保存在 JavaScript 堆中,而是通过 `new` 和 `delete` 进行管理。

垃圾回收

JavaScript 引擎中使用的垃圾回收器采用非移动式的“标记-清除”(Mark and Sweep)设计。自 Qt 6.8 起,它默认以增量方式运行(除非将QV4_GC_TIMELIMIT设置为 0)。 在标记阶段,我们会遍历所有已知存在对象存活引用(live references)的位置。具体包括:

  • 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 的“调试”级别会输出更详细的统计信息,其中还包括垃圾回收器的触发方式、标记和清扫阶段的耗时,以及按字节和地址空间块划分的详细内存使用情况。

© 2026 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.