这篇文章我们直接来分析为什么我们的应用会抛出 OutOfMemoryError
,以及哪些情况下会发生 OutOfMemoryError
。OOM的异常在java层只有 java,lang.OutOfMemoryError
这一个Throwable的定义,抛出这个异常的行为由jni层触发:Thread::ThrowmOutOfMemoryError
Heap::ThrowOutOfMemoryError
我们追溯一下哪些地方可能直接调用 Thread 的 ThrowOutOfMemoryError,先列举几个不常见不怎么需要深入去理解的case:
这个地方发生在 ClassPreDefine 的时候
通过jni的NewStringUTF分配字符串并且超出最大长度的时候。
可以看到Unsafe是直接通过jni层malloc去分配内存的,失败了就扔oom出去。
Java Thread#start 的时候是通过 start -> nativeCreate -> Thread_nativeCreate -> Thread::CreateNativeThread -> pthread_create 执行的。当 pthread_create 分配失败的时候,就会抛出一个 OOM:
OOM会在 Heap 的 AllocateInternalWithGc 里面抛出。所以我们需要接着上一篇文章再来看看我们的堆内存分配的步骤,在 Heap 的 AllocObjectWithAllocator 函数里会调用TryToAllocate函数去分配,如果分配失败会尝试gc后再重新分配:
AllocateInternalWithGc里面的代码比较多,我不截图了,直接把每一步的关键步骤copy过来,我们大概能理解他的意思就行:
if ((was_default_allocator && allocator != GetCurrentAllocator()) ||
(!instrumented && EntrypointsInstrumented())) {
return nullptr;
}
if (last_gc != collector::kGcTypeNone) {
// A GC was in progress and we blocked, retry allocation now that memory has been freed.
mirror::Object* ptr = TryToAllocate<true, false>(self, allocator, alloc_size, bytes_allocated,
usable_size, bytes_tl_bulk_allocated);
if (ptr != nullptr) {
return ptr;
}
}
if (last_gc < tried_type) {
const bool gc_ran = PERFORM_SUSPENDING_OPERATION(
CollectGarbageInternal(tried_type, kGcCauseForAlloc, false, starting_gc_num + 1)
!= collector::kGcTypeNone);
if ((was_default_allocator && allocator != GetCurrentAllocator()) ||
(!instrumented && EntrypointsInstrumented())) {
return nullptr;
}
if (gc_ran && have_reclaimed_enough()) {
mirror::Object* ptr = TryToAllocate<true, false>(self, allocator,
alloc_size, bytes_allocated,
usable_size, bytes_tl_bulk_allocated);
if (ptr != nullptr) {
return ptr;
}
}
}
PERFORM_SUSPENDING_OPERATION(CollectGarbageInternal(gc_plan_.back(), kGcCauseForAlloc, true, GC_NUM_ANY));
if ((was_default_allocator && allocator != GetCurrentAllocator()) ||
(!instrumented && EntrypointsInstrumented())) {
return nullptr;
}
mirror::Object* ptr = nullptr;
if (have_reclaimed_enough()) {
ptr = TryToAllocate<true, true>(self, allocator, alloc_size, bytes_allocated,
usable_size, bytes_tl_bulk_allocated);
}
if (ptr == nullptr) {
ScopedAllowThreadSuspension ats;
ThrowOutOfMemoryError(self, alloc_size, allocator);
}
上述流程画到图里便于理解:
那么 TryToAllocate 里面是如何判断内存是否足够呢?在分配器分配的地方都会调用 IsOutOfMemoryOnAllocation 函数来判断内存是否够,而 TryToAllocate传入的kGrow参数也是在这个函数使用:
这里会对比目标内存大小和最大限制 growth_limit_,如果大于growth_limit_,那么就肯定是OOM了。如果grow是true的话,就会在没有超出最大限制的条件下扩容。所以在分配的时候,前面一次很弱的gc是不清楚软引用+不扩容,后面一次就会升级成清楚软引用+扩容,所以可见虚拟机在保证内存分配尽量成功的前提下,也考虑到了尽量不要占用过多的系统资源。当前内存分配的总大小是从num_bytes_allocated_里面获取的。这个变量的新增在2个地方能看到:
image.png
看到这里我们能得到一个结论了,别看art虚拟机把内存分配分成了一大堆Space,像LargeObjectSpace这种,在arm64上分配了固定大小,在非arm64上没有明显限制,但是在堆内存分配的时候,总内存计算是把这些Space都算到一起去了的。
了解了art里heap的内存分布和对象回收机制,我们基于这些知识点总结一些对应的内存优化思路。
对应的一些思路包括:
进程可用堆内存 * 2
,而在非arm64架构上,可用对内存会变成 进程可用堆内存+可用虚拟内存
。该方案原理可以参考文章:《拯救OOM!字节自研 Android 虚拟机内存管理优化黑科技 mSponge》 链接:https://juejin.cn/post/7052574440734851085该方案没有公开源码,我写了一个复现的demo,源码仅供参考:https://github.com/shaomaicheng/memoryescape
除了上述一些内存优化机制的总结,还有一点比较重要就是治理内存的必要性,内存缺乏治理除了直接OOM的导致,在内存不足的条件下分配,即使没有达到OOM的条件,也仍然可能触发多次的GC,而GC是一个比较占用资源的行为,很可能在一些设备上导致主线程抢占不到时间片,从而影响到APP的各种其他性能指标,例如ANR率,Crash率和卡顿。