背景
我们很多时候也背过GC相关的八股文,比如常见GC机制,标记清除,复制删除,新生老年代等等。今天笔者遇到一个case就是跟GC相关,当然,这里也不打算教大家八股文,只是希望如果大家遇到相似的问题的时候,可以从中学习到相关知识,丰富自己的细节。
并行拷贝机制, 可谓是ART中非常重要的一部分,具体代码在 ConcurrentCopying 这个类中,当我们调用System.gc或者Runtime.gc的时候,就能够触发。
这里面的流程很多,我们着重讲一下Sweep机制。通过这个实际的案例,我们讲不再单纯背诵"八股文",而是运用到实际开发中。在内存海绵方案中,有两个非常关键的步骤 图片引用自(字节msponge方案)
- 对已有的大对象内存进行隐藏
- gc后针对大对象区域的内存,进行不叠加
第一个步骤在我的博客中都有很详细的记录,而第二个步骤在字节本身的方案介绍中,对于细节本身都很少有提及。
下面我们把这块拼图彻底完善。
ConcurrentCopying::Sweep
为了完成步骤2,我们需要知道一些前置知识。我们都知道,当虚拟机内存减少时,会通过Heap::RecordFree,方法,对num_bytes_allocated (负责内存大小记录)变量进行最终删减。而RecordFree方法,会在currentCopying::Sweep开始,逐步被触发。
下面我们来认识一下Sweep方法,它负责内存清除发起,与记录内存的变化数据
scss
void ConcurrentCopying::Sweep(bool swap_bitmaps) {
if (use_generational_cc_ && young_gen_) {
// Only sweep objects on the live stack.
SweepArray(heap_->GetLiveStack(), /* swap_bitmaps= */ false);
} else {
{
TimingLogger::ScopedTiming t("MarkStackAsLive", GetTimings());
accounting::ObjectStack* live_stack = heap_->GetLiveStack();
if (kEnableFromSpaceAccountingCheck) {
// Ensure that nobody inserted items in the live stack after we swapped the stacks.
CHECK_GE(live_stack_freeze_size_, live_stack->Size());
}
heap_->MarkAllocStackAsLive(live_stack);
live_stack->Reset();
}
CheckEmptyMarkStack();
TimingLogger::ScopedTiming split("Sweep", GetTimings());
连续空间
for (const auto& space : GetHeap()->GetContinuousSpaces()) {
if (space->IsContinuousMemMapAllocSpace() && space != region_space_
&& !immune_spaces_.ContainsSpace(space)) {
space::ContinuousMemMapAllocSpace* alloc_space = space->AsContinuousMemMapAllocSpace();
TimingLogger::ScopedTiming split2(
alloc_space->IsZygoteSpace() ? "SweepZygoteSpace" : "SweepAllocSpace", GetTimings());
RecordFree(alloc_space->Sweep(swap_bitmaps));
}
}
大对象
SweepLargeObjects(swap_bitmaps);
}
}
这里我们注意了,针对ContinuousSpaces区域,最终会调用RecordFree方法进行内存数据记录,而针对LargeObjectSpace,调用的方法是SweepLargeObjects。 这两块区域我们之前在ART内存模型中有介绍,这里我们就不再细说,我们来看一下内存海绵方法相关的SweepLargeObjects方法
scss
void ConcurrentCopying :: SweepLargeObjects(bool swap_bitmaps) {
TimingLogger::ScopedTiming split("SweepLargeObjects", GetTimings());
if (heap_->GetLargeObjectsSpace() != nullptr) {
记录大对象
RecordFreeLOS(heap_->GetLargeObjectsSpace()->Sweep(swap_bitmaps));
}
}
RecordFree 跟 RecordFreeLOS的定义如下:
scss
void GarbageCollector::RecordFree(const ObjectBytePair& freed) {
GetCurrentIteration()->freed_.Add(freed);
heap_->RecordFree(freed.objects, freed.bytes);
}
void GarbageCollector::RecordFreeLOS(const ObjectBytePair& freed) {
GetCurrentIteration()->freed_los_.Add(freed);
heap_->RecordFree(freed.objects, freed.bytes);
}
我们到这里就非常清楚的看到了,当执行到了RecordFree / RecordFreeLOS方法,第一个就是记录当前被回收对象的数量,这里ContinuousSpaces记录的是freed对象,LOS记录的是freed_los,就这个区别,然后就调用到了我们熟悉的Heap::RecordFree方法,最终结束对内存的变化记录。
不叠加大对象内存记录
我们从上面可以看到,每一次发生GC操作,如果有大对象被回收,其实都有针对整块内存的大小调整。而内存海绵方案中,因为隐藏了大对象的计算,也就是说。num_bytes_allocated (实际)= num_bytes_allocated(原本)+ 大对象区域内存。 如果不参与任何干预GC的操作,那么num_bytes_allocated 在GC完成时,会被超额删除,即num_bytes_allocated (实际) = num_bytes_allocated (实际) - 回收大对象区域内存。
这里导致的问题就是num_bytes_allocated 计算异常(删除了本来不再计算中的内存大小),当GC发生在大对象中,如果没有进行num_bytes_allocated 的调控,会出现num_bytes_allocated 低于0 的情况,从而导致再分配的Crash。
这里的解决方案就是,我们在对num_bytes_allocated 进行删减操作时(GC过程),如果回收了大对象的内存,那么就要对num_bytes_allocated 进行补偿(增加回来删除的内存 )| 不对大对象内存数值进行删除(不对num_bytes_allocated进行删减)
下面我们分别介绍这两种方案:
不对大对象内存数值进行删除
从上面我们了解到了Sweep过程,实现不对大对象内存数值进行删除,我们只需要采取inline hook GarbageCollector::RecordFreeLOS 方法即可,即让此方法在OOM时失效
RecordFreeLOS方法函数签名是
_ZN3art2gc9collector16GarbageCollector13RecordFreeLOSERKNS1_14ObjectBytePairE
我们的proxy函数如下:
arduino
void record_free_los_proxy(void *heap, void* pair) {
__android_log_print(ANDROID_LOG_ERROR, MSPONGE_TAG, "%s", "释放大对象");
FP堆栈
size_t size = xunwind_fp_unwind(g_frames, sizeof(g_frames) / sizeof(g_frames[0]),NULL);
__android_log_print(ANDROID_LOG_ERROR, MSPONGE_TAG, "%s ",
xunwind_frames_get(g_frames,size,NULL));
}
num_bytes_allocated 进行删除后补偿
Hook RecordFreeLOS 好处是简单方便,但是笔者实际测下来,在小部分OV手机上,通过符号进行inline hook该函数,往往会失效。当然,并不是符号不存在,而是该调用有可能被厂商替换成直接调用了Heap::RecordFree,其他的手机厂商暂时没有这个问题。
那么我们还有一个方法就是,先让Heap::RecordFree执行,如果发现上次记录的大对象内存区域与本次获取的大对象区域产生了偏差,即(上次记录的大对象内存区域< 本次获取),证明num_bytes_allocated会有可能导致溢出,因此我们可以再进行一次RecordFree,传入大对象内存区域的差值进行补偿即可。
对应的代码就是
scss
void heap_record_free_proxy(void *heap, uint64_t freed_objects, int64_t freed_bytes) {
((heap_record_free) heap_record_free_orig)(heap, freed_objects, freed_bytes);
if (los != NULL && start_handle_oom == 1) {
uint64_t currentAllocLOS = get_num_bytes_allocated(los);
if (currentAllocLOS < lastAllocLOS) {
((heap_record_free) heap_record_free_orig)(heap, freed_objects,
currentAllocLOS -
lastAllocLOS);
__android_log_print(ANDROID_LOG_ERROR, MSPONGE_TAG, "%s %lu", "los进行补偿",
currentAllocLOS - lastAllocLOS);
}
}
}
总结
通过了解gc中的Sweep机制,我们能够从源码中找到实际问题的解决思路。当然,了解一个方案,完善一个方案,解决一个问题,都可以让自己快速成长!文章中的代码都放在了我的项目 mooner 中,也非常感谢有小伙伴能够提出issue帮助方案的完善!或许,这就是开源社区的魅力吧!