Як створити витік пам'яті на Java?


3223

Я щойно провів співбесіду, і мене попросили створити витік пам'яті з Java.
Потрібно сказати, що я відчував себе досить німим, не маючи поняття, як навіть почати його створювати.

Що може бути прикладом?


275
Я б сказав їм, що Java використовує сміттєзбірник, і попросив їх бути дещо конкретнішими щодо їх визначення "витоку пам'яті", пояснивши, що - забороняючи помилки JVM - Java не може просочити пам'ять абсолютно однаково C / C ++ може. Ви повинні десь мати посилання на об’єкт .
Дарієн

371
Мені здається смішним, що в більшості відповідей люди шукають ці крайні випадки та хитрощі, і, здається, зовсім не вистачає точки (IMO). Вони могли просто показати код, який зберігає марні посилання на об’єкти, які більше ніколи не будуть використовуватись, і в той же час ніколи не відкидає ці посилання; можна сказати, що ці випадки не є "справжніми" витоками пам'яті, оскільки все ще є посилання на ці об'єкти навколо, але якщо програма ніколи більше не використовує ці посилання, а також ніколи їх не скидає, це цілком еквівалентно (і як погано) " справжній витік пам'яті ".
ehabkost

62
Чесно кажучи, я не можу повірити, що аналогічне запитання, яке я задавав про "Ідіть", зменшилось до -1 Тут: stackoverflow.com/questions/4400311/… В основному витоки пам’яті, про які я говорив, - це ті, хто отримав +200 надбавок до ОП, але все ж мене напали і образили за запитання, чи не було у „Іди” того ж питання. Я якось не впевнений, що всі вікі-речі працюють так чудово.
SyntaxT3rr0r

74
@ SyntaxT3rr0r - відповідь Дарієна - це не фанбоїзм. він прямо визнав, що в деяких JVM можуть бути помилки, які означають, що пам'ять просочується. це інше, ніж сама мовна специфіка, що дозволяє витоку пам'яті.
Пітер Рекор

31
@ehabkost: Ні, вони не рівноцінні. (1) Ви володієте здатністю повернути пам'ять, тоді як у "справжній витоку" ваша програма C / C ++ забуває діапазон, який був виділений, немає безпечного способу відновлення. (2) Ви можете дуже легко виявити проблему з профілюванням, тому що ви можете бачити, які об'єкти стосуються "прошарку". (3) "Справжній витік" - це недвозначна помилка, тоді як програма, яка зберігає багато об'єктів до моменту припинення, може бути навмисною частиною того, як вона має працювати.
Дарієн

Відповіді:


2309

Ось хороший спосіб створити справжню витік пам'яті (об’єкти, недоступні за допомогою коду, але все ще зберігаються в пам'яті) на чистому Java:

  1. Додаток створює довго працюючу нитку (або використовувати пул потоків, щоб просочитися ще швидше).
  2. Нитка завантажує клас через (необов'язково) ClassLoader.
  3. Клас виділяє великий фрагмент пам'яті (наприклад new byte[1000000]), зберігає чітке посилання на нього в статичному полі, а потім зберігає посилання на себе в a ThreadLocal. Виділення додаткової пам'яті необов’язково (протікає екземпляр класу достатньо), але це зробить роботу витоку набагато швидше.
  4. Додаток видаляє всі посилання на користувацький клас або з ClassLoaderнього завантажений.
  5. Повторіть.

Завдяки способу ThreadLocal, реалізованому в JDK Oracle, це створює витік пам'яті:

  • У кожному Threadє приватне поле threadLocals, яке фактично зберігає локальні значення потоку.
  • Кожен ключ на цій карті є слабким посиланням на ThreadLocalоб’єкт, тому після того, як цей ThreadLocalоб’єкт збирається сміттям, його запис видаляється з карти.
  • Але кожне значення є сильним посиланням, тому коли значення (прямо чи опосередковано) вказує на ThreadLocalоб'єкт, який є його ключем , цей об'єкт не буде ні збирати сміття, ні вилучати з карти, поки живуть нитки.

У цьому прикладі ланцюжок чітких посилань виглядає приблизно так:

Threadоб'єкт → threadLocalsкарта → екземпляр класу приклад → клас прикладу → статичне ThreadLocalполе → ThreadLocalоб’єкт.

( ClassLoaderНасправді це не відіграє роль у створенні витоку, це просто посилює витік через цю додаткову довідкову ланцюжок: приклад клас → ClassLoader→ усі класи, які він завантажив. Це було ще гірше у багатьох реалізаціях JVM, особливо до початку Java 7, тому що класи та класи ClassLoaderбули розподілені прямо в permgen і взагалі ніколи не збирали сміття.)

Різновидом цієї схеми є те, чому контейнери додатків (як Tomcat) можуть просочувати пам'ять як сито, якщо ви часто перерозподіляєте додатки, які трапляються з використанням ThreadLocals, які певним чином вказують на себе. Це може статися з ряду тонких причин і часто важко налагодити та / або виправити.

Оновлення : оскільки багато людей продовжують просити, ось приклад коду, який показує таку поведінку в дії .


186
+1 Витоки ClassLoader - це найчастіше болісні витоки пам'яті у світі JEE, які часто спричинені сторонніми конвеєрами, які трансформують дані (BeanUtils, кодеки XML / JSON). Це може статися, коли lib завантажується поза кореневим завантажувачем програми, але містить посилання на ваші класи (наприклад, кешування). Коли ви не повторно використовуєте / переукладаєте додаток, JVM не зможе зібрати сміття завантажувача програми (і, отже, всі класи, завантажені ним), тому при повторному розгортанні сервер додатків врешті-решт загрожує. Якщо вам пощастило, ви знайдете підказку з ClassCastException, zxyAbc не може бути передана zxyAbc
earcam

7
tomcat використовує хитрощі та ліквідує ВСІ статичні змінні у ВСІХ завантажених класах, але в tomcat є багато даних, а також неправильне кодування (потрібно отримати певний час та подати виправлення), а також весь розумний ConcurrentLinkedQueue як кеш для внутрішніх (малих) об'єктів, настільки малий, що навіть ConcurrentLinkedQueue.Node займає більше пам’яті.
bestsss

57
+1: Витоки завантажувача класу - це кошмар. Я проводив тижні, намагаючись їх з'ясувати. Сумно в тому, що, як сказав @earcam, вони здебільшого спричинені сторонніми губами, а також більшість профілів не можуть виявити цих витоків. У цьому блозі є чітке і чітке пояснення щодо витоків Classloader. blogs.oracle.com/fkieviet/entry/…
Адріан М

4
@Nicolas: Ви впевнені? JRockit робить об'єкти класу GC за замовчуванням, а HotSpot ні, але AFAIK JRockit все ще не може GC класу або ClassLoader, на який посилається ThreadLocal.
Даніель Приден

6
Tomcat спробує виявити ці витоки для вас та попередити про них: wiki.apache.org/tomcat/MemoryLeakProtection . Найновіша версія іноді навіть виправить витік для вас.
Matthijs Bierman

1212

Статичне поле, що містить посилання на об'єкт [остаточне поле esp]

class MemorableClass {
    static final ArrayList list = new ArrayList(100);
}

Виклик String.intern()на тривалій струні

String str=readString(); // read lengthy string any source db,textbox/jsp etc..
// This will place the string in memory pool from which you can't remove
str.intern();

Відкриті потоки (файл, мережа тощо)

try {
    BufferedReader br = new BufferedReader(new FileReader(inputFile));
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

Незакриті з'єднання

try {
    Connection conn = ConnectionFactory.getConnection();
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

Ділянки, недоступні для сміттєзбірника JVM , такі як пам'ять, виділена за допомогою власних методів

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

getServletContext().setAttribute("SOME_MAP", map);

Неправильні або невідповідні параметри JVM , такі як noclassgcопція в IBM JDK, що запобігає збору сміття, що не використовується

Див. Налаштування IBM jdk .


178
Я не погоджуюся з тим, що атрибути контексту та сеансу є "витоками". Вони просто довговічні змінні. А статичне підсумкове поле є більш-менш просто константою. Можливо, слід уникати великих констант, але я не думаю, що це справедливо називати це витоком пам'яті.
Ian McLaird

80
Відкриті потоки (незакриті) (файл, мережа тощо ...) не протікають по-справжньому, під час завершення (що буде після наступного циклу GC) планується закрити (), close()як правило, планується ( як правило, не запускається у фіналізаторі нитка, оскільки може бути операцією блокування). Погана практика не закриватись, але це не спричиняє протікання. Незакрите java.sql.Connection те саме.
bestsss

33
У більшості розумних JVM, схоже, клас String має лише слабке посилання на internвміст хештелю. Таким чином , він буде сміття належним чином і не текти. (Але IANAJP) mindprod.com/jgloss/interned.html#GC
Метт Б.

42
Як статичне поле, що містить посилання на об'єкт [остаточне поле esp], є витоком пам'яті
Канагавелу Сугумар

5
@cHao Правда. Небезпека, з якою я стикався, не є проблемою, коли пам'ять просочується потоками. Проблема в тому, що їм просочилося недостатньо пам'яті. Можна просочити багато ручок, але все одно мати багато пам’яті. Тоді колектор сміття може вирішити не заважати робити повну колекцію, тому що у неї ще багато пам’яті. Це означає, що фіналізатор не викликається, тому у вас не вистачає ручок. Проблема полягає в тому, що фіналізатори (як правило) запускаються до того, як у вас не залишиться пам'яті з потоків потоку, але вона може не викликатись, перш ніж у вас закінчиться щось, крім пам’яті.
Патрік М

460

Проста річ - це використовувати HashSet з неправильним (або неіснуючим) hashCode()або equals(), а потім продовжувати додавати "дублікати". Замість того, щоб ігнорувати дублікати, як слід, набір буде тільки коли-небудь зростати, і ви не зможете їх видалити.

Якщо ви хочете, щоб ці погані клавіші / елементи звисали навколо, ви можете використовувати статичне поле типу

class BadKey {
   // no hashCode or equals();
   public final String key;
   public BadKey(String key) { this.key = key; }
}

Map map = System.getProperties();
map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.

68
Насправді ви можете видалити елементи з HashSet, навіть якщо клас елементів отримав hashCode і дорівнює неправильному; просто знайдіть ітератор для набору і використовуйте його метод видалення, оскільки ітератор фактично працює над самими базовими записами, а не елементами. (Зверніть увагу , що невиконаний хеш - код / одно це НЕ досить , щоб викликати витік, за замовчуванням реалізувати простий ідентифікатор об'єкта , і тому ви можете отримати елементи і видалити їх , як правило.)
Донал Fellows

12
@ Доктор, що я намагаюся сказати, я думаю, чи не згоден я з вашим визначенням витоку пам'яті. Я вважаю (продовжую аналогію), що ваша техніка видалення ітератора є крапельницею під витік; витік все ще існує незалежно від капельниці.
corsiKa

94
Я погоджуюся, це не "протікання" пам'яті, тому що ви можете просто видалити посилання на хешсет і чекати, коли GC запуститься, і presto! пам'ять йде назад.
користувач541686

12
@ SyntaxT3rr0r, я розтлумачив ваше запитання як запитання, чи є щось у мові, що, природно, призводить до витоку пам'яті. Відповідь - НІ. Ці питання задають питання, чи можна створити ситуацію, щоб створити щось на зразок витоку пам'яті. Жоден із цих прикладів не протікає в пам'яті таким чином, як програміст C / C ++ зрозумів би це.
Пітер Лорі

11
@ Петер Лоурі: також, що ви думаєте з цього приводу: "На мові C немає нічого, що природно просочиться до витоку пам'яті, якщо ви не забудете вручну звільнити виділену вами пам'ять" . Як би це було за інтелектуальну нечесність? У всякому разі, я втомився: ти можеш мати останнє слово.
SyntaxT3rr0r

271

Нижче буде не очевидний випадок, коли Java протікає, окрім стандартного випадку забутих слухачів, статичних посилань, фальшивих / модифікованих ключів у хеш-мапах або просто ниток, що застрягли без жодного шансу закінчити свій життєвий цикл.

  • File.deleteOnExit() - завжди протікає струна, якщо рядок є підрядком, витік ще гірший (також протікає основний знак [])- у підрядку Java 7 також копіюється char[], тому пізніша версія не застосовується ; @Daniel, хоча голосів немає.

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

  • Runtime.addShutdownHookа не видаляти ... а потім навіть у RemoveShutdownHook через помилку в класі ThreadGroup щодо непроменених потоків він може не збиратися, ефективно протікати ThreadGroup. JGroup має витік у GossipRouter.

  • Створення, але не починаючи, Threadперехід відноситься до тієї ж категорії, що і вище.

  • Створення потоку успадковує ContextClassLoaderі AccessControlContext, а також ThreadGroupі будь-які InheritedThreadLocal, всі ці посилання є потенційними витоками разом із усіма класами, завантаженими завантажувачем класів, і всіма статичними посиланнями, і ja-ja. Ефект особливо помітний у всій системі jucExecutor, яка має надзвичайно простий ThreadFactoryінтерфейс, проте більшість розробників не мають поняття про небезпеку, що ховається. Крім того, багато бібліотек запускають теми за запитом (тому занадто багато популярних у галузі бібліотек).

  • ThreadLocalсхованки; це зло в багатьох випадках. Я впевнений, що кожен бачив досить багато простих кешів, заснованих на ThreadLocal, і погана новина: якщо потік продовжує більше, ніж очікувалося, життя в контексті ClassLoader, це чистий приємний протікання. Не використовуйте кеші ThreadLocal, якщо вони справді не потрібні.

  • Виклик, ThreadGroup.destroy()коли у ThreadGroup немає самих потоків, але вона все ще зберігає дочірні ThreadGroups. Поганий витік, який перешкоджатиме видаленню ThreadGroup з його батьків, але всі діти стають незліченними.

  • Використання WeakHashMap та значення (in) безпосередньо посилаються на ключ. Це важко знайти без сміттєзвалища. Це стосується всіх розширених, Weak/SoftReferenceякі можуть тримати жорсткий посилання на об'єкт, що охороняється.

  • Використання java.net.URLпротоколу HTTP (S) та завантаження ресурсу з (!). Цей особливий, він KeepAliveCacheстворює новий потік у системі ThreadGroup, який протікає з поточного класного завантажувача поточного потоку. Нитка створюється за першим запитом, коли жодної живої нитки не існує, тож вам може пощастить або просто просочиться. Витік уже виправлений в Java 7, і код, який створює потік, належним чином видаляє контекстний завантажувач. Є ще кілька випадків (як ImageFetcher, також зафіксовано ) створення подібних ниток.

  • Використовуючи InflaterInputStreamпередачу new java.util.zip.Inflater()в конструктор ( PNGImageDecoderнаприклад), а не виклик end()надувача. Ну, якщо ви переходите в конструктор просто new, жодних шансів ... І так, виклик close()потоку не закриває інфлятор, якщо він передається вручну як параметр конструктора. Це не справжній витік, оскільки він буде випущений фіналізатором ... коли він вважатиме за потрібне. До цього моменту він з'їдає рідну пам'ять так сильно, що може змусити Linux oom_killer безкарно вбити процес. Головне питання полягає в тому, що доопрацювання на Java дуже ненадійне, і G1 погіршив її до 7.0.2. Мораль історії: звільняйте рідні ресурси, як тільки зможете; фіналізатор занадто поганий.

  • Той самий випадок із java.util.zip.Deflater. Це набагато гірше, оскільки Deflater є голодним на пам'яті Java, тобто завжди використовує 15 біт (макс.) І 8 рівнів пам'яті (9 - макс.), Виділяючи кілька сотень КБ вбудованої пам'яті. На щастя, Deflaterвін не використовується широко, і наскільки мені відомо, JDK не містить зловживань. Завжди дзвоніть, end()якщо ви вручну створили Deflaterабо Inflater. Найкраща частина останніх двох: їх неможливо знайти за допомогою звичайних інструментів для профілювання.

(Я можу додати ще кілька витратників часу, з якими я стикався за запитом.)

Удачі і будьте в безпеці; витоки злі!


23
Creating but not starting a Thread...Так, мене це сильно покусало кілька століть тому! (Java 1.3)
leonbloy

@leonbloy, перш ніж це було ще гірше, оскільки нитка була додана прямо до групи ниток, не починаючи означало дуже сильну протікання. Це не просто збільшує unstartedкількість, але це запобігає руйнуванню групи ниток (менше зла, але все-таки витік)
bestsss

Дякую! "Виклик, ThreadGroup.destroy()коли у ThreadGroup немає самих потоків ..." - це неймовірно тонка помилка; Я переслідував це годинами, збивався з глузду, тому що перерахування нитки в моєму інтерфейсі управління не показало нічого, але група ниток і, мабуть, хоча б одна дочірня група не піде.
Лоуренс Дол

1
@bestsss: Мені цікаво, чому б ви хотіли зняти гачок відключення, враховуючи, що він працює на, ну, JVM вимкнення?
Лоуренс Дол

203

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

Але є справді простіші випадки витоку пам'яті. Збирач сміття лише звільняє те, на що більше не йдеться. Ми, як розробники Java, не дбаємо про пам'ять. Ми виділяємо його за потреби і нехай він буде звільнений автоматично. Чудово.

Але будь-які довгоживучі програми, як правило, мають спільний стан. Це може бути що завгодно, статика, синглтон ... Часто нетривіальні програми, як правило, складають графіки складних об'єктів. Достатньо просто забути встановити посилання на null або частіше забути видалити один об'єкт із колекції, щоб витік пам'яті.

Звичайно, усілякі слухачі (наприклад, слухачі інтерфейсу), кеші чи будь-який довговічний загальний стан мають тенденцію до витоку пам'яті, якщо не належним чином обробляються. Слід розуміти, що це не кутовий випадок Java чи проблема зі смітником. Це проблема дизайну. Ми розраховуємо, що додаємо слухача до довгоживучого об'єкта, але не видаляємо слухача, коли це вже не потрібно. Ми кешуємо об’єкти, але у нас немає стратегії їх видалення з кеша.

У нас може бути складний графік, який зберігає попередній стан, необхідний для обчислення. Але попередній стан сам по собі пов'язаний з державою раніше тощо.

Як і ми повинні закрити з'єднання SQL або файли. Нам потрібно встановити належні посилання на null та видалити елементи з колекції. Ми матимемо належні стратегії кешування (максимальний розмір пам'яті, кількість елементів або таймери). Усі об'єкти, які дозволяють отримувати сповіщення слухача, повинні забезпечувати як метод addListener, так і deleteListener. І коли ці сповіщувачі більше не використовуються, вони повинні очистити свій список слухачів.

Витік пам'яті справді можливий і цілком передбачуваний. Не потрібно особливих мовних особливостей або кутових справ. Витоки пам’яті - це або показник того, що чогось можливо не вистачає, або навіть проблем із дизайном.


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

@Nicolas Bousquet: "Витік пам'яті дійсно можливий" Дякую. +15 виплат. Приємно. Тут я закричав, заявивши про цей факт, як довідки щодо мови Go: stackoverflow.com/questions/4400311 У цьому питанні все ще є негативні наслідки :(
SyntaxT3rr0r

GC в Java та .NET в деякому сенсі визначається на графіку припущень об'єктів, що містять посилання на інші об'єкти, такий самий, як графік об'єктів, які "дбають" про інші об'єкти. Насправді можливо, що у референтному графіку можуть існувати ребра, які не представляють "турботливих" відносин, і об'єкт може піклуватися про існування іншого об'єкта, навіть якщо немає прямого або непрямого опорного шляху (навіть використовуючиWeakReference ) від одного до іншого. Якщо у посилання на об’єкт був запасний біт, може бути корисним показник "піклується про ціль" ...
supercat

... і дозволити системі надавати сповіщення (за допомогою подібних засобів PhantomReference), якщо в обєкті не знайшлося жодного, хто про нього піклувався. WeakReferenceпідходить дещо близько, але перед тим, як його можна використовувати, його слід перетворити на чітке посилання; якщо цикл GC виникає, поки існує сильна посилання, ціль вважатиметься корисною.
supercat

Це, на мій погляд, правильна відповідь. Ми написали симуляцію років тому. Якось ми випадково пов’язали попередній стан із поточним станом, створивши витік пам'яті. Через термін ми ніколи не вирішували витік пам'яті, але зробили її «функцією», задокументувавши її.
nalply

163

Відповідь повністю залежить від того, що поцікавився інтерв'юер.

Чи можливо на практиці змусити витікати Java? Звичайно, є, і в інших відповідях є багато прикладів.

Але є кілька мета-запитань, які, можливо, вам задавали?

  • Чи теоретично "досконала" реалізація Java вразлива для витоків?
  • Чи розуміє кандидат різницю між теорією та реальністю?
  • Чи розуміє кандидат, як працює вивезення сміття?
  • Або як збирання сміття працює в ідеальному випадку?
  • Чи знають вони, що вони можуть називати інші мови через рідний інтерфейс?
  • Чи знають вони просочувати пам’ять іншими мовами?
  • Кандидат навіть знає, що таке управління пам'яттю і що відбувається за сценою на Java?

Я читаю ваше мета-питання як "Що таке відповідь, яку я міг би використати в цій ситуації інтерв'ю". Отже, я збираюся зосередитись на навичках інтерв'ю замість Java. Я вважаю, що ви більше схильні повторити ситуацію, коли не знаєте відповіді на запитання в інтерв'ю, ніж ви повинні опинитися там, де вам потрібно знати, як зробити течі на Java. Тож, сподіваємось, це допоможе.

Однією з найважливіших навичок, яку ви можете розвинути для співбесіди, є навчитися активно слухати питання та працювати з інтерв'юером, щоб отримати їхні наміри. Це не тільки дозволяє вам відповісти на їх запитання так, як вони хочуть, але також показує, що у вас є певні життєві навички спілкування. І коли зводиться до вибору між багатьма однаково талановитими розробниками, я наймаю того, хто слухає, думає та розуміє, перш ніж кожен раз відповідатиме.


22
Щоразу, коли я задавав це запитання, я шукаю досить просту відповідь - продовжуйте рости чергу, не остаточно закривайте db тощо, не непарні деталі завантажувача / потоку, це означає, що вони розуміють, що gc може і що не може зробити для вас. Я думаю, залежить від роботи, яку ви берете на співбесіду.
DaveC

Перегляньте моє запитання, дякую stackoverflow.com/questions/31108772/…
Daniel Newtown,

130

Далі - досить безглуздий приклад, якщо ви не розумієте JDBC . Або хоча б те, як JDBC очікує, що розробник закриється Connection, Statementта ResultSetвипадки перед тим, як відкинути їх або втратити посилання на них, замість того, щоб покладатися на реалізацію finalize.

void doWork()
{
   try
   {
       Connection conn = ConnectionFactory.getConnection();
       PreparedStatement stmt = conn.preparedStatement("some query"); // executes a valid query
       ResultSet rs = stmt.executeQuery();
       while(rs.hasNext())
       {
          ... process the result set
       }
   }
   catch(SQLException sqlEx)
   {
       log(sqlEx);
   }
}

Проблема з вищезазначеним полягає в тому, що Connectionоб'єкт не закритий, а отже, фізичне з'єднання залишатиметься відкритим, поки сміттєзбірник не з’явиться і не побачить, що він недоступний. GC буде викликати finalizeметод, але є драйвери JDBC, які не реалізують finalize, принаймні, не тим самим способом, який Connection.closeреалізований. Внаслідок цього поведінка полягає в тому, що, хоча пам'ять буде відновлено через недоступні об'єкти, що збираються, ресурси (включаючи пам'ять), пов'язані зConnection об'єктом, можуть просто не бути відновлені.

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

Навіть якби драйвер JDBC був реалізований finalize, винятки можуть бути викинуті під час доопрацювання. Внаслідок цього поведінка полягає в тому, що будь-яка пам'ять, пов’язана з об'єктом, що "перебуває в режимі спокою", не буде відшкодована, як finalizeгарантується, що вона буде викликана лише один раз.

Наведений вище сценарій зіткнення винятків під час доопрацювання об'єкта пов'язаний з іншим сценарієм, який, можливо, може призвести до витоку пам'яті - воскресіння об'єкта. Воскресіння об'єкта часто робиться навмисно, створюючи чітке посилання на об'єкт від остаточного завершення, з іншого об'єкта. При неправильному використанні відновлення об'єкта це призведе до витоку пам'яті в поєднанні з іншими джерелами витоку пам'яті.

Є ще багато прикладів, які можна спогадати - як

  • Керування Listекземпляром, коли ви лише додаєте до списку та не видаляєте з нього (хоча ви повинні позбуватися елементів, які вам більше не потрібні), або
  • Відкриваємо Sockets або Files, але не закриваємо їх, коли вони більше не потрібні (подібно до наведеного вище прикладу, що стосується Connectionкласу).
  • Не вивантажуючи Singletons при збиванні програми Java EE. Мабуть, Classloader, що завантажив клас singleton, збереже посилання на клас, і, отже, екземпляр singleton ніколи не буде зібраний. Коли новий екземпляр програми розгорнуто, зазвичай створюється новий завантажувач класів, а колишній завантажувач класів продовжить існувати завдяки синглтон.

98
Ви досягнете максимальної межі відкритого з'єднання, перш ніж зазвичай натисніть обмеження на пам'ять. Не запитуйте мене, чому я знаю ...
Hardwareguy

Драйвер Oracle JDBC відомий для цього.
чотки

@Hardwareguy Я сильно впливав на межі з'єднання баз даних SQL, поки не вклав Connection.closeостаточний блок усіх моїх викликів SQL. Для додаткової розваги я зателефонував до тривалих процедур, що зберігаються в Oracle, що вимагають блокування на стороні Java, щоб запобігти занадто багато дзвінків до бази даних.
Майкл Шопсін

@Hardwareguy Це цікаво, але насправді не потрібно, щоб фактичні межі з'єднання були досягнуті для всіх середовищ. Наприклад, для програми, розгорнутої на веб-сервері веб-додатків 11 г, я помітив витоки з’єднань у великих масштабах. Але завдяки можливості збирання витоків підключень до бази даних залишалися доступними під час впровадження витоків пам'яті. Я не впевнений у всіх середовищах.
Асеем Бансал

На моєму досвіді ви отримуєте витік пам'яті, навіть якщо ви закриваєте з'єднання. Вам потрібно спочатку закрити ResultSet і PreparedStatement. Був сервер, який неодноразово виходив з ладу через години або навіть дні нормальної роботи через OutOfMemoryErrors, поки я не почав це робити.
Bjørn Stenfeldt

119

Напевно, одним з найпростіших прикладів потенційного витоку пам'яті та як цього уникнути, є реалізація ArrayList.remove (int):

public E remove(int index) {
    RangeCheck(index);

    modCount++;
    E oldValue = (E) elementData[index];

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index,
                numMoved);
    elementData[--size] = null; // (!) Let gc do its work

    return oldValue;
}

Якби ви реалізували його самостійно, чи подумали б ви очистити елемент масиву, який більше не використовується ( elementData[--size] = null)? Ця довідка може зберегти величезний об’єкт живим ...


5
І де тут просочується пам'ять?
rds

28
@maniek: Я не мав на увазі, що цей код має витік пам'яті. Я цитував це, щоб показати, що іноді потрібен неочевидний код, щоб уникнути випадкового утримання об'єкта.
meriton

Що таке RangeCheck (індекс); ?
Корай Тугай

6
Джошуа Блох дав цей приклад в Ефективній Java, що демонструє просту реалізацію стеків. Дуже гарна відповідь.
орендує

Але це не було б реальною протіканням пам'яті, навіть якщо її забули. Елемент все ще буде БЕЗПЕЧНО отримати доступ до Reflection, він би просто не був очевидним та безпосередньо доступним через інтерфейс List, але об'єкт і посилання все ще є, і до них можна безпечно отримати доступ.
DGoiko

68

Щоразу, коли ви зберігаєте посилання на об'єкти, які вам більше не потрібні, у вас витік пам'яті. Див. Розділ Обробка витоків пам’яті в програмах Java для прикладів того, як витоки пам’яті проявляються в Java та що ви можете з цим зробити.


14
Я не вірю, що це "витік". Це помилка, і це за дизайном програми та мови. Витік буде предметом, який висить навколо, без посилань на нього.
користувач541686

29
@Mehrdad: Це лише одне вузьке визначення, яке не повністю стосується всіх мов. Я б заперечив, що будь-яка витік пам'яті - це помилка, викликана поганим дизайном програми.
Білл Ящірка

9
@Mehrdad: ...then the question of "how do you create a memory leak in X?" becomes meaningless, since it's possible in any language. Я не бачу, як ти робиш такий висновок. Існує менше способів створити витік пам'яті в Java за будь-яким визначенням. Це, безумовно, все-таки актуальне питання.
Білл Ящірка

7
@ 31eee384: Якщо ваша програма зберігає в пам'яті об'єкти, які вона ніколи не може використовувати, то технічно це просочена пам'ять. Те, що у вас є більші проблеми, насправді цього не змінює.
Білл Ящірка

8
@ 31eee384: Якщо ви знаєте за фактом, що цього не буде, то цього не може бути. Програма, як написано, ніколи не матиме доступу до даних.
Білл Ящірка

51

Ви можете витік пам'яті за допомогою класу sun.misc.Unsafe . Насправді цей клас обслуговування використовується в різних стандартних класах (наприклад, у класах java.nio ). Ви не можете створити екземпляр цього класу безпосередньо , але ви можете використовувати роздуми для цього .

Код не компілюється в IDE Eclipse - компілюйте його за допомогою команди javac(під час компіляції ви отримаєте попередження)

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import sun.misc.Unsafe;


public class TestUnsafe {

    public static void main(String[] args) throws Exception{
        Class unsafeClass = Class.forName("sun.misc.Unsafe");
        Field f = unsafeClass.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);
        System.out.print("4..3..2..1...");
        try
        {
            for(;;)
                unsafe.allocateMemory(1024*1024);
        } catch(Error e) {
            System.out.println("Boom :)");
            e.printStackTrace();
        }
    }

}

1
Виділена пам’ять невидима для збирачів сміття
ствол

4
Виділена пам'ять також не належить Java.
bestsss

Чи специфічно це jvm для сонця / oracle? Наприклад, це буде працювати на IBM?
Берлін Браун

2
Пам'ять, безумовно, "належить Яві", принаймні в тому сенсі, що i) її недоступна ні для кого іншого; ii) після закінчення програми Java вона буде повернута в систему. Це просто поза СВМ.
Майкл Андерсон

3
Це створить затемнення (принаймні, в останніх версіях), але вам потрібно змінити налаштування компілятора: у вікні> Налаштування> Java> Компілятор> Помилки / попередження> Невизначений та обмежений API встановлений Заборонене посилання (правила доступу) на "Попередження ".
Майкл Андерсон

43

Я можу скопіювати свою відповідь звідси: Найпростіший спосіб викликати витік пам'яті на Java?

"Витік пам'яті в інформатиці (або витік в цьому контексті) відбувається, коли комп'ютерна програма споживає пам'ять, але не в змозі повернути її назад в операційну систему." (Вікіпедія)

Проста відповідь: Ви не можете. Java здійснює автоматичне управління пам’яттю і звільнить ресурси, які вам не потрібні. Ви не можете запобігти цьому. ВИНАГО зможе випустити ресурси. У програмах з ручним управлінням пам'яттю це інакше. Ви не можете отримати деяку пам’ять на C за допомогою malloc (). Щоб звільнити пам'ять, вам потрібен вказівник, який повернувся malloc, і виклик на нього free (). Але якщо у вас більше немає вказівника (перезаписаний або перевищено термін служби), ви, на жаль, не зможете звільнити цю пам'ять, і, таким чином, у вас є витік пам'яті.

Всі інші відповіді поки що, на моє визначення, насправді не протікають пам'яті. Всі вони спрямовані на швидке наповнення пам’яті безглуздими речами. Але в будь-який час ви все-таки зможете знеструмити створені вами об'єкти і тим самим звільнити пам'ять -> NO LEAK. Відповідь аккорада дуже близька, хоча, як я маю визнати, оскільки його рішення ефективно просто «розбивати» сміттєзбірник, примушуючи його до нескінченного циклу).

Довга відповідь: Ви можете отримати витік пам'яті, написавши бібліотеку для Java за допомогою JNI, яка може керувати пам'яттю вручну і таким чином мати витоки пам'яті. Якщо ви зателефонуєте до цієї бібліотеки, у вашому процесі Java буде просочена пам'ять. Або у вас можуть бути помилки в JVM, так що JVM втрачає пам'ять. Напевно, у JVM є помилки, можуть бути навіть деякі відомі, оскільки збирання сміття не таке банальне, але тоді це все-таки помилка. За задумом це неможливо. Ви можете запитати код Java, який впливає на такий помилку. Вибачте, я цього не знаю, і це, можливо, більше не буде помилкою у наступній версії Java.


12
Це надзвичайно обмежене (і не дуже корисне) визначення витоків пам'яті. Єдине визначення, яке має сенс для практичних цілей, - "витік пам'яті - це будь-яка умова, в якій програма продовжує зберігати пам'ять, виділену після того, як дані, які вона зберігає, більше не потрібні".
Мейсон Уілер

1
Згадану відповідь аккорада видалено?
Томаш Зато - Відновіть Моніку

1
@ TomášZato: Ні, це не так. Я звернув посилання вище, щоб зараз посилання, так що ви можете легко знайти його.
янкі

Що таке воскресіння об'єкта? Скільки разів викликається деструктор? Як ці питання спростують цю відповідь?
аутист

1
Ну, переконайтеся, що ви можете створити витік пам'яті всередині Java, незважаючи на GC, і все-таки виконати наведене вище визначення. Просто маєте структуру даних, що додається, захищену від зовнішнього доступу, щоб жоден інший код не видаляв її - програма не може звільнити пам'ять, оскільки в ній немає коду.
toolforger

39

Ось простий / зловісний за допомогою http://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29 .

public class StringLeaker
{
    private final String muchSmallerString;

    public StringLeaker()
    {
        // Imagine the whole Declaration of Independence here
        String veryLongString = "We hold these truths to be self-evident...";

        // The substring here maintains a reference to the internal char[]
        // representation of the original string.
        this.muchSmallerString = veryLongString.substring(0, 1);
    }
}

Оскільки підрядка стосується внутрішнього подання оригіналу, набагато довшого рядка, оригінал залишається в пам'яті. Таким чином, доки у вас є StringLeaker у грі, ви також маєте всю оригінальну струну в пам'яті, навіть незважаючи на те, що ви можете подумати, що ви просто тримаєтесь за односимвольний рядок.

Спосіб уникнути небажаного посилання на початковий рядок - це зробити щось подібне:

...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...

Для додаткової шкідливості, ви також можете .intern()підрядок:

...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...

Це дозволить зберегти як початкову довгу рядок, так і похідну підрядку в пам'яті навіть після того, як екземпляр StringLeaker був відкинутий.


4
Я б не називав це течією пам'яті, як такою . Коли muchSmallerStringбуде звільнено (оскільки StringLeakerоб’єкт знищено), також буде звільнена довга струна. Те, що я називаю витоком пам'яті, - це пам'ять, яка ніколи не може бути звільнена в цьому випадку JVM. Тим НЕ менше, ви показали себе , як звільнити пам'ять: this.muchSmallerString=new String(this.muchSmallerString). Зі справжньою витоком пам’яті ви нічого не можете зробити.
rds

2
@rds, це справедливо. Неслучай internможе бути скоріше "сюрпризом пам'яті", ніж "витоком пам'яті". .intern()Однак підрядка, безумовно, створює ситуацію, коли посилання на довший рядок зберігається і не може бути звільнена.
Джон Чемберс

15
Метод substring () створює нову String в java7 (це нова поведінка)
anstarovoyt

Вам навіть не потрібно робити підрядку () самостійно: використовуйте підривник для регулярних вирівнювань, щоб відповідати крихітній частині величезного вводу, і тримати довгий час "витягнутий" рядок. Величезний вклад залишається живим до Яви 6.
Bananeweizen

37

Поширений приклад цього в коді графічного інтерфейсу - це при створенні віджета / компонента та доданні слухача до якогось об’єкта, на який поширюється статичний / додаток, а потім не видалення слухача при знищенні віджета. Ви не тільки отримуєте витік пам’яті, але і хіт виступу, коли коли б ви не слухали події пожеж, вас називають і всі ваші старі слухачі.


1
Платформа android надає приклад витоку пам'яті, створеного кешуванням Bitmap у статичному полі View .
rds

36

Візьміть будь-яку веб-програму, що працює в будь-якому контейнері сервлетів (Tomcat, Jetty, Glassfish, що б там не було ...). Повторно розгортайте додаток 10 чи 20 разів поспіль (може бути достатньо просто торкнутися ВІЙНИ в каталозі автовідтворення сервера.

Якщо хтось насправді цього не перевірив, великі шанси, що ви отримаєте OutOfMemoryError після пари перестановок, оскільки програма не подбала про очищення після себе. Ви можете навіть виявити помилку на своєму сервері за допомогою цього тесту.

Проблема полягає в тому, що термін служби контейнера довший, ніж термін служби вашої програми. Ви повинні переконатися, що всі посилання, які контейнер може мати на об'єкти або класи вашої програми, можуть бути зібрані сміття.

Якщо існує лише одна довідка, яка пережила нерозгортання вашого веб-додатка, відповідний завантажувач і, як наслідок, усі класи вашого веб-додатка не можуть бути зібрані сміттям.

Нитки, розпочаті вашою заявкою, змінні ThreadLocal, реєстрація додатків - деякі звичайні підозрювані, що викликають протікання завантажувача.


1
Це відбувається не через витік пам'яті, а через те, що завантажувач класів не вивантажує попередній набір класів. Для цього не рекомендується повторно розміщувати сервер додатків без перезавантаження сервера (не фізичної машини, а сервера додатків). Я бачив ту саму проблему з WebSphere.
Свен

35

Можливо, використовуючи зовнішній кодовий код через JNI?

З чистою Java це майже неможливо.

Але мова йде про "стандартний" тип витоку пам'яті, коли ви більше не можете отримати доступ до пам'яті, але вона все ще належить додатку. Натомість ви можете зберігати посилання на невикористані об'єкти або відкриті потоки, не закриваючи їх згодом.


22
Це залежить від визначення поняття "витік пам'яті". Якщо "пам'ять, на яку зберігається, але більше не потрібна", це зробити в Java просто. Якщо це "пам'ять, яка виділяється, але зовсім не доступна кодом", то стає трохи складніше.
Йоахім Зауер

@Joachim Sauer - я мав на увазі другий тип. Перший досить легко зробити :)
Рогач

6
"З чистою явою це майже неможливо". Ну, мій досвід - ще один, особливо коли мова йде про використання кеш-пам'яті людьми, які не знають про підводні камені.
Фабіан Барні

4
@Rogach: в основному є 400 заявок на різні відповіді людей, які мають +10 000 представників, що показує, що в обох випадках Йоахім Зауер прокоментував це дуже можливо. Тож ваше "майже неможливо" не має сенсу.
SyntaxT3rr0r

32

Я колись мав приємний "витік пам'яті" стосовно PermGen та XML-розбору. Аналізатор XML, який ми використовували (я не пам'ятаю, який це був), зробив String.intern () для імен тегів, щоб зробити порівняння швидшим. Один з наших клієнтів мав ідею зберігати значення даних не в атрибутах або тексті XML, а як імена тегів, тому у нас був такий документ, як:

<data>
   <1>bla</1>
   <2>foo</>
   ...
</data>

Насправді вони використовували не цифри, а довші текстові ідентифікатори (близько 20 символів), які були унікальними та надходили зі швидкістю 10-15 мільйонів на день. Це складає 200 Мб сміття на день, який більше ніколи не потрібен, і ніколи не стає GCed (оскільки це в PermGen). Пермген у нас був 512 Мб, тому для виходу поза пам'яті (OOME) пішло близько двох днів ...


4
Тільки для того, щоб вибрати свій приклад код: я думаю, що числа (або рядки, що починаються з чисел) не дозволяються як імена елементів у XML
Paŭlo Ebermann

Зауважте, що це більше не стосується JDK 7+, де String interning відбувається в купі. Дивіться цю статтю для детальної реєстрації: java-performance.info/string-intern-in-java-6-7-8
jmiserez

Отже, я думаю, що використання StringBuffer замість String вирішить цю проблему? звичайно?
ануби

24

Що протікає в пам'яті:

  • Це викликано помилкою або поганим дизайном.
  • Це марно пам'ять.
  • З часом стає гірше.
  • Збирач сміття не може його прибрати.

Типовий приклад:

Кеш об'єктів - хороша відправна точка, щоб зіпсувати речі.

private static final Map<String, Info> myCache = new HashMap<>();

public void getInfo(String key)
{
    // uses cache
    Info info = myCache.get(key);
    if (info != null) return info;

    // if it's not in cache, then fetch it from the database
    info = Database.fetch(key);
    if (info == null) return null;

    // and store it in the cache
    myCache.put(key, info);
    return info;
}

Ваш кеш росте і росте. І досить скоро вся база даних засмоктується в пам'ять. Для кращого дизайну використовується LRUMap (зберігає лише нещодавно використані об'єкти в кеші).

Звичайно, ви можете зробити речі набагато складнішими:

  • використовуючи ThreadLocal конструкції .
  • додавання більш складних дерев-орієнтирів .
  • або витоки, викликані сторонніми бібліотеками .

Що часто трапляється:

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


22

Я вважав, що цікаво, що ніхто не використовував внутрішні приклади класу. Якщо у вас внутрішній клас; вона по суті підтримує посилання на клас, що містить. Звичайно, це технічно не є витоком пам'яті, оскільки Java з часом її очистять; але це може спричинити зависання занять довше, ніж передбачалося.

public class Example1 {
  public Example2 getNewExample2() {
    return this.new Example2();
  }
  public class Example2 {
    public Example2() {}
  }
}

Тепер, якщо ви зателефонуєте в Example1 і отримаєте Example2, відкинувши Example1, вам по суті все ще буде посилання на об'єкт Example1.

public class Referencer {
  public static Example2 GetAnExample2() {
    Example1 ex = new Example1();
    return ex.getNewExample2();
  }

  public static void main(String[] args) {
    Example2 ex = Referencer.GetAnExample2();
    // As long as ex is reachable; Example1 will always remain in memory.
  }
}

Я також чув слух про те, що якщо у вас є змінна, яка існує довше певної кількості часу; Java припускає, що він завжди буде існувати і насправді ніколи не намагатиметься його очистити, якщо його більше не можна отримати в коді. Але це абсолютно не підтверджено.


2
внутрішні класи рідко є проблемою. Вони є простою справою і їх дуже легко виявити. Чутка теж просто чутка.
bestsss

2
"Чутка" звучить як хтось напівчитаний про те, як працює генерація GC. Довгоживучі, але тепер недоступні об’єкти дійсно можуть затриматися і зайняти простір на деякий час, тому що JVM просував їх у молодих поколінь, щоб він міг перестати перевіряти їх кожен прохід. Вони ухиляться від примхливих "прочистеньких моїх 5000 тимчасових рядків", за задумом. Але вони не безсмертні. Вони все ще мають право на збір, і якщо VM прив’язаний до оперативної пам’яті, він врешті запустить повний обчислювач GC і поверне цю пам'ять.
cHao

22

Нещодавно я стикався з ситуацією з витоком пам’яті, викликаною способом log4j.

Log4j має цей механізм під назвою Nested Diagnostic Context (NDC), який є інструментом для розрізнення перемежованих результатів журналу від різних джерел. Деталізація, при якій працює NDC, є потоками, тому вона відрізняє виходи журналу від різних потоків окремо.

Для зберігання конкретних тегів для потоку, клас NDC log4j використовує Hashtable, який вводиться самим об'єктом Thread (на відміну від того, щоб сказати ідентифікатор потоку), і таким чином, поки тег NDC не залишиться в пам'яті всіх об'єктів, що звисають з потоку Об'єкт також залишається в пам'яті. У нашому веб-додатку ми використовуємо NDC для тегування вихідних даних з ідентифікатором запиту, щоб окремо відрізняти журнали від одного запиту. Контейнер, який асоціює тег NDC з потоком, також видаляє його, повертаючи відповідь із запиту. Проблема виникла, коли під час обробки запиту з'явився дочірній потік, такий як наступний код:

pubclic class RequestProcessor {
    private static final Logger logger = Logger.getLogger(RequestProcessor.class);
    public void doSomething()  {
        ....
        final List<String> hugeList = new ArrayList<String>(10000);
        new Thread() {
           public void run() {
               logger.info("Child thread spawned")
               for(String s:hugeList) {
                   ....
               }
           }
        }.start();
    }
}    

Отже, контекст NDC асоціювався з вбудованою ниткою вбудованої лінії. Об'єктом потоку, який був ключовим для цього контексту NDC, є вбудована нитка, в якій висить величезний об'єктList. Отже, навіть після того, як нитка закінчила робити те, що вона робила, посилання на величезний список було збережено живим контекстом NDC Hastable, тим самим спричиняючи витік пам'яті.


Це смокче. Ви повинні перевірити цю бібліотеку журналів, яка виділяє пам'ять ZERO під час реєстрації у файлі: mentalog.soliveirajr.com
TraderJoeChicago

+1 Чи знаєте ви напевно, чи є аналогічна проблема з MDC в slf4j / logback (продукти-наступники того ж автора)? Я збираюся зробити глибоке занурення у джерело, але хотів перевірити спочатку. У будь-якому випадку, дякую за повідомлення про це.
sparc_spread

20

Інтерв'юер, мабуть, шукав циркулярну довідку, як код нижче (який, до речі, просочував пам'ять лише у старих JVM, які використовували підрахунок посилань, що вже не так). Але це досить розпливчасте питання, тому це чудова можливість показати своє розуміння управління пам'яттю JVM.

class A {
    B bRef;
}

class B {
    A aRef;
}

public class Main {
    public static void main(String args[]) {
        A myA = new A();
        B myB = new B();
        myA.bRef = myB;
        myB.aRef = myA;
        myA=null;
        myB=null;
        /* at this point, there is no access to the myA and myB objects, */
        /* even though both objects still have active references. */
    } /* main */
}

Тоді ви можете пояснити, що при підрахунку посилань вищевказаний код просочиться в пам'яті. Але більшість сучасних JVM більше не використовують підрахунок посилань, більшість використовують сміттєзбірник, який фактично збирає цю пам’ять.

Далі ви можете пояснити створення об’єкта, який має основний природний ресурс, наприклад:

public class Main {
    public static void main(String args[]) {
        Socket s = new Socket(InetAddress.getByName("google.com"),80);
        s=null;
        /* at this point, because you didn't close the socket properly, */
        /* you have a leak of a native descriptor, which uses memory. */
    }
}

Тоді ви можете пояснити, що це технічно витік пам'яті, але насправді витік спричинений нативним кодом у JVM, що виділяє основні природні ресурси, які не були звільнені вашим кодом Java.

Зрештою, у сучасному JVM вам потрібно написати якийсь код Java, який виділяє нативний ресурс поза межами нормальної сфери обізнаності JVM.


19

Усі завжди забувають маршрут кодового коду. Ось проста формула витоку:

  1. Оголосити власний метод.
  2. У рідному методі дзвоніть malloc. Не дзвоніть free.
  3. Викличте рідний метод.

Пам'ятайте, що розподіл пам'яті в нативного коду відбувається з купи JVM.


1
На реальних подіях.
Рег.

18

Створіть статичну карту і продовжуйте додавати жорсткі посилання на неї. Це ніколи не будуть GC'd.

public class Leaker {
    private static final Map<String, Object> CACHE = new HashMap<String, Object>();

    // Keep adding until failure.
    public static void addToCache(String key, Object value) { Leaker.CACHE.put(key, value); }
}

87
Як це витік? Це робить саме те, що ти просиш. Якщо це витік, створення та зберігання об'єктів де завгодно - це витік.
Фальмарі

3
Я згоден з @Falmarri. Я не бачу там витоку, ви просто створюєте об'єкти. Ви, звичайно, можете "повернути" пам'ять, яку ви тільки що виділили, за допомогою іншого методу, який називається "deleteFromCache". Витік - це коли ви не можете повернути пам'ять.
Кайл

3
Моя думка полягає в тому, що хтось, хто продовжує створювати об'єкти, можливо, вкладаючи їх у кеш, може виявитись помилкою OOM, якщо вони не будуть обережні.
duffymo

8
@duffymo: Але це насправді не те, про що ставили питання. Це не має нічого спільного з простою використанням всієї вашої пам'яті.
Фальмарі

3
Абсолютно недійсний. Ви просто збираєте купу об’єктів у колекції карт. Їх посилання зберігатимуться, тому що Карта зберігає їх.
gyorgyabraham

16

Ви можете створити рухливу витік пам'яті, створивши новий екземпляр класу в методі остаточного завершення класу. Бонусні бали, якщо фіналізатор створює кілька примірників. Ось проста програма, яка просочує всю купу за кілька секунд до декількох хвилин, залежно від розміру вашої купи:

class Leakee {
    public void check() {
        if (depth > 2) {
            Leaker.done();
        }
    }
    private int depth;
    public Leakee(int d) {
        depth = d;
    }
    protected void finalize() {
        new Leakee(depth + 1).check();
        new Leakee(depth + 1).check();
    }
}

public class Leaker {
    private static boolean makeMore = true;
    public static void done() {
        makeMore = false;
    }
    public static void main(String[] args) throws InterruptedException {
        // make a bunch of them until the garbage collector gets active
        while (makeMore) {
            new Leakee(0).check();
        }
        // sit back and watch the finalizers chew through memory
        while (true) {
            Thread.sleep(1000);
            System.out.println("memory=" +
                    Runtime.getRuntime().freeMemory() + " / " +
                    Runtime.getRuntime().totalMemory());
        }
    }
}

15

Я не думаю, що це ще ніхто не сказав: ви можете воскресити об'єкт, змінивши метод finalize () таким, що finalize () десь зберігає посилання на це. Сміттєзбірник буде викликаний лише один раз на об’єкт, після чого об’єкт ніколи не буде знищений.


10
Це неправда. finalize()не буде викликано, але об'єкт буде зібрано, як тільки більше посилань не буде. Також сміттєзбірник не називається.
bestsss

1
Ця відповідь вводить в оману, finalize()метод може бути викликаний лише один раз JVM, але це не означає, що він не може бути повторно зібраний сміттям, якщо об’єкт воскреснути, а потім знову знешкодити. Якщо в finalize()методі є код закриття ресурсу, цей код не запуститься знову, це може спричинити витік пам'яті.
Том Камман

15

Нещодавно я натрапив на більш тонкий вид витоку ресурсів. Ми відкриваємо ресурси через getResourceAsStream завантажувача класу, і траплялося, що ручки вхідного потоку не були закриті.

Гм, можна сказати, який ідіот.

Ну, і що це робить цікавим: це таким чином, ви можете просочувати купу пам'яті основного процесу, а не з купи JVM.

Все, що вам потрібно - це jar-файл з файлом, всередині якого буде посилання з коду Java. Чим більше jar файл, тим швидше виділяється пам'ять.

Ви можете легко створити таку банку з наступним класом:

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class BigJarCreator {
    public static void main(String[] args) throws IOException {
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
        zos.putNextEntry(new ZipEntry("resource.txt"));
        zos.write("not too much in here".getBytes());
        zos.closeEntry();
        zos.putNextEntry(new ZipEntry("largeFile.out"));
        for (int i=0 ; i<10000000 ; i++) {
            zos.write((int) (Math.round(Math.random()*100)+20));
        }
        zos.closeEntry();
        zos.close();
    }
}

Просто вставте у файл з назвою BigJarCreator.java, компілюйте та запустіть його з командного рядка:

javac BigJarCreator.java
java -cp . BigJarCreator

Et voilà: ви знайдете архів jar у вашому поточному робочому каталозі з двома файлами всередині.

Створимо другий клас:

public class MemLeak {
    public static void main(String[] args) throws InterruptedException {
        int ITERATIONS=100000;
        for (int i=0 ; i<ITERATIONS ; i++) {
            MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
        }
        System.out.println("finished creation of streams, now waiting to be killed");

        Thread.sleep(Long.MAX_VALUE);
    }

}

Цей клас в основному нічого не робить, але створює нерозподілені об'єкти InputStream. Ці об’єкти будуть негайно збирати сміття, і, таким чином, не сприятимуть розміру купи. Для нашого прикладу важливо завантажити наявний ресурс з файлу jar, і розмір тут має значення!

Якщо ви сумніваєтеся, спробуйте скласти і розпочати клас вище, але переконайтесь, що обрали пристойний розмір купи (2 Мб):

javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak

Тут ви не зіткнетеся з помилкою OOM, оскільки ніяких посилань не зберігається, додаток продовжуватиме працювати незалежно від того, наскільки великими ви обрали ІТЕРАЦІЇ у наведеному вище прикладі. Споживання пам'яті вашого процесу (видно вгорі (RES / RSS) або провідник процесів) зростає, якщо програма не перейде до команди очікування. У налаштуваннях вище, вона виділить близько 150 Мб пам'яті.

Якщо ви хочете, щоб програма грала в безпеку, закрийте потік вводу там, де він створений:

MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();

і ваш процес не перевищить 35 Мб, незалежно від кількості ітерацій.

Досить просто і дивно.


14

Як вважає багато людей, витік ресурсів досить легко викликати - як приклади JDBC. Фактичні витоки пам’яті трохи складніше - особливо якщо ви не покладаєтесь на зламані біти JVM, щоб зробити це за вас ...

Ідеї ​​створення об'єктів, які мають дуже великий слід і не мають можливості отримати доступ до них, теж не є реальними витоками пам'яті. Якщо нічого не може отримати до нього, то це буде зібране сміття, а якщо щось може отримати до нього, то це не витік ...

Один із способів, який раніше працював - і я не знаю, чи все ще є - це мати три глибокого кругового ланцюга. Оскільки в Об'єкті A є посилання на Об'єкт B, Об'єкт B має посилання на Об'єкт C, а Об'єкт C має посилання на Об'єкт А. В GC був досить розумним, щоб знати, що два глибоких ланцюга - як у A <--> B - можна сміливо збирати, якщо A і B не доступні нічим іншим, але не могли б обробити тристоронній ланцюг ...


7
Це вже не так вже деякий час. Сучасні GC знають, як обробляти кругові посилання.
assylias

13

Інший спосіб створення потенційно величезні витоку пам'яті, щоб зберігати посилання на Map.Entry<K,V>про TreeMap.

Важко оцінити, чому це стосується лише TreeMaps, але, дивлячись на реалізацію, причина може полягати в тому, що: TreeMap.Entryзберігає посилання на своїх побратимів, тому якщо a TreeMapготовий для збору, але якийсь інший клас має посилання на будь-який із його Map.Entry, тоді вся Карта збережеться в пам’яті.


Реальний сценарій:

Уявіть, що у вас є db-запит, який повертає велику TreeMapструктуру даних. Люди зазвичай використовують TreeMaps, оскільки порядок введення елемента зберігається.

public static Map<String, Integer> pseudoQueryDatabase();

Якщо запит називали багато разів, і для кожного запиту (так, для кожного Mapповернутого) ви Entryдесь зберігаєте , пам’ять постійно зростатиме.

Розглянемо наступний клас обгортки:

class EntryHolder {
    Map.Entry<String, Integer> entry;

    EntryHolder(Map.Entry<String, Integer> entry) {
        this.entry = entry;
    }
}

Застосування:

public class LeakTest {

    private final List<EntryHolder> holdersCache = new ArrayList<>();
    private static final int MAP_SIZE = 100_000;

    public void run() {
        // create 500 entries each holding a reference to an Entry of a TreeMap
        IntStream.range(0, 500).forEach(value -> {
            // create map
            final Map<String, Integer> map = pseudoQueryDatabase();

            final int index = new Random().nextInt(MAP_SIZE);

            // get random entry from map
            for (Map.Entry<String, Integer> entry : map.entrySet()) {
                if (entry.getValue().equals(index)) {
                    holdersCache.add(new EntryHolder(entry));
                    break;
                }
            }
            // to observe behavior in visualvm
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

    }

    public static Map<String, Integer> pseudoQueryDatabase() {
        final Map<String, Integer> map = new TreeMap<>();
        IntStream.range(0, MAP_SIZE).forEach(i -> map.put(String.valueOf(i), i));
        return map;
    }

    public static void main(String[] args) throws Exception {
        new LeakTest().run();
    }
}

Після кожного pseudoQueryDatabase()дзвінка mapекземпляри повинні бути готові до збору, але це не відбудеться, оскільки принаймні один Entryзберігається десь в іншому місці.

Залежно від ваших jvmналаштувань, програма може вийти з ладу на ранній стадії через a OutOfMemoryError.

З цього visualvmграфіка видно, як пам'ять постійно зростає.

Дамп пам'яті - TreeMap

Те ж не відбувається з хешованою структурою даних ( HashMap).

Це графік при використанні а HashMap.

Дамп пам'яті - HashMap

Рішення? Просто збережіть ключ / значення (як ви, мабуть, вже робите), а не зберігайте Map.Entry.


Я написав більш великий тест тут .


11

Нитки не збираються, поки вони не припиняться. Вони служать корінням збору сміття. Вони є одним з небагатьох об'єктів, які не будуть повернені, просто забувши про них або очистивши посилання на них.

Поміркуйте: основна схема припинення робочої нитки полягає у встановленні певної змінної умови, яку бачить потік. Потік може періодично перевіряти змінну і використовувати її як сигнал для завершення. Якщо змінна не оголошена volatile, то зміна змінної може не бачитись потоком, тому вона не знатиме завершуватися. Або уявіть, якщо деякі потоки хочуть оновити спільний об’єкт, але тупик, намагаючись заблокувати його.

Якщо у вас є лише кілька потоків, ці помилки, ймовірно, будуть очевидні, оскільки ваша програма перестане працювати належним чином. Якщо у вас є пул потоків, який створює більше потоків за потребою, застарілі / застряглі нитки можуть не помітитись і накопичуватись нескінченно довго, викликаючи витік пам'яті. Нитки, ймовірно, використовуватимуть інші дані у вашій програмі, тому також не дозволять збирати будь-що, на що вони безпосередньо посилаються.

Як приклад іграшки:

static void leakMe(final Object object) {
    new Thread() {
        public void run() {
            Object o = object;
            for (;;) {
                try {
                    sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {}
            }
        }
    }.start();
}

Зателефонуйте System.gc()всім, що вам подобається, але об’єкт, переданий leakMeкомусь, ніколи не загине.

(* відредаговано *)


1
@Spidey Ніщо не "застрягло". Метод виклику повертається негайно, і переданий об'єкт ніколи не буде повернений. Це саме витік.
Боан

1
У вас буде нитка "працює" (або спить, як би там не було) протягом життя вашої програми. Це не вважається для мене витоком. Крім того, басейн не вважається витоком, навіть якщо ви не використовуєте його повністю.
Spidey

1
@Spidey "У вас буде [річ] протягом життя вашої програми. Це не вважається для мене витоком". Ви чуєте себе?
Боан

3
@Spidey Якщо ви вважаєте, що пам'ять, про яку процес знає, не протікає, то всі відповіді тут неправильні, оскільки процес завжди відслідковує, які сторінки у віртуальному адресному просторі відображаються на карті. Коли процес закінчується, ОС очищає всі витоки, ставлячи сторінки назад у вільний стек сторінки. Щоб перейти до наступної крайності, можна було перемогти будь-який аргументований витік, вказавши, що жоден фізичний біт в мікросхемах оперативної пам’яті або в місцях заміни на диску фізично не замінений або знищений, так що ви можете вимкнути комп'ютер і знову, щоб очистити будь-які витоки.
Боан

1
Практичне визначення витоку полягає в тому, що пам'ять втрачена таким чином, що ми не знаємо, і, отже, не можемо виконати процедуру, необхідну для відновлення лише цього; нам доведеться зруйнувати та відновити весь простір пам’яті. Розумна нитка, як це, може виникати природним шляхом через тупикову ситуацію або реалізацію химерної нитки. Об'єкти, на які посилаються такі потоки, навіть опосередковано, тепер не дозволяють збиратись, тому у нас є пам'ять, яка не буде природним чином відшкодована або використана повторно протягом життя програми. Я б назвав це проблемою; зокрема це витік пам'яті.
Боан

10

Я думаю, що вагомим прикладом може бути використання змінних ThreadLocal в середовищі, де потоки об'єднані.

Наприклад, використання змінних ThreadLocal в сервлетах для спілкування з іншими веб-компонентами, що створюють потоки, створені контейнером, і підтримують простої в пулі. Змінні ThreadLocal, якщо їх неправильно очистити, будуть існувати до тих пір, поки, можливо, той же веб-компонент не замінить свої значення.

Звичайно, виявивши проблему, можна легко вирішити.


10

Інтерв'юер, можливо, шукає кругового довідкового рішення:

    public static void main(String[] args) {
        while (true) {
            Element first = new Element();
            first.next = new Element();
            first.next.next = first;
        }
    }

Це класична проблема щодо підрахунку сміттєзбірників. Потім ви ввічливо поясніть, що JVM використовують набагато більш складний алгоритм, який не має цього обмеження.

-Вес Тарле


12
Це класична проблема щодо підрахунку сміттєзбірників. Навіть 15 років тому Java не використовувала підрахунок посилань. Реф. підрахунок також повільніше, ніж GC.
bestsss

4
Не витік пам'яті. Просто нескінченна петля.
Есбен Сков Педерсен

2
@Esben При кожній ітерації попередній firstне корисний і його слід збирати сміття. Під час підрахунку сміттєзбірників об’єкт не буде звільнений, оскільки на нього є активна довідка (сама по собі). Нескінченний цикл є тут, щоб знищити витік: коли ви запускаєте програму, пам'ять підніметься нескінченно.
rds

@rds @ Wesley Tarle припустимо, що цикл не був нескінченним. Чи все-таки буде витік пам'яті?
nz_21
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.