Статья представляет собой "вольный" перевод главы из Object Pascal Language Guide (продублированного в online-help'е), максимально приближенный к оригиналу. Я решил опираться на официальную документацию, т.к. там материал изложен наиболее последовательно и методично, с одной стороны, и далеко не все, к сожалению, в достаточной мере владеют английским, с другой стороны. В то же время здесь собрано почти всё, что касается Memory Manager'а, в т.ч. и такое, чего в хелпе нет (а есть в качестве скудных комментариев в исходниках Borland'а).
В статье рассказывается о том, как программы используют память и перечислены основные функции диспетчера памяти (не путать с функциями для работы с динамической памятью). Думается, что материал имеет смысл и для большинства последующих версий, но всё-таки, если вы используете что-то иное, чем Delphi 5.0, было бы неплохо ознакомиться со списком изменений.
В приложении Delphi диспетчер памяти управляет всеми динамическими выделениями (allocations) и освобождениями памяти. Через него работают стандартные процедуры New, Dispose, GetMem, ReallocMem и FreeMem, равно как и выделение памяти для объектов и длинных строк.
Диспетчер памяти заточен под приложения, выделяющие большое количество небольших объёмов памяти, что является характерным для ООП-приложений и приложений, обрабатывающих строковые данные. Другие менеджеры памяти (такие, как реализации GlobalAlloc, LocalAlloc, а также виндовая поддержка куч (heap)) не являются оптимальными в подобных ситуациях и могут замедлить приложение.
Для обеспечения оптимальной производительности менеджер памяти работает напрямую с ядром виртуальной памяти виндов (Win32 virtual memory API) через функции VirtualAlloc и VirtualFree. Память резервируется 1Mb-ыми секциями и выделяется блоками по 16 Kb по мере надобности.
Блоки всегда выровнены по 4-х байтовой границе и всегда включают 4-х байтовый заголовок, в котором хранятся размер блока и другая статусная информация. Выравнивание по "двойному слову" (double word) гарантирует оптимальную производительность CPU при адресации такого блока.
Диспетчер памяти контролирует две переменные, AllocMemCount и AllocMemSize, содержащие количество выделенных блоков и общую величину всей выделенной памяти. Эти данные приложение может использовать для отладки.
Модуль System содержит две процедуры, GetMemoryManager и SetMemoryManager, которые могут быть использованы для низкоуровневого перехвата обращений к диспетчеру памяти. Тот же модуль представляет функцию GetHeapStatus, которая возвращает запись, содержащую детальную информацию о статусе диспетчера памяти.
Память для глобальных переменных выделяется в сегменте данных приложения и освобождается при его завершении. Локальные переменные живут в стеке (stack). Каждый раз при вызове процедур или функций для них выделяется память, которая освобождается при выходе из процедуры или функции, хотя оптимизация компилятора может их уничтожить гораздо раньше.
Стек приложения определяется двумя значениями: минимальным и максимальным размером. Эти значения задаются директивами компилятора $MINSTACKSIZE (по умолчанию - 16 384 байта) и $MAXSTACKSIZE (по умолчанию - 1 048 576 байт). Винда сообщит об ошибке, если она не сможет при запуске приложения предоставить ему минимальный размер памяти для стека.
Если приложению требуется больше стековой памяти, чем указано в $MINSTACKSIZE, то она выделяется блоками по 4 Kb. Если очередное выделение памяти обламывается, то ли потому, что памяти больше нет, то ли потому, что суммарный объём запрошенной стековой памяти превысил $MAXSTACKSIZE, генерится эксепшн: EstackOverflow, причём контроль переполнения стека является полностью автоматическим, директива $S, когда-то позволявшая его отключить, оставлена только для совместимости с предыдущими версиями.
Динамические переменные, созданные с помощью процедур New или GetMem, размещаются в куче и сохраняются, пока не будут убиты через Dispose и FreeMem соответственно.
Длинные строки, широкие строки (wide strings), динамические массивы, варианты и интерфейсы также размещаются в куче, но выделение памяти под них контролируется диспетчером автоматически.
AllocMemfunction AllocMem(Size: Cardinal): Pointer;Выделяет в куче блок памяти заданного размера. Каждый байт выделенной памяти выставляется в ноль. Для освобождения памяти используется FreeMem.
AllocMemCountvar AllocMemCount: Integer;Содержит количество выделенных блок памяти. Эта переменная увеличивается каждый раз, когда пользователь запрашивает новый блок, и уменьшается, когда блок освобождается. Значения переменной используется для определения количества "оставшихся" блоков.
Так как переменная является глобальной и живёт в модуле System, её прямое использование не всегда безопасно. Модули, слинкованные статически, будут иметь разные экземпляры AllocMemCount. Статически слинкованными считаются приложения, не использующие пакеты времени выполнения (runtime packages). В следующей таблице обобщены сведения по использованию AllocMemCount в зависимости от типа приложения.
Тип приложения | Доступность AllocMemCount |
EXE | Приложения, не использующие пакеты и dll-и Delphi могут спокойно обращаться к данной глобальной переменной, т.к. для них существует только один её экземпляр. |
EXE с пакетами без dll | Приложения, использующие пакеты и не использующие dll-ки также могут спокойно работать с AllocMemCount. В этом случае все модули линкуются динамически, и существует только один экземпляр переменной, т.к. пакеты, в отличие от dll, умеют работать с глобальными переменными. |
EXE со статически слинкованными dll | Если приложение и используемые им dll-ки являются статически слинкованными с библиотекой выполнения (RTL), AllocMemCount никогда не следует использовать напрямую, т.к. и приложение, и dll-ки будут иметь собственные её экземпляры. Вместо этого следует использовать функцию GetAllocMemCount, живущую в BorlandMM, которая возвращает значение глобальной переменной AllocMemCount, объявленную в BorlandMM. Этот модуль отвечает за распределение памяти для всех модулей, в списке uses который первой указан модуль sharemem. Функция в данной ситуации используется потому, что глобальные переменные, объявленные в одной dll невидимы для другой. |
EXE с пакетами и статически слинкованными dll-ками | Не рекомендуется создавать смешанные приложения, использующие и пакеты, и статически слинкованные dll-ки. В этом случае следует с осторожностью работать с динамически выделяемой памятью, т.к. каждый модуль будет содержать собственный AllocMemCount , ссылающийся на память, выделенную и освобождённую именно данным модулем. |
AllocMemSizevar AllocMemSize: Integer;Содержит размер памяти, в байтах, всех блоков памяти, выделенных приложением. Фактически эта переменная показывает, сколько байтов памяти в данный момент использует приложение. Поскольку переменная является глобальной, то к ней относится всё, сказанное в отношении AllocMemCount.
GetHeapStatusfunction GetHeapStatus: THeapStatus;Возвращает текущее состояние диспетчера памяти.
type
THeapStatus = record
TotalAddrSpace: Cardinal;s
TotalUncommitted: Cardinal;
TotalCommitted: Cardinal;
TotalAllocated: Cardinal;
TotalFree: Cardinal;
FreeSmall: Cardinal;
FreeBig: Cardinal;
Unused: Cardinal;
Overhead: Cardinal;
HeapErrorCode: Cardinal;
end;
Если приложение не использует модуль ShareMem, то данные в записи TheapStatus относятся к глобальной куче (heap), в противном случае это могут быть данные о памяти, разделяемой несколькими процессами.
TotalAddrSpace | Адресное пространство, доступное вашей программе в байтах. Значение этого поля будет расти, по мере того, как увеличивается объём памяти, динамически выделяемый вашей программой. |
TotalUncommitted | Показывает, сколько байтов из TotalAddrSpace не находятся в swap-файле. |
TotalCommitted | Показывает, сколько байтов из TotalAddrSpace находятся в swap-файле. Соответственно, TotalCommited + TotalUncommited = TotalAddrSpace |
TotalAllocated | Сколько всего байтов памяти было динамически выделено вашей программой |
TotalFree | Сколько памяти (в байтах) доступно для выделения вашей программой. Если программа превышает это значение, и виртуальной памяти для этого достаточно, ОС автоматом увеличит адресное пространство для вашего приложения и соответственно увеличится значения TotalAddrSpace |
FreeSmall | Доступная, но неиспользуемая память (в байтах), находящаяся в "маленьких" блоках. |
FreeBig | Доступная, но неиспользуемая память (в байтах), находящаяся в "больших" блоках. Большие блоки могут формироваться из непрерывных последовательностей "маленьких". |
Unused | Память (в байтах) никогда не выделявшаяся (но доступная) вашей программой. Unused + FreeSmall + FreeBig = TotalFree. |
Overhead | Сколько памяти (в байтах) необходимо менеджеру кучи, чтобы обслуживать все блоки, динамически выделяемые вашей программой. |
HeapErrorCode | Внутренний статус кучи |
Учтите, что TotalAddrSpace, TotalUncommitted и TotalCommitted относятся к памяти ОС, выделяемой для вашей программы, а TotalAllocated и TotalFree относятся к памяти кучи, используемой для динамического выделения памяти самой программой. Таким образом, для отслеживания того, как ваша программа использует динамическую память, используйте TotalAllocated и TotalFree.
Константы для HeapErrorCode живут в MEMORY.INC (highly recommended для всех продвинутых и интересующихся). За компанию приведём и их.
Код | Константа | Значение |
0 | cHeapOk | Всё отлично |
1 | cReleaseErr | ОС вернула ошибку при попытке освободить память |
2 | cDecommitErr | ОС вернула ошибку при попытке освободить память, выделенную в swap-файле |
3 | cBadCommittedList | Список блоков, выделенных в swap-файле, выглядит подозрительно |
4 | cBadFiller1 | Хреновый филлер. (Ставлю пиво тому, кто объяснит мне, что это значит). Судя по коду в MEMORY.INC, значения выставляются в функции FillerSizeBeforeGap, которая вызывается при различного рода коммитах (т.е. при сливании выделенной памяти в swap). И если что-то в этих сливаниях не срабатывает, функция взводит один из этих трёх флагов. |
5 | cBadFiller2 | "-/-" |
6 | cBadFiller3 | "-/-" |
7 | cBadCurAlloc | Что-то не так с текущей зоной выделения памяти |
8 | cCantInit | Не вышло инициализироваться |
9 | cBadUsedBlock | Используемый блок памяти нездоров |
10 | cBadPrevBlock | Предыдущий перед используемым блок нездоров |
11 | cBadNextBlock | Следующий после используемого блок нездоров |
12 | cBadFreeList | Хреновый список свободных блоков. Судя по коду, речь идёт о нарушении последовательности свободных блоков в памяти |
13 | cBadFreeBlock | Что-то не так со свободным блоком памяти |
14 | cBadBalance | Список свободных блоков не соответствует действительности |
GetMemoryManagerprocedure GetMemoryManager(var MemMgr: TMemoryManager);Возвращает указатель на текущий диспетчер памяти. Структура TMemoryManager описана ниже.
TMemoryManager - структура данных
type
PMemoryManager = ^TMemoryManager;
TMemoryManager = record
GetMem: function(Size: Integer): Pointer;
FreeMem: function(P: Pointer): Integer;
ReallocMem: function(P: Pointer; Size: Integer): Pointer;
end;
Эта запись определяет, какие функции используются для выделения и освобождения памяти.
Функция GetMem должна выделить блок памяти размером Size (Size никогда не может быть равным нулю) и вернуть на него указатель. Если она не может этого сделать, она должна вернуть nil.
Функция FreeMem должна освободить память Size по адресу P. P никогда не должен быть равен nil. Если функция с этим справилась, она должна вернуть ноль.
Функция ReallocMem должна перевыделить память Size для блока P. Здесь P не может быть nil и Size не может быть 0 (хотя при вызове ReallocMem не из диспетчера памяти, это вполне допускается). Функция должна выделить память, при необходимости, переместить блок на новое место и вернуть указатель на это место. Если выделение памяти невозможно, она должна вернуть nil.
HeapAllocFlagsvar HeapAllocFlags: Word = 2;Этими флагами руководствуется диспетчер памяти при работе с памятью. Они могут комбинироваться и принимать следующие значения (по умолчанию - GMEM_MOVEABLE):
Флаг | Значение |
GMEM_FIXED | Выделяет фиксированную память. Т.к. ОС не может перемещать блоки памяти, то и нет нужды блокировать память (соответственно, не может комбинироваться с GMEM_MOVEABLE) |
GMEM_MOVEABLE | Выделяет перемещаемую память. В Win32 блоки не могут быть перемещены, Если они расположены в физической памяти, но могут перемещаться в пределах кучи. |
GMEM_ZEROINIT | При выделении памяти (например, функцией GetMem) все байты этой памяти будут выставлены в 0. (отличная черта) |
GMEM_MODIFY | Используется для изменения атрибутов уже выделенного блока памяти |
GMEM_DDESHARE | Введёны для совместимости с 16-разрядными версиями, но может использоваться для оптимизации DDE операций. Собственно, кроме как для таких операций эти флаги и не должны использоваться |
GMEM_SHARE | "-/-" |
GPTR | Предустановленный, соответствует GMEM_FIXED + GMEM_ZEROINIT |
GHND | Предустановленный, соответствует GMEM_MOVEABLE + GMEM_ZEROINIT |
IsMemoryManagerSetfunction IsMemoryManagerSet:Boolean;Возвращает TRUE, если кто-то успел похерить дефолтовый диспетчер памяти и воткнуть вместо него свой.
ReallocMemprocedure ReallocMem(var P: Pointer; Size: Integer);Перевыделяет память, ранее выделенную под P. Реальные действия процедуры зависят от значений P и Size.
P = nil, Size = 0: ничего не делается;
P = nil, Size <> 0: соответствует вызову P := GetMem (Size);
P <> nil, Size = 0: соответствует вызову FreeMem (P, Size) (с тем отличием, что FreeMem не будет обнулять указатель, а здесь он уже равен nil).
P <> nil, Size <> 0: перевыделяет для указателя P память размером Size. Текущие данные никак не затрагиваются, но если размер блока увеличивается, новая порция памяти будет содержать всякий мусор. Если новый блок "не влазит" на своё старое место, он перемещается на новое место в куче и значение P обновляется соответственно. Это важно: после вызова данной процедуры блок P может оказаться в памяти по совсем другому адресу!
SetMemoryManagerprocedure SetMemoryManager(const MemMgr: TMemoryManager);Устанавливает новый диспетчер памяти. Он будет использоваться при выделении и освобождении памяти процедурами GetMem, FreeMem, ReallocMem, New и Dispose, а также при работе конструкторов и деструкторов объектов и работе с динамическими строками и массивами.
SysFreeMem, SysGetMem, SysReallocMem
Используются при написании собственного диспетчера памяти. Другого смысла в них я не нашёл.
Думаете, очень сложно? Как бы не так. Вот пример из справочной системы самой Delphi: этот диспетчер будет запоминать количество выделений, освобождений и перевыделений памяти:
var
GetMemCount: Integer;
FreeMemCount: Integer;
ReallocMemCount: Integer;
OldMemMgr: TMemoryManager;
function NewGetMem(Size: Integer): Pointer;
begin
Inc(GetMemCount);
Result := OldMemMgr.GetMem(Size);
end;
function NewFreeMem(P: Pointer): Integer;
begin
Inc(FreeMemCount);
Result := OldMemMgr.FreeMem(P);
end;
function NewReallocMem(P: Pointer; Size: Integer): Pointer;
begin
Inc(ReallocMemCount);
Result := OldMemMgr.ReallocMem(P, Size);
end;
const
NewMemMgr: TMemoryManager = (
GetMem: NewGetMem;
FreeMem: NewFreeMem;
ReallocMem: NewReallocMem);
procedure SetNewMemMgr;
begin
GetMemoryManager(OldMemMgr);
SetMemoryManager(NewMemMgr);
end;
Как говорится, комментарии излишни. С таким же успехом вы можете заменить дельфийские функции работы с кучей на функции ОС: GlobalAlloc, GlobalFree и пр., которые, в отличие от дельфийских, выравнивают выделяемую память по 8-байтовой границе.