启动线程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::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 szie是1040KB.这个信息case 1中crash的信息一致
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));
这里主要涉及到linux的clone系统调用(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
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() 大小的堆内存还剩余很大一部分,物理内存,SD卡ActivityManager.MemoryInfo.availMem 还有很多)但是依然抛出OOM即case1 & 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抛出时机
JVM对throw 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 size,Max processes 的限制是整个系统的,不是针对某个进程的
Max locked memory 线程创建过程中分配线程私有 stack 使用的 mmap 调用没有设置 MAP_LOCKED,所以这个限制与线程创建过程无关
Max pending signals,c 层信号个数阈值,这个限制与线程创建过程无关
Max msgqueue size,Android 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./proc/pid/fd 目录下文件数 (fd 数)
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_table(C 层用于存储 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来监控线程使用情况