A Java memóriaszivárgásainak megértése

1. Bemutatkozás

A Java egyik alapvető előnye az automatizált memóriakezelés a beépített Garbage Collector (vagy GC röviden). A GC hallgatólagosan gondoskodik a memória allokációjáról és felszabadításáról, így képes kezelni a memóriaszivárgás problémáinak többségét.

Bár a GC hatékonyan kezeli a memória jó részét, nem garantálja a memória szivárgásának hibátlan megoldását. A GC elég okos, de nem hibátlan. A memóriaszivárgás még mindig lelkiismeretes fejlesztő alkalmazásaiban is felbukkanhat.

Még mindig előfordulhatnak olyan helyzetek, amikor az alkalmazás jelentős számú felesleges objektumot generál, ezáltal kimerítik a kulcsfontosságú memóriaforrásokat, ami néha az egész alkalmazás hibáját eredményezi.

A memóriaszivárgások valódi problémát jelentenek a Java-ban. Ebben az oktatóanyagban meglátjuk mik lehetnek a memóriaszivárgások lehetséges okai, hogyan lehet őket futás közben felismerni és hogyan kell kezelni őket alkalmazásunkban.

2. Mi a memóriaszivárgás

A memóriaszivárgás egy helyzet amikor a kupacban vannak olyan tárgyak, amelyeket már nem használnak, de a szemétszedő képtelen eltávolítani őket a memóriából és így szükségtelenül fenntartják őket.

A memóriaszivárgás rossz, mert az blokkolja a memória erőforrásait és idővel rontja a rendszer teljesítményét. És ha nem foglalkoznak vele, az alkalmazás végül kimeríti erőforrásait, és végül végzettel zárul java.lang.OutOfMemoryError.

Két különböző típusú objektum található a kupac memóriában - hivatkozott és hivatkozatlan. Hivatkozott objektumok azok, akiknek még mindig vannak aktív referenciái az alkalmazáson belül, míg a hivatkozatlan objektumok nem rendelkeznek aktív referenciákkal.

A szemétgyűjtő rendszeresen eltávolítja a hivatkozatlan objektumokat, de soha nem gyűjti azokat a tárgyakat, amelyekre még hivatkoznak. Itt fordulhatnak elő memóriaszivárgások:

A memóriaszivárgás tünetei

  • Súlyos teljesítményromlás, ha az alkalmazás hosszú ideig folyamatosan fut
  • OutOfMemoryError halom hiba az alkalmazásban
  • Spontán és furcsa alkalmazás összeomlik
  • Az alkalmazás alkalmanként elfogynak a kapcsolatobjektumok

Nézzük meg közelebbről ezeket a forgatókönyveket, és hogyan kezeljük őket.

3. A Java memóriaszivárgásainak típusai

Bármely alkalmazásban a memória szivároghat számos okból. Ebben a részben a leggyakoribbakat tárgyaljuk.

3.1. A memória szivárog statikus Mezők

Az első forgatókönyv, amely potenciális memóriaszivárgást okozhat, a statikus változók.

Java-ban statikus a mezők élettartama általában megegyezik a futó alkalmazás teljes élettartamával (hacsak ClassLoader jogosulttá válik a szemétszállításra).

Hozzunk létre egy egyszerű Java programot, amely a statikusLista:

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 ("Debug Point 2"); } public static void main (String [] args) {Log.info ("1. hibakeresési pont"); új StaticTest (). populateList (); Log.info ("Hibakeresési pont 3"); }}

Most, ha elemezzük a kupac memóriát a program végrehajtása során, akkor látni fogjuk, hogy az 1. és 2. hibakeresési pontok között a várakozásoknak megfelelően nőtt a kupac memória.

De amikor elhagyjuk a populateList () módszer a 3. hibakeresési pontnál, a kupac memória még nem gyűlt össze amint ezt a VisualVM válaszban láthatjuk:

A fenti programban azonban a 2. sorban, ha csak eldobjuk a kulcsszót statikus, akkor ez drasztikus változást hoz a memóriahasználatban, ez a Visual VM válasz megmutatja:

Az első rész a hibakeresési pontig majdnem megegyezik azzal, amit mi kaptunk statikus. De ezúttal miután elhagytuk a populateList () módszer, a lista összes memóriája szemétgyűjtés, mivel nincs rá utalásunk.

Ezért nagyon oda kell figyelnünk a statikus változók. Ha a gyűjteményeket vagy a nagy tárgyakat úgy deklaráljuk statikus, akkor az alkalmazás élettartama alatt megmaradnak a memóriában, ezzel blokkolva a létfontosságú memóriát, amelyet egyébként máshol lehetne használni.

Hogyan lehet megakadályozni?

  • Minimalizálja a statikus változók
  • Szingulettek használatakor támaszkodjon olyan megvalósításra, amely lelkesen tölti be az objektumot, ahelyett, hogy lelkesen töltené be

3.2. Záratlan forrásokon keresztül

Amikor új kapcsolatot létesítünk vagy folyamot nyitunk, a JVM memóriát oszt ki ezekhez az erőforrásokhoz. Néhány példa az adatbázis-kapcsolatokra, a bemeneti folyamokra és a munkamenet-objektumokra.

Ha elfelejtette bezárni ezeket az erőforrásokat, blokkolhatja a memóriát, és így távol tartja őket a GC elérhetőségétől. Ez akár olyan kivétel esetén is előfordulhat, amely megakadályozza, hogy a program végrehajtása elérje a kódot kezelő utasítást az erőforrások bezárásához.

Bármelyik esetben, az erőforrásokból maradt nyitott kapcsolat memóriát emészt fel, és ha nem foglalkozunk velük, akkor ronthatják a teljesítményt, és akár azt is eredményezhetik OutOfMemoryError.

Hogyan lehet megakadályozni?

  • Mindig használja végül blokkolja az erőforrások bezárását
  • A kód (még a végül blokk), amely bezárja az erőforrásokat, maga sem rendelkezhet kivétellel
  • A Java 7+ alkalmazásakor használhatjuk próbáld ki-források blokkolásával

3.3. Helytelen egyenlő () és hash kód() Végrehajtások

Az új osztályok definiálásakor nagyon gyakori felügyelet nem írja le a megfelelő felülbírált módszereket egyenlő () és hash kód() mód.

HashSet és HashMap használja ezeket a módszereket számos művelet során, és ha nem írják felül helyesen, akkor a memóriaszivárgás lehetséges problémáinak forrásává válhatnak.

Vegyünk egy példát egy triviálisra Személy osztály és használja kulcsként a HashMap:

public class Személy {public String név; public Person (karakterlánc neve) {this.name = név; }}

Most beillesztünk másolatot Személy tárgyakat a Térkép hogy ezt a kulcsot használja.

Ne feledje, hogy a Térkép nem tartalmazhat ismétlődő kulcsokat:

@Test public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak () {Map map = new HashMap (); for (int i = 0; i <100; i ++) {map.put (új Személy ("jon"), 1); } Assert.assertFalse (map.size () == 1); }

Itt használjuk Személy mint kulcs. Mivel Térkép nem engedélyezi a duplikált kulcsokat, a sok másolat Személy a kulcsként beillesztett objektumok nem növelhetik a memóriát.

De mivel nem definiáltuk a megfelelőt egyenlő () módszerrel az ismétlődő objektumok felhalmozódnak és növelik a memóriát, ezért több tárgyat is látunk a memóriában. A VisualVM-ben lévő halom memória így néz ki:

Azonban, ha felülírtuk a egyenlő () és hash kód() módszereket, akkor csak egy létezne Személy objektum ebben Térkép.

Vessünk egy pillantást a egyenlő () és hash kód() a mi Személy osztály:

public class Személy {public String név; public Person (karakterlánc neve) {this.name = név; } @Orride public boolean egyenlő (Object o) {if (o == this) return true; if (! (o Személy példánya)) {return false; } Személy személy = (Személy) o; visszatér személy.név.egyenlő (név); } @Orride public int hashCode () {int result = 17; eredmény = 31 * eredmény + név.hashCode (); visszatérési eredmény; }}

És ebben az esetben a következő állítások lennének igazak:

@Test public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak () {Map map = new HashMap (); for (int i = 0; i <2; i ++) {map.put (új Személy ("jon"), 1); } Assert.assertTrue (map.size () == 1); }

Miután megfelelően felülírta egyenlő () és hash kód(), a halom memória ugyanahhoz a programhoz így néz ki:

Egy másik példa egy olyan ORM eszköz használatára, mint a Hibernate, amely egyenlő () és hash kód() módszerek az objektumok elemzésére és mentésére a gyorsítótárba.

A memóriaszivárgás esélye meglehetősen nagy, ha ezeket a módszereket nem írják felül mert a hibernálás ekkor nem lenne képes összehasonlítani az objektumokat, és a gyorsítótárát ismétlődő objektumokkal töltené meg.

Hogyan lehet megakadályozni?

  • Ökölszabályként az új entitások meghatározásakor mindig felülírja egyenlő () és hash kód() mód
  • Nem csak a felülbírálás elég, de ezeket a módszereket is optimális módon felül kell írni

További információért keresse fel a Generate oktatóanyagainkat egyenlő () és hash kód() az Eclipse és a Guide to hash kód() Java-ban.

3.4. A külső osztályokra utaló belső osztályok

Ez nem statikus belső osztályok (anonim osztályok) esetén történik. Az inicializáláshoz ezek a belső osztályok mindig megkövetelik a becsatoló osztály egy példányát.

Alapértelmezés szerint minden nem statikus belső osztály implicit hivatkozással rendelkezik az azt tartalmazó osztályra. Ha ezt a belső osztály objektumot használjuk az alkalmazásunkban, akkor még akkor sem, ha a tartalmazó class 'objektumunk kívül esik a hatókörön, nem lesz szemét.

Vegyünk egy osztályt, amely sok terjedelmes objektumra hivatkozik, és amelynek nem statikus belső osztálya van. Amikor csak egy belső osztályú objektumot hozunk létre, a memória modell a következőképpen néz ki:

Ha azonban a belső osztályt statikusnak nyilvánítjuk, akkor ugyanaz a memória modell így néz ki:

Ez azért történik, mert a belső osztály objektum implicit módon hivatkozást tartalmaz a külső osztály objektumra, ezáltal érvénytelen jelölt a szemétszállításra. Ugyanez történik a névtelen osztályok esetében is.

Hogyan lehet megakadályozni?

  • Ha a belső osztálynak nincs szüksége hozzáférésre az osztálytagokhoz, fontolja meg azt, hogy a-ba alakítsa statikus osztály

3.5. Keresztül véglegesítés () Mód

A véglegesítők használata a memóriaszivárgás problémáinak újabb forrása. Amikor egy osztály véglegesítés () metódust felülírják az osztályba tartozó tárgyakat nem gyűjtik azonnal szeméttel. Ehelyett a GC sorba állítja őket a véglegesítéshez, amely egy későbbi időpontban következik be.

Továbbá, ha a kód be van írva véglegesítés () módszer nem optimális, és ha a véglegesítő várólista nem képes lépést tartani a Java szemétgyűjtővel, akkor előbb vagy utóbb alkalmazásunk célja, hogy megfeleljen egy OutOfMemoryError.

Ennek bemutatásához vegyük fontolóra, hogy van egy osztályunk, amelyre felülbíráltuk véglegesítés () módszer, és hogy a módszer végrehajtása egy kis időt vesz igénybe. Amikor ebbe az osztályba nagyszámú objektum gyűjt szemetet, akkor a VisualVM-ben a következőképpen néz ki:

Ha azonban csak eltávolítjuk a felülbíráltakat véglegesítés () módszerrel, akkor ugyanaz a program a következő választ adja:

Hogyan lehet megakadályozni?

  • Mindig kerülnünk kell a véglegesítőket

További részletek a véglegesítés (), olvassa el a 3. szakaszt (A véglegesítők elkerülése) Útmutatónk a Java véglegesítéséhez.

3.6. Internált Húrok

A Java Húr A pool jelentős változáson ment keresztül a Java 7-ben, amikor áthelyezték a PermGen-ből a HeapSpace-be. De a 6-os és annál régebbi verziókon működő alkalmazások esetében figyelmesebbnek kell lennünk, ha nagyokkal dolgozunk Húrok.

Ha hatalmas masszívat olvasunk Húr objektum, és hívja gyakornok() ezen az objektumon, akkor a karakterlánc-készletbe kerül, amely a PermGenben (állandó memória) található, és ott marad, amíg az alkalmazásunk fut. Ez blokkolja a memóriát, és jelentős memóriaszivárgást okoz alkalmazásunkban.

A PermVen erre az esetre a JVM 1.6-ban így néz ki a VisualVM-ben:

Ezzel szemben egy módszerben, ha csak olvassunk egy karakterláncot egy fájlból, és nem internáljuk, akkor a PermGen a következőképpen néz ki:

Hogyan lehet megakadályozni?

  • A probléma megoldásának legegyszerűbb módja a legfrissebb Java verzióra történő frissítés, mivel a Stringkészlet a Java 7-es verziójától kezdve átkerül a HeapSpace-be
  • Ha nagy területen dolgozik Húrok, a potenciál elkerülése érdekében növelje a PermGen tér méretét OutOfMemoryErrors:
    -XX: MaxPermSize = 512m

3.7. Használata ThreadLocals

ThreadLocal (részletesen tárgyalja a ThreadLocal Java oktatóanyagban) egy olyan konstrukció, amely lehetővé teszi számunkra, hogy elkülönítsük az állapotot egy adott száltól, és így lehetővé tesszük a szál biztonságának elérését.

Ha ezt a konstrukciót használjuk, minden szál implicit hivatkozást tartalmaz az a másolatára ThreadLocal változó, és megtartja saját példányát, ahelyett, hogy az erőforrást több szálon osztaná meg, mindaddig, amíg a szál él.

Előnyei ellenére a ThreadLocal A változók ellentmondásosak, mivel rosszul használják a memóriaszivárgások bevezetését, ha nem használják őket megfelelően. Joshua Bloch egyszer kommentálta a szál helyi használatát:

„A szálkészletek hanyag használata a szálak helyiek hanyag használatával együtt akaratlan objektummegtartást okozhat, amint azt sok helyen megjegyezték. De a hibát a helyi lakosokra hárítani indokolatlan.

A memória szivárog ThreadLocals

ThreadLocals állítólag szemetet gyűjtenek, ha a tartószál már nem él. De a probléma akkor merül fel ThreadLocals a modern alkalmazáskiszolgálókkal együtt használják.

A modern alkalmazáskiszolgálók szálkészletet használnak a kérelmek feldolgozásához, újak létrehozása helyett (például a Végrehajtó Apache Tomcat esetében). Sőt, külön osztálytervet is használnak.

Mivel az alkalmazáskiszolgálók szálkészletei a szál újrafelhasználásának koncepcióján dolgoznak, soha nem kerülnek szemétbe, ehelyett egy másik kérelem kiszolgálására használják fel őket.

Ha valamelyik osztály létrehozza a ThreadLocal változó, de nem távolítja el kifejezetten, akkor az objektum másolata a dolgozónál marad cérna a webalkalmazás leállítása után is, megakadályozva ezzel az objektum szemétszedését.

Hogyan lehet megakadályozni?

  • Jó gyakorlat a takarítás ThreadLocals amikor már nem használják - ThreadLocals biztosítja a eltávolítás () metódus, amely eltávolítja az aktuális szál értékét ehhez a változóhoz
  • Ne használja ThreadLocal.set (null) az érték törléséhez - valójában nem törli az értéket, hanem megkeresi a Térkép az aktuális szálhoz társítva, és állítsa be a kulcs-érték párot aktuális szálként és nulla illetőleg
  • Még jobb megfontolni ThreadLocal mint erőforrás, amelyet le kell zárni a végül blokkolja csak annak biztosítása érdekében, hogy mindig zárva legyen, még kivétel esetén is:
    próbáld ki a {threadLocal.set (System.nanoTime ()); // ... további feldolgozás} végül {threadLocal.remove (); }

4. A memóriaszivárgás kezelésének egyéb stratégiái

Bár a memóriaszivárgások kezelésére nincs egy mindenki számára megfelelő megoldás, vannak olyan módszerek, amelyekkel minimalizálhatjuk ezeket a szivárgásokat.

4.1. Profilozás engedélyezése

A Java profilírók olyan eszközök, amelyek figyelik és diagnosztizálják az alkalmazáson keresztüli memóriaszivárgást. Elemzik, hogy mi folyik belső alkalmazásunkban - például hogyan osztják fel a memóriát.

A profilírók segítségével összehasonlíthatjuk a különböző megközelítéseket, és megtalálhatjuk azokat a területeket, ahol az erőforrásainkat optimálisan tudjuk felhasználni.

A Java VisualVM-et használtuk az oktatóanyag 3. szakaszában. Tekintse meg a Java Profiler útmutatónkat, hogy megismerje a különféle típusú profilokat, például a Mission Control, a JProfiler, a YourKit, a Java VisualVM és a Netbeans Profiler.

4.2. Verbose Garbage Collection

A részletes szemétgyűjtés engedélyezésével nyomon követjük a GC részletes nyomát. Ennek engedélyezéséhez a következőket kell hozzáadnunk a JVM-konfigurációnkhoz:

-verbose: gc

Ennek a paraméternek a hozzáadásával láthatjuk a GC-n belül zajló részletek részleteit:

4.3. A memóriaszivárgások elkerülése érdekében használja a referenciaobjektumokat

Használhatunk Java beépített referenciaobjektumokat is java.lang.ref csomagot a memóriaszivárgások kezelésére. Használata java.lang.ref csomag helyett az objektumok közvetlen hivatkozása helyett speciális hivatkozásokat használunk azokra az objektumokra, amelyek lehetővé teszik, hogy könnyen szemetet gyűjtsenek.

A referencia várólistákat arra terveztük, hogy tudatosítsuk bennünket a Garbage Collector által végrehajtott tevékenységekben. További információért olvassa el a Java Referenciák a Java Baeldung oktatóanyagban, különös tekintettel a 4. szakaszra.

4.4. Eclipse memóriaszivárgás figyelmeztetések

A JDK 1.5 vagy újabb verziójú projektek esetében az Eclipse figyelmeztetéseket és hibákat jelenít meg, amikor nyilvánvaló memóriaszivárgási esetekkel találkozik. Tehát az Eclipse fejlesztésekor rendszeresen ellátogathatunk a „Problémák” fülre, és éberebben figyelünk a memóriaszivárgásra vonatkozó figyelmeztetésekre (ha vannak ilyenek):

4.5. Benchmarking

Mérhetjük és elemezhetjük a Java-kód teljesítményét benchmarkok végrehajtásával. Így összehasonlíthatjuk az alternatív megközelítések teljesítményét ugyanazon feladat elvégzéséhez. Ez segíthet a jobb megközelítés kiválasztásában és a memória megőrzésében.

Ha többet szeretne tudni az benchmarkingról, kérjük, látogasson el a Microbenchmarking with Java oktatóanyagra.

4.6. Kód vélemények

Végül, mindig a klasszikus, régi iskola módja van az egyszerű kód-áttekintésnek.

Bizonyos esetekben még ez a triviális megjelenésű módszer is segít megszüntetni néhány általános memóriaszivárgási problémát.

5. Következtetés

Hétköznapi szempontból a memóriaszivárgást olyan betegségnek gondolhatjuk, amely rontja alkalmazásunk teljesítményét azáltal, hogy blokkolja a létfontosságú memóriaforrásokat. És mint minden más betegség, ha nem is gyógyítják, ez idővel halálos kimenetelű összeomlást eredményezhet.

A memóriaszivárgások megoldása bonyolult, és megtalálása bonyolult elsajátítást és parancsot igényel a Java nyelv felett. A memóriaszivárgások kezelése közben nincs egy mindenki számára megfelelő megoldás, mivel a kiszivárogtatások sokféle esemény révén bekövetkezhetnek.

Ha azonban a legjobb gyakorlatokhoz folyamodunk, és rendszeresen végrehajtunk szigorú kód-áttekintéseket és profilalkotásokat, akkor minimalizálhatjuk az alkalmazásunk memóriaszivárgásának kockázatát.

Mint mindig, az ebben az oktatóanyagban bemutatott VisualVM válaszok előállításához használt kódrészletek elérhetőek a GitHubon.