Статьи

Слабые, мягкие и фантомные ссылки: влияние на GC

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

Почему я должен волноваться?

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

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

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

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

При использовании  фантомных ссылок вы должны буквально выполнять ручное управление памятью в отношении пометки таких ссылок, подходящих для сбора мусора. Это опасно, так как поверхностный взгляд на javadoc может привести к тому, что они будут абсолютно безопасны в использовании:

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

Удивительно, но многие разработчики пропускают следующий абзац в том же Javadoc (выделение добавлено):

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

Это верно, мы должны вручную  очистить ()  фантомные ссылки или рискнуть столкнуться с ситуацией, когда JVM начинает умирать с OutOfMemoryError. Причина, по которой ссылки Фантома присутствуют в первую очередь, заключается в том, что это единственный способ выяснить, когда объект фактически стал недоступным с помощью обычных средств. В отличие от мягких или слабых ссылок, вы не можете воскресить объект, достижимый фантомом.

Дайте мне несколько примеров

Давайте посмотрим на  демонстрационное приложение,  которое выделяет много объектов, которые успешно восстанавливаются во время небольших сборок мусора. Принимая во внимание хитрость изменения порога владения на уровне повышения, мы могли бы запустить это приложение с  -Xmx24m -XX: NewSize = 16m -XX: MaxTenuringThreshold = 1  и увидеть это в журналах GC:

2.330: [GC (Allocation Failure)  20933K->8229K(22528K), 0.0033848 secs]
2.335: [GC (Allocation Failure)  20517K->7813K(22528K), 0.0022426 secs]
2.339: [GC (Allocation Failure)  20101K->7429K(22528K), 0.0010920 secs]
2.341: [GC (Allocation Failure)  19717K->9157K(22528K), 0.0056285 secs]
2.348: [GC (Allocation Failure)  21445K->8997K(22528K), 0.0041313 secs]
2.354: [GC (Allocation Failure)  21285K->8581K(22528K), 0.0033737 secs]
2.359: [GC (Allocation Failure)  20869K->8197K(22528K), 0.0023407 secs]
2.362: [GC (Allocation Failure)  20485K->7845K(22528K), 0.0011553 secs]
2.365: [GC (Allocation Failure)  20133K->9501K(22528K), 0.0060705 secs]
2.371: [Full GC (Ergonomics)  9501K->2987K(22528K), 0.0171452 secs]

Полные коллекции в этом случае довольно редки. Однако, если приложение также начинает создавать слабые ссылки ( -Dweak.refs = true ) на эти созданные объекты, ситуация может существенно измениться. Для этого может быть много причин, начиная от использования объекта в качестве ключей на слабой хэш-карте и заканчивая профилированием распределения. В любом случае, использование здесь слабых ссылок может привести к этому:

2.059: [Full GC (Ergonomics)  20365K->19611K(22528K), 0.0654090 secs]
2.125: [Full GC (Ergonomics)  20365K->19711K(22528K), 0.0707499 secs]
2.196: [Full GC (Ergonomics)  20365K->19798K(22528K), 0.0717052 secs]
2.268: [Full GC (Ergonomics)  20365K->19873K(22528K), 0.0686290 secs]
2.337: [Full GC (Ergonomics)  20365K->19939K(22528K), 0.0702009 secs]
2.407: [Full GC (Ergonomics)  20365K->19995K(22528K), 0.0694095 secs]

Как мы видим, в настоящее время существует множество полных коллекций, а продолжительность коллекций на порядок больше! Очевидный случай преждевременного продвижения по службе, но немного хитрый. Основная причина, конечно же, заключается в слабых ссылках. До того, как мы добавили их, объекты, созданные приложением, умирали непосредственно перед переходом в старое поколение. Но с добавлением, они теперь задерживаются на дополнительном раунде GC, чтобы на них можно было провести соответствующую очистку. Простым решением было бы увеличить размер молодого поколения, указав  -Xmx64m -XX: NewSize = 32m :

2.328: [GC (Allocation Failure)  38940K->13596K(61440K), 0.0012818 secs]
2.332: [GC (Allocation Failure)  38172K->14812K(61440K), 0.0060333 secs]
2.341: [GC (Allocation Failure)  39388K->13948K(61440K), 0.0029427 secs]
2.347: [GC (Allocation Failure)  38524K->15228K(61440K), 0.0101199 secs]
2.361: [GC (Allocation Failure)  39804K->14428K(61440K), 0.0040940 secs]
2.368: [GC (Allocation Failure)  39004K->13532K(61440K), 0.0012451 secs]

Объекты теперь снова утилизируются во время незначительного сбора мусора.

Ситуация еще хуже, когда используются  мягкие ссылки  , как показано в  следующем демонстрационном приложении . Мягко достижимые объекты не исправляются, пока приложение не рискует получить ошибку OutOfMemoryError. Замена слабых ссылок мягкими ссылками в демо-приложении немедленно обнаруживает еще много событий Full GC:

2.162: [Full GC (Ergonomics)  31561K->12865K(61440K), 0.0181392 secs]
2.184: [GC (Allocation Failure)  37441K->17585K(61440K), 0.0024479 secs]
2.189: [GC (Allocation Failure)  42161K->27033K(61440K), 0.0061485 secs]
2.195: [Full GC (Ergonomics)  27033K->14385K(61440K), 0.0228773 secs]
2.221: [GC (Allocation Failure)  38961K->20633K(61440K), 0.0030729 secs]
2.227: [GC (Allocation Failure)  45209K->31609K(61440K), 0.0069772 secs]
2.234: [Full GC (Ergonomics)  31609K->15905K(61440K), 0.0257689 secs]

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

Однако добавление одного флага, который отключает очистку фантомных ссылок ( -Dno.ref.clearing = true ), быстро даст нам следующее:

4.180: [Full GC (Ergonomics)  57343K->57087K(61440K), 0.0879851 secs]
4.269: [Full GC (Ergonomics)  57089K->57088K(61440K), 0.0973912 secs]
4.366: [Full GC (Ergonomics)  57091K->57089K(61440K), 0.0948099 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

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

Могут ли быть затронуты мои JVM?

В качестве общей рекомендации рассмотрите возможность включения   опции -XX: + PrintReferenceGC JVM, чтобы увидеть влияние различных ссылок на сборку мусора. Если мы добавим это в приложение из примера WeakReference, мы увидим это:

2.173: [Full GC (Ergonomics) 2.234: [SoftReference, 0 refs, 0.0000151 secs]2.234: [WeakReference, 2648 refs, 0.0001714 secs]2.234: [FinalReference, 1 refs, 0.0000037 secs]2.234: [PhantomReference, 0 refs, 0 refs, 0.0000039 secs]2.234: [JNI Weak Reference, 0.0000027 secs][PSYoungGen: 9216K->8676K(10752K)] [ParOldGen: 12115K->12115K(12288K)] 21331K->20792K(23040K), [Metaspace: 3725K->3725K(1056768K)], 0.0766685 secs] [Times: user=0.49 sys=0.01, real=0.08 secs] 
2.250: [Full GC (Ergonomics) 2.307: [SoftReference, 0 refs, 0.0000173 secs]2.307: [WeakReference, 2298 refs, 0.0001535 secs]2.307: [FinalReference, 3 refs, 0.0000043 secs]2.307: [PhantomReference, 0 refs, 0 refs, 0.0000042 secs]2.307: [JNI Weak Reference, 0.0000029 secs][PSYoungGen: 9215K->8747K(10752K)] [ParOldGen: 12115K->12115K(12288K)] 21331K->20863K(23040K), [Metaspace: 3725K->3725K(1056768K)], 0.0734832 secs] [Times: user=0.52 sys=0.01, real=0.07 secs] 
2.323: [Full GC (Ergonomics) 2.383: [SoftReference, 0 refs, 0.0000161 secs]2.383: [WeakReference, 1981 refs, 0.0001292 secs]2.383: [FinalReference, 16 refs, 0.0000049 secs]2.383: [PhantomReference, 0 refs, 0 refs, 0.0000040 secs]2.383: [JNI Weak Reference, 0.0000027 secs][PSYoungGen: 9216K->8809K(10752K)] [ParOldGen: 12115K->12115K(12288K)] 21331K->20925K(23040K), [Metaspace: 3725K->3725K(1056768K)], 0.0738414 secs] [Times: user=0.52 sys=0.01, real=0.08 secs]

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

Каково решение?

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

  • Слабые ссылки — если проблема вызвана увеличением потребления определенного пула памяти, увеличение соответствующего пула (и, возможно, общей кучи вместе с ним) может помочь вам. Как видно из раздела примеров, увеличение общей кучи и размеров молодого поколения облегчило боль.
  • Фантомные ссылки — убедитесь, что вы действительно очищаете ссылки. Легко отбрасывать определенные угловые случаи и иметь поток очистки, не способный не отставать от темпа, которым очередь заполнена, или прекратить очищать очередь в целом, оказывая большое давление на GC и создавая риск того, что он может закончиться OutOfMemoryError.
  • Мягкие ссылки — когда мягкие ссылки определены как источник проблемы, единственный реальный способ ослабить давление — это изменить внутреннюю логику приложения.

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