Слабые ссылки - насколько они полезны?

Поэтому в последнее время я размышлял над некоторыми идеями автоматического управления памятью - в частности, я смотрел на реализацию менеджера памяти на основе подсчета ссылок. Конечно, все знают, что циклические ссылки убивают наивный подсчет ссылок. Решение: слабые ссылки. Лично я ненавижу использовать слабые ссылки таким образом (есть другие более интуитивные способы справиться с этим, с помощью обнаружения цикла), но это заставило меня задуматься: где еще может быть полезна слабая ссылка?

Я полагаю, что должна быть какая-то причина, по которой они существуют, особенно в языках с трассирующей сборкой мусора, которые не страдают от циклических ловушек ссылок (с C# и Java я знаком, а в Java даже есть три вида слабых ссылок!). Когда я попытался найти для них надежные сценарии использования, у меня возникли идеи типа "Использовать их для реализации кэшей" (я видел это несколько раз на SO). Мне это тоже не нравится, так как они полагаются на тот факт, что трассирующий GC, скорее всего, не будет собирать объект сразу после того, как на него больше нет сильных ссылок, за исключением ситуаций с нехваткой памяти. Подобные случаи абсолютно недопустимы при подсчете ссылок GC, поскольку объект уничтожается сразу после того, как на него больше нет ссылок (за исключением, возможно, в случае циклов).

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

5 ответов

Решение

Обработчики событий - хороший пример использования слабых ссылок. Объект, который запускает события, нуждается в ссылке на объекты, чтобы вызывать обработчики событий, но вы, как правило, не хотите, чтобы удержание ссылки производителя событий препятствовало тому, чтобы потребители событий были GC'd. Скорее, вы бы хотели, чтобы у источника событий была слабая ссылка, и тогда он отвечал бы за проверку того, присутствовал ли уже упомянутый объект.

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

У меня есть общепризнанное догматическое мнение, что слабые ссылки на самом деле должны быть способом по умолчанию для постоянного хранения ссылки на объект с сильными ссылками, требующими более явного синтаксиса, например:

class Foo
{
    ...
    // Stores a weak reference to bar. 'Foo' does not
    // own bar.
    private Bar bar;

    // Stores a strong reference to 'Baz'. 'Foo' does
    // own Baz.
    private strong Baz baz;
}

... тем временем обратное для местных жителей внутри функции / метода:

void some_function()
{
    // Stores a strong reference to 'Bar'. It will
    // not be destroyed until it goes out of scope.
    Bar bar = ...;

    // Stores a weak reference to 'Baz'. It can be
    // destroyed before the weak reference goes out 
    // of scope.
    weak Baz baz_weak = ...;

    ...

    // Acquire a strong reference to 'Baz'.
    Baz baz = baz_weak;
    if (baz)
    {
        // If 'baz' has not been destroyed,
        // do something with it.
        baz.do_something();
    }       
}

Страшная история

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

Это было для 3D-продукта, который имел дело с такими здоровенными вещами, как сетки и текстуры, некоторые из которых могли по отдельности занимать гигабайт в памяти. Программное обеспечение вращалось вокруг графа сцены и архитектуры плагинов, где любой плагин мог получить доступ к графу сцены и элементам внутри, таким как текстуры или сетки, источники света или камеры.

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

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

Логические Утечки

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

В результате, когда пользователь запросил удаление сетки из сцены, возможно, 9/10 мест, где хранились ссылки на данную сетку, должным образом освободили бы ссылку, установив для нее нулевую ссылку или удалив ссылку из списка, чтобы позволить сборщик мусора для его сбора. Однако часто было десятое место, которое забывало обрабатывать такое событие, сохраняя сетку в памяти до тех пор, пока сама эта вещь не была также удалена со сцены (и иногда такие вещи жили вне сцены и сохранялись в корне приложения)., И это иногда каскадно доходило до точки, в которой программное обеспечение просто потребляло бы все больше и больше памяти, чем дольше вы использовали его, до такой степени, что плагины обработчиков (которые оставались без присмотра даже после очистки сцены) продлили бы срок службы всей сцены, сохраняя вставленная ссылка на корень сцены для DI, после чего память не освобождается даже после очистки всей сцены, что требует от пользователей периодически перезапускать программное обеспечение каждый час или два только для того, чтобы вернуть его к нормальному использованию памяти.,

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

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

Страшно!

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

Когда вы пытаетесь исследовать источник всех этих утечек, вы можете просматривать 20 миллионов строк кода, в том числе не зависящих от вас, написанных разработчиками плагинов, и любая из этих строк может молча продлевать срок службы объект гораздо дольше, чем уместно, просто сохраняя ссылку на объект и не выпуская его в ответ на соответствующее событие (я). Хуже того, все это летит под радар QA и автоматизированного тестирования.

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

GC Leaks

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

В моем конкретном случае наиболее желательные ошибки, если мы выбираем зло, - это те, которые легче всего обнаружить и воспроизвести, а утечки ресурсов типа GC не так легко обнаружить и их нелегко воспроизвести в любом смысле, который помогает вам обнаружить источник этой утечки.

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

GC не является практической защитой от утечек памяти, скорее наоборот. Если бы это было так, приложения с наименьшей утечкой в ​​мире были бы написаны на языках, поддерживающих GC, таких как Flash, Java, JavaScript, C#, а самое утекшее программное обеспечение, которое только можно себе представить, было бы написано на языках с наиболее ручным управлением памятью, таких как C, и в этот момент Ядро Linux должно быть чертовски слабой операционной системой, которая требует перезапуска каждый час или два, чтобы уменьшить использование памяти. Но это не так. Часто самые противоположные приложения пишутся против GC, и это потому, что на самом деле GC усложняет предотвращение логических утечек. Здесь это помогает избежать физических утечек (но физические утечки достаточно легко обнаружить и избежать их, в первую очередь, независимо от того, какой язык вы используете), и где это помогает - предотвратить сбой висячих указателей в критически важном программном обеспечении, где это более желательно утечка памяти, а не сбой, потому что на карту поставлена ​​жизнь человека или потому, что сбой может привести к тому, что сервер будет недоступен в течение нескольких часов подряд. Я не работаю в критически важных областях; Я работаю над производительностью и критично для памяти, когда эпические наборы данных обрабатываются с каждым отрисованным кадром.

В конце концов, все, что нам нужно сделать, чтобы создать логическую утечку с помощью GC, это:

class Foo
{
     // This makes 'Foo' instances cause 'bar' to leak, preventing
     // it from being destroyed until the 'Foo' instances are also
     // destroyed unless the 'Foo' instances set this to a null 
     // reference at the right time (ex: when the user requests 
     // to remove whatever Bar is from the software).
     private Bar bar;
}

... но слабые ссылки не рискуют этой проблемой. Когда вы смотрите на миллионы LOC, как указано выше, с одной стороны, и эпические утечки памяти, с другой, это довольно кошмарный сценарий, когда вы должны затем выяснить, какие аналогичные Foo не удалось установить аналог Bar на нулевую ссылку в соответствующее время, потому что это та часть, которая так страшна: код работает просто отлично, если вы игнорируете гигабайты утечки памяти. Ничто не вызывает каких-либо ошибок / исключений, ошибок утверждений и т. Д. Ничего не происходит. Все устройство и интеграция проходят без нареканий. Все это работает, за исключением того, что он теряет гигабайты памяти, вызывая жалобы пользователей влево и вправо, в то время как вся команда ломает голову над тем, какие части кодовой базы имеют утечки, а какие нет, пока QA пытается контролировать ущерб, прагматично предлагая пользователям сохранить их работу и перезапускать программное обеспечение каждые полчаса, как будто это должно быть какое-то решение.

Слабые ссылки очень помогают

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

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

Используя мой пример графа сцены, список исключений камеры не обязательно должен иметь объекты сцены, уже принадлежащие графу сцены. По логике это не имеет смысла, если это так. Если мы находимся у чертежной доски, никто не должен думать: "Да, камеры должны также иметь объекты сцены в дополнение к самому графу сцены".

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

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

Полезность слабых ссылок в командной среде

И это становится причиной полезности слабых ссылок на меня. Они никогда не являются абсолютно необходимыми, если каждый разработчик в вашей команде тщательно удаляет / обнуляет ссылки на объекты в соответствующие моменты времени в ответ на соответствующие события. Но, по крайней мере, в больших командах могут возникать ошибки, которые не могут быть полностью предотвращены техническими стандартами, а иногда и ошеломляющими темпами. А слабые ссылки являются фантастической защитой от тенденции к тому, что приложения, вращающиеся вокруг GC, имеют логическую утечку, чем дольше вы их запускаете. На мой взгляд, это защитный механизм, помогающий переводить ошибки, которые проявляются в виде трудно обнаруживаемых утечек памяти, в легко обнаруживаемое использование недопустимой ссылки на уже уничтоженный объект.

безопасности

Они могут показаться не такими полезными в том смысле, что программист на ассемблере может не найти большого применения для безопасности типов. В конце концов, он может делать все, что ему нужно, только с необработанными битами и байтами и соответствующими инструкциями по сборке. Однако безопасность типов помогает легче обнаруживать человеческие ошибки, заставляя разработчиков более четко выражать то, что они хотят сделать, и ограничивая то, что им разрешено делать с конкретным типом. Я вижу слабые ссылки в аналогичном смысле. Они помогают обнаруживать человеческие ошибки, которые в противном случае могли бы привести к утечке ресурсов, если бы не использовались слабые ссылки. Это умышленно накладывает на вас ограничения, такие как: "Хорошо, это слабая ссылка на объект, поэтому он не может продлить срок его службы и вызвать логическую утечку", что неудобно, но также и безопасность типов для программиста на сборке. Это все еще может помочь предотвратить некоторые очень неприятные ошибки.

Это функция языковой безопасности, если вы спрашиваете меня, и, как и любая другая функция безопасности, она не является абсолютно обязательной, и вы, как правило, не оцените ее до тех пор, пока не столкнетесь с командой, спотыкающейся об одних и тех же вещах снова и снова, потому что такая функция безопасности отсутствовала или не используется должным образом. Для индивидуальных разработчиков безопасность часто является одной из самых простых вещей, которую можно игнорировать, поскольку, если вы компетентны и осторожны, вам действительно может не понадобиться это лично. Но умножьте риск ошибок на целую команду людей со смешанными навыками, и функции безопасности могут стать вещами, которые вы отчаянно находите необходимыми, в то время как люди начинают аналогичным образом скользить по мокрому полу влево и вправо, которого вы тщательно избегаете ежедневно, в результате чего мертвые тела складываются. вокруг тебя. Вместо этого я обнаружил, что с большими командами, если у вас нет стандарта кодирования, которому просто следовать, но который излагает безопасные методы разработки с железным кулаком, то, что всего за месяц вы могли бы накопить более ста тысяч строк чрезвычайно код ошибки с неясными, трудно обнаруживаемыми ошибками, такими как логические утечки GC, упомянутые выше. Количество неработающих кодов, которые могут накапливаться всего за месяц, не имея стандарта для предотвращения распространенных ошибок, довольно ошеломляет.

Во всяком случае, я по общему признанию немного догматичен в этом вопросе, но мнение было сформировано из-за огромного количества эпических утечек памяти, и единственный ответ, который я видел, был только сказать разработчикам: "Будь осторожнее! Вы, ребята, теряете память как псих!" было заставить их чаще использовать слабые ссылки, и в этот момент любая небрежность не переросла бы в эпические объемы утечки памяти. Дело дошло до того, что в ретроспективе мы обнаружили так много мест с утечками, которые пролетали под радаром тестирования, что я намеренно нарушил обратную совместимость с источниками (хотя и не двоичную совместимость) в нашем SDK. Раньше у нас было такое соглашение:

typedef Strong<Mesh> MeshRef;
typedef Weak<Mesh> MeshWeakRef;

... это был проприетарный GC, реализованный на C++ и работающий в отдельном потоке. Я изменил это на это:

typedef Weak<Mesh> MeshRef;
typedef Strong<Mesh> MeshStrongRef;

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

Объект, на который ссылается WeakReference, будет доступен до процесса gc.

Поэтому, если мы хотим получить информацию об объекте, пока он существует, мы можем использовать WeakReference. Например, отладчику и оптимизатору часто нужно иметь информацию об объекте, но они не хотят влиять на процесс GC.

Кстати, SoftReference отличается от WeakReference, потому что связанный объект будет собираться только тогда, когда памяти недостаточно. Таким образом, SoftReference будет использоваться для построения глобального кэша.

Я часто использую WeakReference в сочетании с ThreadLocal или же InheritableThreadLocal, Если мы хотим, чтобы значение было доступно ряду потоков, пока оно имеет смысл, но затем мы удалим значение из этих потоков, мы не сможем на самом деле освободить память сами, потому что нет способа вмешаться в ThreadLocal значение в потоке, отличное от текущего. Однако то, что вы можете сделать, это положить значение в WeakReference в этих других потоках (при создании значения - предполагается, что один и тот же экземпляр является общим для нескольких потоков; обратите внимание, что это имеет смысл только тогда, когда только подмножество потоков должно иметь доступ к этому значению или вы просто используете статику) и хранить жесткая ссылка в другом ThreadLocal для некоторого рабочего потока, который будет удалять значение. Затем, когда значение перестает быть значимым, вы можете попросить рабочий поток удалить жесткую ссылку, в результате чего значения во всех других потоках будут немедленно помещены в очередь для сборки мусора (хотя они могут быть не сразу собраны, поэтому стоит есть другой способ предотвратить доступ к значению).

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

      abstract class Service {

    @SuppressWarnings("InfiniteRecursion")
    protected void start(int data) throws InterruptedException {
        data++;
        notifyListener(data);
        Thread.sleep(1000);
        start(data);
    }

    protected abstract void notifyListener(int data);

    protected abstract void register(Listener l);

}


class ServiceWeak extends Service {
    private WeakReference<Listener> listenerWeak;

    @Override
    protected void notifyListener(int data) {
        if (listenerWeak.get() != null) listenerWeak.get().onEvent(data);
    }
    @Override
    public void register(Listener l) {
        listenerWeak = new WeakReference<>(l);
    }
}

class ServiceStrong extends Service {
    private Listener listener;

    @Override
    protected void notifyListener(int data) {
        listener.onEvent(data);
    }

    @Override
    protected void register(Listener l) {
        listener = l;
    }
}

public class Listener {

    public Listener(Service service) {
        service.register(this);
    }

    public void onEvent(int data) {
        System.out.println("received data=>" + data);
        // show this on screen, make some sound etc.
    }
}

public class Activity {

    private Listener listener;

    public void onStart() throws InterruptedException {
        Service service = new ServiceStrong();
        listener = new Listener(service);
        new Thread(() -> {
            try {
                service.start(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
        Thread.sleep(3000);
        listener = null;
        System.gc();
    }


    public static void main(String[] args) throws InterruptedException {
            new Activity().onStart();
    }
}


В первом случае будем использовать Service со строгой ссылкой на Listener, т.е. ServiceStrong:

      public void onStart() throws InterruptedException {
        Service service = new ServiceStrong();
        listener = new Listener(service);
        new Thread(() -> {
            try {
                service.start(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
        Thread.sleep(3000); // Here your Activity is open, Listener is getting events
        listener = null; // Here you have closed your Activity 
        System.gc(); // Activity is destroyed, so must be Listener, but you can see that Listener is still working, getting events and printing it. This is because Service is holding a strong reference to it. So if you forget to unsubscribe or somehow manually dereference your Listener, then this is memory leak, because usually you have a list of listeners. I used only one for simplicity. 
    }

Теперь, если вы измените реализацию службы на ServiceWeak, вы увидите, что Listener останавливается после вызова System.gc() благодаря WeakReference.

Другие вопросы по тегам