Android应用OutOfMemory -- 1.OOM机制了解

3,079 阅读7分钟

相关阅读

OOM系列文章

Android应用OutOfMemory -- 1.OOM机制了解

ANR系列文章

Android应用ANR源码分析--1.ANR触发机制了解

java.lang.OutOfMemoryError

要分析OOM问题,首先需要弄明白有哪些场景会导致OOM,Android中导致OOM的原因主要可以划分为以下几个类型:(借用美团的图)

美团oom类型.png

Android系统中,OutOfMemoryError这个错误是怎么被系统抛出的?下面基于Android9.0的代码进行搜索得到

抛出oom的代码处.png

重点关注下面两点

    1. 堆内存分配失败 /art/runtime/gc/heap.cc
    1. 创建线程失败/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类型。

heap_cc.png

下面我们通过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 image.png

image.png 解释下上面代码及图:

  • 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中,如下图。这里演示较为麻烦,不作示例。

rosAlloc.png

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的地方进行标记;

thread_anr.png

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.创建线程失败

创建线程也可以归纳为两个步骤:

    1. 调用mmap分配栈内存这里mmap flag中指定了MAP_ANONYMOUS,即匿名内存映射。这是在Linux中分配大块内存的常用方式。其分配的是虚拟内存,对应页的物理内存并不会立即分配,而是在用到的时候触发内核的缺页中断,然后中断处理函数再分配物理内存。
    1. 调用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 image.png image.png 解释下上面代码及图:

  • 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的限制:

image.png

4.线程数数量超过限制

类似FD一样,可以通过/proc/sys/kernel/threads-max来查看。线程数超过限制目前只有华为手机,因为修改了系统对单个进程线程数的限制而导致OOM。其他手机出现OOM大概率不是因为线程数的限制,更可能的是其他原因,尤其是32位应用的虚拟内存问题。

关于线程限制,同样是一直创建线程创建线程失败的例子,在以下两部手机表现就截然不同:

华为mate20pro harmonyOS 2.0,线程数可以创建到2900+ image.png

华为7.0的设备,线程数在达到400+时,一定会OOM 华为7.0设备线程过多oom.png

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();
    }

}

image.png

image.png 解释下上面代码及图:

  • 1.一直创建线程,据图一能看出来,我们创建了1800+个的线程,从这里也可以看到并不像华为设备对线程数的限制那么严格;
  • 2.据图二虚拟内存(Vmsize的值)占用可以看到,这次的崩溃是因为虚拟内存不足,Vmsize将近约3GB。另外我们应当注意到这里的表现是NativeCrash Fatal signal 11 (SIGSEGV)而非为java层的OOM。
  • 3.如果要解决虚拟内存不足的情况,最好的解决办法是升级64位应用

参考链接:

Probe:Android线上OOM问题定位组件

Android高级性能调优;不可思议的OOM!