TL;DR
MarkCompact GC 流程图如下
graph TD
A["Start: MarkCompact::RunPhases"] --> B["InitializePhase"];
B --> E["PreGcVerification"];
subgraph "Mutator Lock (Read)"
E["MarkingPhase"];
end
subgraph "ScopedPause"
E --> G["MarkingPause"];
end
subgraph "Mutator Lock (Read)"
G --> J["ReclaimPhase"];
J --> K["PrepareForCompaction: Determine if compaction needed"];
end
K --> M{"perform_compaction is true"};
M -- No --> N["Skip Compaction"];
M -- Yes --> O["ThreadFlipVisitor, FlipCallback"];
O --> P["FlipThreadRoots: Pause threads, flip roots"];
P --> Q{"IsValidFd(uffd_)"};
Q -- Yes --> S["Acquire Mutator Lock (Read)"];
subgraph "Mutator Lock (Read)"
S["CompactionPhase (with userfaultfd)"];
end
S --> U["FinishPhase"];
Q -- No --> U;
N --> U;
U --> V["PostGcVerification"];
V --> W["Reset thread_running_gc_"];
W --> X[End];
具体的 MarkCompact GC 实现
下面我们以 RunPhases()
函数为脉络,梳理整个 Mark Compact GC 的生命周期。
void MarkCompact::RunPhases() {
Thread* self = Thread::Current(); // 获取当前线程
thread_running_gc_ = self; // 记录当前执行GC的线程
Runtime* runtime = Runtime::Current(); // 获取ART运行时实例
InitializePhase(); // 初始化GC阶段
GetHeap()->PreGcVerification(this); // 在GC前对堆进行验证
{
ReaderMutexLock mu(self, *Locks::mutator_lock_); // 获取读锁,允许mutator线程运行
MarkingPhase(); // 执行标记阶段,标记所有可达对象
}
{
// Marking pause // 标记暂停
ScopedPause pause(this); // 作用域暂停,暂停所有mutator线程
MarkingPause(); // 执行标记暂停阶段的额外操作
if (kIsDebugBuild) {
bump_pointer_space_->AssertAllThreadLocalBuffersAreRevoked(); // 调试模式下,断言所有线程本地缓冲区都已撤销
}
}
bool perform_compaction; // 是否执行压缩
{
ReaderMutexLock mu(self, *Locks::mutator_lock_); // 获取读锁,允许mutator线程运行
ReclaimPhase(); // 执行回收阶段,回收不可达对象
perform_compaction = PrepareForCompaction(); // 准备压缩,判断是否需要执行压缩
}
if (perform_compaction) { // 如果需要执行压缩
// Compaction pause // 压缩暂停
ThreadFlipVisitor visitor(this); // 创建线程翻转访问器
FlipCallback callback(this); // 创建翻转回调
runtime->GetThreadList()->FlipThreadRoots( // 翻转线程根,更新线程栈和寄存器中的对象引用
&visitor, &callback, this, GetHeap()->GetGcPauseListener());
if (IsValidFd(uffd_)) { // 如果用户态文件描述符(userfaultfd)有效
ReaderMutexLock mu(self, *Locks::mutator_lock_); // 获取读锁,允许mutator线程运行
CompactionPhase(); // 执行压缩阶段,移动对象以消除碎片
}
}
FinishPhase(); // 完成GC阶段
GetHeap()->PostGcVerification(this); // 在GC后对堆进行验证
thread_running_gc_ = nullptr; // 清除GC线程记录
}
Phase 1: InitializePhase
- 初始化阶段
这是 GC 的准备阶段。
- 重置状态:清空标记栈 (
mark_stack_
)、免疫空间列表 (immune_spaces_
) 和各种计数器。 - 设定边界:记录当前堆的末尾位置,标记暂停后分配的对象都将被视为“黑色”(默认存活)。
- 计算地址偏移:计算出
moving_space
(当前堆)和from_space
(用于整理的临时源空间)之间的地址差from_space_slide_diff_
。
void MarkCompact::InitializePhase() {
// 计时:记录本函数(初始化阶段)的耗时,便于 GC 性能分析
TimingLogger::ScopedTiming t(__FUNCTION__, GetTimings());
// 1) 获取标记栈,并确保在初始化开始时为空
mark_stack_ = heap_->GetMarkStack();
CHECK(mark_stack_->IsEmpty());
// 2) 重置免疫空间信息(本轮 GC 不会移动/回收的空间集合,稍后会重新构建)
immune_spaces_.Reset();
// 3) 清零各种统计数据/计数器
moving_first_objs_count_ = 0; // 移动分区内“段首对象”数量(用于压缩/滑移边界计算的统计)
non_moving_first_objs_count_ = 0; // 非移动分区内“段首对象”数量
black_page_count_ = 0; // 被标记为“黑页”的页数量(实现相关的统计/优化)
bytes_scanned_ = 0; // 已扫描的对象总字节数
freed_objects_ = 0; // 已释放对象数(清扫/整理阶段统计)
// 4) 压缩缓冲计数器初始化:第一个 buffer(索引 0)预留给 GC 线程,
// 因此这里从 1 开始。使用 relaxed 原子操作,仅需原子性,无需顺序性。
compaction_buffer_counter_.store(1, std::memory_order_relaxed);
// 5) 记录“黑色分配”的起点:从当前 bump-pointer 空间的 Limit 开始的新分配直接置黑
// Limit() 返回的是当前空间的 分配上界(下一个对象分配的起始位置)。
black_allocations_begin_ = bump_pointer_space_->Limit();
// 6) 校验移动空间起点一致性:moving_space_begin_ 应等于 bump-pointer 空间的 Begin
DCHECK_EQ(moving_space_begin_, bump_pointer_space_->Begin());
// 7) 计算 from-space 到 moving-space 的“滑移偏移量”,
// 用于压缩时把旧地址映射到新地址(slide/compaction)
from_space_slide_diff_ = from_space_begin_ - moving_space_begin_;
// 8) 记录本轮移动空间的当前结束边界(Limit 可能随分配/整理变化)
moving_space_end_ = bump_pointer_space_->Limit();
// 9) 如之前记录的黑密集区终点超出起点,说明位图可能留有旧的活性标记;
// 为确保干净起步,清空移动空间位图,并复位黑密集区终点。
if (black_dense_end_ > moving_space_begin_) {
moving_space_bitmap_->Clear();
}
black_dense_end_ = moving_space_begin_;
// 10) 读取镜像的指针大小(32/64 位),后续对象头/偏移/屏障等操作要用
pointer_size_ = Runtime::Current()->GetClassLinker()->GetImagePointerSize();
}
Phase 2: MarkingPhase
- 并发标记阶段
MarkingPhase
目标:找出所有“存活对象”,并在标记位图/结构中记录下来,为后续 计算新地址(计算滑移/压缩后的目标地址) 和 对象复制/滑移 做准备。
并发标记阶段目标:在应用线程继续运行的同时,尽量完成大部分可达性传播与标记,减少最终 STW 的工作量。
关键点:
-
根集初标记(可能在短暂停顿后开始): 调用
MarkRoots(kVisitRootFlagAllRoots | kVisitRootFlagStartLoggingNewRoots
)把当下的线程栈、JNI、Class、静态字段、intern 表等根对象置灰/置黑并入标记栈。从这之后开始记录新增根(StartLoggingNewRoots
),以便最终 STW 回补。 -
设置黑色分配边界: 记录
black_allocations_begin_ = bump_pointer_space_->Limit()
;并发期内新分配对象直接视为黑色,保持三色不变式、避免漏标。 -
堆扫描与传播:
MarkReachableObjects()
:多个 GC worker 并发 drain mark_stack_;AtomicTestAndSet
位图去重;ScanObject
遍历对象引用字段;image
/zygote
对象不移动,通常不入栈或视为已存活。 -
卡表/脏卡预清(preclean):
PrepareCardTableForMarking(true)
:清理/准备卡表;并发期 mutator 写入会把卡标脏;GC 周期性PreCleanCards()
:只要不 STW,就尽量把当前“已知脏卡”里指向的新引用补标,把增量工作抢先做掉,降低最终停顿工作量。由于本实现“没有写/读屏障”,并发期仍然可能遗漏新写入带来的可达性,需要最后 STW 兜底。 -
特殊空间:
MarkZygoteLargeObjects()
/MarkLargeObjects()
:LOS 通常只标不动;Non-moving space
:只标记不重定位。 -
引用处理的并发准备:
ReferenceProcessor::Setup(..., concurrent=true, clear_soft_refs)
:并发期先建好引用处理管线;若本轮不清理软引用,提前ForwardSoftReferences()
,把尽可能多的SoftReference
直接转发到强可达,减轻暂停期开销。
简单说:并发期做“能做的都先做”,尤其是遍历闭包和脏卡预清,但因为没有写/读屏障,最后仍需 STW 把并发期发生的新写入补齐。
void MarkCompact::MarkingPhase() {
// 计时:记录 MarkingPhase 阶段的时间,用于性能分析
TimingLogger::ScopedTiming t(__FUNCTION__, GetTimings());
// 确保当前线程是正在执行垃圾回收的线程
DCHECK_EQ(thread_running_gc_, Thread::Current());
// 锁定堆位图,确保在标记阶段堆的结构不被修改
WriterMutexLock mu(thread_running_gc_, *Locks::heap_bitmap_lock_);
// 调整 GC 结构,确保它们适合当前的标记阶段
MaybeClampGcStructures();
// 准备卡表,为标记阶段清除脏卡
PrepareCardTableForMarking(/*clear_alloc_space_cards*/ true);
// 标记 Zygote(预先创建的共享对象)中的大对象
MarkZygoteLargeObjects();
// 标记根对象(线程栈、JNI 引用、静态字段等)
MarkRoots(
static_cast<VisitRootFlags>(kVisitRootFlagAllRoots | kVisitRootFlagStartLoggingNewRoots));
// 标记所有可达对象
MarkReachableObjects();
// 在暂停之前,先清理已脏的卡,以减少暂停时间
PreCleanCards();
// 设置引用处理,并在启用慢路径(MarkingPause)之前先处理软引用
ReferenceProcessor* rp = GetHeap()->GetReferenceProcessor();
bool clear_soft_references = GetCurrentIteration()->GetClearSoftReferences();
// 配置引用处理器
rp->Setup(thread_running_gc_, this, /*concurrent=*/ true, clear_soft_references);
// 如果不需要清理软引用,尝试转发软引用
if (!clear_soft_references) {
rp->ForwardSoftReferences(GetTimings());
}
}
MaybeClampGcStructures
主要作用是根据当前的 bump-pointer
空间大小调整 GC 数据结构(如 from_space_map_
、live_words_bitmap_
、info_map_
等)以确保它们的大小适配当前分配的内存区域
void MarkCompact::MaybeClampGcStructures() {
// 获取 bump-pointer 空间的容量(分配空间的大小)
size_t moving_space_size = bump_pointer_space_->Capacity();
// 确保正在运行垃圾回收的线程有效
DCHECK(thread_running_gc_ != nullptr);
// 如果 "clamping" 操作还没有完成,执行数据结构的大小调整
if (UNLIKELY(clamp_info_map_status_ == ClampInfoStatus::kClampInfoPending)) {
// 检查 from_space_map_ 是否有效,并调整其大小
CHECK(from_space_map_.IsValid());
if (from_space_map_.Size() > moving_space_size) {
from_space_map_.SetSize(moving_space_size); // 确保 from_space_map_ 不会超过分配空间大小
}
// 调整活动字节位图(live_words_bitmap_)的大小
live_words_bitmap_->SetBitmapSize(moving_space_size);
// 初始化信息映射表,并根据分配空间大小设置其大小
size_t set_size = InitializeInfoMap(info_map_.Begin(), moving_space_size);
CHECK_LT(set_size, info_map_.Size()); // 确保初始化后的大小不超过原始大小
info_map_.SetSize(set_size); // 更新 info_map_ 的大小
// 完成 "clamping" 操作
clamp_info_map_status_ = ClampInfoStatus::kClampInfoFinished;
}
}
Phase 3: MarkingPause
- 标记暂停阶段
目标:在停止所有 mutator 后,做一次确定性的补标记,把并发期的竞态更新(新根、新写入、TLA/分配缓冲尾部等)全部补齐,保证闭包完备;同时切换到“慢路径”以安全处理 Reference
家族。
整个流程如下
flowchart TD
A["进入MarkingPause (STW)"] --> B["获取锁: mutator锁 + heap_bitmap锁"]
B --> C["遍历线程列表"]
C --> C1["标记线程根"]
C1 --> C2["撤销ThreadLocal分配栈/缓冲"]
C2 --> D["结算并发期分配数"]
D --> E["设置黑色分配边界 (页对齐)"]
E --> F["重新标记非线程根"]
F --> G["扫描脏卡对象 (补标)"]
G --> H["交换Live/Mark栈 冻结活对象集"]
H --> I["禁止新增system weaks"]
I --> J["启用Reference处理慢路径"]
J --> K["标记完成 marking_done = true"]
void MarkCompact::MarkingPause() {
// 计时:记录(暂停态)MarkingPause 的耗时
TimingLogger::ScopedTiming t("(Paused)MarkingPause", GetTimings());
Runtime* runtime = Runtime::Current();
// 断言当前持有 Mutator 锁的独占权(Stop-The-World 已生效)
Locks::mutator_lock_->AssertExclusiveHeld(thread_running_gc_);
{
// 并发 GC 的场景下,此处需要集中处理“并发期产生的脏对象/结构”
// 抓堆位图写锁,保证标记位图/栈等结构更新原子一致
WriterMutexLock mu(thread_running_gc_, *Locks::heap_bitmap_lock_);
{
// 与关机/线程表相关的锁,保护线程列表和线程状态遍历
MutexLock mu2(thread_running_gc_, *Locks::runtime_shutdown_lock_);
MutexLock mu3(thread_running_gc_, *Locks::thread_list_lock_);
// 拿到当前所有线程列表
std::list<Thread*> thread_list = runtime->GetThreadList()->GetList();
for (Thread* thread : thread_list) {
// 补标线程本地根(如栈上的本地引用等),flags=0 表示默认遍历
thread->VisitRoots(this, static_cast<VisitRootFlags>(0));
// 线程本地 GC buffer 应为空(避免并发期仍持有未处理的本地缓存)
DCHECK_EQ(thread->GetThreadLocalGcBuffer(), nullptr);
// 撤销线程本地分配栈,避免下面交换分配/存活栈时
// 仍有线程把对象分配进“活栈”
thread->RevokeThreadLocalAllocationStack();
// 撤销 bump-pointer 空间上的线程本地分配缓冲(TLAB)
bump_pointer_space_->RevokeThreadLocalBuffers(thread);
}
}
// 仅抓取“累计分配的对象数”,在上面撤销 TLAB 之后这个计数是可靠的
freed_objects_ += bump_pointer_space_->GetAccumulatedObjectsAllocated();
// 记录此刻 moving-space 的“末端”为黑色分配起点。
// 之后一切新分配对象都视作黑色,避免再次标记/扫描。
// 同时按页对齐,便于后续基于卡表的 ClearCardRange 等操作。
black_allocations_begin_ =
bump_pointer_space_->AlignEnd(thread_running_gc_, gPageSize, heap_);
DCHECK_ALIGNED_PARAM(black_allocations_begin_, gPageSize);
// 重新标记根集合(不含线程根,线程根刚才已处理)
ReMarkRoots(runtime);
// 扫描脏对象:把并发期以来所有“脏卡”涉及的引用出边强制补标。
// paused=true 表示当前处于 STW 阶段;kCardDirty 仅处理已标脏的卡页。
RecursiveMarkDirtyObjects(/*paused*/ true, accounting::CardTable::kCardDirty);
{
// 交换 live/mark 栈,冻结本轮活对象集的快照大小
TimingLogger::ScopedTiming t2("SwapStacks", GetTimings());
heap_->SwapStacks();
live_stack_freeze_size_ = heap_->GetLiveStack()->Size();
}
}
// TODO(注释解释):PreSweeping 验证在“bump-pointer + mark-bitmap + 黑色分配”并存时
// 需要既能按位图走对象,又能覆盖黑色分配(未在位图里),这在锁切换窗口中
// 容易出现遗漏或多访,暂不启用。
// heap_->PreSweepingGcVerification(this);
// 禁止新增 system-weak,避免在 sweep 前新加的弱引用未被标记而被误扫;
// 也避免字符串驻留(intern)返回即将被回收的强引用。
runtime->DisallowNewSystemWeaks();
// 引用处理进入慢路径(禁用 fast-path),必须在 mutator 暂停下切换,
// 因为 GetReferent 的 fast-path 没有加锁。
heap_->GetReferenceProcessor()->EnableSlowPath();
// 标记阶段到此彻底完成
marking_done_ = true;
}
Phase 4: ReclaimPhase
& PrepareForCompaction
- 回收与整理准备阶段
ReclaimPhase
- 回收非移动空间:对大对象空间(LOS)和非移动空间(Non-moving space)执行传统的标记-清除(Mark-Sweep)。
- 处理引用队列:处理
WeakReference
等,将可回收的引用放入队列。
void MarkCompact::ReclaimPhase() {
// 计时:记录 ReclaimPhase 阶段的耗时
TimingLogger::ScopedTiming t(__FUNCTION__, GetTimings());
// 确认当前运行 GC 的线程就是调用线程
DCHECK(thread_running_gc_ == Thread::Current());
Runtime* const runtime = Runtime::Current();
// 1) 并发处理引用对象(软引用、弱引用、虚引用、finalizer 引用等)
// 这里会按照 GC 策略决定哪些引用对象可达、哪些要清理/入队
ProcessReferences(thread_running_gc_);
// 2) 清扫系统弱引用(system weaks),这里是并发执行(paused = false)
// TODO 注释说明:将来可能和压缩暂停阶段的引用更新合并,减少一次扫描
SweepSystemWeaks(thread_running_gc_, runtime, /*paused*/ false);
// 3) 允许新的 system weaks 被注册
// (之前在 MarkingPause 阶段调用过 DisallowNewSystemWeaks())
runtime->AllowNewSystemWeaks();
// 4) 在 system weaks 被清扫之后清理类加载器
// 因为是否有类卸载发生,是通过 system weaks 的清扫结果来判断的
runtime->GetClassLinker()->CleanupClassLoaders();
{
// 抓堆位图写锁,保证后续 Sweep 和位图操作的并发安全
WriterMutexLock mu(thread_running_gc_, *Locks::heap_bitmap_lock_);
// 5) 回收未被标记的对象(白对象),释放内存
Sweep(false);
// 6) 交换各空间的 live/mark 位图
// 优化:这样在 Sweep 中不用显式清 live bits,
// 只需要在这里交换位图即可(避免逐个清位,提升效率)
// 注意:只交换未绑定的位图
SwapBitmaps();
// 7) 解除堆空间和位图之间的绑定关系
// 便于下一个 GC 周期重新初始化
GetHeap()->UnBindBitmaps();
}
}
PrepareForCompaction
- 计算存活密度:分析
moving_space
中对象的存活密度。如果某块区域的对象存活率非常高(例如超过95%),则将其标记为黑色密集区 (black_dense_end_
)。这部分区域的对象将**不被移动**,只更新其内部指针。 - 计算新地址:这是最关键的一步。通过
live_words_bitmap_
和chunk_info_vec_
,对存活字节进行一次前缀和(std::exclusive_scan
)计算。完成后,chunk_info_vec_
就成了一张“地址映射表”,可以通过任何一个对象的旧地址,瞬间计算出它的新地址。 - 计算滑动距离:计算出黑色对象(新分配的对象)需要整体滑动的距离
black_objs_slide_diff_
。
bool MarkCompact::PrepareForCompaction() {
// 计时:记录压缩准备阶段的耗时
TimingLogger::ScopedTiming t(__FUNCTION__, GetTimings());
// 每页包含多少个“chunk”(按 kOffsetChunkSize 切分)
size_t chunk_info_per_page = gPageSize / kOffsetChunkSize;
// 参与压缩的区间长度(以 chunk 为单位):从移动空间起点到黑色分配边界
// black_allocations_begin_ 之后是“黑色分配”,本轮不参与(只做滑移差)
size_t vector_len = (black_allocations_begin_ - moving_space_begin_) / kOffsetChunkSize;
// 运行时检查:长度不得超过预分配的向量容量
DCHECK_LE(vector_len, vector_length_);
// 运行时检查:chunk 向量容量按“每页 chunk 数”对齐
DCHECK_ALIGNED_PARAM(vector_length_, chunk_info_per_page);
// 如果参与压缩的区间为 0,直接返回(无需压缩)
if (UNLIKELY(vector_len == 0)) {
// Nothing to compact.
return false;
}
// 校验预填充的 live-bytes 统计:chunk_info_vec_[i] 必须等于位图统计的 live bytes
for (size_t i = 0; i < vector_len; i++) {
DCHECK_LE(chunk_info_vec_[i], kOffsetChunkSize);
DCHECK_EQ(chunk_info_vec_[i], live_words_bitmap_->LiveBytesInBitmapWord(i));
}
// ----------------------------------------------------------------------------
// 说明:
// 此处的 chunk_info_vec_ 初始装的是“每个 chunk 的活字节数”,
// 稍后会把它原地转换为“前缀和”(old->new 地址映射用),即每个位置表示
// “到该 chunk 为止累计的活字节数”(exclusive_scan)。
//
// 同时,这里引入“黑密集区(black-dense region)”的概念:
// 通过统计页粒度的活字节占比,找到“前段足够致密”的区域(活性很高),
// 可以选择对其“就地更新引用、不移动对象”,以减少更新量(可选优化)。
// ----------------------------------------------------------------------------
size_t black_dense_idx = 0; // 黑密集区终止的 chunk 索引(开区间下标)
GcCause gc_cause = GetCurrentIteration()->GetGcCause();
// 在某些触发场景下(非显式 GC、非收集器切换,且不清软引用)才尝试黑密集优化
if (gc_cause != kGcCauseExplicit && gc_cause != kGcCauseCollectorTransition &&
!GetCurrentIteration()->GetClearSoftReferences()) {
uint64_t live_bytes = 0, total_bytes = 0;
// 将 vector_len 向上对齐到整页的 chunk 数,便于页粒度统计
size_t aligned_vec_len = RoundUp(vector_len, chunk_info_per_page);
size_t num_pages = aligned_vec_len / chunk_info_per_page;
size_t threshold_passing_marker = 0; // 记录“到目前为止达到密度阈值”的页数
std::vector<uint32_t> pages_live_bytes;
pages_live_bytes.reserve(num_pages);
// 逐页累加活字节,寻找“从起始页向后累计达到阈值”的最大前缀
// kBlackDenseRegionThreshold(百分比阈值)用于判断“足够致密”
for (size_t i = 0; i < aligned_vec_len; i += chunk_info_per_page) {
uint32_t page_live_bytes = 0;
for (size_t j = 0; j < chunk_info_per_page; j++) {
page_live_bytes += chunk_info_vec_[i + j];
total_bytes += kOffsetChunkSize;
}
live_bytes += page_live_bytes;
pages_live_bytes.push_back(page_live_bytes);
// 从起点到当前位置的累计活字节比例是否超过阈值
if (live_bytes * 100U >= total_bytes * kBlackDenseRegionThreshold) {
threshold_passing_marker = pages_live_bytes.size();
}
}
DCHECK_EQ(pages_live_bytes.size(), num_pages);
// 去除尾部“不达标”的页:从尾端回溯,找到最后一个达标的页
if (threshold_passing_marker > 0) {
auto iter = std::find_if(
pages_live_bytes.rbegin() + (num_pages - threshold_passing_marker),
pages_live_bytes.rend(),
[](uint32_t bytes) {
return bytes * 100U >= gPageSize * kBlackDenseRegionThreshold;
});
// 计算黑密集区结束的 chunk 下标(页 * 每页chunk数)
black_dense_idx = (pages_live_bytes.rend() - iter) * chunk_info_per_page;
}
// 将“黑密集区结束地址”落到 moving space 线上,并按页对齐
black_dense_end_ = moving_space_begin_ + black_dense_idx * kOffsetChunkSize;
DCHECK_ALIGNED_PARAM(black_dense_end_, gPageSize);
// 处理特殊情况:某些 Class 对象在 black_dense_end_ 之后分配,
// 但其实例在此前页面开始。因为黑密集区内会“就地更新引用”,
// 若类元数据已更新而实例扫描时读取到了“错的类信息(如 ref 位图)”,会出错。
// 因此把 black_dense_end_ 往前(向低地址)收缩到安全边界(类对象最早地址),再页对齐。
for (auto iter = class_after_obj_map_.rbegin();
iter != class_after_obj_map_.rend() &&
reinterpret_cast<uint8_t*>(iter->first.AsMirrorPtr()) >= black_dense_end_;
iter++) {
black_dense_end_ =
std::min(black_dense_end_, reinterpret_cast<uint8_t*>(iter->second.AsMirrorPtr()));
black_dense_end_ = AlignDown(black_dense_end_, gPageSize);
}
// 重新计算黑密集区结束的 chunk 下标,并确保不越界
black_dense_idx = (black_dense_end_ - moving_space_begin_) / kOffsetChunkSize;
DCHECK_LE(black_dense_idx, vector_len);
// 若黑密集区覆盖了整个待压缩区间,则无需压缩
if (black_dense_idx == vector_len) {
// There is nothing to compact.
return false;
}
// 初始化“黑密集区内的不移动首对象”索引(用于就地更新引用)
InitNonMovingFirstObjects(reinterpret_cast<uintptr_t>(moving_space_begin_),
reinterpret_cast<uintptr_t>(black_dense_end_),
moving_space_bitmap_,
first_objs_moving_space_);
}
// 初始化“moving space 中、从黑密集区之后开始”的 page 首对象索引
// (参与移动/压缩的那一段)
InitMovingSpaceFirstObjects(vector_len, black_dense_idx / chunk_info_per_page);
// 初始化 Non-moving space 的 page 首对象索引(非移动区只更新引用,不搬移)
non_moving_first_objs_count_ =
InitNonMovingFirstObjects(reinterpret_cast<uintptr_t>(non_moving_space_->Begin()),
reinterpret_cast<uintptr_t>(non_moving_space_->End()),
non_moving_space_->GetLiveBitmap(),
first_objs_non_moving_space_);
// ===============================================================
// 将 chunk_info_vec_ 从“逐 chunk 的活字节统计”转换为“前缀和”:
// old->new 地址 = 区间起点 + 该 chunk 的前缀活字节
//
// 特殊:需要把向量“多更新一个元素”(heap usage 之后的一个位置),
// 用于“黑色分配对象”的压缩后地址计算(post-compact address)
// ===============================================================
uint32_t total_bytes;
if (vector_len < vector_length_) {
// 还有空间:把 vector_len 往后扩 1 个位置,作为“one-past-end”承载位
vector_len++;
total_bytes = 0;
} else {
// 没有额外空间:把末尾值先取出来(exclusive_scan 会覆盖)
total_bytes = chunk_info_vec_[vector_len - 1];
}
// 对 [black_dense_idx, vector_len) 做“前缀和(排他)”
// 起始偏移量 = black_dense_idx * kOffsetChunkSize
std::exclusive_scan(chunk_info_vec_ + black_dense_idx,
chunk_info_vec_ + vector_len,
chunk_info_vec_ + black_dense_idx,
black_dense_idx * kOffsetChunkSize);
// total_bytes = 最后一个位置的累计值(即“活字节总量”,若上面取过末尾值则加上)
total_bytes += chunk_info_vec_[vector_len - 1];
// 压缩后的移动空间“逻辑末尾”(按页对齐)
post_compact_end_ = AlignUp(moving_space_begin_ + total_bytes, gPageSize);
// 校验:压缩后末尾应等于“moving_first_objs_count_ * 页大小”后的起点
CHECK_EQ(post_compact_end_, moving_space_begin_ + moving_first_objs_count_ * gPageSize)
<< "moving_first_objs_count_:" << moving_first_objs_count_
<< " black_dense_idx:" << black_dense_idx << " vector_len:" << vector_len
<< " total_bytes:" << total_bytes
<< " black_dense_end:" << reinterpret_cast<void*>(black_dense_end_)
<< " chunk_info_per_page:" << chunk_info_per_page;
// 黑对象段的“滑移差”:黑色分配起点相对压缩后末端的偏移
// (黑段整体只需统一平移,不参与前缀和)
black_objs_slide_diff_ = black_allocations_begin_ - post_compact_end_;
// 压缩后不应比压缩前占用更多空间
CHECK_GE(black_objs_slide_diff_, 0);
// 若滑移差为 0:说明“黑段”紧贴压缩后末端,实际上无需再做移动
if (black_objs_slide_diff_ == 0) {
black_dense_end_ = black_allocations_begin_;
return false;
}
// 余下未使用的 chunk_info 位置应为 0(一致性检查)
for (size_t i = vector_len; i < vector_length_; i++) {
DCHECK_EQ(chunk_info_vec_[i], 0u);
}
// ------------------------------------------------------------------
// 重要说明(TLAB 洞的处理):
// 标记暂停之后的分配都算作“黑色”,且它们并不保证连续(各线程 TLAB 分配)。
// 因此:压缩只做“至暂停前”的那段;暂停后的那段做“带洞的滑移”,
// 再在预压缩暂停中修正 TLS 里的 TLAB 信息。
// 对应地,post-marking-pause 的 chunk-info 也会在预压缩暂停中补齐。
// ------------------------------------------------------------------
// 用户态缺页处理(userfaultfd):若未初始化,则此处创建,
// 用于后续并行搬移时的按页拷贝/保护(若启用该策略)
if (!uffd_initialized_) {
CreateUserfaultfd(/*post_fork=*/false);
}
return true;
}
Phase 5: FlipThreadRoots
& CompactionPhase
- 整理暂停与并发整理
FlipThreadRoots
ThreadList::FlipThreadRoots
安全、原子性地将所有线程的根引用(Thread Roots)从指向旧空间(from-space)“翻转”到指向新空间(to-space)。
// 这是一个“检查点/全体暂停”混合模式的函数,用于将线程的根引用从
// from-space(旧空间)切换到 to-space(新空间)。
// 它通过同步所有线程,来标志着(并发)标记阶段的正式开始,
// 同时维护了“to-space 不变性”(即一旦翻转完成,所有代码只能看到指向 to-space 的引用)。
void ThreadList::FlipThreadRoots(Closure* thread_flip_visitor,
Closure* flip_callback,
gc::collector::GarbageCollector* collector,
gc::GcPauseListener* pause_listener) {
TimingLogger::ScopedTiming split("ThreadListFlip", collector->GetTimings());
Thread* self = Thread::Current();
// ... 一系列锁状态的断言 ...
CHECK_NE(self->GetState(), ThreadState::kRunnable);
// 1. 准备阶段:与JNI临界区代码同步,为暂停做准备。
collector->GetHeap()->ThreadFlipBegin(self);
const uint64_t suspend_start_time = NanoTime();
VLOG(threads) << "Suspending all for thread flip";
{
ScopedTrace trace("ThreadFlipSuspendAll");
// 2. 执行暂停:暂停虚拟机中所有其他应用线程(Stop-The-World)。这是暂停的开始。
SuspendAllInternal(self);
}
std::vector<Thread*> flipping_threads; // 存储所有需要被翻转的线程。
int thread_count;
// 用于处理线程在翻转过程中退出的竞争条件。
std::unique_ptr<ThreadExitFlag[]> exit_flags;
{
TimingLogger::ScopedTiming t("FlipThreadSuspension", collector->GetTimings());
if (pause_listener != nullptr) {
// 通知监听器,GC暂停开始。
pause_listener->StartPause();
}
// 3. 进入关键暂停区:获取独占的 mutator_lock_,完全阻止应用代码运行。
Locks::mutator_lock_->ExclusiveLock(self);
suspend_all_histogram_.AdjustAndAddValue(NanoTime() - suspend_start_time);
// 执行由GC收集器传入的回调,通常用于交换堆空间等关键操作。
flip_callback->Run(self);
{
MutexLock mu(self, *Locks::thread_list_lock_);
MutexLock mu2(self, *Locks::thread_suspend_count_lock_);
thread_count = list_.size();
exit_flags.reset(new ThreadExitFlag[thread_count]);
flipping_threads.resize(thread_count, nullptr);
int i = 1;
// 遍历所有线程,为它们设置“翻转函数”。
for (Thread* thread : list_) {
// 为每个线程“安装”翻转任务。线程被恢复后,必须先执行此函数才能继续。
DCHECK(thread == self || thread->IsSuspended());
thread->SetFlipFunction(thread_flip_visitor);
// 把当前GC线程放在列表首位,以便优先处理。
int thread_index = thread == self ? 0 : i++;
flipping_threads[thread_index] = thread;
// 注册一个退出标志,以处理线程在翻转过程中退出的竞争条件。
thread->NotifyOnThreadExit(&exit_flags[thread_index]);
}
DCHECK(i == thread_count);
}
if (pause_listener != nullptr) {
pause_listener->EndPause();
}
}
{
MutexLock mu(self, *Locks::thread_list_lock_);
Locks::thread_suspend_count_lock_->Lock(self);
// 4. 恢复所有线程。线程醒来后会发现有翻转任务等待执行,从而被“困住”,无法立即执行应用代码。
ResumeAllInternal(self);
}
// 记录本次暂停的总时长。
collector->RegisterPause(NanoTime() - suspend_start_time);
// --- “翻转竞赛”开始 ---
// 所有线程(包括本GC线程)都会尝试执行翻转函数。
// 通过一个原子状态确保每个线程的翻转函数只会被成功执行一次。
// 尝试在其他线程上运行翻转闭包。
TimingLogger::ScopedTiming split3("RunningThreadFlips", collector->GetTimings());
// 5. GC线程重新获取共享锁。此时其他线程都被翻转任务阻塞,所以能无竞争地获取。
AcquireMutatorLockSharedUncontended(self);
Locks::thread_suspend_count_lock_->Unlock(self);
// JNI同步结束。
collector->GetHeap()->ThreadFlipEnd(self);
// 6. 开始“翻转竞赛”:GC线程主动尝试为其他线程启动翻转函数,以加速进程。
for (int i = 0; i < thread_count; ++i) {
bool finished;
// 这是一个非阻塞的尝试。
Thread::EnsureFlipFunctionStarted(
self, flipping_threads[i], Thread::StateAndFlags(0), &exit_flags[i], &finished);
if (finished) {
// 如果翻转已完成,则注销其退出标志。
MutexLock mu2(self, *Locks::thread_list_lock_);
flipping_threads[i]->UnregisterThreadExitFlag(&exit_flags[i]);
flipping_threads[i] = nullptr;
}
}
// 7. 等待所有翻转完成:阻塞等待任何尚未完成的翻转任务,确保所有线程都已更新。
for (int i = 0; i < thread_count; ++i) {
if (UNLIKELY(flipping_threads[i] != nullptr)) {
flipping_threads[i]->WaitForFlipFunctionTestingExited(self, &exit_flags[i]);
MutexLock mu2(self, *Locks::thread_list_lock_);
flipping_threads[i]->UnregisterThreadExitFlag(&exit_flags[i]);
}
}
// 调试检查:确保所有退出标志都已被正确注销。
Thread::DCheckUnregisteredEverywhere(&exit_flags[0], &exit_flags[thread_count - 1]);
// 8. 清理并结束:释放共享锁,所有线程现在都可以在新的内存布局上自由运行。
Locks::mutator_lock_->SharedUnlock(self);
}
CompactionPause
这个函数在 STW 暂停期间,按顺序执行了以下几个关键任务:
1. 同步最新分配的对象
在并发标记和这次暂停之间,应用线程可能已经分配了新的对象(尤其是在 TLAB - 线程本地分配缓冲区中)。这些新对象默认都是存活的。
UpdateMovingSpaceBlackAllocations()
: 扫描这些新分配的“黑色”对象,并将它们信息更新到 GC 的内部数据结构中(如 live_words_bitmap_
),确保它们也会被正确处理。
UpdateNonMovingSpaceBlackAllocations()
: 对非移动空间执行同样的操作。
compacting_ = true;
: 设置一个全局标志位,通知系统的其他部分(尤其是 userfaultfd
信号处理器),整理阶段正式开始。
2. 更新Immune Spaces
Immune Spaces
是指那些自身不参与 GC 的特殊内存区域,例如 Zygote
空间和 Boot Image
空间。
为什么需要更新?:虽然这些空间本身不被回收,但它们内部的对象可能引用了普通堆(Moving Space
)中的对象。当普通堆中的对象被移动后,这些引用就需要被更新。
如何高效更新?:通过卡片表(Card Table)或 Mod-Union Table。这些数据结构记录了免疫空间中哪些“卡片”(小的内存块)被“弄脏”(即被写入过)。GC 只需扫描这些被标记为“脏”的卡片,而不需要扫描整个巨大的免疫空间,从而极大地提高了效率。
3. 全面根引用更新与特殊空间处理
这是暂停期间最繁重的工作:更新虚拟机中所有的根引用(GC Roots)。
runtime->VisitConcurrentRoots(...)
, runtime->VisitNonThreadRoots(...)
, linker->VisitClassLoaders(...)
等:这些调用会地毯式地扫描所有类型的根,包括 JNI 全局引用、类的静态字段、活动的类加载器等,并将其中所有指向旧地址的指针,全部更新为指向新地址。
SweepSystemWeaks(...)
: 处理系统中的弱引用、软引用等。
处理 LinearAllocSpace
:这是用于 ART 内部对象(如 ArtMethod)的特殊空间,其对象不被移动。
并发模式:在常规模式下,函数会收集所有需要处理的 LinearAlloc
内存块(Arenas),并将它们存入 linear_alloc_arenas_
列表。这些内存块的指针修复工作将在暂停结束之后,与应用线程并发地进行。
后备模式/Zygote 场景:在后备模式或 Zygote 首次 fork 前的特殊情况下,没有并发处理阶段,因此所有 LinearAlloc
空间的指针修复工作会直接在本次暂停内完成。
KernelPreparation()
: 准备内核。这是为并发整理做准备的关键一步,它很可能会在这里通过 ioctl
调用来设置 userfaultfd
,将需要并发整理的内存区域“注册”给内核。
4. 准备并发或执行后备方案
UpdateNonMovingSpace()
: 更新非移动空间中的对象引用。
后备模式 (if (uffd_ == kFallbackMode)
): 如果 userfaultfd 不可用或初始化失败,GC 会放弃并发,进入“后备模式”。此时,它会调用 CompactMovingSpace<kFallbackMode>
,在本次 STW 暂停内,直接完成所有对象的移动和整理工作。这会导致一次较长的 GC 暂停。
并发模式 (else
): 在正常情况下,函数在完成所有准备工作后就结束了。真正的对象移动(CompactMovingSpace<kCopyMode>
)将在暂停结束之后,由 GC 线程与恢复运行的应用线程并发地进行。
void MarkCompact::CompactionPause() {
// 本函数是 Mark-Compact GC 中最核心的“Stop-The-World” (STW) 暂停。
// 在此期间,所有应用线程都被暂停,以便GC可以安全地完成所有
// 无法并发执行的关键更新和准备工作,为对象整理阶段铺平道路。
TimingLogger::ScopedTiming t(__FUNCTION__, GetTimings());
Runtime* runtime = Runtime::Current();
non_moving_space_bitmap_ = non_moving_space_->GetLiveBitmap();
// ... 调试相关的栈地址记录 ...
{
// --- 阶段1:数据追赶 ---
TimingLogger::ScopedTiming t2("(Paused)UpdateCompactionDataStructures", GetTimings());
ReaderMutexLock rmu(thread_running_gc_, *Locks::heap_bitmap_lock_);
// 刷新数据结构,以包含自并发标记暂停以来新分配的对象(“黑色对象”)。
// 这些新对象(尤其是在TLAB中)默认都是存活的。
UpdateMovingSpaceBlackAllocations();
UpdateNonMovingSpaceBlackAllocations();
// 设置全局标志位,通知其他部分(如uffd信号处理器)整理阶段已正式开始。
compacting_ = true;
// 执行一轮初步的根更新。
heap_->GetReferenceProcessor()->UpdateRoots(this);
}
{
// --- 阶段2:更新“免疫空间”(如Zygote空间、Boot Image空间)---
// 这些空间本身不被GC,但它们内部的指针可能指向需要移动的对象,因此需要更新。
TimingLogger::ScopedTiming t2("(Paused)UpdateImmuneSpaces", GetTimings());
accounting::CardTable* const card_table = heap_->GetCardTable();
for (auto& space : immune_spaces_.GetSpaces()) {
// ...
accounting::ModUnionTable* table = heap_->FindModUnionTableFromSpace(space);
ImmuneSpaceUpdateObjVisitor visitor(this);
if (table != nullptr) {
// 优先使用 ModUnionTable,它更精确地记录了被修改的区域。
table->ProcessCards();
table->VisitObjects(ImmuneSpaceUpdateObjVisitor::Callback, &visitor);
} else {
// 如果没有 ModUnionTable,则回退到扫描卡片表(Card Table)来寻找“脏”卡片。
WriterMutexLock wmu(thread_running_gc_, *Locks::heap_bitmap_lock_);
card_table->Scan<false>(live_bitmap, space->Begin(), space->Limit(), visitor, ...);
}
}
}
{
// --- 阶段3:全面根引用更新与特殊空间处理 ---
TimingLogger::ScopedTiming t2("(Paused)UpdateRoots", GetTimings());
// 地毯式扫描并更新所有非线程的根引用,如JNI全局/弱全局引用、类的静态字段等。
runtime->VisitConcurrentRoots(this, kVisitRootFlagAllRoots);
runtime->VisitNonThreadRoots(this);
{
// 更新所有类加载器及其类表中的根。
ClassLinker* linker = runtime->GetClassLinker();
ClassLoaderRootsUpdater updater(this);
ReaderMutexLock rmu(thread_running_gc_, *Locks::classlinker_classes_lock_);
linker->VisitClassLoaders(&updater);
linker->GetBootClassTable()->VisitRoots(updater, /*skip_classes=*/true);
}
// 处理系统中的弱引用、软引用等。
SweepSystemWeaks(thread_running_gc_, runtime, /*paused=*/true);
bool has_zygote_space = heap_->HasZygoteSpace();
GcVisitedArenaPool* arena_pool =
static_cast<GcVisitedArenaPool*>(runtime->GetLinearAllocArenaPool());
// 更新类表。
UpdateClassTableClasses(runtime, uffd_ == kFallbackMode || !has_zygote_space);
// 为处理LinearAllocSpace加锁,确保在处理期间没有新的Arena被分配或修改。
WriterMutexLock pool_wmu(thread_running_gc_, arena_pool->GetLock());
auto arena_visitor = [this](...) { /* ... 定义一个用于更新LinearAlloc页面的访问器 ... */ };
// 根据模式决定如何处理 LinearAlloc 空间(ART内部对象,不移动,但需更新指针)。
if (uffd_ == kFallbackMode || (!has_zygote_space && runtime->IsZygote())) {
// 在后备模式或Zygote首次fork前,直接在暂停期间更新所有LinearAlloc Arena。
arena_pool->VisitRoots(arena_visitor);
} else {
// 在并发模式下,收集需要并发处理的Arena,并推迟它们的释放以避免竞争。
arena_pool->DeferArenaFreeing();
// 遍历所有Arena,将需要并发处理的加入列表,其余的在暂停期间直接处理。
arena_pool->ForEachAllocatedArena(
[this, arena_visitor, has_zygote_space](const TrackedArena& arena) {
// ... (根据arena类型决定是现在处理还是加入并发处理列表) ...
});
}
// 重置SIGBUS信号计数器,为并发整理阶段做准备。
sigbus_in_progress_count_[0].store(0, std::memory_order_relaxed);
sigbus_in_progress_count_[1].store(0, std::memory_order_release);
// 【关键】准备内核,通常是设置userfaultfd并注册需要并发整理的内存范围。
KernelPreparation();
}
// --- 阶段4:准备并发或执行后备方案 ---
// 更新非移动空间中的对象引用。
UpdateNonMovingSpace();
if (uffd_ == kFallbackMode) {
// 【后备路径】如果userfaultfd不可用,则进入后备模式。
// 在本次STW暂停内,直接完成所有对象的移动整理工作。这将导致一次较长的暂停。
CompactMovingSpace<kFallbackMode>(nullptr);
// 记录释放的内存。
int32_t freed_bytes = black_objs_slide_diff_;
bump_pointer_space_->RecordFree(freed_objects_, freed_bytes);
RecordFree(ObjectBytePair(freed_objects_, freed_bytes));
} else {
// 【并发路径】正常情况下,暂停在此结束。真正的对象移动将在暂停后并发进行。
DCHECK_EQ(compaction_buffer_counter_.load(std::memory_order_relaxed), 1);
}
stack_low_addr_ = nullptr;
}
CompactionPhase
可以把CompactionPhase
的工作流程看作以下几个步骤:
- 记录统计信息
- 执行核心整理任务:
CompactMovingSpace<kCopyMode>(...)
: 调用 CompactMovingSpace 函数来处理Moving Space中的对象。绝大多数的 Java 对象都分配在这里。这个函数会把所有存活的对象紧凑地移动到空间的一端。ProcessLinearAlloc()
: 处理线性分配空间。这里的对象通常不会被移动(例如 Class 对象、ArtMethod 等),但它们内部可能引用了“可移动空间”中的对象。因此,这个函数的主要任务是更新这些引用,让它们指向移动后的新地址。 - 与应用线程(Mutator)同步: 使用了一个精巧的两阶段同步机制来确保 GC 线程和应用线程不会互相干扰。
wait_for_compaction_counter(0)
: 第一阶段等待。GC 线程在这里设置一个“整理完成”的标志位,并等待所有正在处理内存访问冲突(SIGBUS 信号)的应用线程完成它们当前的操作。UnregisterUffd(...)
: 通知内核(通过 userfaultfd 机制)GC 不再关心这些内存区域的访问了。这是为并发整理做清理。wait_for_compaction_counter(1)
: 第二阶段等待。GC 线程再次设置一个“清理完成”的标志位,并等待。 - 清理和安全防护:
from_space_map_.MadviseDontNeedAndZero()
: 释放“from-space”(对象移动前的旧空间)的物理内存。通过 madvise 告诉操作系统这些内存可以回收了。mprotect(..., PROT_NONE)
: 这是一个非常重要的安全措施。它将对象移动前的旧内存区域设置为完全不可读写。如果在 GC 之后,程序中还有任何代码(因为 bug)试图访问对象的旧地址,程序会立刻崩溃。这能帮助开发者迅速定位到“悬挂指针”或“引用未更新”的严重错误。
void MarkCompact::CompactionPhase() {
// 开始计时,用于性能分析。
TimingLogger::ScopedTiming t(__FUNCTION__, GetTimings());
{
// 记录本次GC整理后释放的对象数量和字节数。
int32_t freed_bytes = black_objs_slide_diff_;
bump_pointer_space_->RecordFree(freed_objects_, freed_bytes);
RecordFree(ObjectBytePair(freed_objects_, freed_bytes));
}
// 【核心步骤1】调用核心函数,整理和移动“可移动空间”(Moving Space)中的对象。
// 这是对象搬运的主要工作。
CompactMovingSpace<kCopyMode>(compaction_buffers_map_.Begin());
// 【核心步骤2】处理“线性分配空间”(Linear Alloc Space)。
// 这个空间的对象本身不移动,但需要更新它们内部指向“可移动空间”的指针。
ProcessLinearAlloc();
// 定义一个等待函数,用于和应用线程(mutator)进行同步。
// 它通过一个原子计数器来工作,等待所有正在处理内存访问冲突(SIGBUS信号)的应用线程完成。
auto wait_for_compaction_counter = [this](size_t idx) {
// 设置“整理完成”标志位,并获取当前计数。
SigbusCounterType count = sigbus_in_progress_count_[idx].fetch_or(
kSigbusCounterCompactionDoneMask, std::memory_order_acq_rel);
// 等待所有已经在处理SIGBUS信号的应用线程完成它们的工作。
for (uint32_t i = 0; count > 0; i++) {
BackOff(i);
count = sigbus_in_progress_count_[idx].load(std::memory_order_acquire);
// 清除标志位,只检查真实的计数器值。
count &= ~kSigbusCounterCompactionDoneMask;
}
};
// 【第一阶段同步点】
// GC线程在此设置第一个标志位,通知应用线程:“对象整理已经完成”。
// 然后等待,确保没有应用线程卡在旧的内存访问处理流程中。
wait_for_compaction_counter(0);
// 注销 userfaultfd 对内存区域的监控。
// GC线程告诉内核:“我不再负责处理这些内存区域的页错误了”。
// 首先是“可移动空间”。
size_t moving_space_size = bump_pointer_space_->Capacity();
size_t used_size = (moving_first_objs_count_ + black_page_count_) * gPageSize;
if (used_size > 0) {
UnregisterUffd(bump_pointer_space_->Begin(), used_size);
}
// 然后是“线性分配空间”。
for (auto& data : linear_alloc_spaces_data_) {
DCHECK_EQ(data.end_ - data.begin_, static_cast<ssize_t>(data.shadow_.Size()));
UnregisterUffd(data.begin_, data.shadow_.Size());
}
// 【第二阶段同步点】
// GC线程在此设置第二个标志位,通知应用线程:“userfaultfd注销已经完成”。
// 再次等待,确保所有应用线程都感知到这个变化。之后,应用线程如果再次访问失败,就知道这是真正的错误,而不是GC过程中的临时状态。
wait_for_compaction_counter(1);
// 释放“from-space”(对象移动前的旧空间)所占用的物理内存。
// MadviseDontNeed是一个给操作系统的提示,表明这些内存页可以被回收了。
from_space_map_.MadviseDontNeedAndZero();
// 【重要安全措施】
// 将“from-space”的内存保护设置为PROT_NONE(不可读、不可写、不可执行)。
// 如果在GC之后,程序中还有任何代码(通常是bug)试图访问对象的旧地址,
// 就会立刻触发段错误(Segmentation Fault),从而帮助开发者快速定位问题。
DCHECK_EQ(mprotect(from_space_begin_, moving_space_size, PROT_NONE), 0)
<< "mprotect(PROT_NONE) for from-space failed: " << strerror(errno);
// 释放为“线性分配空间”服务的页面状态数组所占用的内存。
for (auto& data : linear_alloc_spaces_data_) {
data.page_status_map_.MadviseDontNeedAndZero();
}
}
CompactMovingSpace
CompactMovingSpace
函数负责把“可移动空间”中的存活对象从零散的旧位置(from-space)拷贝到连续的新位置(to-space)。
反向遍历
函数从内存空间的末尾向开头处理。注释中解释了原因:这样做可以优先处理 TLAB 和最近分配的对象。根据“代际假说”,新创建的对象更有可能存活,优先处理它们可以提高效率。
分段处理
CompactMovingSpace
将整个“可移动空间”逻辑上分为三段,并用三个 while 循环来处理:
-
第一段:处理“纯黑页” (Allocated-black pages): 这些页面是在 GC 标记阶段开始后分配的,ART 知道这些页面上的所有对象都是存活的。处理方式:整体滑动 (Slide)。因为整个页面都是存活对象,所以不需要逐个对象判断,可以直接把整个页面的内容移动到新的位置,效率极高。
-
第二段:处理“混合页”: 这些页面上既有存活对象,也有死亡对象。处理方式:选择性拷贝 (CompactPage)。GC 线程需要借助位图(live-words bitmap)来识别出哪些是存活对象,然后只将这些存活的对象紧凑地拷贝到目标空间。
-
第三段:处理“非移动页” (Non-moving pages): 这些页面位于堆的起始位置,经过整理后,它们的位置可能不需要改变。处理方式:原地更新引用 (UpdateNonMovingPage)。虽然这些页面上的对象本身不动,但它们内部可能引用了前面两段中被移动过的对象。因此,这个循环的主要工作是更新这些对象内部的指针,让它们指向正确的新地址。
template <int kMode>
void MarkCompact::CompactMovingSpace(uint8_t* page) {
TimingLogger::ScopedTiming t(__FUNCTION__, GetTimings());
// 初始化循环变量和指针。
size_t page_status_arr_len = moving_first_objs_count_ + black_page_count_;
size_t idx = page_status_arr_len; // 页索引,从后往前遍历。
// black_dense_end_idx 是整理后存活对象区域的末尾页索引。
size_t black_dense_end_idx = (black_dense_end_ - moving_space_begin_) / gPageSize;
// to_space_end 指向目标空间的当前末尾,对象将被拷贝到这里。
uint8_t* to_space_end = moving_space_begin_ + page_status_arr_len * gPageSize;
// pre_compact_page 指向源空间(整理前)的当前页。
uint8_t* pre_compact_page = black_allocations_begin_ + (black_page_count_ * gPageSize);
DCHECK(IsAlignedParam(pre_compact_page, gPageSize));
// 初始化用于回收源空间(from-space)页面的变量。
last_reclaimed_page_ = pre_compact_page;
last_reclaimable_page_ = last_reclaimed_page_;
cur_reclaimable_page_ = last_reclaimed_page_;
last_checked_reclaim_page_idx_ = idx;
class_after_obj_iter_ = class_after_obj_map_.rbegin();
mirror::Object* next_page_first_obj = nullptr;
// --- 阶段1: 处理“纯黑页”(Allocated-black pages) ---
// 这些页面是在GC标记开始后分配的,因此页面上的所有对象都保证是存活的。
// 处理方式是最高效的“整体滑动”(Slide)。
while (idx > moving_first_objs_count_) {
idx--;
pre_compact_page -= gPageSize;
to_space_end -= gPageSize;
if (kMode == kFallbackMode) {
page = to_space_end;
}
mirror::Object* first_obj = first_objs_moving_space_[idx].AsMirrorPtr();
uint32_t first_chunk_size = black_alloc_pages_first_chunk_size_[idx];
if (first_obj != nullptr) {
// DoPageCompactionWithStateChange 封装了页面状态变更和实际的整理操作。
DoPageCompactionWithStateChange<kMode>(idx,
to_space_end,
page,
/*map_immediately=*/true,
[&]() REQUIRES_SHARED(Locks::mutator_lock_) {
// 直接将整个页面的存活内容“滑动”到新位置。
SlideBlackPage(first_obj,
next_page_first_obj,
first_chunk_size,
pre_compact_page,
page,
kMode == kCopyMode);
});
// 为了效率,不是每处理一页就回收一页,而是累积一定数量后再批量回收。
if (idx % DivideByPageSize(kMinFromSpaceMadviseSize) == 0) {
FreeFromSpacePages(idx, kMode, /*end_idx_for_mapping=*/0);
}
}
next_page_first_obj = first_obj;
}
DCHECK_EQ(pre_compact_page, black_allocations_begin_);
// 预留一个页面,用于在找不到可回收页面作为临时缓冲区时的备用方案。
uint8_t* reserve_page = page;
size_t end_idx_for_mapping = idx;
// --- 阶段2: 处理“混合页” ---
// 这些页面上既有存活对象,也有死亡对象(内存碎片)。
// 这是最典型的整理场景,需要“选择性拷贝”(Compact)。
while (idx > black_dense_end_idx) {
idx--;
to_space_end -= gPageSize;
if (kMode == kFallbackMode) {
page = to_space_end;
} else {
DCHECK_EQ(kMode, kCopyMode);
// 尝试从源空间(from-space)的已处理部分找到一个空闲页面作为临时拷贝缓冲区。
if (cur_reclaimable_page_ > last_reclaimable_page_) {
cur_reclaimable_page_ -= gPageSize;
page = cur_reclaimable_page_ + from_space_slide_diff_;
} else {
// 如果找不到,则使用预留的页面。
page = reserve_page;
}
}
mirror::Object* first_obj = first_objs_moving_space_[idx].AsMirrorPtr();
bool success = DoPageCompactionWithStateChange<kMode>(
idx,
to_space_end,
page,
/*map_immediately=*/page == reserve_page,
[&]() REQUIRES_SHARED(Locks::mutator_lock_) {
// 扫描当前页,只把存活的对象拷贝到新位置。
CompactPage(first_obj, pre_compact_offset_moving_space_[idx], page, kMode == kCopyMode);
});
// 在某些情况下,需要提前将已整理好的页面映射回内存,使其对应用线程可见。
if (kMode == kCopyMode && (!success || page == reserve_page) && end_idx_for_mapping - idx > 1) {
MapMovingSpacePages(idx + 1,
end_idx_for_mapping,
/*from_fault=*/false,
/*return_on_contention=*/true,
/*tolerate_enoent=*/false);
}
// 尝试回收源空间的已处理页面。
if (FreeFromSpacePages(idx, kMode, end_idx_for_mapping)) {
end_idx_for_mapping = idx;
}
}
// --- 阶段3: 处理“非移动页” ---
// 这些页面位于整理后存活区域的内部,因此页面上的对象不需要移动。
// 但是,这些对象内部可能引用了被移动过的对象,需要“原地更新引用”。
while (idx > 0) {
idx--;
to_space_end -= gPageSize;
mirror::Object* first_obj = first_objs_moving_space_[idx].AsMirrorPtr();
if (first_obj != nullptr) {
DoPageCompactionWithStateChange<kMode>(
idx,
to_space_end,
to_space_end + from_space_slide_diff_,
/*map_immediately=*/false,
[&]() REQUIRES_SHARED(Locks::mutator_lock_) {
// 遍历页面上的所有对象,更新它们内部的指针,使其指向移动后的新地址。
UpdateNonMovingPage(
first_obj, to_space_end, from_space_slide_diff_, moving_space_bitmap_);
if (kMode == kFallbackMode) {
memcpy(to_space_end, to_space_end + from_space_slide_diff_, gPageSize);
}
});
} else {
// 这个页面上没有任何可达对象,直接标记为已处理和已映射。
DCHECK_EQ(moving_pages_status_[idx].load(std::memory_order_relaxed),
static_cast<uint8_t>(PageState::kUnprocessed));
moving_pages_status_[idx].store(static_cast<uint8_t>(PageState::kProcessedAndMapped),
std::memory_order_release);
}
if (FreeFromSpacePages(idx, kMode, end_idx_for_mapping)) {
end_idx_for_mapping = idx;
}
}
// 最后一次映射,确保所有剩余的页面都对应用线程可见。
if (kMode == kCopyMode && end_idx_for_mapping > 0) {
MapMovingSpacePages(idx,
end_idx_for_mapping,
/*from_fault=*/false,
/*return_on_contention=*/false,
/*tolerate_enoent=*/false);
}
// 检查点:确保目标空间的指针最终回到了空间的起始位置,表示整个空间都已处理完毕。
DCHECK_EQ(to_space_end, bump_pointer_space_->Begin());
}
单页的整理操作
DoPageCompactionWithStateChange函数为单页的整理操作提供了一个线程安全的“事务”封装。它通过原子状态机来确保一个页面只被处理一次。
实际执行Compact操作的函数为参数CompactionFn
。 包括了UpdateNonMovingPage
, SlideBlackPage
,CompactPage
。
template <int kMode, typename CompactionFn>
bool MarkCompact::DoPageCompactionWithStateChange(size_t page_idx,
uint8_t* to_space_page,
uint8_t* page,
bool map_immediately,
CompactionFn func) {
uint32_t expected_state = static_cast<uint8_t>(PageState::kUnprocessed); // 期望的初始状态
uint32_t desired_state = static_cast<uint8_t>(map_immediately ? PageState::kProcessingAndMapping
: PageState::kProcessing); // 想要设置的目标状态
// 【核心并发控制】
// 使用原子CAS(比较并交换)操作尝试“声明”对该页面的处理权。
// 只有当页面的当前状态是 kUnprocessed 时,CAS才会成功。
// 使用 acquire 内存顺序,确保在CAS失败时,能看到其他线程 release 的所有内存写入。
if (kMode == kFallbackMode || moving_pages_status_[page_idx].compare_exchange_strong(
expected_state, desired_state, std::memory_order_acquire)) {
// CAS 成功,本线程获得处理权。
// 1. 执行由调用者传入的、实际的整理工作(如 SlideBlackPage 或 CompactPage)。
func();
// 2. 根据模式和参数,完成“事务提交”。
if (kMode == kCopyMode) { // 只在并发模式下需要复杂的提交逻辑。
if (map_immediately) {
// --- 路径A:立即映射 ---
// 立即调用 ioctl,使更新对所有线程可见。
CopyIoctl(to_space_page,
page,
gPageSize,
/*return_on_contention=*/false,
/*tolerate_enoent=*/false);
// 更新状态为最终的“已处理并映射”。
// relaxed即可,因为ioctl本身起到了内存屏障的作用。
moving_pages_status_[page_idx].store(static_cast<uint8_t>(PageState::kProcessedAndMapped),
std::memory_order_relaxed);
} else {
// --- 路径B:延迟映射(用于批处理)---
// 不立即映射,而是将源页面的信息和新状态打包存储,待后续统一处理。
DCHECK(from_space_map_.HasAddress(page));
// 计算源页面的地址偏移。
uint32_t store_val = page - from_space_begin_;
DCHECK_EQ(store_val & kPageStateMask, 0u);
// 将状态 kProcessed 打包到低位。
store_val |= static_cast<uint8_t>(PageState::kProcessed);
// 将打包后的值存回状态数组。
// 【关键】必须使用 release 内存顺序!
// 这确保 func() 中的所有内存写入,对于任何之后读到这个新状态的线程都是可见的。
moving_pages_status_[page_idx].store(store_val, std::memory_order_release);
}
}
return true; // 表示本线程成功处理了此页面。
} else {
// CAS失败,意味着其他线程已经处理或正在处理此页面,本线程放弃操作。
DCHECK_NE(expected_state, static_cast<uint8_t>(PageState::kProcessed));
return false; // 表示本线程未处理此页面。
}
}
UpdateNonMovingPage函数
UpdateNonMovingPage
函数负责更新一个“非移动页面”上所有存活对象的内部引用。对象本身不移动,但它们指向其他已移动对象的指针需要被修正。主要挑战在于正确处理跨越页面边界的对象。
RefsUpdateVisitor
这是一个“访问者”对象。它的内部逻辑封装了旧地址 -> 新地址的转换规则。它会查询一个映射表(info_map_
),找到被移动对象的转发地址。
void MarkCompact::UpdateNonMovingPage(mirror::Object* first,
uint8_t* page,
ptrdiff_t from_space_diff,
accounting::ContinuousSpaceBitmap* bitmap) {
DCHECK_LT(reinterpret_cast<uint8_t*>(first), page + gPageSize);
mirror::Object* curr_obj = first; // 当前正在处理的对象(位于to-space)。
uint8_t* from_page = page + from_space_diff; // 当前页面在from-space的对应地址。
uint8_t* from_page_end = from_page + gPageSize;
// 使用标记位图来遍历当前页面上所有的“存活”对象。
// VisitMarkedRange 会为每个存活对象(的起始点)调用一次lambda。
bitmap->VisitMarkedRange(
// 从页面上第一个对象的头部之后开始扫描。
reinterpret_cast<uintptr_t>(first) + mirror::kObjectHeaderSize,
reinterpret_cast<uintptr_t>(page + gPageSize), // 扫描直到页面末尾。
[&](mirror::Object* next_obj) {
// from_obj 指向对象在整理前的原始位置,我们需要从这里读取旧的引用信息。
mirror::Object* from_obj = reinterpret_cast<mirror::Object*>(
reinterpret_cast<uint8_t*>(curr_obj) + from_space_diff);
// --- 处理跨页对象的核心逻辑 ---
if (reinterpret_cast<uint8_t*>(curr_obj) < page) {
// 情况1:当前对象是从前一个页面延伸过来的。
// 创建一个引用更新访问器,它负责具体的“旧地址 -> 新地址”转换和写入操作。
RefsUpdateVisitor</*kCheckBegin*/ true, /*kCheckEnd*/ false> visitor(
this, from_obj, from_page, from_page_end);
// 计算在当前页面内的起始扫描偏移量。
MemberOffset begin_offset(page - reinterpret_cast<uint8_t*>(curr_obj));
// 调用对象的访问方法,遍历其在当前页面内的所有引用字段。
// 因为对象头部在前一页,其NativeRoots已处理过,此处不再访问。
from_obj->VisitRefsForCompaction</*kFetchObjSize*/ false, /*kVisitNativeRoots*/ false>(
visitor, begin_offset, MemberOffset(-1));
} else {
// 情况2:当前对象完整地起始于本页面内。
RefsUpdateVisitor</*kCheckBegin*/ false, /*kCheckEnd*/ false> visitor(
this, from_obj, from_page, from_page_end);
// 从对象的0偏移处开始扫描所有引用。
from_obj->VisitRefsForCompaction</*kFetchObjSize*/ false>(
visitor, MemberOffset(0), MemberOffset(-1));
}
// 移动到下一个由VisitMarkedRange找到的存活对象。
curr_obj = next_obj;
});
// VisitMarkedRange的lambda处理了除最后一个对象外的所有对象。
// 这里需要单独处理页面上的最后一个存活对象,因为它可能延伸到下一页。
mirror::Object* from_obj =
reinterpret_cast<mirror::Object*>(reinterpret_cast<uint8_t*>(curr_obj) + from_space_diff);
// 计算结束扫描的偏移量,确保不超过页面边界。
MemberOffset end_offset(page + gPageSize - reinterpret_cast<uint8_t*>(curr_obj));
if (reinterpret_cast<uint8_t*>(curr_obj) < page) {
// 情况A:最后一个对象也是从前一页延伸过来的。
RefsUpdateVisitor</*kCheckBegin*/ true, /*kCheckEnd*/ true> visitor(
this, from_obj, from_page, from_page_end);
from_obj->VisitRefsForCompaction</*kFetchObjSize*/ false, /*kVisitNativeRoots*/ false>(
visitor, MemberOffset(page - reinterpret_cast<uint8_t*>(curr_obj)), end_offset);
} else {
// 情况B:最后一个对象起始于本页面内。
RefsUpdateVisitor</*kCheckBegin*/ false, /*kCheckEnd*/ true> visitor(
this, from_obj, from_page, from_page_end);
from_obj->VisitRefsForCompaction</*kFetchObjSize*/ false>(visitor, MemberOffset(0), end_offset);
}
}
CompactPage函数
-
阶段一:拷贝所有存活的数据条带
VisitLiveStrides
。此函数不关心对象的边界,而是使用live_words_bitmap_
。live_words_bitmap_->VisitLiveStrides(...)
会扫描位图,找出所有连续的、被标记为存活的内存区间,称之为“条带”(Stride)。一个条带可能包含多个小对象,也可能只是一个大对象的一部分。在VisitLiveStrides
的回调中,函数对找到的每一个条带执行一次memcpy
,将这块连续的存活数据从from_space_
拷贝到目标地址addr
。这种方式比逐个对象拷贝要高效得多。 -
阶段二:修复拷贝后数据中的指针: 此时,所有存活数据都已紧凑地排列在
start_addr
开始的内存区域里,但内部指针全是错的。接下来的代码就是遍历这块新内存,逐个对象进行修复。
- Part A: 处理第一个对象(可能跨页而来): 页面上的第一个存活数据块,可能只是一个从前一页延伸过来的大对象的“尾巴”。代码首先处理这个特殊情况。它通过
offset
参数计算出这个对象在当前页内的起始偏移offset_within_obj
,然后调用VisitRefsForCompaction
只修复这个对象落在当前页内的那部分引用。 - Part B: 处理中间的完整对象: 第一个 while 循环负责处理后续的、可以确定是完整位于新拷贝数据区域内部的对象。它通过
ref->VisitRefsForCompaction
修复对象的引用,获取该对象的实际大小obj_size
,循环可以准确地“跳”到下一个对象的起始位置 (bytes_done += obj_size
),从而完成遍历。这个阶段的边界检查较少,效率更高。 - Part C: 处理最后一个条带中的对象: 第二个 while 循环专门处理最后一个数据条带中的对象。情况更复杂,因为最后一个对象可能会延伸到当前页的范围之外。因此,在调用
VisitRefsForCompaction
时,需要传入页面末尾的边界,并启用kCheckEnd
模板参数,进行更严格的边界检查。
void MarkCompact::CompactPage(mirror::Object* obj,
uint32_t offset,
uint8_t* addr,
bool needs_memset_zero) {
// 本函数是“混合页面”的整理核心。它分两步:
// 1. 使用 live_words_bitmap_ 将所有存活的内存“条带”(strides) 快速 memcpy 到目标地址。
// 2. 遍历拷贝后的紧凑数据,识别出对象,并逐个修复其内部的引用指针。
// ... 一系列调试断言 ...
uint8_t* const start_addr = addr; // 记录目标区域的起始地址。
size_t stride_count = 0;
// ...
obj = GetFromSpaceAddr(obj);
// --- 阶段一:拷贝所有存活的数据“条带”(Live Strides) ---
// 这是“粗活”,只管搬数据,不管内容是什么。
live_words_bitmap_->VisitLiveStrides(
offset,
black_allocations_begin_,
gPageSize,
[&](uint32_t stride_begin, size_t stride_size, [[maybe_unused]] bool is_last)
REQUIRES_SHARED(Locks::mutator_lock_) {
const size_t stride_in_bytes = stride_size * kAlignment;
// ...
// 从 from_space_ 拷贝连续的存活数据到目标地址 addr。
memcpy(addr, from_space_begin_ + stride_begin * kAlignment, stride_in_bytes);
// ... (Debug build 的校验代码) ...
last_stride = addr;
addr += stride_in_bytes; // 移动目标地址指针。
stride_count++;
});
// ... 调试断言 ...
// --- 阶段二:修复指针 ---
// 这是“细活”,遍历刚拷贝过来的数据,更新里面的指针。
size_t obj_size = 0;
// 计算页面上第一个对象在页面内的偏移量。
uint32_t offset_within_obj = offset * kAlignment
- (reinterpret_cast<uint8_t*>(obj) - from_space_begin_);
// Part 2a: 处理第一个对象(可能从前一页延伸而来)。
if (offset_within_obj > 0) {
mirror::Object* to_ref = reinterpret_cast<mirror::Object*>(start_addr - offset_within_obj);
if (stride_count > 1) {
// 如果有多个条带,说明对象不会延伸出页面。
RefsUpdateVisitor</*kCheckBegin*/true, /*kCheckEnd*/false> visitor(this, to_ref, start_addr, nullptr);
obj_size = obj->VisitRefsForCompaction</*kFetchObjSize*/true, /*kVisitNativeRoots*/false>(
visitor, MemberOffset(offset_within_obj), MemberOffset(-1));
} else {
// 只有一个条带,对象可能延伸出页面,需要进行边界检查。
RefsUpdateVisitor</*kCheckBegin*/true, /*kCheckEnd*/true> visitor(this, to_ref, start_addr, start_addr + gPageSize);
obj_size = obj->VisitRefsForCompaction</*kFetchObjSize*/true, /*kVisitNativeRoots*/false>(
visitor, MemberOffset(offset_within_obj), MemberOffset(offset_within_obj + gPageSize));
}
obj_size = RoundUp(obj_size, kAlignment);
// ... 调试断言 ...
obj_size -= offset_within_obj; // 计算对象在本页内的剩余大小。
// ...
}
uint8_t* const end_addr = addr; // 记录拷贝数据的末尾地址。
addr = start_addr;
size_t bytes_done = obj_size; // bytes_done 记录已处理(修复完指针)的字节数。
// Part 2b: 处理中间的、完整的对象(位于除最后一个条带外的区域)。
// 这个循环的边界检查较少,效率较高。
DCHECK_LE(addr, last_stride);
size_t bytes_to_visit = last_stride - addr;
while (bytes_to_visit > bytes_done) {
mirror::Object* ref = reinterpret_cast<mirror::Object*>(addr + bytes_done);
// ...
RefsUpdateVisitor</*kCheckBegin*/false, /*kCheckEnd*/false>
visitor(this, ref, nullptr, nullptr);
// 修复引用,并获取对象大小,以便跳到下一个对象。
obj_size = ref->VisitRefsForCompaction(visitor, MemberOffset(0), MemberOffset(-1));
obj_size = RoundUp(obj_size, kAlignment);
bytes_done += obj_size;
}
// Part 2c: 处理最后一个条带中的对象(需要进行严格的边界检查)。
uint8_t* from_addr = from_space_begin_ + last_stride_begin * kAlignment;
bytes_to_visit = end_addr - addr;
while (bytes_to_visit > bytes_done) {
mirror::Object* ref = reinterpret_cast<mirror::Object*>(addr + bytes_done);
obj = reinterpret_cast<mirror::Object*>(from_addr);
// ...
// 此处的 Visitor 启用了末尾边界检查(kCheckEnd=true)。
RefsUpdateVisitor</*kCheckBegin*/false, /*kCheckEnd*/true>
visitor(this, ref, nullptr, start_addr + gPageSize);
obj_size = obj->VisitRefsForCompaction(visitor,
MemberOffset(0),
MemberOffset(end_addr - (addr + bytes_done)));
obj_size = RoundUp(obj_size, kAlignment);
// ... 调试断言 ...
from_addr += obj_size;
bytes_done += obj_size;
}
// 如果需要,将页面末尾的松弛空间清零。
if (needs_memset_zero && UNLIKELY(bytes_done < gPageSize)) {
std::memset(addr + bytes_done, 0x0, gPageSize - bytes_done);
}
}
SlideBlackPage函数
SlideBlackPage 是一个高度优化的页面整理函数,专门用于处理纯黑页(Pure-Black Pages)。
“纯黑页”是指在 GC 标记阶段开始后新分配的内存页,因此 ART 虚拟机可以保证该页面上的所有对象都是存活的(即“黑色的”)。由于没有死亡对象,页面上不存在需要消除的内存碎片。
因此,这个函数的目标不是像 CompactPage 那样去“压缩”页面,而是更简单、更高效地滑动(Slide) 整个页面的有效内容:
-
整体拷贝:将页面上连续的、存活的对象块作为一个整体,通过 memcpy 快速拷贝到新的目标位置。
-
指针修复:在整体拷贝完成后,遍历新位置上的这些对象,并更新它们内部的引用指针。
void MarkCompact::SlideBlackPage(mirror::Object* first_obj,
mirror::Object* next_page_first_obj,
uint32_t first_chunk_size,
uint8_t* const pre_compact_page,
uint8_t* dest,
bool needs_memset_zero) {
// 本函数是一个优化,用于高效处理“纯黑页”(页面上所有对象都存活)。
// 它将页面上连续的活对象块作为一个整体“滑动”(memcpy)到新位置,然后修复内部指针。
// ... 调试断言和变量初始化 ...
uint8_t* src_addr = reinterpret_cast<uint8_t*>(GetFromSpaceAddr(first_obj));
uint8_t* pre_compact_addr = reinterpret_cast<uint8_t*>(first_obj);
// ...
// 如果页面起始处有空白(因为第一个对象是从前一页延伸过来的),则先处理这部分空白。
if (pre_compact_addr > pre_compact_page) {
bytes_copied = pre_compact_addr - pre_compact_page;
if (needs_memset_zero) {
std::memset(dest, 0x0, bytes_copied);
}
dest += bytes_copied;
} else {
// ... 处理对象起始地址在页面之前的情况 ...
}
// --- 步骤1:拷贝第一个已知的连续存活对象块 ---
std::memcpy(dest, src_addr, first_chunk_size);
// --- 步骤2:修复刚拷贝的这个块中的指针 ---
{
size_t bytes_to_visit = first_chunk_size;
size_t obj_size;
// Part 2a: 特殊处理第一个对象,因为它可能从前一页延伸而来,需要计算偏移。
size_t offset = pre_compact_addr - reinterpret_cast<uint8_t*>(first_obj);
if (bytes_copied == 0 && offset > 0) {
mirror::Object* to_obj = reinterpret_cast<mirror::Object*>(dest - offset);
mirror::Object* from_obj = reinterpret_cast<mirror::Object*>(src_addr - offset);
// 根据下一个页面的起始对象位置,决定是否需要进行末尾边界检查。
if (next_page_first_obj == nullptr || ...) {
RefsUpdateVisitor</*kCheckBegin*/true, /*kCheckEnd*/false> visitor(...);
obj_size = from_obj->VisitRefsForCompaction</*kFetchObjSize*/true, ...>(visitor, ...);
} else {
RefsUpdateVisitor</*kCheckBegin*/true, /*kCheckEnd*/true> visitor(...);
obj_size = from_obj->VisitRefsForCompaction</*kFetchObjSize*/true, ...>(visitor, ...);
if (first_obj == next_page_first_obj) {
// 如果此对象是本页唯一的对象,修复完直接返回。
return;
}
}
obj_size = RoundUp(obj_size, kAlignment);
obj_size -= offset; // 计算对象在本页内的剩余大小。
dest += obj_size;
bytes_to_visit -= obj_size;
}
bytes_copied += first_chunk_size;
// ... 预处理最后一个对象,判断是否需要边界检查 ...
bool check_last_obj = false;
if (next_page_first_obj != nullptr && ...) {
// ...
check_last_obj = true;
}
// Part 2b: 遍历并修复第一个块中的其余对象。
while (bytes_to_visit > 0) {
mirror::Object* dest_obj = reinterpret_cast<mirror::Object*>(dest);
RefsUpdateVisitor</*kCheckBegin*/false, /*kCheckEnd*/false> visitor(...);
// 修复引用,并获取对象大小,以便跳到下一个对象。
obj_size = dest_obj->VisitRefsForCompaction(visitor, ...);
obj_size = RoundUp(obj_size, kAlignment);
bytes_to_visit -= obj_size;
dest += obj_size;
}
DCHECK_EQ(bytes_to_visit, 0u);
// Part 2c: 如果需要,对最后一个对象进行带边界检查的修复。
if (check_last_obj) {
// ... (对最后一个跨页对象的特殊处理) ...
return;
}
}
// --- 步骤3:处理页面上可能存在的、后续的存活对象块(例如,新的TLAB)---
if (bytes_copied < gPageSize) {
src_addr += first_chunk_size;
pre_compact_addr += first_chunk_size;
// 使用标记位图在页面剩余部分寻找下一个活对象的起始点。
uintptr_t start_visit = reinterpret_cast<uintptr_t>(pre_compact_addr);
uintptr_t page_end = reinterpret_cast<uintptr_t>(pre_compact_page_end);
mirror::Object* found_obj = nullptr;
moving_space_bitmap_->VisitMarkedRange</*kVisitOnce*/true>(start_visit, page_end, ...);
size_t remaining_bytes = gPageSize - bytes_copied;
if (found_obj == nullptr) {
// 页面剩余部分没有活对象了,清零并返回。
if (needs_memset_zero) {
std::memset(dest, 0x0, remaining_bytes);
}
return;
}
// Part 3a: 拷贝页面剩余的所有内容(包括对象和之间可能的小间隙)。
std::memcpy(dest, src_addr, remaining_bytes);
// Part 3b: 遍历新拷贝的区域,修复所有活对象的指针。
moving_space_bitmap_->VisitMarkedRange(..., [&](mirror::Object* obj) {
// ... (找到对象在新位置的地址 ref)
RefsUpdateVisitor</*kCheckBegin*/false, /*kCheckEnd*/false> visitor(...);
ref->VisitRefsForCompaction</*kFetchObjSize*/false>(visitor, ...);
// ...
});
// 对最后一个找到的对象进行带边界检查的修复。
// ...
}
}
ProcessLinearAlloc
ProcessLinearAlloc
函数安全地更新非移动对象内部的指针,确保它们指向移动后的新地址。
LinearAlloc
里的对象自身不动,但它们可能包含指向普通 Java 对象(在 MovingSpace
中)的字段。当 MovingSpace
中的对象被 CompactMovingSpace
移动后,这些字段里的指针就变成了无效的旧地址。
修复机制:影子空间 (Shadow Space) + 原子替换
遍历与更新: 函数遍历所有 LinearAlloc 的内存区域 (arena)。对于每个区域,它会逐个检查里面的对象,并更新所有指向被移动对象的指针。
写入影子空间: 更新操作不是在原始内存上直接进行的。而是将修改后的对象写入一个临时的、地址平行的影子空间 (shadow_)。这样做是为了避免在更新过程中,应用线程读到不一致的数据(一个对象里有些指针更新了,有些还没更新)。
原子替换: 当一个或多个页面的影子空间都准备好后,函数会调用 MapUpdatedLinearAllocPages
。这个函数利用 userfaultfd
机制,原子性地将这些更新过的影子页面映射到原始地址上。对于应用线程来说,这个地址上的内容仿佛在“一瞬间”就从旧版本变成了新版本,从而保证了数据的一致性和线程安全。
void MarkCompact::ProcessLinearAlloc() {
GcVisitedArenaPool* arena_pool =
static_cast<GcVisitedArenaPool*>(Runtime::Current()->GetLinearAllocArenaPool());
// 调试检查:确保当前线程是执行GC的线程。
DCHECK_EQ(thread_running_gc_, Thread::Current());
// 用于追踪连续的、待映射的内存范围。
uint8_t* unmapped_range_start = nullptr;
uint8_t* unmapped_range_end = nullptr;
// 指向当前正在处理的 LinearAlloc 空间元数据。
LinearAllocSpaceData* space_data = nullptr;
// 指向当前空间的状态数组,用于线程同步。
Atomic<PageState>* state_arr = nullptr;
// 原始空间与其影子空间之间的地址偏移量。
ptrdiff_t diff = 0;
// 定义一个lambda函数,负责将更新过的影子页面映射回原始地址。
auto map_pages = [&]() {
// 一系列健全性检查。
DCHECK_NE(diff, 0);
DCHECK_NE(space_data, nullptr);
DCHECK_GE(unmapped_range_start, space_data->begin_);
// ...
DCHECK_ALIGNED_PARAM(unmapped_range_end - unmapped_range_start, gPageSize);
size_t page_idx = DivideByPageSize(unmapped_range_start - space_data->begin_);
// 调用核心函数,执行内存映射,将 unmapped_range_start + diff (影子空间) 的内容
// 映射到 unmapped_range_start (原始空间)。
// MapUpdatedLinearAllocPages函数它通过 userfaultfd 的 UFFDIO_COPY 命令,请求 Linux 内核执行内存页的替换。
// 内核会把 start_shadow_page 指向的物理内存,直接映射到 start_page 指向的虚拟地址上。
MapUpdatedLinearAllocPages(unmapped_range_start,
unmapped_range_start + diff,
state_arr + page_idx,
unmapped_range_end - unmapped_range_start,
/*free_pages=*/true,
/*single_ioctl=*/false,
/*tolerate_enoent=*/false);
};
// 遍历所有需要处理的内存块(Arenas)。
for (auto& pair : linear_alloc_arenas_) {
const TrackedArena* arena = pair.first;
size_t arena_size = arena->Size();
uint8_t* arena_begin = arena->Begin();
// arenas是按地址排序的,这里确保我们是顺序处理。
DCHECK_LE(unmapped_range_end, arena_begin);
DCHECK(space_data == nullptr || arena_begin > space_data->begin_)
<< "space-begin:" << static_cast<void*>(space_data->begin_)
<< " arena-begin:" << static_cast<void*>(arena_begin);
// 如果当前arena属于一个新的LinearAlloc空间(或者这是第一个arena)
if (space_data == nullptr || space_data->end_ <= arena_begin) {
// 切换空间之前,先把上一个空间里已处理的页面进行映射。
if (space_data != nullptr && unmapped_range_end != nullptr) {
map_pages();
unmapped_range_end = nullptr;
}
// 找到当前arena所属的LinearAlloc空间,并设置好space_data, diff等变量。
LinearAllocSpaceData* curr_space_data = space_data;
for (auto& data : linear_alloc_spaces_data_) {
if (data.begin_ <= arena_begin && arena_begin < data.end_) {
// ...
diff = data.shadow_.Begin() - data.begin_; // 计算与影子空间的偏移
state_arr = reinterpret_cast<Atomic<PageState>*>(data.page_status_map_.Begin());
space_data = &data;
break;
}
}
CHECK_NE(space_data, curr_space_data)
<< "Couldn't find space for arena-begin:" << static_cast<void*>(arena_begin);
}
// 如果在同一个空间内发现了不连续的arena(有空洞),则先把前面的部分映射掉。
if (unmapped_range_end != nullptr && unmapped_range_end < arena_begin) {
map_pages();
unmapped_range_end = nullptr;
}
// 开始一个新的连续处理范围。
if (unmapped_range_end == nullptr) {
unmapped_range_start = unmapped_range_end = arena_begin;
}
// 扩大当前待处理的范围,将当前arena包含进来。
unmapped_range_end += arena_size;
{
// 加读锁,防止在更新arena时,它被其他线程释放。
ReaderMutexLock rmu(thread_running_gc_, arena_pool->GetLock());
// 如果arena在GC暂停后已被标记为待删除,则跳过。
if (arena->IsWaitingForDeletion()) {
continue;
}
uint8_t* last_byte = pair.second;
DCHECK_ALIGNED_PARAM(last_byte, gPageSize);
// 定义一个“访问者”lambda,用于处理arena中的每一页。
auto visitor = [space_data, last_byte, diff, this, state_arr](
uint8_t* page_begin, // 当前页的起始地址(在原始空间)
uint8_t* first_obj, // 页面上第一个对象的地址
size_t page_size) REQUIRES_SHARED(Locks::mutator_lock_) {
// 如果页面已经超出了需要更新的范围,则直接返回。
if (page_begin >= last_byte) {
return;
}
LinearAllocPageUpdater updater(this);
size_t page_idx = DivideByPageSize(page_begin - space_data->begin_);
DCHECK_LT(page_idx, space_data->page_status_map_.Size());
PageState expected_state = PageState::kUnprocessed;
// 使用原子操作CAS(比较并交换)来确保每个页面只被处理一次。
// 将页面状态从 kUnprocessed 切换到 kProcessing。
if (state_arr[page_idx].compare_exchange_strong(
expected_state, PageState::kProcessing, std::memory_order_acquire)) {
// 根据页面类型(多对象或单对象),调用updater来更新指针。
// 更新操作是写入到影子空间中 (page_begin + diff)。
if (first_obj != nullptr) {
updater.MultiObjectArena(page_begin + diff, first_obj + diff);
} else {
updater.SingleObjectArena(page_begin + diff, page_size);
}
expected_state = PageState::kProcessing;
// 使用原子store操作,将页面状态更新为 kProcessed 或 kProcessedAndMapped。
// release内存顺序确保了对影子页面的所有写入都已完成。
if (updater.WasLastPageTouched()) {
state_arr[page_idx].store(PageState::kProcessed, std::memory_order_release);
} else {
// 如果页面未被触碰(没有需要更新的指针),则直接用0填充并标记为已映射。
ZeropageIoctl(
page_begin, gPageSize, /*tolerate_eexist=*/false, /*tolerate_enoent=*/false);
state_arr[page_idx].store(PageState::kProcessedAndMapped, std::memory_order_release);
}
}
};
// 调用arena的VisitRoots方法,它会用上面定义的visitor来遍历arena中的所有对象和页面。
arena->VisitRoots(visitor);
}
}
// 循环结束后,处理最后一批待映射的页面。
if (unmapped_range_end > unmapped_range_start) {
map_pages();
}
}
MapUpdatedLinearAllocPages
通过 userfaultfd
的 ioctl(UFFDIO_COPY)
命令,将 start_shadow_page (影子空间) 的内容原子性地映射到 start_page (原始空间) 的虚拟地址上。
bool MarkCompact::MapUpdatedLinearAllocPages(uint8_t* start_page,
uint8_t* start_shadow_page,
Atomic<PageState>* state,
size_t length,
bool free_pages,
bool single_ioctl,
bool tolerate_enoent) {
DCHECK_ALIGNED_PARAM(length, gPageSize);
// 用于最后 madvise 释放内存的指针和变量。
Atomic<PageState>* madv_state = state;
size_t madv_len = length;
uint8_t* madv_start = start_shadow_page;
// 标记是否在处理过程中遇到了并发操作,如果是,则在释放内存前需要额外等待。
bool check_state_for_madv = false;
uint8_t* end_page = start_page + length;
// 主循环,批量处理待映射的页面。
while (start_page < end_page) {
size_t map_len = 0;
// 步骤1:寻找一个连续的、状态为 kProcessed 的页面范围,以便一次性映射。
// kProcessed 意味着GC线程已将更新内容写入影子页,只待映射。
for (Atomic<PageState>* cur_state = state;
map_len < length && cur_state->load(std::memory_order_acquire) == PageState::kProcessed;
map_len += gPageSize, cur_state++) {
// 循环体为空,仅用于计算连续的长度 map_len。
}
if (map_len == 0) {
// 如果找不到任何可立即映射的页面。
if (single_ioctl) {
// 如果只允许一次ioctl,则检查当前页面是否已映射并返回。
return state->load(std::memory_order_relaxed) == PageState::kProcessedAndMapped;
}
// 跳过所有非 kProcessed 的页面 (如 kUnprocessed, kProcessing, kProcessedAndMapped)。
while (length > 0) {
PageState s = state->load(std::memory_order_relaxed);
if (s == PageState::kProcessed) {
// 找到了下一个可以开始处理的页面,退出内层循环。
break;
}
// 如果发现页面正被其他线程(mutator)处理,则设置标志位。
check_state_for_madv |= s > PageState::kUnprocessed && s < PageState::kProcessedAndMapped;
// 移动指针,继续寻找。
state++;
length -= gPageSize;
start_shadow_page += gPageSize;
start_page += gPageSize;
}
} else {
// 步骤2:【核心操作】对找到的连续页面范围执行 ioctl 映射。
map_len = CopyIoctl(start_page,
start_shadow_page,
map_len,
/*return_on_contention=*/false,
tolerate_enoent);
DCHECK_NE(map_len, 0u);
// 步骤3:映射成功后,将这些页面的状态更新为 kProcessedAndMapped,
// 通知其他线程它们已可用。'release'内存顺序确保状态更新前的所有操作都已完成。
for (size_t l = 0; l < map_len; l += gPageSize, state++) {
PageState s = state->load(std::memory_order_relaxed);
DCHECK(s == PageState::kProcessed || s == PageState::kProcessedAndMapped) << "state:" << s;
state->store(PageState::kProcessedAndMapped, std::memory_order_release);
}
if (single_ioctl) {
break;
}
// 更新指针,准备处理下一批页面。
start_page += map_len;
start_shadow_page += map_len;
length -= map_len;
}
}
// --- 清理阶段:回收影子页面的物理内存 ---
if (free_pages) {
if (check_state_for_madv) {
// 如果之前检测到有并发操作,必须在此等待,确保所有页面都已映射完毕,才能安全释放。
// 这是为了防止在mutator还在使用影子页时,GC线程就将其释放(Use-After-Free)。
for (size_t l = 0; l < madv_len; l += gPageSize, madv_state++) {
uint32_t backoff_count = 0;
PageState s = madv_state->load(std::memory_order_relaxed);
while (s > PageState::kUnprocessed && s < PageState::kProcessedAndMapped) {
BackOff(backoff_count++);
s = madv_state->load(std::memory_order_relaxed);
}
}
}
// 调用 madvise(MADV_DONTNEED) 将影子页面的物理内存归还给操作系统。
ZeroAndReleaseMemory(madv_start, madv_len);
}
return true;
}
CopyIoctl
是 userfaultfd UFFDIO_COPY ioctl
系统调用的底层封装。
size_t MarkCompact::CopyIoctl(
void* dst, void* buffer, size_t length, bool return_on_contention, bool tolerate_enoent) {
int32_t backoff_count = -1;
int32_t max_backoff = 10;
// 准备传递给内核的 ioctl 参数结构体。
struct uffdio_copy uffd_copy;
// 【关键】设置模式为 MMAP_TRYLOCK。这是一种非阻塞的乐观尝试。
// 如果内核需要的 mmap_lock 被其他线程持有,ioctl 会立即返回 EAGAIN 而不是等待。
uffd_copy.mode = gUffdSupportsMmapTrylock ? UFFDIO_COPY_MODE_MMAP_TRYLOCK : 0;
uffd_copy.src = reinterpret_cast<uintptr_t>(buffer);
uffd_copy.dst = reinterpret_cast<uintptr_t>(dst);
uffd_copy.len = length;
uffd_copy.copy = 0; // copy 是一个出参,内核会通过它返回成功处理的字节数。
// 使用循环来处理可能发生的瞬时失败(如竞争)。
while (true) {
// 【核心系统调用】请求内核执行页面拷贝/重映射操作。
int ret = ioctl(uffd_, UFFDIO_COPY, &uffd_copy);
if (ret == 0) {
// 成功:所有页面都已成功映射。
DCHECK_EQ(uffd_copy.copy, static_cast<ssize_t>(length));
break;
} else if (errno == EAGAIN) {
// 失败原因:EAGAIN,表示有竞争(Contention)。
DCHECK_NE(uffd_copy.copy, 0);
if (uffd_copy.copy > 0) {
// 部分成功:内核在处理了 uffd_copy.copy 字节后遇到了竞争。接受这个结果。
DCHECK(IsAlignedParam(uffd_copy.copy, gPageSize));
break;
} else {
// 完全失败:内核在开始前就因为无法获取 mmap_lock 而失败。
DCHECK_EQ(uffd_copy.copy, -EAGAIN);
uffd_copy.copy = 0;
if (return_on_contention) {
// 如果调用者要求在遇到竞争时立即返回,则照做。
break;
}
}
// --- 退避(Backoff)和策略升级 ---
if (backoff_count == -1) {
// 根据线程优先级动态调整最大退避次数。
int prio = Thread::Current()->GetNativePriority();
max_backoff -= prio;
backoff_count = 0;
}
if (backoff_count < max_backoff) {
// 先进行几次短暂的等待和重试。
BackOff</*kYieldMax=*/3, /*kSleepUs=*/1000>(backoff_count++);
} else {
// 如果多次重试仍然失败,则升级策略,关闭 TRYLOCK 模式。
// 下一次 ioctl 将会是阻塞调用,会一直等待直到锁被释放。
uffd_copy.mode = 0;
}
} else if (errno == EEXIST) {
// 失败原因:EEXIST,目标页面已被映射。
// 这是一个正常的并发场景,可能是一个mutator线程抢先完成了映射。
DCHECK_NE(uffd_copy.copy, 0);
if (uffd_copy.copy < 0) {
uffd_copy.copy = 0;
}
// 内核返回了遇到EEXIST之前的字节数,我们需要手动加上这个已存在的页面大小。
uffd_copy.copy += gPageSize;
break;
} else {
// 其他失败情况。
// 通常是 ENOENT,表示 userfaultfd 注册已失效(例如GC已进入FinishPhase)。
CHECK(tolerate_enoent && errno == ENOENT)
<< "ioctl_userfaultfd: copy failed: " << strerror(errno);
return uffd_copy.copy > 0 ? uffd_copy.copy : 0;
}
}
// 返回最终成功映射的字节数。
return uffd_copy.copy;
}
Phase 6: FinishPhase
- 结束阶段
总而言之,FinishPhase
函数是一个大扫除函数。它不做复杂的 GC 逻辑,而是专注于清理、重置和回收资源。其核心目标是:
- 释放内存:将 GC 过程中占用的所有临时内存归还给系统。
- 恢复状态:确保堆和分配器处于一个正确的、可用的新状态。
- 准备未来:重置所有 GC 相关的数据结构,为下一次垃圾回收做好准备。
void MarkCompact::FinishPhase() {
// 记录本次GC扫描过的字节数,用于统计和分析。
GetCurrentIteration()->SetScannedBytes(bytes_scanned_);
// 检查当前进程是否是Zygote进程。Zygote是Android中用于启动应用的特殊进程。
bool is_zygote = Runtime::Current()->IsZygote();
// 重置状态标志位,表示整理阶段和标记阶段都已经完成。
compacting_ = false;
marking_done_ = false;
// 释放用于对象整理(Compaction)的临时缓冲区内存。
ZeroAndReleaseMemory(compaction_buffers_map_.Begin(), compaction_buffers_map_.Size());
// 释放用于存储对象信息的映射表(如对象移动后的新地址)。
// MadviseDontNeed是一个给操作系统的提示,表明这块内存暂时不再需要,可以回收。
info_map_.MadviseDontNeedAndZero();
// 清除用于标记存活“字”(word)的位图。
live_words_bitmap_->ClearBitmap();
// 清理“可移动空间”(moving space)的位图。
// black_dense_end_ 指向的是所有存活对象整理后末尾的位置。
if (moving_space_begin_ == black_dense_end_) {
// 如果没有存活对象被移动,则清空整个位图。
moving_space_bitmap_->Clear();
} else {
// 否则,只清除从存活对象末尾到移动空间末尾的这部分区域的位图标记,
// 因为这部分现在是空闲区域了。
DCHECK_LT(moving_space_begin_, black_dense_end_);
DCHECK_LE(black_dense_end_, moving_space_end_);
moving_space_bitmap_->ClearRange(reinterpret_cast<mirror::Object*>(black_dense_end_),
reinterpret_cast<mirror::Object*>(moving_space_end_));
}
// 更新“指针碰撞”(Bump Pointer)分配器的状态。
// 告诉分配器,所有存活对象占据的“黑色密集区域”的大小,新的对象分配将从这个区域之后开始。
bump_pointer_space_->SetBlackDenseRegionSize(black_dense_end_ - moving_space_begin_);
// Zygote进程的特殊处理。
if (UNLIKELY(is_zygote && IsValidFd(uffd_))) {
// 如果是Zygote进程且使用了userfaultfd机制(用于高效的并发整理),
// 则关闭相关的文件描述符。关闭操作会附带注销所有已注册的内存范围。
close(uffd_);
uffd_ = kFdUnused;
uffd_initialized_ = false;
}
// 确保标记栈(mark stack)在GC结束时是空的,这表明所有可达对象都已被正确处理。
CHECK(mark_stack_->IsEmpty());
// 重置标记栈,为下一次GC做准备。
mark_stack_->Reset();
// 调试检查:确保执行收尾阶段的线程就是启动GC的线程。
DCHECK_EQ(thread_running_gc_, Thread::Current());
// 在Debug构建模式下,清空用于记录根(root)更新的列表。
if (kIsDebugBuild) {
MutexLock mu(thread_running_gc_, lock_);
if (updated_roots_.get() != nullptr) {
updated_roots_->clear();
}
}
// 清理GC期间使用的其他临时数据结构。
class_after_obj_map_.clear();
linear_alloc_arenas_.clear();
{
// 加锁,以线程安全的方式清除堆上所有对象的标记位。
ReaderMutexLock mu(thread_running_gc_, *Locks::mutator_lock_);
WriterMutexLock mu2(thread_running_gc_, *Locks::heap_bitmap_lock_);
// 清除所有对象的标记位,为下一次GC的标记阶段做准备。
heap_->ClearMarkedObjects();
}
// 获取GC专用的内存池。
GcVisitedArenaPool* arena_pool =
static_cast<GcVisitedArenaPool*>(Runtime::Current()->GetLinearAllocArenaPool());
// 删除内存池中不再使用的内存块(Arenas),以减少GC自身的内存占用。
arena_pool->DeleteUnusedArenas();
}