Извините, ничего не найдено.

Не расстраивайся! Лучше выпей чайку!
Регистрация
Справка
Календарь

Вернуться   www.boolean.name > Программирование игр для компьютеров > BlitzMax > FAQ и уроки

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

Вступление

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

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

Пусть у нас имеются три взаимосвязанных класса:
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 Кб, 190 просмотров)
(Offline)
 
Ответить с цитированием
Эти 12 пользователя(ей) сказали Спасибо Жека за это полезное сообщение:
ABTOMAT (21.10.2009), cheaters-hater (06.11.2009), Dream (21.10.2009), Dzirt (07.11.2012), Harter (21.12.2009), Horror (21.10.2009), Illidan (21.10.2009), johnk (21.10.2009), moka (21.10.2009), Nex (21.10.2009), Randomize (22.10.2009), SBJoker (21.10.2009)
Старый 21.10.2009, 11:38   #2
SBJoker
Злобный Админ
 
Аватар для SBJoker
 
Регистрация: 04.09.2005
Сообщений: 5,921
Написано 3,411 полезных сообщений
(для 9,319 пользователей)
Ответ: Утечка памяти из-за оставшихся "внутренних" ссылок

Добавлю что достаточно написать деструктор класса, для которого зарезервировано имя Delete:

Method Delete()
'очищаем все внутренние ресурсы чистим ссылки
End Method

Чем уникален деструктор? А тем что он автоматически вызывается для удаляемых объектов. ЧТо избавляет нас от ручного вызова функции очистки.
Если уж сказал про деструктор скажу и про конструктор, за которым закреплено имя New:

Method New()
'инициализация внутрених объектов, загрузка необходимых данных и т.д.
End Method

Этот метод автоматически вызывается при создании объекта.
__________________
(Offline)
 
Ответить с цитированием
Эти 7 пользователя(ей) сказали Спасибо SBJoker за это полезное сообщение:
ABTOMAT (21.10.2009), Harter (21.12.2009), Illidan (21.10.2009), impersonalis (21.10.2009), moka (21.10.2009), Reizel (10.10.2010), Жека (22.10.2009)
Старый 22.10.2009, 04:25   #3
Жека
Дэвелопер
 
Регистрация: 04.09.2005
Адрес: Красноярск
Сообщений: 1,374
Написано 489 полезных сообщений
(для 882 пользователей)
Ответ: Утечка памяти из-за оставшихся "внутренних" ссылок

Для моего примера я не смог очистить память с помощью Delete(), добавленного в класс TDataSource. А причина всё та же - список этого класса хранит данные с указателями на экземпляры этого же класса в поле parent, а потому данный метод не вызывается.
(Offline)
 
Ответить с цитированием
Старый 22.10.2009, 06:48   #4
SBJoker
Злобный Админ
 
Аватар для SBJoker
 
Регистрация: 04.09.2005
Сообщений: 5,921
Написано 3,411 полезных сообщений
(для 9,319 пользователей)
Ответ: Утечка памяти из-за оставшихся "внутренних" ссылок

Сборщик мусора у марка неумет обрабатывать перекрёстные ссылки, потому деструкторы не вызываются.
И в этом случае ваш вариант единственно правильный.
__________________
(Offline)
 
Ответить с цитированием
Старый 08.08.2010, 22:09   #5
Черный крыс
 
Сообщений: n/a
Ответ: Утечка памяти из-за оставшихся "внутренних" ссылок

Гы....
А я то думаю что он деструкторы не вызывает?....
как я понимаю мы должны избавится от ВСЕХ ссылок на экземпляр чтобы GC вызвал деструктор и почистил память?
 
Ответить с цитированием
Старый 10.10.2010, 05:07   #6
Reizel
Задрот
 
Аватар для Reizel
 
Регистрация: 24.07.2009
Адрес: Ивановская область, г. Кинешма
Сообщений: 1,571
Написано 407 полезных сообщений
(для 863 пользователей)
Ответ: Утечка памяти из-за оставшихся "внутренних" ссылок

Товарищи!
У меня есть класс, в нем список элементов этого класса.
Для удаления использую такую ф-ю:
function Remove(C:Class)
ListRemove(C,List)
C=null
GCCollect()
endFunction

все работло хорошо, но! для AI пришлось создать еще один класс, который мог содержать ссылку на объект класса Class. Как только вызывалась функция REmove() у того объекта, он выпадал из списка, на него терялась ссылка, но в том классе (ИИ) оставалась, поэтому GCCollect не удалял объект. Неужели придется при удалении объекта перелистывать все доступные ресурсы и обнулять в них ссылки на себя???????
__________________
(Offline)
 
Ответить с цитированием
Старый 13.12.2010, 09:05   #7
Жека
Дэвелопер
 
Регистрация: 04.09.2005
Адрес: Красноярск
Сообщений: 1,374
Написано 489 полезных сообщений
(для 882 пользователей)
Ответ: Утечка памяти из-за оставшихся "внутренних" ссылок

Вероятно, придётся.

Ещё есть понятие "паспорт объекта". Это по сути экземпляр объекта, содержащий дополнительные поля.

Например так

Type TObjectEx
  Field obj:TObject
  Global usedCount:Int
  Field bDeleted:Int
End Type
При создании нового экземпляра плюсуешь usedCount, при удалении минусуешь (если нужно знать количество экземпляров).

А при удалении присваиваешь bDeleted = true.

Далее в том месте где обрабатываешь объекты, пробегая по циклу, пишешь

for local obj:TObjectEx = eachin listObjs
  if(obj.bDeleted = true)
     listObjs.Delete(obj)
     continue
  endif

  'а тут обработка
next

А может это изврат - создавать ещё один класс - и проще в классы (type) объектов поле bDeleted дописать и всё.

Я лично вижу такой подход (наличие булевой переменной) применимым вполне, т.к. после однократного прохода по всем спискам, в которых есть ссылка на наш объект - эти ссылки удалятся, и получим то что хотим.
(Offline)
 
Ответить с цитированием
Сообщение было полезно следующим пользователям:
ABTOMAT (13.12.2010)
Ответ


Опции темы

Ваши права в разделе
Вы не можете создавать темы
Вы не можете отвечать на сообщения
Вы не можете прикреплять файлы
Вы не можете редактировать сообщения

BB коды Вкл.
Смайлы Вкл.
[IMG] код Вкл.
HTML код Выкл.

Похожие темы
Тема Автор Раздел Ответов Последнее сообщение
Lib_memclean: очистка оперативной памяти от "мусора". ViNT Библиотеки 7 17.12.2009 15:35
Можно ли сделать так чтобы при нажатии "вверх" и "вниз" двигалась одна картинка, а при нажатии "вправо" и "влево" - другая Total_Nube_&_Lamo Основной форум 2 13.12.2009 19:00
"Кодирование/декодирование изображений", или "Давайте попробуем скрыть ресурсы мидлетов" Richik Библиотеки 17 03.06.2009 10:18
Игра "Три слова". Рассказ "Время планет" Ilyich Юмор 77 02.04.2007 13:49
"Дневник памяти" impersonalis Болтовня 2 15.12.2006 22:27


Часовой пояс GMT +1, время: 00:34.


vBulletin® Version 3.6.5.
Copyright ©2000 - 2018, Jelsoft Enterprises Ltd.
Перевод: zCarot
Style crйe par Allan - vBulletin-Ressources.com