Статья
Версия для печати
Обсудить на форуме
Управление памятью в Delphi 5.0: диспетчер памяти



Вводная


Статья представляет собой "вольный" перевод главы из 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 - значения кодов ошибок


КодКонстантаЗначение
0cHeapOkВсё отлично
1cReleaseErrОС вернула ошибку при попытке освободить память
2cDecommitErrОС вернула ошибку при попытке освободить память, выделенную в swap-файле
3cBadCommittedListСписок блоков, выделенных в swap-файле, выглядит подозрительно
4cBadFiller1Хреновый филлер. (Ставлю пиво тому, кто объяснит мне, что это значит). Судя по коду в MEMORY.INC, значения выставляются в функции FillerSizeBeforeGap, которая вызывается при различного рода коммитах (т.е. при сливании выделенной памяти в swap). И если что-то в этих сливаниях не срабатывает,  функция взводит один из этих трёх флагов.
5cBadFiller2"-/-"
6cBadFiller3"-/-"
7cBadCurAllocЧто-то не так с текущей зоной выделения памяти

8cCantInitНе вышло инициализироваться
9cBadUsedBlockИспользуемый блок памяти нездоров
10cBadPrevBlockПредыдущий перед используемым блок нездоров
11cBadNextBlockСледующий после используемого блок нездоров
12cBadFreeListХреновый список свободных блоков. Судя по коду, речь идёт о нарушении последовательности свободных блоков в памяти
13cBadFreeBlockЧто-то не так со свободным блоком памяти
14cBadBalanceСписок свободных блоков не соответствует действительности

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
Версия для печати
Обсудить на форуме