Показать сообщение отдельно
Старый 21.10.2009, 14:16   #1
Жека
Дэвелопер
 
Регистрация: 04.09.2005
Адрес: Красноярск
Сообщений: 1,376
Написано 491 полезных сообщений
(для 886 пользователей)
Утечка памяти из-за оставшихся "внутренних" ссылок

Вступление

Истоки: возникла "необоснованная" утечка памяти.
Ниже речь пойдёт о причине её возникновения и способе устранения.

Рассматривать будем на "учебном" примере.

Пусть у нас имеются три взаимосвязанных класса:
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


Что полностью совпадает с результатом, полученным в первой части статьи - вся память корректно освободилась.


Итоги

Рассмотрен случай с утечкой памяти, когда в памяти остаются «внутренние» ссылки объектов друг на друга.


Ребят, люблю и тех, для кого всё вышесказанное – очевидность.
Вот и всё.
Вложения
Тип файла: rar memTest.rar (1.5 Кб, 725 просмотров)
(Offline)
 
Ответить с цитированием
Эти 12 пользователя(ей) сказали Спасибо Жека за это полезное сообщение:
ABTOMAT (22.10.2009), cheaters-hater (06.11.2009), Dream (22.10.2009), Dzirt (07.11.2012), Harter (21.12.2009), Horror (21.10.2009), Illidan (21.10.2009), johnk (21.10.2009), moka (22.10.2009), Nex (21.10.2009), Randomize (22.10.2009), SBJoker (21.10.2009)