Невероятный ООМ

Java

Резюме:
 В этой статье обнаружен класс OOM (OutOfMemoryError),Этот тип OOM характеризуется достаточным объемом памяти кучи Java и физической памяти устройства при сбое., исследует и объясняет, почему выбрасываются такие OOM.

Ключевые слова:  OutOfMemoryError, OOM, ошибка pthread_create, не удалось выделить среду JNI.

1. Введение

Для каждого мобильного разработчика память — это ресурс, который нужно использовать осторожно, и ошибка OOM (OutOfMemoryError), появляющаяся в сети, сведет разработчиков с ума, потому что интуитивная информация о стеке, на которую мы обычно полагаемся, обычно не помогает в обнаружении таких проблем.  В интернете много материалов, которые учат нас использовать драгоценную кучевую память «намертво» (например, с помощью мелких картинок, мультиплексирования битмапов и т. д.), но:

  • Действительно ли OOM на линии вызван тесной памятью кучи?
  • Существует ли возможность OOM, когда памяти кучи приложения много, а физической памяти устройства также много?

Сбой OOM при переполнении памяти?Кажется невероятным, однако, когда автор недавно исследовал проблему с помощью собственной APM-платформы, было обнаружено, что большинство OOM продукта компании действительно имеют такие характеристики, а именно:

  • При сбое OOM память кучи java намного ниже верхнего предела, установленного виртуальной машиной Android, и физической памяти достаточно, и места на SD-карте достаточно.

 Поскольку памяти достаточно, почему в это время происходит сбой OOM?

2. Описание проблемы

  Прежде чем подробно описывать проблему, уточним одну проблему:

    Что вызывает ООМ?

Вот несколько API-интерфейсов для официально заявленных пределов памяти Android:

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 Причины OOM кучи Java  Этот тип OOM может быть очень удобным для проверки (например: попытаться подать заявку на память кучи, превышающую пороговое значение 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 обычно бывают следующих двух типов:

  1. Этот тип OOM встречается в различных моделях Android 6.0 и Android 7.0 и называетсяООМ один, сообщение об ошибке выглядит следующим образом:
java.lang.OutOfMemoryError: Could not allocate JNI Env

         Рисунок 2-4 Сообщение об ошибке OOM 1

  1. OOM, который в основном встречается в мобильных телефонах Huawei с Android 7.0 и выше (EmotionUI_5.0 и выше), называетсяООМ II, соответствующее сообщение об ошибке выглядит следующим образом:
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Out of memory

         Рисунок 2-5 Сообщение об ошибке OOM 2

3. Анализ проблемы и решение

3.1 Анализ кода

  Как система выдает ошибку OutOfMemoryError в системе Android? Ниже приведен простой анализ кода на базе Android 6.0:

  1. Код, в котором виртуальная машина Android, наконец, выдает OutOfMemoryError, находится по адресу/art/runtime/thread.cc
void Thread::ThrowOutOfMemoryError(const char* msg)
参数msg携带了OOM时的错误信息

         Рис. 3-1 Положение для метания во время выполнения ART

  1. При поиске кода можно обнаружить, что указанный выше метод вызывается в следующих местах и ​​выдает ошибку OutOfMemoryError.
  • Первое место, когда куча работает
系统源码文件:
    /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 OOM кучи Java

  • Второе место - когда поток создан
系统源码文件:
    /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).

  • Существуют и другие сообщения об ошибках, такие как «[XXXClassName] длины XXX будет переполнена», поскольку система ограничивает длину строки или массива и не обсуждается в этой статье.

Затем нас интересует ошибка OOM, возникающая, когда Thread::CreateNativeThread,Почему создание потока вызывает OOM?

3.2 Вывод

 Поскольку 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 

        Рис. 3-4 Пример ограничения процесса Linux  Используйте метод исключения для фильтрации ограничений в приведенном выше примере:

  • Ограничение максимального размера стека и максимального количества процессов для всей системы, а не для процесса, исключая
  • Макс. заблокированная память, исключенная, будет проанализирована позже, вызов mmap, используемый для выделения частного стека потока во время процесса создания потока, не устанавливает MAP_LOCKED, поэтому это ограничение не имеет ничего общего с процессом создания потока.
  • Максимальное количество ожидающих сигналов, пороговое значение количества сигналов в слое c, не имеет значения, исключено
  • Максимальный размер очереди сообщений, механизм Android IPC не поддерживает очередь сообщений, исключить

  Среди остальных пунктов лимитов,Max open filesЭто ограничение является наиболее сомнительным.
 Максимальное представление открытых файловМаксимальное количество открытых файлов на процесс,обработатьКаждый раз, когда файл открывается, создается файловый дескриптор fd (записывается в /proc/pid/fd), этот предел показывает, чтоКоличество fd не может превышать количество, указанное в Max open files..
  Последующий анализ процесса создания потока обнаружит, что в этом процессе участвуют файловые дескрипторы.

  1. Ограничения описаны в /proc/sys/kernel

  Эти ограничения связаны с потоками./proc/sys/kernel/threads-max, который указывает верхний предел количества потоков, создаваемых каждым процессом., поэтому причина создания потока, вызывающего OOM, также может быть связана с этим ограничением.

3.3 Проверка

Приведенный выше вывод проверяется ниже в два этапа: локальная проверка и онлайн-принятие.

  • Локальная проверка: проверка выводов локально,Попытка воспроизвести OOM в соответствии с сообщением об ошибке, показанным на рис. [2-4] OOM 1 и рис. [2-5] OOM 2.
  • Онлайн прием:Это действительно происходит из-за приведенного выше вывода, когда выпускается подключаемый модуль и принимается OOM онлайн-пользователя..

локальная аутентификация

эксперимент первый:
  Инициировать большое количество сетевых соединений (каждое соединение находится в отдельном потоке) и сохранить его, каждый открытый сокет будет увеличивать fd (еще один элемент в /proc/pid/fd)
 Примечание: не только этот способ увеличить количество fd, но и другие методы, такие как открытие файлов, создание потоков обработчиков и т. д.

  • Экспериментальные ожидания:
    OOM возникает, когда количество файловых файлов процесса (полученных с помощью ls /proc/pid/fd | wc -l ) превышает максимальное количество открытых файлов, указанное в /proc/pid/limits.
  • Результаты экспериментов:
    Когда количество fds достигает максимального количества открытых файлов, указанного в /proc/pid/limits, продолжение открытия потоков действительно приведет к OOM. Сообщение об ошибке и стек следующие:
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 Подробная информация об OOM, вызванном превышением номера FD
Как можно заметить,Сообщение об ошибке, когда возникает этот OOM, действительно такое же, как «Не удалось выделить JNI Env» для OOM, найденного в Интернете.совпадение, поэтому OOM, о котором сообщается в Интернете, являетсявозможныйЭто вызвано тем, что номер FD превышает лимит, но окончательное решение необходимо проверить онлайн (следующий раздел).
  Кроме того, из Журнала виртуальной машины ART есть еще одна ключевая информация"art: ashmem_create_region не удалось выполнить "таблицу непрямых ссылок": слишком много открытых файлов", который будет использоваться позже для локализации проблемы и объяснения.

Эксперимент 2:
 Создавать много пустых тем (ничего не делать, просто спать)

  • Экспериментальное ожидание: сбой OOM происходит, когда количество потоков (которое можно просмотреть в режиме реального времени в элементе потоков в /proc/pid/status) превышает верхний предел, указанный в /proc/sys/kernel/threads-max.

  • Результаты экспериментов:

  1. OOM генерируется на мобильных телефонах Android 7.0 и выше, мобильных телефонах Huawei (EmotionUI_5.0 и выше), количество потоков этих мобильных телефонов очень мало (это должны быть лимиты, специально измененные прошивкой Huawei), и каждый процесс одновременно разрешено открывать не более 500 тем, поэтому их легко воспроизвести. Сообщение об ошибке во время 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 2, с которым мы столкнулись в сети: «Ошибка pthread_create (стек 1040 КБ): нехватка памяти».
  Кроме того, в виртуальной машине ART есть ключевой Журнал:«Ошибка pthread_create: ошибка клонирования: недостаточно памяти», который будет использоваться позже для локализации проблемы и объяснения.

  1. Верхний предел количества потоков мобильного телефона для других ПЗУ относительно велик, и воспроизвести вышеуказанные проблемы непросто. но,Для 32-битных систем OOM также будет происходить, когда логического адресного пространства процесса недостаточно., Каждому потоку обычно требуется около 1 МБ пространства стека mapp (размер стека можно задать самостоятельно), 32 — это логический адрес системного процесса, 4 ГБ, а пользовательское пространство меньше 3 ГБ. Недостаточно логического адресного пространства (Используемый адрес логического пространства можно просмотреть в записи VmPeak/VmSize в /proc/pid/status.), OOM, сгенерированный при создании потока, содержит следующую информацию:
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, вызванный заполнением логического адресного пространства

Онлайн-прием и решение проблем

Рисунок [3-5] в сообщении об ошибке OOM при локальной попытке воспроизведения больше соответствует онлайновой ситуации OOM 1, а рисунок [3-6] больше соответствует онлайновой ситуации OOM 2., но количество FD превышает лимит, когда действительно превышено количество OOM 1 на линии. Действительно ли OOM 2 вызван превышением лимита количества потоков мобильных телефонов Huawei? Для окончательного определения также необходимо взять данные онлайн-устройства для проверки.

Метод проверки подлинности:
  Распространите подключаемый модуль среди онлайн-пользователей и запишите следующую информацию в каталог /proc/pid, когда Thread.UncaughtExceptionHandler перехватит OutOfMemoryError:

  1. Количество файлов в каталоге /proc/pid/fd (номер fd)
  2. Элемент потоков в /proc/pid/status (текущее количество потоков)
  3. Информация журнала OOM (информация вне стека также содержит другую предупреждающую информацию

Онлайн проверка OOM one
 Информация, полученная с онлайн-устройства, на котором происходит OOM 1:

  1. Количество файлов в каталоге /proc/pid/fd такое же, как и максимальное количество открытых файлов в /proc/pid/limits., что доказывает, что число ФД заполнено
  2. Информация журнала во время сбоя в основном такая же, как на рисунке [3-5].

Таким образом, доказано, чтоОнлайн-OOM действительно является OOM, вызванным слишком большим количеством FD, и делается вывод, что проверка прошла успешно..

Позиционирование и решение OOM one:
 Последняя причина заключается в том, что библиотека длинных соединений, используемая в приложении, иногда мгновенно выдает большое количество HTTP-запросов (что приводит к резкому увеличению количества FD), что было исправлено.

Онлайн проверка OOM два
Образец информации, собранный при сбое OOM 2 системы Huawei, выглядит следующим образом (модели устройств, включенные в собранные образцы, включают VKY-AL00, TRT-AL00A, BLN-AL20, BLN-AL10, DLI-AL10, TRT-TL10, WAS-AL00 и т. д.):

  1. Все записи потоков в /proc/pid/status достигли верхнего предела: Threads: 500
  2. Информация журнала во время сбоя в основном такая же, как на рисунке [3-6].

Проверка вывода прошла успешно, т.е.Ограниченное количество потоков приводит к сбою клонирования при создании потоков, что приводит к онлайновому OOM 2..

Позиционирование и решение OOM II:
 Проблема в бизнес-коде приложения все еще решается

3.4 Объяснение

Давайте проанализируем, как происходит описанный в этой статье OOM из кода.Во-первых, упрощенная блок-схема создания потока выглядит следующим образом:

Рисунок 3-8 Процесс создания потока

На приведенном выше рисунке показано два ключевых этапа создания потока:

  • в первой колонкеСоздайте частную структуру потока JNIENV(среда выполнения JNI, используемая на уровне C для вызова кода уровня Java)
  • во второй колонкеВызовите функцию pthread_create библиотеки posix C для создания потока.

Ключевые узлы блок-схемы (отмечены на рисунке) описаны ниже:

  1. Узел 1 на рисунке, функция Thread:CreateNativeThread часть функции Thread:CreateNativeThread в /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());

         Рисунок 3-9 Thread:CreateNativeThread отрывок Мы знаем:

  • Сообщение об ошибке OOM, когда JNIENV не создан успешно,«Не удалось выделить JNI Env», что соответствует OOM в тексте.
  • При сбое pthread_create появляется сообщение об ошибке OOM: «Ошибка pthread_create (стек %s): %s». Подробную информацию об ошибке дает возвращаемое значение (код ошибки) pthread_create. Соответствие между кодами ошибок и описаниями ошибок см.bionic/libc/include/sys/_errdefs.hопределение в . Конкретное сообщение об ошибке OOM 2 в тексте — «Недостаточно памяти», что означает, что возвращаемое значение pthread_create равно 12.
...
__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

  1. Узлы ② и ③ на рисунке являются ключевыми узлами для создания процесса JNIENV.Узел ②/art/runtime/mem_map.ccРоль функции MemMap:MapAnonymous заключается в применении памяти для таблицы Indirect_Reference_table (уровень C используется для хранения локальных/глобальных переменных JNI) в структуре JNIENV., метод запроса памяти - это функция, показанная в узле ③ashmem_create_region (создает часть анонимной разделяемой памяти ashmen и возвращает дескриптор файла). Выдержки из кода узла ② следующие:
  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 для« таблицы косвенных ссылок »: слишком много открытых файлов» согласуется с информацией, напечатанной здесь.. Описание ошибки «Слишком много открытых файлов» указывает, что errno (системный глобальный идентификатор ошибки) здесь равен 24 (см. Рисунок [3-10] Определение системной ошибки _errdefs.h).
 Видно, что наш онлайнOOM one вызван тем, что количество файловых дескрипторов заполнено, а ashmem_create_region не может вернуть новый FD..

  1. Узлы ④ и ⑤ на рисунке — это ссылки при вызове библиотеки C для создания потока.Вызовите функцию __allocate_thread, чтобы применить выделенную для потока память стека (стек) и т. д.,ПотомВызвать метод clone для создания потока. При подаче заявки на стек используется метод 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;
  }

         Рисунок 3-12 Фрагмент __create_thread_mapped_space
Напечатанное сообщение об ошибке согласуется с сообщением об ошибке OOM, вызванным полным логическим адресом процесса на рисунке [3-7]., сообщение об ошибке «Попробуйте еще раз» на рисунке [3-7] указывает, что системный глобальный флаг ошибки 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] указывает, что системный глобальный флаг ошибки errno равен 12 (см. Рисунок [3-10] Определение системной ошибки _errdefs.h).
 Из этого онлайнВторой OOM заключается в том, что из-за ограничения количества потоков клон на узле 5 не может вызвать OOM..

4. Заключение и мониторинг

4.1 Причины ООМ

Подводя итог, причины, которые могут привести к OOM, следующие:

  1. Количество файловых дескрипторов (fd) превышает лимит, то есть количество файлов в proc/pid/fd превышает лимит в /proc/pid/limits. Возможные сценарии:
    Большое количество запросов за короткий промежуток времени приводит к всплеску количества fds сокета, большому количеству (повторяющихся) открытых файлов и т.д.
  2. Превышено количество потоков, то есть количество потоков (элемент threads), записанных в proc/pid/status, превышает максимальное количество потоков, указанное в /proc/sys/kernel/threads-max. Возможные сценарии:
    Использование многопоточности в приложении нецелесообразно, например, несколько OKhttpclient, которые не используют общие пулы потоков и т. д.
  3. традиционныйограничение памяти кучи java, то есть размер памяти кучи приложения превышает Runtime.getRuntime().maxMemory()
  4. (Низкая вероятность) 32 означает, что логическое пространство системного процесса заполнено, что приводит к OOM.
  5. разное

4.2 Меры контроля

Вы можете использовать механизм inotify Linux для мониторинга:

  • смотреть /proc/pid/fd для мониторинга открытия файлов приложением,
  • смотреть /proc/pid/task для мониторинга использования потока.

5. Демо

Код POC (Proof of concept) см.:GitHub.com/piece-he-and-me…