61

Как известно, сборщик мусора в C# (точнее, в CLR) время от времени проводит чистку оперативной памяти, освобождая память, занятую переменными, которые больше не используются. Кроме этого он также производит дефрагментацию памяти, "уплотняя" кучу.

В связи с этим происходит коррекция ссылок на объекты, пережившие сборку мусора. Вероятно, что-то аналогичное происходит при сборке мусора и в других языках.

В С++ нет сборщика мусора. В таком случае, даже если программист не забудет очистить всю память, выделенную ранее, то ее все равно может оказаться недостаточно из-за фрагментации, так как процесс дефрагментации не проводится.

То есть возможна парадоксальная ситуация, когда общий размер свободной памяти больше, чем требуется для создания нового объекта, но объект не может быть создан.
Так ли это? Есть ощущение, что я ошибаюсь в своих рассуждениях, но где?

DreamChild
  • 36,244
  • 2
    В 32-bit архитектурах такое возможно, в 64-bit практически невероятно. – avp Jan 07 '13 at 21:02
  • почему же? – DreamChild Jan 07 '13 at 21:06
  • 11
    Что значит "почему же?"?

    Почему в 64-bit практически невероятно?

    Например потому, что на создание такой ситуации уйдет слишком много времени.

    Про винду не знаю, а в линуксе память под большие объекты каждый раз запрашивается у ядра в виде целого числа страниц. Реально ядро может выделять и несмежные физические страницы, которые отображаются в непрерывный диапазон виртуальной памяти. При освобождении они возвращаются и дефрагментации не происходит.

    Ожидаемой Вами дефрагментации с кусками меньшими 4К добиться, наверное, можно, но такое время программы не живут.

    – avp Jan 07 '13 at 21:24
  • @DreamChild Во-первых сборщики мусора под C/C++ есть, например http://en.wikipedia.org/wiki/Boehm_garbage_collector (про дефрагментацию, разумеется, можно забыть).

    @avp живут, живут. Гигабайты свопа -- величина не бесконечная. :)

    – alexlz Jan 08 '13 at 03:11
  • @avp, не догоняю, причем тут количество бит. Ведь реальный физическуий объем памяти ограничен, и как правило, значительно меньше 18 ЭкзаБайт (даже учитывая своп-файл). И ситуация, описанная автором, даже в 64-битной системе вполне реальна - когда останется куча дырок, меньших по размеру, нежели объем объекта, который пытаемся выделить посредством new. – PaulD Aug 13 '13 at 15:09
  • @avp, я имею в виду, при удалении участков памяти, меньших 4 Кб, весьма маловероятно, что будет удаляться вся страница, содержащий такой объект, ведь в странице содержатся и объекты, которые должны житьд альше. – PaulD Aug 13 '13 at 15:12
  • @SoloMio, ниже есть ответ @mega, где он подробно обсуждает такую же идею.

    Если коротко, то проблема во времени, затрачиваемом на пэйджинг в такой ситуации с нехваткой памяти.

    – avp Aug 13 '13 at 16:26

5 Answers5

36

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

Другое дело, если фрагментация происходит на уровне виртуальной памяти. Это может запросто произойти в программах на С или С++, где происходит многочисленные выделения и удаления небольших фрагментов памяти. Это может привести к сильной утечки памяти (хотя в коде вся выделенная память освобождается!) и, возможно, к исчерпанию всей системной памяти. Но тут уже всему настанет кердык, если система такие ситуации не отслеживает и не выгружает "прожорливые" процессы.

skegg
  • 23,934
  • 2
  • 38
  • 69
30

Да, такое возможно. Причем часто возникает в нагруженных приложениях.

Для решения этой ситуации есть много решений. Например кастомные аллокаторы. Пусть в приложении нужно выделять много раз память под мелкие объекты. Кастомный аллокатор выделяет память немного большего размера (округляя до кратного 2 в степени). Аллокатор при старте выделяет один большой объем памяти и разбивает его на участки для 2в8, для 2в10 (и так далее). При правильном подходе аллокатор хоть и будет тратить больше памяти, но не будет фрагментации. Деструктор не возвразщает память назад системе, а просто помечает как свободной.

KoVadim
  • 112,121
  • 6
  • 94
  • 160
15

Вопрос почему-то опять вызвал интерес. Вот взял и попробовал. Всегда приятно узнавать что-то новое.

Краткий отчет:

Оказалось, что в убунте, если soft limit не установлен, то вместо ожидаемого ENOMEM мы получаем SIGKILL от ядра.

На 64-бит виртуалке с гигом ОЗУ и 2.5 гигами свопа при malloc(1000) этот процесс продолжается всего-то 1.5 минуты !!! (Так что будем считать, что я просто пошутил, утверждая, "что так долго не живут").

После установки лимита на виртуальную память (800 мегов), malloc все-таки стал возвращать 0 и я проверил вопрос о фрагментации.

Действительно, освободив 80 мегов realloc-ом "по месту" (уменьшая каждый блок на 100 байт), не смог выделить 1000 байт malloc-ом, а по 50 байт удалось получить, как не сложно догадаться, только половину из освобожденной ранее памяти.

(Если тестовая программка кому-то интересна, то напишите, завтра вставлю в дополнение ответа.)


(наверное, лучше поздно, чем никогда -))

/* https://drive.google.com/file/d/0BzY1LBmZNGbwbUYtNS01VWVXUG8/view?usp=sharing
  show memory fragmentation

use ulimit -a for look limits ulimit -S -v NNNNN for see malloc returns 0 and try realloc show or ulimit -S -v unlimited for SIGKILL if no more memory */

#include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <limits.h> #include <string.h> #include <signal.h> #include <errno.h> #include <setjmp.h> #include <sys/sysinfo.h> #include <unistd.h> #include <sys/wait.h>

#include <time.h>

struct memblk { struct memblk *next; size_t mbsize; char d[]; };

struct data { void *addr; size_t nb; };

#define M1 (1024 * 1024) #ifndef SWAP_BOUND #define SWAP_BOUND 10 /* когда мы выберем весь freeram и freeswap (величины при запуске программы) кроме SWAP_BOUND доли swap (например 50 это 1/50-я), мы начинаем слать данные о выделяемой памяти по пайпу для печати окончательного результата (поскольку тут уже в любой момент можем получить SIGKILL от ядра) */ #endif

sigjmp_buf jmp; int signo = 0;

void catch (int sig) { signo = sig; siglongjmp(jmp, sig); }

/* время в миллисекундах */ static long long mtime() { struct timeval t;

gettimeofday(&t, NULL); long long mt = (long long)t.tv_sec * 1000 + t.tv_usec / 1000; return mt; }

int main (int ac, char av[]) { size_t mbsize = av[1] ? atoi(av[1]) : 1000, n1000 = 0, s1000 = 0; struct memblk l1000 = 0, *p; struct sysinfo info; sysinfo(&info); printf("total(free)ram: %ld (%ld) total(free)swap: %ld (%ld) (in %d units)\n", info.totalram, info.freeram, info.totalswap, info.freeswap, info.mem_unit); int chan[2]; pipe(chan); struct data tr = {0}; long long start = mtime(); pid_t child;

if (child = fork()) { close(chan[1]); int s, crit = 0; while (read(chan[0], &tr, sizeof(tr)) == sizeof(tr)) crit++; pid_t p = wait(&s);

if (crit)
  printf(&quot;Fin %d critical blocks\n&quot;
     &quot;total %ld blocks %ld bytes (%f MB) %lld msec\n&quot;,
     crit, 
     (long)tr.nb, (long)(tr.nb * mbsize), 
     ((double)(tr.nb * mbsize)) / M1, mtime() - start);
else
  printf(&quot;no final critical data  %lld msec\n&quot;, mtime() - start);

if (p == child)
  if (WIFEXITED(s))
exit(WEXITSTATUS(s));
  else
raise(WTERMSIG(s));
return puts(&quot;unexepcted exit&quot;);

}

int sig; for (sig = 1; sig < 64; sig++) if (signal(sig, catch) == SIG_ERR) printf ("err signo %d\n", sig);

if (sig = sigsetjmp(jmp, 0)) { printf ("catch sig %d (signo %d)\n", sig, signo); exit(0); }

int done = 0; while (p = (typeof(p))malloc(mbsize)) { p->mbsize = mbsize; p->next = l1000; l1000 = p; n1000++; s1000 += mbsize; if (n1000 % M1 == 0) printf ("%ld blocks %ld bytes (%f MB) %lld msec\n", (long)n1000, (long)s1000, ((double)s1000) / M1, mtime() - start); else if (n1000 * mbsize > info.freeram * info.mem_unit + info.freeswap * info.mem_unit - info.freeswap * info.mem_unit / SWAP_BOUND) { tr.nb = n1000; tr.addr = p; write(chan[1], &tr, sizeof(tr)); if (!done) done = 1, printf("begin crit: %ld\n", (long)tr.nb);

}

}

printf ("End %ld blocks %ld bytes (%f MB) %lld msec\n", (long)n1000, (long)s1000, ((double)s1000) / M1, mtime() - start); close(chan[0]); close(chan[1]); if (p = malloc(5)) puts ("malloc(5) yes"); printf ("malloc(50) %s\n", malloc(50) ? "yes" : "no");

typeof (p) prev = 0, t; size_t save = 0, d = mbsize / 10; start = mtime(); for (p = l1000; p; p = p->next) { if (t = realloc(p, p->mbsize - d)) { t->mbsize -= d; if (t != p) { puts ("new addr"); if (prev) prev->next = t; else l1000 = t; } prev = p = t; save += d; } else { puts ("can't realloc"); exit(1); } }

printf ("realloc d: %ld sum: %ld (%lld msec)\n", (long)d, (long)save, mtime() - start); printf ("malloc(1000) again: %s\n", malloc(mbsize) ? "yes" : "no"); save = 0; while (malloc(50)) save += 50; printf ("malloc(50) = %ld\n", (long)save);

return 0; }

avp
  • 46,098
  • 6
  • 48
  • 116
  • 1

    и 2.5 гигами свопа

    Этим выполнилось мое второе условие: кончилась дисковая память, выделенная под своп, но виртуальная память не исчерпалась. Настаиваю - не живут!

    – mega Aug 15 '13 at 05:05
  • @mega, вопрос тут в том, насколько можно увеличить своп со стандартным ядром?

    Все структуры данных, описывающие распределение памяти процесса, отображение виртуальных страниц на физические и своп лежат в линуксе в невыгружаемой памяти ядра. А они тоже место занимают.

    --

    Основной же практический вывод из моего "исследования" - перед запуском серьезных программ посмотрите (и если надо переустановите) ulimit в bash.

    Кстати, подобное поведение (a'la kill -9) вполне вероятно не только для virtual (mmap()?), но и для data seg size (sbrk()).

    – avp Aug 15 '13 at 08:34
  • @avp, если сохранилась еще тестовая программка, то вставьте пожалуйста в дополнение ответа. – Alexcei Shmakov Dec 23 '15 at 06:15
  • 1
    @AlexceiShmakov, нашел. Если настаиваете, я положу код в ответ, а если нет, то см. https://drive.google.com/file/d/0BzY1LBmZNGbwbUYtNS01VWVXUG8/view?usp=sharing – avp Dec 25 '15 at 12:14
  • @avp спасибо, возьму по ссылке – Alexcei Shmakov Dec 25 '15 at 12:16
12

Ожидаемой Вами дефрагментации с кусками меньшими 4К добиться, наверное, можно, но такое время программы не живут.


живут, живут. Гигабайты свопа -- величина не бесконечная.

Хотел ответить комментарием, но не хватило места.

Для того, чтобы исчерпалась виртуальная память, в 64-битных системах в теории требуются эксабайты (на практике существует поддержка до пета-, но в общем случае - тера-).

А если учесть, что реальный объем физической памяти редко превышает объем в 32Гб (возьмем к примеру 4 слота по 8Гб), то простое распределение (не резервирование) памяти до теоретических (и даже до практических) пределов (да еще и фрагментированное по каким-то там килобайтам) будет занимать столько времени на операциях свопирования, что требуемый для этого uptime любой 64-битной системы не уложится и в нескольких десятках лет (могу ошибаться в порядках).

Так что @avp, скорее прав -- врятли :)

Скорее случится одно из двух:

  1. Деградирует операционная система.
  2. Закончится дисковая память, выделенная под своп.

Комментарии:

А чем отличается распредление от резервирования?

В комментариях уже объяснял. Резервирование - это резервирование виртуальных страниц под любые нужды приложения, но минуя операцию выделения на этот диапазон физических страниц.

Да и разве своп может вырасти до 18 Эксб? Имхо, предел - пара Гб (зависит от настроек ОС, конечно. у меня 2 гб стоит.)

Свопу просто не дадут вырасти до таких пределов, тоже уже обсуждали в комментах.

p.s.: у меня комментарии здесь уже кончились, так что, если будут вопросы - буду комментировать в ответе, обращайте на него внимание, пожалуйста.

mega
  • 5,364
  • Что есть "простое распределение" и что есть "резервирование"? За сколько времени можно делая malloc (без free) по 4Кб исчерпать память (виртуальную) на Вашем компьютере (можно провести эксперимент, только если Вы под линуксом, то в память что-то писать надо, хоть по байту). – alexlz Jan 08 '13 at 06:06
  • 1
    распределение - COMMIT
    резервирование - RESERVE
    --
    Естественно, инициализировать что-то в ней надо, чтобы "заляпать" страницу (это делает и куча, это делает и менеджер памяти Си, например в какой-нибудь malloc_dbg). Память Вы не исчерпаете. Скорее - деградирует система.
    – mega Jan 08 '13 at 06:15
  • Кто такие COMMIT и RESERVE?

    До уровня COMMIT/RESERVE мне анализировать libc не приходилось. Так что интересны результаты опыта. А какое отношение эта парочка COMMIT/RESERVE имеет к Optimistic Memory Allocation?

    – alexlz Jan 08 '13 at 06:36
  • Я этой парочкой просто уточняю, о какой именно операции я говорю, т.е. операция резервирования не требует сиеминутной поддержки запрашиваемого объема, она просто резервирует диапазон адресов от задействования его в других операциях с памятью. – mega Jan 08 '13 at 06:44
  • @mega Т.е. линуксовый malloc делает именно RESERVE. А цифры Вы привели интересные. У Вас действительно своп 16ЭксБ? Ну, разумеется, высказывание @avp "так долго не живут" можно интерпретировать в смысле "пользователь прибьёт их раньше", но ведь это уже совсем другая история. – alexlz Jan 08 '13 at 06:57
  • нет, RESERVE/COMMIT не зависят от ОСи, это базовые операции распределения памяти, в любой системе они доступны.

    16ЭксБ - это теоретический предел свопа, аналогичный 4Gb в 32-разрядных системах. Практический предел к нему стремится, но ограничен возможностями современных процессоров и операционных систем. У Intel'а уже есть процессоры, поддерживающие адресацию на границе петабайта, соответственно и поддерживающие своп такого размера.

    Так долго не живут я понимаю так, что система будет перезагружена раньше хотя бы раз.

    – mega Jan 08 '13 at 07:17
  • Разговор ни о чём. Наиболее вероятная причина перезагрузки -- система занята собой (шуршит винчестерами, где своп), администратор в отчаянии и не догадывается прибить процесс. Или всё же процесс прибивает (SIGKILL), что для процесса немногим лучше перезагрузки.

    А насчёт свопа в 16ЭксБ -- "съесть-то он съест, да ктож ему даст"

    – alexlz Jan 08 '13 at 07:23
  • Это уже человеческий фактор. Вобщем об этом я и говорю: просто терпения не хватит, да и не должно его хватать. Программа, которая имеет возможность так раздуваться просто не достигнет своих границ всилу огромного числа факторов, в основе которых, основной аргумент - бессмысленная трата времени, на которую ни кто не согласится. – mega Jan 08 '13 at 07:32
  • А чем отличается распредление от резервирования? Распредление, насколько я понимаю, это именно процесс "занятия" памяти в выделенных страницах, а резервирование, это именно выделение процессу опредленного числа страниц из пула? – PaulD Aug 13 '13 at 20:20
  • Да и разве своп может вырасти до 18 Эксб? Имхо, предел - пара Гб (зависит от настроек ОС, конечно. у меня 2 гб стоит.) – PaulD Aug 13 '13 at 20:31
5

Вопрос очень хороший спросили, и его оживлённо читают. Мой ответ скорее не ответ, а вопрос в продолжение темы почему-то уже принятого вопроса.

Возможно ли померять степень фрагментации данных в памяти своего приложения? Чужого? Может есть возможность вообще построить наглядную карту памяти с Ее дефрагментацией?

Помнится во времена xp было пару программ, которые занимались тем, что втихую дефрагментировали молча данные в ОЗУ и отправляли их в своп при длительном не использовании, к сожалению пример названий сейчас этих двух приложений уже и не вспомню.

pincher1519
  • 2,548