Android 启动线程OOM

启动线程OOM有两种情况

case 1:

pthread_create failed: couldn't allocate 1069056-bytes mapped space: Out of memory
pthread_create failed: couldn't allocate 1069056-bytes mapped space: Out of memory
Throwing OutOfMemoryError "pthread_create (1040KB stack) failed: Try again"
HeapInfo: checking storage, bytes_hprof_total: 0, bytes_available: 38325297152, bytes_fileMinNeeded: 814915040
HeapInfo: bytes_available: 38325297152, bytes_fileMinNeeded: 814915040
hprof: heap dump "/data/misc/hprof/heap-dump-1518384637-pid4012.hprof" starting...


case 2:

java.lang.OutOfMemoryError: Could not allocate JNI Env

java.lang.Thread.nativeCreate(Native Method)

java.lang.Thread.start(Thread.java:729)

针对这两种case.其中最为常见的是case1.不管是那种case分析的起点都是一致的.

1 java-->native

public synchronized void start() {

        nativeCreate(this, stackSize, daemon);

}


解释如下:

1.this:Thread对象本身

2.stackSize:指定了新建Thread stack的大小,单位是字节.如果设置为0表示忽略.

            提高stackSize会减少StackOverFlow的发生,而降低stackSize会减少OOM的发生.

            该参数和平台相关,某些平台会忽略该参数.

3.daemon:是否为守护线程

2 native-->ART

路径:art/runtime/native/java_lang_Thread.cc

static void Thread_nativeCreate(JNIEnv* env, jclass, jobject java_thread, jlong stack_size, jboolean daemon) {

    Thread::CreateNativeThread(env, java_thread, stack_size, daemon == JNI_TRUE);

}


路径:art/runtime/thread.cc

       void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {

    Thread* child_thread = new Thread(is_daemon);

    //分配stack size

    stack_size = FixStackSize(stack_size);

    //java中每一个 java线程 对应一个 JniEnv 结构。这里的JniEnvExt 就是ART 中的 JniEnv

    //Try to allocate a JNIEnvExt for the thread. We do this here as we might be out of memory and do not have a good way to report this on the child's side.

    std::unique_ptr<JNIEnvExt> child_jni_env_ext(

    JNIEnvExt::Create(child_thread, Runtime::Current()->GetJavaVM(), &error_msg));


    //创建线程的主要逻辑

    if (child_jni_env_ext.get() != nullptr) {

        pthread_create_result = pthread_create(&new_pthread,

                                               &attr,

                                               Thread::CreateCallback,

                                               child_thread);

    }


    //执行创建流程失败的收尾逻辑

    //分别对应异常case 2 & case 1

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


    //抛出OOM异常

    soa.Self()->ThrowOutOfMemoryError(msg.c_str());

}

其中child_jni_env_ext.get() == nullptr对应case 2,pthread_create对应case 1

默认的stack szie

路径:art/runtime/thread.cc

static size_t FixStackSize(size_t stack_size) { 

    if (stack_size == 0) { 

    // GetDefaultStackSize是启动art时命令行的"-Xss="参数,Android 中没有该参数,因此为0.

    stack_size = Runtime::Current()->GetDefaultStackSize();

    }  

    // bionic pthread 默认栈大小是 1M

    stack_size += 1 * MB;


    if (Runtime::Current()->ExplicitStackOverflowChecks()) {    

        // 8K

        stack_size += GetStackOverflowReservedBytes(kRuntimeISA);

    } else {    

        // 1M + 8K + 8K

        stack_size += Thread::kStackOverflowImplicitCheckSize +

        GetStackOverflowReservedBytes(kRuntimeISA);

  }

  return stack_size;//1M + 8K + 8K = 1040K

}


因此默认的stack szie1040KB.这个信息case 1crash的信息一致

case 1.1

通过上面的分析主要逻辑是调用pthread_create.

pthread_create_result = pthread_create(&new_pthread,

                                               &attr,

                                               Thread::CreateCallback,

                                               child_thread);


参数解释:

new_pthread:新建线程句柄

attr:指定新建thread的属性,包括stack size

Thread::CreateCallback: 新创建的线程的routine函数,即,线程的入口函数。
child_thread: callbac的唯一参数,此处是 native 层的 Thread


case 1.2 ART-->pthread

路径:bionic/libc/bionic/pthread_create.cpp

int pthread_create(pthread_t* thread_out, pthread_attr_t const* attr, void* (*start_routine)(void*), void* arg) { 

    //1.分配stack

    pthread_internal_t* thread = NULL; 

    void* child_stack = NULL; 

    int result = __allocate_thread(&thread_attr, &thread, &child_stack); 

    if (result != 0) { 

        return result;

    }

    //2.linux 系统调用 clone,执行真正的创建动作。

    int rc = clone(__pthread_start, child_stack, flags, thread, &(thread->tid), tls, &(thread->tid));

      if (rc == -1) {

          __libc_format_log(ANDROID_LOG_WARN, "libc", "pthread_create failed: clone failed: %s", strerror(errno));

  }
}

static int __allocate_thread {

    mmap_size = BIONIC_ALIGN(attr->stack_size + sizeof(pthread_internal_t), PAGE_SIZE);

    attr->stack_base = __create_thread_mapped_space(mmap_size, attr->guard_size);

}

static void* __create_thread_mapped_space(size_t mmap_size, size_t stack_guard_size) {

    //调用mmap分配内存

    int flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE;

    void* space = mmap(NULL, mmap_size, prot, flags, -1, 0); 


    //判断StackOverflow的场景,避免栈内存溢出污染其他内存区域

    //Stack is at the lower end of mapped space, stack guard region is at the lower end of stack.

    //Set the stack guard region to PROT_NONE, so we can detect thread stack overflow.

    if (mprotect(space, stack_guard_size, PROT_NONE) == -1) {

        munmap(space, mmap_size);

        return NULL;

    }

    prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, space, stack_guard_size, "thread stack guard page"); 

 

   return space;

}


Case1.3 pthread-->Linux内核

int rc = clone(__pthread_start, child_stack, flags, thread, &(thread->tid), tls, &(thread->tid));


这里主要涉及到linuxclone系统调用(SystemCall)(http://man7.org/linux/man-pages/man2/clone.2.html)

man page中对clone的描述:

clone() creates a new process, in a manner similar to fork(2).

因为unix中只有进程的概念,所有clone是实现线程的一种手段.

Fork:创建新的进程.并将父进程的内存全部copy到子进程,也就说父子进程内存不共享.

Clone:创建新的进程,且父子进程共享内存

调用链

nativeCreate-->CreateNativeThread-->pthread_create-->__allocate_thread-->__create_thread_mapped_space-->mmap-->clone

Case 2.1 native-->ART

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

Case2发生的原因:JNIEnvExt::Create调用失败。

JNIEnvExt* JNIEnvExt::Create(Thread* self_in, JavaVMExt* vm_in, std::string* error_msg) {

  std::unique_ptr<JNIEnvExt> ret(new JNIEnvExt(self_in, vm_in, error_msg)); 

 if (CheckLocalsValid(ret.get())) { 

   return ret.release();

  }

     return nullptr;

}

直接原因是CheckLocalsValidreturn false,再进一步是JniEnvExt::table_mem_map_ nullptr.

调用链是 JniEnvExt::Create() -> JNIEnvExt::JNIEnvExt()(构造函数) -> IndirectReferenceTable::IndirectReferenceTable()    

const size_t table_bytes = max_count * sizeof(IrtEntry);

table_mem_map_.reset(MemMap::MapAnonymous(..., table_bytes, ...));

max_count是常量 art::kLocalsInitial == 512计算了一下sizeof(IrtEntry) == 8.所以 table_bytes = 512 * 8 = 4096 = 4k,刚好是一个内存页的大小.因此是调用MemMap::MapAnonymous() 失败了。

路径:art/runtime/mem_map.cc

// 1. 创建 ashmemfd.reset(

  ashmem_create_region(debug_friendly_name.c_str(), page_aligned_byte_count));

// 2. 调用mmap映射到用户态内存地址空间void* actual = MapInternal(..., fd.get(), ...);

步骤1失败的话,fd.get()返回-1,步骤2仍然会正常执行,只不过其行为有所不同。

如果步骤1成功的话,两个步骤则是:

1.通过Andorid的匿名共享内存(Anonymous Shared Memory)分配 4KB(一个page)内核态内存

2.再通过 Linux mmap 调用映射到用户态虚拟内存地址空间.

如果步骤1失败的话,步骤2则是:

    通过 Linux mmap 调用创建一段虚拟内存.分配虚拟内存失败了

考察失败的场景:

    步骤1 失败的情况一般是内核分配内存失败,这种情况下,整个设备/OS的内存应该都处于非常紧张的状态。

    步骤2 失败的情况一般是 进程虚拟内存地址空间耗尽

OOM分析结论

不管是case 1,还是堆栈case 2:

    创建线程过程中发生OOM是因为进程内的虚拟内存地址空间耗尽了

Linux内存分布


什么情况下虚拟内存地址空间才会耗尽呢?32位系统中,用户空间的内存是3G大,简单起见,我们粗略估计一下,假设

1.可见虚拟内存是3G大(实际值更小)

2.创建一个进程需要1M虚拟内存(实际值更大)

因此再假设有一个进程,除了创建线程什么都不干,那他最多能创建多少个线程?3G/1M = 3000

在完全理想的情况下最多是3000个线程。综合其他因素,实际值会明显小于3000。虽然3000的上限看上去很大,而如果有代码逻辑问题,创建很多线程,其实很容易爆掉。

Case 1:抛出OOM的地方已经保留了错误码信息

pthread_create_result = pthread_create(...);

StringPrintf("pthread_create (%s stack) failed: %s",

             PrettySize(stack_size).c_str(), strerror(pthread_create_result)));

pthread_create failed: clone failed: Out of memory11-06 12:27:00.256 30775 31188 W art     : Throwing OutOfMemoryError "pthread_create (1040KB stack) failed: Out of memory"

错误码定义

路径:bionic/lib/private/bionic_errdefs/bionic_errdefs.h

__BIONIC_ERRDEF( EBADF          ,   9, "Bad file descriptor" )

__BIONIC_ERRDEF( ECHILD         ,  10, "No child processes" )

__BIONIC_ERRDEF( EAGAIN         ,  11, "Try again" )

__BIONIC_ERRDEF( ENOMEM         ,  12, "Out of memory" )

__BIONIC_ERRDEF( EMFILE         ,  24, "Too many open files" )

Case 2:抛出OOM的地方已经保留了错误码信息,属于 FileDescriptor 耗尽

fd.Reset(ashmem_create_region(debug_friendly_name.c_str(), page_aligned_byte_count),  /* check_usage */ false);  

   if (fd.Fd() == -1) {

      *error_msg = StringPrintf("ashmem_create_region failed for '%s': %s", name, strerror(errno));

ashmem_create_region failed for 'indirect ref table': Too many open files11-06 06:25:54.193  3725  8575 W art     : Throwing OutOfMemoryError "Could not allocate JNI Env"

内存充足OOM

在堆内存,物理内存,SD卡空间充足时也会发生OOM

下面是几个关于 Android 官方声明内存限制阈值的 API

ActivityManager.getMemoryClass()    

虚拟机 java 堆大小的上限,分配对象时突破这个大小就会 OOM

ActivityManager.getLargeMemoryClass()

manifest 中设置 largeheap=true 时虚拟机 java 堆的上限

Runtime.getRuntime().maxMemory()


当前虚拟机实例的内存使用上限,为上述两者之一

Runtime.getRuntime().totalMemory()

当前已经申请的内存,包括已经使用的和还没有使用的

Runtime.getRuntime().freeMemory()                                    

上一条中已经申请但是尚未使用的那部分。那么已经申请并且正在使用的部分 used=totalMemory() - freeMemory()

ActivityManager.MemoryInfo.totalMem:

设备总内存

ActivityManager.MemoryInfo.availMem

设备当前可用内存

/proc/meminfo

记录设备的内存信息

堆内存不够用即Runtime.getRuntime().maxMemory() 这个指标满足不了申请堆内存大小时

Runtime.getRuntime().maxMemory() 这个指标满足不了申请堆内存大小时

此时通过 new byte[] 的方式尝试申请超过阈值 maxMemory() 的堆内存

通常这种 OOM 的错误信息通常如下

java.lang.OutOfMemoryError: Failed to allocate a XXX byte allocation with XXX free bytes and XXXKB until OOM

各个空间都充裕(堆内存Runtime.getRuntime().maxMemory() 大小的堆内存还剩余很大一部分,物理内存,SDActivityManager.MemoryInfo.availMem 还有很多)但是依然抛出OOMcase1 & case2

Case 1:抛出异常信息如下

java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Out of memory

Case 2:抛出异常信息如下

java.lang.OutOfMemoryError: Could not allocate JNI Env

OOM抛出时机

JVMthrow OOM定义

路径:art/runtime/thread.cc

void Thread::ThrowOutOfMemoryError(const char* msg)

参数 msg 携带了 OOM 时的错误信息

重点关注如下几个抛出时机

1.堆操作,该种情况抛出其实是堆内存不够及申请的堆内存超过了Runtime.getRuntime().maxMemory()

路径:/art/runtime/gc/heap.cc

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";


2.创建线程

路径:/art/runtime/thread.cc

void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon)

抛出时的错误信息:

    "Could not allocate JNI Env"

  或者

    StringPrintf("pthread_create (%s stack) failed: %s", PrettySize(stack_size).c_str(), strerror(pthread_create_result)));

系统限制

抛出 OOM,一定是线程创建过程中触发了某些限制,既然不是 Art 虚拟机为我们设置的堆上限,那么可能是更底层的限制。Android 系统基于 linux,所以 linux 的限制对于 Android 同样适用,这些限制有:

1 ./proc/pid/limits 描述着 linux 系统对对应进程的限制,下面是一个样例:

Limit                     Soft Limit           Hard Limit           Units     

Max cpu time              unlimited            unlimited            seconds   

Max file size             unlimited            unlimited            bytes     

Max data size             unlimited            unlimited            bytes     

Max stack size            8388608              unlimited            bytes     

Max core file size        0                    unlimited            bytes     

Max resident set          unlimited            unlimited            bytes     

Max processes             13419                13419                processes 

Max open files            1024                 4096                 files     

Max locked memory         67108864             67108864             bytes     

Max address space         unlimited            unlimited            bytes     

Max file locks            unlimited            unlimited            locks     

Max pending signals       13419                13419                signals   

Max msgqueue size         819200               819200               bytes     

Max nice priority         40                   40                   

Max realtime priority     0                    0                    

Max realtime timeout      unlimited            unlimited            us

Max stack sizeMax processes 的限制是整个系统的,不是针对某个进程的

Max locked memory 线程创建过程中分配线程私有 stack 使用的 mmap 调用没有设置 MAP_LOCKED,所以这个限制与线程创建过程无关

Max pending signalsc 层信号个数阈值,这个限制与线程创建过程无关

Max msgqueue sizeAndroid IPC 机制不支持消息队列

Max open files 最可疑,Max open files 表示每个进程最大打开文件的数目,进程 每打开一个文件就会产生一个文件描述符 fd(记录在 /proc/pid/fd 下面),这个限制表明 fd 的数目不能超过 Max open files 规定的数目

2 . /proc/sys/kernel 中描述的限制

这些限制中与线程相关的是 /proc/sys/kernel/threads-max,规定了每个进程创建线程数目的上限,所以线程创建导致 OOM 的原因也有可能与这个限制相关

Could not allocate JNI Env

触发大量网络连接且每个连接处于单独的线程中并保持/循环打开多个文件并保持/创建大量的handlerthread等等

当进程 fd 数(可以通过 ls /proc/pid/fd | wc -l 获得)突破 /proc/pid/limits 中规定的 Max open files 时,产生 OOM;

堆栈信息

ashmem_create_region failed for 'indirect ref table': Too many open files

java.lang.OutOfMemoryError: Could not allocate JNI Env

Thread.UncaughtExceptionHandler 捕获到 OutOfMemoryError 时记录 /proc/pid 目录下的如下信息:

  1. 1./proc/pid/fd 目录下文件数 (fd )
  2. 2./proc/pid/status threads 项(当前线程数目)

3. OOM 的日志信息(出了堆栈信息还包含其他的一些 warning 信息

比较/proc/pid/fd 目录下文件数与 /proc/pid/limits 中的 Max open files 数目,验证FD数目

pthread_create

创建大量线程当线程数(可以在 /proc/pid/status 中的 threads 项实时查看)超过 /proc/sys/kernel/threads-max 中规定的上限时产生 OOM 崩溃.已用逻辑空间地址可以查看 /proc/pid/status VmPeak/VmSize 记录

堆栈信息

pthread_create failed: clone failed: Out of memory

Throwing OutOfMemoryError "pthread_create (1040KB stack) failed: Out of memory"

验证/proc/pid/status threads 记录是否到达上限

线程创建的简易版流程

线程创建大概有两个关键的步骤

1.创建线程私有的结构体 JNIENV(JNI 执行环境,用于 C 层调用 Java 层代码)

2.调用 posix C 库的函数 pthread_create 进行线程创建工作

说明:

1.节点

路径:art/runtime/thread.cc

std::string msg(child_jni_env_ext.get() == nullptr ?

        "Could not allocate JNI Env" :

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

JNIENV 创建不成功时产生 OOM 的错误信息为 "Could not allocate JNI Env"

pthread_create失败时抛出 OOM 的错误信息为"pthread_create (%s stack) failed: %s".其中详细的错误信息由 pthread_create 的返回值(错误码)给出.

2. 节点②和③是创建JNIENV过程的关键节点

2.1节点/art/runtime/mem_map.cc中函数MemMap:MapAnonymous 的作用是为 JNIENV 结构体中 Indirect_Reference_tableC 层用于存储 JNI 局部 / 全局变量)申请内存

if (fd.get() == -1) {

      *error_msg = StringPrintf("ashmem_create_region failed for '%s': %s", name, strerror(errno));

      return nullptr;}


2.2节点③申请内存的方法.函数ashmem_create_region(创建一块 ashmen 匿名共享内存, 并返回一个文件描述符)

3. 图中节点④和⑤是调用C库创建线程时的环节,创建线程首先调用 __allocate_thread 函数申请线程私有的栈内存(stack)等,然后调用 clone方法进行线程创建.申请 stack 采用的时 mmap 的方式,节点⑤代码节选如下:

  

if (space == MAP_FAILED) {

    __libc_format_log(ANDROID_LOG_WARN,

                      "libc",

                      "pthread_create failed: couldn't allocate %zu-bytes mapped space: %s",

                      mmap_size, strerror(errno));

    return NULL;

  }

节点④代码

导致OOM 发生的原因


1. 文件描述符 (fd) 数目超限,即 proc/pid/fd 下文件数目突破 /proc/pid/limits 中的限制。可能的发生场景有:短时间内大量请求导致 socket fd 数激增,大量(重复)打开文件等 ;

2. 线程数超限,即proc/pid/status中记录的线程数(threads 项)突破 /proc/sys/kernel/threads-max 中规定的最大线程数。可能的发生场景有:app 内多线程使用不合理,如多个不共享线程池的 OKhttpclient 等等 ;

3. 传统的 java 堆内存超限,即申请堆内存大小超过了 Runtime.getRuntime().maxMemory();

4. (低概率)32 为系统进程逻辑空间被占满导致 OOM;

5. 其他

监控措施


可以利用 linux inotify 机制进行监控:

watch /proc/pid/fd来监控 app 打开文件的情况,

watch /proc/pid/task来监控线程使用情况.

参考

https://mp.weixin.qq.com/s/Z7cCCF8jzS6NpVEd0b0Emg

https://mp.weixin.qq.com/s/AjtzDxwJzyqC95FXgDPS1g

Android系统匿名共享内存AshmemAnonymous Shared Memory)驱动程序源代码分析:http://blog.csdn.net/luoshengyang/article/details/6664554

技术内幕AndroidLinux内核的增强 Ashmem:http://www.jmpcrash.com/?p=315

Android Kernel Features:

https://elinux.org/Android_Kernel_Features#ashmem

匿名内存映射

https://www.wikiwand.com/en/Mmap#/File-backed_and_anonymous

Clone系统调用

http://man7.org/linux/man-pages/man2/clone.2.html

虚拟内存布局

https://events.linuxfoundation.org/sites/events/files/slides/elc_2016_mem.pdf






  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值