Теорія та практика Java Усунення витоків пам’яті за допомогою слабких посилань

Слабкі посилання спрощують вираження зв'язків життєвого циклу об'єкта

практика

Серія контенту:

Цей контент є частиною # із серії # статей: Теорія та практика Java

Цей контент є частиною серії: Теорія та практика Java

Слідкуйте за виходом нових статей цієї серії.

Під час складання сміття (garbage collection, GC) повернення пам'яті, зайнятої об'єктами, які більше не використовуються програмою, вимагає, щоб логічний час існування об'єкта (той період часу, коли він використовується додатком) і фактичний час існування незавершених зв'язків з цим об'єктом був однаковим. У більшості випадків, грамотні прийоми проектування програмного забезпечення гарантують автоматичне виконання цієї умови, без необхідності приділяти особливу увагу проблемі часу існування об'єкта. Але іноді ми створюємо посилання, яке утримує об'єкт у пам'яті довше, ніж очікувалося; подібну ситуацію називають випадковим утриманням об'єкта у пам'яті.

Витоку пам'яті та глобальні карти розподілу

Найбільш поширеною причиною випадкового утримання об'єкта у пам'яті є використання карти розподілу Map для зв'язку метаданих з тимчасовими об'єктами. Скажімо, у Вас є об'єкт із середнім часом існування – більш тривалим, ніж час існування методу виклику, але більш коротким, порівняно з часом існування програми – наприклад, канал підключення клієнта. Ви хочете зв'язати деякі метадані з цим каналом, наприклад, ідентифікатор користувача, який встановив з'єднання. Вам невідома ця інформація при створенні об'єкта Socket (З'єднання), і ви не можете додати дані об'єкту Socket оскільки не контролюєте клас Socket або йогоекземпляр. В даному випадку звичайним прийомом є зберігання такої інформації в глобальній карті розподілу Map, як це зроблено в класі SocketManager в Лістингу 1:

Лістинг 1. Використання глобальної картки розподілу Map для співвіднесення метаданих з об'єктом

Недоліком цього підходу і те, що час існування метаданих необхідно прив'язати до часу існування сполуки. Коли ви будете точно знати, що програмі більше не потрібне дане з'єднання, Вам потрібно буде не забути видалити відповідний запис з картки розподілу ( Map ), інакше об'єкти З'єднання ( Socket ) та Користувач ( User ) будуть існувати в Map завжди - довгий час після того , як запит було обслужено, а з'єднання закрито. Це не дозволить очистити пам'ять, яку займають об'єкти Socket та User , навіть якщо вони ніколи не будуть використані в роботі програми. Будучи безконтрольним, це явище легко може призвести до переповнення пам'яті, якщо програма працює досить довго. У більшості випадків прийоми для визначення того, що Socket більше не потрібно програмі, нагадують дратівливі та сприятливі помилки прийоми, необхідні для неавтоматизованого управління пам'яттю.

Виявлення витоків пам'яті

Першим знаком того, що Ваша програма має витік пам'яті, зазвичай стає повідомлення про помилку OutOfMemoryError (Недостача Пам'яті), або незадовільна продуктивність програми через частого очищення пам'яті. На щастя, збирач сміття дозволяє отримати доступ до великої кількості інформації, яка може бути використана для виявлення витоку пам'яті. Якщо ви активуєте JVM з опцією -verbose:gc або -Xloggc, повідомлення про виявлення помилки виводиться на екран або в лог-файл при кожному запускузбирача сміття, і містить також інформацію про витрачений час, поточне використання динамічної пам'яті та обсяг відновленої пам'яті. Збір даних збирачем сміття та занесення їх у лог-файл не докучає Вам, тому розумно дозволити цю опцію для збирача сміття за умовчанням на випадок, якщо Вам коли-небудь доведеться аналізувати проблеми пам'яті або налаштовувати збирач сміття.

Інструментальні засоби можуть отримувати вихідні дані збирача сміття та відображати їх у графічній формі, одним із таких засобів є безкоштовний JTune (див. Ресурси). Вивчивши графік стану динамічної пам'яті після складання сміття, ви побачите динаміку використання пам'яті програмою. Для більшості програм можна розділити використання пам'яті на два компоненти: мінімальне використання пам'яті та поточне завантаження пам'яті. Для серверної програми мінімальне використання пам'яті відповідає стану, коли програма не виконує жодних запитів, але готова обслужити запит при надходженні; поточне завантаження пам'яті - це пам'ять зайнята під час обробки запиту, але звільнена після її завершенні. Оскільки завантаження пам'яті приблизно стабільне, програми зазвичай досить швидко досягають рівня сталого стану завантаження пам'яті. Якщо завантаження пам'яті продовжує зростати, навіть якщо програма завершила ініціалізацію та її завантаження не збільшуються, програма, ймовірно, утримує в пам'яті об'єкти, створені при обробці попередніх запитів.

У Лістингу 2 наведено приклад програми з витоком пам'яті. MapLeaker обробляє завдання у потоковому пулі та записує стан кожного завдання у Map. На жаль, вона не видаляє записи після завершення виконання завдання, тому записи стану та об'єкти завдань (а також їх внутрішній стан) постійнонакопичуються.

Лістинг 2. Програма з витоком пам'яті, пов'язана з картою розподілу

На малюнку 1 показано графік зміни завантаження динамічної пам'яті програмою після очищення пам'яті для MapLeaker з часом. Зміна кривої по висхідній – показник наявності витоку пам'яті. (У цих додатках кут нахилу кривої не буває настільки різким, але тенденція стає очевидною, якщо дані збирача сміття збираються досить довго.)

Рисунок 1. Стійка висхідна динаміка використання пам'яті

теорія

теорія

Після того, як Ви переконалися, що має місце витік пам'яті, наступним кроком буде встановлення того, який тип об'єктів є її причиною. Будь-який профіль пам'яті дозволяє зробити моментальний знімок стану пам'яті, дефрагментованої об'єктним класом. Існують чудові комерційні програми, що надають таку можливість, але Вам не доведеться витрачати гроші для виявлення витоків пам'яті – вбудований інструмент hprof також може виконати це завдання. Щоб використовувати hprof і відстежити використання пам'яті, викличте JVM з опцією -Xrunhprof:heap=sites .

У Лістингу 3 показана відповідна частина вихідних даних hprof, що ілюструє порушення використання пам'яті додатком. (Інструмент hprof перериває використання пам'яті після завершення роботи програми або після подачі сигналу kill -3 додатком або при натисканні Ctrl+Break у Windows.) Зверніть увагу на помітне збільшення об'єктів Map.Entry , Task та int[] між двома знімками.

У Лістингу 4 наведена інша частина вихідних даних hprof, що містить інформацію про стек викликів до місць розміщення для об'єктів Map.Entry. Ці дані повідомляють нам, які ланцюжки викликів генерують об'єкти Map.Entry; провівшианаліз програми, зазвичай, досить просто виявити джерело витоку пам'яті.

Лістинг 4. Вихідні дані HPROF, що показують розташування для об'єктів Map.Entry

Слабкі посилання поспішають на допомогу

Проблема з SocketManager полягає в тому, що час існування посилання Socket - User має збігатися з часом існування Socket, але мова програмування не дає нам можливості для простої реалізації цієї умови. Це змушує програму вдаватися до прийомів, що нагадують керування пам'яттю вручну. На щастя, починаючи з JDK 1.2, програма очищення пам'яті дає можливість оголошувати такі залежності життєвого циклу об'єктів, щоб збирач сміття міг допомогти нам уникнути витоків пам'яті такого роду - за допомогою слабких посилань.

Об'єкт посилання слабкого зв'язку WeakReference створюється під час роботи конструктора та її значення, якщо він ще стертий, може бути отримано у вигляді методу get() . Якщо слабкі посилання були стерті (оскільки об'єкт посилання був оброблений збирачем сміття, або тому що був викликаний метод WeakReference.clear() ), get() повертає null . Відповідно, необхідно завжди перевіряти, чи є повернене методом get() значення ненульовим, перш ніж використовувати його результат, оскільки передбачається, що з часом об'єкт посилання буде оброблений збирачем сміття.

Копіюючи посилання на об'єкт використовуючи звичайні (сильні) посилання, Ви накладаєте обмеження на час існування об'єкта посилання, яке має бути щонайменше таким самим, як і у посилання, що копіюється. Якщо ви не уважні, воно може бути таким самим, як у Вашої програми - як коли Ви поміщаєте об'єкт у глобальну колекцію. З іншого боку, створюючи слабке посилання об'єкт, Ви збільшуєте час існування об'єкта посилання зовсім; Ви простозабезпечуєте альтернативний спосіб звернення до нього, поки він ще існує.

Слабкі посилання найбільш корисні при створенні слабких колекцій, таких, як зберігають метадані про об'єкти рівно стільки, скільки додаток їх (об'єкти) використовує саме те, що повинен робити клас SocketManager . Оскільки це звичайне використання слабких посилань, WeakHashMap , що використовує слабкі посилання для ключів (а не значень), було також додано до класу бібліотек JDK 1.2. Якщо Ви використовуєте об'єкт як ключ у звичайній HashMap, він не може бути оброблений збирачем сміття доки запис не видалено з карти розподілу Map; WeakHashMap дозволяє використовувати об'єкт як ключ Map , не перешкоджаючи його обробці збирачем сміття. У Лістингу 5 наведена можлива реалізація методу get() WeakHashMap , демонструє використання слабких посилань:

Лістинг 5. Можлива реалізація методу WeakReference.get()

При додаванні запису розміщення інформації в WeakHashMap , пам'ятайте, що цей запис розміщення може пізніше стати "неробочим", тому що ключ буде оброблений збирачем сміття. У цьому випадку get() повертає null , що робить перевірку значення, що повертається get() (перевірку, чи є це значення null ) ще більш важливою, ніж зазвичай.

Усунення витоку за допомогою WeakHashMap

Усунення витоку в SocketManager нескладно; просто замініть HashMap на WeakHashMap , як показано в Лістингу 6. ​​(Якщо SocketManager має бути потоково-орієнтованим, Ви можете перенести WeakHashMap за допомогою Collections.synchronizedMap() ). Цей спосіб може використовуватися завжди, коли час існування картки розміщення повинен бути прив'язаний до часу існування ключа. Проте не слід зловживати цим прийомом; в більшості випадків звичайнаHashMap є придатною для використання реалізацією Map.

Лістинг 6. Виправлення SocketManager за допомогою WeakHashMap

Черги посилань

WeakHashMap використовує слабкі посилання для зберігання ключів карт, що дозволяє об'єктам ключів бути обробленими програмою очищення пам'яті, коли вони більше не використовуються програмою, а така реалізація методу get() як WeakReference.get() може розпізнати, чи об'єкт використовується чи ні, повертаючи значення null у разі. Але це лише половина того, що потрібно для утримання використання пам'яті карткою розподілу Map від зростання протягом життєвого циклу програми; щось ще має бути зроблено для видалення невикористовуваних більше записів з Map після того, як ключ був оброблений збирачем сміття. Інакше Map просто буде переповнена записами, що відповідають "мертвим" ключам. І хоча програма не буде цього бачити, може виникнути перевитрата пам'яті програмою, оскільки Map.Entry і об'єкти, що зберігають значення, не будуть оброблені збирачем сміття, незважаючи на те, що оброблені ключі.

Записи, що не використовуються, можуть бути виявлені і видалені періодичним скануванням Map за допомогою виклику get() до кожного слабкого посилання і видаленням запису, якщо значення, що повертається get() - null . Але цей спосіб є малопродуктивним, якщо Map зберігає багато робочих записів. Було б дуже зручно отримувати повідомлення, якщо об'єкт посилання був оброблений програмою очищення пам'яті. Саме це завдання виконують черги посилань.

WeakHashMap має також власний метод expungeStaleEntries() , який викликається при більшості операцій Map , що по порядку обробляє в черзі посилань будь-які неробочі посилання та видаляє асоційовані відображення. Можлива реалізація методуexpungeStaleEntries() наведена в Лістингу 7. Тип Entry , який використовується для зберігання відображень ключ-значення, є розширенням WeakReference , тому коли expungeStaleEntries() запитує наступне неробоче посилання, йому повертається Entry . Використання черг посилань для очищення Map замість періодичного пошуку по всьому вмісту більш ефективно, оскільки робочі записи ніколи не зачіпаються в процесі очищення; очищення відбувається лише за наявності посилань, справді поміщених у чергу.

Лістинг 7. Можлива реалізація методу WeakHashMap.expungeStaleEntries()

Висновок

Ресурси для скачування

Схожі теми

  • Оригінал статті: Plugging memory leaks with weak references.
  • JTune: Безкоштовний інструмент JTune, що дозволяє обробляти реєстраційні записи збирача сміття, представляти у графічному вигляді обсяг динамічної пам'яті, тривалість складання сміття (процесу очищення пам'яті) та інші корисні дані з управління пам'яттю.
  • "Налаштування очищення пам'яті в HotSpot JVM": Кірк Пеппердайн (Kirk Pepperdine) і Джек Ширазі (Jack Shirazi) покажуть, як навіть незначний витік пам'яті з часом починає чинити сильний тиск на збирач сміття.
  • "HPROF": У цьому документі Sun описує вбудований інструмент HPROF для аналізу профілю.
  • Посилальні об'єкти та складання сміття: У цьому документі Sun, написаному невдовзі після додавання посилальних об'єктів у бібліотеку класів, описано, як збирач сміття обробляє об'єкти посилань.
  • Розділ Java-технології: Сотні статей щодо кожного аспекту програмування на Java.

Коментарі