Эта статья является текстовой рекордной версией контента, который я поделился в компании Techday. Я не хотел писать такую длинную статью, потому что многие одноклассники спрашивают, можете ли я написать связанную текстовую версию, нет никого.,
Говоря об этом, это второй раз, когда я делился на TechDay. Четыре года назад, первый TechDay не знал неба и неба, и я поднялся, чтобы рассказать о «Лучших практиках MySQL». Теперь, когда я думаю о эти лучшие практики, кажется, что лучшие практики не очень хороши. Без дальнейших церемоний, давайте посмотрим на конкретное содержание.
Тема этой публикации — «Изучение проблем с памятью», которая будет разделена на следующие аспекты для обсуждения.
- Основополагающий принцип знания памяти Linux
- Основной принцип реализации malloc и free
- Принцип реализации ptmalloc2
- Внутренняя структура Arena, Heap, Chunk, Bin
- Описание проблем с памятью, связанные с Java Development
Зачем делиться этой темой
Поскольку это самый частый вопрос, который мне задают, упс, что мне делать, если моя программа не работает, что мне делать, если память моей программы превышает квоту и ее убивает k8s, моя программа, похоже, использует много памяти, это нормально?
На следующем рисунке показано, как использование памяти Nginx было слишком высоким и было убито операционной системой, когда мы ранее проводили стресс-тест.
Когда приходит трафик измерения давления, память Nginx продолжает расти, и когда она достигает примерно 130G, ее убивает системный OOM-killer, на входе трафика возникают узкие места, и измерение давления не может продолжаться. Когда возникает проблема со зрелым компонентом, как ее устранить?
Вторая причина в том, что знания об управлении памятью очень велики, такие как тройка linux, CPU, IO и память.Память можно сказать самая сложная, и она неразрывно связана с производительностью CPU и IO Только после того, как проблема с памятью может быть действительно решена, многие проблемы, связанные с производительностью Linux.
Недавно мы читали в 8:00 утра, каждый день с 8:00 до 9:00, чтобы разделить час, я провел почти 18 часов, говоря о знаниях, связанных с памятью, но есть еще много вещей, которые я не рассмотрел. . Поэтому я хотел бы воспользоваться этой возможностью, чтобы еще раз кое-что добавить, прежде чем мы снова поговорим о совместном использовании, поскольку наш процесс разработки является наиболее часто используемым чем-то понятным.
Понимание памяти может помочь нам лучше понять некоторые проблемы, такие как:
- Почему golang изначально поддерживает функции с несколькими возвращаемыми значениями
- Как работает golang escape-анализ
- Как анализировать утечки памяти Java вне кучи
- Как реализованы умные указатели C++
Часть II: Принципы управления памятью в Linux
Далее давайте приступим к основному содержанию этого обмена: принцип управления памятью Linux, как и три основные проблемы человека, есть три похожие проблемы в памяти, что такое память, откуда берется память и куда она уходит. после его выпуска.
Виртуальная память и физическая память
Прежде всего, давайте взглянем на виртуальную память и физическую память.Взаимосвязь между виртуальной и физической памятью подтверждает известную поговорку: «Любую проблему в операционной системе можно решить с помощью абстрактного среднего уровня», а виртуальная память — это именно тот.
Без виртуальной памяти процесс может напрямую изменять данные памяти других процессов.Появление виртуальной памяти изолирует использование памяти, и каждый процесс имеет независимое, непрерывное и унифицированное виртуальное адресное пространство (хорошая иллюзия). Как влюбленный мужчина, иметь ее — все равно, что владеть всем миром.
Приложение видит только виртуальную память. Отображение виртуальной памяти в физическую выполняется через MMU. Мы знаем, что память Linux выровнена по 4k, 4k = 2^12, а младшие 12 бит в виртуальном адресе на самом деле являются смещением. .
Теперь представим таблицу страниц как одномерный массив, для каждой страницы в виртуальном адресе выделяется слот в массиве, и этот слот указывает на реальный адрес в физическом адресе. Тогда есть такой адрес виртуальной памяти 0x1234010, затем 0x010 — смещение в странице, 0x1234 — номер виртуальной страницы, ЦП находит адрес страницы физической памяти, отображаемый 0x1234 через MMU, предполагая, что это 0x2b601000, и затем добавляет смещение страницы 0x010, то был найден реальный адрес физической памяти 0x2b601010. Как показано ниже.
Четырехуровневая таблица страниц Linux
Однако у этого метода есть очевидная проблема.Виртуальное адресное пространство может быть очень большим.Даже если взять в качестве примера 32-битную систему, виртуальное адресное пространство составляет 4 ГБ, объем страница составляет 4 КБ, а размер массива составляет 786432 (1024 * 1024). Каждая запись в таблице страниц хранится в 4 байтах, поэтому карта пространства размером 4 ГБ требует 3 МБ памяти для хранения таблицы карты. (Примечание: здесь много информации говорит о 4M, и в этом нет большой проблемы. Я считаю, что пространство ядра является общим, так что не слишком запутывайтесь.)
Для одного процесса занимать 3М вроде бы ничего, но таблица страниц эксклюзивна для процесса, каждому процессу нужна своя таблица страниц, если процессов сотня, то он будет занимать 300Мб памяти, это только для того, чтобы сделать Память потрачено на сопоставление адресов. Если учесть большое виртуальное адресное пространство 64-битных систем, эта реализация одномерного массива еще более непрактична.
Чтобы решить эту проблему, люди используют концепцию уровня.Структура таблицы страниц разделена на несколько уровней, а размер записи в таблице страниц связан только с тем, сколько пространства виртуальной памяти фактически используется. В предыдущем представлении одномерного массива количество записей в таблице страниц пропорционально размеру виртуального адресного пространства.Эта многоуровневая структура делает ненужной неиспользуемую память для выделения записей в таблице страниц.
Поэтому люди придумали форму многоуровневой таблицы страниц, которая очень удобна, поскольку виртуальное адресное пространство большинства областей фактически не используется, а использование многоуровневой таблицы страниц позволяет значительно сократить объем памяти, занимаемой таблицей страниц. сам. В 64-битных системах Linux использует четырехуровневую таблицу страниц.
- PGD: Глобальный каталог страниц, глобальный каталог страниц, является таблицей страниц верхнего уровня.
- PUD: Page Upper Directory, верхний каталог страницы, является таблицей страниц второго уровня.
- PMD: средний каталог страниц, который представляет собой таблицу страниц третьего уровня.
- PTE: запись таблицы страниц, таблица страниц, последний уровень таблицы страниц, указывающий на физическую страницу.
Как показано ниже.
Приложение видит только виртуальную память, а не физические адреса. Конечно, есть способ каким-то образом получить физический адрес через виртуальный адрес. Например, в этом примере мы выделили пространство размером 1 МБ и вернули виртуальный адрес 0x7ffff7eec010.Как узнать адрес физической памяти, соответствующий этому виртуальному адресу?
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
int main() {
char *p = NULL;
p = malloc(1024 * 1024);
*p = 0;
printf("ptr: %p, pid: %d\n", p, getpid());
getchar();
return 0;
}
ptr: 0x7ffff7eec010, pid: 2621
На прикладном уровне нет возможности сделать это, но для нас это не сложно, давайте напишем модуль расширения ядра для достижения этой функции.
Написать модуль ядра тоже очень просто, разделив его на следующие шаги:
- Определите два хука обратного вызова, module_init, module_exit
- Task_struct процесса получается через входящий pid, pgd можно получить через переменную mm в task_struct и входящий адрес виртуальной памяти va.
- Можно получить pud через pgd, а потом получить pmd, лучше всего получить pte, этот pte уже хранит страничный фрейм физической памяти
- Окончательный адрес физической памяти можно получить по нижнему 12-битному смещению внутри страницы.
Упрощенный код показан ниже.
#include <linux/module.h>
...
int my_module_init(void) {
unsigned long pa = 0;
pgd_t *pgd = NULL; pud_t *pud = NULL;
pmd_t *pmd = NULL; pte_t *pte = NULL;
struct pid *p = find_vpid(pid);
struct task_struct *task_struct = pid_task(p, PIDTYPE_PID);
pgd = pgd_offset(task_struct->mm, va); // 获取第一级 pgd
pud = pud_offset(pgd, va); // 获取第二级 pud
pmd = pmd_offset(pud, va); // 获取第三级 pmd
pte = pte_offset_kernel(pmd, va); // 获取第四级 pte
unsigned long page_addr = pte_val(*pte) & PAGE_MASK;
unsigned long page_addr &= 0x7fffffffffffffULL;
page_offset = va & ~PAGE_MASK;
pa = page_addr | page_offset; // 加上偏移量
printk("virtual address 0x%lx in RAM Page is 0x%lx\n", va, pa);
return 0;
}
void my_module_exit(void) {
printk("module exit!\n");
}
module_init(my_module_init); // 注册回调钩子
module_exit(my_module_exit); // 注册回调钩子
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Arthur.Zhang");
MODULE_DESCRIPTION("A simple virtual memory inspect");
Мы компилируем этот модуль ядра для создания файла .ko, а затем загружаем файл .ko, передавая идентификатор процесса и адрес виртуальной памяти.
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
insmod my_mem.ko pid=2621 va=0x7ffff7eec010
Затем выполните dmesg -T, чтобы увидеть реальное значение физического адреса.
[Sat Oct 10 05:11:12 2020] virtual address 0x7ffff7eec010 in RAM Page is 0x2358a4010
Вы можете видеть, что в этом примере физический адрес, соответствующий виртуальному адресу 0x7ffff7eec010, равен 0x2358a4010.
Полный код смотрите по адресу:GitHub.com/Артур-Станция…
структура памяти процесса
Связь между виртуальным ядром и физической памятью упоминалась ранее. Мы знаем, что формат исполняемого файла в Linux — elf, а elf — это статический файл. Этот статический файл состоит из разных разделов. Здесь мы называем его разделом. , Некоторые разделы, относящиеся к среде выполнения, сопоставляются с виртуальным адресным пространством процесса, например сегмент кода и сегмент данных на рисунке. Помимо этой части статической области, есть много областей потребления динамической памяти после старта процесса, таких как область стека, кучи, mmap.
Изображение ниже является частью макета памяти, выводимого нашей онлайн-службой Java с использованием pmap, как показано на изображении ниже.
Итак, как вы смотрите на эти части? Это требует от нас глубокого понимания того, как делится память процесса в Linux.
Изучение принципа управления памятью libc
Существует три уровня управления памятью в Linux: первый уровень — это уровень управления пользователями, такими как пул памяти нашей собственной программы, буферный пул mysql, а второй уровень — библиотека времени выполнения C. Эта часть кода является оболочкой для ядро, что удобно. Приложение верхнего уровня удобнее разрабатывать, а следующий уровень — это наш уровень ядра.
Сегодня мы хотим сосредоточиться на среднем уровне.Этот уровень реализуется библиотекой libc.Давайте подробно рассмотрим, как осуществляется управление памятью в libc.
Основная идея управления нагрузкой Linux является иерархическое управление, оптом и розничная, а также скрывает внутренние детали. Нам также необходимо иметь в виду, что управление кузоком в LIBC предназначено для небольшого выделения и выделения памяти. Для объединения интерфейсов программирования также поддерживается большая память.
Давайте сначала рассмотрим две функции освобождения приложений памяти, malloc и free.Определения этих двух функций следующие.
#include <stdlib.h>
void *malloc(size_t size);
void free(void *ptr);
Эти две функции вовсе не являются системными вызовами, это просто инкапсуляция системных вызовов brk, mmap, munmap, так зачем нам libc, чтобы инкапсулировать еще один уровень с этими системными вызовами?
Одна из основных причин заключается в том, что системные вызовы очень дороги, а использование и освобождение памяти происходят очень часто, поэтому libc использует пакетное применение, а затем, как спекулянта памяти, медленно распределяет ее по следующим приложениям.
Вторая причина в единстве программирования.Например, иногда используется brk, а иногда используется mmap, что не очень дружелюбно. brk нужно залочить под многопоточность, а использовать malloc очень вкусно.
Распределитель памяти Linux
В Linux есть много видов распределителей памяти.Вначале это был dlmalloc, разработанный Дугом Ли.Этот распределитель не поддерживает многопоточность.При многопоточности он будет конкурировать за глобальные блокировки.Позднее кто-то разработал ptmalloc на основе dmalloc с добавлением многопоточной памяти.Поддержка, в дополнение к официальному ptmalloc для Linux, различные крупные производители разработали различные алгоритмы malloc, такие как jemalloc от facebook и tcmalloc от google.
Эти распределители памяти призваны решить две проблемы: степень детализации многопоточной блокировки, будь то глобальная блокировка, локальная блокировка или отсутствие блокировки. Вторая проблема — это проблема небольшого высвобождения памяти и фрагментации памяти, например, jemalloc имеет значительное преимущество в фрагментации памяти.
Основные понятия ptmalloc
Далее, давайте посмотрим на распределитель памяти по умолчанию в Linux, ptmalloc.Я резюмировал четыре основные концепции, связанные с ним: Arena, Heap, Chunk, Bins.
Arena
Давайте сначала посмотрим на арену, Китайский перевод арены означает основное поле битвы и этап, что соответствует распределению памяти здесь, которое относится к основному полю битвы распределения памяти.
Появление Арены впервые используется для решения проблемы глобальных блокировок при многопоточности.Его идея состоит в том, чтобы позволить потоку максимально монополизировать Арену.При этом поток будет претендовать на одну или несколько куч, и освобожденная память попадет в корзину.Арена Используется для управления этими кучами и корзинами.
Как выглядит структура данных Арены? Это структура и может быть представлена следующей диаграммой.
Это односторонний циркулярный связанный список, который использует блокировки Mutex для обращения с несколькими резьбой, и освобожденные небольшие кусочки памяти помещают в структуру Bins.
Как упоминалось ранее, Arena попытается позволить одному потоку монополизировать одну блокировку, поэтому, если у меня есть тысячи потоков, будут ли генерироваться тысячи арен? Очевидно, что нет. Все проблемы с узкими местами, связанные с потоками, в конечном итоге придут к пределу количества ядер ЦП. Существует также верхний предел количества областей распределения. В 64-битной системе количество областей распределения равно ядрам ЦП. , В восемь раз больше, несколько арен образуют односторонний круговой связанный список.
Мы можем написать код для печати информации об Арене. Его принцип заключается в том, что для определенной программы адрес main_arena — это некий адрес, находящийся в библиотеке glibc, и мы можем напечатать этот адрес в средстве отладки gdb. Вы также можете использовать команду ptype для просмотра информации о структуре, соответствующей этому адресу, как показано на следующем рисунке.
На этой основе мы можем написать do while для обхода этого кругового связанного списка. Мы превращаем адрес main_arena в указатель на malloc_state, а затем делаем обход, пока не будет пройден заголовок связанного списка.
struct malloc_state {
int mutex;
int flags;
void *fastbinsY[NFASTBINS];
struct malloc_chunk *top;
struct malloc_chunk *last_remainder;
struct malloc_chunk *bins[NBINS * 2 - 2];
unsigned int binmap[4];
struct malloc_state *next;
struct malloc_state *next_free;
size_t system_mem;
size_t max_system_mem;
};
void print_arenas(struct malloc_state *main_arena) {
struct malloc_state *ar_ptr = main_arena;
int i = 0;
do {
printf("arena[%02d] %p\n", i++, ar_ptr);
ar_ptr = ar_ptr->next;
} while (ar_ptr != main_arena);
}
#define MAIN_ARENA_ADDR 0x7ffff7bb8760
int main() {
...
print_arenas((void*)MAIN_ARENA_ADDR);
return 0;
}
Вывод следующий.
Тогда зачем различать основное распределение и неосновное распределение?
Это немного похоже на отношения между императором и принцем.Основная область выделения только одна.Он также имеет привилегию использовать область кучи рядом с сегментом DATA.Применяется для освобождения памяти путем корректировки указателя brk.
В некотором смысле зона кучи - это просто расширение сегмента данных.
非主分配区呢? It is more like a feudal in the field, the princes start their own businesses, on the use of mmap wholesale chunk of memory (64M) when it wants to heap memory as a child (Sub Heap), and then slowly to the upper retail Приложения.
Выбежен на 64 м, а затем открыть новый, но также используйте связанные списки, связанные между множеством субстатов, куча арена может иметь несколько детей. Взял в контент, мы продолжим подробно.
Heap
Далее, давайте рассмотрим вторую основную концепцию ptmalloc2, куча используется для представления большой непрерывной области памяти.
О куче в области основного распределения говорить нечего. Здесь мы сосредоточимся на подкуче "неосновного распределения" (также известной как смоделированная куча). Как упоминалось ранее, резка и розничная продажа.
Итак, как понимать фразу «раскрой розницы»? Его реализация также очень проста.Сначала применяется для 64M нечитаемой, недоступной для записи, неисполняемой (PROT_NONE) области памяти.Когда память необходима, используйте mprotect, чтобы изменить разрешение области памяти на чтение и запись (R+W ), эта область памяти может быть выделена вышестоящему приложению.
В качестве примера возьмем структуру памяти нашего предыдущего Java-процесса.
Две области памяти посередине принадлежат подкуче, и их общий размер составляет 64 МБ, а затем с помощью mprotrect выделяется область памяти 1,3 МБ, а оставшаяся область около 63 МБ недоступна для чтения и чтения. исполняемая область, подлежащая выделению.
Знай что это использование? Это очень полезно. Если вы Google All Java Off-куча памяти и другие проблемы, существует высокая вероятность того, что вы найдете проблему Magic 64M памяти Linux. С знанием здесь у вас будет лучшее представление о том, что такое проблема с памятью 64M.
Как и в предыдущей арене, мы также можем пройтись по всем спискам кучи всех арен в коде, код выглядит следующим образом.
struct heap_info {
struct malloc_state *ar_ptr;
struct heap_info *prev;
size_t size;
size_t mprotect_size;
char pad[0];
};
void dump_non_main_subheaps(struct malloc_state *main_arena) {
struct malloc_state *ar_ptr = main_arena->next;
int i = 0;
while (ar_ptr != main_arena) {
printf("arena[%d]\n", ++i);
struct heap_info *heap = heap_for_ptr(ar_ptr->top);
do {
printf("arena:%p, heap: %p, size: %d\n", heap->ar_ptr, heap, heap->size);
heap = heap->prev;
} while (heap != NULL);
ar_ptr = ar_ptr->next;
}
}
#define MAIN_ARENA_ADDR 0x7ffff7bb8760
dump_non_main_subheaps((void*)MAIN_ARENA_ADDR);
Chunk
Далее, давайте посмотрим на базовую единицу выделения, чанк.Чанк буквально означает «толстый блок; толстый срез», а чанк — это базовая единица распределения памяти в glibc. Начните с простого примера.
#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);
}
Логика этого кода состоит в том, чтобы вызвать malloc три раза подряд, каждый раз выделяя 1k памяти, а затем мы будем наблюдать его адрес памяти.
./malloc_test
0x602010
0x602420
0x602830
Видно, что разница между адресами памяти 0х410, а 1024 равно 0х400, что за лишние 0х10 байт? Давайте сначала нажмем без стола.
Оглядываясь назад на malloc и free, мы не можем не задать себе вопрос: параметр функции free — это всего лишь указатель, откуда она знает, сколько памяти нужно освободить?
#include <stdlib.h>
void *malloc(size_t size);
void free(void *ptr);
Гонконгский писатель Чжан Сяосянь сказал: «У всего есть цена, а цена счастья — это боль». Для того, чтобы хранить 1к данных, действительно нужны некоторые данные для записи метаданных этого куска памяти. Этот дополнительный фрагмент данных называется заголовком фрагмента и имеет длину 16 байт. Это дополнительные 0x10 байт, которые мы видели ранее.
Этот метод очень распространен путем добавления головы перед фактическими данными, такими как новое целое число (1024) в java, фактический размер данных намного больше, чем 4 байта, он имеет огромный заголовок объекта, в котором хранится хэш-код object , После нескольких GC это было расценено как синхронизация блокировки.
Можно сказать, что Java раздута.
Прежде чем мы продолжим смотреть, что хранится в этом 16-байтовом заголовке, ниже показана его структурная схема.
Он разделен на две части, первые 8 байтов представляют размер предыдущего блока фрагментов, следующие 8 байтов представляют размер текущего блока фрагментов, поскольку блок фрагментов выровнен по 16 байтам, поэтому нижние 4 байта бесполезны. , три из них используются в качестве маркерных битов, эти три являются AMP, где A указывает, является ли это основной областью распределения, M указывает, является ли это большим фрагментом, выделенным mmap, а P указывает, используется ли предыдущий фрагмент.
Взяв в качестве примера предыдущий пример, мы можем использовать gdb для просмотра этой части памяти.
Вы можете видеть, что 8 байтов, соответствующих размеру, равны 0x0411. Откуда взялось это значение? На самом деле он выровнен до 16 байт в соответствии с размером + 8 плюс три младших бита B001.
0x0400 + 0x10 + 0x01 = 0x0411
Поскольку при использовании чанка prev_size его следующего чанка не имеет значения, эти 8 байтов могут использоваться этим текущим чанком. Не удивляйтесь, это просто так. Далее давайте посмотрим на повторное использование prev_size в чанках. Тестируемый код выглядит следующим образом.
#include <stdlib.h>
#include <string.h>
void main() {
char *p1, *p2;
p1 = (char *)malloc(sizeof(char) * 18); // 0x602010
p2 = (char *)malloc(sizeof(char) * 1); // 0x602030
memcpy(p1, "111111111111111111", 18);
}
Скомпилируйте этот исходный файл, затем используйте gdb для отладки и пошагового просмотра адресов p1 и p2.
p/x p1
$2 = 0x602010
(gdb) p/x p2
$3 = 0x602030
Затем выведите область памяти рядом с p1 и p2.
(gdb) x/64bx p1-0x10
0x602000: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x602008: 0x21 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x602010: 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31
0x602018: 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31
0x602020: 0x31 0x31 0x00 0x00 0x00 0x00 0x00 0x00
0x602028: 0x21 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x602030: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x602038: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
Схема показана на рисунке ниже.
Место ограничено, здесь я показываю только структуру чанка malloc, чанки Free, Top и Last Remainder не раскрываются, вы можете обратиться к другим материалам.
Bins
Давайте посмотрим на последнюю концепцию, корзина Корзины малой памяти.
Корзина памяти делится на две категории: первая — обычная корзина, а вторая — быстрая корзина.
- fastbin использует односвязный список, размер свободных фрагментов в каждом связанном списке определяется, а вставка и удаление выполняются в конце очереди.
- Обычные бункеры разделены на три типа: маленькие, большие и несортированные в зависимости от размера восстановленной памяти. Они хранятся в вдвойне связанном списке. Самая большая разница между ними состоит в том, что диапазон размеров кусков, которые они хранят, отличается.
Далее мы рассмотрим детали этих двух бункеров.
Обычные бины хранятся в двусвязном списке и определяются в виде массива, всего элементов 254. Два элемента массива образуют бин, а двусвязный список формируется fd и bk Структура немного похожа на игрушка с девятью цепями.
Таким образом, общее количество обычных ячеек равно 254/2 = 127. Среди них есть только 1 несортированная корзина, 62 маленькие корзины, 63 большие корзины, и одна еще не использовалась, как показано на следующем рисунке.
smallbin
Среди них smallbin используется для поддержки блоков памяти
largebin
Чанки в одной и той же цепочке в largebin имеют «разные» размеры.
- Разделить на 6 групп
- Количество бинов в каждой группе — 33, 15, 8, 4, 2, 1, а максимальный допустимый размер фрагмента в каждом связанном списке — 64 байт, 512 байт, 4096 байт, 32768 байт, 262 144 байт и т. д.
Структура показана на рисунке ниже.
unsorted bin
Несортированный бин имеет только один двусвязный список со следующими характеристиками.
- Незанятые фрагменты не сортируются
- Блоки памяти размером более 128 байт сначала помещаются в несортированную корзину при освобождении.
Его структура показана на рисунке ниже.
Ниже представлен обзор всех обычных корзин.
FastBin
Имея общий бин, присмотритесь к FastBin, FastBin предназначен для повышения эффективности распределения памяти малого размера, которая устроена следующим образом.
Он имеет следующие характеристики.
- Выделения памяти меньше 128 байт будут сначала искать в быстрой корзине.
- Односвязный список, размер фрагмента в каждом связанном списке одинаков, существует 7 связанных списков без фрагментов, размер фрагмента каждого бина составляет 32 байт, 48 байт, 64 байт, 80 байт, 96 байт, 112 байт, 128 байт.
- Поскольку это односвязный список, указатель bk в fastbin не используется, а указатель fd первого фрагмента указывает на специальный адрес 0
- Флаг P всегда равен 1, обычно не объединяется
- FIFO, добавление и удаление выполняются с конца очереди
Быстрые мусорные баки можно рассматривать как небольшую часть кеша маленьких бункеров.
Выделение и освобождение памяти
Обладая предыдущими знаниями, мы можем ответить на вопрос в начале обмена, откуда берется память. О применении больших блоков памяти можно сказать немногое: системный вызов mmap напрямую применяется к блоку, и когда он освобождается, он напрямую возвращается в операционную систему.
Приложение для небольших блоков памяти намного сложнее.Принцип заключается в том, чтобы сначала найти его в корзине фрагментов.Если вы найдете его лучшим, вы можете вернуть его напрямую, не обращаясь к ядру для применения. Как это делается?
Сначала размер реального чанка будет рассчитан по размеру входящего.По этому размеру смотрим, находится ли он в интервале, который не является фастбином.Если есть,возвращаемся напрямую из фастбина.Если нет,пробуем smallbin,и затем, если нет маленькой корзины, она инициирует однократное слияние, а затем поиск в несортированной корзине. Если нет, она будет искать в большой корзине. Если верхний блок не вырезается, а верхний блок отсутствует, куча будет повторно применяется или размер кучи будет скорректирован, как показано на следующем рисунке.
Теперь давайте ответим на последний вопрос.Куда девается память после освобождения?Существуют разные стратегии обработки в зависимости от размера.
- Сверхмаленький блок памяти, соответствующий фастбину, сразу кладется в фастбин односвязный список, и он быстро освобождается.Озвучка так мало места, стоит ли мне с ней долго возиться?
- Сверхбольшая память напрямую возвращается в логику управления, чтобы ядро не попало в корзину. Голос за кадром говорит о том, что к крупным клиентам нужно особое отношение, ведь крупные клиенты редкость.
- Большинство из них находятся посередине, и при освобождении они первыми помещаются в несортированную корзину. Объединяйте и переносите свободные блоки в зависимости от ситуации и обновляйте верхний блок, когда он приближается к верхнему. Это нормальная жизнь.
стек памяти
Большая часть из вышеперечисленного - это память в куче.На самом деле, еще одна очень важная вещь - это память стека.Размер памяти стека по умолчанию в LInux составляет 8 МБ, а затем добавляется защищенная область 4 КБ. Эта защищенная область 4 КБ не может быть прочитана, записана или записана. Выполнить, когда стек выходит за пределы, его можно найти раньше и как можно быстрее потерпеть неудачу.
На этом рисунке показана структура памяти стека типичного собственного потока Linux.Вы можете видеть пространство стека размером 8 МБ и защитную область размером 4 КБ.
Для Java были внесены некоторые незначительные изменения, размер стека по умолчанию составляет 1 МБ, а затем есть КРАСНАЯ область 4 КБ и желтая область 8 КБ, чтобы сделать более детальный контроль переполнения стека, здесь желтая область и красная область Что Я уже писал статью о потоках и стеках, поэтому не буду их здесь подробно описывать.
Часть 3. Описание проблем с памятью, связанных с разработкой
Переезд в наш последний раздел, связанные с разработкой Вопросы памяти.
Xmx и потребление памяти
Первое, о чем я хочу поговорить, это часто задаваемый вопрос: почему потребление памяти моим Java-приложением намного больше, чем у Xmx?Это также очень часто задаваемый вопрос о переполнении стека.
На самом деле, мы должны выяснить, что в дополнение к потреблению памяти в куче процесс имеет много других накладных расходов, как показано ниже.
- Heap
- Code Cache
- GC накладные расходы
- Metaspace
- Thread Stack
- Direct Buffers
- Mapped files
- C/C++ Собственное потребление памяти
- Накладные расходы самого malloc
- . . .
Пользователи больших памяти не шутки. Согласно годам практики разумно установить XMX примерно до 65% памяти контейнера.
занятие ВИЭ
Второй вопрос, использование RES в команде top очень высокое, значит ли это, что программа действительно много потребляет?
На самом деле нет, возьмем в качестве примера простейшую программу на Java, при использовании -Xms1G -Xmx1G для запуска программы.
java -Xms1G -Xmx1G MyTest
Его объем памяти выглядит следующим образом.
Мы немного изменили команду запуска, чтобы добавить AlwaysPreTouch, как показано ниже.
java -XX:+AlwaysPreTouch -Xms1G -Xmx1G MyTest
На этот раз занятость RES показана ниже.
Бизнес-программа 1G здесь на самом деле не используется, но JVM записывает память, так что, когда она на самом деле используется позже, нет необходимости инициировать прерывание неисправности страницы, чтобы фактически применяться для физической памяти.
Использование памяти не настолько мало, насколько это возможно, но также количество GC и время паузы GC.
Заменить распределитель памяти по умолчанию
Распределитель памяти по умолчанию в Linux не очень хорошо работает с точки зрения производительности и фрагментации памяти.Вы можете попробовать заменить распределитель памяти по умолчанию на jemalloc или tcmalloc, просто добавьте новую переменную среды LD_PRELOAD.
LD_PRELOAD=/usr/local/lib/libjemalloc.so
В реальной службе использование памяти одной службой изменилось с 7G на 3G, и эффект все еще очень заметен.
анализ собственной памяти
Анализ памяти кучи Java очень прост: команда jmap создает дамп памяти, а затем использует jprofile, mat, perfma и другие платформы для быстрого анализа. Тем не менее, использование встроенной памяти слишком велико, это более проблематично. Здесь можно использовать мощные функции профилей jemalloc и tcmalloc. Взяв в качестве примера jemalloc, отношение памяти к приложению может быть сгенерировано в svg.
export MALLOC_CONF=prof:true,lg_prof_sample:1,lg_prof_interval:30,prof_prefix:jeprof.out
jeprof –svg /path/to/svg jeprof.out.* > out.svg
Полученная схема SVG показана ниже.
резюме
Это введение - только вершина айсберга проблем с памятью. Многие детали не были подробно описаны в этом обмене. Если у вас есть какие-либо вопросы, вы можете прийти пообщаться.
После этого PPT кто-то сказал мне, что здесь было немного холодно. Концовка не плохая, я думала, что это середина.
Полный общий PPT можно скачать здесь:GitHub.com/Артур-Станция…