背景
咱们许多时分也背过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办法,它担任内存铲除发起,与记载内存的改变数据
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办法
void ConcurrentCopying :: SweepLargeObjects(bool swap_bitmaps) {
TimingLogger::ScopedTiming split("SweepLargeObjects", GetTimings());
if (heap_->GetLargeObjectsSpace() != nullptr) {
记载大目标
RecordFreeLOS(heap_->GetLargeObjectsSpace()->Sweep(swap_bitmaps));
}
}
RecordFree 跟 RecordFreeLOS的定义如下:
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函数如下:
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,传入大目标内存区域的差值进行补偿即可。
对应的代码便是
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帮助计划的完善!或许,这便是开源社区的魅力吧!