Вступление
Истоки: возникла "необоснованная" утечка памяти.
Ниже речь пойдёт о причине её возникновения и способе устранения.
Рассматривать будем на "учебном" примере.
Пусть у нас имеются три взаимосвязанных класса:
1. TData - для хранения данных
2. TDataSource - для хранения набора данных с типом TData
3. TDataManager - для хранения набора данных с типом TDataSource
ЧАСТЬ 1: с памятью всё отлично.
Классы вот такие:
класс TData
'класс для хранения данных
Type TData
Field data[1000] 'ститически объявленный массив из 1000 элементов типа int
Global value:TData 'это просто поле такого же типа как сам класс, использованию подвергать не будем
End Type
класс TDataSource
'класс для хранения набора данных типа TData
Type TDataSource
Field list:TList = New TList 'список для хранения экземпляров TData
Global data:TData 'вспомогательная переменная, можно и без нее
'заполнение данными ("конструктор с параметром")
'cnt - количество создаваемых экземпляров
'возвращает экземпляр класса TDataSource
Function fnCreate:TDataSource(cnt)
Local ds:TDataSource = New TDataSource
For Local k = 0 Until cnt
data = New TData 'создаем экземпляр с данными
ds.list.AddLast(data) 'добавляем его в список
Next
data = Null 'это лишь указатель, зануляем, т.к. больше не нужен
'возвращаем экземпляр с набором данных
Return ds
End Function
End Type
и класс TDataManager
'менеджер "ресурсов", все переменные глобальные (статические)
Type TDataManager
Global list:TList = New TList 'список для хранения экземпляров TDataSource
Global source:TDataSource 'вспомогательная переменная, можно и без нее
'заполнение данными
'cnt1 - количество экземпляров TDataSource для менеджера
'cnt2 - количество экземпляров TData для каждого экземпляра TDataSource
Function fnCreate(cnt1, cnt2)
For Local k = 0 Until cnt1
source = TDataSource.fnCreate(cnt2) 'создаем
list.AddLast(source) 'добавляем в список
Next
source = Null 'это лишь указатель, зануляем, т.к. больше не нужен
End Function
'функция удаления элементов, просто удаляем всё из списка
Function fnClear()
list.Clear()
End Function
End Type
Теперь напишем консольный примерчик, который будет создавать и удалять объекты и показывать сколько памяти было использовано в конкретный момент времени.
Вот такой код:
Strict
'ручной режим очистки памяти - при вызове GCCollect()
GCSetMode(2)
'на всякий случай сразу вызываем очистку
GCCollect()
'выводим количество использованной памяти на момент запуска программы
DebugLog "mem1 = " + GCMemAlloced()
'теперь создаём 10 экземпляров для TDataSource,
'в каждом из которых будет по 200 экземпляров TData
TDataManager.fnCreate(10, 200)
'смотрим сколько теперь памяти использовано
DebugLog "mem2 = " + GCMemAlloced()
'удаляем элементы из менеджера, очищая список
TDataManager.fnClear()
'запускаем сборщик мусора
GCCollect()
'смотрим сколько теперь памяти использовано
DebugLog "mem3 = " + GCMemAlloced()
'а теперь создадим 10 экземпляров для TDataSource,
'в каждом из которых будет по 100 экземпляров TData
TDataManager.fnCreate(10, 100)
'опять проверка количества памяти
DebugLog "mem5 = " + GCMemAlloced()
'снова удаление всех элементов и сбор мусора
TDataManager.fnClear()
GCCollect()
'вывод результирующего объема использованной памяти
DebugLog "mem6 = " + GCMemAlloced()
'дословно: конец
End
Теперь у нас есть всё для запуска.
Привожу весь код целиком.
Strict
'ручной режим очистки памяти - при вызове GCCollect()
GCSetMode(2)
'на всякий случай сразу вызываем очистку
GCCollect()
'выводим количество использованной памяти на момент запуска программы
DebugLog "mem1 = " + GCMemAlloced()
'теперь создаём 10 экземпляров для TDataSource,
'в каждом из которых будет по 200 экземпляров TData
TDataManager.fnCreate(10, 200)
'смотрим сколько теперь памяти использовано
DebugLog "mem2 = " + GCMemAlloced()
'удаляем элементы из менеджера, очищая список
TDataManager.fnClear()
'запускаем сборщик мусора
GCCollect()
'смотрим сколько теперь памяти использовано
DebugLog "mem3 = " + GCMemAlloced()
'а теперь создадим 10 экземпляров для TDataSource,
'в каждом из которых будет по 100 экземпляров TData
TDataManager.fnCreate(10, 100)
'опять проверка количества памяти
DebugLog "mem5 = " + GCMemAlloced()
'снова удаление всех элементов и сбор мусора
TDataManager.fnClear()
GCCollect()
'вывод результирующего объема использованной памяти
DebugLog "mem6 = " + GCMemAlloced()
'дословно: конец
End
'класс для хранения данных
Type TData
Field data[1000] 'ститически объявленный массив из 1000 элементов типа int
Global value:TData 'это просто поле такого же типа как сам класс, использованию подвергать не будем
End Type
'Field parent:TDataSource
'data.parent = ds
'For source = EachIn list
' 'source.fnClear()
'Next
'source = Null
'класс для хранения набора данных типа TData
Type TDataSource
Field list:TList = New TList 'список для хранения экземпляров TData
Global data:TData 'вспомогательная переменная, можно и без нее
'заполнение данными ("конструктор с параметром")
'cnt - количество создаваемых экземпляров
'возвращает экземпляр класса TDataSource
Function fnCreate:TDataSource(cnt)
Local ds:TDataSource = New TDataSource
For Local k = 0 Until cnt
data = New TData 'создаем экземпляр с данными
ds.list.AddLast(data) 'добавляем его в список
Next
data = Null 'это лишь указатель, зануляем, т.к. больше не нужен
'возвращаем экземпляр с набором данных
Return ds
End Function
'функция удаления элементов, просто удаляем всё из списка
Method fnClear()
list.Clear()
End Method
End Type
'менеджер "ресурсов", все переменные глобальные (статические)
Type TDataManager
Global list:TList = New TList 'список для хранения экземпляров TDataSource
Global source:TDataSource 'вспомогательная переменная, можно и без нее
'заполнение данными
'cnt1 - количество экземпляров TDataSource для менеджера
'cnt2 - количество экземпляров TData для каждого экземпляра TDataSource
Function fnCreate(cnt1, cnt2)
For Local k = 0 Until cnt1
source = TDataSource.fnCreate(cnt2) 'создаем
list.AddLast(source) 'добавляем в список
Next
source = Null 'это лишь указатель, зануляем, т.к. больше не нужен
End Function
'функция удаления элементов, просто удаляем всё из списка
Function fnClear()
list.Clear()
End Function
End Type
Запускаем на исполнение, и смотрим в debuglog.
И видим там такие значения:
mem1 = 16246
mem2 = 8129054
mem3 = 16246
mem5 = 4073054
mem6 = 16246
Как видно,
mem1 = mem3 = mem6, т.е. от чего уходили, к тому и приходили – использованная память освободилась. Так и должно быть.
ЧАСТЬ 2: делаем очистку, но объём использованной памяти продолжает увеличиваться!
Теперь давайте изменим код, а именно: добавим в класс TData поле
parent с типом TDataSource. В этом поле будем хранить указатель на экземпляр TDataSource, давший жизнь нашему экземпляру TData.
'класс для хранения данных
Type TData
Field data[1000] 'ститически объявленный массив из 1000 элементов типа int
Field parent:TDataSource 'указатель на "родителя"
Global value:TData 'это просто поле такого же типа как сам класс, использованию подвергать не будем
End Type
Указатель на родителя будем присваивать в функции создания экземпляров TData, в классе TDataSource.
'заполнение данными ("конструктор с параметром")
'cnt - количество создаваемых экземпляров
'возвращает экземпляр класса TDataSource
Function fnCreate:TDataSource(cnt)
Local ds:TDataSource = New TDataSource
For Local k = 0 Until cnt
data = New TData 'создаем экземпляр с данными
data.parent = ds 'указываем родителя
ds.list.AddLast(data) 'добавляем его в список
Next
data = Null 'это лишь указатель, зануляем, т.к. больше не нужен
'возвращаем экземпляр с набором данных
Return ds
End Function
Пришло время запустить выполнение программы и посмотреть результат в окне debuglog’а.
А результат такой:
mem1 = 16246
mem2 = 8137054
mem3 = 8136686
mem5 = 12197510
mem6 = 12197126
Как видно, результат сильно отличается от того, который был замечен в первой части.
Тут
mem1 <> mem3 <> mem6.
Хотя сборщик мусора и поработал, но очистил менее 1 кб, а счёт шёл на мегабайты!
Т.е. происходит утечка памяти! Разве так можно! (можно, но не нужно.)
Объяснение: почему память не очищается?
Т.к. мы лишь добавили поле
parent, то естественно, всё
дело в этом поле, точнее –
в присвоении ему значения.
Ведь что делает много(или мало?)уважаемый GC? Он удаляет из памяти те ресурсы, на которые в программе
нет ссылок (=указателей) на момент его вызова.
Вывод: ссылки на ресурсы есть.
Но позвольте: мы же очистили список в менеджере – и все ссылки потеряли, не так ли!? Так, но «внутри» программы ссылки остались, хоть они больше и не доступны для использования.
Подробнее:
Присвоение полю
parent надлежащего значения создает связь между экземпляром класса TData и экземпляром класса TDataSource, ведь суть поля parent – указатель на экземпляр класса TDataSource.
А в списке
list класса TDataManager хранятся тоже указатели на экземпляры класса TDataSource. Мало того –
это указатели на одни и те же объекты.
Т.е. указатели в списке менеджера и указатели полей parent указывают на одни и те же объекты!
Вот оно!
Да, при очистке списка в менеджере мы «зануляем» его указатели,
однако хранимые в нём экземпляры содержат указатели на экземпляры TData (в своих списках list), а те экземпляры содержат указатели на родительские экземпляры TDataSorce в поле
parent.
Таким образом: список очищен, доступ к его элементам мы потеряли, однако указатели остались. А потому, GC их "оставляет", а как же иначе!
Программное решение
Задача: избавиться от связи экземпляров TData с экземплярами TDataSource.
Чтобы это сделать, необходимо и достаточно удалить все экземпляры TData из всех экземпляров TDataSource.
Делается это простой очисткой списков
list для экземпляров TDataSource.
Итак, добавим в класс TDataSource следующий метод:
'функция удаления элементов, просто удаляем всё из списка
Method fnClear()
list.Clear()
End Method
И подправим функцию очистки в классе TDataManager:
'функция удаления элементов, просто удаляем всё из списков
Function fnClear()
'сначала удаляем данные из экземпляров TDataSource
For source = EachIn list
source.fnClear()
Next
source = Null
'а теперь очищаем список
list.Clear()
End Function
Остаётся запустить программу и взглянуть на результат её выполнения в лог’е.
А результат явился таковым:
mem1 = 16246
mem2 = 8137054
mem3 = 16246
mem5 = 4077054
mem6 = 16246
Что полностью совпадает с результатом, полученным в первой части статьи - вся память корректно освободилась.
Итоги
Рассмотрен случай с утечкой памяти, когда в памяти остаются «внутренние» ссылки объектов друг на друга.
Ребят, люблю и тех, для кого всё вышесказанное – очевидность.
Вот и всё.