v8 GC 触发机制
v8 通过中断机制处理线程之间的协作调度,如 GC 的安全点检查,主线程响应编译线程的 INSTALL_CODE 请求等,下面主要看下 Maglev 中对于 GC 的处理机制。
StackGuard
StackGuard 作为运行时保护机制,一个作用是做栈溢出检测,更重要的是其作为中断处理程序,处理个线程发送的中断信息,作为线程中间协作调度的核心机制。
stack_guard.h 中定义了各种中断的类型,会在 HandleInterrupts 中一一检查这些中断并处理
#define INTERRUPT_LIST(V) \
V(TERMINATE_EXECUTION, TerminateExecution, 0, InterruptLevel::kNoGC) \
V(GC_REQUEST, GC, 1, InterruptLevel::kNoHeapWrites) \
V(INSTALL_CODE, InstallCode, 2, InterruptLevel::kAnyEffect) \
V(INSTALL_BASELINE_CODE, InstallBaselineCode, 3, InterruptLevel::kAnyEffect) \
V(API_INTERRUPT, ApiInterrupt, 4, InterruptLevel::kNoHeapWrites) \
V(DEOPT_MARKED_ALLOCATION_SITES, DeoptMarkedAllocationSites, 5, \
InterruptLevel::kNoHeapWrites) \
V(GROW_SHARED_MEMORY, GrowSharedMemory, 6, InterruptLevel::kAnyEffect) \
V(LOG_WASM_CODE, LogWasmCode, 7, InterruptLevel::kAnyEffect) \
V(WASM_CODE_GC, WasmCodeGC, 8, InterruptLevel::kNoHeapWrites) \
V(INSTALL_MAGLEV_CODE, InstallMaglevCode, 9, InterruptLevel::kAnyEffect) \
V(GLOBAL_SAFEPOINT, GlobalSafepoint, 10, InterruptLevel::kNoHeapWrites) \
V(START_INCREMENTAL_MARKING, StartIncrementalMarking, 11, \
InterruptLevel::kNoHeapWrites)
// The stack limit has two values: the one with the real_ prefix is the // actual stack limit set for the VM. The one without the real_ prefix has // the same value as the actual stack limit except when there is an // interruption (e.g. debug break or preemption) in which case it is lowered // to make stack checks fail. Both the generated code and the runtime system // check against the one without the real_ prefix. // For simulator builds, we also use a separate C++ stack limit.
stack_guard 中定义了两种 stack limit,一种 real_jslimit_ 一种 jslimit_。根据注释中的说法 real_jslimit_ 作为实际的 stack limit,在线程初始化的时候被赋值,而 jslimit_ 是一个动态调整的值。大多数情况下等于 real_jslimit_ 用于判断栈溢出检测。当有别的线程发出中断请求时,这个值被设置成一个其他的值,以此让栈溢出检测的判断失效,陷入中断处理程序。通过这种机制在大多数情况下完整一次检查、两个用途。
void StackGuard::ThreadLocal::Initialize(Isolate* isolate,
const ExecutionAccess& lock) {
const uintptr_t kLimitSize = v8_flags.stack_size * KB;
DCHECK_GT(base::Stack::GetStackStart(), kLimitSize);
uintptr_t limit = base::Stack::GetStackStart() - kLimitSize;
real_jslimit_ = SimulatorStack::JsLimitFromCLimit(isolate, limit);
set_jslimit(SimulatorStack::JsLimitFromCLimit(isolate, limit));
#ifdef USE_SIMULATOR
real_climit_ = limit;
set_climit(limit);
#endif
interrupt_scopes_ = nullptr;
interrupt_flags_ = 0;
}
thread 初始化的时候根据 stacklimitsize 以及当前的 stack start 计算出一个限制点,real_jslimit 以及 jslimit 都是这个值,栈增长不能超过这个限制,否则出发栈溢出检测。
工作原理:
1. 正常情况:jslimit == real_jslimit,sp > jslimit,没有发生栈溢出
Stack: [high address] ────────────────── [low address]—————————
| | |
fp sp jslimit
↑
real_jslimit
2. 有中断待处理:jslimit = kInterruptLimit(一个非常小的值,0xfffffffffffffffe),
任何栈检查都会失败!触发慢路径 → 调用 Runtime → 处理中断
Stack: ——————————[high address] ───────────── [low address]—————————
| | | |
jslimit fp sp real_jslimit
关键设计:一次检查,两种机制
修改 jslimit
以 GC 为例说明 jslimit 修改的逻辑
- 在创建对象是需要分配内存,最终走到 AllocateRaw 的逻辑中
AllocationResult HeapAllocator::AllocateRaw(int size_in_bytes,
AllocationType type,
AllocationOrigin origin,
AllocationAlignment alignment,
AllocationHint hint) {
switch (type) {
case AllocationType::kYoung:
return AllocateRaw<AllocationType::kYoung>(size_in_bytes, origin,
alignment, hint);
case AllocationType::kOld:
return AllocateRaw<AllocationType::kOld>(size_in_bytes, origin, alignment,
hint);
// .............................
}
UNREACHABLE();
}
- 走到具体的 Allocator 逻辑中,首先判断能否做 BumpPointer 分配的快速路径,不能的话会走到 slowpath 中
AllocationResult MainAllocator::AllocateRaw(int size_in_bytes,
AllocationAlignment alignment,
AllocationOrigin origin,
AllocationHint hint) {
size_in_bytes = ALIGN_TO_ALLOCATION_ALIGNMENT(size_in_bytes);
DCHECK_EQ(in_gc(), origin == AllocationOrigin::kGC);
DCHECK_EQ(in_gc(), isolate_heap()->IsInGC());
// We are not supposed to allocate in fast c calls.
DCHECK_IMPLIES(is_main_thread(),
v8_flags.allow_allocation_in_fast_api_call ||
!isolate_heap()->isolate()->InFastCCall());
AllocationResult result;
if (alignment != kTaggedAligned) [[unlikely]] {
result = AllocateFastAligned(size_in_bytes, nullptr, alignment, origin);
} else {
result = AllocateFastUnaligned(size_in_bytes, origin);
}
return result.IsFailure() ? AllocateRawSlow(size_in_bytes, alignment, origin)
: result;
}
- slowpath 会进行 Allocation 分配的检查,根据不同的 allocation_policy 有不同的检查逻辑,在主线程会判断是否直接做并发标记,子线程会走到下面的逻辑。
bool PagedSpaceAllocatorPolicy::EnsureAllocation(int size_in_bytes,
AllocationAlignment alignment,
AllocationOrigin origin) {
if (allocator_->identity() == NEW_SPACE) {
DCHECK(allocator_->is_main_thread());
space_heap()->StartMinorMSConcurrentMarkingIfNeeded();
}
if ((allocator_->identity() != NEW_SPACE) && !allocator_->in_gc()) {
// Start incremental marking before the actual allocation, this allows the
// allocation function to mark the object black when incremental marking is
// running.
space_heap()->StartIncrementalMarkingIfAllocationLimitIsReached(
allocator_->local_heap(), space_heap()->GCFlagsForIncrementalMarking(),
kGCCallbackScheduleIdleGarbageCollection);
}
// We don't know exactly how much filler we need to align until space is
// allocated, so assume the worst case.
size_in_bytes += Heap::GetMaximumFillToAlign(alignment);
if (allocator_->allocation_info().top() + size_in_bytes <=
allocator_->allocation_info().limit()) {
return true;
}
return RefillLab(size_in_bytes, origin);
}
如果达到了 kHardLimit,会做增量标记,只能在主线程做。所以如果在主线程就直接 Start,在子线程需要注册中断信号,主线程陷入中断处理程序的时候识别中断,暂停所有 JS 线程开始增量标记。
void Heap::StartIncrementalMarkingIfAllocationLimitIsReached(
LocalHeap* local_heap, GCFlags gc_flags,
const GCCallbackFlags gc_callback_flags) {
if (incremental_marking()->IsStopped() &&
incremental_marking()->CanAndShouldBeStarted()) {
switch (IncrementalMarkingLimitReached()) {
case IncrementalMarkingLimit::kHardLimit:
if (local_heap->is_main_thread_for(this)) {
StartIncrementalMarking(
gc_flags,
OldGenerationSpaceAvailable() <= NewSpaceTargetCapacity()
? GarbageCollectionReason::kAllocationLimit
: GarbageCollectionReason::kGlobalAllocationLimit,
gc_callback_flags);
} else {
ExecutionAccess access(isolate());
isolate()->stack_guard()->RequestStartIncrementalMarking();
if (auto* job = incremental_marking()->incremental_marking_job()) {
job->ScheduleTask();
}
}
break;
case IncrementalMarkingLimit::kSoftLimit:
if (auto* job = incremental_marking()->incremental_marking_job()) {
job->ScheduleTask(TaskPriority::kUserVisible);
}
break;
case IncrementalMarkingLimit::kFallbackForEmbedderLimit:
// This is a fallback case where no appropriate limits have been
// configured yet.
if (local_heap->is_main_thread_for(this) &&
memory_reducer() != nullptr) {
memory_reducer()->NotifyPossibleGarbage();
}
break;
case IncrementalMarkingLimit::kNoLimit:
break;
}
}
}
RequestStartIncrementalMarking 通过一个宏定义,实际调用的是 RequestInterrupt,并传入 START_INCREMENTAL_MARKING 标记
#define INTERRUPT_LIST(V) \
V(TERMINATE_EXECUTION, TerminateExecution, 0, InterruptLevel::kNoGC) \
V(GC_REQUEST, GC, 1, InterruptLevel::kNoHeapWrites) \
V(INSTALL_CODE, InstallCode, 2, InterruptLevel::kAnyEffect) \
V(INSTALL_BASELINE_CODE, InstallBaselineCode, 3, InterruptLevel::kAnyEffect) \
V(API_INTERRUPT, ApiInterrupt, 4, InterruptLevel::kNoHeapWrites) \
V(DEOPT_MARKED_ALLOCATION_SITES, DeoptMarkedAllocationSites, 5, \
InterruptLevel::kNoHeapWrites) \
V(GROW_SHARED_MEMORY, GrowSharedMemory, 6, InterruptLevel::kAnyEffect) \
V(LOG_WASM_CODE, LogWasmCode, 7, InterruptLevel::kAnyEffect) \
V(WASM_CODE_GC, WasmCodeGC, 8, InterruptLevel::kNoHeapWrites) \
V(INSTALL_MAGLEV_CODE, InstallMaglevCode, 9, InterruptLevel::kAnyEffect) \
V(GLOBAL_SAFEPOINT, GlobalSafepoint, 10, InterruptLevel::kNoHeapWrites) \
V(START_INCREMENTAL_MARKING, StartIncrementalMarking, 11, \
InterruptLevel::kNoHeapWrites)
#define V(NAME, Name, id, interrupt_level) \
inline bool Check##Name() { return CheckInterrupt(NAME); } \
inline void Request##Name() { RequestInterrupt(NAME); } \
inline void Clear##Name() { ClearInterrupt(NAME); }
INTERRUPT_LIST(V)
#undef V
把标记设置到 interrupt_flags_ 上,并修改 stack_limit
void StackGuard::RequestInterrupt(InterruptFlag flag) {
ExecutionAccess access(isolate_);
// Check the chain of InterruptsScope for interception.
if (thread_local_.interrupt_scopes_ &&
thread_local_.interrupt_scopes_->Intercept(flag)) {
return;
}
// Not intercepted. Set as active interrupt flag.
thread_local_.interrupt_flags_ |= flag;
update_interrupt_requests_and_stack_limits(access);
// If this isolate is waiting in a futex, notify it to wake up.
isolate_->futex_wait_list_node()->NotifyWake();
}
把 jslimit 设置成 kInterruptLimit
void StackGuard::update_interrupt_requests_and_stack_limits(
const ExecutionAccess& lock) {
DCHECK_NOT_NULL(isolate_);
if (has_pending_interrupts(lock)) {
thread_local_.set_jslimit(kInterruptLimit);
#ifdef USE_SIMULATOR
thread_local_.set_climit(kInterruptLimit);
#endif
} else {
thread_local_.set_jslimit(thread_local_.real_jslimit_);
#ifdef USE_SIMULATOR
thread_local_.set_climit(thread_local_.real_climit_);
#endif
}
for (InterruptLevel level :
std::array{InterruptLevel::kNoGC, InterruptLevel::kNoHeapWrites,
InterruptLevel::kAnyEffect}) {
thread_local_.set_interrupt_requested(
level, InterruptLevelMask(level) & thread_local_.interrupt_flags_);
}
}
Maglev
陷入中断
Maglev 有三个地方可能陷入中断处理程序
- 在函数编译的入口插入 functionentrystackcheck,做栈溢出检测以及中断检测
bool MaglevGraphBuilder::Build() {
// Don't use the AddNewNode helper for the function entry stack check, so
// that we can set a custom deopt frame on it.
FunctionEntryStackCheck* function_entry_stack_check =
NodeBase::New<FunctionEntryStackCheck>(zone(), 0);
new (function_entry_stack_check->lazy_deopt_info()) LazyDeoptInfo(
zone(), GetDeoptFrameForEntryStackCheck(),
interpreter::Register::invalid_value(), 0, compiler::FeedbackSource());
reducer_.AddInitializedNodeToGraph(function_entry_stack_check);
}
- JumpLoop 字节码,处理循环跳转的时候插入中断跳转
void ReduceInterruptBudgetForLoop::GenerateCode(MaglevAssembler* masm,
const ProcessingState& state) {
GenerateReduceInterruptBudget(masm, this, ToRegister(feedback_cell()),
ReduceInterruptBudgetType::kLoop, amount());
}
- Return 字节码,函数返回的时候插入中断跳转
void ReduceInterruptBudgetForReturn::GenerateCode(
MaglevAssembler* masm, const ProcessingState& state) {
GenerateReduceInterruptBudget(masm, this, ToRegister(feedback_cell()),
ReduceInterruptBudgetType::kReturn, amount());
}
其中 JumpLoop 跟 Return 插入的中断主要是为了处理预算消减,以此判断是否需要做更优化的编译
FunctionEntry
GenerateCode 的时候先拿到 Condition,判断是否需要陷入中断。Condition 中用的是 kInterruptStackLimit 拿到 jslimit,也就是动态变化的栈边界(另一种 kind 拿到 realjslimit 只做栈溢出判断)。根据前面提到如果有线程注册中断,jslimit 已经被改成了一个极小值,sp 合法的判断一定不通过,会生成 CallBuiltin 调用,最终跳转到中断处理函数中。
inline Condition MaglevAssembler::FunctionEntryStackCheck(
int stack_check_offset) {
TemporaryRegisterScope temps(this);
Register stack_cmp_reg = sp;
if (stack_check_offset >= kStackLimitSlackForDeoptimizationInBytes) {
stack_cmp_reg = temps.AcquireScratch();
Sub(stack_cmp_reg, sp, stack_check_offset);
}
Register interrupt_stack_limit = temps.AcquireScratch();
LoadStackLimit(interrupt_stack_limit, StackLimitKind::kInterruptStackLimit);
Cmp(stack_cmp_reg, interrupt_stack_limit);
return kUnsignedGreaterThanEqual;
}
void FunctionEntryStackCheck::GenerateCode(MaglevAssembler* masm,
const ProcessingState& state) {
// Stack check. This folds the checks for both the interrupt stack limit
// check and the real stack limit into one by just checking for the
// interrupt limit. The interrupt limit is either equal to the real
// stack limit or tighter. By ensuring we have space until that limit
// after building the frame we can quickly precheck both at once.
const int stack_check_offset = masm->code_gen_state()->stack_check_offset();
// Only NewTarget can be live at this point.
DCHECK_LE(register_snapshot().live_registers.Count(), 1);
Builtin builtin =
register_snapshot().live_tagged_registers.has(
kJavaScriptCallNewTargetRegister)
? Builtin::kMaglevFunctionEntryStackCheck_WithNewTarget
: Builtin::kMaglevFunctionEntryStackCheck_WithoutNewTarget;
ZoneLabelRef done(masm);
Condition cond = __ FunctionEntryStackCheck(stack_check_offset);
if (masm->isolate()->is_short_builtin_calls_enabled()) {
__ JumpIf(cond, *done, Label::kNear);
__ Move(kReturnRegister0, Smi::FromInt(stack_check_offset));
__ MacroAssembler::CallBuiltin(builtin);
masm->DefineLazyDeoptPoint(lazy_deopt_info());
} else {
__ JumpToDeferredIf(
NegateCondition(cond),
[](MaglevAssembler* masm, ZoneLabelRef done,
FunctionEntryStackCheck* node, Builtin builtin,
int stack_check_offset) {
__ Move(kReturnRegister0, Smi::FromInt(stack_check_offset));
__ MacroAssembler::CallBuiltin(builtin);
masm->DefineLazyDeoptPoint(node->lazy_deopt_info());
__ Jump(*done);
},
done, this, builtin, stack_check_offset);
}
__ bind(*done);
}
Loop & Return
从 feedback 中拿到当前的中断预算并递减存回 feedback 中。预算不够生成中断跳转,最终执行到 tieringmanager 中,判断是否晋升(从 maglev 优化到 turbofan 优化)
void GenerateReduceInterruptBudget(MaglevAssembler* masm, Node* node,
Register feedback_cell,
ReduceInterruptBudgetType type, int amount) {
MaglevAssembler::TemporaryRegisterScope temps(masm);
Register scratch = temps.Acquire();
Register budget = scratch.W();
__ Ldr(budget,
FieldMemOperand(feedback_cell, FeedbackCell::kInterruptBudgetOffset));
__ Subs(budget, budget, Immediate(amount));
__ Str(budget,
FieldMemOperand(feedback_cell, FeedbackCell::kInterruptBudgetOffset));
ZoneLabelRef done(masm);
__ JumpToDeferredIf(lt, HandleInterruptsAndTiering, done, node, type,
scratch);
__ Bind(*done);
}