Уведомления о сборке мусора

Во-первых, извиняюсь за объем этого вопроса, но я хочу полностью объяснить себя с самого начала.

Хорошо, немного предыстории. Я работал над кодом, который реализует шаблон слабого события с помощью объекта WeakReference. При этом я столкнулся с обычной проблемой утечки объектов в определенных сценариях, когда издатель перестает генерировать события. Если вам интересно, то есть немного информации о узор там. Команда WPF реализовала WeakEventManager для решения проблемы, которая Я считаю, что использует планировщик для проверки каких-либо просочившихся объектов и избавляется от них.

Я решил попробовать другой подход. Вместо использования планировщика я хотел запустить обнаружение и отказ от подписки на просочившиеся объекты с помощью сборок мусора. Мне это кажется логичным, поскольку цели любых WeakReference объектов удаляются только в результате коллекций. Затем это заставило меня написать код, который будет вызывать событие, когда происходит сборка мусора. Сначала я рассмотрел возможность использования GC.RegisterForFullGCNotification механизма фреймворка, но быстро понял, что это невозможно, поскольку он нельзя использовать с одновременной сборкой мусора. Затем я немного почитал по этой теме и нашел решение, но у него есть пара проблем, и оно предупреждает вас только о коллекциях gen0 и gen2

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

using System;

namespace Test
{
    /// <summary>
    /// Class responsible for monitoring garbage collections.
    /// </summary>
    public static class GarbageCollectionMonitor
    {
        private static readonly object syncLock;
        private static int generation0CollectionCount;
        private static EventHandler<EventArgs> generation0Subscriptions;
        private static int generation1CollectionCount;
        private static EventHandler<EventArgs> generation1Subscriptions;
        private static int generation2CollectionCount;
        private static EventHandler<EventArgs> generation2Subscriptions;

        public static event EventHandler<EventArgs> Generation0GarbageCollected
        {
            add
            {
                lock (GarbageCollectionMonitor.syncLock)
                {
                    GarbageCollectionMonitor.generation0Subscriptions = (EventHandler<EventArgs>)Delegate.Combine(  GarbageCollectionMonitor.generation0Subscriptions,
                                                                                                                    value);
                }
            }

            remove
            {
                lock (GarbageCollectionMonitor.syncLock)
                {
                    GarbageCollectionMonitor.generation0Subscriptions = (EventHandler<EventArgs>)Delegate.Remove(   GarbageCollectionMonitor.generation0Subscriptions,
                                                                                                                    value);
                }
            }
        }

        public static event EventHandler<EventArgs> Generation1GarbageCollected
        {
            add
            {
                lock (GarbageCollectionMonitor.syncLock)
                {
                    GarbageCollectionMonitor.generation1Subscriptions = (EventHandler<EventArgs>)Delegate.Combine(  GarbageCollectionMonitor.generation1Subscriptions,
                                                                                                                    value);
                }
            }

            remove
            {
                lock (GarbageCollectionMonitor.syncLock)
                {
                    GarbageCollectionMonitor.generation1Subscriptions = (EventHandler<EventArgs>)Delegate.Remove(   GarbageCollectionMonitor.generation1Subscriptions,
                                                                                                                    value);
                }
            }
        }

        public static event EventHandler<EventArgs> Generation2GarbageCollected
        {
            add
            {
                lock (GarbageCollectionMonitor.syncLock)
                {
                    GarbageCollectionMonitor.generation2Subscriptions = (EventHandler<EventArgs>)Delegate.Combine(  GarbageCollectionMonitor.generation2Subscriptions,
                                                                                                                    value);
                }
            }

            remove
            {
                lock (GarbageCollectionMonitor.syncLock)
                {
                    GarbageCollectionMonitor.generation2Subscriptions = (EventHandler<EventArgs>)Delegate.Remove(   GarbageCollectionMonitor.generation2Subscriptions,
                                                                                                                    value);
                }
            }
        }

        /// <summary>
        /// Constructs the garbage collection monitor type
        /// </summary>
        static GarbageCollectionMonitor()
        {
            GarbageCollectionMonitor.syncLock = new object();

            // Construct a detector object
            //
            // N.B. No reference to the detector is held so that it can immediately be
            // collected by the garbage collector.
            new Detector();
        }

        /// <summary>
        /// Class responsible for detecting the operation of the garbage collector
        /// via its finalization method
        /// </summary>
        private sealed class Detector
        {
            /// <summary>
            /// Constructs a detector object
            /// </summary>
            public Detector()
            {
            }

            /// <summary>
            /// Finalizes a detector object
            /// </summary>
            ~Detector()
            {
                // Get the generation 0 collection count
                //
                // Since the finalizer thread is frozen when the garbage collector is
                // operating there is no danger of race conditions when retrieving the
                // garbage collection counts
                int generation0CollectionCount = GC.CollectionCount(0);

                // Determine if the current generation 0 collection count is greater than
                // the monitor's generation 0 collection count
                //
                // This indicates that a generation 0 garbage collection has taken place
                // since the last time a detector object was finalized.
                if (generation0CollectionCount > GarbageCollectionMonitor.generation0CollectionCount)
                {
                    // Update the monitor's generation 0 collection count to the current
                    // collection count
                    GarbageCollectionMonitor.generation0CollectionCount = generation0CollectionCount;

                    // Process any generation 0 event subscriptions
                    this.ProcessSubscriptions(GarbageCollectionMonitor.generation0Subscriptions);
                }

                int generation1CollectionCount = GC.CollectionCount(1);

                if (generation1CollectionCount > GarbageCollectionMonitor.generation1CollectionCount)
                {
                    GarbageCollectionMonitor.generation1CollectionCount = generation1CollectionCount;

                    this.ProcessSubscriptions(GarbageCollectionMonitor.generation1Subscriptions);
                }

                int generation2CollectionCount = GC.CollectionCount(2);

                if (generation2CollectionCount > GarbageCollectionMonitor.generation2CollectionCount)
                {
                    GarbageCollectionMonitor.generation2CollectionCount = generation2CollectionCount;

                    this.ProcessSubscriptions(GarbageCollectionMonitor.generation2Subscriptions);
                }

                // Construct a new generation 0 detector object
                new Detector();
            }

            /// <summary>
            /// Processes event subscriptions
            /// </summary>
            /// <param name="subscriptions">The subscriptions</param>
            private void ProcessSubscriptions(EventHandler<EventArgs> subscriptions)
            {
                // N.B. A local reference to the subscriptions delegate is required because
                // this method is run on the finalizer thread which is started AFTER the
                // garbage collector has finished running. As a result it is likely that
                // the application threads that were frozen by the garbage collector will
                // have been thawed. Since delegates are immutable, by getting a local
                // reference the processing of the subscriptions is made thread-safe as any
                // attempt by another thread to asynchronously add or remove a subscription
                // will result in a separate new delegate being constructed.

                // Determine if any event subscriptions need to be invoked
                //
                // N.B. If a local reference were not used then there would be a risk of
                // the following:
                //
                // (1) The null reference inequality check yields a true result.
                // (2) The finalizer thread is paused.
                // (3) Another thread removes all subscriptions to the event causing the
                // subscriptions delegate to be replaced with a null reference.
                // (4) The finalizer thread is unpaused.
                // (5) The attempt to invoke the subscriptions delegate results in a null
                // reference exception being thrown.
                if (subscriptions != null)
                {
                    // Invoke the event
                    subscriptions.Invoke(   this,
                                            EventArgs.Empty);
                }
            }
        }
    }
}

Кажется, он работает хорошо, но при тестировании с помощью следующего кода ...

    private void Gen0GarbageCollected(  object sender,
                                        System.EventArgs e)
    {
        Console.Write("Gen0  " + GC.CollectionCount(0) + Environment.NewLine);
    }

    private void Gen1GarbageCollected(  object sender,
                                        System.EventArgs e)
    {
        Console.Write("Gen1  " + GC.CollectionCount(1) + Environment.NewLine);
    }

    private void Gen2GarbageCollected(  object sender,
                                        System.EventArgs e)
    {
        Console.Write("Gen2  " + GC.CollectionCount(2) + Environment.NewLine);
    }

... я получаю следующие результаты

    Gen0    1
    Gen0    2
    Gen1    1
    Gen0    3
    Gen0    4
    Gen1    2
    Gen2    1
    Gen0    5
    Gen1    3
    Gen2    2
    Gen0    7
    Gen0    8
    Gen0    9
    Gen1    4
    Gen0    10
    Gen0    11
    Gen0    12
    Gen1    5
    Gen2    3
    Gen0    14
    Gen0    15
    Gen0    16
    Gen1    6
    Gen0    17

Кажется, что не все сборки мусора запускают поток завершения. В этом примере 6-я и 13-я коллекции поколения 0 не вызывают событий.

Теперь (наконец) возникает вопрос.

Меня это сбивает с толку, и мне нужно понять, почему это происходит. Мое рассуждение состоит в том, что, поскольку нет гарантии, когда будет запущен поток финализатора, возможно, что между экземплярами запущенного потока финализатора может произойти несколько сборок мусора (любого поколения).

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

Это лучшее, что я могу придумать, но я был бы очень признателен, если бы кто-то с более глубокими знаниями о внутреннем устройстве сборки мусора мог подтвердить, что это правильно, и у меня нет огромной ошибки реализации в моем решении.

Спасибо за терпение.


person 0b101010    schedule 17.07.2013    source источник
comment
Я не хочу набирать это в качестве ответа, потому что я только уточняю ваше предположение. Когда GC запускается и находит объект для сбора, если у него есть финализатор, он помещается в список freachable, который удаляет его из GC, делая его непригодным для сбора. Таким образом, когда запускается финализатор, GC на самом деле не полностью выполняется для этого объекта, он только заметил это и отложил для дальнейшей обработки. Только после выполнения финализатора и при следующем сборке мусора объект будет фактически собран. Не уверен, что это хоть как-то влияет на ваш вопрос.   -  person Lasse V. Karlsen    schedule 17.07.2013
comment
Кроме того, я не уверен, что опубликованный код Рихтера не может быть изменен для сообщения о коллекциях Gen1, но для этого потребуются некоторые уловки и, вероятно, не удастся поймать их все.   -  person Lasse V. Karlsen    schedule 17.07.2013
comment
И да, поток финализатора продвигается по финализирующим объектам в свободном списке в своем собственном темпе. Он не только выполняется между коллекциями, поэтому вы не сможете поймать все коллекции, используя только этот метод. Могу я спросить зачем вам нужно перехватывать сборку мусора? Для описанной вами проблемы действительно нужно реагировать на каждый цикл сборки мусора?   -  person Lasse V. Karlsen    schedule 17.07.2013
comment
@Lasse V. Karlsen Кроме того, я не уверен, что код, опубликованный Рихтером, нельзя изменить, чтобы сообщить о коллекциях Gen1, но для этого потребуются некоторые уловки и, вероятно, не удастся поймать их все. Вчера я провел весь день, играя с ним, и довел его до состояния, когда он сообщает о коллекциях первого поколения. Однако код был намного сложнее, чем образец, который я разместил здесь, и более ненадежным из-за различных проблем с подходом к перерегистрации.   -  person 0b101010    schedule 17.07.2013
comment
@Lasse V. Karlsen Что касается проблемы, которую вы описали, действительно ли вам нужно реагировать на каждый цикл сборки мусора? Нет, вы правы, мне не нужно получать уведомления о каждой коллекции. Мне просто интересно узнать причину, и я хочу убедиться, что в моей логике нет огромной концептуальной дыры.   -  person 0b101010    schedule 17.07.2013
comment
Помните, что финализаторы могут выделять и выделяют память, что, в свою очередь, может спровоцировать новый цикл сборки мусора. Так что нет, вы не можете реагировать на каждый цикл с помощью этого подхода.   -  person Lasse V. Karlsen    schedule 17.07.2013
comment
@Lasse V. Karlsen Помните, что финализаторы могут и выделяют память, что, в свою очередь, может спровоцировать новый цикл сборки мусора. Хороший момент. Не думал об этом. Цените ваши мысли. Спасибо.   -  person 0b101010    schedule 17.07.2013
comment
Я не понимаю, почему вы этим занимаетесь. Вы уже знаете, что финализатор бесполезен для получения уведомлений, нет смысла продолжать этот путь.   -  person Hans Passant    schedule 17.07.2013
comment
@Hans Passant Финализатор полезен для моих целей, поскольку, как заметил Лассе, не критично получать уведомления о каждой отдельной коллекции, чтобы эффективно очищать просочившиеся слабые подписки на события. Такой подход отлично подходит для описанного сценария использования. Я согласен с тем, что это не идеально, поскольку сборка мусора и финализация - это два отдельных процесса. Однако они сильно связаны, и это лучший механизм, который я мог придумать. Было бы очень интересно, если бы вы могли предложить лучший подход.   -  person 0b101010    schedule 17.07.2013
comment
Если вы делаете это только для очистки слабых ссылок, то совсем не критично, чтобы это выполнялось синхронно с коллекциями. Разве ты не можешь просто сделать это, когда добавляешь новый? Или использовать обычный таймер?   -  person Hans Passant    schedule 17.07.2013
comment
@Hans Passant Что делать, если не добавляется новый? На что вы устанавливаете интервал таймера? Эти триггеры не связаны с механизмом, который освобождает слабые ссылки, и ИМХО - худшая путаница, чем использование подхода финализатора. По крайней мере, финализаторы связаны с процессом сборки мусора. Во всяком случае, это все вскользь; сценарий использования на самом деле не важен. Вопрос был больше о взаимодействии между сборщиком мусора и потоком финализации. Это не критично, решение работает нормально, мне просто было любопытно, и я хотел убедиться, что правильно понял что-то важное.   -  person 0b101010    schedule 17.07.2013


Ответы (2)


Почему бы не использовать ConditionalWeakTable<TKey, TValue>, где ключ - это объект, который вы хотите монитор для очистки, а значение представляет собой специально созданный CriticalHandle < / a>, который реализует ReleaseHandle от установка в коде статического логического флага, означающего «пора очистить обработчики»?

Затем вы можете использовать отдельный таймер для запуска потока / задачи, которая проверяет логический флаг и, если true, запускает ручную очистку (эта очистка не будет и не должна выполняться во время финализатора или во время вызова ReleaseHandle).

person Sam Harwell    schedule 07.08.2013

Есть только две ситуации, когда действительно важно, будут ли очищены мертвые подписки на слабые события:

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

  • Если неограниченное количество новых подписчиков может быть добавлено без очистки мертвых подписок, количество мертвых подписок может неограниченно расти.

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

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

person supercat    schedule 07.08.2013