Теория и практика на Java Отстраняване на проблеми с изтичане на памет със слаби препратки
Слабите референции улесняват изразяването на връзките на жизнения цикъл на обекта
Серия съдържание:
Това съдържание е част # от поредица # статии: Теория и практика на Java
Това съдържание е част от поредицата: Теория и практика на Java
Очаквайте нови статии от тази серия.
Събирането на отпадъци (GC) за възстановяване на паметта, заета от обекти, които вече не се използват от програмата, изисква логическият живот на обекта (периодът от време, през който се използва от приложението) и действителният живот на чакащите асоциации с този обект да бъдат еднакви. В повечето случаи добрите техники за проектиране на софтуер автоматично ще гарантират, че това условие е изпълнено, без да е необходимо да се обръща специално внимание на въпроса за живота на обекта. Но от време на време създаваме препратка, която държи обект в паметта по-дълго от очакваното; тази ситуация се нарича случайно задържане на обект в паметта.
Изтичане на памет и глобални карти за разпределение
Най-честата причина за случайно задържане на обект в паметта е използването на карта за свързване на метаданни с временни обекти. Да приемем, че имате обект със среден живот - по-дълъг от живота на извикващия метод, но по-кратък от живота на приложението - като канал за връзка на клиент. Искате да свържете някои метаданни с този канал, като например ID на потребителя, който е направил връзката. Не знаете тази информация, когато създавате обекта Socket (Връзка) и не можете да добавяте данни към обекта Socket, защото не контролирате класа Socket или неговияинстанция. В този случай обичайният трик е да съхраните тази информация в глобално разпространение на карта, както е направено в класа SocketManager в листинг 1:
Листинг 1. Използване на картата за глобално разпространение за картографиране на метаданни към обект
Недостатъкът на този подход е, че животът на метаданните трябва да бъде свързан с живота на връзката. Когато знаете със сигурност, че програмата вече не се нуждае от тази връзка, ще трябва да запомните да премахнете съответния запис от картата за разпространение ( Карта ), в противен случай обектите Socket и User винаги ще съществуват в Картата - дълго след като заявката е била обслужена и връзката е затворена. Това няма да изчисти паметта, заета от обектите Socket и User, дори ако те никога повече не се използват от приложението. Оставено без отметка, това явление може лесно да доведе до препълване на паметта, ако програмата работи достатъчно дълго. В повечето случаи триковете за определяне кога дадена програма вече не се нуждае от Socket наподобяват досадните и склонни към грешки трикове, необходими за ръчно управление на паметта.
Откриване на изтичане на памет
Първият знак, че вашата програма има изтичане на памет, обикновено е съобщение за грешка OutOfMemoryError или лоша производителност на програмата поради често почистване на паметта. За щастие, събирачът на отпадъци ви позволява да получите достъп до много информация, която може да се използва за откриване на изтичане на памет. Ако активирате JVM с опцията -verbose:gc или -Xloggc, съобщение за грешка се отпечатва на екрана или в регистрационния файл при всяко стартиране.събирач на боклук и също така съдържа информация за изминалото време, текущото динамично използване на паметта и количеството възстановена памет. Събирането на данни от събирача на боклука и записването им не ви притеснява, така че е разумно да активирате тази опция за събирача на боклук по подразбиране, в случай че някога трябва да анализирате проблеми с паметта или да настроите събирача на боклук.
Инструментите могат да вземат изхода на събирача на отпадъци и да го показват графично, като един такъв инструмент е безплатният JTune (вижте Ресурси). Като разгледате графиката на състоянието на динамичната памет след събиране на боклука, можете да видите динамиката на използването на паметта на програмата. За повечето програми можете да разделите използването на паметта на два компонента: минимално използване на паметта и текущо използване на паметта. За сървърно приложение минималното използване на паметта съответства на състоянието, когато приложението не прави никакви заявки, но е готово да обслужи заявката, когато пристигне; текущото натоварване на паметта е паметта, използвана при обработката на заявката, но която ще бъде освободена, когато тя бъде завършена. Тъй като използването на паметта е приблизително стабилно, приложенията обикновено достигат стабилно състояние на използване на паметта доста бързо. Ако използването на паметта продължава да се увеличава, въпреки че приложението е завършило инициализацията и използването му не се увеличава, програмата вероятно държи обекти, създадени чрез обработка на предишни заявки в паметта.
Листинг 2 показва примерна програма с изтичане на памет. MapLeaker обработва задания в пула от нишки и записва състоянието на всяко задание в карта. За съжаление, той не изтрива записи, когато дадено задание завърши, така че записите на състояние и обектите на заданието (и тяхното вътрешно състояние) са постояннонатрупвам.
Листинг 2. Програма с изтичане на памет на карта за разпределение
Фигура 1 показва графика на това как се променя динамичното използване на паметта на приложението след почистване на паметта за MapLeaker с течение на времето. Възходяща тенденция в кривата е индикация за изтичане на памет. (В реални приложения наклонът на кривата не е толкова остър, но тенденцията става очевидна, ако данните от събирача на отпадъци се събират достатъчно дълго.)
Фигура 1. Устойчива възходяща тенденция в използването на паметта
След като сте проверили, че има изтичане на памет, следващата ви стъпка е да определите какъв тип обект го причинява. Всеки профайлър на паметта ви позволява да направите моментна снимка на състоянието на паметта, дефрагментирана от обектен клас. Има отлични търговски програми, които предоставят тази възможност, но не е нужно да харчите пари, за да откриете течове на памет - вграденият инструмент hprof също може да свърши работа. За да използвате hprof и да проследите използването на паметта, извикайте JVM с опцията -Xrunhprof:heap=sites.
Списък 3 показва съответната част от изхода на hprof, илюстриращ злоупотребата с памет на приложението. (Инструментът hprof прекъсва използването на паметта, когато приложението излезе, или когато се даде сигнал за спиране -3 на приложението, или когато се натисне Ctrl+Break в Windows.) Обърнете внимание на забележимото увеличение на обектите Map.Entry, Task и int[] между двете моментни снимки.
Списък 4 показва другата част от изхода на hprof, който съдържа информация за стека за извикване към разположенията за обектите Map.Entry. Тези данни ни казват кои вериги на повиквания генерират Map.Entry обекти; като е прекараланализ на програмата, обикновено е достатъчно просто да откриете източника на изтичане на памет.
Списък 4. HPROF изход, показващ разположението на Map.Entry обекти
Слаби връзки към спасението
Проблемът с SocketManager е, че продължителността на живота на препратката Socket - User трябва да бъде същата като продължителността на живота на Socket, но езикът за програмиране не ни дава начин лесно да приложим това условие. Това принуждава програмата да прибягва до техники, напомнящи ръчно управление на паметта. За щастие, започвайки с JDK 1.2, програмата за почистване на паметта дава възможност да се декларират такива зависимости от жизнения цикъл на обекта, така че събирачът на боклук да може да ни помогне да избегнем изтичане на памет от този вид - чрез използване на слаби препратки.
Обект WeakReference се създава при изпълнение на конструктора и неговата стойност, ако все още не е изтрита, може да бъде извлечена чрез метода get(). Ако слабите препратки са били изтрити (тъй като референтният обект е бил събран боклук или защото е извикан методът WeakReference.clear(), get() връща null. Съответно, винаги трябва да проверявате дали върнатата стойност на метода get() е различна от null, преди да използвате неговия резултат, тъй като се очаква референтният обект да бъде събиран боклук с течение на времето.
Чрез копиране на препратка към обект, използвайки нормални (силни) препратки, вие налагате ограничение върху живота на реферирания обект, който трябва да бъде поне същият като този на копираната препратка. Ако не внимавате, може да е същото като на вашата програма - като когато поставите обект в глобалната колекция. От друга страна, чрез създаване на слаба препратка към обект, вие изобщо не увеличавате живота на реферирания обект; Ти простоосигурете алтернативен начин за позоваване на него, докато все още съществува.
Слабите препратки са най-полезни при създаване на слаби колекции, като тези, които съхраняват метаданни за обекти само докато приложението ги използва (обекти) - точно това, което трябва да прави класът SocketManager. Тъй като това е обичайна употреба на слаби препратки, WeakHashMap, който използва слаби препратки за ключове (вместо стойности), също беше добавен към библиотечния клас в JDK 1.2. Ако използвате обект като ключ в обикновен HashMap, той не може да бъде събиран за боклук, докато записът не бъде премахнат от картата за разпространение; WeakHashMap ви позволява да използвате обект като ключ на карта, без да го предпазвате от събиране на боклук. Списък 5 показва възможна реализация на метода get() от WeakHashMap, демонстрирайки използването на слаби препратки:
Листинг 5. Възможна реализация на метода WeakReference.get().
Когато добавяте запис за разпределение към WeakHashMap, имайте предвид, че този запис за разпределение може по-късно да стане „изпуснат“ (счупен), тъй като ключът се събира за боклук. В този случай get() връща null, което прави проверката на стойността, върната от get() (проверка дали е null) дори по-важна от обикновено.
Коригиране на теч с WeakHashMap
Коригирането на теч в SocketManager е лесно; просто заменете HashMap с WeakHashMap, както е показано в списък 6. (Ако SocketManager трябва да бъде безопасен за нишки, можете да обвиете WeakHashMap с Collections.synchronizedMap()). Този метод може да се използва винаги, когато животът на картата на местоположението трябва да бъде обвързан с живота на ключа. Въпреки това, тази техника не трябва да се злоупотребява; в повечето случаи нормалноHashMap е използваема реализация на Map.
Листинг 6. Коригиране на SocketManager с WeakHashMap
Опашки за връзки
WeakHashMap използва слаби препратки за съхраняване на ключове за карта, което позволява на ключовите обекти да бъдат обработени от програмата за почистване на паметта, когато вече не се използват от приложението, а внедряването на метод get() като WeakReference.get() може да разпознае дали даден обект се използва или не, връщайки null в последния случай. Но това е само половината от това, което е необходимо, за да се предотврати увеличаването на използването на паметта на картата за разпределение на картата през целия живот на приложението; трябва да се направи нещо друго, за да се премахнат неизползваните записи от картата, след като ключът бъде събран. В противен случай картата просто ще се препълни със записи, съответстващи на "мъртви" ключове. Въпреки че приложението няма да види това, приложението може да използва прекомерно памет, тъй като Map.Entry и обектите, които съхраняват стойностите, няма да бъдат събрани за боклук, въпреки че ключовете са обработени.
Неизползваните записи могат да бъдат намерени и премахнати чрез периодично сканиране на картата чрез извикване на get() за всяка слаба препратка и изтриване на записа, ако върнатата стойност на get() е null. Но този метод е непродуктивен, ако картата съхранява много работни записи. Би било много удобно да бъдете уведомени, ако референтният обект е обработен от програмата за почистване на паметта. Това е задачата на опашките за връзки.
WeakHashMap също има свой собствен метод expungeStaleEntries(), който се извиква по време на повечето операции на Map, като обработва всички повредени препратки по ред в опашката за препратки и премахва свързаните съпоставяния. Възможна реализация на методаexpungeStaleEntries() е показано в листинг 7. Типът Entry, използван за съхраняване на съпоставянията ключ-стойност, разширява WeakReference, така че когато expungeStaleEntries() поиска следващата повредена препратка, той връща Entry. Използването на референтни опашки за почистване на карта вместо периодично търсене в цялото й съдържание е по-ефективно, тъй като работните записи никога не се засягат от процеса на почистване; почистването се извършва само ако има действително поставени на опашка връзки.
Листинг 7. Възможна реализация на метода WeakHashMap.expungeStaleEntries()
Заключение
Изтегляне на ресурси
Свързани теми
- Оригинална статия: Запушване на изтичане на памет със слаби препратки.
- JTune: Безплатен инструмент JTune, който ви позволява да обработвате регистрационни файлове на събирача на отпадъци, да изобразявате графики на количеството динамична памет, продължителността на събирането на отпадъци (процес на почистване на паметта) и други полезни данни за управление на паметта.
- „Конфигуриране на почистване на паметта в HotSpot JVM“: Кърк Пепърдайн и Джак Ширази ще покажат как дори малко изтичане на памет може да окаже голям натиск върху събирача на отпадъци с течение на времето.
- „HPROF“: Този документ от Sun описва как да използвате вградения инструмент HPROF за анализ на профили.
- Референтни обекти и събиране на боклук: Написана малко след добавянето на референтни обекти към библиотеката на класовете, тази бяла книга от Sun описва как събирачът на боклук обработва референтни обекти.
- Секция за технологии на Java: Стотици статии за всеки аспект на програмирането на Java.