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.