Razumijevanje curenja memorije u Javi

1. Uvod

Jedna od osnovnih prednosti Jave je automatizirano upravljanje memorijom uz pomoć ugrađenog kolektora smeća (ili GC za kratko). GC se implicitno brine za dodjelu i oslobađanje memorije te je stoga sposoban riješiti većinu problema s curenjem memorije.

Iako GC učinkovito obrađuje dobar dio memorije, to ne jamči pouzdano rješenje za curenje memorije. GC je prilično pametan, ali ne i besprijekoran. Curenje memorije i dalje se može prikrasti čak i u aplikacijama savjesnog programera.

Još uvijek mogu biti situacije kada aplikacija generira znatan broj suvišnih objekata, iscrpljujući tako ključne memorijske resurse, što ponekad rezultira neuspjehom cijele aplikacije.

Curenje memorije pravi je problem na Javi. U ovom uputstvu ćemo vidjeti koji su potencijalni uzroci curenja memorije, kako ih prepoznati tijekom izvođenja i kako se nositi s njima u našoj aplikaciji.

2. Što je curenje memorije

Curenje memorije je situacija kada su u hrpi prisutni objekti koji se više ne koriste, ali sakupljač smeća ne može ih ukloniti iz memorije te se stoga nepotrebno održavaju.

Curenje memorije je loše jer to blokira resurse memorije i s vremenom pogoršava performanse sustava. A ako se ne riješi, aplikacija će na kraju iscrpiti svoje resurse, konačno završavajući fatalno java.lang.OutOfMemoryError.

Postoje dvije različite vrste objekata koji se nalaze u hrpi memorije - referencirani i bez referenciranja. Referencirani objekti su oni koji još uvijek imaju aktivne reference u aplikaciji, dok nereferencirani objekti nemaju aktivne reference.

Sakupljač smeća povremeno uklanja nereferencirane objekte, ali nikada ne prikuplja objekte na koje se još uvijek upućuje. Ovdje može doći do curenja memorije:

Simptomi curenja memorije

  • Teška degradacija performansi kada aplikacija kontinuirano radi dulje vrijeme
  • OutOfMemoryError gomila pogreške u aplikaciji
  • Spontani i čudni pad aplikacija
  • Aplikaciji povremeno ponestaje objekata veze

Pogledajmo pobliže neke od ovih scenarija i kako se nositi s njima.

3. Vrste curenja memorije u Javi

U bilo kojoj aplikaciji može doći do curenja memorije iz brojnih razloga. U ovom ćemo odjeljku razgovarati o najčešćim.

3.1. Propuštanje memorije statički Polja

Prvi scenarij koji može prouzročiti potencijalno curenje memorije je intenzivno korištenje statički varijable.

U Javi, statički polja imaju životni vijek koji se obično podudara s cijelim životnim vijekom pokrenute aplikacije (osim ako ClassLoader postaje prihvatljiv za odvoz smeća).

Stvorimo jednostavan Java program koji popunjava a statičkiPopis:

javna klasa StaticTest {javni statički popis popisa = novi ArrayList (); javna praznina populateList () {for (int i = 0; i <10000000; i ++) {list.add (Math.random ()); } Log.info ("Točka otklanjanja pogrešaka 2"); } public static void main (String [] args) {Log.info ("Debug Point 1"); novi StaticTest (). populateList (); Log.info ("Točka otklanjanja pogrešaka 3"); }}

Ako sada analiziramo Heap memoriju tijekom izvođenja ovog programa, vidjet ćemo da se između točaka otklanjanja pogrešaka 1 i 2, očekivano, povećala memorija hrpe.

Ali kad napustimo populateList () metoda u točki za otklanjanje pogrešaka 3, memorija hrpe još nije prikupljeno smeće kao što možemo vidjeti u ovom odgovoru VisualVM-a:

Međutim, u gornjem programu, u retku broj 2, ako samo ispustimo ključnu riječ statički, tada će donijeti drastičnu promjenu u upotrebi memorije, ovaj Visual VM odgovor pokazuje:

Prvi dio do točke otklanjanja pogrešaka gotovo je isti onome što smo dobili u slučaju statički. Ali ovaj put nakon što napustimo populateList () metoda, sva memorija popisa je prikupljeno smeće jer na njega nemamo nikakve reference.

Stoga moramo jako paziti na našu upotrebu statički varijable. Ako su zbirke ili veliki predmeti deklarirani kao statički, zatim ostaju u memoriji tijekom cijelog životnog vijeka aplikacije, čime blokiraju vitalnu memoriju koja bi se inače mogla koristiti negdje drugdje.

Kako to spriječiti?

  • Umanjite upotrebu statički varijable
  • Kada koristite singletone, oslanjajte se na implementaciju koja lijeno učitava objekt umjesto da ga nestrpljivo učitava

3.2. Kroz zatvorene resurse

Kad god uspostavimo novu vezu ili otvorimo tok, JVM dodjeljuje memoriju za te resurse. Nekoliko primjera uključuje veze s bazom podataka, ulazne tokove i objekte sesije.

Zaborav na zatvaranje tih resursa može blokirati memoriju, čime se čuvaju izvan dosega GC-a. To se čak može dogoditi u slučaju iznimke koja sprečava izvršavanje programa da dođe do izraza koji obrađuje kôd da zatvori ove resurse.

U svakom slučaju, otvorena veza preostala iz resursa troši memoriju, a ako se s njima ne pozabavimo, oni mogu pogoršati performanse i čak rezultirati OutOfMemoryError.

Kako to spriječiti?

  • Uvijek koristite konačno blok za zatvaranje resursa
  • Kod (čak i u konačno blok) koji zatvara resurse sam po sebi ne bi trebao imati izuzetaka
  • Kada koristimo Javu 7+, možemo iskoristiti probati-s blokom-resursa

3.3. Nepravilno jednako () i hashCode () Provedbe

Pri definiranju novih klasa, vrlo uobičajeni previd nije pisanje ispravnih nadjačanih metoda za jednako () i hashCode () metode.

HashSet i HashMap koristite ove metode u mnogim operacijama, a ako nisu nadjačane ispravno, tada mogu postati izvor potencijalnih problema s curenjem memorije.

Uzmimo primjer trivijalnog Osoba klase i koristiti ga kao ključ u a HashMap:

javni razred Osoba {ime javnog niza; javna osoba (ime niza) {this.name = ime; }}

Sad ćemo umetnuti duplikat Osoba predmeti u Karta koji koristi ovaj ključ.

Zapamtite da a Karta ne može sadržavati duplicirane ključeve:

@Test javna praznina givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak () {Map map = new HashMap (); for (int i = 0; i <100; i ++) {map.put (nova Osoba ("jon"), 1); } Assert.assertFalse (map.size () == 1); }

Ovdje koristimo Osoba kao ključ. Od Karta ne dopušta duplicirane ključeve, brojne duplikate Osoba objekti koje smo umetnuli kao ključ ne bi trebali povećavati memoriju.

Ali budući da nismo definirali pravilno jednako () metodom, duplicirani se objekti gomilaju i povećavaju memoriju, zato u memoriji vidimo više objekata. Memorija hrpe u VisualVM-u za ovo izgleda ovako:

Međutim, da smo nadjačali jednako () i hashCode () metode pravilno, tada bi postojala samo jedna Osoba objekt u ovome Karta.

Pogledajmo pravilne implementacije jednako () i hashCode () za naše Osoba razred:

javni razred Osoba {ime javnog niza; javna osoba (ime niza) {this.name = ime; } @Override public boolean equals (Objekt o) {if (o == this) return true; if (! (o instanceof Person)) {return false; } Osoba osoba = (Osoba) o; vratiti person.name.equals (ime); } @Override public int hashCode () {int rezultat = 17; rezultat = 31 * rezultat + ime.hashCode (); povratni rezultat; }}

I u ovom bi slučaju slijedeće tvrdnje bile istinite:

@Test javna praznina givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak () {Map map = new HashMap (); for (int i = 0; i <2; i ++) {map.put (nova Osoba ("jon"), 1); } Assert.assertTrue (map.size () == 1); }

Nakon pravilnog nadjačavanja jednako () i hashCode (), Heap Memory za isti program izgleda ovako:

Drugi je primjer korištenja ORM alata poput Hibernate, koji koristi jednako () i hashCode () metode za analizu objekata i njihovo spremanje u predmemoriju.

Šanse za curenje memorije prilično su velike ako se ove metode ne nadjačaju jer Hibernate tada ne bi mogao uspoređivati ​​objekte i ispunio bi svoju predmemoriju duplikatima objekata.

Kako to spriječiti?

  • U pravilu, prilikom definiranja novih entiteta, uvijek nadjačajte jednako () i hashCode () metode
  • Nije dovoljno samo poništiti, već se i ove metode moraju nadjačati na optimalan način

Za više informacija posjetite naše vodiče Generiraj jednako () i hashCode () s Eclipseom i Vodičem za hashCode () na Javi.

3.4. Unutarnji razredi koji upućuju na vanjske razrede

To se događa u slučaju nestatičnih unutarnjih klasa (anonimnih klasa). Za inicijalizaciju ove unutarnje klase uvijek zahtijevaju primjerak klase koja obuhvaća.

Svaka nestatična unutarnja klasa prema zadanim postavkama ima implicitnu referencu na svoju klasu koja sadrži. Ako u svojoj aplikaciji koristimo ovaj objekt unutarnje klase, tada čak i nakon što objekt koji sadrži klasu izađe iz opsega, neće se sakupljati smeće.

Razmotrimo klasu koja sadrži referencu na puno glomaznih predmeta i ima nestatičnu unutarnju klasu. Sada kada kreiramo objekt samo unutarnje klase, model memorije izgleda ovako:

Međutim, ako samo deklariramo unutarnju klasu kao statičku, tada isti model memorije izgleda ovako:

To se događa zato što unutarnji objekt klase implicitno sadrži referencu na vanjski objekt klase, čineći ga time nevažećim kandidatom za odvoz smeća. Isto se događa u slučaju anonimnih predavanja.

Kako to spriječiti?

  • Ako unutarnja klasa ne treba pristup sadržanim članovima klase, razmislite o tome da je pretvorite u statički razred

3.5. Kroz finalizirati () Metode

Korištenje finalizatora još je jedan izvor potencijalnih problema s curenjem memorije. Kad god finalizirati () metoda je nadjačana objekti te klase ne skupljaju se odmah. Umjesto toga, GC ih stavlja u red za finalizaciju, što se događa kasnije.

Uz to, ako je kod napisan u finalizirati () metoda nije optimalna i ako red za finalizaciju ne može pratiti Java sakupljač smeća, prije ili kasnije naša je aplikacija predodređena da ispuni OutOfMemoryError.

Da bismo to demonstrirali, uzmimo u obzir da imamo klasu za koju smo nadjačali finalizirati () i da za izvođenje metode treba malo vremena. Kada velik broj objekata ove klase prikupi smeće, onda u VisualVM-u to izgleda kao:

Međutim, ako samo uklonimo nadjačano finalizirati () metodom, tada isti program daje sljedeći odgovor:

Kako to spriječiti?

  • Uvijek bismo trebali izbjegavati finalizatore

Za više detalja o finalizirati (), pročitajte odjeljak 3 (Izbjegavanje finalizatora) u našem Vodiču za finaliziranje metode u Javi.

3.6. Interniran Žice

Java Niz pool je prošao kroz veliku promjenu u Javi 7 kada je prebačen iz PermGena u HeapSpace. Ali za programe koji rade na verziji 6 i starijim, trebali bismo biti pažljiviji kada radimo s velikim Žice.

Ako čitamo ogromnu masivu Niz objekt i poziv pripravnik () na tom objektu, zatim ide u spremište nizova, koje se nalazi u PermGenu (trajna memorija) i tamo će ostati sve dok naša aplikacija radi. To blokira memoriju i stvara veliko curenje memorije u našoj aplikaciji.

PermGen za ovaj slučaj u JVM 1.6 izgleda ovako u VisualVM-u:

Suprotno tome, u metodi, ako samo čitamo niz iz datoteke i ne interniramo je, PermGen izgleda ovako:

Kako to spriječiti?

  • Najjednostavniji način rješavanja ovog problema je nadogradnjom na najnoviju Javinu verziju jer se spremište nizova premješta u HeapSpace od Jave verzije 7 nadalje.
  • Ako se radi na velikim Žice, povećajte veličinu prostora PermGen kako biste izbjegli potencijal OutOfMemoryErrors:
    -XX: MaxPermSize = 512m

3.7. Koristeći ThreadLocals

ThreadLocal (detaljno raspravljeno u Uvodu u ThreadLocal u Java tutorialu) je konstrukcija koja nam daje mogućnost izoliranja stanja na određenu nit i tako nam omogućuje postizanje sigurnosti niti.

Kada koristite ovu konstrukciju, svaka nit sadržavat će implicitnu referencu na svoju kopiju a ThreadLocal varijabla i održat će vlastitu kopiju, umjesto da dijeli resurs u više niti, sve dok je nit živa.

Unatoč svojim prednostima, upotreba ThreadLocal varijable kontroverzna je jer je neslavna po uvođenje curenja memorije ako se ne koristi pravilno. Joshua Bloch jednom je komentirao lokalnu upotrebu niti:

„Neuredna upotreba spremišta niti u kombinaciji s neurednom upotrebom lokalnih niti može prouzročiti nenamjerno zadržavanje objekata, kao što je primijećeno na mnogim mjestima. Ali prebacivanje krivnje na lokalne stanovnike nije opravdano. "

Memorija curi s ThreadLocals

ThreadLocals bi trebali biti smeće prikupljeno nakon što nit za držanje više ne bude živa. Ali problem nastaje kada ThreadLocals koriste se zajedno sa modernim poslužiteljima aplikacija.

Suvremeni aplikacijski poslužitelji koriste skup niti za obradu zahtjeva umjesto stvaranja novih (na primjer Izvršitelj u slučaju Apache Tomcat). Štoviše, oni također koriste zasebni učitavač razreda.

Budući da skupovi niti na poslužiteljima aplikacija rade na konceptu ponovne upotrebe niti, oni se nikada ne prikupljaju smeće - umjesto toga, ponovno se koriste za posluživanje drugog zahtjeva.

Ako bilo koja klasa kreira a ThreadLocal varijablu, ali je eksplicitno ne uklanja, tada će kopija tog objekta ostati radniku Nit čak i nakon što je web aplikacija zaustavljena, čime se sprječava prikupljanje smeća.

Kako to spriječiti?

  • Čišćenje je dobra praksa ThreadLocals kad se više ne koriste - ThreadLocals pružiti ukloniti() metoda koja uklanja vrijednost trenutne niti za ovu varijablu
  • Nemojte koristiti ThreadLocal.set (null) za brisanje vrijednosti - to zapravo ne briše vrijednost, već će umjesto toga potražiti Karta povezan s trenutnom niti i postavi par ključ / vrijednost kao trenutnu nit i null odnosno
  • Još je bolje razmotriti ThreadLocal kao resurs koji treba zatvoriti u a konačno blokirajte samo kako biste bili sigurni da je uvijek zatvoren, čak i u slučaju iznimke:
    isprobajte {threadLocal.set (System.nanoTime ()); // ... daljnja obrada} napokon {threadLocal.remove (); }

4. Ostale strategije za rješavanje curenja memorije

Iako ne postoji jedinstveno rješenje za sve kada se radi o curenju memorije, postoje neki načini na koje možemo smanjiti ta curenja.

4.1. Omogući profiliranje

Java profilatori alati su koji nadziru i dijagnosticiraju curenje memorije kroz aplikaciju. Oni analiziraju što se interno događa u našoj aplikaciji - na primjer, kako se dodjeljuje memorija.

Koristeći profilere, možemo usporediti različite pristupe i pronaći područja u kojima možemo optimalno koristiti svoje resurse.

Javu VisualVM koristili smo kroz odjeljak 3 ovog vodiča. Molimo pogledajte naš Vodič za Java profilere kako biste saznali više o različitim vrstama profilera, poput Mission Control, JProfiler, YourKit, Java VisualVM i Netbeans Profiler.

4.2. Opsežno sakupljanje smeća

Omogućujući detaljno prikupljanje smeća, pratimo detaljan trag GC-a. Da bismo to omogućili, u našu JVM konfiguraciju moramo dodati sljedeće:

-verbozna: gc

Dodavanjem ovog parametra možemo vidjeti detalje onoga što se događa unutar GC-a:

4.3. Upotrijebite referentne objekte kako biste izbjegli curenje memorije

Također se možemo poslužiti referentnim objektima u Javi koji dolaze s ugrađenim softverom java.lang.ref paket za rješavanje curenja memorije. Koristeći java.lang.ref paket, umjesto da izravno upućujemo na objekte, koristimo posebne reference na objekte koji im omogućuju lako sakupljanje smeća.

Referentni redovi osmišljeni su tako da nas upoznaju s radnjama koje obavlja Sakupljač smeća. Za više informacija pročitajte Soft Reference u vodiču za Java Baeldung, posebno odjeljak 4.

4.4. Upozorenja o propuštanju memorije Eclipse

Za projekte na JDK 1.5 i novijim verzijama, Eclipse prikazuje upozorenja i pogreške kad god naiđe na očite slučajeve curenja memorije. Dakle, kada razvijamo Eclipse, možemo redovito posjećivati ​​karticu "Problemi" i biti oprezniji u vezi s upozorenjima na curenje memorije (ako postoje):

4.5. Benchmarking

Izvođenjem mjerila možemo mjeriti i analizirati izvedbu Java koda. Na taj način možemo usporediti izvedbu alternativnih pristupa za obavljanje istog zadatka. To nam može pomoći da odaberemo bolji pristup i može nam pomoći da sačuvamo pamćenje.

Za više informacija o benčmarkingu, molimo vas da prijeđete na naš vodič za Microbenchmarking s Javom.

4.6. Pregledi kodova

Napokon, uvijek imamo klasični, old-school način jednostavnog prolaska kroz kod.

U nekim slučajevima čak i ova trivijalna metoda može pomoći u uklanjanju nekih uobičajenih problema s curenjem memorije.

5. Zaključak

Laički rečeno, curenje memorije možemo smatrati bolešću koja pogoršava performanse naše aplikacije blokirajući vitalne memorijske resurse. I kao i sve druge bolesti, ako se ne izliječe, s vremenom može rezultirati fatalnim padovima aplikacije.

Curenje memorije teško je riješiti, a njihovo pronalaženje zahtijeva složeno majstorstvo i zapovijedanje jezikom Java. Dok se bavimo curenjem memorije, ne postoji jedinstveno rješenje, jer se curenje može dogoditi kroz širok raspon različitih događaja.

Međutim, ako pribjegnemo najboljim praksama i redovito izvodimo rigorozne korake i profiliranje koda, tada možemo smanjiti rizik od curenja memorije u našoj aplikaciji.

Kao i uvijek, isječci koda koji se koriste za generiranje VisualVM odgovora prikazanih u ovom vodiču dostupni su na GitHubu.