forum.boolean.name

forum.boolean.name (http://forum.boolean.name/index.php)
-   FAQ и уроки (http://forum.boolean.name/forumdisplay.php?f=110)
-   -   Утечка памяти из-за оставшихся "внутренних" ссылок (http://forum.boolean.name/showthread.php?t=9558)

Жека 21.10.2009 14:16

Утечка памяти из-за оставшихся "внутренних" ссылок
 
Вложений: 1
Вступление

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

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

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


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


Итоги

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


Ребят, люблю и тех, для кого всё вышесказанное – очевидность.
Вот и всё. ;)

SBJoker 21.10.2009 15:38

Ответ: Утечка памяти из-за оставшихся "внутренних" ссылок
 
Добавлю что достаточно написать деструктор класса, для которого зарезервировано имя Delete:

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

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

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

Этот метод автоматически вызывается при создании объекта.

Жека 22.10.2009 08:25

Ответ: Утечка памяти из-за оставшихся "внутренних" ссылок
 
Для моего примера я не смог очистить память с помощью Delete(), добавленного в класс TDataSource. А причина всё та же - список этого класса хранит данные с указателями на экземпляры этого же класса в поле parent, а потому данный метод не вызывается.

SBJoker 22.10.2009 10:48

Ответ: Утечка памяти из-за оставшихся "внутренних" ссылок
 
Сборщик мусора у марка неумет обрабатывать перекрёстные ссылки, потому деструкторы не вызываются.
И в этом случае ваш вариант единственно правильный.

Черный крыс 09.08.2010 02:09

Ответ: Утечка памяти из-за оставшихся "внутренних" ссылок
 
Гы....
А я то думаю что он деструкторы не вызывает?....
как я понимаю мы должны избавится от ВСЕХ ссылок на экземпляр чтобы GC вызвал деструктор и почистил память?

Reizel 10.10.2010 09:07

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

все работло хорошо, но! для AI пришлось создать еще один класс, который мог содержать ссылку на объект класса Class. Как только вызывалась функция REmove() у того объекта, он выпадал из списка, на него терялась ссылка, но в том классе (ИИ) оставалась, поэтому GCCollect не удалял объект. Неужели придется при удалении объекта перелистывать все доступные ресурсы и обнулять в них ссылки на себя???????

Жека 13.12.2010 12:05

Ответ: Утечка памяти из-за оставшихся "внутренних" ссылок
 
Вероятно, придётся.

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

Например так

Код:

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 дописать и всё.

Я лично вижу такой подход (наличие булевой переменной) применимым вполне, т.к. после однократного прохода по всем спискам, в которых есть ссылка на наш объект - эти ссылки удалятся, и получим то что хотим.


Часовой пояс GMT +4, время: 17:33.

vBulletin® Version 3.6.5.
Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
Перевод: zCarot