Циркулярные ссылки Причина утечки памяти? - PullRequest
34 голосов
/ 30 декабря 2008

Я пытаюсь устранить утечку памяти в приложении Windows Form. Сейчас я смотрю на форму, которая содержит несколько встроенных форм. Меня беспокоит то, что дочерние формы в своем конструкторе берут ссылку на родительскую форму и хранят ее в закрытом поле. Так что мне кажется, что настало время сбора мусора:

Родитель имеет ссылку на дочернюю форму через коллекцию элементов управления (там вложена дочерняя форма). Дочерняя форма не GC'd.

Дочерняя форма имеет ссылку на родительскую форму через поле закрытого члена. Родительская форма не GC'd.

Это точное понимание того, как сборщик мусора оценит ситуацию? Есть ли способ «доказать» это для целей тестирования?

Ответы [ 6 ]

38 голосов
/ 30 декабря 2008

Отличный вопрос!

Нет, обе формы будут (могут быть) GC'd, потому что GC напрямую не ищет ссылки в других ссылках. Он ищет только то, что называется «корневыми» ссылками ... Это включает в себя ссылочные переменные в стеке (переменная находится в стеке, фактический объект находится в куче), ссылочные переменные в регистрах ЦП и ссылочные переменные, которые статические поля в классах ...

Все остальные ссылочные переменные доступны (и GC'd) только в том случае, если на них ссылаются в свойстве одного из «корневых» ссылочных объектов, найденных вышеописанным процессом ... (или в объекте, на который ссылается ссылка в корневой объект и т.д ...)

Таким образом, только если одна из форм упоминается где-то еще в «корневой» ссылке - тогда обе формы будут защищены от GC.

Единственный способ «доказать» это (без использования утилит трассировки памяти) - создать пару сотен тысяч этих форм в цикле внутри метода, а затем, находясь в методе, посмотреть на объем памяти приложения, затем выйдите из метода, вызовите GC и снова посмотрите на этот элемент.

15 голосов
/ 30 декабря 2008

Как уже говорили другие, у GC нет проблем с циклическими ссылками. Я просто хотел бы добавить, что обычным местом утечки памяти в .NET являются обработчики событий. Если одна из ваших форм имеет прикрепленный обработчик события к другому объекту, который «жив», то есть ссылка на вашу форму, и форма не получит GC'd.

12 голосов
/ 30 декабря 2008

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

  1. Ссылки на глобальные объекты
  2. Ссылки на статические объекты
  3. Ссылки на статические поля
  4. Ссылки в стеке на локальные объекты
  5. Ссылки в стеке на параметры объекта, передаваемые в методы
  6. Ссылки на объекты, ожидающие завершения
  7. Ссылки в регистрах ЦП на объекты в управляемой куче

Список активных корней поддерживается CLR. Сборщик мусора работает, просматривая объекты в управляемой куче и видя, что все еще доступно приложению, то есть доступно через корень приложения. Такой объект считается корневым.

Теперь предположим, что у вас есть родительская форма, которая содержит ссылки на дочерние формы, а эти дочерние формы содержат ссылки на родительскую форму. Кроме того, предположим, что приложение больше не содержит ссылок на родительский элемент для какой-либо из дочерних форм. Затем для целей сборщика мусора эти управляемые объекты больше не являются корневыми и будут собираться мусором в следующий раз, когда происходит сборка мусора.

5 голосов
/ 30 декабря 2008

Если на родителя и ребенка не ссылаются, но они ссылаются только друг на друга, они получают GCed.

Получите профилировщик памяти, чтобы действительно проверить ваше приложение и ответить на все ваши вопросы. Я могу порекомендовать http://memprofiler.com/

2 голосов
/ 25 июня 2011

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

Допустим, у вас есть тип, который является источником события, например ::100100

interface IEventSource
{
    event EventHandler SomethingHappened;
}

Вот фрагмент класса, который обрабатывает события из экземпляров этого типа. Идея состоит в том, что всякий раз, когда вы назначаете новый экземпляр свойству, вы сначала отписываетесь от любого предыдущего назначения, а затем подписываетесь на новый экземпляр. Нулевые проверки гарантируют правильное поведение границ, и, что более важно, упрощают удаление: все, что вы делаете, - обнуляете свойство.

Что поднимает точку утилизации. Любой класс, подписывающийся на события, должен реализовывать интерфейс IDisposable, поскольку события являются управляемыми ресурсами. (N.B. Для краткости я пропустил правильную реализацию шаблона Dispose в примере, но вы поняли.)

class MyClass : IDisposable
{
    IEventSource m_EventSource;
    public IEventSource EventSource
    {
        get { return m_EventSource; }
        set
        {
            if( null != m_EventSource )
            {
                m_EventSource -= HandleSomethingHappened;
            }
            m_EventSource = value;
            if( null != m_EventSource )
            {
                m_EventSource += HandleSomethingHappened;
            }
        }
    }

    public Dispose()
    {
        EventSource = null;
    }

    // ...
}
0 голосов
/ 30 декабря 2008

ГК может правильно обращаться с циклическими ссылками, и если бы эти ссылки были единственными вещами, поддерживающими форму, то они были бы собраны.
У меня было много проблем с .net не вернуть память из форм. В версии 1.1 были некоторые ошибки в меню (я думаю), которые означали, что они не удалялись и могли вытекать из памяти. В этом случае добавление явного вызова для удаления и очистки переменной-члена в методе Dispose формы решило проблему. Мы обнаружили, что это также помогло восстановить память для некоторых других типов элементов управления.
Я также провел много времени с CLR-профилировщиком, глядя на то, почему формы не собирались. Насколько я мог судить, ссылки были сохранены в рамках. Один на тип формы. Таким образом, если вы создадите 100 экземпляров Form1, а затем закроете их все, только 99 будут восстановлены должным образом. Я не нашел способа вылечить это.
Наше приложение с тех пор перешло на .net 2, и это выглядит намного лучше. Память нашего приложения все еще увеличивается, когда мы открываем первую форму, и не закрывается, когда она закрыта, но я считаю, что это происходит из-за кода JIT и загружаемых дополнительных библиотек управления. Я также обнаружил, что, хотя GC может работать с циклическими ссылками, у него, кажется, есть проблемы (иногда) с циклическими ссылками на обработчики событий. IE object1 ссылается на object2, а object1 имеет метод, который обрабатывает и событие из object2. Я обнаружил обстоятельства, когда это не освободило объекты, когда я ожидал, но я так и не смог воспроизвести его в тестовом примере.

...