Я столкнулся с проблемой восстановления памяти, вызванной glibc, процесс поиска причины и экспериментов более интересен, в основном он включает следующее:
- Типичная проблема с большой областью памяти 64 МБ в Linux
- Основной принцип распределителя памяти glibc ptmalloc2
- Как написать пользовательскую библиотеку динамической компоновки хука malloc, чтобы
- Глибко принцип распределения памяти (арена, структура кусочки, мусорные баки и т. Д.)
- Влияние malloc_trim на реальное восстановление памяти
- инструмент отладки кучи gdb использует
- Введение и применение библиотеки jemalloc
задний план
Некоторое время назад одноклассник сообщил, что проект java RPC был убит, потому что память контейнера превысила квоту в 1500 МБ вскоре после того, как он был запущен в контейнере.Я помог взглянуть.
После запуска в локальной среде Linux объем памяти RES, видимый top после запуска JVM, превысил 1,5 ГБ, как показано на следующем рисунке.
Прежде всего, я думаю о проверке распределения памяти.Использование Arthas-хороший выбор.Войдите в панель управления, чтобы просмотреть текущее использование памяти, как показано ниже.
Видно, что память кучи, занятая найденным процессом, составляет всего около 300М, а не куча (некуча) тоже очень мала, в сумме всего около 500М, так что кто потребляет память. Это зависит от нескольких компонентов памяти JVM.
Куда уходит память JVM?
Память JVM примерно разделена на следующие части
- Куча: eden, metaspace, old area и т.д.
- Стек потоков: каждый стек потоков резервирует размер стека потоков 1M.
- Нечек (не куча): содержит код_cache, MetAscace и т. Д.
- Память вне кучи: память вне кучи, запрашиваемая unsafe.allocateMemory и DirectByteBuffer
- Память, выделенная нативным кодом (код C/C++)
- Существует также память, необходимая для запуска самой JVM, например GC.
Далее есть подозрения, что могут быть утечки в памяти вне кучи и в родной памяти. Память вне кучи можно отслеживать, включив NMT (NativeMemoryTracking), а также-XX:NativeMemoryTracking=detail
Запустите программу еще раз и обнаружите, что значение использования памяти намного меньше, чем значение использования памяти RES.
Поскольку NMT не отслеживает объем памяти, запрошенный нативным кодом (код C/C++), в основном подозревается, что это вызвано нативным кодом. Помимо нативного кода, используемого rockdb, в нашем проекте осталась только сама JVM. Продолжайте расследование.
Знакомая для Linux проблема с памятью 64M
Используйте pmap -x для просмотра распределения памяти и обнаружите, что существует большое количество областей памяти около 64 МБ, как показано на следующем рисунке.
Это явление слишком знакомо, не является ли это классической проблемой памяти 64M в linux glibc?
ptmalloc2 и арена
Ранняя версия malloc в Linux была реализована Дугом Ли. У нее есть серьезная проблема, заключающаяся в том, что для выделения памяти существует только одна область выделения (арена). Каждый раз, когда память выделяется, область выделения должна быть заблокирована. Существует острая конкуренция за одновременные запросы на снятие блокировки памяти. Буквальное значение слова «арена» — «сцена; арена», которая может быть основным полем битвы производительности библиотеки распределения памяти.
Так что поработайте с другой версией, разве вы не жестокая многопоточная конкуренция замков, тогда я открою еще несколько арен, ситуация с замком, естественно, улучшится.
Wolfram Gloger улучшил на основе Doug Lea, так что malloc Glibc может поддерживать многопоточность, то есть ptmalloc2. На основе только одной области распределения добавляется неосновная область распределения (неосновная арена).Основная область распределения только одна, а неосновных областей распределения может быть много.Конкретное количество будет описано позже. .
При вызове malloc для выделения памяти он сначала проверяет, существует ли уже область распределения в частной переменной текущего потока. Если он существует, попробуйте заблокировать арену
- Если блокировка прошла успешно, эта область выделения будет использоваться для выделения памяти.
- Если блокировка не удалась, это означает, что ее используют другие потоки, затем просмотрите список арен, чтобы найти незаблокированную область арены, и, если она будет найдена, используйте эту область арены для выделения памяти.
Основная область выделения может использовать brk и mmap для обращения к виртуальной памяти, а неосновная область выделения может быть только mmap. Размер блока виртуальной памяти, который glibc применяет каждый раз, равен64MB
, glibc затем разрезается на мелкие части для розничной продажи в соответствии с потребностями приложения.
Это типичная проблема 64 МБ в распределении памяти процесса Linux, сколько таких областей? В 64-битных системах это значение равно8 * number of cores
, если это 4 ядра, имеется до 32 областей памяти размером 64M.
Это потому, что слишком много арен?
Помогает ли установка MALLOC_ARENA_MAX=1?
При добавлении этой переменной среды для запуска процесса java область памяти в 64 МБ действительно исчезла, но сконцентрировалась в большой области памяти, близкой к 700 МБ, как показано на следующем рисунке.
На данный момент проблема высокого использования памяти не решена, а дальше метаться дальше.
кто выделяет и освобождает память
Затем напишите кастомный хук функции malloc. Хук фактически использует переменную окружения LD_PRELOAD для замены реализации функции в glibc.Перед вызовом функций malloc, free, realloc и calloc печатается журнал, а затем вызывается фактический метод. Взяв в качестве примера хук функции malloc, часть кода показана ниже.
// 获取线程 id 而不是 pid
static pid_t gettid() {
return syscall(__NR_gettid);
}
static void *(*real_realloc)(void *ptr, size_t size) = 0;
void *malloc(size_t size) {
void *p;
if (!real_malloc) {
real_malloc = dlsym(RTLD_NEXT, "malloc");
if (!real_malloc) return NULL;
}
p = real_malloc(size);
printLog("[0x%08x] malloc(%u)\t= 0x%08x ", GETRET(), size, p);
return p;
}
Установите LD_PRELOAD для запуска JVM.
LD_PRELOAD=/app/my_malloc.so java -Xms -Xmx -jar ....
В процессе запуска JVM включите jstack для одновременной печати стека потоков.Когда процесс jvm полностью запущен, проверьте журнал вывода malloc и журнал jstack.
Здесь выводится журнал malloc из десятков M, как показано ниже. Первый столбец журнала — это идентификатор потока.
Используйте журнал обработки awk, чтобы подсчитать количество обработок потоков.
cat malloc.log | awk '{print $1}' | less| sort | uniq -c | sort -rn | less
284881 16342
135 16341
57 16349
16 16346
10 16345
9 16351
9 16350
6 16343
5 16348
4 16347
1 16352
1 16344
Видно, что поток 16342 выделяет и освобождает память наиболее брутально, так что же делает этот поток? Выполняя поиск потока 16342 (0x3fd6) в выходном журнале jstack, вы можете увидеть, что распаковка пакета jar выполняется много раз.
Java использует класс java.util.zip.Inflater для обработки zip, и вызов его конечного метода освобождает собственную память. Увидев это, я подумал, что метод end не был вызван. Это действительно возможно. Метод close класса java.util.zip.InflaterInputStream не будет вызывать метод Inflater.end в некоторых сценариях, как показано ниже.
Рановато радоваться. На самом деле это не так.Даже если вызов верхнего уровня не вызывает Inflater.end, метод finalize класса Inflater также вызывает метод end.Я заставил GC попробовать это.
jcmd `pidof java` GC.run
Подтвердил по логу GC, что FullGC действительно срабатывал, но память не подвела. Просматривая утечки памяти с помощью таких инструментов, как valgrind, ничего не было обнаружено.
Если в реализации самой JVM утечки памяти нет, то это проблема самого glibc.Вызов free возвращает память glibc, но glibc не освобождает ее окончательно.Диллер памяти сам обрезал память.
Принцип распределения памяти glibc
Это очень сложная тема, если вы совсем не знакомы с этой темой, то предлагаю вам сначала ознакомиться со следующими материалами.
-
Understanding glibc malloc SP lol IT fun.WordPress.com/2015/02/10/…
-
«Управление памятью Glibc — анализ исходного кода Ptmalloc2», автор Taobao Huating Masterpaper.see bug.org/papers/Arch…
В целом необходимо понимать следующие понятия:
- Область выделения памяти Арена
- блок памяти
- Корзины для свободных чанков (баков)
Область выделения памяти Арена
Понятие области распределения памяти Arena было введено ранее, и оно относительно простое. Чтобы более интуитивно понять внутреннюю структуру кучи, вы можете использовать пакет расширения кучи gdb, наиболее распространенными являются
- libheap: GitHub.com/cloudburst/…
- Pwngdb: GitHub.com/Market без антенны AP/PW…
- pwndbg: GitHub.com/Taste Годовой отчет/Редко…
Это также инструменты, которые можно использовать для тем, связанных с кучей CTF.Затем для ознакомления используется инструмент Pwngdb. Введите информацию об арене, чтобы увидеть список арен, как показано ниже.
В этом примере имеется 1 основная зона распределения «Арена» и 15 дополнительных областей распределения «Арена».
Структура блока памяти
Понятие чанка также легко понять.Чанк буквально означает "большой блок", предназначенный для пользователей.Память, выделенная пользователями, представлена чанками.
Это может быть трудно понять, поэтому давайте проиллюстрируем это на реальном примере.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
void *p;
p = malloc(1024);
printf("%p\n", p);
p = malloc(1024);
printf("%p\n", p);
p = malloc(1024);
printf("%p\n", p);
getchar();
return (EXIT_SUCCESS);
}
Этот код выделяет 1 КБ памяти три раза, и адрес памяти:
./malloc_test
0x602010
0x602420
0x602830
Результат вывода pmap показан ниже.
Вы можете видеть, что адрес первой выделенной области памяти 0x602010 находится по смещению 16 (0x10) от базового адреса (0x602000) этой области памяти.
Давайте посмотрим на адрес второй выделенной области памяти 0x602420 и 0x602010. Разница 1040 = 1024 + 16(0x10)
Память, выделенная в третий раз, одинакова и так далее, 0x10 байт каждый раз пусты. Что осталось 0x10 посередине?
Понятно, если посмотреть с помощью gdb, посмотреть на 32-байтовую область, начинающуюся с 0x10 байт перед этими тремя адресами памяти.
Вы можете видеть, что на самом деле хранится 0x0411,
0x0411 = 1024(0x0400) + 0x10(block size) + 0x01
Среди них 1024 — это, очевидно, размер запрашиваемой пользователем области памяти Что такое 0x11? Поскольку все выделения памяти выровнены, младшие 3 бита фактически не имеют значения для размера памяти, а младшие 3 бита заимствуются для специального значения. Используемая структура фрагмента показана ниже.
Значения трех младших цифр следующие:
- A: Указывает, что фрагмент принадлежит к основной области размещения или неосновной области размещения, если он принадлежит к неосновной области размещения, этот бит устанавливается в 1, в противном случае он устанавливается в 0.
- M: Указывает область памяти, из которой получен текущий фрагмент. M равно 1, чтобы указать, что фрагмент выделяется из области отображения mmap, в противном случае он выделяется из области кучи.
- P: указывает, используется ли предыдущий блок, если P равно 0, это означает, что предыдущий блок свободен, тогда первое поле prev_size блока допустимо.
Самые младшие три цифры в этом примереb001
, A = 0 означает, что этот фрагмент не принадлежит основной области выделения, M = 0 означает, что он выделен из области кучи, а P = 1 означает, что используется предыдущий фрагмент.
Нагляднее это видно из исходного кода glibc.
#define PREV_INUSE 0x1
/* extract inuse bit of previous chunk */
#define prev_inuse(p) ((p)->size & PREV_INUSE)
#define IS_MMAPPED 0x2
/* check for mmap()'ed chunk */
#define chunk_is_mmapped(p) ((p)->size & IS_MMAPPED)
#define NON_MAIN_ARENA 0x4
/* check for chunk from non-main arena */
#define chunk_non_main_arena(p) ((p)->size & NON_MAIN_ARENA)
#define SIZE_BITS (PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA)
/* Get size, ignoring use bits */
#define chunksize(p) ((p)->size & ~(SIZE_BITS))
Структура выделенного чанка была представлена ранее. Структура свободного чанка после освобождения отличается. Существует также структура, называемая верхним чанком, которая здесь не будет раскрываться.
Корзины для чанков
bin буквально означает «мусорный бак». После того, как приложение вызовет free для освобождения памяти, чанк не обязательно будет немедленно возвращен в систему, а будет отрезан вторым дилером glibc. Это также сделано из соображений эффективности.Когда пользователь в следующий раз запрашивает выделение памяти, ptmalloc2 сначала попытается найти подходящую область памяти из пула свободной памяти фрагментов и вернуть ее приложению, избегая, таким образом, частых системных вызовов brk и mmap.
Для более эффективного управления выделением и высвобождением памяти в ptmalloc2 используется массив, поддерживающий 128 бинов.
Эти бункеры описаны ниже.
- bin0 в настоящее время не используется
- bin1 это
unsorted bin
, который в основном используется для хранения только что освобожденных блоков кучи фрагментов и оставшихся блоков кучи после выделения большого блока кучи Размер не ограничен. - bin2~bin63 да
small bins
, используемый для поддержания блоков памяти фрагментов index * 16Например, размер блока связанного списка, соответствующего ячейке 2, равен 32 (0x20), а размер части связанного списка, соответствующего ячейке 3, равен 48 (0x30). Примечания: есть проблема в pdf Taobao, pdf вsize * 8
, посмотрите исходный код, он должен быть*16
Верно - бин64~бин126 да
large bins
, используется для поддержки блоков кучи > 1024 Б. Размер блоков кучи в одном и том же связанном списке не обязательно одинаков. Конкретные правила не рассматриваются в этом введении и не будут расширяться.
В частности, в этом примере информацию о бинах каждой арены можно просмотреть в Pwngdb. Как показано ниже.
fastbin
В нормальных условиях программа будет часто выделять небольшой объем памяти во время работы.Если эти небольшие объемы памяти часто объединяются и вырезаются, эффективность будет относительно низкой.Поэтому, в дополнение к вышеупомянутым компонентам корзины, ptmalloc также имеет очень Важная структура fastbin, предназначенная для управления небольшими блоками кучи памяти.
В 64-битной системе после освобождения блока кучи памяти размером не более 128 байт он сначала будет помещен в fastbin, флаг P блока в fastbin всегда равен 1, а блок кучи fastbin будет использоваться как используется, поэтому он не будет использоваться.
При выделении менее 128 байт памяти ptmalloc сначала будет искать соответствующий свободный блок в фастбине, а если нет, то ищет в других бинах.
С другой точки зрения, fastbin можно рассматривать как кеш smallbin.
Фрагментация памяти и восстановление
Далее, давайте проведем эксперимент, чтобы увидеть, как фрагментация памяти влияет на высвобождение памяти glibc.Код выглядит следующим образом.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define K (1024)
#define MAXNUM 500000
int main() {
char *ptrs[MAXNUM];
int i;
// malloc large block memory
for (i = 0; i < MAXNUM; ++i) {
ptrs[i] = (char *)malloc(1 * K);
memset(ptrs[i], 0, 1 * K);
}
//never free,only 1B memory leak, what it will impact to the system?
char *tmp1 = (char *)malloc(1);
memset(tmp1, 0, 1);
printf("%s\n", "malloc done");
getchar();
printf("%s\n", "start free memory");
for(i = 0; i < MAXNUM; ++i) {
free(ptrs[i]);
}
printf("%s\n", "free done");
getchar();
return 0;
}
В программе сначала выделите часть памяти размером 500 МБ, а затем выделите 1 байт памяти (фактически больше 1 байта, но это не влияет на описание), а затем освободите 500 МБ памяти.
Объем памяти до освобождения показан ниже.
После вызова free используйте top для просмотра результатов RES следующим образом.
Видно, что glibc на самом деле не возвращает память в систему. Вместо этого он помещается в собственную несортированную корзину, которую можно четко увидеть с помощью инструмента arenainfo в gdb.
0x1efe9200 — это 520 000 000 в десятичном виде, что составляет около 500 МБ памяти, которую мы только что освободили.
Если я закомментирую второй malloc в коде, glibc сможет немедленно освободить память.
В этом эксперименте сравнивалось влияние внутреннего стадиона на потребление памяти GLIBC.
glibc и malloc_trim
Функция malloc_trim предоставляется в glibc, а документация находится здесь:
Из документации следует, что он должен просто вернуть всю свободную память на вершине кучи в систему, а вернуть дыры в памяти на вершине кучи возможности нет. Но это не так: в этом примере вызов malloc_trim действительно возвращает системе более 500 МБ памяти.
gdb --batch --pid `pidof java` --ex 'call malloc_trim()'
Глядя на исходный код glibc, базовая реализация malloc_trim была изменена для прохождения всей арены, затем для прохождения всех бинов для каждой арены, выполнения системного вызова madvise для уведомления MADV_DONTNEED и уведомления ядра о том, что этот блок может быть переработан.
Это можно подтвердить синхронно с помощью сценария Systemtap.
probe begin {
log("begin to probe\n")
}
probe kernel.function("SYSC_madvise") {
if (ppid() == target()) {
printf("\nin %s: %s\n", probefunc(), $$vars)
print_backtrace();
}
}
При выполнении malloc_trim происходит большое количество системных вызовов madvise, как показано на следующем рисунке.
Здесь behavior=0x4 означает MADV_DONTNEED, len_in означает длину, а start означает начальный адрес памяти.
malloc_trim также подходит для экспериментов с фрагментацией памяти в предыдущем разделе.
Джемаллок дебют
Поскольку это похоже на утечку памяти из-за проблемы восстановления фрагментированной памяти, вызванной стратегией выделения памяти glibc, есть ли лучшая библиотека malloc для фрагментированной памяти? Наиболее распространенными в отрасли являются tcmalloc от Google и jemalloc от Facebook.
Я пробовал оба из них, и эффект jemalloc более очевиден.Используйте LD_PRELOAD для монтирования библиотеки jemalloc.
LD_PRELOAD=/usr/local/lib/libjemalloc.so
Перезапустите программу Java, вы увидите, что потребление RES памяти уменьшилось примерно до 1G.
Использование jemalloc примерно на 500 М меньше, чем glibc, лишь немногим более 900 М для malloc_trim.
Что касается того, почему jemalloc так силен в этой сцене, это сложная тема, я не буду ее здесь раскрывать, если у вас есть время, вы можете подробно рассказать о принципе реализации jemalloc.
После многих экспериментов malloc_trim может вызвать сбой JVM, поэтому будьте осторожны при его использовании.
После замены ptmalloc2 на jemalloc значительно снижается занятость памяти RES процессом, а за производительностью и стабильностью требуется дальнейшее наблюдение.
Дополнительно
Недавно я пишу простую библиотеку malloc, возможно, я написал ее до того, как узнал, какие болевые точки пытаются решить tcmalloc и jemalloc, и как достигается компромисс в сложных проектах.
резюме
Проблемы, связанные с памятью, относительно сложны, и на них влияет много факторов.Если это собственная проблема прикладного уровня, это проще всего.Если это проблема glibc или самого ядра, это можно сделать только с помощью жирного шрифта предположения. Проверьте это. Выделение памяти и управление ею — относительно сложная тема, и я надеюсь представить ее подробно в следующей статье.
Приведенное выше содержание может быть неправильным, просто посмотрите на идею.