Pochopenie úniku pamäte v prostredí Java

1. Úvod

Jednou z hlavných výhod Javy je automatizovaná správa pamäte pomocou zabudovaného programu Garbage Collector (alebo GC v skratke). GC sa implicitne stará o pridelenie a uvoľnenie pamäte, a je teda schopný zvládnuť väčšinu problémov s únikom pamäte.

Aj keď procesor GC efektívne spracováva veľkú časť pamäte, nezaručuje spoľahlivé riešenie úniku pamäte. GC je dosť inteligentný, ale nie bezchybný. Úniky pamäte sa môžu stále prepašovať aj v aplikáciách svedomitého vývojára.

Stále sa môžu vyskytnúť situácie, keď aplikácia vygeneruje značný počet nadbytočných objektov, čím vyčerpá rozhodujúce pamäťové zdroje, čo niekedy vyústi do zlyhania celej aplikácie.

Úniky pamäte sú skutočným problémom v Jave. V tomto návode uvidíme aké sú potenciálne príčiny úniku pamäte, ako ich rozpoznať za behu a ako s nimi zaobchádzať v našej aplikácii.

2. Čo je to únik pamäte

Únik pamäte je situácia keď sú v halde objekty, ktoré sa už nepoužívajú, ale zberač odpadu ich nedokáže odstrániť z pamäte a teda sú zbytočne udržiavané.

Únik pamäte je zlý, pretože je to tak blokuje pamäťové zdroje a časom zhoršuje výkon systému. Ak sa s tým nebude nakladať, aplikácia nakoniec vyčerpá svoje zdroje a nakoniec skončí fatálne java.lang.OutOfMemoryError.

V pamäti haldy sa nachádzajú dva rôzne typy objektov - referencované a neodkazované. Odkazované objekty sú tie, ktoré majú v aplikácii stále aktívne referencie, zatiaľ čo neodkazované objekty nemajú žiadne aktívne referencie.

Odpadkový koš pravidelne odstraňuje neodkazy na objekty, ale nikdy nezhromažďuje objekty, na ktoré sa stále odkazuje. Tu môže dôjsť k úniku pamäte:

Príznaky úniku pamäte

  • Výrazné zníženie výkonu, keď je aplikácia neustále v prevádzke dlhší čas
  • OutOfMemoryError chyba haldy v aplikácii
  • Spontánne a podivné zlyhania aplikácie
  • Aplikácii občas dôjdu objekty pripojenia

Pozrime sa bližšie na niektoré z týchto scenárov a na to, ako s nimi zaobchádzať.

3. Typy úniku pamäte v Jave

V ktorejkoľvek aplikácii môže dôjsť k úniku pamäte z mnohých dôvodov. V tejto časti rozoberieme tie najbežnejšie.

3.1. Únik pamäte statický Polia

Prvý scenár, ktorý môže spôsobiť potenciálny únik pamäte, je rozsiahle využitie statický premenné.

V Jave statický polia majú životnosť, ktorá sa obvykle zhoduje s celou životnosťou spustenej aplikácie (pokiaľ ClassLoader sa stáva oprávneným na odvoz odpadu).

Vytvorme jednoduchý program Java, ktorý bude obsahovať a statickýZoznam:

public class StaticTest {public static List list = new ArrayList (); public void populateList () {for (int i = 0; i <10 000 000; i ++) {list.add (Math.random ()); } Log.info ("Debugovací bod 2"); } public static void main (String [] args) {Log.info ("Debug Point 1"); new StaticTest (). populateList (); Log.info („Debugovací bod 3“); }}

Ak teraz analyzujeme haldu pamäte počas vykonávania tohto programu, uvidíme, že medzi ladiacimi bodmi 1 a 2 sa halda podľa očakávania zvýšila.

Ale keď opustíme populateList () metóda v ladiacom bode 3, halda pamäte ešte nie je zhromaždená ako vidíme v tejto odpovedi VisualVM:

Avšak vo vyššie uvedenom programe, v riadku číslo 2, ak kľúčové slovo iba vypustíme statický, potom to prinesie drastickú zmenu vo využívaní pamäte, táto odpoveď Visual VM ukazuje:

Prvá časť až do bodu ladenia je takmer rovnaká ako tá, ktorú sme získali v prípade statický. Ale tentokrát potom, čo opustíme populateList () metóda, celá pamäť zoznamu je zhromažďovaná odpadkami, pretože na ňu nemáme žiadny odkaz.

Preto musíme venovať osobitnú pozornosť nášmu používaniu statický premenné. Ak sú zbierky alebo veľké objekty deklarované ako statický, potom zostanú v pamäti po celú dobu životnosti aplikácie, čím blokujú životne dôležitú pamäť, ktorú by inak bolo možné použiť inde.

Ako tomu zabrániť?

  • Minimalizujte použitie statický premenné
  • Pri použití singletonov sa spoliehajte na implementáciu, ktorá lenivo načíta objekt namiesto dychtivého načítania

3.2. Prostredníctvom neuzavretých zdrojov

Kedykoľvek vytvoríme nové pripojenie alebo otvoríme stream, JVM pridelí pamäť pre tieto zdroje. Niekoľko príkladov zahŕňa databázové pripojenia, vstupné toky a objekty relácie.

Ak zabudnete zavrieť tieto zdroje, môže to zablokovať pamäť, čím sa udržíte mimo dosahu GC. To sa môže dokonca stať v prípade výnimky, ktorá zabráni spusteniu programu v dosiahnutí príkazu, ktorý spracováva kód, aby tieto prostriedky zavrel.

V každom prípade, otvorené spojenie, ktoré zostalo zo zdrojov, spotrebúva pamäť, a ak s nimi nebudeme jednať, môžu zhoršiť výkon a dokonca to môže mať za následok OutOfMemoryError.

Ako tomu zabrániť?

  • Vždy používajte konečne blok na zatvorenie zdrojov
  • Kód (aj v konečne blok), ktorý uzatvára zdroje, by nemal mať sám o sebe žiadne výnimky
  • Pri použití Java 7+ môžeme využiť skús- blokovanie zdrojov

3.3. Nesprávny rovná sa () a hashCode () Implementácie

Pri definovaní nových tried veľmi častým dohľadom nie je písanie správnych prepísaných metód pre rovná sa () a hashCode () metódy.

HashSet a HashMap používajte tieto metódy pri mnohých operáciách, a ak nie sú správne prepísané, môžu sa stať zdrojom možných problémov s únikom pamäte.

Zoberme si príklad triviálneho Osoba triedy a použiť ju ako kľúč v a HashMap:

public class Osoba {public String name; verejná osoba (meno reťazca) {this.name = meno; }}

Teraz vložíme duplikát Osoba predmety do a Mapa ktorý používa tento kľúč.

Pamätajte, že a Mapa nemôže obsahovať duplicitné kľúče:

@Test public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak () {Mapa mapy = nový HashMap (); for (int i = 0; i <100; i ++) {map.put (new Person ("jon"), 1); } Assert.assertFalse (map.size () == 1); }

Tu používame Osoba ako kľúč. Odkedy Mapa neumožňuje duplicitné kľúče, početné duplikáty Osoba objekty, ktoré sme vložili ako kľúč, by nemali zvyšovať pamäť.

ale pretože sme nedefinovali správne rovná sa () metódou sa duplicitné objekty hromadia a zväčšujú pamäť, preto v pamäti vidíme viac ako jeden objekt. Halda pamäte vo VisualVM vyzerá takto:

Avšak keby sme prepísali rovná sa () a hashCode () správne metódy, potom by existoval iba jeden Osoba objekt v tomto Mapa.

Poďme sa pozrieť na správne implementácie rovná sa () a hashCode () pre naše Osoba trieda:

public class Osoba {public String name; verejná osoba (meno reťazca) {this.name = meno; } @Override public boolean equals (Object o) {if (o == this) return true; if (! (o instanceof Person)) {return false; } Osoba osoba = (Osoba) o; návratová osoba.názov.názov (meno); } @Override public int hashCode () {int vysledok = 17; výsledok = 31 * výsledok + meno.hashCode (); návratový výsledok; }}

A v tomto prípade by platili nasledujúce tvrdenia:

@Test public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak () {Mapa mapy = nový HashMap (); for (int i = 0; i <2; i ++) {map.put (new Person ("jon"), 1); } Assert.assertTrue (map.size () == 1); }

Po správnom vyradení rovná sa () a hashCode (), halda pamäte pre rovnaký program vyzerá takto:

Ďalším príkladom je použitie nástroja ORM, ako je napríklad Hibernate, ktorý používa rovná sa () a hashCode () metódy na analýzu objektov a ich uloženie do medzipamäte.

Šance na únik pamäte sú pomerne vysoké, ak tieto metódy nie sú prepísané pretože Hibernate by potom nemohol porovnávať objekty a zaplnil by svoju vyrovnávaciu pamäť duplicitnými objektmi.

Ako tomu zabrániť?

  • Ako pravidlo platí, že pri definovaní nových entít vždy prepísať rovná sa () a hashCode () metódy
  • Nestačí len prepísať, ale aj tieto metódy musia byť prekonané optimálnym spôsobom

Pre viac informácií navštívte naše návody Generovať rovná sa () a hashCode () s programom Eclipse a Sprievodca po hashCode () v Jave.

3.4. Vnútorné triedy, ktoré odkazujú na vonkajšie triedy

To sa deje v prípade nestatických vnútorných tried (anonymné triedy). Na inicializáciu tieto vnútorné triedy vždy vyžadujú inštanciu priloženej triedy.

Každá nestatická vnútorná trieda má v predvolenom nastavení implicitný odkaz na svoju triedu obsahujúcu. Ak v našej aplikácii použijeme tento objekt vnútornej triedy ', potom ani potom, čo náš obsahujúci objekt triedy 'vyjde z rozsahu, nebude zbieraný odpad.

Zvážte triedu, ktorá obsahuje odkazy na veľa objemných objektov a ktorá má nestatickú vnútornú triedu. Keď teraz vytvoríme objekt iba vnútornej triedy, pamäťový model vyzerá takto:

Ak však iba deklarujeme vnútornú triedu ako statickú, potom ten istý pamäťový model vyzerá takto:

Stáva sa to preto, lebo objekt vnútornej triedy implicitne obsahuje odkaz na objekt vonkajšej triedy, čím sa stáva neplatným kandidátom na zber odpadu. To isté sa deje v prípade anonymných tried.

Ako tomu zabrániť?

  • Ak vnútorná trieda nepotrebuje prístup k členom triedy, ktorá ju obsahuje, zvážte jej premenu na a statický trieda

3.5. Skrz dokončiť () Metódy

Použitie finalizátorov je ďalším zdrojom potenciálnych problémov s únikom pamäte. Kedykoľvek v triede dokončiť () metóda je potom prepísaná objekty tejto triedy nie sú okamžite zhromažďované odpadky. Namiesto toho ich GC čaká na finalizáciu, ku ktorej dôjde neskôr.

Navyše, ak je kód napísaný v dokončiť () metóda nie je optimálna a ak front finalizátora nemôže držať krok so zberačom odpadkov Java, potom je naša aplikácia skôr či neskôr predurčená na splnenie OutOfMemoryError.

Aby sme to demonštrovali, zvážme, že máme triedu, pre ktorú sme prepísali dokončiť () a že vykonanie tejto metódy trvá trochu času. Keď sa veľké množstvo objektov tejto triedy zhromaždí odpadky, potom to vo VisualVM vyzerá takto:

Ak by sme však iba odstránili prepísané dokončiť () metódou, potom rovnaký program poskytne nasledujúcu odpoveď:

Ako tomu zabrániť?

  • Finalizátorom by sme sa mali vždy vyhnúť

Pre viac podrobností o dokončiť (), prečítajte si časť 3 (Vyhýbanie sa finalizátorom) v našom Sprievodcovi dokončením metódy v jazyku Java.

3.6. Internovaný Struny

Java String pool prešiel významnou zmenou v prostredí Java 7, keď bol prevedený z PermGen do HeapSpace. Ale pri aplikáciách fungujúcich na verzii 6 a nižšej by sme pri práci s veľkými mali byť pozornejší Struny.

Keby sme čítali obrovskú masu String objekt a zavolajte stážista () na tomto objekte, potom ide do fondu reťazcov, ktorý je umiestnený v PermGen (permanentná pamäť) a zostane tam tak dlho, kým bude bežať naša aplikácia. To blokuje pamäť a vytvára veľký únik pamäte v našej aplikácii.

PermGen pre tento prípad v JVM 1.6 vyzerá takto vo VisualVM:

Na rozdiel od toho, ak v metóde iba čítame reťazec zo súboru a neinteragujeme ho, potom PermGen vyzerá takto:

Ako tomu zabrániť?

  • Najjednoduchší spôsob, ako vyriešiť tento problém, je aktualizácia na najnovšiu verziu Java, keď sa reťazec String Pool presunie do HeapSpace od verzie Java 7 ďalej
  • Ak pracujete na veľkom Struny, zväčšite veľkosť priestoru PermGen, aby ste predišli možnému potenciálu OutOfMemoryErrors:
    -XX: MaxPermSize = 512 m

3.7. Použitím ThreadLocals

ThreadLocal (podrobne rozobraný v úvode do ThreadLocal v Java tutorial) je konštrukt, ktorý nám dáva schopnosť izolovať stav od konkrétneho vlákna a umožňuje nám tak dosiahnuť bezpečnosť vlákna.

Pri použití tohto konštruktu každé vlákno bude obsahovať implicitný odkaz na svoju kópiu a ThreadLocal premenná a zachová si svoju vlastnú kópiu namiesto zdieľania prostriedku medzi viacerými vláknami, pokiaľ je vlákno živé.

Cez jeho výhody, použitie ThreadLocal premenné sú kontroverzné, pretože sú neslávne známe tým, že spôsobujú úniky pamäte, ak sa nepoužívajú správne. Joshua Bloch raz komentoval miestne použitie vlákna:

„Nedbalé používanie oblastí vlákien v kombinácii s nedbanlivým používaním miestnych vlákien môže spôsobiť neúmyselné uchovanie objektov, ako bolo zaznamenané na mnohých miestach. Ale kladenie viny na miestnych občanov nie je oprávnené. “

Pamäť uniká s ThreadLocals

ThreadLocals majú byť zhromaždené odpadky, akonáhle pridržiavacia niť už nie je nažive. Problém však nastáva, keď ThreadLocals sa používajú spolu s modernými aplikačnými servermi.

Moderné aplikačné servery používajú na spracovanie požiadaviek skupinu vlákien namiesto vytvárania nových (napríklad Exekútor v prípade Apache Tomcat). Okrem toho tiež používajú samostatný triedyloader.

Pretože fondy vlákien v aplikačných serveroch fungujú na koncepcii opätovného použitia vlákien, nikdy sa nezhromažďujú odpadky - namiesto toho sa znova používajú na poskytnutie inej žiadosti.

Teraz, ak nejaká trieda vytvorí a ThreadLocal premenná, ale výslovne ju neodstráni, potom kópia tohto objektu zostane pracovníkovi Závit aj po zastavení webovej aplikácie, čím sa zabráni zhromažďovaniu odpadu z objektu.

Ako tomu zabrániť?

  • Je dobrým zvykom upratovať ThreadLocals keď sa už nepoužívajú - ThreadLocals poskytnúť odstrániť () metóda, ktorá odstráni hodnotu aktuálneho vlákna pre túto premennú
  • Nepoužívať ThreadLocal.set (null) na vymazanie hodnoty - v skutočnosti nevymaže hodnotu, ale vyhľadá Mapa priradené k aktuálnemu vláknu a nastavte pár kľúč - hodnota ako aktuálne vlákno a nulový resp
  • Je ešte lepšie zvážiť ThreadLocal ako zdroj, ktorý je potrebné uzavrieť v a konečne blokujte, aby ste sa uistili, že je vždy zatvorený, a to aj v prípade výnimky:
    try {threadLocal.set (System.nanoTime ()); // ... ďalšie spracovanie} konečne {threadLocal.remove (); }

4. Ďalšie stratégie riešenia problémov s únikmi pamäte

Aj keď pri riešení únikov pamäte neexistuje univerzálne riešenie, existuje niekoľko spôsobov, ako môžeme tieto úniky minimalizovať.

4.1. Povoliť profilovanie

Profilovače jazyka Java sú nástroje, ktoré monitorujú a diagnostikujú úniky pamäte prostredníctvom aplikácie. Analyzujú, čo sa interne deje v našej aplikácii - napríklad to, ako je alokovaná pamäť.

Pomocou profilovačov môžeme porovnávať rôzne prístupy a nájsť oblasti, kde môžeme optimálne využiť naše zdroje.

V celej časti 3 tohto tutoriálu sme použili Java VisualVM. Prečítajte si nášho Sprievodcu profilermi Java, kde sa dozviete o rôznych druhoch profilovačov, ako sú Mission Control, JProfiler, YourKit, Java VisualVM a Netbeans Profiler.

4.2. Verbose Garbage Collection

Povolením podrobného zberu odpadu sledujeme podrobné sledovanie GC. Aby sme to umožnili, musíme do našej konfigurácie JVM pridať nasledovné:

-verbose: gc

Pridaním tohto parametra môžeme vidieť podrobnosti o tom, čo sa deje vo vnútri GC:

4.3. Na zabránenie úniku pamäte použite referenčné objekty

Môžeme sa tiež uchýliť k referenčným objektom v Jave, ktoré sú súčasťou balenia java.lang.ref balíček na riešenie úniku pamäte. Použitím java.lang.ref balíček, namiesto priameho odkazovania na objekty, používame špeciálne odkazy na objekty, ktoré umožňujú ich ľahký zber odpadu.

Referenčné fronty sú určené na to, aby nás upozornili na činnosti vykonávané nástrojom Garbage Collector. Ďalšie informácie nájdete v dokumentácii Soft References v tutoriáli Java Baeldung, konkrétne v časti 4.

4.4. Varovania týkajúce sa úniku pamäte pri zatmení

Pre projekty na JDK 1.5 a novších Eclipse zobrazuje varovania a chyby vždy, keď narazí na zjavné prípady úniku pamäte. Pri vývoji v Eclipse teda môžeme pravidelne navštevovať kartu „Problémy“ a dávať si väčší pozor na varovania o úniku pamäte (ak existujú):

4.5. Benchmarking

Výkon kódu Java môžeme merať a analyzovať vykonávaním testovacích kritérií. Týmto spôsobom môžeme porovnať výkon alternatívnych prístupov k vykonaniu rovnakej úlohy. To nám môže pomôcť zvoliť lepší prístup a môže nám pomôcť ušetriť pamäť.

Ak chcete získať viac informácií o testovaní, prečítajte si náš návod Mikrobenchmarking s jazykom Java.

4.6. Recenzie kódu

Nakoniec tu máme vždy klasický, old-school spôsob, ako urobiť jednoduchý postup pomocou kódu.

V niektorých prípadoch môže dokonca aj táto triviálna metóda pomôcť vylúčiť niektoré bežné problémy s únikom pamäte.

5. Záver

Laicky povedané, o úniku pamäti môžeme uvažovať ako o chorobe, ktorá zhoršuje výkonnosť našej aplikácie blokovaním životne dôležitých pamäťových zdrojov. Rovnako ako všetky ostatné choroby, ak nebudú vyliečené, môže to mať za následok fatálne zlyhanie aplikácie v priebehu času.

Úniky pamäte sú zložité na riešenie a ich nájdenie si vyžaduje zložité zvládnutie a ovládanie jazyka Java. Pokiaľ sa jedná o úniky pamäte, neexistuje univerzálne riešenie, pretože k únikom môže dochádzať pri rôznych udalostiach.

Ak sa však uchýlime k osvedčeným postupom a budeme pravidelne vykonávať dôsledné prechádzanie kódom a profilovanie, môžeme minimalizovať riziko úniku pamäte v našej aplikácii.

Útržky kódu použité na generovanie odpovedí VisualVM znázornených v tomto tutoriále sú ako vždy k dispozícii na GitHub.


$config[zx-auto] not found$config[zx-overlay] not found