refactor: 收集路径原地化退役移动式sweep与门控

This commit is contained in:
2026-10-01 09:06:23 +00:00
parent 1711ae948b
commit 5c8699733d
14 changed files with 309 additions and 573 deletions
+1 -10
View File
@@ -697,20 +697,11 @@ fn async_state_mut(obj: &JsObject) -> Option<&mut AsyncState> {
None
} else {
// SAFETY: native_data 在 create_async_context 中由 Box::into_raw 分配,
// 生命周期与异步上下文对象一致;GC 路径(mark 边收集、执行期门控扫描)只读状态盒。
// 生命周期与异步上下文对象一致;GC 路径(mark 边收集)只读状态盒。
Some(unsafe { &mut *ptr })
}
}
/// 异步挂起帧是否持有 for-in 迭代器:执行期收集门控按指针扫描
/// (迭代器体是堆上 Box,门控是保守门)。
pub(crate) fn async_holds_suspended_for_in(obj: &JsObject) -> bool {
let Some(state) = async_state_mut(obj) else {
return false;
};
!state.suspended.for_in_iters.is_empty()
}
/// 异步状态内全部引用边的扁平列表(GC mark 边):对象/字符串/BigInt 均产出,
/// 消费侧按值类型分发到对象栈与存活集。
pub(crate) fn async_native_edges(obj: &JsObject) -> Vec<JsValue> {
+1 -10
View File
@@ -1671,20 +1671,11 @@ fn async_gen_state_mut(obj: &JsObject) -> Option<&mut AsyncGeneratorState> {
None
} else {
// SAFETY: native_data 在 create_async_generator_object 中由 Box::into_raw 分配,
// 生命周期与异步生成器对象一致;GC 路径(mark 边收集、执行期门控扫描)只读状态盒。
// 生命周期与异步生成器对象一致;GC 路径(mark 边收集)只读状态盒。
Some(unsafe { &mut *ptr })
}
}
/// 异步生成器挂起帧是否持有 for-in 迭代器:执行期收集门控按指针扫描
/// (迭代器体是堆上 Box,门控是保守门)。
pub(crate) fn async_generator_holds_suspended_for_in(obj: &JsObject) -> bool {
let Some(state) = async_gen_state_mut(obj) else {
return false;
};
!state.suspended.for_in_iters.is_empty()
}
/// 异步生成器状态内全部引用边的扁平列表(GC mark 边):对象/字符串/BigInt
/// 均产出,消费侧按值类型分发到对象栈与存活集。
pub(crate) fn async_generator_native_edges(obj: &JsObject) -> Vec<JsValue> {
+2 -11
View File
@@ -959,20 +959,11 @@ fn generator_state_mut(obj: &JsObject) -> Option<&mut GeneratorState> {
None
} else {
// SAFETY: native_data 在 create_generator_object 中由 Box::into_raw 分配,
// 生命周期与生成器对象一致;GC 路径(mark 边收集、执行期门控扫描)只读状态盒。
// 生命周期与生成器对象一致;GC 路径(mark 边收集)只读状态盒。
Some(unsafe { &mut *ptr })
}
}
/// 生成器挂起帧是否持有 for-in 迭代器:执行期收集门控按指针扫描
/// (迭代器体是堆上 Box,门控是保守门)。
pub(crate) fn generator_holds_suspended_for_in(obj: &JsObject) -> bool {
let Some(state) = generator_state_mut(obj) else {
return false;
};
!state.suspended.for_in_iters.is_empty()
}
/// 生成器状态内全部引用边的扁平列表(GC mark 边):对象/字符串/BigInt 均产出,
/// 消费侧按值类型分发到对象栈与存活集。
pub(crate) fn generator_native_edges(obj: &JsObject) -> Vec<JsValue> {
@@ -1031,7 +1022,7 @@ pub(crate) fn generator_native_cell_edges(obj: &JsObject) -> Vec<*mut Cell> {
out
}
/// 用转发函数重写状态快照中的所有 JsValue(session GC 移动式清扫 / promote 用)。
/// 用转发函数重写状态快照中的所有 JsValue(promote 用)。
pub(crate) fn rewrite_generator_native(obj: &JsObject, mut rewrite: impl FnMut(JsValue) -> JsValue) {
let Some(state) = generator_state_mut(obj) else {
return;
+7 -17
View File
@@ -33,11 +33,11 @@ pub(crate) fn promise_native_edges(obj: &JsObject) -> Vec<JsValue> {
edges
}
/// 用转发函数重写状态盒中的所有 JsValue(session GC 移动式清扫 / promote 用)。
/// 用转发函数重写状态盒中的所有 JsValue(promote 用)。
///
/// 结算链指针是裸指针边、不在本函数改写面:晋升路径克隆新分配于 session
/// arena(原件地址不变),同 arena 改写场景指针天然有效;搬移换址场景由
/// 调用方在重写完成后经 `repoint_promise_promoted_clone` 按转发表重定位。
/// 结算链指针是裸指针边、不在本函数改写面:晋升克隆新分配于 session arena
/// (原件地址不变),改写场景指针天然有效;结算链接链由
/// `migrate_settlement_to_newest_clone` 负责。
pub(crate) fn rewrite_promise_native(obj: &JsObject, mut rewrite: impl FnMut(JsValue) -> JsValue) {
let Some(state) = promise_state_mut(obj) else {
return;
@@ -53,12 +53,12 @@ pub(crate) fn rewrite_promise_native(obj: &JsObject, mut rewrite: impl FnMut(JsV
}
}
/// 深拷贝状态盒到新对象(promote / sweep 用):新对象持独立 Box,源盒可安全释放。
/// 深拷贝状态盒到新对象(promote 用):新对象持独立 Box,源盒可安全释放。
///
/// 源的反应**迁移**(非复制)进新对象:源盒随 epoch 释放,留在源上的反应会
/// 永久丢失、原件侧消费者永不触发;每条反应的 JsValue 随状态盒其余字段同一
/// pass 晋升/改写。结算链指针按原值保留(晋升场景由
/// `migrate_settlement_to_newest_clone` 接链,搬移场景按转发表重定位)。
/// pass 晋升/改写。结算链指针按原值保留(接链由
/// `migrate_settlement_to_newest_clone` 负责)。
pub(crate) fn clone_promise_native_with_rewrite(
old: &JsObject, new: &mut JsObject, mut rewrite: impl FnMut(JsValue) -> JsValue,
) {
@@ -144,16 +144,6 @@ pub(crate) fn migrate_settlement_to_newest_clone(
}
}
/// 按给定转发改写结算链指针(裸指针边、非 JsValue),session GC 搬移后调用。
pub(crate) fn repoint_promise_promoted_clone(obj: &JsObject, forward: impl FnOnce(*mut JsObject) -> *mut JsObject) {
let Some(state) = promise_state_mut(obj) else {
return;
};
if !state.promoted_clone.is_null() {
state.promoted_clone = forward(state.promoted_clone);
}
}
/// 改写单条反应的四条 JsValue 引用(派生 promise 能力与处理器),源反应迁移
/// 与再晋升合并共用。
fn rewrite_reaction<F: FnMut(JsValue) -> JsValue>(r: PromiseReaction, rewrite: &mut F) -> PromiseReaction {
+1 -1
View File
@@ -19,7 +19,7 @@ mod settlement;
pub(crate) use gc_edges::{
clone_promise_native_with_rewrite, drop_promise_native, migrate_settlement_to_newest_clone, promise_native_edges,
promise_native_size, repoint_promise_promoted_clone, rewrite_promise_native,
promise_native_size, rewrite_promise_native,
};
pub(crate) use jobs::{for_each_job_value, rewrite_job_values};
pub use reactions::promise_settled_value;
+28 -53
View File
@@ -9,15 +9,15 @@ use crate::vm::Vm;
use oxide_builtins::{array_buffer, data_view, disposable_stack, map, module, regexp, set, typed_array, weak_map};
impl Vm {
/// 单对象晋升测试入口:取/清共享转发表后调 `promote_object_inner`。
/// 生产路径(边界修复/执行期晋升档/根晋升)各自持表管理,不经过此包装。
/// 单对象晋升测试入口:局部转发表下调 `promote_object_inner`。
/// 生产路径(边界修复/根晋升)各自持表管理,不经过此包装。
#[cfg(test)]
pub(crate) fn promote_object(&mut self, src: *mut JsObject) -> *mut JsObject {
vm_debug!("promote_object: src={:p}", src);
let mut forwarding = std::mem::take(&mut self.gc_state.forwarding);
let mut forwarding = HashMap::with_hasher(FxBuildHasher);
let result = self.promote_object_inner(src, &mut forwarding);
// 局部表随作用域丢弃,晋升克隆的转发改写已在其上完成。
forwarding.clear();
self.gc_state.forwarding = forwarding;
result
}
@@ -53,7 +53,7 @@ impl Vm {
});
} else if src_ref.is_weak_map_obj() {
// 值边随克隆晋升(强边);弱键原样搬运,生死交晋升收敛后的
// 弱键定夺路径按同一转发表判定。
// 弱键定夺路径(现为原样保留)。
weak_map::clone_weak_map_native_with_rewrite(src_ref, dst_ref, |value| {
self.promote_value_if_epoch_object(value, forwarding)
});
@@ -145,7 +145,8 @@ impl Vm {
/// global 对象的属性值(逃逸写直通后 epoch 值直落 global 槽,global 不入
/// session 对象表,由 `rewrite_session_epoch_refs` 同趟改写)。
/// `rewrite_object_values` 覆盖元素/meta/属性/proto/captured_this/home_object/
/// cell 值;克隆子树经 forwarding 去重,环与共享引用各克隆一次。
/// cell 值;克隆子树经局部转发表去重,环与共享引用各克隆一次。
/// 统一入口 Box 化后生产路径不产 epoch 对象,本方法为晋升族空转保留面。
///
/// # 注意事项
/// - 须在执行外的安全点调用(无在途 builtin 局部裸指针、dispatch 未重入):
@@ -154,14 +155,12 @@ impl Vm {
/// - session 对象表为空时 global 的 epoch 子引用仍须修复,不做空表早退。
pub fn promote_session_epoch_refs(&mut self) {
let objects = std::mem::take(&mut self.gc_state.session_object_ptrs);
let mut forwarding = std::mem::take(&mut self.gc_state.forwarding);
let mut forwarding = HashMap::with_hasher(FxBuildHasher);
self.rewrite_session_epoch_refs(&objects, &mut forwarding);
// 弱键按收敛后的转发表定生死:未入表 epoch 键随 epoch 释放而亡。
// 弱键定夺退化为原样保留(定夺路径无克隆可重指,见
// resolve_weak_key_after_promotion);调用保留以收口族内流程。
self.rewrite_weak_map_keys_after_promotion(&objects, &mut forwarding);
forwarding.clear();
self.gc_state.forwarding = forwarding;
// 改写期新克隆已随晋升推入 gc_state 侧的表(此刻仅含克隆体),把取出的
// 旧对象按原相对序拼回同一表,表 = 克隆体 + 旧对象,各恰登记一次。
// 局部转发表随作用域丢弃:晋升新克隆已推入对象表,旧对象按原序拼回。
self.gc_state.session_object_ptrs.extend(objects);
}
@@ -173,8 +172,8 @@ impl Vm {
/// 与列表同趟改写,无 native 盒只覆盖 JS 边。
///
/// # 注意事项
/// - 调用方持有转发表期间不得让其它晋升路径改动 `gc_state.forwarding`;
/// 晋升新克隆推入的对象表由调用方决定何时拼回。
/// - 调用方持有局部转发表期间,晋升新克隆推入的对象表由调用方决定何时
/// 拼回;本方法不改表。
/// - 死对象(不可达)的 epoch 子引用改写会把死对象一并克隆进 session——
/// 克隆体无根,下一轮收集按死对象出表,不泄漏也不悬垂。
pub(crate) fn rewrite_session_epoch_refs(
@@ -244,15 +243,15 @@ impl Vm {
}
/// 晋升收敛后的弱键定夺:对候选集(session 对象列表 + 已晋升克隆)中的
/// 每个弱表按转发表判定弱键——表内键改指新址,未入表 epoch 键为死键丢
/// 条目,session/P 键保留。
/// 每个弱表按定夺路径处理条目。统一入口 Box 化后定夺退化为原样保留
/// (无克隆可重指、reset 边界 mark 位无本轮可达性信息),本路径不改表项。
///
/// # 副作用
/// - 每个弱表原位重建条目表;无死键时条目与容量口径不变。
/// - 每个弱表原位重建条目表(整表重建口径,条目内容不变)。
///
/// # 边界与前提
/// - 须在主改写路径(JS 边 + 值边)对转发表收敛完成后调用;转发表届时
/// 即最终强可达集。调用方负责其后的清表。
/// 即最终强可达集(统一入口树上恒空)。调用方负责其后的清表。
/// - global 上的弱表经其晋升克隆进入转发表值集,候选集无需单列 global。
pub(crate) fn rewrite_weak_map_keys_after_promotion(
&mut self, objects: &[*mut JsObject], forwarding: &mut HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>,
@@ -267,7 +266,7 @@ impl Vm {
if obj.is_weak_map_obj() {
weak_map::rewrite_weak_map_native(
obj,
|key| crate::session_gc::resolve_weak_key_after_promotion(key, forwarding),
|key| crate::session_gc::resolve_weak_key_after_promotion(key),
|value| value,
);
}
@@ -280,7 +279,7 @@ impl Vm {
if obj.is_weak_map_obj() {
weak_map::rewrite_weak_map_native(
obj,
|key| crate::session_gc::resolve_weak_key_after_promotion(key, forwarding),
|key| crate::session_gc::resolve_weak_key_after_promotion(key),
|value| value,
);
}
@@ -289,26 +288,19 @@ impl Vm {
}
/// 把根直接持有的 epoch 对象(顶层 var 寄存器、挂起句柄等)晋升进 session,
/// 并把根引用改写到克隆体。
/// 并把根引用改写到克隆体。统一入口 Box 化后生产路径无 epoch 对象,
/// 本方法为晋升族空转保留面(基准留存测量与图留存测试仍经此入口)。
///
/// # 副作用
/// - 每个根 epoch 对象深克隆进 session(含 native 状态盒,子引用递归晋升),
/// 账目计入克隆字节;根引用按转发表重写。
/// 账目计入克隆字节;根引用按局部转发表重写。
///
/// # 注意事项
/// - 须在执行外的安全点调用(无在途 builtin 局部裸指针、dispatch 未重入):
/// 本方法改写全部根寄存器槽,执行期调用将使 builtin 局部裸指针失效
/// (与 `collect_session_gc` 同前提)。
/// - 供 workload 后观测点(基准留存测量)使用:本方法之后存活集完全对
/// session 可见,后续完整 GC 的留存账目不再漏"仅驻留 epoch arena 的对象"。
/// - 与 `promote_session_epoch_refs` 互补:本方法处理根直接持有的 epoch 对象,
/// 后者处理 session 对象持有的 epoch 子引用(闭包捕获等绕过写屏障的来源)。
/// - 弱键定夺(`rewrite_weak_map_keys_after_promotion`)只按根直接子树的
/// 转发表为判据:经 session 对象(非根)强持的 epoch 键不在表内会被判死
/// 丢条目,而本调用不释放 epoch——键强存活时条目即消失。配对调用
/// (随后 `promote_session_epoch_refs`)由第二趟收敛转发表,但第一趟已丢
/// 条目不可复原;独立调用须根直接子树已覆盖全部强可达 epoch 对象
/// (即不存在经 session 对象强持的弱键)方为合法时机。
pub fn promote_rooted_epoch_objects(&mut self) {
let mut epoch_roots = Vec::new();
self.for_each_root(|value| {
@@ -322,23 +314,22 @@ impl Vm {
}
}
});
let mut forwarding = std::mem::take(&mut self.gc_state.forwarding);
// 局部转发表:晋升克隆去重共享与环,作用域结束即弃。
let mut forwarding = HashMap::with_hasher(FxBuildHasher);
for ptr in epoch_roots {
self.promote_object_inner(ptr, &mut forwarding);
}
crate::session_gc::rewrite_vm_roots(self, &forwarding);
// 弱表键定夺:根直接持有的弱表克隆其弱键按转发表判生死。
// 弱表键定夺退化为原样保留(定夺路径无克隆可重指,见
// resolve_weak_key_after_promotion);调用保留以收口族内流程。
self.rewrite_weak_map_keys_after_promotion(&[], &mut forwarding);
forwarding.clear();
self.gc_state.forwarding = forwarding;
}
/// 逃逸写屏障:写向全局/session 目标的对象值原样直通。
///
/// session 对象持有 epoch 子引用是 GC 设计的一等存活态:epoch 边界 reset
/// 经 `promote_session_epoch_refs` 统一克隆晋升(转发表去重共享与环),
/// 执行期收集晋升档按同一转发表改写根与存活 session 对象子引用,两条
/// 路径覆盖全部跨界持有面;builtin 调用期内 epoch 不重置,帧局部裸指针
/// 经 `promote_session_epoch_refs` 统一克隆晋升(转发表去重共享与环)
/// 覆盖全部跨界持有面;builtin 调用期内 epoch 不重置,帧局部裸指针
/// 全程有效。此处若克隆,同一逻辑对象分裂为原件与克隆两份,经克隆的写
/// 对方经原件的读不可见(builtin 帧指针与逃逸目标双引用并存时即暴露)。
pub(crate) fn promote_if_needed_for_write_ptr(&mut self, _target_ptr: *mut JsObject, value: JsValue) -> JsValue {
@@ -530,20 +521,4 @@ mod tests {
assert!(!is_epoch_object(&vm, meta.get));
assert!(!is_epoch_object(&vm, meta.set));
}
#[test]
fn promote_clears_forwarding_map() {
let mut vm = Vm::new();
let first = plain_object(&mut vm);
let promoted = vm.promote_object(first);
assert!(!promoted.is_null());
// 共享 forwarding 表必须在每次 promote 后清空,使后续 promote(或 GC 清扫)
// 永不观察到过期的 old->new 映射。
assert!(vm.gc_state.forwarding.is_empty());
let second = plain_object(&mut vm);
let promoted2 = vm.promote_object(second);
assert!(!promoted2.is_null());
assert!(vm.gc_state.forwarding.is_empty());
}
}
+160 -266
View File
@@ -13,10 +13,10 @@ use oxide_builtins::{array_buffer, data_view, disposable_stack, map, module, reg
/// session 级 mark-sweep GC 的状态与统计。
///
/// 回收 session arena 中不再可达的对象与 `Vm::new_string` 分配的 session 字符串:
/// `mark` 从 VM roots 标记存活对象,`sweep` 将存活对象复制进新 arena(移动式)、
/// 更新所有引用,并释放死对象;`sweep_session_strings` 按存活标记回收字符串。
/// 所有统计字段供外部观测 GC 行为。
/// 回收 session 对象表中不再可达的对象与 `Vm::new_string` 分配的 session 字符串:
/// `mark` 从 VM roots 标记存活对象,`sweep` 原地清扫(存活对象带位保留、地址不变,
/// 死对象本体、堆区与独占 upvalue 列表原地释放出表);`sweep_session_strings`
/// 按存活标记回收字符串。所有统计字段供外部观测 GC 行为。
pub struct SessionGc {
pub total_collections: u64,
pub total_bytes_freed: u64,
@@ -51,8 +51,10 @@ impl SessionGc {
self.clear_session_marks(vm);
}
/// 只清 session 对象表的 mark 位。移动式 sweep 末尾的旧表对象已全释放/克隆
/// (克隆未标记),清位须与表重写同步执行,独立成臂供调用点按需取用。
/// 只清 session 对象表的 mark 位。原地 sweep 后死对象已出表、存活对象
/// 的位在活分支清掉,正常收集路径无残留位;此臂处理 strings-only 路径
/// 与收集前历史残留——残留 true 会让下一次 `mark` 的 DFS 在已标对象处
/// 短路,漏扫其新增引用边。
pub(crate) fn clear_session_marks(&mut self, vm: &mut Vm) {
for &ptr in &vm.gc_state.session_object_ptrs {
if ptr.is_null() {
@@ -368,12 +370,13 @@ impl SessionGc {
}
}
/// 从 VM roots 标记存活的 session/epoch 对象、字符串与 BigInt,供 `sweep` 判定。
/// 从 VM roots 标记存活 session 对象、字符串与 BigInt,供 `sweep` 判定。
///
/// 对象 DFS 同时跟踪 session 与 epoch 两张表,两族 mark 位同字节,晋升收集按
/// “标记 ∩ epoch 表”取活集;字符串边走 rope 闭包传播(Cons 子节点与扁平化产物),
/// BigInt 边直接入存活集。roots 由 `Vm::for_each_root` 枚举,字段清单与
/// `rewrite_values` 一一对应、须同步。只置位不搬移,调用前须先 `clear_all_marks` 清位。
/// 对象 DFS 跟踪 session 对象单表(统一入口 Box 化后无 epoch 表);perm
/// 对象不置位(其位跨收集残留会令 DFS 短路漏扫),走独立已访集防环。
/// 字符串边走 rope 闭包传播(Cons 子节点与扁平化产物),BigInt 边直接入
/// 存活集。roots 由 `Vm::for_each_root` 枚举,字段清单与 `rewrite_values`
/// 一一对应、须同步。只置位不搬移,调用前须先 `clear_all_marks` 清位。
pub(crate) fn mark(&mut self, vm: &Vm) {
vm_debug!("[GC] mark phase: {} roots", vm.gc_state.session_object_ptrs.len());
let mut seeds = Vec::new();
@@ -464,20 +467,20 @@ impl SessionGc {
/// 释放对象本体之外的堆外属性数据(元素区、元素 meta、hash 属性区、属性 meta
/// 与各族 native 状态盒),按 capacity 经 `Box::from_raw` 各恰好释放一次,返回
/// 字节数。`obj_ptr` 为空时返回 0;`require_session` 为 true 时断言对象属
/// session epoch,epoch 原件走 false 分支。不含 upvalue 列表与对象本体,由调用方处理。
pub(crate) fn drop_object_heap_data(obj_ptr: *mut JsObject, require_session: bool) -> u64 {
/// 字节数。`obj_ptr` 为空时返回 0。不含 upvalue 列表与对象本体,由调用方处理。
///
/// # 边界与前提
/// - 统一对象表内全部对象为 session 对象(统一入口 Box 化后生产路径无
/// epoch 原件),本函数只服务表内对象的释放路径,不做归属断言。
pub(crate) fn drop_object_heap_data(obj_ptr: *mut JsObject) -> u64 {
if obj_ptr.is_null() {
return 0;
}
// SAFETY: `obj_ptr` 在调用本辅助函数前已校验,指向 VM session arena 拥有的
// session 对象。只重建 JsObject::ensure_hash_props/ensure_prop_meta 分配的 Box,
// SAFETY: `obj_ptr` 在调用本辅助函数前已校验,指向 VM 拥有的 session 对象。
// 只重建 JsObject::ensure_hash_props/ensure_prop_meta 分配的 Box,
// 且只在这里释放一次。
unsafe {
let obj = &mut *obj_ptr;
if require_session {
debug_assert!(obj.is_session_epoch());
}
let mut freed_bytes = 0u64;
let elems_ptr = obj.array_elements_raw() as *mut Vec<JsValue>;
@@ -529,19 +532,23 @@ impl SessionGc {
}
}
fn drop_session_object_heap_data(obj_ptr: *mut JsObject) -> u64 {
Self::drop_object_heap_data(obj_ptr, true)
}
/// 释放一个死 session 对象:对象本体 + 堆数据 + upvalue 列表,返回释放字节数。
///
/// # 注意事项
/// 仅 sweep 死分支到达此处:只有存活对象被克隆、克隆与原件共享同一 upvalues
/// Box,死对象从不被克隆,其 upvalue 列表 Box 无其他持有者,在此恰好释放
/// 一次;存活分支绝不释放(克隆仍引用同一 Box)。释放后置空:死对象已移出
/// 对象表,收尾统一释放按表枚举不会再见,置空保证对象侧幂等、无陈旧指针。
fn drop_dead_session_object(obj_ptr: *mut JsObject) -> u64 {
let mut freed = Self::drop_session_object_heap_data(obj_ptr) + size_of::<JsObject>() as u64;
/// # 步骤
/// 1. 释放堆数据(元素区 / 属性区 / meta 区 / native 状态盒)。
/// 2. 释放 upvalue 列表 Box 并置空对象侧字段。
/// 3. 释放对象本体:堆载体(HEAP_BIT)经 `Box::from_raw` 释放;
/// Bump 载体(晋升族克隆,仅测试形态)随旧 Bump 换新归还,不在此释放。
///
/// # 边界与前提
/// - `obj_ptr` 必须非空:空指针时第 1 步返回 0,但本体字节与位域读取
/// 对空指针无效,调用点(sweep 死分支 / 收尾逐对象路径)先行跳过空位。
/// - 仅 sweep 死分支与统一收尾到达此处:死对象从不被克隆,upvalue 列表
/// Box 无其他持有者,在此恰好释放一次;存活分支绝不释放(若有克隆仍
/// 引用同一 Box)。释放后死对象已移出对象表,收尾统一释放按表枚举
/// 不会再见,置空保证对象侧幂等、无陈旧指针。
pub(crate) fn drop_dead_session_object(obj_ptr: *mut JsObject) -> u64 {
let mut freed = Self::drop_object_heap_data(obj_ptr) + size_of::<JsObject>() as u64;
// SAFETY: obj_ptr 来自 session 对象表,sweep 期间仍指向合法对象;
// upvalues Box 仅本对象持有(死对象不克隆),保证恰好释放一次。
unsafe {
@@ -553,8 +560,8 @@ impl SessionGc {
(*obj_ptr).upvalues = std::ptr::null_mut();
std::mem::drop(vec);
}
// 释放对象本体:堆载体(HEAP_BIT)经 Box::from_raw 释放,arena 载体
// (克隆)随旧 arena 归还,不在此释放。
// 释放对象本体:堆载体(HEAP_BIT)经 Box::from_raw 释放,Bump
// 载体(克隆)随旧 Bump 换新归还,不在此释放。
if (*obj_ptr).is_heap_alloc() {
drop(Box::from_raw(obj_ptr));
}
@@ -576,172 +583,116 @@ impl SessionGc {
bytes
}
/// 移动式清扫 session 对象:存活对象复制进新 arena,死对象原地释放。
/// 原地清扫 session 对象:存活对象带位保留(清位并入活分支)、地址不变,
/// 死对象释放本体、堆区与独占 upvalue 列表并出表;弱表弱键在死对象释放前
/// 按 mark 位定生死(死键条目丢弃)。免转发表与根重写。
///
/// 按 GC mark 位分流:存活对象经 `clone_for_session_epoch` 复制(native 盒随族深拷)
/// 并登记 forwarding 旧址→新址,死对象释放本体、堆区与独占 upvalue;随后按转发表重写
/// 新对象的值边、native 边与 promise 结算链裸指针,转发查找使共享与环去重到同一克隆。
/// 最后重写全部根引用、换表并以新 Bump 接管 session epoch,旧 arena 整体归还并清 mark 位。
/// # 步骤
/// 1. 弱键定夺:对每个存活弱表按 mark 位重建条目表(此刻全部键对象
/// 仍分配,位域可读,见 `resolve_weak_key_sweep` 时序前提)。
/// 2. 分流:已标对象保留并清位(无残留位),未标对象原地释放出表。
/// 3. 表回写存活集,按存活重算对象口径账目,累计扫描/存活/死统计。
///
/// # 副作用
/// - `session_object_ptrs` 仅剩存活对象;`session_bytes_allocated` 重算为
/// 存活对象字节(串/BigInt/cell 分量由各清扫路径自行补回);
/// - `total_*` 与 `last_collection_*` 统计按对象口径更新(绝对赋值,
/// 串/BigInt/cell 清扫在其上累加)。
///
/// # 边界与前提
/// - 调用前须完成 `mark`(位域为本次收集的判定依据);
/// - 死对象从不被克隆,upvalue 列表 Box 无其他持有者,恰好一次释放成立。
pub(crate) fn sweep(&mut self, vm: &mut Vm) -> u64 {
let old_ptrs = std::mem::take(&mut vm.gc_state.session_object_ptrs);
let mut forwarding = std::mem::take(&mut vm.gc_state.forwarding);
let new_arena = bumpalo::Bump::new();
let mut survivors = 0u64;
let mut dead = 0u64;
let mut freed_bytes = 0u64;
self.sweep_in_place(vm).2
}
for old_ptr in old_ptrs {
if old_ptr.is_null() {
/// 原地 sweep 核心:弱键定夺、死对象释放与出表、存活重算、统计累计。
/// `sweep`(完整收集)与 `collect_in_run`(执行期收集)共用,保证两条
/// 收集路径的弱键定夺口径一致。返回 (存活数, 死数, 释放字节)。
fn sweep_in_place(&mut self, vm: &mut Vm) -> (u64, u64, u64) {
let old_ptrs = std::mem::take(&mut vm.gc_state.session_object_ptrs);
let mut survivors = Vec::with_capacity(old_ptrs.len());
// 弱键定夺先于死对象释放:键对象(含死键)此刻全部仍分配,
// 对象头位域读取安全(时序前提见 resolve_weak_key_sweep)。
for &ptr in &old_ptrs {
if ptr.is_null() {
continue;
}
// SAFETY: old_ptr 来自 session 对象表,sweep 运行期间仍指向合法对象。
let is_live = unsafe { (*old_ptr).is_gc_marked() };
let is_heap = unsafe { (*old_ptr).is_heap_alloc() };
if is_live {
survivors += 1;
let old_ref = unsafe { &*old_ptr };
let clone = old_ref.clone_for_session_epoch();
let new_ptr = new_arena.alloc(clone) as *mut JsObject;
let new_ref = unsafe { &mut *new_ptr };
if old_ref.is_map() {
map::clone_map_native_with_rewrite(old_ref, new_ref, |value| value);
} else if old_ref.is_set() {
set::clone_set_native_with_rewrite(old_ref, new_ref, |value| value);
} else if old_ref.is_weak_map_obj() {
// 弱键原样搬运:键生死按转发表收敛后的判定(phase 2 与
// 晋升定夺路径同判据),此处只深拷贝盒与改写值边。
weak_map::clone_weak_map_native_with_rewrite(old_ref, new_ref, |value| value);
} else if old_ref.is_disposable_stack_obj() || old_ref.is_async_disposable_stack_obj() {
disposable_stack::clone_dispose_native_with_rewrite(old_ref, new_ref, |value| value);
} else if old_ref.is_array_buffer_obj() {
array_buffer::clone_array_buffer_native(old_ref, new_ref);
} else if old_ref.is_shared_array_buffer_obj() {
array_buffer::clone_shared_array_buffer_native(old_ref, new_ref);
} else if old_ref.is_typed_array_obj() {
typed_array::clone_typed_array_native_with_rewrite(old_ref, new_ref, |value| value);
} else if old_ref.is_data_view_obj() {
data_view::clone_data_view_native_with_rewrite(old_ref, new_ref, |value| value);
} else if old_ref.is_promise_obj() {
crate::promise::clone_promise_native_with_rewrite(old_ref, new_ref, |value| value);
} else if old_ref.is_async_obj() {
crate::async_func::clone_async_native_with_rewrite(old_ref, new_ref, |value| value);
} else if old_ref.is_async_generator_obj() {
crate::async_generator::clone_async_generator_native_with_rewrite(old_ref, new_ref, |value| value);
} else if old_ref.is_generator_obj() {
crate::generator::clone_generator_native_with_rewrite(old_ref, new_ref, |value| value);
} else if old_ref.holds_compiled_regex() {
// 已编译正则是 Box 深拷贝到新对象:源 Box 由 drop 释放,互不共享。
regexp::clone_regexp_native(old_ref, new_ref);
} else if old_ref.is_module_namespace() {
// 条目表深拷贝:`clone_for_session_epoch` 只浅拷贝 native_data 指针。
module::clone_module_ns_native_with_rewrite(old_ref, new_ref, |value| value);
}
forwarding.insert(old_ptr, new_ptr);
freed_bytes += Self::drop_session_object_heap_data(old_ptr);
// 释放原件本体:堆载体(HEAP_BIT)经 Box::from_raw 释放,arena
// 载体(克隆)随旧 arena 归还,不在此释放。
if is_heap {
freed_bytes += size_of::<JsObject>() as u64;
unsafe {
drop(Box::from_raw(old_ptr));
}
// SAFETY: ptr 来自 session 对象表登记,sweep 运行期间有效。
let obj = unsafe { &*ptr };
if obj.is_gc_marked() && obj.is_weak_map_obj() {
// SAFETY: 弱表 native 盒独占,整表重建在定夺期间无并发读者。
unsafe {
weak_map::rewrite_weak_map_native(
&mut *ptr,
|key| resolve_weak_key_sweep(key),
|value| value,
);
}
}
}
// 按 mark 位分流:存活保留清位,死对象释放本体、堆区与 upvalue 出表。
let mut dead = 0u64;
let mut freed_bytes = 0u64;
for &ptr in &old_ptrs {
if ptr.is_null() {
continue;
}
// SAFETY: ptr 来自 session 对象表登记,sweep 运行期间有效。
if unsafe { (*ptr).is_gc_marked() } {
// 原地 sweep 不搬移对象:存活对象带位保留,清位并入活分支
// (收集后无残留,残留 true 使下一次 mark DFS 短路漏标)。
unsafe { (*ptr).set_gc_mark(false) };
survivors.push(ptr);
} else {
// SAFETY: 死对象从不被克隆,upvalue 列表 Box 无其他持有者。
freed_bytes += Self::drop_dead_session_object(ptr);
dead += 1;
freed_bytes += Self::drop_dead_session_object(old_ptr);
}
}
let live = survivors.len() as u64;
vm.gc_state.session_object_ptrs = survivors;
for &dst in forwarding.values() {
// SAFETY: forwarding 中的指针全是新分配且已初始化的对象。
let obj = unsafe { &mut *dst };
obj.rewrite_object_values(|value| {
if value.is_object() {
let ptr = value.as_js_object_ptr();
if let Some(&fwd) = forwarding.get(&ptr) {
return JsValue::from_js_object(fwd);
}
}
value
});
if obj.is_map() {
map::rewrite_map_native(obj, |value| rewrite_forwarded_value(value, &forwarding));
} else if obj.is_set() {
set::rewrite_set_native(obj, |value| rewrite_forwarded_value(value, &forwarding));
} else if obj.is_weak_map_obj() {
// 键按转发表 + mark 位定生死(死键条目丢弃),值走强边转发改写。
weak_map::rewrite_weak_map_native(
obj,
|key| resolve_weak_key_sweep(key, &forwarding),
|value| rewrite_forwarded_value(value, &forwarding),
);
} else if obj.is_disposable_stack_obj() || obj.is_async_disposable_stack_obj() {
disposable_stack::rewrite_dispose_native(obj, |value| rewrite_forwarded_value(value, &forwarding));
} else if obj.is_typed_array_obj() {
typed_array::rewrite_typed_array_native(obj, |value| rewrite_forwarded_value(value, &forwarding));
} else if obj.is_data_view_obj() {
data_view::rewrite_data_view_native(obj, |value| rewrite_forwarded_value(value, &forwarding));
} else if obj.is_generator_obj() {
crate::generator::rewrite_generator_native(obj, |value| rewrite_forwarded_value(value, &forwarding));
} else if obj.is_promise_obj() {
crate::promise::rewrite_promise_native(obj, |value| rewrite_forwarded_value(value, &forwarding));
// 结算链指针是裸指针边(非 JsValue):搬移换址后按转发表重定位,
// 旧克隆的结算传导仍指向后继克隆的新址。
crate::promise::repoint_promise_promoted_clone(obj, |ptr| {
rewrite_forwarded_value(JsValue::from_js_object(ptr), &forwarding).as_js_object_ptr()
});
} else if obj.is_async_obj() {
crate::async_func::rewrite_async_native(obj, |value| rewrite_forwarded_value(value, &forwarding));
} else if obj.is_async_generator_obj() {
crate::async_generator::rewrite_async_generator_native(obj, |value| {
rewrite_forwarded_value(value, &forwarding)
});
} else if obj.is_module_namespace() {
module::rewrite_module_ns_native(obj, |value| rewrite_forwarded_value(value, &forwarding));
}
}
rewrite_vm_roots(vm, &forwarding);
vm.gc_state.session_object_ptrs = forwarding.values().copied().collect();
forwarding.clear();
vm.gc_state.forwarding = forwarding;
vm.gc_state.session_epoch = new_arena;
// 对象口径重算:存活对象头 + 堆数据求和(串/BigInt/cell 分量不在此口径,
// 由各清扫路径按存活补回,与完整收集的最终账目一致)。
vm.gc_state.session_bytes_allocated = vm
.gc_state
.session_object_ptrs
.iter()
.filter(|&&ptr| !ptr.is_null())
.map(|&ptr| {
// SAFETY: 存活对象在 session 表中,sweep 运行期间有效。
let obj = unsafe { &*ptr };
size_of::<JsObject>() as u64 + Self::object_heap_data_bytes(obj)
})
.sum::<u64>() as usize;
let total_ptrs = survivors + dead;
if total_ptrs > 0 {
// 统计按对象口径累计(串/BigInt/cell 清扫在其上累加释放字节)。
self.total_bytes_freed = self.total_bytes_freed.saturating_add(freed_bytes);
self.total_objects_scanned += live + dead;
self.total_objects_live += live;
self.total_objects_dead += dead;
self.last_collection_objects_scanned = live + dead;
self.last_collection_objects_live = live;
self.last_collection_objects_dead = dead;
self.last_collection_bytes_freed = freed_bytes;
if live + dead > 0 {
if dead == 0 {
vm_debug!("[GC] sweep phase -> no objects collected ({} live, {} dead)", survivors, dead);
vm_debug!("[GC] sweep phase -> no objects collected ({} live, {} dead)", live, dead);
} else {
vm_debug!(
"[GC] sweep phase: {} scanned, {} live, {} dead, {} bytes",
total_ptrs,
survivors,
live + dead,
live,
dead,
freed_bytes
);
}
}
self.total_bytes_freed += freed_bytes;
self.total_objects_scanned += total_ptrs;
self.total_objects_live += survivors;
self.total_objects_dead += dead;
self.last_collection_objects_scanned = total_ptrs;
self.last_collection_objects_live = survivors;
self.last_collection_objects_dead = dead;
self.last_collection_bytes_freed = freed_bytes;
freed_bytes
(live, dead, freed_bytes)
}
/// 清扫 session `JsString`:保留 `mark()` 阶段记为存活的部分,其余经
@@ -884,10 +835,11 @@ impl SessionGc {
}
}
/// 完整收集编排:`mark` → `sweep`(对象移动式搬移)→ `sweep_session_strings` →
/// `sweep_session_bigints`,累加三类释放字节;字符串与 BigInt 地址稳定、无需
/// forwarding。更新各表、字节账目与 mark 位,累计收集次数与最近/最大/最小耗时,
/// 释放字节大于 0 或每满 100 次时输出统计摘要。
/// 完整收集编排:`mark` → `sweep`(对象原地清扫)→ `sweep_session_strings` →
/// `sweep_session_bigints` → `sweep_session_cells`,累加三类释放字节;
/// 对象与字符串、BigInt 地址均稳定、无需 forwarding。更新各表、字节账目与
/// mark 位,累计收集次数与最近/最大/最小耗时,释放字节大于 0 或每满 100 次
/// 时输出统计摘要。
pub(crate) fn collect(&mut self, vm: &mut Vm) {
let start = Instant::now();
@@ -922,9 +874,9 @@ impl SessionGc {
/// 仅回收 session 字符串:完整 mark(对象只置位不搬移)→ 按存活集清扫字符串。
///
/// 刻意跳过对象 sweep 搬移——builtin/dispatch 层持有跨分配点的 session 对象
/// 裸指针,移动式 sweep 会使其悬垂;对象执行期回收仍只在 reset 统一进行。
/// 字符串每串独立 Box、地址稳定,清扫无需 forwarding 与根重写。
/// 刻意跳过对象清扫——轻量档只走串表释放,对象口径的回收归完整收集与
/// 执行期收集(dispatch 安全点)。字符串每串独立 Box、地址稳定,清扫
/// 无需 forwarding 与根重写。
///
/// # 副作用
/// - 清空全部对象 mark 位;按存活集释放死串;累计 `total_collections` 与时长统计;
@@ -1006,22 +958,22 @@ impl SessionGc {
}
}
/// 执行期收集:session 原地非移动 sweep。
/// 执行期收集:session 原地 sweep(与完整收集共用清扫核心)。
///
/// 在 dispatch 安全点(循环顶 + `native_call_depth == 0`)回收单 run 分配
/// 包络内的死对象:死 session 对象原地释放(堆区 + upvalue 列表 + 本体)
/// 并出表;存活对象不移动——地址不变、免 forwarding/rewrite,用户可观察
/// identity 不分裂。
/// 并出表,弱键按 mark 位定夺;存活对象不移动——地址不变、免转发表与
/// 根重写,用户可观察 identity 不分裂。
///
/// # 边界与前提
/// - 调用方已完成门控(无活跃/挂起 for-in):门控是保守门,迭代器体
/// 是堆上 Box,收集不搬移迭代器体;
/// - 无门控:for-in 迭代器体是堆上 Box,活跃形经 VM 表、挂起形经状态盒
/// 边进入根收集,键引用被 mark 标活,原地清扫不误释放;
/// - 调用点为 dispatch 安全点(`native_call_depth == 0`),无在途 builtin
/// 局部裸指针、dispatch 未重入。
///
/// # 副作用
/// - `session_object_ptrs` 仅剩存活,`session_bytes_allocated` 按存活重算
/// (串/BigInt 归 strings-only 路径,本路径不动,仅对象口径变化);
/// - `session_object_ptrs` 仅剩存活,`session_bytes_allocated` 按存活整体
/// 重算(对象 + 串 + BigInt;本路径不扫串/BigInt,串 GC 归 strings-only);
/// - `gc_watermark` = 当前分配包络 + 阈值增量;
/// - 累计/时长统计更新;mark 位全清(收集后无残留)。
pub(crate) fn collect_in_run(&mut self, vm: &mut Vm) {
@@ -1033,44 +985,16 @@ impl SessionGc {
self.mark(vm);
// 死 session 对象原地释放(堆区 + upvalue 列表 + 本体)并出表;存活
// 对象不动——地址不变,免 forwarding/rewrite。
let session_ptrs = std::mem::take(&mut vm.gc_state.session_object_ptrs);
let mut survivors = Vec::with_capacity(session_ptrs.len());
let mut dead_session = 0u64;
let mut session_freed = 0u64;
for &ptr in &session_ptrs {
if ptr.is_null() {
continue;
}
// SAFETY: ptr 来自 session 对象表登记,arena 存活期内有效。
if unsafe { (*ptr).is_gc_marked() } {
// 原地 sweep 不搬移对象:存活对象带位保留,清位并入活分支
// (收集后无残留,残留 true 使下一次 mark DFS 短路漏标)。
unsafe { (*ptr).set_gc_mark(false) };
survivors.push(ptr);
} else {
session_freed += Self::drop_dead_session_object(ptr);
dead_session += 1;
}
}
let live_session = survivors.len() as u64;
vm.gc_state.session_object_ptrs = survivors;
// 死 session 对象原地释放(堆区 + upvalue 列表 + 本体)并出表,弱键
// 按 mark 位定夺;存活对象不动——地址不变,免转发表与根重写。对象
// 口径统计由清扫核心累计,此处只补集合与账目。
self.sweep_in_place(vm);
// 死 cell 随死对象出表:存活 cell 经 mark 种子 + 对象边入存活集,此处清扫。
self.sweep_session_cells(vm);
// 对象口径重算:存活对象 + 存活串 + BigInt(串/BigInt 本路径不动,
// 随公式整体重算保持与 strings-only 口径一致)。
let mut object_bytes: usize = 0;
for &ptr in &vm.gc_state.session_object_ptrs {
if ptr.is_null() {
continue;
}
// SAFETY: 存活对象在 session 表中,arena 存活期内有效。
let obj = unsafe { &*ptr };
object_bytes += size_of::<JsObject>() + Self::object_heap_data_bytes(obj) as usize;
}
// 整体口径重算:对象分量已由清扫核心写入,此处补串与 BigInt 分量
// (本路径不扫串/BigInt,按当前表整体计入,与 strings-only 口径一致)。
let mut string_bytes: usize = 0;
for &ptr in &vm.gc_state.session_string_ptrs {
if ptr.is_null() {
@@ -1080,30 +1004,18 @@ impl SessionGc {
string_bytes += size_of::<JsString>() + unsafe { (*ptr).payload_bytes() };
}
let bigint_bytes = vm.gc_state.session_bigint_ptrs.borrow().len() * size_of::<num_bigint::BigInt>();
vm.gc_state.session_bytes_allocated = object_bytes + string_bytes + bigint_bytes;
vm.gc_state.session_bytes_allocated =
vm.gc_state.session_bytes_allocated.saturating_add(string_bytes).saturating_add(bigint_bytes);
// 抬高下次触发水位:当前分配包络 + 阈值增量——存活包络超阈值时不每指令
// 重复触发无死对象可回收的白跑。
vm.gc_state.gc_watermark = vm.run_alloc_bytes().saturating_add(vm.gc_state.gc_threshold_cached);
vm.gc_state.gc_gate_retry_alloc = 0;
let live = live_session;
let dead = dead_session;
let freed_bytes = session_freed;
let elapsed = start.elapsed();
self.total_collections += 1;
self.last_collection_duration_us = elapsed.as_micros() as u64;
self.max_collection_duration_us = self.max_collection_duration_us.max(self.last_collection_duration_us);
self.min_collection_duration_us = self.min_collection_duration_us.min(self.last_collection_duration_us);
self.total_bytes_freed = self.total_bytes_freed.saturating_add(freed_bytes);
self.total_objects_scanned += session_ptrs.len() as u64;
self.total_objects_live += live;
self.total_objects_dead += dead;
self.last_collection_objects_scanned = session_ptrs.len() as u64;
self.last_collection_objects_live = live;
self.last_collection_objects_dead = dead;
self.last_collection_bytes_freed = freed_bytes;
vm_info!(
"[GC] in-run cycle #{} end: {} scanned, {} live, {} dead, {:.1}ms, {} bytes freed",
@@ -1112,7 +1024,7 @@ impl SessionGc {
self.last_collection_objects_live,
self.last_collection_objects_dead,
elapsed.as_secs_f64() * 1000.0,
freed_bytes,
self.last_collection_bytes_freed,
);
}
@@ -1165,56 +1077,38 @@ fn rewrite_forwarded_value(
.unwrap_or(value)
}
/// 移动式 sweep 改写期的弱键判定:转发表内键改指新址;未入表的 session/
/// epoch 键按 mark 位定生死(已标 = 强可达、随后晋升或原地存活,未标 =
/// 死键丢弃);P 键不可死,恒保留。
/// 原地 sweep 的弱键判定:session 键按 mark 位定生死(已标 = 强可达保留,
/// 未标 = 死键丢弃),非 session 键(perm 对象等)不可死、恒保留;symbol
/// 键按值恒等、恒保留。
///
/// # 边界与前提
/// - 未入表键读归属位与 mark 位须解引用旧指针:调用点(sweep 改写相)
/// 旧 arena 尚未归还、清位发生在改写相之后,对象头位域可读。
/// - 死键指针只作哈希键与位域读取,不 deref 其已释放的堆数据。
pub(crate) fn resolve_weak_key_sweep(
key: weak_map::WeakKey, forwarding: &HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>,
) -> Option<weak_map::WeakKey> {
/// - 时序前提:定夺须先于死对象本体释放执行——判定要读键对象本体的 mark
/// 位域,调用点(sweep 弱键定夺相)位于死对象释放相之前,全部键对象(含
/// 死键)此刻仍分配,位域读取安全;死键指针此后只作被丢弃的表项内容,
/// 不再被解引用。
/// - 死键指针只读对象头位域,不解引用其堆数据(属性区等随释放销毁)。
pub(crate) fn resolve_weak_key_sweep(key: weak_map::WeakKey) -> Option<weak_map::WeakKey> {
let weak_map::WeakKey::Obj(ptr) = key else {
return Some(key);
};
let mut_ptr = ptr as *mut JsObject;
if let Some(&new) = forwarding.get(&mut_ptr) {
return Some(weak_map::WeakKey::Obj(new as *const JsObject));
}
// SAFETY: 见函数边界与前提——sweep 改写相旧 arena 存活,对象头位域可读。
// SAFETY: 见时序前提——定夺相键对象仍分配,对象头位域可读。
let old = unsafe { &*mut_ptr };
if (old.is_session_epoch() || old.is_epoch()) && !old.is_gc_marked() {
if old.is_session_epoch() && !old.is_gc_marked() {
return None;
}
Some(key)
}
/// 晋升收敛后的弱键定夺:转发表内键改指新址;未入表 epoch 键 = 本轮晋升
/// 未覆盖 = 死键丢弃;session 键不搬移、原样保留(惰性判定交下一轮 sweep);
/// P 键不可死,恒保留。
/// 晋升收敛后的弱键定夺:统一入口 Box 化后晋升族在生产路径不产克隆
/// (epoch 守卫恒假、无 epoch 对象),转发表恒空、无重指需求;reset 边界
/// 的 mark 位已被前序收集清位,不携带本轮可达性信息。故定夺退化为原样
/// 保留——弱键生死由原地 sweep 的 mark 位判定单一裁定,本路径不改表项。
///
/// # 边界与前提
/// - 晋升定夺路径(原地晋升 / in-run 晋升档)的转发表已收敛为最终强可达集,
/// 判据不含 mark 位(reset 边界路径此前已清位)。
/// - 解引用仅限未入表键的归属位读取,调用点旧 arena 存活(epoch 释放 /
/// 清表发生在定夺之后)。
pub(crate) fn resolve_weak_key_after_promotion(
key: weak_map::WeakKey, forwarding: &HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>,
) -> Option<weak_map::WeakKey> {
let weak_map::WeakKey::Obj(ptr) = key else {
return Some(key);
};
let mut_ptr = ptr as *mut JsObject;
if let Some(&new) = forwarding.get(&mut_ptr) {
return Some(weak_map::WeakKey::Obj(new as *const JsObject));
}
// SAFETY: 见函数边界与前提——定夺点旧 arena 存活,对象头位域可读。
let old = unsafe { &*mut_ptr };
if old.is_epoch() {
return None;
}
/// - 不解引用任何对象本体(无位域读取),无定夺时序面;
/// - 晋升族整体为死代码保留面,弱表族本体退役时一并删除。
pub(crate) fn resolve_weak_key_after_promotion(key: weak_map::WeakKey) -> Option<weak_map::WeakKey> {
Some(key)
}
+58 -78
View File
@@ -224,7 +224,8 @@ fn sweep_preserves_cycle_and_collects_unreachable() {
vm.gc_state.session_gc = gc;
assert_eq!(vm.gc_state.session_object_ptrs.len(), 2);
assert!(!vm.gc_state.session_object_ptrs.contains(&root_session));
// 原地 sweep 不搬移存活对象:根与子保持原址(克隆体即原对象),死对象出表。
assert!(vm.gc_state.session_object_ptrs.contains(&root_session));
assert!(!vm.gc_state.session_object_ptrs.contains(&dead_session));
assert!(!vm
.gc_state
@@ -262,39 +263,19 @@ fn sweep_preserves_array_elements_and_collects_dead_element_object() {
let _ = gc.sweep(&mut vm);
vm.gc_state.session_gc = gc;
// sweep 复制存活对象并把 VM 根改写为新 arena 指针:regs[0] 是数组的新地址,
// 旧指针已随旧 arena 释放,不可再用。
let arr_new = vm.regs[0].as_js_object_ptr();
let live_session = unsafe { (*arr_new).get_prop_at(0).as_js_object_ptr() };
// 原地 sweep 不搬移存活对象:数组保持原址、根指针不变,死元素对象
// 原地释放并出表,存活集合只剩数组与元素 0 的对象。
let arr_addr = vm.regs[0].as_js_object_ptr();
assert_eq!(arr_addr, arr_session, "存活数组原地保留,根指针不变");
let live_session = unsafe { (*arr_addr).get_prop_at(0).as_js_object_ptr() };
assert_eq!(unsafe { (*live_session).prop_count() }, 0);
assert_eq!(unsafe { (*arr_new).prop_count() }, 2);
assert_eq!(unsafe { (*arr_new).get_prop_at(0).as_js_object_ptr() }, live_session);
// 元素 1 引用已断开:死元素对象被回收,存活集合只剩数组与元素 0 的对象。
assert_eq!(unsafe { (*arr_new).get_prop_at(1) }, JsValue::undefined());
assert_eq!(unsafe { (*arr_addr).prop_count() }, 2);
assert_eq!(unsafe { (*arr_addr).get_prop_at(0).as_js_object_ptr() }, live_session);
// 元素 1 引用已断开:死元素对象被回收。
assert_eq!(unsafe { (*arr_addr).get_prop_at(1) }, JsValue::undefined());
assert_eq!(vm.gc_state.session_object_ptrs.len(), 2);
}
#[test]
fn forwarding_is_cleared_after_sweep() {
let mut vm = Vm::new();
let root = plain_object(&mut vm);
let child = plain_object(&mut vm);
unsafe {
(*root).set_prop_at(0, JsValue::from_js_object(child));
}
let root_session = vm.promote_object(root);
vm.regs[0] = JsValue::from_js_object(root_session);
let _ = vm.promote_object(child);
let mut gc = std::mem::take(&mut vm.gc_state.session_gc);
gc.mark(&vm);
let _ = gc.sweep(&mut vm);
vm.gc_state.session_gc = gc;
// 复用的 forwarding 表必须在 sweep 后清空,否则 promote 会观察到指向已释放
// arena 的过期 old->new 条目。
assert!(vm.gc_state.forwarding.is_empty());
}
fn vm_with_low_threshold() -> Vm {
let mut cfg = KernelConfig::minimal();
cfg.set_session_gc_threshold(1);
@@ -402,7 +383,7 @@ fn gc_stats_summary_includes_collection() {
}
#[test]
fn moving_sweep_rewrites_global_root_edges() {
fn full_collect_keeps_global_root_edges_in_place() {
let mut vm = vm_with_low_threshold();
let obj = plain_object(&mut vm);
unsafe {
@@ -426,7 +407,10 @@ fn moving_sweep_rewrites_global_root_edges() {
.expect("global slot");
let new_value = global.get_prop_at(pos);
assert!(new_value.is_object());
assert!(!std::ptr::eq(new_value.as_js_object_ptr(), old_ptr));
assert!(
std::ptr::eq(new_value.as_js_object_ptr(), old_ptr),
"原地 sweep 不搬移对象:global 根边原址不变"
);
assert_eq!(unsafe { (*new_value.as_js_object_ptr()).get_prop_at(0) }, JsValue::int(42));
}
@@ -508,8 +492,8 @@ fn weak_map_object(vm: &mut Vm) -> *mut JsObject {
vm.alloc_object(obj)
}
/// 弱键不产 mark 边:强可达键跨完整收集保留,条目完整、键与值边改写
/// 到搬移后新址,经改写后的根读回同一克隆体。
/// 弱键不产 mark 边:强可达键跨完整收集保留,条目完整、键与值边原地不变,
/// 经原键读回同一对象。
#[test]
fn weak_map_entry_survives_promotion_with_live_key() {
let mut vm = vm_with_low_threshold();
@@ -540,13 +524,13 @@ fn weak_map_entry_survives_promotion_with_live_key() {
let live_wm = unsafe { &*vm.regs[0].as_js_object_ptr() };
assert_eq!(oxide_builtins::weak_map::weak_map_entry_count(live_wm), 1, "强可达键的条目须在收集后存活");
// 键经移动式 sweep 搬移,条目键改指克隆体:按改写后的根读回。
// 原地 sweep 不搬移对象:键与值边原地保留,按原键读回同一对象。
let stored = oxide_builtins::weak_map::weak_map_probe_get(live_wm, vm.regs[1]);
assert_eq!(stored, vm.regs[2], "值须按改写后的键读回同一克隆体");
assert!(!std::ptr::eq(stored.as_js_object_ptr(), value), "值边须改写到搬移后新址");
assert_eq!(stored, vm.regs[2], "值须按原键读回同一对象");
assert!(std::ptr::eq(stored.as_js_object_ptr(), value), "值边原地不变");
}
/// 弱键强不可达 = 死键:收集按转发表判定丢条目,表不留死键残影。
/// 弱键强不可达 = 死键:收集按 mark 位定夺丢条目,表不留死键残影。
#[test]
fn weak_map_entry_dropped_when_key_unrooted() {
let mut vm = vm_with_low_threshold();
@@ -575,8 +559,7 @@ fn weak_map_entry_dropped_when_key_unrooted() {
assert_eq!(oxide_builtins::weak_map::weak_map_entry_count(live_wm), 0, "死键条目须在收集时丢弃");
}
/// 值边为强边:值对象唯一引用仅来自值边时仍入存活集,搬移后条目按转发表
/// 改写指到新址。
/// 值边为强边:值对象唯一引用仅来自值边时仍入存活集,原地保留原址。
#[test]
fn weak_map_value_edge_keeps_value_alive() {
let mut vm = vm_with_low_threshold();
@@ -605,19 +588,18 @@ fn weak_map_value_edge_keeps_value_alive() {
vm.gc_state.session_gc = gc;
let live_wm = unsafe { &*vm.regs[0].as_js_object_ptr() };
// 键经移动式 sweep 搬移,条目键改指克隆体:按改写后的根读回。
// 原地 sweep 不搬移对象:键与值边原地保留,按原键读回同一对象。
let stored = oxide_builtins::weak_map::weak_map_probe_get(live_wm, vm.regs[1]);
assert!(stored.is_object(), "唯一经值边引用的值须存活");
assert!(!std::ptr::eq(stored.as_js_object_ptr(), value_session), "值边须在搬移后改写到新址");
assert!(std::ptr::eq(stored.as_js_object_ptr(), value_session), "值边原地不变");
assert!(vm.is_session_ptr(stored.as_js_object_ptr()));
// key、value、wm 三对象均存活(key/value 经寄存器根、wm 经寄存器根),
// 搬移后各占一份克隆。
// 原地保留原址。
assert_eq!(vm.gc_state.session_object_ptrs.len(), 3);
}
/// in-run 原地 sweep·死键面:无强根的 session 键在原地 sweep 判死出表;
/// P 键不可死,条目保留且按原键读回。原地 sweep 不动弱表条目表,死键条目
/// 的定夺交后续单 mark 位判据,此处只钉死键对象出表 + P 键条目保留。
/// in-run 原地 sweep·死键面:无强根的 session 键在原地 sweep 按 mark 位判死,
/// 死键条目丢弃、死键对象释放出表;P 键不可死,条目保留且按原键读回。
#[test]
fn weak_map_dead_key_freed_by_in_run_sweep() {
let mut vm = vm_with_threshold(65536);
@@ -642,6 +624,11 @@ fn weak_map_dead_key_freed_by_in_run_sweep() {
assert_eq!(vm.session_gc_stats().total_collections, 1, "in-run 收集应跑一轮");
assert!(!vm.gc_state.session_object_ptrs.contains(&key), "死键对象须出表");
let live_wm = unsafe { &*vm.regs[0].as_js_object_ptr() };
assert_eq!(
oxide_builtins::weak_map::weak_map_entry_count(live_wm),
1,
"死键条目须丢弃,仅 P 键条目保留"
);
assert_eq!(
oxide_builtins::weak_map::weak_map_probe_get(live_wm, p_key),
JsValue::int(7),
@@ -1122,8 +1109,8 @@ fn global_prop_opt(vm: &Vm, name: &str) -> Option<JsValue> {
}
/// 闭包捕获变量跨对象 sweep 存活:reset 触发完整收集(session_epoch 替换)。
/// cell 独立堆分配、不入 session arena,arena 整体回收时不连带释放——否则跨
/// arena 存活的 cell 指针悬垂;sweep 只重写 cell.value 中的对象引用,值跨搬移保留。
/// cell 独立堆分配、地址稳定,原地 sweep 不搬移对象、不触碰 cell 结构——
/// 跨收集的 cell 指针与值均保留。
#[test]
fn closure_cell_survives_object_sweep() {
let mut vm = vm_with_threshold(1);
@@ -1134,11 +1121,11 @@ fn closure_cell_survives_object_sweep() {
.expect("run1");
assert!(!vm.gc_state.session_cell_ptrs.borrow().is_empty(), "run1 应分配 upvalue cell");
// reset 触发对象 sweep:存活对象搬到新 arena,旧 arena 释放。
// reset 触发对象 sweep:存活对象原地保留,死对象原地释放。
vm.reset();
assert!(vm.session_gc_stats().total_collections > 0, "reset 应触发对象收集");
// 跨搬移后从 global 重新取 inc 函数对象:upvalue cell 指针稳定、值保留。
// 收集后从 global 重新取 inc 函数对象:upvalue cell 指针稳定、值保留。
let inc_val = global_prop_opt(&vm, "inc").expect("inc 应挂在 global 上");
let obj = unsafe { &*inc_val.as_js_object_ptr() };
let cells = obj.upvalues_slice();
@@ -1149,8 +1136,8 @@ fn closure_cell_survives_object_sweep() {
}
/// 私有字段类的 brand cell 跨对象 sweep 存活:`@@class_brand` upvalue cell
/// 存类 brand 对象,sweep 后其值经 forwarding 重写为搬移后的新对象地址。
/// cell 独立堆分配,指针在周边 arena 回收后仍稳定,值不被内存复用覆盖。
/// 存类 brand 对象,原地 sweep 后其值为原 brand 对象(地址不变)。cell 独立
/// 堆分配、地址稳定,原地 sweep 不触碰 cell 结构体。
#[test]
fn private_brand_cell_survives_object_sweep() {
let mut vm = vm_with_threshold(1);
@@ -1346,8 +1333,8 @@ impl Drop for NsCleanup {
}
/// 模块 ns cell 根:依赖模块可重赋导出挂共享 cell(条目表 `Cell` 面),ns 对象
/// 经 global 可达;清寄存器仅留 ns 根后完整收集,cell 跨移动式 sweep 存活(深拷
/// 表保留同一 cell 指针),`module_ns_export` 读回正确。
/// 经 global 可达;清寄存器仅留 ns 根后完整收集,cell 跨原地 sweep 存活
/// (ns 对象与条目表原地不变),`module_ns_export` 读回正确。
#[test]
fn module_ns_cell_root_survives_collect() {
let cwd = std::env::current_dir().expect("cwd");
@@ -1385,10 +1372,10 @@ fn module_ns_cell_root_survives_collect() {
"模块 ns cell 应跨完整收集存活"
);
// 移动式 sweep 后 ns 晋升 session(原件出表),从 global 重取克隆读回。
// 原地 sweep 不搬移对象:ns 与条目表原地保留,从 global 重取读回。
let ns_val2 = global_prop_opt(&vm, "__ns").expect("__ns 应挂在 global 上");
let ns_obj2 = unsafe { &*ns_val2.as_js_object_ptr() };
assert!(module::module_ns_cell_edges(ns_obj2).contains(&cell_ptr), "克隆应共享同一 cell 指针");
assert!(module::module_ns_cell_edges(ns_obj2).contains(&cell_ptr), "ns 条目表应保留同一 cell 指针");
let name_si = vm.kernel_core().perm_interner().intern("x").0;
let query = module::module_ns_export(ns_obj2, name_si).expect("x 应可导出");
match query {
@@ -1705,12 +1692,14 @@ fn in_run_collection_reclaims_dead_and_keeps_live() {
assert_eq!(a, Some(JsValue::int(1)));
}
/// for-in 门控双形钉住:活跃形(迭代器在 `vm.iters` 表内)与挂起形
/// (生成器在 for-in 内 yield,迭代器搬入状态盒)都须拦下执行期收集——
/// 门控是保守门,迭代器体为堆上 Box;门控开后方可收集。
/// 无门控 for-in 双形钉住:活跃形(迭代器在 `vm.iters` 表内)与挂起形
/// (生成器在 for-in 内 yield,迭代器搬入状态盒)存在时执行期收集照常执行
/// ——迭代器键引用经根收集标活、原地清扫不搬移对象,无门控亦不误释放,
/// yield 值跨收集保持正确。
#[test]
fn active_and_suspended_for_in_block_in_run_collection() {
// 活跃形:迭代器直接压在 vm.iters,O(1) 子句即拦。
fn active_and_suspended_for_in_do_not_block_in_run_collection() {
// 活跃形:迭代器直接压在 vm.iters,收集照常执行;死对象原地释放,
// 迭代器键经根收集标活。
let mut vm = vm_with_threshold(65536);
let dead = plain_object(&mut vm);
let iter = Box::into_raw(Box::new(crate::vm::ForInIter {
@@ -1720,17 +1709,11 @@ fn active_and_suspended_for_in_block_in_run_collection() {
vm.iters.push_for_in(iter);
vm.maybe_collect_in_run();
assert_eq!(vm.session_gc_stats().total_collections, 0, "活跃 for-in:门控关闭,不收集");
assert!(vm.gc_state.session_object_ptrs.contains(&dead), "死对象仍在 session 表");
// 迭代器出表后同一调用点即应收集。
vm.iters.pop_for_in();
vm.maybe_collect_in_run();
assert_eq!(vm.session_gc_stats().total_collections, 1, "门控开:应收集");
assert_eq!(vm.session_gc_stats().total_collections, 1, "无门控:活跃 for-in 存在时收集照常执行");
assert!(!vm.gc_state.session_object_ptrs.contains(&dead), "死 session 对象应被原地释放并出表");
// 挂起形(拦截):生成器在 for-in 内 yield 后 run 结束,迭代器经
// 状态盒持有(vm.iters 已空)——收集点必须拦下。
// 挂起形:生成器在 for-in 内 yield 后 run 结束,迭代器经状态盒持有
// (vm.iters 已空)——键经状态盒边入根收集标活,收集照常执行。
let mut vm = vm_with_threshold(65536);
vm.run(&Arc::new(compile(
"var o = { a: 1, b: 2 }; \
@@ -1739,10 +1722,9 @@ fn active_and_suspended_for_in_block_in_run_collection() {
)))
.expect("run1");
assert!(vm.iters.for_in_iters.is_empty(), "挂起时迭代器已搬入状态盒");
assert!(vm.suspended_holds_for_in(), "生成器状态盒应持挂起 for-in 迭代器");
vm.maybe_collect_in_run();
assert_eq!(vm.session_gc_stats().total_collections, 0, "挂起 for-in:门控关闭,不收集");
assert_eq!(vm.session_gc_stats().total_collections, 1, "挂起 for-in:收集照常执行");
// 挂起生成器对象保持原址(未释放):状态盒与迭代器一体存活。
let gen_in_session = vm
.gc_state
@@ -1751,9 +1733,8 @@ fn active_and_suspended_for_in_block_in_run_collection() {
.any(|&p| !p.is_null() && unsafe { (*p).type_tag == JsObject::OBJ_TYPE_GENERATOR });
assert!(gen_in_session, "挂起生成器应仍在 session 表");
// 挂起形(放行):同一 run 内续跑至 for-in 结束(迭代器释放),
// 阈值 1 使指令边界自动触发:挂起期门控关(Bump 不换新),完成后
// 门控开、执行期收集放行;yield 值跨收集保持正确。
// 同一 run 内续跑至 for-in 结束(迭代器释放),阈值 1 使指令边界自动触发
// 收集:挂起期与完成期收集均照常放行,yield 值跨收集保持正确。
let mut vm = vm_with_threshold(1);
vm.run(&Arc::new(compile(
"var o = { a: 1, b: 2 }; \
@@ -1764,10 +1745,9 @@ fn active_and_suspended_for_in_block_in_run_collection() {
.expect("run2");
assert_eq!(vm.lookup_str(global_prop_opt(&vm, "v1").expect("v1")), Some("a".to_string()));
assert_eq!(vm.lookup_str(global_prop_opt(&vm, "v2").expect("v2")), Some("b".to_string()));
assert!(!vm.suspended_holds_for_in(), "for-in 结束后状态盒不应再持迭代器");
assert!(vm.session_gc_stats().total_collections >= 1, "完成后门控开:执行期收集应放行");
assert!(vm.session_gc_stats().total_collections >= 1, "执行期收集应已放行");
// 生成器对象跨收集仍为合法生成器(晋升 session 或保持 epoch 完好)。
// 生成器对象跨收集仍为合法生成器(session 对象原地保留)。
let g = global_prop_opt(&vm, "g").expect("g 应挂在 global");
assert!(unsafe { (*g.as_js_object_ptr()).is_generator_obj() }, "收集后生成器对象应完好");
}
+4 -3
View File
@@ -123,7 +123,7 @@ impl Vm {
// 返回顶层后检查恢复,存活值此时已回拷为执行根。账目未超水位时仅
// 少量字段比较。
if self.native_call_depth == 0 {
// 宿主 `$262.gc()` 置位的强制收集在此边界消费:移动式 sweep
// 宿主 `$262.gc()` 置位的强制收集在此边界消费:完整收集
// 唯一安全点(无在途 dispatch 局部值),与正常触发路径同一入口。
if self.gc_state.pending_forced_collect {
self.gc_state.pending_forced_collect = false;
@@ -142,8 +142,9 @@ impl Vm {
if bytes >= self.gc_state.string_gc_watermark {
self.maybe_collect_session_strings();
}
// 执行期两档收集:O(1) 包络超触发水位 → 门控(活跃/挂起 for-in)
// + epoch 晋升收集 + session 原地 sweep(边界契约见 maybe_collect_in_run)。
// 执行期原地 sweep 收集:O(1) 包络超触发水位 → 收集(无门控:
// 迭代器键引用经根收集标活,原地释放不搬移存活对象;
// 边界契约见 maybe_collect_in_run)。
if alloc >= self.gc_state.gc_watermark {
self.maybe_collect_in_run();
}
+8 -51
View File
@@ -239,60 +239,17 @@ impl Vm {
self.gc_state.session_gc = session_gc;
}
/// 门控子句二:三型状态盒(生成器/异步函数/异步生成器)的
/// `suspended.for_in_iters` 是否非空。
///
/// 门控是保守门:for-in 迭代器活跃/挂起期间拦下执行期收集;
/// 挂起帧把迭代器搬入状态盒(`vm.iters` 不可见),故须逐状态盒扫描。
///
/// # 边界与前提
/// - 扫描 session 对象表——挂起状态盒宿主对象在表内;
/// - 挂起帧的 keys 经状态盒边收为 GC 根(被枚举对象不误释放),迭代器
/// 体是堆上 Box,故按指针扫 `for_in_iters`;
/// - 保守口径:死对象的状态盒同样计入(持挂起 for-in 的 run 放弃执行期
/// 收集),正确性优先于收益;
/// - 新增挂起态持有者须在此登记。
pub(crate) fn suspended_holds_for_in(&self) -> bool {
let holds = |&ptr: &*mut JsObject| {
if ptr.is_null() {
return false;
}
// SAFETY: 对象表登记的指针,存活期内有效。
let obj = unsafe { &*ptr };
match obj.type_tag {
JsObject::OBJ_TYPE_GENERATOR => crate::generator::generator_holds_suspended_for_in(obj),
JsObject::OBJ_TYPE_ASYNC => crate::async_func::async_holds_suspended_for_in(obj),
JsObject::OBJ_TYPE_ASYNC_GENERATOR => {
crate::async_generator::async_generator_holds_suspended_for_in(obj)
}
_ => false,
}
};
self.gc_state.session_object_ptrs.iter().any(holds)
}
/// 执行期两档收集的 dispatch 安全点入口:仅在循环顶
/// 执行期原地 sweep 收集的 dispatch 安全点入口:仅在循环顶
/// (`native_call_depth == 0`,无 builtin 局部裸指针、dispatch 未重入)
/// 由触发块调用;先门控后收集。
/// 由触发块在水位命中后调用,无门控直接收集。
///
/// # 边界与前提
/// - 子句一(活跃 for-in,O(1))未过:免费重试,不设锚;
/// - 子句二(状态盒扫描,O(对象数))未过:重扫按包络增长一个阈值设锚,
/// 避免持挂起 for-in 的 run 每指令边界重扫;
/// - 门控过但无对象可回收时仍跑一轮(mark + 换新 Bump),水位同点抬高,
/// - for-in 迭代器体是堆上 Box:活跃形经 VM 表、挂起形经状态盒边进入
/// 根收集,键引用被 mark 标活,原地清扫不搬移对象、不误释放,
/// 无需拦截;
/// - 无对象可回收时仍跑一轮(mark + 原地清扫),水位同点抬高,
/// 触发间距由包络增量控制。
pub(crate) fn maybe_collect_in_run(&mut self) {
if !self.iters.for_in_iters.is_empty() {
return;
}
if self.gc_state.gc_gate_retry_alloc > self.run_alloc_bytes() {
return;
}
if self.suspended_holds_for_in() {
self.gc_state.gc_gate_retry_alloc =
self.run_alloc_bytes().saturating_add(self.gc_state.gc_threshold_cached);
return;
}
let mut session_gc = std::mem::take(&mut self.gc_state.session_gc);
session_gc.collect_in_run(self);
self.gc_state.session_gc = session_gc;
@@ -373,10 +330,10 @@ impl Vm {
bytes
}
/// 无条件执行一次完整 session GC(mark + 移动式 sweep + 串/BigInt 清扫)。
/// 无条件执行一次完整 session GC(mark + 对象原地清扫 + 串/BigInt 清扫)。
///
/// # 副作用
/// - 存活对象复制进新 session arena,全部根与原生盒按转发表重写;
/// - 存活对象地址不变、根与原生盒免改写;
/// `session_bytes_allocated` 重置为清扫后的存活字节。
///
/// # 注意事项
+10 -15
View File
@@ -11,8 +11,6 @@
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use rustc_hash::FxBuildHasher;
use crate::session_gc::SessionGc;
use crate::vm::ForInIter;
use oxide_builtins::iterator::BuiltinIterKind;
@@ -35,9 +33,9 @@ pub(crate) struct GcState {
/// mark/sweep 回收(值量少),只在 full_reset 统一释放。
pub(crate) session_bigint_ptrs: RefCell<Vec<*mut num_bigint::BigInt>>,
/// upvalue cell(`Box<Cell>`)追踪表。`RefCell` 使 `&self` 的分配入口也能
/// 登记新 box。cell 独立堆分配、地址稳定,不参与对象搬移(sweep 只重写
/// `cell.value` 中的对象引用);参与 mark-sweep 回收(死 cell 随收集释放),
/// full_reset 为收尾兜底(表已空时 no-op)。
/// 登记新 box。cell 独立堆分配、地址稳定,不参与对象搬移(原地 sweep
/// 不触碰 cell,也不重写 `cell.value`);参与 mark-sweep 回收(死 cell
/// 随收集释放),full_reset 为收尾兜底(表已空时 no-op)。
pub(crate) session_cell_ptrs: RefCell<Vec<*mut UpvalueCell>>,
pub(crate) session_bytes_allocated: usize,
/// 执行期 session 堆账目的峰值高水位(`session_bytes_allocated` 的采样上界),
@@ -52,24 +50,21 @@ pub(crate) struct GcState {
pub(crate) string_gc_watermark: usize,
/// 缓存的 GC 阈值(从 config 读一次),热路径只做 usize 比较,免 Arc 解引用。
pub(crate) gc_threshold_cached: usize,
/// 执行期两档收集(epoch 晋升 + session 原地 sweep)的触发水位:本次
/// 收集后的分配包络 + 阈值增量。仅当 `run_alloc_bytes` 超过水位才在指令
/// 边界触发——存活包络超阈值时不每指令重复触发无死对象可回收的白跑。
/// 执行期原地 sweep 收集的触发水位:本次收集后的分配包络 + 阈值增量。
/// 仅当 `run_alloc_bytes` 超过水位才在指令边界触发——存活包络超阈值时
/// 不每指令重复触发无死对象可回收的白跑。
pub(crate) gc_watermark: usize,
/// 门控未过(挂起态持有 for-in)后的重扫锚:包络再增长一个阈值才重扫
/// 三型状态盒(O(对象数) 扫描),避免持挂起 for-in 的 run 每指令边界重扫。
pub(crate) gc_gate_retry_alloc: usize,
/// 宿主强制收集旗标(`$262.gc()`):native 重入中置位,由下一个顶层
/// 指令边界(移动式 sweep 的唯一安全点)消费并执行完整收集。
/// 指令边界(完整收集的唯一安全点:无在途 builtin 局部裸指针)消费。
pub(crate) pending_forced_collect: bool,
pub(crate) forwarding: HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>,
}
impl GcState {
/// 分配一个 upvalue cell:独立堆分配并返回裸指针,脱离 session arena 生命周期。
///
/// cell 指针登记进 `session_cell_ptrs`,在 `full_reset` 统一释放。对象 sweep
/// 搬移不触碰 cell 结构体(地址稳定),只重写 `cell.value` 中的对象引用。
/// cell 指针登记进 `session_cell_ptrs`,在 `full_reset` 统一释放。对象
/// sweep 原地化不搬移对象、不触碰 cell 结构体(地址稳定),cell 的
/// 生死由 mark-sweep 自行裁定(死 cell 随收集释放)。
/// `&self` 使调用方可与 `cell_stack` 等其它字段的借用并存(分字段借用)。
pub(crate) fn alloc_cell(&self, value: JsValue, initialized: bool) -> *mut UpvalueCell {
let ptr = Box::into_raw(Box::new(UpvalueCell::new(value, initialized)));
+18 -46
View File
@@ -134,9 +134,7 @@ impl Vm {
string_gc_watermark: gc_threshold,
gc_threshold_cached: gc_threshold,
gc_watermark: gc_threshold,
gc_gate_retry_alloc: 0,
pending_forced_collect: false,
forwarding: std::collections::HashMap::with_hasher(rustc_hash::FxBuildHasher),
},
symbols: SymbolState {
symbol_counter: 0,
@@ -271,9 +269,7 @@ impl Vm {
string_gc_watermark: gc_threshold,
gc_threshold_cached: gc_threshold,
gc_watermark: gc_threshold,
gc_gate_retry_alloc: 0,
pending_forced_collect: false,
forwarding: std::collections::HashMap::with_hasher(rustc_hash::FxBuildHasher),
},
symbols: SymbolState {
symbol_counter: 0,
@@ -408,65 +404,42 @@ impl Vm {
self.gc_state.session_bytes_peak = 0;
self.gc_state.run_alloc_peak = 0;
self.gc_state.string_gc_watermark = self.kernel_core.config().session_gc_threshold;
// 两档收集水位与门控重扫锚同点复位(同式:阈值增量起算)。
// 执行期收集水位同点复位(同式:阈值增量起算):旧 run 的存活包络
// 不延续到新 run 的触发判定。
self.gc_state.gc_watermark = self.gc_state.gc_threshold_cached;
self.gc_state.gc_gate_retry_alloc = 0;
self.gc_state.session_gc = crate::session_gc::SessionGc::new();
self.symbols.reset();
self.root_reg_limit = 0;
self.active_reg_limit = 0;
}
/// 释放全部 session 堆数据:session 对象堆数据 + upvalue 列表 + 对象本体
/// 释放全部 session 堆数据:session 对象(本体 + 堆数据 + upvalue 列表)
/// + session 串 + BigInt box + upvalue cell box。
///
/// 供 `full_reset` 与 `Drop` 共用——堆载体对象本体(HEAP_BIT)经
/// `Box::from_raw` 释放,arena 载体(克隆)随调用方换新的 session Bump 归还,
/// 本函数不释放。原生盒在 GC 搬移/晋升时已深拷贝为单所有权,此处恰好释放一次。
/// 供 `full_reset` 与 `Drop` 共用——统一入口路径上对象本体全部为堆载体
/// (`Box::from_raw` 恰好释放一次;晋升族 Bump 克隆仅测试形态,本体随
/// 换新 Bump 归还),堆数据与 upvalue 列表各恰好释放一次。独占所有权
/// 免去重:死对象已出表不重复枚举,表内无共享 upvalue Box 的原件-克隆对
/// (死对象从不被克隆,生产路径不产克隆)。
pub(crate) fn teardown_session_heap_data(&mut self) {
// 迭代器体是堆上 Box:收尾路径(full_reset 与 Drop 共用)逐条释放,
// 防 Vm 直接 drop 时表内残留体泄漏。
self.iters.reset();
// upvalue 列表先于对象表清空释放:去重枚举依赖对象表尚存。
self.free_session_upvalues();
// 对象逐条独占释放:本体 + 堆数据 + upvalue 列表,顺序由
// drop_dead_session_object 收口。
for ptr in self.gc_state.session_object_ptrs.drain(..) {
crate::session_gc::SessionGc::drop_object_heap_data(ptr, true);
// 释放对象本体:堆载体(HEAP_BIT)经 Box::from_raw 释放,arena
// 载体(克隆)随换新 Bump 归还,不在此释放。
if !ptr.is_null() && unsafe { (*ptr).is_heap_alloc() } {
unsafe {
drop(Box::from_raw(ptr));
}
if ptr.is_null() {
continue;
}
// SAFETY: ptr 来自 session 对象表登记,收尾时仍指向合法对象;
// 表独占持有,无其他释放点。
crate::session_gc::SessionGc::drop_dead_session_object(ptr);
}
self.free_session_string_heap_data();
self.free_session_bigint_heap_data();
self.gc_state.free_cells();
}
/// 集中释放 upvalue 列表(`Box<Vec<*mut Cell>>`):原件与晋升克隆经
/// `clone_for_session_epoch` 共享同一 Box 分配,逐对象路径释放会双放,
/// 只在此处按指针去重后统一释放。须在对象表清空之前调用
/// (枚举仍存对象完成去重),此时对象已死、Box 无其他读者。
fn free_session_upvalues(&mut self) {
let mut seen = std::collections::HashSet::new();
for &ptr in self.gc_state.session_object_ptrs.iter() {
if ptr.is_null() {
continue;
}
// SAFETY: ptr 来自 session 对象表登记,收尾时仍指向合法对象。
let up = unsafe { (*ptr).upvalues };
if up.is_null() || !seen.insert(up) {
continue;
}
// SAFETY: up 由 set_upvalues 的 Box::into_raw 分配,去重保证恰好释放一次。
unsafe {
drop(Box::from_raw(up as *mut Vec<*mut oxide_types::object::Cell>));
(*ptr).upvalues = std::ptr::null_mut();
}
}
}
/// 释放 VM 内建原型 P 对象(生成器/Promise/异步族与 Object 原型)的堆外属性区。
///
/// # 注意事项
@@ -576,8 +549,8 @@ impl Vm {
pub fn reset(&mut self) {
self.clear_execution_state();
self.maybe_collect_session_gc();
// session 对象可持有 epoch 子引用(函数对象捕获、原生盒直插):
// 把 epoch 子引用原地克隆晋升进 session,避免悬垂指针。
// 晋升族空转调用保留:统一入口 Box 化后生产路径不产 epoch 对象,
// 本调用在统一表上恒无操作(随晋升族整体退役)。
self.promote_session_epoch_refs();
self.bytecode = Arc::default();
// 表代际注册表不动:存活函数对象(含挂起帧 callee)按创建期代际仍须
@@ -586,9 +559,8 @@ impl Vm {
self.active_immutables = std::ptr::slice_from_raw_parts(std::ptr::null(), 0);
// 单 run 分配包络按 run 边界重起算(与 run_alloc_bytes 起算口径同源)。
self.gc_state.run_alloc_peak = 0;
// 两档收集水位与包络同起算:旧 run 的存活包络不延续到新 run 的触发判定。
// 执行期收集水位与包络同起算:旧 run 的存活包络不延续到新 run 的触发判定。
self.gc_state.gc_watermark = self.gc_state.gc_threshold_cached;
self.gc_state.gc_gate_retry_alloc = 0;
self.root_reg_limit = 0;
self.active_reg_limit = 0;
}
+5 -5
View File
@@ -225,8 +225,8 @@ fn reentry_pump_hits_alloc_cap() {
assert!(err.contains("memory limit"), "unexpected error: {err}");
}
/// 直接恢复生成器一步:等价 `it.next()`——回归聚焦 GC 搬移后的状态盒
/// 有效性(同 run 内恢复,不经 run 边界换表)。
/// 直接恢复生成器一步:等价 `it.next()`——回归聚焦 GC 收集后的状态盒
/// 有效性(原地 sweep 不搬移对象;同 run 内恢复,不经 run 边界换表)。
fn resume_one_step(vm: &mut Vm, gen: JsValue) -> JsValue {
match vm.resume_generator(gen, crate::generator::GeneratorResumeMode::Next(JsValue::undefined())) {
Ok(crate::generator::GeneratorStep::Suspended { value }) => value,
@@ -248,12 +248,12 @@ fn generator_survives_object_sweep_and_resumes() {
let mut vm = vm_with_low_threshold();
let _ = run_source(&mut vm, "function* g(){ yield 1; yield 2; } globalThis.it = g(); globalThis.it.next(); 0");
// 直接触发完整收集(保留执行上下文):存活生成器克隆进新 arena,
// 状态盒深拷贝为新 Box(走收集入口而非 run 边界:聚焦 GC 搬移本身)。
// 直接触发完整收集(保留执行上下文):存活生成器原地保留、
// 状态盒不动(走收集入口而非 run 边界:聚焦原地收集本身)。
vm.maybe_collect_session_gc();
assert!(vm.session_gc_stats().total_collections > 0, "应触发对象收集");
// 从 global 取 sweep 重写后的生成器(同 run,模块表未换发,可恢复)。
// 从 global 取 sweep 后存活的生成器(同 run,模块表未换发,可恢复)。
let it = global_prop(&vm, "it");
assert_eq!(resume_one_step(&mut vm, it), JsValue::int(2), "sweep 后应恢复第二次 yield");
}
+6 -7
View File
@@ -1,5 +1,5 @@
//! 生成器 × session GC 回归:promote 后 full_reset 释放 epoch 原对象与 session 克隆
//! 的堆数据(各单所有权),不 double-free、不悬垂,VM 继续可用。
//! 生成器 × session GC 回归:full_reset 逐对象释放 session 生成器(本体 + 堆数据
//! + upvalue 列表,各单所有权),不 double-free、不悬垂,VM 继续可用。
//! sweep 变体与闭包捕获变体见 `vm_support.rs` 内部测试(需 crate 内 `resume_generator`
//! 在模块表未重建时恢复执行,integration 层无法触达)。
@@ -15,8 +15,8 @@ fn compile(source: &str) -> oxide_bytecode::module::CompiledModule {
Compiler::new().compile(&program).expect("compile")
}
/// promote + full_reset 变体:global 根持有生成器克隆,full_reset 释放 epoch 原对象
/// 与 session 克隆的堆数据(各单所有权),不 double-free、不悬垂,VM 继续可用。
/// full_reset 变体:global 根持有生成器,full_reset 释放其本体、堆数据与
/// upvalue 列表(各单所有权),不 double-free、不悬垂,VM 继续可用。
#[test]
fn generator_promoted_to_global_survives_full_reset_cleanly() {
let mut vm = Vm::new();
@@ -24,10 +24,9 @@ fn generator_promoted_to_global_survives_full_reset_cleanly() {
"function* g(){ yield 1; yield 2; } globalThis.it = g(); globalThis.it.next(); 0",
)))
.expect("run1");
assert!(vm.session_object_count() > 0, "生成器挂 global 应被 promote 进 session");
assert!(vm.session_object_count() > 0, "生成器挂 global 应为 session 对象");
// global 含 session 对象即强制重建:旧 global 丢弃、it 槽不存在,全程无悬垂访问
// (修复前 epoch 原对象释放共享状态盒 → 读已释放内存)。
// global 含 session 对象即强制重建:旧 global 丢弃、it 槽不存在,全程无悬垂访问。
vm.full_reset();
assert_eq!(vm.session_object_count(), 0, "full_reset 应清空 session 对象");