Управление памятью в Delphi 5.0: диспетчер памяти - Языки программирования - Shelek
Вводная


Статья представляет собой "вольный" перевод главы из 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), динамические массивы, варианты и интерфейсы также размещаются в куче, но выделение памяти под них контролируется диспетчером автоматически.

Описание переменных и функций


AllocMem

function AllocMem(Size: Cardinal): Pointer;
Выделяет в куче блок памяти заданного размера. Каждый байт выделенной памяти выставляется в ноль. Для освобождения памяти используется FreeMem.

AllocMemCount

var 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 , ссылающийся на память, выделенную и освобождённую именно данным модулем.

AllocMemSize

var AllocMemSize: Integer;
Содержит размер памяти, в байтах, всех блоков памяти, выделенных приложением. Фактически эта переменная показывает, сколько байтов памяти в данный момент использует приложение. Поскольку переменная является глобальной, то к ней относится всё, сказанное в отношении AllocMemCount.

GetHeapStatus
function 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 для всех продвинутых и интересующихся). За компанию приведём и их.

HeapErrorCode - значения кодов ошибок


Код Константа Значение
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 Список свободных блоков не соответствует действительности

GetMemoryManager

procedure 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.

HeapAllocFlags

var 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

IsMemoryManagerSet

function IsMemoryManagerSet:Boolean;
Возвращает TRUE, если кто-то успел похерить дефолтовый диспетчер памяти и воткнуть вместо него свой.

ReallocMem

procedure 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 может оказаться в памяти по совсем другому адресу!

SetMemoryManager

procedure 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-байтовой границе.


Автор: x77
Information
  • Posted on 31.01.2010 23:53
  • Просмотры: 2208