Стаття управління пам'яттю в delphi 5

Управління пам'яттю в Delphi 5.0: диспетчер пам'яті

У статті розповідається про те, як програми використовують пам'ять і перераховані основні функції диспетчера пам'яті (не плутати з функціями для роботи з динамічною пам'яттю). Здається, що матеріал має сенс і для більшості наступних версій, але все-таки, якщо ви використовуєте щось інше, ніж Delphi 5.0, було б непогано ознайомитися зі списком змін.

У додатку Delphi диспетчер пам'яті керує всіма динамічними виділеннями (allocations) і звільненнями пам'яті. Через нього працюють стандартні процедури New, Dispose, GetMem, ReallocMem і FreeMem, так само як і виділення пам'яті для об'єктів і довгих рядків.

Диспетчер пам'яті заточений під додатки, що виділяють велику кількість невеликих обсягів пам'яті, що є характерним для ООП-додатків і додатків, що обробляють строкові дані. Інші менеджери пам'яті (такі, як реалізації GlobalAlloc, LocalAlloc, а також віндового підтримка куп (heap)) не є оптимальними в подібних ситуаціях і можуть уповільнити додаток.

Для забезпечення оптимальної продуктивності менеджер пам'яті працює безпосередньо з ядром віртуальної пам'яті виндов (Win32 virtual memory API) через функції VirtualAlloc і VirtualFree. Пам'ять резервується 1Mb-ими секціями та виділяється блоками по 16 Kb у міру потреби.

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

Опис змінних та функцій

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

var AllocMemCount: Integer;
Містить кількість виділених блок пам'яті. Ця змінна збільшується кожного разу, коли користувач запитує новий блок, і зменшується, коли блок звільняється. Значення змінної використовується для визначення кількості "залишилися" блоків.

Так як змінна є глобальною і живе в модулі System, її пряме використання не завжди безпечно. Модулі, слінкованние статично, матимуть різні екземпляри AllocMemCount. Статично слінкованнимі вважаються додатки, які не використовують пакети часу виконання (runtime packages). У наступній таблиці узагальнені відомості по використанню AllocMemCount в залежності від типу програми.

Додатки, які не використовують пакети і dll-і Delphi можуть спокійно звертатися до даної глобальної змінної, тому що для них існує тільки один її примірник.

EXE з пакетами без dll

Програми, що використовують пакети і які не використовують dll-ки також можуть спокійно працювати з AllocMemCount. В цьому випадку всі модулі лінкуются динамічно, і існує тільки один екземпляр, тому що пакети, на відміну від dll, вміють працювати з глобальними змінними.

EXE зі статично слінкованнимі dll

Якщо додаток і які він використовував dll-ки є статично слінкованнимі з бібліотекою виконання (RTL), AllocMemCount ніколи не слід використовувати безпосередньо, тому що і додаток, і dll-ки будуть мати власні її екземпляри. Замість цього слід використовувати функцію GetAllocMemCount, яка живе в BorlandMM, яка повертає значення глобальної змінної AllocMemCount, оголошену в BorlandMM. Цей модуль відповідає за розподіл пам'яті для всіх модулів, в списку uses який першій вказано модуль sharemem. Функція в даній ситуації використовується тому, що глобальні змінні, оголошені в одній dll невидимі для іншої.

EXE з пакетами і статично слінкованнимі dll-ками

Не рекомендується створювати змішані функції, які залежать і пакети, і статично слінкованние dll-ки. В цьому випадку слід з обережністю працювати з динамічно виділеної пам'яттю, тому що кожен модуль буде містити власний AllocMemCount. посилається на пам'ять, виділену і звільнену саме даним модулем.

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 не перебувають в swap-файлі.

Показує, скільки байтів з TotalAddrSpace знаходяться в swap-файлі. Відповідно, TotalCommited + TotalUncommited = TotalAddrSpace

Скільки всього байтів пам'яті було динамічно виділено вашою програмою

Доступна, але якою не пам'ять (в байтах), що знаходиться в "маленьких" блоках.

Доступна, але якою не пам'ять (в байтах), що знаходиться в "великих" блоках. Великі блоки можуть формуватися з безперервних послідовностей "маленьких".

Пам'ять (в байтах) ніколи не виділялася (але доступна) вашою програмою. Unused + FreeSmall + FreeBig = TotalFree.

Скільки пам'яті (в байтах) необхідно менеджеру купи, щоб обслуговувати всі блоки, динамічно виділяються вашою програмою.

Внутрішній статус купи

Врахуйте, що TotalAddrSpace, TotalUncommitted і TotalCommitted відносяться до пам'яті ОС, що виділяється для вашої програми, а TotalAllocated і TotalFree відносяться до пам'яті купи, використовуваної для динамічного виділення пам'яті самою програмою. Таким чином, для відстеження того, як ваша програма використовує динамічну пам'ять, використовуйте TotalAllocated і TotalFree.

Константи для HeapErrorCode живуть в MEMORY.INC (highly recommended для всіх просунутих і цікавляться). За компанію наведемо і їх.

HeapErrorCode - значення кодів помилок

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.

Функція ReallocMem повинна перевиделіть пам'ять Size для блоку P. Тут P не може бути nil і Size не може бути 0 (хоча при виклику ReallocMem не з диспетчера пам'яті, це цілком допускається). Функція повинна виділити пам'ять, при необхідності, перемістити блок на нове місце і повернути покажчик на це місце. Якщо виділення пам'яті неможливо, вона повинна повернути nil.

var HeapAllocFlags: Word = 2;
Цими прапорами керується диспетчер пам'яті при роботі з пам'яттю. Вони можуть комбінуватися і набувати наступних значень (за замовчуванням - GMEM_MOVEABLE):

Виділяє фіксовану пам'ять. Оскільки ОС не може переміщати блоки пам'яті, то і немає потреби блокувати пам'ять (відповідно, не може комбінуватися з GMEM_MOVEABLE)

Виділяє переміщувану пам'ять. У Win32 блоки не можуть бути переміщені, Якщо вони розташовані у фізичній пам'яті, але можуть переміщатися в межах купи.

При виділенні пам'яті (наприклад, функцією GetMem) все байти цієї пам'яті будуть виставлені в 0. (відмінна риса)

Використовується для зміни атрибутів вже виділеного блоку пам'яті

Введений для сумісності з 16-розрядними версіями, але може використовуватися для оптимізації DDE операцій. Власне, крім як для таких операцій ці прапори і не повинні використовуватися

Попередньо, відповідає GMEM_FIXED + GMEM_ZEROINIT

Попередньо, відповідає GMEM_MOVEABLE + GMEM_ZEROINIT

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

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;

Схожі статті