相关阅读
OOM系列文章
Android应用OutOfMemory -- 1.OOM机制了解
ANR系列文章
java.lang.OutOfMemoryError
要分析OOM问题,首先需要弄明白有哪些场景会导致OOM,Android中导致OOM的原因主要可以划分为以下几个类型:(借用美团的图)
Android系统中,OutOfMemoryError这个错误是怎么被系统抛出的?下面基于Android9.0的代码进行搜索得到
重点关注下面两点
-
- 堆内存分配失败 /art/runtime/gc/heap.cc
-
- 创建线程失败/art/runtime/thread.cc 下面来展开说说。
1.堆内存分配失败
void Heap::ThrowOutOfMemoryError(Thread* self, size_t byte_count, AllocatorType allocator_type)
抛出时的错误信息: oss << "Failed to allocate a " << byte_count << " byte allocation with " << total_bytes_free << " free bytes and " << PrettySize(GetFreeMemoryUntilOOME()) << " until OOM";
这是在进行堆内存分配时抛出的OOM错误,这里也可以细分成两种不同的类型:
- 1.为对象分配内存时达到进程的内存上限。由Runtime.getRuntime.MaxMemory()可以得到Android中每个进程被系统分配的内存上限,当进程占用内存达到这个上限时就会发生OOM,这也是Android中最常见的OOM类型。
下面我们通过demo来测试下虚拟机堆内存不足的情况
List<byte[]> bytesList = new ArrayList<>();
private void testCreatHeap() {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e("oom", "i..." + i++);
//每次申请10MB内存
bytesList.add(new byte[1024 * 1024 * 10]);
}
}
测试机型: 华为mate20pro harmonyOS 2.0
解释下上面代码及图:
-
1.示例代码我们每次分配10MB的内存
-
2.通过图一可以看到,总共分配了51次,堆内存占用达到约535MB,达到甚至超过512MB(maxMemory),然而此时的GC并没有能回收掉任何内存。
-
3.最终因为堆内存不足抛出ThrowOutOfMemoryError,得到图二报错。
-
2. 没有足够大小的连续地址空间。这种情况一般是进程中存在大量的内存碎片导致的,其堆栈信息会比第一种OOM堆栈多出一段信息:failed due to fragmentation (required continguous free “<< required_bytes << “ bytes for a new buffer where largest contiguous free ” << largest_continuous_free_pages << “ bytes)”; 其详细代码在art/runtime/gc/allocator/rosalloc.cc中,如下图。这里演示较为麻烦,不作示例。
2. 创建线程失败
先来看下创建线程的代码
/art/runtime/thread.cc
void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
CHECK(java_peer != nullptr);
Thread* self = static_cast<JNIEnvExt*>(env)->GetSelf();
if (VLOG_IS_ON(threads)) {
ScopedObjectAccess soa(env);
ArtField* f = jni::DecodeArtField(WellKnownClasses::java_lang_Thread_name);
ObjPtr<mirror::String> java_name
f->GetObject(soa.Decode<mirror::Object>(java_peer))->AsString();
std::string thread_name;
if (java_name != nullptr) {
thread_name = java_name->ToModifiedUtf8();
} else {
thread_name = "(Unnamed)";
}
VLOG(threads) << "Creating native thread for " << thread_name;
self->Dump(LOG_STREAM(INFO));
}
Runtime* runtime = Runtime::Current();
// Atomically start the birth of the thread ensuring the runtime isn't shutting down.
bool thread_start_during_shutdown = false;
{
MutexLock mu(self, *Locks::runtime_shutdown_lock_);
if (runtime->IsShuttingDownLocked()) {
thread_start_during_shutdown = true;
} else {
runtime->StartThreadBirth();
}
}
if (thread_start_during_shutdown) {
ScopedLocalRef<jclass> error_class(env, env->FindClass("java/lang/InternalError"));
env->ThrowNew(error_class.get(), "Thread starting during runtime shutdown");
return;
}
Thread* child_thread = new Thread(is_daemon);
// Use global JNI ref to hold peer live while child thread starts.
child_thread->tlsPtr_.jpeer = env->NewGlobalRef(java_peer);
stack_size = FixStackSize(stack_size);
// Thread.start is synchronized, so we know that nativePeer is 0, and know that we're not racing
// to assign it.
env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer,
reinterpret_cast<jlong>(child_thread));
// Try to allocate a JNIEnvExt for the thread. We do this here as we might be out of memory and
//1.尝试分配一个JNIEnvExt给新的thread
// do not have a good way to report this on the child's side.
std::string error_msg;
std::unique_ptr<JNIEnvExt> child_jni_env_ext(
JNIEnvExt::Create(child_thread, Runtime::Current()->GetJavaVM(), &error_msg));
int pthread_create_result = 0;
if (child_jni_env_ext.get() != nullptr) {
pthread_t new_pthread;
pthread_attr_t attr;
child_thread->tlsPtr_.tmp_jni_env = child_jni_env_ext.get();
CHECK_PTHREAD_CALL(pthread_attr_init, (&attr), "new thread");
CHECK_PTHREAD_CALL(pthread_attr_setdetachstate, (&attr, PTHREAD_CREATE_DETACHED),
"PTHREAD_CREATE_DETACHED");
CHECK_PTHREAD_CALL(pthread_attr_setstacksize, (&attr, stack_size), stack_size);
//2.创建新的线程
pthread_create_result = pthread_create(&new_pthread,
&attr,
Thread::CreateCallback,
child_thread);
CHECK_PTHREAD_CALL(pthread_attr_destroy, (&attr), "new thread");
if (pthread_create_result == 0) {
// pthread_create started the new thread. The child is now responsible for managing the
// JNIEnvExt we created.
// Note: we can't check for tmp_jni_env == nullptr, as that would require synchronization
// between the threads.
child_jni_env_ext.release();
return;
}
}
// Either JNIEnvExt::Create or pthread_create(3) failed, so clean up.
{
MutexLock mu(self, *Locks::runtime_shutdown_lock_);
runtime->EndThreadBirth();
}
// Manually delete the global reference since Thread::Init will not have been run.
env->DeleteGlobalRef(child_thread->tlsPtr_.jpeer);
child_thread->tlsPtr_.jpeer = nullptr;
delete child_thread;
child_thread = nullptr;
// TODO: remove from thread group?
env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer, 0);
{
std::string msg(child_jni_env_ext.get() == nullptr ?
StringPrintf("Could not allocate JNI Env: %s", error_msg.c_str()) :
StringPrintf("pthread_create (%s stack) failed: %s",
PrettySize(stack_size).c_str(), strerror(pthread_create_result)));
ScopedObjectAccess soa(env);
soa.Self()->ThrowOutOfMemoryError(msg.c_str());
}
}
总结native层创建thread主要流程:
- 1.是创建JNIEnvExt,
- 2.创建thread。 梳理整个过程如下图(对,又是借用美团的);并对可能会导致oom的地方进行标记;
1.创建JNIEnv失败
创建JNIEnv可以分为两个步骤:
- 1.通过Andorid的匿名共享内存(Anonymous Shared Memory)分配 4KB(一个page)内核态内存。
- 2.再通过Linux的mmap调用映射到用户态虚拟内存地址空间。
第一步:创建匿名共享内存时,需要打开/dev/ashmem文件,所以需要一个FD(文件描述符)。此时,如果创建的FD数已经达到上限,则会导致创建JNIEnv失败,抛出错误信息如下:
E/art: ashmem_create_region failed for 'indirect ref table':
Too many open files java.lang.OutOfMemoryError:
Could not allocate JNI Env at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:730)
第二步:调用mmap时,如果进程虚拟内存地址空间耗尽,也会导致创建JNIEnv失败,抛出错误信息如下:
E/art: Failed anonymous mmap(0x0, 8192, 0x3, 0x2, 116, 0): Operation not permitted.
See process maps in the log. java.lang.OutOfMemoryError:
Could not allocate JNI Env at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:1063)
2.创建线程失败
创建线程也可以归纳为两个步骤:
-
- 调用mmap分配栈内存。这里mmap flag中指定了MAP_ANONYMOUS,即匿名内存映射。这是在Linux中分配大块内存的常用方式。其分配的是虚拟内存,对应页的物理内存并不会立即分配,而是在用到的时候触发内核的缺页中断,然后中断处理函数再分配物理内存。
-
- 调用clone方法 进行线程创建。
其中第一步调用mmap分配栈内存分配栈内存失败可能的原因是进程的虚拟内存不足,可能的抛出错误信息如下:
W/libc: pthread_create failed: couldn't allocate 1073152-bytes mapped space: Out of memory
W/tch.crowdsourc: Throwing OutOfMemoryError with VmSize 4191668 kB "pthread_create (1040KB stack) failed: Try again"
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again
at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:753)
来做个测试:
//一直创建线程
private void testCreatThread() {
while (true) {
Log.e("oom", "i..." + i++);
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "i..." + i).start();
}
}
华为mate20pro harmonyOS 2.0
解释下上面代码及图:
-
1.据图一能看到,华为mate20pro harmonyOS 2.0设备的单个进程限制。
-
2.我们创建了2900+个的线程,并最终导致虚拟内存不足出现OOM,至于虚拟内存相关的,下面单独解释。
其中第二步clone方法失败可能的原因是线程数超出了限制,抛出错误信息一般如下,这里不做演示:
W/libc: pthread_create failed: clone failed: Out of memory
W/art: Throwing OutOfMemoryError "pthread_create (1040KB stack) failed: Out of memory"
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Out of memory
at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:1078)
3.FD数量超过限制
因为Android是基于linux系统的,我们可以通过 /proc/pid/limits 来查看描述着系统对对应进程的限制,对于非特大类型的应用或者错误使用,FD一般是不会被触顶的。下面来看一部华为Android7.0的设备关于FD的限制:
4.线程数数量超过限制
类似FD一样,可以通过/proc/sys/kernel/threads-max来查看。线程数超过限制目前只有华为手机,因为修改了系统对单个进程线程数的限制而导致OOM。其他手机出现OOM大概率不是因为线程数的限制,更可能的是其他原因,尤其是32位应用的虚拟内存问题。
关于线程限制,同样是一直创建线程创建线程失败的例子,在以下两部手机表现就截然不同:
华为mate20pro harmonyOS 2.0,线程数可以创建到2900+
华为7.0的设备,线程数在达到400+时,一定会OOM
5.虚拟内存不足
关于Android虚拟内存,或许你已经知道;如果不知道的可以参考这篇文章,Android内存 。简单总结下,当应用程序进行内存分配时,得到的是虚拟内存,只有真正去写这一内存块时,才会产生缺页中断,进而分配物理内存。虚拟内存的大小主要受CPU架构及内核的限制。
32位的CPU架构(即arm-v7),其地址空间最大为4GB,内核占用了部分高地址,用户空间所能使用的地址最多为3GB,而对于arm64来说,用户态地址空间为512GB。目前很多应用的实际情况是,作为32位的应用程序,普遍运行在64位CPU架构上。这种情况下,应用可独占4GB的低地址空间,而内核依然可以使用512GB的高地址。所以虚拟内存不足的情况基本集中在32位应用上。
下面看这段代码在voviY66,android6.0.1的32位设备上的遇到的虚拟内存不足的情况
//一直创建线程
private void testCreatThread() {
while (true) {
Log.e("oom", "i..." + i++);
try {
if (i == 1801) {
Thread.sleep(200);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
try {
if (i == 1800) {
//获取proc/pid/status状态
Log.e("oom", "i..." + DeviceUtil.getProcData());
}
//保证线程尽量活着
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "i..." + i).start();
}
}
解释下上面代码及图:
- 1.一直创建线程,据图一能看出来,我们创建了1800+个的线程,从这里也可以看到并不像华为设备对线程数的限制那么严格;
- 2.据图二虚拟内存(Vmsize的值)占用可以看到,这次的崩溃是因为虚拟内存不足,Vmsize将近约3GB。另外我们应当注意到这里的表现是NativeCrash Fatal signal 11 (SIGSEGV)而非为java层的OOM。
- 3.如果要解决虚拟内存不足的情况,最好的解决办法是升级64位应用。
参考链接: