Forståelse af hukommelseslækage i Java

1. Introduktion

En af de grundlæggende fordele ved Java er den automatiserede hukommelsesstyring ved hjælp af den indbyggede Garbage Collector (eller GC for kort). GC tager implicit sig af tildeling og frigørelse af hukommelse og er således i stand til at håndtere størstedelen af ​​hukommelseslækageproblemerne.

Mens GC effektivt håndterer en god del af hukommelsen, garanterer det ikke en idiotsikker løsning på hukommelseslækage. GC er ret smart, men ikke fejlfri. Hukommelseslækager kan stadig snige sig selv i applikationer fra en samvittighedsfuld udvikler.

Der kan stadig være situationer, hvor applikationen genererer et betydeligt antal overflødige objekter, hvilket udtømmer vigtige hukommelsesressourcer, hvilket undertiden resulterer i, at hele applikationen fejler.

Hukommelseslækage er et ægte problem i Java. I denne vejledning ser vi hvad de potentielle årsager til hukommelseslækager er, hvordan man genkender dem under kørsel, og hvordan man håndterer dem i vores applikation.

2. Hvad er hukommelseslækage?

En hukommelseslækage er en situation når der er genstande til stede i bunken, som ikke længere bruges, men affaldssamleren ikke er i stand til at fjerne dem fra hukommelsen og således vedligeholdes de unødigt.

En hukommelseslækage er dårlig, fordi den blokerer hukommelsesressourcer og nedbryder systemets ydeevne over tid. Og hvis det ikke behandles, vil applikationen til sidst udtømme sine ressourcer og til sidst ende med en fatal java.lang.OutOfMemoryError.

Der er to forskellige typer objekter, der findes i Heap-hukommelse - der henvises til og ikke refereres til. Henviste objekter er dem, der stadig har aktive referencer i applikationen, mens ikke-referencede objekter ikke har nogen aktive referencer.

Affaldssamleren fjerner ikke-refererede genstande med jævne mellemrum, men den samler aldrig de objekter, der stadig refereres til. Det er her, hukommelseslækage kan forekomme:

Symptomer på hukommelseslækage

  • Alvorlig ydelsesforringelse, når applikationen kører kontinuerligt i lang tid
  • OutOfMemoryError bunkefejl i applikationen
  • Spontan og mærkelig applikation går ned
  • Applikationen løber lejlighedsvis tør for forbindelsesobjekter

Lad os se nærmere på nogle af disse scenarier og hvordan vi skal håndtere dem.

3. Typer af hukommelseslækage i Java

I ethvert program kan hukommelseslækage forekomme af flere årsager. I dette afsnit diskuterer vi de mest almindelige.

3.1. Hukommelseslækage igennem statisk Felter

Det første scenario, der kan forårsage en potentiel hukommelseslækage, er tung brug af statisk variabler.

I Java, statisk felter har en levetid, der normalt svarer til hele den løbende applikations levetid (med mindre ClassLoader bliver berettiget til affaldsindsamling).

Lad os oprette et simpelt Java-program, der udfylder en statiskListe:

offentlig klasse StaticTest {offentlig statisk liste liste = ny ArrayList (); public void populateList () {for (int i = 0; i <10000000; i ++) {list.add (Math.random ()); } Log.info ("Fejlfindingspunkt 2"); } offentlig statisk ugyldig hoved (String [] args) {Log.info ("Debug Point 1"); ny StaticTest (). populateList (); Log.info ("Fejlfindingspunkt 3"); }}

Hvis vi nu analyserer Heap-hukommelsen under denne programudførelse, vil vi se, at mellem forventningspunkter 1 og 2, som forventet, steg heap-hukommelsen.

Men når vi forlader populateList () metode ved fejlfindingspunkt 3, bunkehukommelsen er endnu ikke indsamlet skrald som vi kan se i dette VisualVM-svar:

Men i ovenstående program, i linje nummer 2, hvis vi bare slipper nøgleordet statisk, så vil det medføre en drastisk ændring af hukommelsesforbruget, dette Visual VM-svar viser:

Den første del indtil fejlfindingspunktet er næsten den samme som hvad vi opnåede i tilfælde af statisk. Men denne gang efter vi forlader populateList () metode, hele hukommelsen på listen er indsamlet affald, fordi vi ikke har nogen henvisning til det.

Derfor er vi nødt til at være meget opmærksomme på vores brug af statisk variabler. Hvis samlinger eller store objekter erklæres som statisk, så forbliver de i hukommelsen i hele applikationens levetid og blokerer dermed den vitale hukommelse, der ellers kunne bruges andetsteds.

Hvordan forhindres det?

  • Minimer brugen af statisk variabler
  • Når du bruger singletoner, skal du stole på en implementering, der dovent laver objektet i stedet for ivrigt at indlæse

3.2. Gennem lukkede ressourcer

Når vi opretter en ny forbindelse eller åbner en stream, tildeler JVM hukommelse til disse ressourcer. Et par eksempler inkluderer databaseforbindelser, inputstrømme og sessionsobjekter.

At glemme at lukke disse ressourcer kan blokere hukommelsen og dermed holde dem utilgængelige for GC. Dette kan endda ske i tilfælde af en undtagelse, der forhindrer programudførelsen i at nå den erklæring, der håndterer koden for at lukke disse ressourcer.

I begge tilfælde, den åbne forbindelse, der er tilbage fra ressourcerne, forbruger hukommelse, og hvis vi ikke behandler dem, kan de forringe ydeevnen og kan endda resultere i OutOfMemoryError.

Hvordan forhindres det?

  • Brug altid langt om længe blokere for at lukke ressourcer
  • Koden (selv i langt om længe blok), der lukker ressourcerne, bør ikke i sig selv have nogen undtagelser
  • Når vi bruger Java 7+, kan vi gøre brug af prøve-med ressourcer blok

3.3. Upassende lige med() og hashCode () Implementeringer

Når man definerer nye klasser, skriver et meget almindeligt tilsyn ikke rigtige tilsidesatte metoder til lige med() og hashCode () metoder.

HashSet og HashMap brug disse metoder i mange operationer, og hvis de ikke tilsidesættes korrekt, kan de blive en kilde til potentielle hukommelseslækageproblemer.

Lad os tage et eksempel på et trivielt Person klasse og bruge den som en nøgle i en HashMap:

offentlig klasse Person {offentligt strengnavn; offentlig person (strengnavn) {this.name = navn; }}

Nu indsætter vi duplikat Person genstande i en Kort der bruger denne nøgle.

Husk at en Kort kan ikke indeholde duplikatnøgler:

@Test offentlig ugyldighed givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak () {Map map = new HashMap (); for (int i = 0; i <100; i ++) {map.put (ny person ("jon"), 1); } Assert.assertFalse (map.size () == 1); }

Her bruger vi Person som en nøgle. Siden Kort tillader ikke duplikatnøgler, de mange kopier Person objekter, som vi har indsat som en nøgle, bør ikke øge hukommelsen.

Men da vi ikke har defineret korrekt lige med() metode samles de dobbelte objekter op og øger hukommelsen, derfor ser vi mere end et objekt i hukommelsen. Heap Memory i VisualVM til dette ligner:

Imidlertid, hvis vi havde tilsidesat lige med() og hashCode () metoder korrekt, så ville der kun eksistere en Person modstand i dette Kort.

Lad os se på ordentlige implementeringer af lige med() og hashCode () for vores Person klasse:

offentlig klasse Person {offentligt strengnavn; offentlig person (strengnavn) {this.name = navn; } @ Override offentlige boolske lig (Objekt o) {hvis (o == dette) returnerer sandt; hvis (! (o eksempel af person)) {returner falsk; } Person person = (Person) o; returner person.name.equals (navn); } @ Override public int hashCode () {int result = 17; resultat = 31 * resultat + navn.hashCode (); returresultat }}

Og i dette tilfælde ville følgende påstande være sandt:

@Test offentlig ugyldighed givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak () {Map map = new HashMap (); for (int i = 0; i <2; i ++) {map.put (ny person ("jon"), 1); } Assert.assertTrue (map.size () == 1); }

Efter korrekt tilsidesættelse lige med() og hashCode (), ser Heap Memory til det samme program ud:

Et andet eksempel er at bruge et ORM-værktøj som Hibernate, som bruger lige med() og hashCode () metoder til at analysere objekterne og gemme dem i cachen.

Chancerne for hukommelseslækage er ret store, hvis disse metoder ikke tilsidesættes fordi dvaletilstand ikke ville være i stand til at sammenligne objekter og ville udfylde dens cache med duplikerede objekter.

Hvordan forhindres det?

  • Som en tommelfingerregel skal du altid tilsidesætte, når du definerer nye enheder lige med() og hashCode () metoder
  • Det er ikke bare nok til at tilsidesætte, men disse metoder skal også tilsidesættes på en optimal måde

For mere information, besøg vores tutorials Generer lige med() og hashCode () med formørkelse og vejledning til hashCode () i Java.

3.4. Indre klasser, der refererer til ydre klasser

Dette sker i tilfælde af ikke-statiske indre klasser (anonyme klasser). Til initialisering kræver disse indre klasser altid en forekomst af den omsluttende klasse.

Hver ikke-statisk indre klasse har som standard en implicit henvisning til den indeholdende klasse. Hvis vi bruger dette indre klasses objekt i vores applikation, så selv efter at vores indeholdende klasses genstand er uden for anvendelsesområdet, indsamles det ikke skrald.

Overvej en klasse, der indeholder henvisningen til mange voluminøse genstande og har en ikke-statisk indre klasse. Når vi nu opretter et objekt af kun den indre klasse, ser hukommelsesmodellen ud:

Men hvis vi bare erklærer den indre klasse som statisk, ser den samme hukommelsesmodel sådan ud:

Dette sker, fordi det indre klasseobjekt implicit indeholder en henvisning til det ydre klasseobjekt, hvilket gør det til en ugyldig kandidat til affaldsindsamling. Det samme sker i tilfælde af anonyme klasser.

Hvordan forhindres det?

  • Hvis den indre klasse ikke har brug for adgang til de indeholdende klassemedlemmer, kan du overveje at gøre det til et statisk klasse

3.5. igennem færdiggør () Metoder

Brug af finalizers er endnu en kilde til potentielle problemer med hukommelseslækage. Når en klasse ' færdiggør () metoden tilsidesættes genstande af denne klasse indsamles ikke straks affald. I stedet sætter GC dem i kø til afslutning, hvilket sker på et senere tidspunkt.

Derudover, hvis koden er skrevet ind færdiggør () metoden er ikke optimal, og hvis færdiggørelseskøen ikke kan følge med Java-affaldssamleren, er vores applikation før eller senere bestemt til at opfylde en OutOfMemoryError.

For at demonstrere dette, lad os overveje, at vi har en klasse, som vi har tilsidesat færdiggør () metode, og at metoden tager lidt tid at udføre. Når et stort antal objekter i denne klasse samles affald, ser det i VisualVM ud som:

Men hvis vi bare fjerner det tilsidesatte færdiggør () metode, så giver det samme program følgende svar:

Hvordan forhindres det?

  • Vi bør altid undgå slutbehandlere

For flere detaljer om færdiggør (), læs afsnit 3 (Undgå færdiggørere) i vores guide til færdiggørelsesmetoden i Java.

3.6. Interneret Strenge

Java Snor pool havde gennemgået en større ændring i Java 7, da den blev overført fra PermGen til HeapSpace. Men for applikationer, der fungerer på version 6 og derunder, skal vi være mere opmærksomme, når vi arbejder med store Strenge.

Hvis vi læser en enorm massiv Snor objekt, og ring praktikant () på det objekt, så går det til strengpoolen, som er placeret i PermGen (permanent hukommelse) og bliver der, så længe vores applikation kører. Dette blokerer hukommelsen og skaber en stor hukommelseslækage i vores applikation.

PermGen for denne sag i JVM 1.6 ser sådan ud i VisualVM:

I modsætning til dette, i en metode, hvis vi bare læser en streng fra en fil og ikke internerer den, ser PermGen ud som:

Hvordan forhindres det?

  • Den enkleste måde at løse dette problem er ved at opgradere til den nyeste Java-version, da String-pool flyttes til HeapSpace fra Java-version 7 og fremefter
  • Hvis du arbejder på stort Strenge, øg størrelsen på PermGen-rummet for at undgå ethvert potentiale OutOfMemoryErrors:
    -XX: MaxPermSize = 512m

3.7. Ved brug af Trådlokals

Trådlokal (diskuteret detaljeret i Introduktion til Trådlokal i Java tutorial) er en konstruktion, der giver os muligheden for at isolere tilstanden til en bestemt tråd og dermed giver os mulighed for at opnå trådsikkerhed.

Når du bruger denne konstruktion, hver tråd vil indeholde en implicit henvisning til sin kopi af en Trådlokal variabel og opretholder sin egen kopi i stedet for at dele ressourcen på tværs af flere tråde, så længe tråden er i live.

På trods af dets fordele er brugen af Trådlokal variabler er kontroversielle, da de er berygtede for at indføre hukommelseslækage, hvis de ikke bruges korrekt. Joshua Bloch kommenterede en gang tråd lokal brug:

”Slurvet brug af trådpuljer i kombination med sjusket brug af tråd lokale kan forårsage utilsigtet tilbageholdelse af objekter, som det er blevet bemærket mange steder. Men det er uberettiget at lægge skylden på tråden lokale. ”

Hukommelse lækker med TrådLokaler

TrådLokaler formodes at blive indsamlet skrald, når holdetråden ikke længere er i live. Men problemet opstår når TrådLokaler bruges sammen med moderne applikationsservere.

Moderne applikationsservere bruger en pool af tråde til at behandle anmodninger i stedet for at oprette nye (f.eks Eksekutor i tilfælde af Apache Tomcat). Desuden bruger de også en separat klasselæsser.

Da trådpuljer i applikationsservere arbejder på begrebet trådgenbrug, indsamles de aldrig skrald - i stedet genbruges de til at tjene en anden anmodning.

Nu, hvis nogen klasse opretter en Trådlokal variabel, men fjerner det ikke eksplicit, så forbliver en kopi af objektet hos arbejdstageren Tråd selv efter at webapplikationen er stoppet, hvilket forhindrer genstanden i at blive indsamlet skrald.

Hvordan forhindres det?

  • Det er en god praksis at rydde op TrådLokaler når de ikke længere bruges - TrådLokaler give den fjerne() metode, der fjerner den aktuelle tråds værdi for denne variabel
  • Brug ikke ThreadLocal.set (null) for at rydde værdien - det rydder faktisk ikke værdien, men ser i stedet op Kort tilknyttet den aktuelle tråd, og indstil nøgleværdipar som den aktuelle tråd og nul henholdsvis
  • Det er endnu bedre at overveje Trådlokal som en ressource, der skal lukkes i en langt om længe bloker bare for at sikre, at det altid er lukket, selv i tilfælde af en undtagelse:
    prøv {threadLocal.set (System.nanoTime ()); // ... yderligere behandling} endelig {threadLocal.remove (); }

4. Andre strategier til håndtering af hukommelseslækager

Selvom der ikke findes nogen løsning, der passer til alle, når der er tale om hukommelseslækage, er der nogle måder, hvorpå vi kan minimere disse lækager.

4.1. Aktivér profilering

Java-profiler er værktøjer, der overvåger og diagnosticerer hukommelseslækage gennem applikationen. De analyserer, hvad der foregår internt i vores applikation - for eksempel hvordan hukommelse fordeles.

Ved hjælp af profiler kan vi sammenligne forskellige tilgange og finde områder, hvor vi optimalt kan bruge vores ressourcer.

Vi har brugt Java VisualVM gennem sektion 3 i denne vejledning. Se vores guide til Java-profiler for at lære om forskellige typer profiler som Mission Control, JProfiler, YourKit, Java VisualVM og Netbeans Profiler.

4.2. Verbose affaldssamling

Ved at aktivere detaljeret indsamling af affald sporer vi detaljeret spor af GC. For at aktivere dette skal vi tilføje følgende til vores JVM-konfiguration:

-verbose: gc

Ved at tilføje denne parameter kan vi se detaljerne om, hvad der sker inden for GC:

4.3. Brug referenceobjekter for at undgå hukommelseslækager

Vi kan også ty til referenceobjekter i Java, der følger med indbygget java.lang.ref pakke til håndtering af hukommelseslækage. Ved brug af java.lang.ref i stedet for direkte at henvise til objekter, bruger vi specielle referencer til objekter, der gør det let for dem at blive indsamlet skrald.

Referencekøer er designet til at gøre os opmærksomme på handlinger udført af Garbage Collector. For mere information, læs bløde referencer i Java Baeldung-tutorial, specifikt afsnit 4.

4.4. Advarsler om formørkelse af hukommelse

For projekter på JDK 1.5 og derover viser Eclipse advarsler og fejl, når den støder på åbenlyse tilfælde af hukommelseslækage. Så når vi udvikler os i Eclipse, kan vi regelmæssigt besøge fanen "Problemer" og være mere opmærksomme på advarsler om hukommelseslækage (hvis nogen):

4.5. Benchmarking

Vi kan måle og analysere Java-kodens ydeevne ved at udføre benchmarks. På denne måde kan vi sammenligne udførelsen af ​​alternative tilgange for at udføre den samme opgave. Dette kan hjælpe os med at vælge en bedre tilgang og kan hjælpe os med at spare hukommelse.

For mere information om benchmarking, bedes du gå over til vores Microbenchmarking med Java-vejledning.

4.6. Kode anmeldelser

Endelig har vi altid den klassiske, gamle skole måde at lave en simpel kodegennemgang på.

I nogle tilfælde kan selv denne trivielle metode hjælpe med at eliminere nogle almindelige hukommelseslækageproblemer.

5. Konklusion

I lægmandssprog kan vi tænke på hukommelseslækage som en sygdom, der forringer vores applikations præstationer ved at blokere vitale hukommelsesressourcer. Og som alle andre sygdomme, hvis det ikke helbredes, kan det resultere i fatale applikationsnedbrud over tid.

Hukommelseslækage er vanskeligt at løse, og at finde dem kræver indviklet mestring og kommando over Java-sproget. Mens der beskæftiger sig med hukommelseslækage, er der ingen løsning, der passer til alle, da lækager kan forekomme gennem en lang række forskellige begivenheder.

Men hvis vi anvender bedste praksis og regelmæssigt udfører streng gennemgang af kode og profilering, kan vi minimere risikoen for hukommelseslækage i vores applikation.

Som altid er de kodestykker, der bruges til at generere VisualVM-svarene, der er afbildet i denne vejledning, tilgængelige på GitHub.