Vraag Een geheugenlek creëren met Java


Ik had net een interview en ik werd gevraagd om een ​​geheugenlek te maken met Java. Onnodig te zeggen dat ik me behoorlijk dom voelde omdat ik geen idee had hoe ik er zelfs maar een zou kunnen maken.

Wat zou een voorbeeld zijn?


2634


oorsprong


antwoorden:


Dit is een goede manier om een ​​echt geheugenlek te creëren (objecten ontoegankelijk door code uit te voeren maar nog steeds in het geheugen te bewaren) in pure Java:

  1. De toepassing maakt een langlopende thread (of gebruikt een threadpool om nog sneller te lekken).
  2. De thread laadt een klasse via een (optioneel aangepaste) ClassLoader.
  3. De klasse wijst een groot geheugengeheugen toe (bijv. new byte[1000000]), slaat een sterke verwijzing ernaar op in een statisch veld en slaat vervolgens een verwijzing naar zichzelf op in een ThreadLocal. Het toewijzen van extra geheugen is optioneel (lekken van de Klasse-instantie is voldoende), maar het lek zal zo veel sneller werken.
  4. De thread wist alle verwijzingen naar de aangepaste klasse of de ClassLoader waaruit deze is geladen.
  5. Herhaling.

Dit werkt omdat de ThreadLocal een verwijzing naar het object houdt, dat een verwijzing naar zijn klasse houdt, die op zijn beurt een verwijzing naar zijn ClassLoader houdt. De ClassLoader houdt op zijn beurt een verwijzing bij naar alle klassen die hij heeft geladen.

(Het was slechter in veel JVM-implementaties, met name voorafgaand aan Java 7, omdat klassen en ClassLoaders rechtstreeks in permgen werden toegewezen en nooit helemaal werden GC'd. Ongeacht hoe de JVM omgaat met klasse-ontlading, een ThreadLocal blijft echter een Klasseobject wordt teruggevorderd.)

Een variatie op dit patroon is de reden waarom toepassingscontainers (zoals Tomcat) geheugen kunnen lekken als een zeef als je applicaties die ThreadLocals gebruiken op welke manier dan ook vaak opnieuw implementeert. (Aangezien de toepassingscontainer Threads gebruikt zoals beschreven, en elke keer dat u de toepassing opnieuw implementeert, wordt een nieuwe ClassLoader gebruikt.)

Bijwerken: Omdat veel mensen ernaar blijven vragen, hier is een voorbeeldcode die dit gedrag in actie weergeeft.


1931



Statisch veld met objectreferentie [esp laatste veld]

class MemorableClass {
    static final ArrayList list = new ArrayList(100);
}

Roeping String.intern() op een lange reeks

String str=readString(); // read lengthy string any source db,textbox/jsp etc..
// This will place the string in memory pool from which you can't remove
str.intern();

(Niet-gesloten) open streams (bestand, netwerk enz ...)

try {
    BufferedReader br = new BufferedReader(new FileReader(inputFile));
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

Niet-gesloten verbindingen

try {
    Connection conn = ConnectionFactory.getConnection();
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

Gebieden die onbereikbaar zijn voor de vuilnisman van JVM, zoals geheugen toegewezen via native methoden

In webtoepassingen worden sommige objecten in het toepassingsbereik opgeslagen totdat de toepassing expliciet wordt gestopt of verwijderd.

getServletContext().setAttribute("SOME_MAP", map);

Onjuiste of ongepaste JVM-opties, zoals de noclassgc optie op IBM JDK die ongebruikte klassenafvalverzameling voorkomt

Zien IBM jdk-instellingen.


1059



Een eenvoudig ding om te doen is om een ​​HashSet te gebruiken met een onjuiste (of niet-bestaande) hashCode() of equals()en voeg vervolgens "duplicaten" toe. In plaats van duplicaten te negeren zoals het hoort, zal de set alleen maar groeien en kun je ze niet verwijderen.

Als u wilt dat deze slechte sleutels / elementen blijven hangen, kunt u een statisch veld gebruiken

class BadKey {
   // no hashCode or equals();
   public final String key;
   public BadKey(String key) { this.key = key; }
}

Map map = System.getProperties();
map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.

390



Hieronder zal er een niet voor de hand liggend geval zijn waarbij Java lekt, naast het standaard geval van vergeten luisteraars, statische verwijzingen, valse / aanpasbare sleutels in hashmaps, of gewoon threads vastzitten zonder enige kans om hun levenscyclus te beëindigen.

  • File.deleteOnExit()- lekt altijd de snaar, als de string een substring is, is het lek nog erger (de onderliggende char [] is ook gelekt) - in Java 7 substring kopieert ook de char[], dus de laatste is niet van toepassing; @Daniel, geen behoefte aan stemmen.

Ik zal me concentreren op discussies om het gevaar van onbeheerde discussies aan te tonen, ik wil zelfs de swing niet aanraken.

  • Runtime.addShutdownHook en niet verwijderen ... en dan zelfs met removeShutdownHook als gevolg van een fout in de ThreadGroup-klasse met betrekking tot niet-gestarte threads die mogelijk niet wordt verzameld, lekt de ThreadGroup effectief. JGroup heeft het lek in GossipRouter.

  • Maken, maar niet starten, een Thread valt in dezelfde categorie als hierboven.

  • Het maken van een thread erft de ContextClassLoader en AccessControlContext, plus de ThreadGroup En elk InheritedThreadLocal, al die verwijzingen zijn potentiële lekken, samen met de hele klassen geladen door de classloader en alle statische referenties, en ja-ja. Het effect is vooral zichtbaar in het gehele j.u.c.Executor-framework met een supereenvoudige ThreadFactory interface, maar de meeste ontwikkelaars hebben geen idee van het loerende gevaar. Ook beginnen veel bibliotheken op verzoek met threads (veel te veel populaire bibliotheken in de industrie).

  • ThreadLocal caches; die zijn in veel gevallen slecht. Ik weet zeker dat iedereen nogal wat simpele caches heeft gezien op basis van ThreadLocal, nou het slechte nieuws: als de draad meer dan verwacht blijft leven in de context ClassLoader, is het een puur en aardig klein lekje. Gebruik geen Threadlokale caches tenzij echt nodig.

  • Roeping ThreadGroup.destroy() wanneer de ThreadGroup zelf geen threads heeft, maar het kind nog steeds ThreadGroups houdt. Een slecht lek waardoor de ThreadGroup niet van zijn ouder kan worden verwijderd, maar alle kinderen onverteerbaar worden.

  • Het gebruik van WeakHashMap en de waarde (in) verwijst direct naar de sleutel. Dit is moeilijk om te vinden zonder een heap dump. Dat geldt voor alle verlengde Weak/SoftReference die misschien een moeilijke referentie terughoudt naar het bewaakte object.

  • Gebruik makend van java.net.URL met het HTTP (S) -protocol en het laden van de bron van (!). Deze is speciaal, de KeepAliveCache maakt een nieuwe thread in het systeem ThreadGroup die de contextklasloader van de huidige thread lekt. De thread is gemaakt op het eerste verzoek wanneer er geen levend draad bestaat, dus je kunt geluk hebben of gewoon lekken. Het lek is al opgelost in Java 7 en de code die de thread goed maakt, verwijdert de contextklasse-lader. Er zijn weinig meer gevallen (zoals ImageFetcher, ook opgelost) om vergelijkbare threads te maken.

  • Gebruik makend van InflaterInputStream voorbijgaand new java.util.zip.Inflater() in de constructor (PNGImageDecoder bijvoorbeeld) en niet bellen end() van de inflater. Nou, als je de constructeur met rechtvaardig doorgeeft new, geen kans ... En ja, bellen close() op de stream sluit de inflater niet als deze handmatig is doorgegeven als constructorparameter. Dit is geen echt lek omdat het door de finalist zou worden vrijgegeven ... als het dit nodig acht. Tot dat moment eet het native-geheugen zo erg dat Linux oom_killer het proces straffeloos kan doden. Het belangrijkste probleem is dat de laatste versie in Java zeer onbetrouwbaar is en dat G1 tot 7.0.2 het erger heeft gemaakt. Moraal van het verhaal: lever native bronnen zo snel mogelijk vrij; de finalizer is gewoon te arm.

  • Dezelfde case met java.util.zip.Deflater. Deze is veel erger, omdat Deflater geheugengebrek heeft in Java, d.w.z. altijd 15 bits (max) gebruikt en 8 geheugenniveaus (9 is max), die meerdere honderden kB native geheugen toewijzen. Gelukkig, Deflater wordt niet veel gebruikt en voor zover ik weet bevat JDK geen misbruik. Bel altijd end() als u handmatig een maakt Deflater of Inflater. Het beste deel van de laatste twee: je kunt ze niet vinden via normale profileergereedschappen.

(Ik kan wat meer verspilling van tijd toevoegen die ik op verzoek heb aangetroffen.)

Veel geluk en blijf veilig; lekken zijn slecht!


228



De meeste voorbeelden zijn hier "te complex". Het zijn edge cases. Met deze voorbeelden maakte de programmeur een fout (zoals het niet opnieuw definiëren van gelijken / hashcode), of is gebeten door een hoekgeval van de JVM / JAVA (lading van klasse met statische ...). Ik denk dat dit niet het type voorbeeld is dat een interviewer wil, of zelfs het meest voorkomende geval.

Maar er zijn echt eenvoudigere gevallen voor geheugenlekken. De garbage collector maakt alleen vrij waar naar wordt verwezen. Wij als Java-ontwikkelaars geven niets om het geheugen. We wijzen het toe wanneer nodig en laten het automatisch worden vrijgegeven. Fijn.

Maar een langlevende applicatie heeft meestal een gedeelde status. Het kan van alles zijn, van statica tot singletons ... Vaak hebben niet-triviale applicaties de neiging om complexe objectgrafieken te maken. Gewoon vergeten om een ​​verwijzing naar null in te stellen of vaker te vergeten om één object uit een verzameling te verwijderen is genoeg om een ​​geheugenlek te veroorzaken.

Natuurlijk hebben alle soorten luisteraars (zoals UI-luisteraars), caches of een langlevende gedeelde toestand de neiging om geheugenlekken te produceren als ze niet goed worden behandeld. Wat begrepen zal worden is dat dit geen Java-hoekgeval is, of een probleem met de vuilnisman. Het is een ontwerpprobleem. We ontwerpen dat we een luisteraar toevoegen aan een object met een lange levensduur, maar we verwijderen de luisteraar niet wanneer deze niet langer nodig is. We cachen objecten, maar we hebben geen strategie om ze uit de cache te verwijderen.

We hebben misschien een complexe grafiek die de vorige staat opslaat die nodig is voor een berekening. Maar de vorige toestand is zelf gekoppeld aan de vorige staat enzovoort.

Alsof we de SQL-verbindingen of -bestanden moeten sluiten. We moeten de juiste verwijzingen naar null instellen en elementen uit de verzameling verwijderen. We zullen de juiste caching-strategieën hebben (maximale geheugengrootte, aantal elementen of timers). Alle objecten die toestaan ​​dat een luisteraar wordt aangemeld, moeten zowel een addListener- als removeListener-methode bevatten. En wanneer deze melders niet langer worden gebruikt, moeten ze hun listenerlijst wissen.

Een geheugenlek is inderdaad echt mogelijk en is perfect voorspelbaar. Geen behoefte aan speciale taalfuncties of hoekgevallen. Geheugenlekken zijn ofwel een indicator dat er iets ontbreekt of zelfs van ontwerpproblemen.


150



Het antwoord hangt volledig af van wat de interviewer dacht dat ze vroegen.

Is het in de praktijk mogelijk om Java te laten lekken? Natuurlijk is dat zo, en er zijn veel voorbeelden in de andere antwoorden.

Maar er zijn meerdere metavragen geweest die mogelijk zijn gesteld?

  • Is een theoretisch "perfecte" Java-implementatie kwetsbaar voor lekken?
  • Begrijpt de kandidaat het verschil tussen theorie en werkelijkheid?
  • Begrijpt de kandidaat hoe garbagecollection werkt?
  • Of hoe garbage collection zou moeten werken in een ideale zaak?
  • Weten ze dat ze andere talen kunnen bellen via native interfaces?
  • Weten zij het geheugen in die andere talen te lekken?
  • Weet de kandidaat zelfs wat geheugenbeheer is en wat gebeurt er achter de schermen op Java?

Ik lees je meta-vraag als "Wat is een antwoord dat ik in deze interviewsituatie had kunnen gebruiken". En daarom ga ik me richten op interviewvaardigheden in plaats van Java. Ik geloof dat je meer geneigd bent om de situatie te herhalen dat je het antwoord op een vraag in een interview niet kent dan dat je op een plek moet zijn waar je moet weten hoe je Java kunt laten lekken. Dus hopelijk helpt dit.

Een van de belangrijkste vaardigheden die je kunt ontwikkelen voor het interviewen, is leren om actief naar de vragen te luisteren en met de interviewer samen te werken om hun intentie te ontlenen. Niet alleen laat dit je hun vraag beantwoorden zoals ze willen, maar laat ook zien dat je een aantal essentiële communicatievaardigheden hebt. En als het gaat om de keuze tussen vele even getalenteerde ontwikkelaars, huur ik degene die luistert, denkt en begrijpt voordat ze elke keer reageren.


133



Het volgende is een vrij zinloos voorbeeld, als je het niet begrijpt JDBC. Of op zijn minst hoe JDBC een ontwikkelaar verwacht te sluiten Connection, Statement en ResultSet voordat ze worden weggegooid of verwijzingen naar hen worden geweigerd, in plaats van te vertrouwen op de implementatie van finalize.

void doWork()
{
   try
   {
       Connection conn = ConnectionFactory.getConnection();
       PreparedStatement stmt = conn.preparedStatement("some query"); // executes a valid query
       ResultSet rs = stmt.executeQuery();
       while(rs.hasNext())
       {
          ... process the result set
       }
   }
   catch(SQLException sqlEx)
   {
       log(sqlEx);
   }
}

Het probleem met het bovenstaande is dat het Connection object is niet gesloten en daarom blijft de fysieke verbinding open totdat de garbagecollector rond komt en ziet dat deze onbereikbaar is. GC zal het finalize methode, maar er zijn JDBC-stuurprogramma's die het finalize, althans niet op dezelfde manier Connection.close is geïmplementeerd. Het resulterende gedrag is dat terwijl het geheugen wordt teruggevorderd vanwege onbereikbare objecten die worden verzameld, bronnen (inclusief geheugen) geassocieerd met de Connection object kan eenvoudig niet worden teruggevorderd.

In zo'n geval waar de Connection's finalize methode niet alles opruimt, kan men er zelfs achter komen dat de fysieke verbinding met de databaseserver een aantal garbage collection-cycli duurt, totdat de databaseserver er uiteindelijk achter komt dat de verbinding niet levend is (als dat wel het geval is) en moet worden gesloten.

Zelfs als het JDBC-stuurprogramma zou worden geïmplementeerd finalize, tijdens de afronding kunnen uitzonderingen worden gegooid. Het resulterende gedrag is dat elk geheugen dat is gekoppeld aan het nu "slapende" object niet zal worden teruggevorderd, zoals finalize kan maar één keer worden aangeroepen.

Het bovenstaande scenario van het tegenkomen van uitzonderingen tijdens het finaliseren van objecten is gerelateerd aan een ander ander scenario dat mogelijk kan leiden tot een geheugenlek - object-opstanding. Opstanding van objecten wordt vaak opzettelijk gedaan door een sterke verwijzing te maken naar het object dat gefinaliseerd moet worden, naar een ander object. Wanneer de opstanding van objecten verkeerd wordt gebruikt, zal dit leiden tot een geheugenlek in combinatie met andere bronnen van geheugenlekken.

Er zijn nog veel meer voorbeelden die je kunt oproepen - zoals

  • Beheer van een List bijvoorbeeld waar u alleen toevoegt aan de lijst en niet verwijdert van de lijst (hoewel u elementen moet verwijderen die u niet langer nodig heeft), of
  • Opening Sockets of Files, maar ze niet te sluiten wanneer ze niet langer nodig zijn (vergelijkbaar met het bovenstaande voorbeeld met betrekking tot de Connection klasse).
  • Niet uitgeladen Singletons bij het verwijderen van een Java EE-applicatie. Blijkbaar zal de Classloader die de singleton-klasse heeft geladen een verwijzing naar de klasse behouden en daarom zal de instantie singleton nooit worden verzameld. Wanneer een nieuw exemplaar van de toepassing wordt geïmplementeerd, wordt meestal een nieuwe klasse-lader gemaakt en de voormalige klasse-lader blijft bestaan ​​vanwege de singleton.

113



Waarschijnlijk een van de eenvoudigste voorbeelden van een mogelijk geheugenlek en hoe dit te voorkomen, is de implementatie van ArrayList.remove (int):

public E remove(int index) {
    RangeCheck(index);

    modCount++;
    E oldValue = (E) elementData[index];

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index,
                numMoved);
    elementData[--size] = null; // (!) Let gc do its work

    return oldValue;
}

Als u het zelf zou implementeren, zou u gedacht hebben het array-element dat niet langer wordt gebruikt (elementData[--size] = null)? Die referentie kan een enorm object in leven houden ...


102



Telkens wanneer u referenties rondhoudt naar objecten die u niet langer nodig heeft, heeft u een geheugenlek. Zien Omgaan met geheugenlekken in Java-programma's voor voorbeelden van hoe geheugenlekken zich in Java manifesteren en wat u eraan kunt doen.


63



Je kunt geheugen lekken sun.misc.Unsafe klasse. In feite wordt deze serviceklasse gebruikt in verschillende standaardklassen (bijvoorbeeld in java.nio klassen). U kunt geen exemplaar van deze klasse rechtstreeks maken, maar jij mag gebruik reflectie om dat te doen.

Code compileert niet in Eclipse IDE - compileer deze met behulp van het commando javac (tijdens de compilatie ontvang je waarschuwingen)

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import sun.misc.Unsafe;


public class TestUnsafe {

    public static void main(String[] args) throws Exception{
        Class unsafeClass = Class.forName("sun.misc.Unsafe");
        Field f = unsafeClass.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);
        System.out.print("4..3..2..1...");
        try
        {
            for(;;)
                unsafe.allocateMemory(1024*1024);
        } catch(Error e) {
            System.out.println("Boom :)");
            e.printStackTrace();
        }
    }

}

43



Ik kan mijn antwoord hier naartoe kopiëren: De eenvoudigste manier om geheugenlekken te veroorzaken in Java?

"Een geheugenlek, in de informatica (of lekkage, in deze context), doet zich voor wanneer een computerprogramma geheugen gebruikt, maar het niet terug naar het besturingssysteem kan vrijgeven." (Wikipedia)

Het eenvoudige antwoord is: dat kun je niet. Java voert automatisch geheugenbeheer uit en maakt bronnen vrij die u niet nodig hebt. Je kunt dit niet voorkomen. Het zal ALTIJD in staat zijn om de middelen vrij te maken. In programma's met handmatig geheugenbeheer is dit anders. Je kunt wat geheugen in C krijgen met malloc (). Om het geheugen vrij te maken, hebt u de aanwijzer nodig die door malloc is geretourneerd en belt u gratis () erop. Maar als u de aanwijzer niet meer heeft (overschreven of levensduur overschreden), dan bent u helaas niet in staat om dit geheugen te bevrijden en hebt u dus een geheugenlek.

Alle andere antwoorden tot nu toe zijn in mijn definitie niet echt geheugenlekken. Ze willen allemaal heel snel het geheugen vullen met zinloos spul. Maar u kunt op elk moment nog steeds de door u gemaakte objecten derefferentieeren en zo het geheugen vrijmaken -> GEEN LEKKAGE. het antwoord van acconrad komt aardig in de buurt, want ik moet toegeven, omdat zijn oplossing erin bestaat om de garbage collector gewoon te "verpletteren" door hem in een eindeloze lus te dwingen).

Het lange antwoord is: je kunt een geheugenlek krijgen door een bibliotheek voor Java te schrijven met behulp van de JNI, die handmatig geheugenbeheer kan hebben en dus geheugenlekken kan hebben. Als u deze bibliotheek aanroept, zal uw Java-proces geheugen lekken. Of u kunt bugs in de JVM hebben, zodat de JVM geheugen verliest. Er zijn waarschijnlijk bugs in de JVM, er kunnen zelfs enkele bekende zijn omdat garbage collection niet zo triviaal is, maar dan is het nog steeds een bug. Door ontwerp is dit niet mogelijk. Mogelijk vraagt ​​u om een ​​of andere Java-code die door een dergelijke bug wordt veroorzaakt. Sorry, ik ken er geen een en het is in de volgende Java-versie sowieso geen bug meer.


37