专栏名称: 互联网后端架构
主要介绍Java后端架构。其中也会掺杂一些前端、GO、Python、Linux,目标:全栈工程师!---好像很牛叉的样子 ^-^
目录
相关文章推荐
51好读  ›  专栏  ›  互联网后端架构

超牛X的OOM

互联网后端架构  · 公众号  · 架构  · 2017-11-10 08:18

正文

摘要: 本文发现了一类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崩溃时,java堆内存远远低于Android虚拟机设定的上限,并且物理内存充足,SD卡空间充足

既然内存充足,这时候为什么会有OOM崩溃呢?

二. 问题描述

在详细描述问题之前,先弄清楚一个问题:

什么导致了OOM的产生?

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

通常认为OOM发生是由于java堆内存不够用了,即

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

这种OOM可以非常方便的验证(比如: 通过new byte[]的方式尝试申请超过阈值maxMemory()的堆内存),通常这种OOM的错误信息通常如下:

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

而前面已经提到了, 本文中发现的OOM案例中堆内存充裕(Runtime.getRuntime().maxMemory()大小的堆内存还剩余很大一部分),设备当前内存也很充裕(ActivityManager.MemoryInfo.availMem还有很多) 。这些OOM的错误信息大致有下面两种:

  1. 这种OOM在Android6.0,Android7.0上各个机型均有发生,文中简称为 OOM一 ,错误信息如下:

java.lang.OutOfMemoryError: Could not allocate JNI Env
  1. 集中发生在Android7.0及以上的华为手机(EmotionUI_5.0及以上)的OOM,简称为 OOM二 ,对应错误信息如下:

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

三. 问题分析及解决

3.1 代码分析

Android系统中,OutOfMemoryError这个错误是怎么被系统抛出的?下面基于Android6.0的代码进行简单分析:

  1. Android虚拟机最终抛出OutOfMemoryError的代码位于 /art/runtime/thread.cc

void Thread::ThrowOutOfMemoryError(const char* msg)
参数msg携带了OOM时的错误信息
  1. 搜索代码可以发现以下几个地方调用了上述方法抛出OutOfMemoryError错误

  • 第一个地方是堆操作时


这种抛出的其实就是堆内存不够用的时候,即前面提到的申请堆内存大小超过了Runtime.getRuntime().maxMemory()

  • 第二个地方是创建线程时


对比错误信息,可以知道我们遇到的OOM崩溃就是这个时机,即创建线程的时候(Thread::CreateNativeThread)产生的。

  • 还有其他的一些错误信息如”[XXXClassName] of length XXX would overflow“是系统限制String/Array的长度所致,不在本文讨论之列。

那么,我们关心的就是Thread::CreateNativeThread时抛出的OOM错误, 创建线程为什么会导致OOM呢?

3.2 推断

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

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


用排除法筛选上面样例中的limits:

  • Max stack size,Max processes的限制是整个系统的,不是针对某个进程的,排除

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

  • Max pending signals,c层信号个数阈值,无关,排除

  • Max msgqueue size,Android IPC机制不支持消息队列,排除

剩下的limits项中, Max open files 这一项限制最可疑
Max open files表示 每个进程最大打开文件的数目 ,进程 每打开一个文件就会产生一个文件描述符fd(记录在/proc/pid/fd下面) ,这个限制表明 fd的数目不能超过Max open files规定的数目
后面分析线程创建过程中会发现过程中涉有及到文件描述符。

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

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

3.3 验证

下面对上述的推断进行验证,分两步:本地验证和线上验收。

  • 本地验证:在本地验证推断, 试图复现与图[2-4]OOM一与图[2-5]OOM二所示错误消息一致的OOM

  • 线上验收: 下发插件,验收线上用户OOM时确实是由于上面的推断的原因导致的

本地验证

实验一:
触发大量网络连接(每个连接处于独立的线程中)并保持,每打开一个socket都会增加一个fd(/proc/pid/fd下多一项)
注:不只有这一种增加fd数的方式,也可以用其他方法,比如打开文件,创建handlerthread等等

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

  • 实验结果:
    当fd数目到达 /proc/pid/limits中规定的Max open files时,继续开线程确实会导致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崩溃

  • 实验结果:

  1. 在Android7.0及以上的华为手机(EmotionUI_5.0及以上)的手机产生OOM,这些手机的线程数限制都很小(应该是华为rom特意修改的limits),每个进程只允许最大同时开500个线程,因此很容易复现了。OOM时错误信息如下:







请到「今天看啥」查看全文