Forstå minnelekkasjer i Java

1. Introduksjon

En av de viktigste fordelene med Java er automatisk minneadministrasjon ved hjelp av den innebygde Garbage Collector (eller GC for kort). GC tar implisitt seg av å tildele og frigjøre minne og er dermed i stand til å håndtere de fleste av minnelekkasjeproblemene.

Mens GC effektivt håndterer en god del minne, garanterer det ikke en idiotsikker løsning på minne som lekker. GC er ganske smart, men ikke feilfri. Minnelekkasjer kan fortsatt snike seg selv i applikasjoner fra en samvittighetsfull utvikler.

Det kan fortsatt være situasjoner der applikasjonen genererer et betydelig antall overflødige gjenstander, og dermed tømmer viktige minnesressurser, noen ganger resulterer det i at hele applikasjonen mislykkes.

Minnelekkasjer er et ekte problem i Java. I denne opplæringen får vi se hva de potensielle årsakene til minnelekkasjer er, hvordan man kan gjenkjenne dem ved kjøretid, og hvordan vi skal håndtere dem i applikasjonen vår.

2. Hva er en minnelekkasje

En minnelekkasje er en situasjon når det er gjenstander i haugen som ikke lenger brukes, men søppeloppsamleren ikke klarer å fjerne dem fra minnet og dermed vedlikeholdes de unødvendig.

En minnelekkasje er dårlig fordi den blokkerer minnesressurser og forringer systemets ytelse over tid. Hvis den ikke blir behandlet, vil søknaden til slutt tømme ressursene og til slutt avslutte med en dødelig utgang java.lang.OutOfMemoryError.

Det er to forskjellige typer objekter som ligger i Heap-minne - referert og ikke referert. Refererte objekter er de som fremdeles har aktive referanser i applikasjonen, mens ikke-refererte objekter ikke har noen aktive referanser.

Søppeloppsamleren fjerner ikke refererte gjenstander med jevne mellomrom, men den samler aldri gjenstandene som det fortsatt er referert til. Det er her minnelekkasjer kan oppstå:

Symptomer på minnelekkasje

  • Alvorlig ytelsesforringelse når applikasjonen kjører i lang tid
  • OutOfMemoryError haugfeil i applikasjonen
  • Spontan og merkelig applikasjon krasjer
  • Søknaden går noen ganger tom for tilkoblingsobjekter

La oss se nærmere på noen av disse scenariene og hvordan vi skal håndtere dem.

3. Typer minnelekkasjer i Java

I alle applikasjoner kan minnelekkasjer oppstå av flere grunner. I denne delen vil vi diskutere de vanligste.

3.1. Minne Lekkasje Gjennom statisk Enger

Det første scenariet som kan forårsake en potensiell minnelekkasje er tung bruk av statisk variabler.

I Java, statisk felt har en levetid som vanligvis samsvarer med hele levetiden til den kjørende applikasjonen (med mindre ClassLoader blir kvalifisert for søppeloppsamling).

La oss lage et enkelt Java-program som fyller et statiskListe:

public class StaticTest {public static List list = new ArrayList (); public void populateList () {for (int i = 0; i <10000000; i ++) {list.add (Math.random ()); } Log.info ("Feilsøkingspunkt 2"); } public static void main (String [] args) {Log.info ("Feilsøkingspunkt 1"); ny StaticTest (). populateList (); Log.info ("Feilsøkingspunkt 3"); }}

Nå hvis vi analyserer Heap-minnet under dette programutførelsen, så ser vi at mellom feilsøkingspunkt 1 og 2, som forventet, økte heapminnet.

Men når vi forlater populateList () metode ved feilsøkingspunkt 3, haugminnet er ennå ikke samlet søppel som vi kan se i dette VisualVM-svaret:

Imidlertid, i det ovennevnte programmet, på linje nummer 2, hvis vi bare slipper nøkkelordet statisk, så vil det føre til en drastisk endring i minnebruk, viser dette Visual VM-svaret:

Den første delen til feilsøkingspunktet er nesten den samme som vi fikk i tilfelle statisk. Men denne gangen etter at vi forlater populateList () metode, alt minnet i listen er søppel samlet fordi vi ikke har noen referanse til det.

Derfor må vi følge nøye med på bruken av statisk variabler. Hvis samlinger eller store gjenstander blir erklært som statisk, så forblir de i minnet gjennom hele programmets levetid, og blokkerer dermed det vitale minnet som ellers kan brukes andre steder.

Hvordan forhindre det?

  • Minimer bruken av statisk variabler
  • Når du bruker singletons, må du stole på en implementering som lat laster objektet i stedet for ivrig lasting

3.2. Gjennom lukkede ressurser

Hver gang vi oppretter en ny forbindelse eller åpner en strøm, tildeler JVM minne for disse ressursene. Noen få eksempler inkluderer databasekoblinger, inngangsstrømmer og sesjonsobjekter.

Å glemme å lukke disse ressursene kan blokkere minnet og dermed holde dem utenfor GCs rekkevidde. Dette kan til og med skje i tilfelle et unntak som forhindrer at programutførelsen når uttalelsen som håndterer koden for å lukke disse ressursene.

I begge tilfeller, den åpne forbindelsen som er igjen fra ressursene, bruker minne, og hvis vi ikke takler dem, kan de forringe ytelsen og til og med føre til OutOfMemoryError.

Hvordan forhindre det?

  • Bruk alltid endelig blokker for å lukke ressurser
  • Koden (selv i endelig blokk) som stenger ressursene, bør ikke i seg selv ha noen unntak
  • Når du bruker Java 7+, kan vi benytte oss av prøve-med ressursblokk

3.3. Upassende er lik() og hashCode () Implementeringer

Når man definerer nye klasser, skriver ikke en veldig vanlig tilsyn ikke riktige overstyrte metoder for er lik() og hashCode () metoder.

HashSet og HashMap bruk disse metodene i mange operasjoner, og hvis de ikke blir overstyrt riktig, kan de bli en kilde for potensielle minnelekkasjeproblemer.

La oss ta et eksempel på et trivielt Person klasse og bruke den som en nøkkel i en HashMap:

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

Nå setter vi inn duplikat Person gjenstander i en Kart som bruker denne nøkkelen.

Husk at a Kart kan ikke inneholde dupliserte nøkler:

@Test offentlig ugyldig 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 bruker vi Person som en nøkkel. Siden Kart tillater ikke dupliserte nøkler, de mange duplikatene Person objekter som vi har satt inn som en nøkkel, bør ikke øke minnet.

Men siden vi ikke har definert riktig er lik() metoden, hoper duplikatobjektene seg opp og øker minnet, det er derfor vi ser mer enn ett objekt i minnet. Heap Memory i VisualVM for dette ser ut som:

Derimot, hvis vi hadde overstyrt er lik() og hashCode () metoder riktig, da ville det bare eksistere en Person innvender i dette Kart.

La oss ta en titt på riktig implementering av er lik() og hashCode () for vår Person klasse:

offentlig klasse Person {offentlig Strengnavn; offentlig person (strengnavn) {this.name = navn; } @ Override offentlig boolsk er lik (Objekt o) {hvis (o == dette) returnerer sant; if (! (o eksempel of Person)) {return false; } Personperson = (Person) o; returner person.name.equals (navn); } @ Override public int hashCode () {int result = 17; resultat = 31 * resultat + navn.hashCode (); returresultat; }}

Og i dette tilfellet vil følgende påstander være sanne:

@Test offentlig ugyldig 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); }

Etter riktig overstyring er lik() og hashCode (), ser Heap Memory for det samme programmet ut:

Et annet eksempel er å bruke et ORM-verktøy som Hibernate, som bruker er lik() og hashCode () metoder for å analysere objektene og lagre dem i hurtigbufferen.

Sjansene for minnelekkasje er ganske store hvis disse metodene ikke overstyres fordi dvalemodus ikke ville være i stand til å sammenligne objekter og ville fylle hurtigbufferen med dupliserte objekter.

Hvordan forhindre det?

  • Når du definerer nye enheter, må du alltid overstyre den er lik() og hashCode () metoder
  • Det er ikke bare nok til å overstyre, men disse metodene må også overstyres på en optimal måte

For mer informasjon, besøk tutorials Generate er lik() og hashCode () med formørkelse og guide til hashCode () i Java.

3.4. Indre klasser som refererer til ytre klasser

Dette skjer i tilfelle ikke-statiske indre klasser (anonyme klasser). For initialisering krever disse indre klassene alltid en forekomst av den omsluttende klassen.

Hver ikke-statisk indre klasse har som standard en implisitt referanse til den inneholder klassen. Hvis vi bruker denne indre klassens objekt i applikasjonen vår, da selv etter at vårt holdende klasseobjekt går utenfor omfanget, blir det ikke søppel samlet inn.

Tenk på en klasse som inneholder referansen til mange klumpete gjenstander og har en ikke-statisk indre klasse. Nå når vi lager et objekt av bare den indre klassen, ser minnemodellen ut som:

Men hvis vi bare erklærer den indre klassen som statisk, ser den samme minnemodellen slik ut:

Dette skjer fordi det indre klasseobjektet implisitt har en referanse til det ytre klasseobjektet, og dermed gjør det til en ugyldig kandidat for søppeloppsamling. Det samme skjer når det gjelder anonyme klasser.

Hvordan forhindre det?

  • Hvis den indre klassen ikke trenger tilgang til de klassemedlemmene som inneholder, bør du vurdere å gjøre den til en statisk klasse

3.5. Gjennom fullfør () Metoder

Bruk av sluttbehandlere er nok en kilde til potensielle minnelekkasjeproblemer. Når en klasse fullfør () metoden blir overstyrt, da gjenstander i den klassen blir ikke søppel samlet inn umiddelbart. I stedet stiller GC dem i kø for sluttbehandling, noe som skjer på et senere tidspunkt.

I tillegg, hvis koden er skrevet inn fullfør () metoden er ikke optimal, og hvis sluttbehandlerkøen ikke kan følge med Java-søppeloppsamleren, er søknaden vår før eller senere bestemt til å møte en OutOfMemoryError.

For å demonstrere dette, la oss vurdere at vi har en klasse som vi har overstyrt fullfør () metode og at metoden tar litt tid å utføre. Når et stort antall gjenstander i denne klassen samler søppel, ser det ut i VisualVM slik:

Men hvis vi bare fjerner det overstyrte fullfør () metode, gir det samme programmet følgende svar:

Hvordan forhindre det?

  • Vi bør alltid unngå sluttbehandlere

For mer informasjon om fullfør (), les avsnitt 3 (Unngå sluttbehandlere) i vår guide til å fullføre metoden i Java.

3.6. Internert Strenger

Java String pool hadde gjennomgått en stor endring i Java 7 da den ble overført fra PermGen til HeapSpace. Men for applikasjoner som fungerer på versjon 6 og nyere, bør vi være mer oppmerksomme når vi jobber med store Strenger.

Hvis vi leser en enorm massiv String objekt, og ring turnuskandidat() på det objektet, så går det til strengbassenget, som ligger i PermGen (permanent minne) og vil forbli der så lenge applikasjonen vår kjører. Dette blokkerer minnet og skaper en stor minnelekkasje i applikasjonen vår.

PermGen for dette tilfellet i JVM 1.6 ser slik ut i VisualVM:

I motsetning til dette, hvis vi bare leser en streng fra en fil og ikke internerer den i en metode, ser PermGen ut som:

Hvordan forhindre det?

  • Den enkleste måten å løse dette problemet er ved å oppgradere til den nyeste Java-versjonen ettersom String pool flyttes til HeapSpace fra Java versjon 7 og utover
  • Hvis du arbeider med store Strenger, øke størrelsen på PermGen-rommet for å unngå potensial OutOfMemoryErrors:
    -XX: MaxPermSize = 512m

3.7. Ved hjelp av Trådlokals

Trådlokal (diskutert i detalj i Introduksjon til Trådlokal i Java tutorial) er en konstruksjon som gir oss muligheten til å isolere tilstanden til en bestemt tråd og dermed tillater oss å oppnå trådsikkerhet.

Når du bruker denne konstruksjonen, hver tråd vil inneholde en implisitt referanse til kopien av en Trådlokal variabel og vil opprettholde sin egen kopi, i stedet for å dele ressursen over flere tråder, så lenge tråden er i live.

Til tross for fordelene, bruk av Trådlokal variabler er kontroversielle, da de er beryktede for å innføre minnelekkasjer hvis de ikke brukes riktig. Joshua Bloch kommenterte en gang tråden lokal bruk:

”Slurvet bruk av trådbassenger i kombinasjon med slurvet bruk av tråd lokale kan føre til utilsiktet gjenstandsretensjon, som det er blitt bemerket mange steder. Men å legge skylden på tråden lokalbefolkningen er uberettiget. ”

Minne lekker med ThreadLocals

ThreadLocals antas å være samlet søppel når holdetråden ikke lenger lever. Men problemet oppstår når ThreadLocals brukes sammen med moderne applikasjonsservere.

Moderne applikasjonsservere bruker et utvalg av tråder til å behandle forespørsler i stedet for å opprette nye (for eksempel Leder i tilfelle Apache Tomcat). Videre bruker de også en egen klasselaster.

Siden trådbassenger i applikasjonsservere jobber med konseptet med gjenbruk av tråder, blir de aldri søppel samlet - i stedet blir de gjenbrukt for å tjene en annen forespørsel.

Nå, hvis noen klasse skaper en Trådlokal variabel, men fjerner den ikke eksplisitt, så vil en kopi av objektet ligge hos arbeideren Tråd selv etter at nettapplikasjonen er stoppet, og forhindrer dermed at gjenstanden blir samlet inn søppel.

Hvordan forhindre det?

  • Det er en god praksis å rydde opp ThreadLocals når de ikke lenger brukes - ThreadLocals gi den fjerne() metode, som fjerner gjeldende tråds verdi for denne variabelen
  • Ikke bruk ThreadLocal.set (null) for å fjerne verdien - det tømmer faktisk ikke verdien, men vil i stedet slå opp Kart tilknyttet den gjeldende tråden og angi nøkkelverdiparet som gjeldende tråd og null henholdsvis
  • Det er enda bedre å vurdere Trådlokal som en ressurs som må lukkes i en endelig blokker bare for å være sikker på at den alltid er lukket, selv i tilfelle et unntak:
    prøv {threadLocal.set (System.nanoTime ()); // ... videre behandling} til slutt {threadLocal.remove (); }

4. Andre strategier for å håndtere minnelekkasjer

Selv om det ikke finnes en løsning som passer alle sammen når det gjelder minnelekkasjer, er det noen måter vi kan minimere disse lekkasjene på.

4.1. Aktiver profilering

Java-profiler er verktøy som overvåker og diagnostiserer minnelekkasjer gjennom applikasjonen. De analyserer hva som skjer internt i applikasjonen vår - for eksempel hvordan minne tildeles.

Ved hjelp av profilere kan vi sammenligne ulike tilnærminger og finne områder der vi kan bruke ressursene våre optimalt.

Vi har brukt Java VisualVM gjennom avsnitt 3 i denne opplæringen. Ta en titt på vår guide til Java-profiler for å lære om forskjellige typer profiler, som Mission Control, JProfiler, YourKit, Java VisualVM og Netbeans Profiler.

4.2. Rikelig søppelinnsamling

Ved å aktivere detaljert søppelinnsamling sporer vi detaljert spor av GC. For å aktivere dette må vi legge til følgende i JVM-konfigurasjonen:

-verbose: gc

Ved å legge til denne parameteren kan vi se detaljene om hva som skjer i GC:

4.3. Bruk referanseobjekter for å unngå minnelekkasjer

Vi kan også ty til referanseobjekter i Java som følger med innebygd java.lang.ref pakke for å håndtere minnelekkasjer. Ved hjelp av java.lang.ref pakken, i stedet for direkte å referere til objekter, bruker vi spesielle referanser til objekter som gjør at de lett kan samles opp søppel.

Referansekøer er laget for å gjøre oss oppmerksomme på handlinger utført av søppeloppsamleren. For mer informasjon, les Soft References i Java Baeldung tutorial, spesielt avsnitt 4.

4.4. Advarsler om formørkelse av minnet

For prosjekter på JDK 1.5 og nyere viser Eclipse advarsler og feil når det støter på åpenbare tilfeller av minnelekkasjer. Så når vi utvikler oss i Eclipse, kan vi regelmessig besøke fanen "Problemer" og være mer årvåkne når det gjelder advarsler om minnelekkasje (hvis noen):

4.5. Referansemåling

Vi kan måle og analysere ytelsen til Java-koden ved å utføre referanser. På denne måten kan vi sammenligne ytelsen til alternative tilnærminger for å gjøre den samme oppgaven. Dette kan hjelpe oss med å velge en bedre tilnærming og kan hjelpe oss med å spare minne.

For mer informasjon om benchmarking, vennligst gå til Microbenchmarking with Java tutorial.

4.6. Kodevurderinger

Til slutt har vi alltid den klassiske måten å gjøre en enkel kodegjennomgang på.

I noen tilfeller kan til og med denne trivielle metoden bidra til å eliminere noen vanlige minnelekkasjeproblemer.

5. Konklusjon

I lekmannsbetingelser kan vi tenke på minnelekkasje som en sykdom som forringer applikasjonens ytelse ved å blokkere viktige minnesressurser. Og som alle andre sykdommer, hvis det ikke er kurert, kan det føre til dødelig søknadskrasj over tid.

Minnelekkasjer er vanskelig å løse, og å finne dem krever intrikat mestring og kommando over Java-språket. Mens du arbeider med minnelekkasjer, er det ingen løsning som passer alle, ettersom lekkasjer kan oppstå gjennom et bredt spekter av forskjellige begivenheter.

Imidlertid, hvis vi bruker beste fremgangsmåter og regelmessig utfører nøye kodegjennomganger og profilering, kan vi minimere risikoen for minnelekkasjer i applikasjonen vår.

Som alltid er kodebitene som brukes til å generere VisualVM-svarene som er vist i denne opplæringen, tilgjengelige på GitHub.