Відстеження проблеми витоку пам'яті / збору сміття в Java


79

Це проблема, яку я намагаюся відстежити вже пару місяців. У мене запущена програма Java, яка обробляє xml-канали та зберігає результат у базі даних. Були періодичні проблеми з ресурсами, які дуже важко відстежити.

Довідкова інформація: У виробничій коробці (де проблема найбільш помітна) я не маю особливо хорошого доступу до коробки та не можу запустити Jprofiler. Цей ящик - це 64-бітна чотириядерна машина на 8 Гб, на якій працюють centos 5.2, tomcat6 та java 1.6.0.11. Починається з цих java-опцій

JAVA_OPTS="-server -Xmx5g -Xms4g -Xss256k -XX:MaxPermSize=256m -XX:+PrintGCDetails -
XX:+PrintGCTimeStamps -XX:+UseConcMarkSweepGC -XX:+PrintTenuringDistribution -XX:+UseParNewGC"

Стек технологій такий:

  • Centos 64-розрядний 5.2
  • Java 6u11
  • Tomcat 6
  • Весна / WebMVC 2.5
  • Зимовий сон 3
  • Кварц 1.6.1
  • DBCP 1.2.1
  • Mysql 5.0.45
  • Ehcache 1.5.0
  • (і звичайно безліч інших залежностей, зокрема бібліотеки jakarta-commons)

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

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

Проблема: Схоже, JVM повністю ігнорує налаштування використання пам’яті, заповнює всю пам’ять і не реагує. Це проблема для замовника, котрий стикається з кінцем, який очікує регулярного опитування (5-хвилинна основа та 1-хвилинна повторна спроба), а також для наших оперативних команд, які постійно отримують повідомлення про те, що коробка перестала реагувати, і її потрібно перезапустити. На цій коробці немає нічого іншого значного.

Проблема, схоже, у вивозі сміття. Ми використовуємо колектор ConcurrentMarkSweep (як зазначено вище), оскільки оригінальний колектор STW викликав таймаути JDBC і ставав дедалі повільнішим. Журнали показують, що в міру збільшення використання пам'яті, тобто починає видавати помилки cms, і повертається до початкового колектору зупинки світу, який потім, здається, неправильно збирається.

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

Що я пробував:

  • Профілювання та фіксація гарячих точок.
  • Використання збирачів сміття STW, Parallel та CMS.
  • Запуск з мінімальним / максимальним розміром купи з кроком 1 / 2,2 / 4,4 / 5,6 / 6.
  • Запуск із простіром пермгену з кроком 256 М до 1 Гб.
  • Безліч поєднань вищесказаного.
  • Я також проконсультувався з JVM [посиланням на налаштування] (http://java.sun.com/javase/technologies/hotspot/gc/gc_tuning_6.html), але насправді не можу знайти нічого, що пояснює цю поведінку, або будь-які приклади налаштування _which_ параметри для використання в подібній ситуації.
  • Я також (безуспішно) пробував jprofiler в автономному режимі, з'єднуючись з jconsole, visualvm, але, здається, не можу знайти нічого, що може взаємодіяти з моїми даними журналу gc.

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

Хто-небудь може дати будь-яку пораду щодо:
а) Чому JVM використовує 8 фізичних концертів та 2 Гб місця для обміну, коли він налаштований на максимальне обмеження менше ніж 6.
b) Посилання на налаштування GC, яке насправді пояснює або дає обґрунтовані приклади про те, коли і з яким параметром використовувати розширені колекції.
в) Посилання на найпоширеніші витоки пам'яті Java (я розумію незатребувані посилання, але я маю на увазі на рівні бібліотеки / фреймворку або щось інше в inherenet в структурах даних, як хеш-карти).

Дякуємо за будь-яку інформацію, яку ви можете надати.

EDIT
Emil H:
1) Так, мій кластер розробки є дзеркалом виробничих даних, аж до медіасервера. Основною відмінністю є 32/64 біт та обсяг доступної оперативної пам'яті, яку я не можу дуже легко відтворити, але код, запити та налаштування однакові.

2) Існує деякий застарілий код, який спирається на JaxB, але при переупорядкуванні завдань, щоб уникнути конфліктів планування, я вважаю, що виконання, як правило, усувається, оскільки воно виконується один раз на день. Первинний аналізатор використовує запити XPath, які викликають пакет java.xml.xpath. Це було джерелом кількох гарячих точок, для одного запити не були попередньо скомпільовані, а два посилання на них були в жорстко закодованих рядках. Я створив безпечний кеш-пам'ять (hashmap) і врахував посилання на запити xpath як остаточні статичні рядки, що значно знизило споживання ресурсів. Запит все ще є значною частиною обробки, але це повинно бути тому, що це основна відповідальність програми.

3) Додаткова примітка, інший основний споживач - це операції із зображеннями від JAI (повторна обробка зображень із стрічки). Мені незнайомі графічні бібліотеки Java, але з того, що я виявив, вони не особливо діряві.

(дякую за відповіді поки що, люди!)

ОНОВЛЕННЯ:
Я зміг підключитися до виробничого екземпляра за допомогою VisualVM, але він відключив опцію візуалізації GC / run-GC (хоча я міг переглядати її локально). Цікава річ: розподіл купи ВМ підпорядковується JAVA_OPTS, а фактично виділена купа зручно сидить на рівні 1-1,5 концертів, і, здається, не витікає, але моніторинг рівня ящиків все ще показує схему витоку, але це не відображається у моніторингу ВМ. На цій коробці більше нічого не працює, тому я тупився.


Чи використовуєте ви дані реального світу та базу даних реального світу для тестування? Переважно копія виробничих даних?
Emil H

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

Крім того, який XML-парсер ви використовуєте?
Emil H

Чи ви розглядали кількість виділених байт-буферів і хто їх розподіляє?
Шон МакКоліфф,

Перевірте цю відповідь: stackoverflow.com/a/35610063 , там є деталі про витоки рідної пам'яті Java.
Lari Hotari,

Відповіді:


92

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

Я намагався jmap, поки процес діяв, але це зазвичай спричиняло зависання jvm, і мені довелося б запускати його за допомогою --force. Це призвело до дампів купи, в яких, здавалося, бракувало багато даних або, принаймні, відсутні посилання між ними. Для аналізу я спробував jhat, який представляє багато даних, але не так багато, як їх інтерпретувати. По-друге, я спробував інструмент аналізу пам'яті на основі затемнення ( http://www.eclipse.org/mat/ ), який показав, що купа в основному є класами, пов'язаними з tomcat.

Проблема полягала в тому, що jmap не повідомляв про фактичний стан програми, а лише вловлював класи після завершення роботи, які в основному були класами tomcat.

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

Використовуючи це, я проаналізував журнали повільних запитів та кілька непов'язаних проблем із продуктивністю. Я спробував надто ліниве завантаження ( http://docs.jboss.org/hibernate/core/3.3/reference/en/html/performance.html ), а також замінив кілька операцій сплячого режиму прямими запитами jdbc (здебільшого там, де це мав справу із завантаженням та роботою з великими колекціями - заміни jdbc просто працювали безпосередньо на таблицях об'єднання) і замінили деякі інші неефективні запити, які реєстрував mysql.

Ці кроки покращили ефективність роботи інтерфейсу, але все ще не вирішували проблему витоку, програма все ще була нестабільною і діяла непередбачувано.

Нарешті, я знайшов варіант: -XX: + HeapDumpOnOutOfMemoryError. Нарешті це створило дуже великий (~ 6,5 ГБ) файл hprof, який точно показував стан програми. Як не дивно, файл був настільки великий, що jhat не міг проаналізувати його, навіть на коробці з 16 Гб оперативної пам'яті. На щастя, MAT зміг створити кілька приємних графіків і показав кращі дані.

Цього разу стирчав один кварцовий потік, який зайняв 4,5 ГБ з 6 ГБ купи, і більшість із них - сплячий режим StatefulPersistenceContext ( https://www.hibernate.org/hib_docs/v3/api/org/hibernate /engine/StatefulPersistenceContext.html ). Цей клас використовується hibernate внутрішньо як його основний кеш (я вимкнув кеші другого рівня та кеші запитів, підтримувані EHCache).

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

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

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

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

Рішення: створіть метод dao, який явно викликає session.flush () і session.clear (), і викликайте цей метод на початку кожного завдання.

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

Дякуємо за допомогу всім у цьому, було досить хитро виправити помилку, оскільки все робило саме те, що передбачалося, але врешті-решт методом із 3 рядків вдалося виправити всі проблеми.


13
Хороший підсумок процесу налагодження та подяка за подальші дії та опублікування дозволу.
Борис Терзіч

1
Дякую за приємне пояснення. У мене була подібна проблема в сценарії пакетного читання (SELECT), що призвело до того, що StatefulPersistenceContext стає настільки великим. Я не міг запустити em.clear () або em.flush (), як це робив мій основний метод циклу @Transactional(propagation = Propagation.NOT_SUPPORTED). Це було вирішено шляхом зміни розповсюдження на Propagation.REQUIREDта викликом em.flush / em.clear ().
Мохсен,

3
Я не розумію одного: якщо сеанс ніколи не змивався, це означає, що фактичні дані не зберігалися в БД. Хіба ці дані не отримані десь у вашому додатку, щоб ви могли побачити, що їх немає?
yair

1
Надане посилання для StatefulPersistenceContext порушено. Це docs.jboss.org/hibernate/orm/4.3/javadocs/org/hibernate/engine/… зараз?
Віктор Стафуса,

1
Ліаме, дякую тоні. Я вважаю, що у мене така сама проблема, і MAT вказує на сплячий режим statefulPersistentContext. Думаю, прочитавши вашу статтю, я отримав достатньо підказок. Дякую за таку чудову інформацію.
Reddymails

4

Чи можете ви запустити виробничу скриньку з включеним JMX?

-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=<port>
...

Моніторинг та управління за допомогою JMX

А потім приєднати за допомогою JConsole , VisualVM ?

Чи добре робити дамп купи з jmap ?

Якщо так, ви можете проаналізувати дамп купи на витоки за допомогою JProfiler (у вас вже є), jhat , VisualVM, Eclipse MAT . Також порівняйте дампи купи, які можуть допомогти знайти витоки / шаблони.

І як ви згадали про jakarta-commons. Існує проблема під час використання журналу jakarta-commons-log, пов’язаного з утриманням на завантажувачі класів. Щоб добре прочитати цю перевірку

День у житті мисливця за витоками пам’яті ( release(Classloader))


1) Я справді спробував visualvm та кілька інших інструментів сьогодні, але мені потрібно правильно відкрити порти. 2) Я бачив проблему c-журналювання на своєму останньому робочому місці, і ця проблема мені про це нагадала. Служба на рівні компанії регулярно припиняла роботу, і її відстежували до відомого витоку в спільному доступі, я вважаю, це було щось подібне до того, що ви пов’язували. Я намагався зберегти більшість журналів як log4j, але у мене немає великого вибору для залежних проектів, для яких потрібен спільний пакет. У нас також є кілька класів за допомогою simpleFacade, я зараз дивлюся, чи можу я зробити щось більш послідовним.
liam 02

4

Здається, що пам'ять, крім купи, витікає, ви згадуєте, що купа залишається стабільною. Класичним кандидатом є пермген (постійне покоління), який складається з 2 речей: завантажених об'єктів класу та інтернованих рядків. Оскільки ви повідомляєте про підключення до VisualVM, ви повинні мати змогу здавати кількість завантажених класів, якщо триває збільшення завантажених класів (важливо, VisualVm також показує загальну кількість коли-небудь завантажених класів, це нормально, якщо це зросте, але кількість завантажених класів повинна стабілізуватися через певний час).

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

jmap -permstat <pid> > somefile<timestamp>.txt

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

Після того, як ви визначили певні класи як завантажені, а не розвантажені, ви можете подумки зрозуміти, де вони можуть бути створені, інакше ви можете використовувати jhat для аналізу дампів, створених за допомогою jmap -dump. Я збережу це для подальшого оновлення, якщо вам знадобиться інформація.


Гарна пропозиція. Я спробую це сьогодні вдень.
liam

jmap не допоміг, але був близько. див. повну відповідь для пояснення.
liam

2

Я б шукав безпосередньо виділений ByteBuffer.

З javadoc.

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

Можливо, код Tomcat використовує це do для вводу-виводу; налаштуйте Tomcat на використання іншого з'єднувача.

Якщо не вдасться мати потік, який періодично виконує System.gc (). "-XX: + ExplicitGCInvokesConcurrent" може бути цікавим варіантом спробувати.


1) Коли ви говорите "з'єднувач", ви маєте на увазі з'єднувач БД або інший клас прив'язки вводу-виводу? Особисто я волів би не намагатися запровадити новий пул з'єднань, навіть якщо c3p0 є близьким, але погано поставив це як можливість. 2) Я не стикався з явним прапором GC, але обов’язково розгляну його. Однак це відчуває себе трохи хакерським, і завдяки застарілій кодовій базі такого розміру я намагаюся відійти від цього підходу. (напр .: кілька місяців тому мені довелося відстежувати кілька плям, які просто породжували нитки як побічні ефекти. Нитки консолідуються зараз).
liam 02

1) Минув деякий час з того часу, як я налаштував tomcat. У нього була концепція під назвою Connector, щоб ви могли налаштувати її на прослуховування запитів від Apache httpd або безпосереднє прослуховування HTTP. У якийсь момент існував роз'єм NIO http і основний роз'єм HTTP. Ви можете побачити, які варіанти конфігурації доступні для роз'єму NIO HTTP, або побачити, чи доступний єдиний базовий роз'єм. 2) Вам потрібен лише потік, який періодично викликає System.gc (), або ви можете повторно використати часовий потік. Так, це абсолютно хакі.
Шон МакКоліфф, 02

Див. Stackoverflow.com/questions/26041117/… щодо налагодження витоків рідної пам’яті.
Lari Hotari

1

Будь-який JAXB? Я вважаю, що JAXB - це ковзанка для хімічної завивки.

Крім того, я вважаю, що visualgc , який зараз постачається з JDK 6, є чудовим способом побачити, що відбувається в пам'яті. Це чудово показує рай, покоління та промінь, а також перехідну поведінку GC. Все, що вам потрібно - це PID процесу. Можливо, це допоможе під час роботи над JProfile.

А як щодо аспектів трасування / реєстрації весни? Можливо, ви можете написати простий аспект, застосувати його декларативно, і зробити так, як потрібно, профілі бідної людини.


1) Я працюю з SA, щоб спробувати відкрити віддалений порт, і я спробую власні інструменти на основі Java / jmx (я спробував декілька, включаючи jprofiler - чудовий інструмент! - але отримати занадто складно відповідний рівень рівня системи там). 2) Я досить обережно ставлюся до будь-чого, орієнтованого на аспекти, навіть з весни. З мого досвіду, навіть наявність залежності від цього робить речі більш заплутаними та важчими для налаштування. Маю це на увазі, якщо ніщо інше не працює.
liam 02

1

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

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

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


1

У мене була та ж проблема, з кількома відмінностями ..

Моя технологія така:

граали 2.2.4

tomcat7

кварцовий плагін 1.0

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

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

Моєю проблемою була помилка в ORM grails у поєднанні з тим, як плагін обробляє сесію та моїми двома джерелами даних.

Кварцовий плагін мав слухач для запуску та знищення сеансів сплячого режиму

public class SessionBinderJobListener extends JobListenerSupport {

    public static final String NAME = "sessionBinderListener";

    private PersistenceContextInterceptor persistenceInterceptor;

    public String getName() {
        return NAME;
    }

    public PersistenceContextInterceptor getPersistenceInterceptor() {
        return persistenceInterceptor;
    }

    public void setPersistenceInterceptor(PersistenceContextInterceptor persistenceInterceptor) {
        this.persistenceInterceptor = persistenceInterceptor;
    }

    public void jobToBeExecuted(JobExecutionContext context) {
        if (persistenceInterceptor != null) {
            persistenceInterceptor.init();
        }
    }

    public void jobWasExecuted(JobExecutionContext context, JobExecutionException exception) {
        if (persistenceInterceptor != null) {
            persistenceInterceptor.flush();
            persistenceInterceptor.destroy();
        }
    }
}

У моєму випадку, persistenceInterceptorекземпляри AggregatePersistenceContextInterceptor, і він мав список HibernatePersistenceContextInterceptor. По одному для кожного джерела даних.

Кожна операція виконується з AggregatePersistenceContextInterceptorїї передачею HibernatePersistence без будь-яких модифікацій та обробок.

Коли ми дзвінки init()на HibernatePersistenceContextInterceptorнього збільшує змінний статичну нижче

private static ThreadLocal<Integer> nestingCount = new ThreadLocal<Integer>();

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

Поки тут я просто не поясню ценаріо.

Проблема виникає зараз ...

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

Флеш відбувається ідеально, а знищення ні, тому що HibernatePersistenceзробіть одну перевірку перед закритим сеансом глибокого сну ... Він перевіряє, nestingCountчи є значення більшим за 1. Якщо відповідь так, він не закриває сесію.

Спрощення того, що було зроблено Hibernate:

if(--nestingCount.getValue() > 0)
    do nothing;
else
    close the session;

Це основа мого витоку пам’яті. Кварцові потоки все ще живі з усіма об’єктами, що використовуються в сеансі, оскільки grails ORM не закриває сеанс через помилку, спричинену тим, що у мене є два джерела даних.

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

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