Я щойно провів співбесіду, і мене попросили створити витік пам'яті з Java.
Потрібно сказати, що я відчував себе досить німим, не маючи поняття, як навіть почати його створювати.
Що може бути прикладом?
Я щойно провів співбесіду, і мене попросили створити витік пам'яті з Java.
Потрібно сказати, що я відчував себе досить німим, не маючи поняття, як навіть почати його створювати.
Що може бути прикладом?
Відповіді:
Ось хороший спосіб створити справжню витік пам'яті (об’єкти, недоступні за допомогою коду, але все ще зберігаються в пам'яті) на чистому Java:
ClassLoader
.new byte[1000000]
), зберігає чітке посилання на нього в статичному полі, а потім зберігає посилання на себе в a ThreadLocal
. Виділення додаткової пам'яті необов’язково (протікає екземпляр класу достатньо), але це зробить роботу витоку набагато швидше.ClassLoader
нього завантажений.Завдяки способу ThreadLocal
, реалізованому в JDK Oracle, це створює витік пам'яті:
Thread
є приватне поле threadLocals
, яке фактично зберігає локальні значення потоку.ThreadLocal
об’єкт, тому після того, як цей ThreadLocal
об’єкт збирається сміттям, його запис видаляється з карти.ThreadLocal
об'єкт, який є його ключем , цей об'єкт не буде ні збирати сміття, ні вилучати з карти, поки живуть нитки.У цьому прикладі ланцюжок чітких посилань виглядає приблизно так:
Thread
об'єкт → threadLocals
карта → екземпляр класу приклад → клас прикладу → статичне ThreadLocal
поле → ThreadLocal
об’єкт.
( ClassLoader
Насправді це не відіграє роль у створенні витоку, це просто посилює витік через цю додаткову довідкову ланцюжок: приклад клас → ClassLoader
→ усі класи, які він завантажив. Це було ще гірше у багатьох реалізаціях JVM, особливо до початку Java 7, тому що класи та класи ClassLoader
були розподілені прямо в permgen і взагалі ніколи не збирали сміття.)
Різновидом цієї схеми є те, чому контейнери додатків (як Tomcat) можуть просочувати пам'ять як сито, якщо ви часто перерозподіляєте додатки, які трапляються з використанням ThreadLocal
s, які певним чином вказують на себе. Це може статися з ряду тонких причин і часто важко налагодити та / або виправити.
Оновлення : оскільки багато людей продовжують просити, ось приклад коду, який показує таку поведінку в дії .
Статичне поле, що містить посилання на об'єкт [остаточне поле 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 .
close()
як правило, планується ( як правило, не запускається у фіналізаторі нитка, оскільки може бути операцією блокування). Погана практика не закриватись, але це не спричиняє протікання. Незакрите java.sql.Connection те саме.
intern
вміст хештелю. Таким чином , він буде сміття належним чином і не текти. (Але IANAJP) mindprod.com/jgloss/interned.html#GC
Проста річ - це використовувати 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.
Нижче буде не очевидний випадок, коли Java протікає, окрім стандартного випадку забутих слухачів, статичних посилань, фальшивих / модифікованих ключів у хеш-мапах або просто ниток, що застрягли без жодного шансу закінчити свій життєвий цикл.
File.deleteOnExit()
- завжди протікає струна, 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
. Найкраща частина останніх двох: їх неможливо знайти за допомогою звичайних інструментів для профілювання.
(Я можу додати ще кілька витратників часу, з якими я стикався за запитом.)
Удачі і будьте в безпеці; витоки злі!
Creating but not starting a Thread...
Так, мене це сильно покусало кілька століть тому! (Java 1.3)
unstarted
кількість, але це запобігає руйнуванню групи ниток (менше зла, але все-таки витік)
ThreadGroup.destroy()
коли у ThreadGroup немає самих потоків ..." - це неймовірно тонка помилка; Я переслідував це годинами, збивався з глузду, тому що перерахування нитки в моєму інтерфейсі управління не показало нічого, але група ниток і, мабуть, хоча б одна дочірня група не піде.
Більшість прикладів тут "занадто складні". Вони є крайніми справами. За допомогою цих прикладів програміст допустив помилку (наприклад, не переосмислюючи рівний / хеш-код), або був укушений кутовим випадком JVM / JAVA (навантаження класу зі статикою ...). Я думаю, що це не той тип прикладу, який хоче інтерв'юер, або навіть найпоширеніший випадок.
Але є справді простіші випадки витоку пам'яті. Збирач сміття лише звільняє те, на що більше не йдеться. Ми, як розробники Java, не дбаємо про пам'ять. Ми виділяємо його за потреби і нехай він буде звільнений автоматично. Чудово.
Але будь-які довгоживучі програми, як правило, мають спільний стан. Це може бути що завгодно, статика, синглтон ... Часто нетривіальні програми, як правило, складають графіки складних об'єктів. Достатньо просто забути встановити посилання на null або частіше забути видалити один об'єкт із колекції, щоб витік пам'яті.
Звичайно, усілякі слухачі (наприклад, слухачі інтерфейсу), кеші чи будь-який довговічний загальний стан мають тенденцію до витоку пам'яті, якщо не належним чином обробляються. Слід розуміти, що це не кутовий випадок Java чи проблема зі смітником. Це проблема дизайну. Ми розраховуємо, що додаємо слухача до довгоживучого об'єкта, але не видаляємо слухача, коли це вже не потрібно. Ми кешуємо об’єкти, але у нас немає стратегії їх видалення з кеша.
У нас може бути складний графік, який зберігає попередній стан, необхідний для обчислення. Але попередній стан сам по собі пов'язаний з державою раніше тощо.
Як і ми повинні закрити з'єднання SQL або файли. Нам потрібно встановити належні посилання на null та видалити елементи з колекції. Ми матимемо належні стратегії кешування (максимальний розмір пам'яті, кількість елементів або таймери). Усі об'єкти, які дозволяють отримувати сповіщення слухача, повинні забезпечувати як метод addListener, так і deleteListener. І коли ці сповіщувачі більше не використовуються, вони повинні очистити свій список слухачів.
Витік пам'яті справді можливий і цілком передбачуваний. Не потрібно особливих мовних особливостей або кутових справ. Витоки пам’яті - це або показник того, що чогось можливо не вистачає, або навіть проблем із дизайном.
WeakReference
) від одного до іншого. Якщо у посилання на об’єкт був запасний біт, може бути корисним показник "піклується про ціль" ...
PhantomReference
), якщо в обєкті не знайшлося жодного, хто про нього піклувався. WeakReference
підходить дещо близько, але перед тим, як його можна використовувати, його слід перетворити на чітке посилання; якщо цикл GC виникає, поки існує сильна посилання, ціль вважатиметься корисною.
Відповідь повністю залежить від того, що поцікавився інтерв'юер.
Чи можливо на практиці змусити витікати Java? Звичайно, є, і в інших відповідях є багато прикладів.
Але є кілька мета-запитань, які, можливо, вам задавали?
Я читаю ваше мета-питання як "Що таке відповідь, яку я міг би використати в цій ситуації інтерв'ю". Отже, я збираюся зосередитись на навичках інтерв'ю замість Java. Я вважаю, що ви більше схильні повторити ситуацію, коли не знаєте відповіді на запитання в інтерв'ю, ніж ви повинні опинитися там, де вам потрібно знати, як зробити течі на Java. Тож, сподіваємось, це допоможе.
Однією з найважливіших навичок, яку ви можете розвинути для співбесіди, є навчитися активно слухати питання та працювати з інтерв'юером, щоб отримати їхні наміри. Це не тільки дозволяє вам відповісти на їх запитання так, як вони хочуть, але також показує, що у вас є певні життєві навички спілкування. І коли зводиться до вибору між багатьма однаково талановитими розробниками, я наймаю того, хто слухає, думає та розуміє, перш ніж кожен раз відповідатиме.
Далі - досить безглуздий приклад, якщо ви не розумієте 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
екземпляром, коли ви лише додаєте до списку та не видаляєте з нього (хоча ви повинні позбуватися елементів, які вам більше не потрібні), абоSocket
s або File
s, але не закриваємо їх, коли вони більше не потрібні (подібно до наведеного вище прикладу, що стосується Connection
класу).Connection.close
остаточний блок усіх моїх викликів SQL. Для додаткової розваги я зателефонував до тривалих процедур, що зберігаються в Oracle, що вимагають блокування на стороні Java, щоб запобігти занадто багато дзвінків до бази даних.
Напевно, одним з найпростіших прикладів потенційного витоку пам'яті та як цього уникнути, є реалізація 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
)? Ця довідка може зберегти величезний об’єкт живим ...
Щоразу, коли ви зберігаєте посилання на об'єкти, які вам більше не потрібні, у вас витік пам'яті. Див. Розділ Обробка витоків пам’яті в програмах Java для прикладів того, як витоки пам’яті проявляються в Java та що ви можете з цим зробити.
...then the question of "how do you create a memory leak in X?" becomes meaningless, since it's possible in any language.
Я не бачу, як ти робиш такий висновок. Існує менше способів створити витік пам'яті в Java за будь-яким визначенням. Це, безумовно, все-таки актуальне питання.
Ви можете витік пам'яті за допомогою класу 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();
}
}
}
Я можу скопіювати свою відповідь звідси: Найпростіший спосіб викликати витік пам'яті на Java?
"Витік пам'яті в інформатиці (або витік в цьому контексті) відбувається, коли комп'ютерна програма споживає пам'ять, але не в змозі повернути її назад в операційну систему." (Вікіпедія)
Проста відповідь: Ви не можете. Java здійснює автоматичне управління пам’яттю і звільнить ресурси, які вам не потрібні. Ви не можете запобігти цьому. ВИНАГО зможе випустити ресурси. У програмах з ручним управлінням пам'яттю це інакше. Ви не можете отримати деяку пам’ять на C за допомогою malloc (). Щоб звільнити пам'ять, вам потрібен вказівник, який повернувся malloc, і виклик на нього free (). Але якщо у вас більше немає вказівника (перезаписаний або перевищено термін служби), ви, на жаль, не зможете звільнити цю пам'ять, і, таким чином, у вас є витік пам'яті.
Всі інші відповіді поки що, на моє визначення, насправді не протікають пам'яті. Всі вони спрямовані на швидке наповнення пам’яті безглуздими речами. Але в будь-який час ви все-таки зможете знеструмити створені вами об'єкти і тим самим звільнити пам'ять -> NO LEAK. Відповідь аккорада дуже близька, хоча, як я маю визнати, оскільки його рішення ефективно просто «розбивати» сміттєзбірник, примушуючи його до нескінченного циклу).
Довга відповідь: Ви можете отримати витік пам'яті, написавши бібліотеку для Java за допомогою JNI, яка може керувати пам'яттю вручну і таким чином мати витоки пам'яті. Якщо ви зателефонуєте до цієї бібліотеки, у вашому процесі Java буде просочена пам'ять. Або у вас можуть бути помилки в JVM, так що JVM втрачає пам'ять. Напевно, у JVM є помилки, можуть бути навіть деякі відомі, оскільки збирання сміття не таке банальне, але тоді це все-таки помилка. За задумом це неможливо. Ви можете запитати код Java, який впливає на такий помилку. Вибачте, я цього не знаю, і це, можливо, більше не буде помилкою у наступній версії Java.
Ось простий / зловісний за допомогою 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 був відкинутий.
muchSmallerString
буде звільнено (оскільки StringLeaker
об’єкт знищено), також буде звільнена довга струна. Те, що я називаю витоком пам'яті, - це пам'ять, яка ніколи не може бути звільнена в цьому випадку JVM. Тим НЕ менше, ви показали себе , як звільнити пам'ять: this.muchSmallerString=new String(this.muchSmallerString)
. Зі справжньою витоком пам’яті ви нічого не можете зробити.
intern
може бути скоріше "сюрпризом пам'яті", ніж "витоком пам'яті". .intern()
Однак підрядка, безумовно, створює ситуацію, коли посилання на довший рядок зберігається і не може бути звільнена.
Поширений приклад цього в коді графічного інтерфейсу - це при створенні віджета / компонента та доданні слухача до якогось об’єкта, на який поширюється статичний / додаток, а потім не видалення слухача при знищенні віджета. Ви не тільки отримуєте витік пам’яті, але і хіт виступу, коли коли б ви не слухали події пожеж, вас називають і всі ваші старі слухачі.
Візьміть будь-яку веб-програму, що працює в будь-якому контейнері сервлетів (Tomcat, Jetty, Glassfish, що б там не було ...). Повторно розгортайте додаток 10 чи 20 разів поспіль (може бути достатньо просто торкнутися ВІЙНИ в каталозі автовідтворення сервера.
Якщо хтось насправді цього не перевірив, великі шанси, що ви отримаєте OutOfMemoryError після пари перестановок, оскільки програма не подбала про очищення після себе. Ви можете навіть виявити помилку на своєму сервері за допомогою цього тесту.
Проблема полягає в тому, що термін служби контейнера довший, ніж термін служби вашої програми. Ви повинні переконатися, що всі посилання, які контейнер може мати на об'єкти або класи вашої програми, можуть бути зібрані сміття.
Якщо існує лише одна довідка, яка пережила нерозгортання вашого веб-додатка, відповідний завантажувач і, як наслідок, усі класи вашого веб-додатка не можуть бути зібрані сміттям.
Нитки, розпочаті вашою заявкою, змінні ThreadLocal, реєстрація додатків - деякі звичайні підозрювані, що викликають протікання завантажувача.
Можливо, використовуючи зовнішній кодовий код через JNI?
З чистою Java це майже неможливо.
Але мова йде про "стандартний" тип витоку пам'яті, коли ви більше не можете отримати доступ до пам'яті, але вона все ще належить додатку. Натомість ви можете зберігати посилання на невикористані об'єкти або відкриті потоки, не закриваючи їх згодом.
Я колись мав приємний "витік пам'яті" стосовно PermGen та XML-розбору. Аналізатор XML, який ми використовували (я не пам'ятаю, який це був), зробив String.intern () для імен тегів, щоб зробити порівняння швидшим. Один з наших клієнтів мав ідею зберігати значення даних не в атрибутах або тексті XML, а як імена тегів, тому у нас був такий документ, як:
<data>
<1>bla</1>
<2>foo</>
...
</data>
Насправді вони використовували не цифри, а довші текстові ідентифікатори (близько 20 символів), які були унікальними та надходили зі швидкістю 10-15 мільйонів на день. Це складає 200 Мб сміття на день, який більше ніколи не потрібен, і ніколи не стає GCed (оскільки це в PermGen). Пермген у нас був 512 Мб, тому для виходу поза пам'яті (OOME) пішло близько двох днів ...
Що протікає в пам'яті:
Типовий приклад:
Кеш об'єктів - хороша відправна точка, щоб зіпсувати речі.
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 (зберігає лише нещодавно використані об'єкти в кеші).
Звичайно, ви можете зробити речі набагато складнішими:
Що часто трапляється:
Якщо цей інформаційний об'єкт має посилання на інші об'єкти, які знову мають посилання на інші об'єкти. Зрештою, ви могли також вважати це витоком пам’яті (спричиненим поганим дизайном).
Я вважав, що цікаво, що ніхто не використовував внутрішні приклади класу. Якщо у вас внутрішній клас; вона по суті підтримує посилання на клас, що містить. Звичайно, це технічно не є витоком пам'яті, оскільки 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 припускає, що він завжди буде існувати і насправді ніколи не намагатиметься його очистити, якщо його більше не можна отримати в коді. Але це абсолютно не підтверджено.
Нещодавно я стикався з ситуацією з витоком пам’яті, викликаною способом 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, тим самим спричиняючи витік пам'яті.
Інтерв'юер, мабуть, шукав циркулярну довідку, як код нижче (який, до речі, просочував пам'ять лише у старих 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.
Створіть статичну карту і продовжуйте додавати жорсткі посилання на неї. Це ніколи не будуть 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); }
}
Ви можете створити рухливу витік пам'яті, створивши новий екземпляр класу в методі остаточного завершення класу. Бонусні бали, якщо фіналізатор створює кілька примірників. Ось проста програма, яка просочує всю купу за кілька секунд до декількох хвилин, залежно від розміру вашої купи:
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());
}
}
}
Я не думаю, що це ще ніхто не сказав: ви можете воскресити об'єкт, змінивши метод finalize () таким, що finalize () десь зберігає посилання на це. Сміттєзбірник буде викликаний лише один раз на об’єкт, після чого об’єкт ніколи не буде знищений.
finalize()
не буде викликано, але об'єкт буде зібрано, як тільки більше посилань не буде. Також сміттєзбірник не називається.
finalize()
метод може бути викликаний лише один раз JVM, але це не означає, що він не може бути повторно зібраний сміттям, якщо об’єкт воскреснути, а потім знову знешкодити. Якщо в finalize()
методі є код закриття ресурсу, цей код не запуститься знову, це може спричинити витік пам'яті.
Нещодавно я натрапив на більш тонкий вид витоку ресурсів. Ми відкриваємо ресурси через 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 Мб, незалежно від кількості ітерацій.
Досить просто і дивно.
Як вважає багато людей, витік ресурсів досить легко викликати - як приклади JDBC. Фактичні витоки пам’яті трохи складніше - особливо якщо ви не покладаєтесь на зламані біти JVM, щоб зробити це за вас ...
Ідеї створення об'єктів, які мають дуже великий слід і не мають можливості отримати доступ до них, теж не є реальними витоками пам'яті. Якщо нічого не може отримати до нього, то це буде зібране сміття, а якщо щось може отримати до нього, то це не витік ...
Один із способів, який раніше працював - і я не знаю, чи все ще є - це мати три глибокого кругового ланцюга. Оскільки в Об'єкті A є посилання на Об'єкт B, Об'єкт B має посилання на Об'єкт C, а Об'єкт C має посилання на Об'єкт А. В GC був досить розумним, щоб знати, що два глибоких ланцюга - як у A <--> B - можна сміливо збирати, якщо A і B не доступні нічим іншим, але не могли б обробити тристоронній ланцюг ...
Інший спосіб створення потенційно величезні витоку пам'яті, щоб зберігати посилання на Map.Entry<K,V>
про TreeMap
.
Важко оцінити, чому це стосується лише TreeMap
s, але, дивлячись на реалізацію, причина може полягати в тому, що: TreeMap.Entry
зберігає посилання на своїх побратимів, тому якщо a TreeMap
готовий для збору, але якийсь інший клас має посилання на будь-який із його Map.Entry
, тоді вся Карта збережеться в пам’яті.
Реальний сценарій:
Уявіть, що у вас є db-запит, який повертає велику TreeMap
структуру даних. Люди зазвичай використовують TreeMap
s, оскільки порядок введення елемента зберігається.
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
графіка видно, як пам'ять постійно зростає.
Те ж не відбувається з хешованою структурою даних ( HashMap
).
Це графік при використанні а HashMap
.
Рішення? Просто збережіть ключ / значення (як ви, мабуть, вже робите), а не зберігайте Map.Entry
.
Я написав більш великий тест тут .
Нитки не збираються, поки вони не припиняться. Вони служать корінням збору сміття. Вони є одним з небагатьох об'єктів, які не будуть повернені, просто забувши про них або очистивши посилання на них.
Поміркуйте: основна схема припинення робочої нитки полягає у встановленні певної змінної умови, яку бачить потік. Потік може періодично перевіряти змінну і використовувати її як сигнал для завершення. Якщо змінна не оголошена 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
комусь, ніколи не загине.
(* відредаговано *)
Я думаю, що вагомим прикладом може бути використання змінних ThreadLocal в середовищі, де потоки об'єднані.
Наприклад, використання змінних ThreadLocal в сервлетах для спілкування з іншими веб-компонентами, що створюють потоки, створені контейнером, і підтримують простої в пулі. Змінні ThreadLocal, якщо їх неправильно очистити, будуть існувати до тих пір, поки, можливо, той же веб-компонент не замінить свої значення.
Звичайно, виявивши проблему, можна легко вирішити.
Інтерв'юер, можливо, шукає кругового довідкового рішення:
public static void main(String[] args) {
while (true) {
Element first = new Element();
first.next = new Element();
first.next.next = first;
}
}
Це класична проблема щодо підрахунку сміттєзбірників. Потім ви ввічливо поясніть, що JVM використовують набагато більш складний алгоритм, який не має цього обмеження.
-Вес Тарле
first
не корисний і його слід збирати сміття. Під час підрахунку сміттєзбірників об’єкт не буде звільнений, оскільки на нього є активна довідка (сама по собі). Нескінченний цикл є тут, щоб знищити витік: коли ви запускаєте програму, пам'ять підніметься нескінченно.