Electron 和 V8 内存笼
Electron 21 及更高版本将启用 V8 内存笼,这对一些原生模块有影响。
要跟踪有关 Electron 21+ 中原生模块使用的持续讨论,请参阅 electron/electron#35801.
在 Electron 21 中,我们将启用 V8 沙箱指针 在 Electron 中,遵循 Chrome 的 在 Chrome 103 中做出同样的决定。这对原生模块有一些影响。此外,我们之前在 Electron 14 中启用了相关的技术,指针压缩。我们当时没有过多谈论它,但指针压缩对 V8 堆的最大大小有影响。
当启用这两种技术时,它们对安全、性能和内存使用都有显著的益处。但是,启用它们也有一些缺点。
启用沙箱指针的主要缺点是
启用指针压缩的主要缺点是
该 Electron 升级工作组 认为指针压缩和 V8 内存笼的益处大于其缺点。这样做有三个主要原因: 最后,对于真正需要更大堆大小的应用程序,有一些解决方法。例如,可以在应用程序中包含 Node.js 的副本,该副本是在禁用指针压缩的情况下构建的,并将内存密集型工作转移到子进程。虽然有点复杂,但也可以构建禁用指针压缩的 Electron 自定义版本,如果您决定要为您的特定用例进行不同的权衡。最后,在不久的将来, wasm64 将允许使用 WebAssembly 在 Web 和 Electron 中构建的应用程序使用超过 4GB 的内存。 在 Electron 20+ 中,尝试使用 ArrayBuffer 包装外部内存会导致运行时崩溃。 如果您在应用程序中未使用任何原生 Node 模块,那么您是安全的,因为无法从纯 JS 触发此崩溃。此更改只会影响在 V8 堆之外分配内存(例如,使用 在渲染器进程中,可以使用 一些文档将其称为“V8 沙箱”,但该术语很容易与 Chromium 中的其他类型的沙箱 混淆,因此我将坚持使用“内存笼”一词。 有一种相当常见的 V8 漏洞,其工作原理如下: V8 内存笼是一种旨在彻底防止这种攻击的技术。其实现方式是不在 V8 堆中存储任何指针。相反,对 V8 堆中其他内存的所有引用都存储为从某个保留区域开始的偏移量。这样,即使攻击者设法破坏 ArrayBuffer 的基地址,例如通过利用 V8 中的类型混淆错误,他们能做的最坏情况也只是读写笼子内的内存,而他们很可能已经能够这样做。关于 V8 内存笼的工作原理还有很多资料可供阅读,所以我在这里不再赘述,最好的入门资料可能是来自 Chromium 团队的高级设计文档。 重构原生模块使其兼容 V8 内存笼有两种方法。第一种是复制外部创建的缓冲区到 V8 内存笼中,然后再将它们传递给 JavaScript。这通常是一个简单的重构,但当缓冲区很大时可能会很慢。另一种方法是使用 V8 的内存分配器来分配内存,你打算最终将这些内存传递给 JavaScript。这有点复杂,但可以避免复制,这意味着对大型缓冲区来说性能更好。 为了更具体地说明,以下是一个使用外部数组缓冲区的示例 N-API 模块 当启用内存笼时,这会导致崩溃,因为数据是在笼子外部分配的。重构以将数据复制到笼子中,我们得到 这会将数据复制到一个新分配的内存区域,该区域位于 V8 内存笼中。可选地,N-API 还可以提供指向新复制数据的指针,以防你之后需要修改或引用它。 重构以使用 V8 的内存分配器稍微复杂一些,因为它要求修改
常见问题解答
如何知道我的应用程序是否会受到此更改的影响?
malloc
或 new
)然后使用 ArrayBuffer 包装外部内存的原生 Node 模块。这是一种相当罕见的情况,但有一些模块确实使用这种技术,这些模块需要重构才能与 Electron 20+ 兼容。如何衡量我的应用程序使用了多少 V8 堆内存,从而知道我是否接近 4GB 限制?
performance.memory.usedJSHeapSize
,它将返回 V8 堆使用量(以字节为单位)。在主进程中,可以使用 process.memoryUsage().heapUsed
,它与之类似。什么是 V8 内存笼?
我想重构一个 Node 原生模块以支持 Electron 21+。我该怎么做?
// Create some externally-allocated buffer.
// |create_external_resource| allocates memory via malloc().
size_t length = 0;
void* data = create_external_resource(&length);
// Wrap it in a Buffer--will fail if the memory cage is enabled!
napi_value result;
napi_create_external_buffer(
env, length, data,
finalize_external_resource, NULL, &result);size_t length = 0;
void* data = create_external_resource(&length);
// Create a new Buffer by copying the data into V8-allocated memory
napi_value result;
void* copied_data = NULL;
napi_create_buffer_copy(env, length, data, &copied_data, &result);
// If you need to access the new copy, |copied_data| is a pointer
// to it!create_external_resource
函数以使用 V8 分配的内存,而不是使用malloc
。这可能或多或少可行,具体取决于你是否控制create_external_resource
的定义。思路是首先使用 V8 创建缓冲区,例如使用napi_create_buffer
,然后将资源初始化到 V8 分配的内存中。保留对 Buffer 对象的napi_ref
对于资源的生命周期很重要,否则 V8 可能会垃圾回收 Buffer,并可能导致使用后释放错误。