摘要:
本文发现了一类OOM(OutOfMemoryError),这类OOM的特点是崩溃时java堆内存和设备物理内存都充足,探索并解释了这类OOM抛出的原因。
关键字:
OutOfMemoryError ,OOM,pthread_create failed , Could not allocate JNI Env
对于每一个移动开发者,内存是都需要小心使用的资源,而线上出现的OOM(OutOfMemoryError)都会让开发者抓狂,因为我们通常仰仗的直观的堆栈信息对于定位这种问题通常帮助不大。
网上有很多资料教我们如何“紧衣缩食“的利用宝贵的堆内存(比如,使用小图片,bitmap复用等),可是:
- 线上的OOM真的全是由于堆内存紧张导致的吗?
- 有没有App堆内存宽裕,设备物理内存也宽裕的情况下发生OOM的可能?
内存充裕的时候出现OOM崩溃?看似不可思议,然而,最近笔者在调查一个问题的时候,通过自研的APM平台发现公司的一个产品的大部分OOM确实有这样的特征,即:
既然内存充足,这时候为什么会有OOM崩溃呢?
在详细描述问题之前,先弄清楚一个问题:
什么导致了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 记录设备的内存信息
图2-1 Android内存指标
通常认为OOM发生是由于java堆内存不够用了,即
Runtime.getRuntime().maxMemory()这个指标满足不了申请堆内存大小时
图2-2 Java堆OOM产生原因
这种OOM可以非常方便的验证(比如: 通过new byte[]的方式尝试申请超过阈值maxMemory()的堆内存),通常这种OOM的错误信息通常如下:
java.lang.OutOfMemoryError: Failed to allocate a XXX byte allocation with XXX free bytes and XXXKB until OOM
图2-3 堆内存不够导致的OOM的错误信息
而前面已经提到了,本文中发现的OOM案例中堆内存充裕(Runtime.getRuntime().maxMemory()大小的堆内存还剩余很大一部分),设备当前内存也很充裕(ActivityManager.MemoryInfo.availMem还有很多)。这些OOM的错误信息大致有下面两种:
java.lang.OutOfMemoryError: Could not allocate JNI Env
图2-4 OOM一的错误信息
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Out of memory
图2-5 OOM二的错误信息
Android系统中,OutOfMemoryError这个错误是怎么被系统抛出的?下面基于Android6.0的代码进行简单分析:
void Thread::ThrowOutOfMemoryError(const char* msg)
参数msg携带了OOM时的错误信息
图3-1 ART Runtime抛出的位置
系统源码文件:
/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";
图3-2 Java堆OOM
这种抛出的其实就是堆内存不够用的时候,即前面提到的申请堆内存大小超过了Runtime.getRuntime().maxMemory()
系统源码文件:
/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)));
图3-3 线程创建时OOM
对比错误信息,可以知道我们遇到的OOM崩溃就是这个时机,即创建线程的时候(Thread::CreateNativeThread)产生的。
那么,我们关心的就是Thread::CreateNativeThread时抛出的OOM错误,创建线程为什么会导致OOM呢?
既然抛出来OOM,一定是线程创建过程中触发了某些我们不知道的限制,既然不是Art虚拟机为我们设置的堆上限,那么可能是更底层的限制。
Android系统基于linux,所以linux的限制对于Android同样适用,这些限制有:
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
图3-4 Linux进程限制示例
用排除法筛选上面样例中的limits:
剩下的limits项中,Max open files这一项限制最可疑
Max open files表示每个进程最大打开文件的数目,进程每打开一个文件就会产生一个文件描述符fd(记录在/proc/pid/fd下面),这个限制表明fd的数目不能超过Max open files规定的数目。
后面分析线程创建过程中会发现过程中涉有及到文件描述符。
这些限制中与线程相关的是/proc/sys/kernel/threads-max,规定了每个进程创建线程数目的上限,所以线程创建导致OOM的原因也有可能与这个限制相关。
下面对上述的推断进行验证,分两步:本地验证和线上验收。
实验一:
触发大量网络连接(每个连接处于独立的线程中)并保持,每打开一个socket都会增加一个fd(/proc/pid/fd下多一项)
注:不只有这一种增加fd数的方式,也可以用其他方法,比如打开文件,创建handlerthread等等
E/art: ashmem_create_region failed for 'indirect ref table': Too many open files
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.netease.demo.oom, PID: 2435
java.lang.OutOfMemoryError: Could not allocate JNI Env
at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:730)
......
图3-5 FD数超限导致OOM的详细信息
可以看出,此OOM发生时的错误信息确与线上发现的OOM一的“Could not allocate JNI Env”吻合,因此线上上报的OOM一可能就是由FD数超限导致的,不过最终确定需要到线上进行验证(下一小节).
此外从ART虚拟机的Log中看出,还有一个关键的信息“ art: ashmem_create_region failed for 'indirect ref table': Too many open files”,后面会用于问题定位及解释。
实验二:
创建大量的空线程(不做任何事情,直接sleep)
实验预期:
当线程数(可以在/proc/pid/status中的threads项实时查看)超过/proc/sys/kernel/threads-max中规定的上限时产生OOM崩溃
实验结果:
W libc : pthread_create failed: clone failed: Out of memory
W art : Throwing OutOfMemoryError "pthread_create (1040KB stack) failed: Out of memory"
E AndroidRuntime: FATAL EXCEPTION: main
Process: com.netease.demo.oom, PID: 4973
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:745)
......
图3-6 线程数超限导致的OOM详细信息
可以看出错误信息与我们线上遇到的OOM二吻合:"pthread_create (1040KB stack) failed: Out of memory"
另外ART虚拟机还有一个关键Log:“pthread_create failed: clone failed: Out of memory”,后面会用于问题定位及解释。
W/libc: pthread_create failed: couldn't allocate 1069056-bytes mapped space: Out of memory
W/art: Throwing OutOfMemoryError "pthread_create (1040KB stack) failed: Try again"
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.netease.demo.oom, PID: 8638
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again
at java.lang.Thread.nativeCreate(Native Method)
at java.lang.Thread.start(Thread.java:1063)
......
图3-7 逻辑地址空间占满导致的OOM
本地尝试复现的OOM错误信息中图[3-5]与线上OOM一情况比较吻合,图[3-6]与线上OOM二的情况比较吻合,但线上的OOM一真的时FD数目超限,OOM二真的是由于华为手机线程数超限的原因导致的吗?最终确定还需要取线上设备的数据进行验证.
验证方法:
下发插件到线上用户,当Thread.UncaughtExceptionHandler捕获到OutOfMemoryError时记录/proc/pid目录下的如下信息:
线上OOM一验证
发生OOM一的线上设备中采集到的信息:
由此,证明线上的OOM一确实是由于FD数目过多导致的OOM,推断验证成功.
OOM一的定位与解决:
最终原因是App中使用的长连接库再某些时候会有瞬时发出大量http请求的bug(导致FD数激增),已修复
线上OOM二验证
集中在华为系统的OOM二崩溃时收集到的信息样例如下,(收集的样例中包含的devicemodel有VKY-AL00,TRT-AL00A,BLN-AL20,BLN-AL10,DLI-AL10,TRT-TL10,WAS-AL00等):
推断验证成功,即线程数受限导致创建线程时clone failed导致了线上的OOM二。
OOM二的定位与解决:
关于App业务代码中的问题还在定位修复中
下面从代码分析本文描述的OOM是怎么发生的,首先线程创建的简易版流程图如下所示:
图3-8 线程创建流程
上图中,线程创建大概有两个关键的步骤:
下面对流程图中关键节点(图中有标号的)进行说明:
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());
图3-9 Thread:CreateNativeThread节选
可知:
...
__BIONIC_ERRDEF( EAGAIN , 11, "Try again" )
__BIONIC_ERRDEF( ENOMEM , 12, "Out of memory" )
...
__BIONIC_ERRDEF( EMFILE , 24, "Too many open files" )
...
图3-10 系统错误定义_errdefs.h
if (fd.get() == -1) {
*error_msg = StringPrintf("ashmem_create_region failed for '%s': %s", name, strerror(errno));
return nullptr;
}
图3-11 MemMap:MapAnonymous节选
我们线上的OOM一的错误信息"ashmem_create_region failed for 'indirect ref table': Too many open files",与此处打印的信息吻合."Too many open files"的错误描述说明此处的errno(系统全局错误标识)为24(见图[3-10]系统错误定义_errdefs.h).
由此看出我们线上的OOM一是由于文件描述符数目已满,ashmem_create_region无法返回新的FD而导致的.
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;
}
图3-12 __create_thread_mapped_space节选
打印的错误信息与图[3-7]中进程逻辑地址占满导致的OOM错误信息吻合,图[3-7]中错误信息" Try again"说明系统全局错误标识errno为11(见图[3-10]系统错误定义_errdefs.h).
pthread_create过程中,节点4相关代码如下:
int rc = clone(__pthread_start, child_stack, flags, thread, &(thread->tid), tls, &(thread->tid));
if (rc == -1) {
int clone_errno = errno;
// We don't have to unlock the mutex at all because clone(2) failed so there's no child waiting to
// be unblocked, but we're about to unmap the memory the mutex is stored in, so this serves as a
// reminder that you can't rewrite this function to use a ScopedPthreadMutexLocker.
pthread_mutex_unlock(&thread->startup_handshake_mutex);
if (thread->mmap_size != 0) {
munmap(thread->attr.stack_base, thread->mmap_size);
}
__libc_format_log(ANDROID_LOG_WARN, "libc", "pthread_create failed: clone failed: %s", strerror(errno));
return clone_errno;
}
图3-13 pthread_create节选
此处输出的错误日志"pthread_create failed: clone failed: %s"与我们线上发现的OOM二吻合,图[3-6]中的错误描述" Out of memory"说明系统全局错误标识errno为12(见图[3-10]系统错误定义_errdefs.h).
由此线上的OOM二就是由于线程数的限制而在节点5 clone失败导致OOM.
综上,可以导致OOM的原因有以下几种:
可以利用linux的inotify机制进行监控:
POC(Proof of concept) 代码参见:https://github.com/piece-the-world/OOMDemo
作者:陶菜菜
链接:https://www.jianshu.com/p/e574f0ffdb42
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。