Come creare una perdita di memoria in Java?


3223

Ho appena avuto un'intervista e mi è stato chiesto di creare una perdita di memoria con Java.
Inutile dire che mi sentivo piuttosto stupido non avendo idea di come iniziare a crearne uno.

Quale sarebbe un esempio?


275
Vorrei dire loro che Java utilizza un garbage collector, e chiedere loro di essere un po 'più specifico circa la loro definizione di "perdita di memoria", spiegando che - salvo errori JVM - Java non può perdere la memoria in tutto allo stesso modo C / C ++ can. Devi avere un riferimento all'oggetto da qualche parte .
Darien,

371
Trovo divertente che sulla maggior parte delle risposte le persone stiano cercando quei casi limite e quei trucchi e sembrano mancare completamente il punto (IMO). Potrebbero semplicemente mostrare codice che mantiene inutili riferimenti a oggetti che non useranno mai più, e allo stesso tempo non rilasciano mai quei riferimenti; si potrebbe dire che quei casi non sono "vere" perdite di memoria perché ci sono ancora riferimenti a quegli oggetti in giro, ma se il programma non usa mai più quei riferimenti e non li rilasciano mai, è completamente equivalente a (e cattivo come) a " vera perdita di memoria ".
ehabkost,

62
Onestamente non riesco a credere che la domanda simile che ho posto su "Go" sia stata ridimensionata a -1. Qui: stackoverflow.com/questions/4400311/… Fondamentalmente le perdite di memoria di cui stavo parlando sono quelle che hanno ottenuto +200 voti positivi all'OP e tuttavia sono stato attaccato e insultato per aver chiesto se "Go" avesse lo stesso problema. In qualche modo non sono sicuro che tutte le cose wiki funzionino così bene.
SyntaxT3rr0r

74
@ SyntaxT3rr0r - la risposta di darien non è il fanboy. ha ammesso esplicitamente che alcune JVM possono avere bug che significano perdite di memoria. questo è diverso dalle specifiche del linguaggio stesso, consentendo perdite di memoria.
Peter Recore,

31
@ehabkost: No, non sono equivalenti. (1) Possiedi la capacità di recuperare la memoria, mentre in una "vera perdita" il tuo programma C / C ++ dimentica l'intervallo che è stato allocato, non c'è modo sicuro per recuperare. (2) Puoi facilmente rilevare il problema con la profilazione, perché puoi vedere quali oggetti comporta il "gonfiamento". (3) Una "vera perdita" è un errore inequivocabile, mentre un programma che mantiene molti oggetti in giro fino alla sua conclusione potrebbe essere una parte deliberata di come dovrebbe funzionare.
Darien,

Risposte:


2309

Ecco un buon modo per creare una vera perdita di memoria (oggetti inaccessibili eseguendo codice ma ancora memorizzati in memoria) in Java puro:

  1. L'applicazione crea un thread di lunga durata (o utilizza un pool di thread per perdere ancora più velocemente).
  2. Il thread carica una classe tramite un (facoltativamente personalizzato) ClassLoader.
  3. La classe alloca un grosso pezzo di memoria (ad es. new byte[1000000]), Memorizza un riferimento forte ad esso in un campo statico e quindi memorizza un riferimento a se stesso in a ThreadLocal. L'allocazione della memoria aggiuntiva è facoltativa (è sufficiente perdere l'istanza della classe), ma la perdita funzionerà molto più velocemente.
  4. L'applicazione cancella tutti i riferimenti alla classe personalizzata o alla ClassLoader è stata caricata.
  5. Ripetere.

A causa del modo ThreadLocal è implementato in Oracle JDK, questo crea una perdita di memoria:

  • Ogni Thread ha un campo privato threadLocals, che in realtà memorizza i valori thread-local.
  • Ogni chiave in questa mappa è un debole riferimento aThreadLocal oggetto, quindi dopo che l' ThreadLocaloggetto è stato immesso nella spazzatura, la sua voce viene rimossa dalla mappa.
  • Ma ogni valore è un forte riferimento, quindi quando un valore (direttamente o indirettamente) punta ThreadLocalall'oggetto che è la sua chiave , quell'oggetto non sarà né immesso nella spazzatura né rimosso dalla mappa finché il thread rimane in vita.

In questo esempio, la catena di riferimenti forti è simile alla seguente:

Threadoggetto → threadLocalsmappa → istanza della classe esempio → classe esempio → staticoThreadLocal campo → ThreadLocaloggetto.

( ClassLoaderNon ha davvero un ruolo nella creazione della perdita, ma peggiora la perdita a causa di questa catena di riferimento aggiuntiva: esempio di classe → ClassLoader→ tutte le classi che ha caricato. Era anche peggio in molte implementazioni JVM, specialmente prima di Java 7, perché classi eClassLoader s sono state allocate direttamente in permgen e non sono mai state affatto raccolte.

Una variazione su questo modello è il motivo per cui i contenitori di applicazioni (come Tomcat) possono perdere la memoria come un setaccio se si ridistribuiscono frequentemente applicazioni che usano in genere ThreadLocals che in qualche modo rimandano a se stesse. Ciò può accadere per una serie di ragioni impercettibili ed è spesso difficile eseguire il debug e / o correggere.

Aggiornamento : poiché molte persone continuano a chiederlo, ecco alcuni esempi di codice che mostrano questo comportamento in azione .


186
Le perdite di +1 ClassLoader sono alcune delle perdite di memoria più comunemente dolorose nel mondo JEE, spesso causate da librerie di terze parti che trasformano i dati (BeanUtils, codec XML / JSON). Questo può accadere quando la lib viene caricata al di fuori del programma di caricamento classi principale dell'applicazione ma contiene riferimenti alle classi (ad es. Mediante memorizzazione nella cache). Quando si annulla la distribuzione / ridistribuzione dell'app, la JVM non è in grado di eseguire il garbage collection del classloader dell'app (e quindi di tutte le classi caricate da essa), quindi con ripetute distribuzioni il server delle app alla fine si blocca. Se sei fortunato, puoi avere un'idea di ClassCastException zxyAbc non può essere trasmesso a zxyAbc
earcam

7
tomcat utilizza trucchi e nil TUTTE le variabili statiche in TUTTE le classi caricate, ma Tomcat ha molti datarace e codici errati (è necessario prendere un po 'di tempo e inviare correzioni), oltre a tutte le strabilianti ConcurrentLinkedQueue come cache per oggetti interni (piccoli), così piccolo che anche il ConcurrentLinkedQueue.Node occupa più memoria.
bestsss

57
+1: Le perdite di Classloader sono un incubo. Ho trascorso settimane cercando di capirli. La cosa triste è che, come ha detto @earcam, sono principalmente causati da lib di terze parti e anche la maggior parte dei profiler non è in grado di rilevare queste perdite. C'è una buona e chiara spiegazione su questo blog sulle perdite di Classloader. blogs.oracle.com/fkieviet/entry/…
Adrian M

4
@Nicolas: sei sicuro? JRockit esegue gli oggetti GC Class per impostazione predefinita e HotSpot no, ma AFAIK JRockit non è ancora in grado di GC una Class o ClassLoader a cui fa riferimento un ThreadLocal.
Daniel Pryden,

6
Tomcat proverà a rilevare queste perdite per te e ti avviserà: wiki.apache.org/tomcat/MemoryLeakProtection . La versione più recente a volte risolve anche la perdita per te.
Matthijs Bierman

1212

Riferimento statico oggetto oggetto di trattenimento [esp campo finale]

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

chiamata String.intern() una stringa lunga

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();

Flussi aperti (non chiusi) (file, rete ecc ...)

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

Connessioni non chiuse

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

Aree non raggiungibili dal Garbage Collector di JVM , come la memoria allocata tramite metodi nativi

Nelle applicazioni Web, alcuni oggetti vengono archiviati nell'ambito dell'applicazione fino a quando l'applicazione viene esplicitamente arrestata o rimossa.

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

Opzioni JVM errate o inadeguate , come l' noclassgcopzione su IBM JDK che impedisce la garbage collection di classe non utilizzata

Vedi le impostazioni IBM jdk .


178
Non sarei d'accordo sul fatto che gli attributi di contesto e sessione siano "perdite". Sono solo variabili di lunga durata. E il campo finale statico è più o meno solo una costante. Forse le costanti di grandi dimensioni dovrebbero essere evitate, ma non credo sia giusto chiamarlo perdita di memoria.
Ian McLaird,

80
Flussi aperti (non chiusi) (file, rete ecc ...) , non perdono per davvero, durante la finalizzazione (che sarà dopo il prossimo ciclo GC) verrà pianificato close () ( close()normalmente non viene invocato nel finalizzatore thread poiché potrebbe essere un'operazione di blocco). È una cattiva pratica non chiudere, ma non provoca perdite. Il java.sql.Connection non chiuso è lo stesso.
bestsss

33
Nella maggior parte dei JVM sani di mente, sembra che la classe String abbia solo un riferimento debole sul suo interncontenuto hashtable. Come tale, è spazzatura raccolta correttamente e non una perdita. (ma IANAJP) mindprod.com/jgloss/interned.html#GC
Matt B.

42
In che modo il campo statico che contiene l'oggetto di riferimento [esp campo finale] è una perdita di memoria ??
Kanagavelu Sugumar,

5
@cHao True. Il pericolo che ho incontrato non sono i problemi causati dalla memoria trapelata dagli Stream. Il problema è che non c'è abbastanza memoria trapelata da loro. Puoi perdere molte maniglie, ma hai ancora molta memoria. Il Garbage Collector potrebbe quindi decidere di non preoccuparsi di fare una raccolta completa perché ha ancora molta memoria. Questo significa che il finalizzatore non viene chiamato, quindi finisci le maniglie. Il problema è che, in genere, i finalizzatori verranno eseguiti prima che si esaurisca la memoria a causa di perdite di flussi, ma potrebbe non essere chiamato prima che si esaurisca qualcosa di diverso dalla memoria.
Patrick M,

460

Una cosa semplice da fare è usare un HashSet con un errato (o inesistente) hashCode()o equals(), quindi continuare ad aggiungere "duplicati". Invece di ignorare i duplicati come dovrebbe, il set crescerà sempre e non sarai in grado di rimuoverli.

Se vuoi che queste chiavi / elementi cattivi rimangano sospesi puoi usare un campo statico come

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.

68
In realtà, è possibile rimuovere gli elementi da un HashSet anche se la classe element ottiene hashCode ed è uguale a errata; basta ottenere un iteratore per il set e usare il suo metodo di rimozione, poiché l'iteratore funziona effettivamente sulle voci sottostanti e non sugli elementi. (Notare che un hashCode / uguale non implementato non è sufficiente per innescare una perdita; le impostazioni predefinite implementano la semplice identità dell'oggetto e quindi è possibile ottenere gli elementi e rimuoverli normalmente.)
Donal Fellows

12
@Donal quello che sto cercando di dire, immagino, è che non sono d'accordo con la tua definizione di perdita di memoria. Considererei (per continuare l'analogia) che la tua tecnica di rimozione dell'iteratore è una flebo sotto una perdita; la perdita esiste ancora indipendentemente dal gocciolatoio.
corsiKa

94
Sono d'accordo, questa non è una "perdita" di memoria, perché puoi semplicemente rimuovere i riferimenti all'hashset e attendere che il GC si avvii, e presto! il ricordo risale.
user541686

12
@ SyntaxT3rr0r, ho interpretato la tua domanda come se ci fosse qualcosa nella lingua che naturalmente porta a perdite di memoria. La risposta è no. Questa domanda chiede se è possibile escogitare una situazione per creare qualcosa come una perdita di memoria. Nessuno di questi esempi è una perdita di memoria nel modo in cui un programmatore C / C ++ lo comprenderebbe.
Peter Lawrey,

11
@Peter Lawrey: inoltre, cosa ne pensi di questo: "Non c'è nulla nel linguaggio C che perde naturalmente alla perdita di memoria se non si dimentica di liberare manualmente la memoria allocata" . Come sarebbe per la disonestà intellettuale? Ad ogni modo, sono stanco: puoi avere l'ultima parola.
SyntaxT3rr0r

271

Di seguito ci sarà un caso non ovvio in cui Java perde, oltre al caso standard di ascoltatori dimenticati, riferimenti statici, chiavi fasulle / modificabili in hashmap o solo thread bloccati senza alcuna possibilità di terminare il loro ciclo di vita.

  • File.deleteOnExit() - perde sempre la stringa, se la stringa è una sottostringa, la perdita è ancora peggiore (anche il carattere sottostante [] perde)- in Java 7 anche la sottostringa copia il char[], quindi il successivo non si applica ; @Daniel, non c'è bisogno di voti, però.

Mi concentrerò sui thread per mostrare principalmente il pericolo di thread non gestiti, non voglio nemmeno toccare lo swing.

  • Runtime.addShutdownHooke non rimuovere ... e quindi anche con removeShutdownHook a causa di un bug nella classe ThreadGroup relativo ai thread non avviati che potrebbe non essere raccolto, perde effettivamente il ThreadGroup. JGroup ha la perdita in GossipRouter.

  • La creazione, ma non l'avvio, Threadentra nella stessa categoria di cui sopra.

  • La creazione di un thread eredita ContextClassLoadere AccessControlContext, più il ThreadGroupe qualsiasi InheritedThreadLocal, tutti quei riferimenti sono potenziali perdite, insieme alle intere classi caricate dal classloader e tutti i riferimenti statici e ja-ja. L'effetto è particolarmente visibile con l'intero framework jucExecutor che presenta un'interfaccia super semplice ThreadFactory, ma la maggior parte degli sviluppatori non ha idea del pericolo in agguato. Inoltre, molte librerie iniziano i thread su richiesta (troppe librerie popolari del settore).

  • ThreadLocalcache; quelli sono cattivi in ​​molti casi. Sono sicuro che tutti hanno visto un bel po 'di semplici cache basate su ThreadLocal, beh la cattiva notizia: se il thread continua ad andare più del previsto nella vita del contesto ClassLoader, è una piccola perdita pura. Non utilizzare le cache ThreadLocal a meno che non siano realmente necessarie.

  • Chiamare ThreadGroup.destroy()quando il ThreadGroup non ha thread in sé, ma mantiene comunque ThreadGroups figlio. Una cattiva perdita che impedirà a ThreadGroup di rimuoverlo dal suo genitore, ma tutti i figli non saranno enumerabili.

  • Utilizzando WeakHashMap e il valore (in) fa riferimento direttamente alla chiave. Questo è difficile da trovare senza una discarica. Questo vale per tutti gli estesi Weak/SoftReferenceche potrebbero mantenere un riferimento rigido all'oggetto protetto.

  • Utilizzo java.net.URLcon il protocollo HTTP (S) e caricamento della risorsa da (!). Questo è speciale, KeepAliveCachecrea un nuovo thread nel ThreadGroup di sistema che perde il classloader di contesto del thread corrente. Il thread viene creato alla prima richiesta quando non esiste alcun thread attivo, quindi potresti essere fortunato o semplicemente perdere. La perdita è già stata risolta in Java 7 e il codice che crea il thread rimuove correttamente il classloader di contesto.Ci sono pochi altri casi (come ImageFetcher, anche risolto ) di creazione di thread simili.

  • Usando InflaterInputStreampassare new java.util.zip.Inflater()nel costruttore ( PNGImageDecoderper esempio) e non chiamare end()il gonfiatore. Bene, se passi nel costruttore con solo new, nessuna possibilità ... E sì, chiamando close()il flusso non chiude il gonfiatore se viene passato manualmente come parametro del costruttore. Questa non è una vera perdita poiché sarebbe stata rilasciata dal finalizzatore ... quando lo riterrà necessario. Fino a quel momento mangia così tanta memoria nativa che può causare Linux oom_killer a uccidere il processo con impunità. Il problema principale è che la finalizzazione in Java è molto inaffidabile e G1 ha peggiorato la situazione fino alla 7.0.2. Morale della storia: rilasciare risorse native il prima possibile; il finalizzatore è troppo scarso.

  • Lo stesso caso con java.util.zip.Deflater. Questo è molto peggio poiché Deflater ha molta memoria in Java, ovvero usa sempre 15 bit (max) e 8 livelli di memoria (9 è max) allocando diverse centinaia di KB di memoria nativa. Fortunatamente, Deflaternon è ampiamente usato e per quanto ne so JDK non contiene abusi. Chiama sempre end()se crei manualmente un Deflatero Inflater. La parte migliore degli ultimi due: non puoi trovarli tramite i normali strumenti di profilazione disponibili.

(Posso aggiungere altri perditempo che ho incontrato su richiesta.)

Buona fortuna e stai al sicuro; le perdite sono cattive!


23
Creating but not starting a Thread...Yikes, sono stato gravemente morso da questo alcuni secoli fa! (Java 1.3)
leonbloy,

@leonbloy, prima che fosse anche peggio poiché il thread veniva aggiunto direttamente al threadgroup, non iniziare significava una perdita molto difficile. Non solo aumenta il unstartedconteggio, ma ciò impedisce alla distruzione del gruppo di thread (minore male ma comunque una perdita)
bestsss

Grazie! "Chiamare ThreadGroup.destroy()quando il ThreadGroup non ha thread in sé ..." è un bug incredibilmente sottile; L'ho inseguito per ore, sviato perché l'enumerazione del thread nella mia GUI di controllo non mostrava nulla, ma il gruppo di thread e, presumibilmente, almeno un gruppo figlio non sarebbe andato via.
Lawrence Dol,

1
@bestsss: Sono curioso, perché dovresti rimuovere un hook di spegnimento, dato che funziona allo spegnimento di JVM?
Lawrence Dol,

203

La maggior parte degli esempi qui sono "troppo complessi". Sono casi limite. Con questi esempi, il programmatore ha commesso un errore (come non ridefinire equals / hashcode), oppure è stato morso da un caso angolare di JVM / JAVA (carico di classe con static ...). Penso che non sia il tipo di esempio desiderato da un intervistatore o anche il caso più comune.

Ma ci sono casi davvero più semplici per le perdite di memoria. Il garbage collector libera solo ciò a cui non si fa più riferimento. Noi come sviluppatori Java non ci importa della memoria. Lo allociamo quando necessario e lo lasciamo liberare automaticamente. Belle.

Ma qualsiasi applicazione di lunga durata tende ad avere uno stato condiviso. Può essere qualsiasi cosa, statica, singoli ... Spesso le applicazioni non banali tendono a creare grafici di oggetti complessi. Basta dimenticare di impostare un riferimento su null o più spesso dimenticare di rimuovere un oggetto da una raccolta è sufficiente per perdere la memoria.

Naturalmente tutti i tipi di ascoltatori (come i listener dell'interfaccia utente), le cache o qualsiasi stato condiviso di lunga durata tendono a produrre perdite di memoria se non gestiti correttamente. Ciò che deve essere compreso è che questo non è un caso d'angolo Java o un problema con il Garbage Collector. È un problema di progettazione. Progettiamo di aggiungere un ascoltatore a un oggetto di lunga durata, ma non rimuoviamo l'ascoltatore quando non è più necessario. Memorizziamo gli oggetti nella cache, ma non abbiamo una strategia per rimuoverli dalla cache.

Forse abbiamo un grafico complesso che memorizza lo stato precedente necessario per un calcolo. Ma lo stato precedente è esso stesso collegato allo stato precedente e così via.

Come se dovessimo chiudere connessioni o file SQL. Dobbiamo impostare riferimenti adeguati su null e rimuovere elementi dalla raccolta. Avremo strategie di memorizzazione nella cache adeguate (dimensione massima della memoria, numero di elementi o timer). Tutti gli oggetti che consentono la notifica a un listener devono fornire sia un metodo addListener sia il metodo removeListener. E quando questi notificatori non vengono più utilizzati, devono cancellare il loro elenco di ascoltatori.

Una perdita di memoria è davvero possibile ed è perfettamente prevedibile. Non sono necessarie funzionalità linguistiche speciali o custodie angolari. Le perdite di memoria sono un indicatore della mancanza di qualcosa o addirittura di problemi di progettazione.


24
Trovo divertente che su altre risposte le persone stiano cercando quei casi limite e quei trucchi e sembrano mancare completamente il punto. Potrebbero semplicemente mostrare codice che mantiene inutili riferimenti a oggetti che non useranno mai più e non rimuovono mai quei riferimenti; si potrebbe dire che quei casi non sono "vere" perdite di memoria perché ci sono ancora riferimenti a quegli oggetti in giro, ma se il programma non usa mai più quei riferimenti e non li rilasciano mai, è completamente equivalente a (e cattivo come) a " vera perdita di memoria ".
ehabkost,

@Nicolas Bousquet: "La perdita di memoria è davvero possibile" Grazie mille. +15 voti positivi. Bello. Sono stato urlato qui per aver affermato questo fatto, come premessa di una domanda sulla lingua Go: stackoverflow.com/questions/4400311 Questa domanda ha ancora negativi negativi :(
SyntaxT3rr0r

Il GC in Java e .NET è in un certo senso basato sul grafico dell'assunzione di oggetti che contengono riferimenti ad altri oggetti è lo stesso del grafico degli oggetti che "si preoccupano" di altri oggetti. In realtà, è possibile che nel grafico di riferimento esistano degli spigoli che non rappresentano relazioni "premurose" ed è possibile che un oggetto si preoccupi dell'esistenza di un altro oggetto anche se non esiste un percorso di riferimento diretto o indiretto (anche usando WeakReference) dall'uno all'altro. Se un riferimento a un oggetto avesse un bit di riserva, potrebbe essere utile avere un indicatore "si preoccupa per il bersaglio" ...
supercat

... e fai in modo che il sistema fornisca notifiche (tramite mezzi simili a PhantomReference) se un oggetto è stato trovato non avere a nessuno interessarsene. WeakReferencesi avvicina un po ', ma deve essere convertito in un riferimento forte prima di poter essere utilizzato; se si verifica un ciclo GC mentre esiste un riferimento forte, si presume che l'obiettivo sia utile.
supercat,

Questa è secondo me la risposta corretta. Abbiamo scritto una simulazione anni fa. In qualche modo abbiamo accidentalmente collegato lo stato precedente allo stato corrente creando una perdita di memoria. A causa di una scadenza non abbiamo mai risolto la perdita di memoria ma l'abbiamo resa una "caratteristica" documentandola.
nalply,

163

La risposta dipende interamente da ciò che l'intervistatore pensava stessero chiedendo.

È possibile in pratica far perdere Java? Certo che lo è, e ci sono molti esempi nelle altre risposte.

Ma ci sono più meta-domande che potrebbero essere state poste?

  • Un'implementazione Java teoricamente "perfetta" è vulnerabile alle perdite?
  • Il candidato comprende la differenza tra teoria e realtà?
  • Il candidato capisce come funziona la garbage collection?
  • O come dovrebbe funzionare la garbage collection in un caso ideale?
  • Sanno che possono chiamare altre lingue tramite interfacce native?
  • Sanno che perdono memoria in quelle altre lingue?
  • Il candidato sa anche cos'è la gestione della memoria e cosa sta succedendo dietro la scena in Java?

Sto leggendo la tua meta-domanda come "Qual è la risposta che avrei potuto usare in questa situazione di intervista". E quindi, mi concentrerò sulle abilità di intervista anziché su Java. Credo che tu abbia più probabilità di ripetere la situazione di non conoscere la risposta a una domanda in un'intervista piuttosto che di trovarti in un luogo in cui devi sapere come far fuoriuscire Java. Quindi, si spera, questo aiuterà.

Una delle abilità più importanti che puoi sviluppare per il colloquio è imparare ad ascoltare attivamente le domande e lavorare con l'intervistatore per estrarre il loro intento. Questo non solo ti consente di rispondere alle loro domande nel modo che desiderano, ma mostra anche che hai alcune abilità comunicative vitali. E quando si tratta di scegliere tra molti sviluppatori altrettanto talentuosi, assumerò colui che ascolta, pensa e capisce prima che rispondano ogni volta.


22
Ogni volta che ho posto questa domanda, cerco una risposta piuttosto semplice: continuare a far crescere una coda, non chiudere finalmente db ecc., Non dispari dettagli del classloader / thread, implica che capiscono cosa può fare e cosa non può fare il gc per te. Dipende dal lavoro che stai intervistando, immagino.
DaveC,

Dai un'occhiata alla mia domanda, grazie stackoverflow.com/questions/31108772/…
Daniel Newtown,

130

Il seguente è un esempio abbastanza inutile, se non capisci JDBC . O almeno come JDBC si aspetta che uno sviluppatore chiuda Connection, Statemente le ResultSetistanze prima di scartarle o perdere riferimenti a loro, invece di fare affidamento sull'implementazione di 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);
   }
}

Il problema con quanto sopra è che l' Connectionoggetto non è chiuso, e quindi la connessione fisica rimarrà aperta, fino a quando il garbage collector non si accorge e vede che è irraggiungibile. GC invocherà il finalizemetodo, ma ci sono driver JDBC che non implementano finalize, almeno non nello stesso modo in cui Connection.closeè implementato. Il comportamento risultante è che mentre la memoria verrà recuperata a causa della raccolta di oggetti non raggiungibili, le risorse (compresa la memoria) associate Connectionall'oggetto potrebbero semplicemente non essere recuperate.

In tal caso in cui il metodo del Connection' finalizenon pulisce tutto, si potrebbe effettivamente scoprire che la connessione fisica al server di database durerà diversi cicli di garbage collection, fino a quando il server di database alla fine non scoprirà che la connessione non è attiva (se sì) e dovrebbe essere chiuso.

Anche se il driver JDBC dovesse essere implementato finalize, è possibile che vengano generate eccezioni durante la finalizzazione. Il comportamento risultante è che qualsiasi memoria associata all'oggetto ora "dormiente" non verrà recuperata, poiché finalizeè garantito che venga invocata una sola volta.

Lo scenario sopra riportato di incontrare eccezioni durante la finalizzazione degli oggetti è correlato a un altro scenario che potrebbe portare a una perdita di memoria: la resurrezione dell'oggetto. La resurrezione dell'oggetto viene spesso effettuata intenzionalmente creando un forte riferimento all'oggetto da finalizzare, da un altro oggetto. Quando la resurrezione dell'oggetto viene utilizzata in modo improprio, si verificherà una perdita di memoria in combinazione con altre fonti di perdite di memoria.

Ci sono molti altri esempi che puoi evocare - come

  • Gestire Listun'istanza in cui si sta solo aggiungendo all'elenco e non si elimina da esso (anche se è necessario eliminare gli elementi non più necessari), oppure
  • Aprire Sockets o Files, ma non chiuderli quando non sono più necessari (simile all'esempio sopra che coinvolge la Connectionclasse).
  • Non scaricare Singletons quando si annulla un'applicazione Java EE. Apparentemente, il Classloader che ha caricato la classe singleton manterrà un riferimento alla classe, e quindi l'istanza singleton non verrà mai raccolta. Quando viene distribuita una nuova istanza dell'applicazione, viene in genere creato un nuovo caricatore di classi e il precedente caricatore di classi continuerà a esistere a causa del singleton.

98
Raggiungerai il limite massimo di connessione aperta prima di raggiungere normalmente i limiti di memoria. Non chiedermi perché lo so ...
Hardwareguy,

Il driver Oracle JDBC è noto per farlo.
Chotchki,

@Hardwareguy Ho raggiunto molto i limiti di connessione dei database SQL fino a quando non ho inserito Connection.closeil blocco di tutte le mie chiamate SQL. Per ulteriore divertimento, ho chiamato alcune stored procedure Oracle di lunga durata che richiedevano blocchi sul lato Java per impedire troppe chiamate al database.
Michael Shopsin

@Hardwareguy È interessante, ma non è davvero necessario che vengano raggiunti limiti di connessione effettivi per tutti gli ambienti. Ad esempio, per un'applicazione distribuita sul server di app weblogic 11g ho visto perdite di connessione su larga scala. Ma a causa di un'opzione di raccolta delle connessioni trapelate, le connessioni al database sono rimaste disponibili durante l'introduzione delle perdite di memoria. Non sono sicuro di tutti gli ambienti.
Aseem Bansal,

Nella mia esperienza hai una perdita di memoria anche se chiudi la connessione. È necessario prima chiudere ResultSet e PreparedStatement. Aveva un server che si arrestava in modo anomalo ripetutamente dopo ore o addirittura giorni di lavoro, a causa di OutOfMemoryErrors, fino a quando non ho iniziato a farlo.
Bjørn Stenfeldt,

119

Probabilmente uno degli esempi più semplici di una potenziale perdita di memoria, e come evitarlo, è l'implementazione di 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;
}

Se lo stessi implementando da solo, avresti pensato di cancellare l'elemento array che non viene più utilizzato ( elementData[--size] = null)? Tale riferimento potrebbe mantenere in vita un oggetto enorme ...


5
E dov'è la perdita di memoria qui?
rds

28
@maniek: non intendevo implicare che questo codice presenti una perdita di memoria. L'ho citato per dimostrare che a volte è necessario un codice non ovvio per evitare la ritenzione accidentale degli oggetti.
Meriton,

Che cos'è RangeCheck (indice); ?
Koray Tugay,

6
Joshua Bloch ha fornito questo esempio in Effective Java che mostra una semplice implementazione di Stacks. Un'ottima risposta
affitta il

Ma quella non sarebbe una VERA perdita di memoria, anche se dimenticata. L'elemento sarebbe comunque accessibile in SICUREZZA con Reflection, non sarebbe ovvio e direttamente accessibile tramite l'interfaccia Elenco, ma l'oggetto e il riferimento sono ancora lì e possono essere raggiunti in modo sicuro.
DGoiko,

68

Ogni volta che conservi riferimenti ad oggetti che non ti servono più, hai una perdita di memoria. Vedere Gestione delle perdite di memoria nei programmi Java per esempi di come le perdite di memoria si manifestano in Java e cosa è possibile fare al riguardo.


14
Non credo che questa sia una "perdita". È un bug ed è progettato dal programma e dalla lingua. Una perdita sarebbe un oggetto in giro senza alcun riferimento ad esso.
user541686,

29
@Mehrdad: questa è solo una definizione ristretta che non si applica completamente a tutte le lingue. Direi che qualsiasi perdita di memoria è un errore causato dalla cattiva progettazione del programma.
Bill the Lizard,

9
@Mehrdad: ...then the question of "how do you create a memory leak in X?" becomes meaningless, since it's possible in any language. non vedo come stai per trarre quella conclusione. Esistono meno modi per creare una perdita di memoria in Java con qualsiasi definizione. È sicuramente ancora una domanda valida.
Bill the Lizard,

7
@ 31eee384: se il tuo programma mantiene in memoria oggetti che non potrà mai usare, tecnicamente è una perdita di memoria. Il fatto che tu abbia problemi più grandi non lo cambia davvero.
Bill the Lizard,

8
@ 31eee384: se sai per certo che non lo sarà, non può esserlo. Il programma, come scritto, non accederà mai ai dati.
Bill the Lizard,

51

Sei in grado di perdere memoria con la classe sun.misc.Unsafe . In effetti questa classe di servizio viene utilizzata in diverse classi standard (ad esempio nelle classi java.nio ). Non è possibile creare direttamente l'istanza di questa classe , ma è possibile utilizzare reflection per farlo .

Il codice non viene compilato in Eclipse IDE: compilalo usando il comando javac(durante la compilazione riceverai degli avvisi)

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();
        }
    }

}

1
La memoria allocata è invisibile per il garbage collector
deriva l'

4
Anche la memoria allocata non appartiene a Java.
bestsss

Questo sole / oracolo jvm è specifico? Ad esempio, funzionerà su IBM?
Berlin Brown,

2
La memoria certamente "appartiene a Java", almeno nel senso che i) non è disponibile per nessun altro, ii) quando l'applicazione Java esce, verrà restituita al sistema. È appena fuori dalla JVM.
Michael Anderson,

3
Questo verrà creato in eclipse (almeno nelle versioni recenti) ma dovrai modificare le impostazioni del compilatore: in Finestra> Preferenze> Java> Compilatore> Errori / Avviso> Set API obsoleto e limitato Riferimento proibito (regole di accesso) su "Avviso ".
Michael Anderson,

43

Posso copiare la mia risposta da qui: il modo più semplice per causare perdite di memoria in Java?

"Una perdita di memoria, nell'informatica (o una perdita, in questo contesto), si verifica quando un programma per computer consuma memoria ma non è in grado di rilasciarla nuovamente nel sistema operativo." (Wikipedia)

La risposta semplice è: non puoi. Java gestisce automaticamente la memoria e libera risorse che non sono necessarie per te. Non puoi impedire che ciò accada. Sarà SEMPRE in grado di liberare le risorse. Nei programmi con gestione manuale della memoria, questo è diverso. Puoi ottenere un po 'di memoria in C usando malloc (). Per liberare la memoria, è necessario il puntatore che malloc ha restituito e chiamare free () su di esso. Ma se non hai più il puntatore (sovrascritto o la durata è stata superata), sfortunatamente non sei in grado di liberare questa memoria e quindi hai una perdita di memoria.

Tutte le altre risposte finora sono nella mia definizione non proprio perdite di memoria. Mirano tutti a riempire la memoria con cose inutili molto velocemente. Ma in qualsiasi momento potresti comunque dereferenziare gli oggetti che hai creato e liberare così la memoria -> NO LEAK. la risposta di acconrad arriva piuttosto vicina, anche se devo ammettere che la sua soluzione è effettivamente quella di "schiantare" il cestino della spazzatura forzandolo in un ciclo infinito).

La risposta lunga è: è possibile ottenere una perdita di memoria scrivendo una libreria per Java utilizzando JNI, che può avere una gestione manuale della memoria e quindi avere perdite di memoria. Se si chiama questa libreria, il processo java perderà memoria. Oppure, puoi avere dei bug nella JVM, in modo che la JVM perda la memoria. Probabilmente ci sono bug nella JVM, potrebbero essercene anche alcuni noti poiché la garbage collection non è così banale, ma è comunque un bug. In base alla progettazione questo non è possibile. È possibile che tu stia chiedendo del codice java che viene effettuato da un tale bug. Spiacente, non ne conosco uno e potrebbe non essere più un bug nella prossima versione di Java.


12
Questa è una definizione estremamente limitata (e non molto utile) di perdite di memoria. L'unica definizione che ha senso ai fini pratici è "una perdita di memoria è qualsiasi condizione in cui il programma continua a conservare la memoria allocata dopo che i dati in essa contenuti non sono più necessari".
Mason Wheeler,

1
La risposta acconrad menzionata è stata cancellata?
Tomáš Zato - Ripristina Monica il

1
@ TomášZato: No non lo è. Ho girato il riferimento sopra per collegare ora, in modo da poterlo trovare facilmente.
Yankee,

Che cos'è la resurrezione degli oggetti? Quante volte viene chiamato un distruttore? In che modo queste domande confutano questa risposta?
Autistico,

1
Bene, certo che puoi creare una perdita di memoria all'interno di Java, nonostante GC, e comunque soddisfare la definizione di cui sopra. È sufficiente disporre di una struttura di dati di sola aggiunta, protetta dall'accesso esterno in modo che nessun altro codice lo rimuova: il programma non può rilasciare la memoria perché non ha il codice.
Toolforger,

39

Eccone uno semplice / sinistro su http://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29 .

public class StringLeaker
{
    private final String muchSmallerString;

    public StringLeaker()
    {
        // Imagine the whole Declaration of Independence here
        String veryLongString = "We hold these truths to be self-evident...";

        // The substring here maintains a reference to the internal char[]
        // representation of the original string.
        this.muchSmallerString = veryLongString.substring(0, 1);
    }
}

Poiché la sottostringa si riferisce alla rappresentazione interna della stringa originale, molto più lunga, l'originale rimane in memoria. Pertanto, fintanto che hai uno StringLeaker in gioco, hai anche tutta la stringa originale in memoria, anche se potresti pensare di aggrapparti a una stringa a carattere singolo.

Il modo per evitare di memorizzare un riferimento indesiderato alla stringa originale è fare qualcosa del genere:

...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...

Per ulteriore cattiveria, potresti anche .intern()la sottostringa:

...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...

Ciò manterrà in memoria sia la stringa lunga originale che la sottostringa derivata anche dopo che l'istanza StringLeaker è stata scartata.


4
Non la definirei una perdita di memoria, di per sé . Quando muchSmallerStringviene liberato (poiché l' StringLeakeroggetto viene distrutto), verrà liberata anche la stringa lunga. Ciò che chiamo perdita di memoria è la memoria che non può mai essere liberata in questa istanza di JVM. Tuttavia, si è dimostrato persona come per liberare la memoria: this.muchSmallerString=new String(this.muchSmallerString). Con una vera perdita di memoria, non c'è niente che tu possa fare.
martedì

2
@rds, questo è un punto giusto. Il non interncaso potrebbe essere più una "sorpresa di memoria" che una "perdita di memoria". .intern()la sottostringa, tuttavia, crea certamente una situazione in cui il riferimento alla stringa più lunga viene conservato e non può essere liberato.
Jon Chambers,

15
Il metodo substring () crea una nuova stringa in java7 (è un nuovo comportamento)
anstarovoyt

Non è nemmeno necessario eseguire la sottostringa () da soli: utilizzare un matcher regex per abbinare una piccola parte di un input enorme e portare la stringa "estratta" in giro per molto tempo. L'enorme contributo rimane vivo fino a Java 6.
Bananeweizen,

37

Un esempio comune di ciò nel codice della GUI è quando si crea un widget / componente e si aggiunge un listener a qualche oggetto con ambito statico / applicativo e non si rimuove il listener quando il widget viene distrutto. Non solo si verifica una perdita di memoria, ma anche un colpo di scena come quando si ascoltano eventi di incendi, vengono chiamati anche tutti i vecchi ascoltatori.


1
La piattaforma Android offre l'esempio di una perdita di memoria creata memorizzando nella cache una bitmap nel campo statico di una vista .
rds

36

Prendi qualsiasi applicazione web in esecuzione in qualsiasi contenitore servlet (Tomcat, Jetty, Glassfish, qualunque cosa ...). Ridistribuire l'app 10 o 20 volte di seguito (potrebbe essere sufficiente toccare semplicemente il WAR nella directory di distribuzione automatica del server.

A meno che nessuno lo abbia effettivamente testato, è molto probabile che otterrai un OutOfMemoryError dopo un paio di ridistribuzioni, perché l'applicazione non si è occupata di ripulire dopo se stessa. Con questo test potresti persino trovare un bug nel tuo server.

Il problema è che la durata del contenitore è più lunga della durata dell'applicazione. Devi assicurarti che tutti i riferimenti che il contenitore potrebbe avere su oggetti o classi della tua applicazione possano essere raccolti.

Se esiste un solo riferimento sopravvissuto alla disoccupazione della tua app Web, il classloader corrispondente e di conseguenza tutte le classi della tua app web non possono essere spazzate via.

I thread avviati dall'applicazione, le variabili ThreadLocal e gli appendici di registrazione sono alcuni dei soliti sospetti che causano perdite del programma di caricamento classi.


1
Ciò non è dovuto a una perdita di memoria, ma perché il programma di caricamento classi non scarica il set di classi precedente. Pertanto non è consigliabile ridistribuire un server delle applicazioni senza riavviare il server (non la macchina fisica, ma il server delle app). Ho visto lo stesso problema con WebSphere.
Sven,

35

Forse usando un codice nativo esterno tramite JNI?

Con Java puro, è quasi impossibile.

Ma si tratta di un tipo "standard" di perdita di memoria, quando non è più possibile accedere alla memoria, ma è ancora di proprietà dell'applicazione. Puoi invece conservare riferimenti ad oggetti inutilizzati o aprire flussi senza chiuderli successivamente.


22
Ciò dipende dalla definizione di "perdita di memoria". Se "memoria trattenuta, ma non più necessaria", allora è facile farlo in Java. Se si tratta di "memoria allocata ma non accessibile dal codice", diventa leggermente più difficile.
Joachim Sauer,

@Joachim Sauer - Intendevo il secondo tipo. Il primo è abbastanza facile da realizzare :)
Rogach,

6
"Con java puro, è quasi impossibile." Bene, la mia esperienza è un'altra soprattutto quando si tratta di implementare cache da persone che non sono a conoscenza delle insidie ​​qui.
Fabian Barney,

4
@Rogach: ci sono fondamentalmente +400 voti positivi su varie risposte di persone con +10 000 rappresentanti che dimostrano che in entrambi i casi Joachim Sauer ha commentato che è molto possibile. Quindi il tuo "quasi impossibile" non ha senso.
SyntaxT3rr0r

32

Ho avuto una bella "perdita di memoria" in relazione all'analisi PermGen e XML una volta. Il parser XML che abbiamo usato (non ricordo quale fosse) ha fatto String.intern () sui nomi dei tag, per rendere il confronto più veloce. Uno dei nostri clienti ha avuto la grande idea di archiviare i valori dei dati non in attributi XML o testo, ma come tagname, quindi avevamo un documento come:

<data>
   <1>bla</1>
   <2>foo</>
   ...
</data>

In realtà, non usavano numeri ma ID testuali più lunghi (circa 20 caratteri), che erano unici e arrivavano al ritmo di 10-15 milioni al giorno. Ciò rende 200 MB di rifiuti al giorno, che non sono più necessari e mai GCed (poiché è in PermGen). Permgen era impostato su 512 MB, quindi ci sono voluti circa due giorni per l'eccezione di memoria insufficiente (OOME) per arrivare ...


4
Solo per fare il nitpick del tuo codice di esempio: penso che i numeri (o le stringhe che iniziano con i numeri) non siano consentiti come nomi di elementi in XML.
Paŭlo Ebermann,

Si noti che questo non è più vero per JDK 7+, in cui l'internamento di stringhe avviene sull'heap. Vedi questo articolo per una recensione dettagliata: java-performance.info/string-intern-in-java-6-7-8
jmiserez,

Quindi, penso che l'uso di StringBuffer al posto di String risolverà questo problema? non è vero?
anubhs

24

Che cos'è una perdita di memoria:

  • È causato da un bug o da un cattivo design.
  • È uno spreco di memoria.
  • Nel tempo peggiora.
  • Il garbage collector non può pulirlo.

Esempio tipico:

Una cache di oggetti è un buon punto di partenza per rovinare le cose.

private static final Map<String, Info> myCache = new HashMap<>();

public void getInfo(String key)
{
    // uses cache
    Info info = myCache.get(key);
    if (info != null) return info;

    // if it's not in cache, then fetch it from the database
    info = Database.fetch(key);
    if (info == null) return null;

    // and store it in the cache
    myCache.put(key, info);
    return info;
}

La tua cache cresce e cresce. E molto presto l'intero database viene risucchiato nella memoria. Una progettazione migliore utilizza un LRUMap (mantiene solo gli oggetti utilizzati di recente nella cache).

Certo, puoi rendere le cose molto più complicate:

  • utilizzando costruzioni ThreadLocal .
  • aggiungendo altro alberi di riferimento complessi .
  • o perdite causate da librerie di terze parti .

Cosa succede spesso:

Se questo oggetto Info ha riferimenti ad altri oggetti, che hanno di nuovo riferimenti ad altri oggetti. In un certo senso potresti anche considerare che si tratta di una sorta di perdita di memoria (causata da una cattiva progettazione).


22

Ho pensato che fosse interessante che nessuno usasse gli esempi di classe interni. Se hai una classe interna; mantiene intrinsecamente un riferimento alla classe contenente. Ovviamente non è tecnicamente una perdita di memoria perché Java alla fine lo pulirà; ma ciò può causare il blocco delle classi più a lungo del previsto.

public class Example1 {
  public Example2 getNewExample2() {
    return this.new Example2();
  }
  public class Example2 {
    public Example2() {}
  }
}

Ora se chiami Esempio1 e ottieni un Esempio2 che scarta Esempio1, avrai intrinsecamente ancora un collegamento a un oggetto Esempio1.

public class Referencer {
  public static Example2 GetAnExample2() {
    Example1 ex = new Example1();
    return ex.getNewExample2();
  }

  public static void main(String[] args) {
    Example2 ex = Referencer.GetAnExample2();
    // As long as ex is reachable; Example1 will always remain in memory.
  }
}

Ho anche sentito una voce secondo cui se hai una variabile che esiste da più di un determinato periodo di tempo; Java presume che esisterà sempre e in realtà non tenterà mai di ripulirlo se non è più possibile raggiungerlo nel codice. Ma questo è completamente non verificato.


2
le classi interne raramente rappresentano un problema. Sono un caso semplice e molto facile da rilevare. La voce è anche solo una voce.
bestsss

2
La "voce" sembra che qualcuno abbia letto a metà su come funziona la GC generazionale. Gli oggetti di lunga durata, ma ora irraggiungibili, possono davvero rimanere in piedi e occupare spazio per un po ', perché la JVM li ha promossi fuori dalle generazioni più giovani in modo da poter smettere di controllarli ad ogni passaggio. Eviteranno i passaggi "pulisci le mie 5000 stringhe temporali", secondo la progettazione. Ma non sono immortali. Sono ancora idonei per la raccolta e, se la VM è legata alla RAM, alla fine eseguirà una scansione completa del GC e recupererà quella memoria.
cHao,

22

Di recente ho riscontrato una situazione di perdita di memoria causata in qualche modo da log4j.

Log4j ha questo meccanismo chiamato Nested Diagnostic Context (NDC) che è uno strumento per distinguere l'output dei registri interfogliati da diverse fonti. La granularità con cui lavora NDC sono i thread, quindi distingue separatamente gli output dei log da thread diversi.

Per memorizzare tag specifici del thread, la classe NDC di log4j utilizza un Hashtable che è codificato dall'oggetto Thread stesso (al contrario di dire l'id del thread), e quindi fino a quando il tag NDC rimane in memoria tutti gli oggetti che pendono dal thread anche l'oggetto rimane in memoria. Nella nostra applicazione Web utilizziamo NDC per contrassegnare i logoutput con un ID richiesta per distinguere i registri da una singola richiesta separatamente. Il contenitore che associa il tag NDC a un thread lo rimuove anche restituendo la risposta da una richiesta. Il problema si è verificato quando nel corso dell'elaborazione di una richiesta è stato generato un thread figlio, simile al seguente codice:

pubclic class RequestProcessor {
    private static final Logger logger = Logger.getLogger(RequestProcessor.class);
    public void doSomething()  {
        ....
        final List<String> hugeList = new ArrayList<String>(10000);
        new Thread() {
           public void run() {
               logger.info("Child thread spawned")
               for(String s:hugeList) {
                   ....
               }
           }
        }.start();
    }
}    

Quindi un contesto NDC è stato associato al thread inline generato. L'oggetto thread che era la chiave per questo contesto NDC, è il thread inline che ha l'oggetto hugeList in sospeso. Quindi, anche dopo che il thread ha terminato di fare ciò che stava facendo, il riferimento all'enorme Elenco è stato mantenuto in vita dal contesto NDC Hastable, causando così una perdita di memoria.


Che schifo È necessario controllare questa libreria di registrazione che alloca memoria ZERO durante la registrazione in un file: mentalog.soliveirajr.com
TraderJoeChicago

+1 Sai a mano se c'è un problema simile con MDC in slf4j / logback (prodotti successivi dello stesso autore)? Sto per fare un tuffo profondo sulla fonte, ma volevo controllare prima. Ad ogni modo, grazie per aver pubblicato questo.
sparc_spread,

20

L'intervistatore stava probabilmente cercando un riferimento circolare come il codice seguente (che per inciso trapelava solo memoria in JVM molto vecchie che utilizzavano il conteggio dei riferimenti, il che non è più il caso). Ma è una domanda piuttosto vaga, quindi è un'occasione privilegiata per mostrare la tua comprensione della gestione della memoria JVM.

class A {
    B bRef;
}

class B {
    A aRef;
}

public class Main {
    public static void main(String args[]) {
        A myA = new A();
        B myB = new B();
        myA.bRef = myB;
        myB.aRef = myA;
        myA=null;
        myB=null;
        /* at this point, there is no access to the myA and myB objects, */
        /* even though both objects still have active references. */
    } /* main */
}

Quindi puoi spiegare che con il conteggio dei riferimenti, il codice sopra perderebbe memoria. Ma la maggior parte delle JVM moderne non usa più il conteggio dei riferimenti, la maggior parte usa un spazzino spazzatura, che in effetti raccoglierà questa memoria.

Successivamente potresti spiegare la creazione di un oggetto che ha una risorsa nativa sottostante, in questo modo:

public class Main {
    public static void main(String args[]) {
        Socket s = new Socket(InetAddress.getByName("google.com"),80);
        s=null;
        /* at this point, because you didn't close the socket properly, */
        /* you have a leak of a native descriptor, which uses memory. */
    }
}

Quindi puoi spiegare che questa è tecnicamente una perdita di memoria, ma in realtà la perdita è causata dal codice nativo nella JVM che alloca le risorse native sottostanti, che non sono state liberate dal tuo codice Java.

Alla fine della giornata, con una JVM moderna, è necessario scrivere del codice Java che alloca una risorsa nativa al di fuori del normale ambito di conoscenza della JVM.


19

Tutti dimenticano sempre la route del codice nativo. Ecco una semplice formula per una perdita:

  1. Dichiarare il metodo nativo.
  2. Nel metodo nativo, chiama malloc. Non chiamare free.
  3. Chiama il metodo nativo.

Ricorda, le allocazioni di memoria nel codice nativo provengono dall'heap JVM.


1
Basato su una storia vera.
Reg

18

Crea una mappa statica e continua ad aggiungere riferimenti rigidi ad essa. Quelli non saranno mai GC'd.

public class Leaker {
    private static final Map<String, Object> CACHE = new HashMap<String, Object>();

    // Keep adding until failure.
    public static void addToCache(String key, Object value) { Leaker.CACHE.put(key, value); }
}

87
Come è una perdita? Sta facendo esattamente quello che gli stai chiedendo di fare. Se questa è una perdita, la creazione e la memorizzazione di oggetti ovunque è una perdita.
Falmarri,

3
Sono d'accordo con @Falmarri. Non vedo una perdita lì, stai solo creando oggetti. Potresti certamente "recuperare" la memoria che hai appena allocato con un altro metodo chiamato 'removeFromCache'. Una perdita è quando non è possibile recuperare la memoria.
Kyle,

3
Il mio punto è che qualcuno che continua a creare oggetti, magari inserendoli in una cache, potrebbe finire con un errore OOM se non stanno attenti.
duffymo,

8
@duffymo: Ma non è proprio questa la domanda. Non ha nulla a che fare con il semplice utilizzo di tutta la memoria.
Falmarri,

3
Assolutamente invalido Stai solo raccogliendo un mucchio di oggetti in una raccolta Mappa. I loro riferimenti verranno conservati perché la Mappa li contiene.
Gyorgyabraham,

16

È possibile creare una perdita di memoria mobile creando una nuova istanza di una classe nel metodo finalize di quella classe. Punti bonus se il finalizzatore crea più istanze. Ecco un semplice programma che perde l'intero heap tra qualche secondo e qualche minuto, a seconda della dimensione dell'heap:

class Leakee {
    public void check() {
        if (depth > 2) {
            Leaker.done();
        }
    }
    private int depth;
    public Leakee(int d) {
        depth = d;
    }
    protected void finalize() {
        new Leakee(depth + 1).check();
        new Leakee(depth + 1).check();
    }
}

public class Leaker {
    private static boolean makeMore = true;
    public static void done() {
        makeMore = false;
    }
    public static void main(String[] args) throws InterruptedException {
        // make a bunch of them until the garbage collector gets active
        while (makeMore) {
            new Leakee(0).check();
        }
        // sit back and watch the finalizers chew through memory
        while (true) {
            Thread.sleep(1000);
            System.out.println("memory=" +
                    Runtime.getRuntime().freeMemory() + " / " +
                    Runtime.getRuntime().totalMemory());
        }
    }
}

15

Non credo che nessuno l'abbia ancora detto: puoi resuscitare un oggetto sovrascrivendo il metodo finalize () in modo tale che finalize () memorizzi un riferimento di questo da qualche parte. Il garbage collector verrà chiamato una sola volta sull'oggetto, quindi l'oggetto non verrà mai distrutto.


10
Questo non è vero. finalize()non verrà chiamato ma l'oggetto verrà raccolto una volta che non ci saranno più riferimenti. Nemmeno il garbage collector viene "chiamato".
bestsss

1
Questa risposta è fuorviante, il finalize()metodo può essere chiamato solo una volta dalla JVM, ma ciò non significa che non possa essere ri-immondizia raccolta se l'oggetto viene resuscitato e quindi nuovamente referenziato. Se nel finalize()metodo è presente un codice di chiusura delle risorse, questo codice non verrà eseguito nuovamente, ciò potrebbe causare una perdita di memoria.
Tom Cammann,

15

Di recente mi sono imbattuto in un tipo più sottile di perdita di risorse. Apriamo le risorse tramite getResourceAsStream del caricatore di classi ed è accaduto che gli handle del flusso di input non fossero chiusi.

Uhm, potresti dire, che idiota.

Bene, ciò che rende questo interessante è: in questo modo, puoi perdere la memoria heap del processo sottostante, piuttosto che dall'heap di JVM.

Tutto ciò che serve è un file jar con un file all'interno del quale verrà fatto riferimento dal codice Java. Più grande è il file jar, più memoria viene allocata.

Puoi facilmente creare un tale vaso con la seguente classe:

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class BigJarCreator {
    public static void main(String[] args) throws IOException {
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
        zos.putNextEntry(new ZipEntry("resource.txt"));
        zos.write("not too much in here".getBytes());
        zos.closeEntry();
        zos.putNextEntry(new ZipEntry("largeFile.out"));
        for (int i=0 ; i<10000000 ; i++) {
            zos.write((int) (Math.round(Math.random()*100)+20));
        }
        zos.closeEntry();
        zos.close();
    }
}

Basta incollare in un file chiamato BigJarCreator.java, compilare ed eseguirlo dalla riga di comando:

javac BigJarCreator.java
java -cp . BigJarCreator

Et voilà: trovi un archivio jar nella tua attuale directory di lavoro con due file all'interno.

Creiamo una seconda classe:

public class MemLeak {
    public static void main(String[] args) throws InterruptedException {
        int ITERATIONS=100000;
        for (int i=0 ; i<ITERATIONS ; i++) {
            MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
        }
        System.out.println("finished creation of streams, now waiting to be killed");

        Thread.sleep(Long.MAX_VALUE);
    }

}

Questa classe praticamente non fa nulla, ma crea oggetti InputStream senza riferimento. Tali oggetti verranno immediatamente raccolti e quindi non contribuiranno alla dimensione dell'heap. È importante per il nostro esempio caricare una risorsa esistente da un file jar e le dimensioni contano qui!

In caso di dubbi, prova a compilare e avviare la classe sopra, ma assicurati di scegliere una dimensione heap decente (2 MB):

javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak

Qui non si verificherà un errore OOM, poiché non vengono conservati riferimenti, l'applicazione continuerà a funzionare indipendentemente dalla grandezza scelta ITERAZIONI nell'esempio precedente. Il consumo di memoria del processo (visibile in alto (RES / RSS) o dell'esploratore del processo) aumenta a meno che l'applicazione non ottenga il comando wait. Nella configurazione sopra, allocerà circa 150 MB in memoria.

Se vuoi che l'applicazione giochi in modo sicuro, chiudi il flusso di input proprio dove è stato creato:

MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();

e il processo non supererà i 35 MB, indipendentemente dal conteggio delle iterazioni.

Abbastanza semplice e sorprendente.


14

Come molte persone hanno suggerito, le perdite di risorse sono abbastanza facili da causare, come negli esempi JDBC. Le perdite di memoria effettive sono un po 'più difficili, specialmente se non ti affidi a bit rotti della JVM per farlo per te ...

Le idee di creare oggetti che hanno un ingombro molto grande e quindi non poter accedere ad essi non sono nemmeno vere perdite di memoria. Se nulla può accedervi, allora sarà spazzatura raccolta e se qualcosa può accedervi, non è una perdita ...

Un modo che un tempo funzionava - e non so se lo sia ancora - è avere una catena circolare a tre profondità. Come nell'oggetto A ha un riferimento all'oggetto B, l'oggetto B ha un riferimento all'oggetto C e l'oggetto C ha un riferimento all'oggetto A. Il GC era abbastanza intelligente da sapere che una catena profonda due - come in A <--> B - può essere tranquillamente raccolto se A e B non sono accessibili da nessun altro, ma non sono in grado di gestire la catena a tre vie ...


7
Non è il caso da qualche tempo. I GC moderni sanno come gestire i riferimenti circolari.
assylias,

13

Un altro modo per creare perdite di memoria potenzialmente enormi è quello di contenere riferimenti a Map.Entry<K,V>di TreeMap.

È difficile capire perché questo si applica solo a TreeMaps, ma osservando l'implementazione la ragione potrebbe essere che: a TreeMap.Entrymemorizza i riferimenti ai suoi fratelli, quindi se a TreeMapè pronta per essere raccolta, ma qualche altra classe contiene un riferimento a uno qualsiasi dei suo Map.Entry, quindi l' intera mappa verrà mantenuta in memoria.


Scenario di vita reale:

Immagina di avere una query db che restituisce una TreeMapstruttura di big data. Le persone di solito usano TreeMaps come l'ordine di inserimento degli elementi viene mantenuto.

public static Map<String, Integer> pseudoQueryDatabase();

Se la query fosse chiamata molte volte e, per ogni query (quindi, per ogni Maprestituita) ne salvassi un Entryposto, la memoria continuerebbe a crescere.

Considera la seguente classe wrapper:

class EntryHolder {
    Map.Entry<String, Integer> entry;

    EntryHolder(Map.Entry<String, Integer> entry) {
        this.entry = entry;
    }
}

Applicazione:

public class LeakTest {

    private final List<EntryHolder> holdersCache = new ArrayList<>();
    private static final int MAP_SIZE = 100_000;

    public void run() {
        // create 500 entries each holding a reference to an Entry of a TreeMap
        IntStream.range(0, 500).forEach(value -> {
            // create map
            final Map<String, Integer> map = pseudoQueryDatabase();

            final int index = new Random().nextInt(MAP_SIZE);

            // get random entry from map
            for (Map.Entry<String, Integer> entry : map.entrySet()) {
                if (entry.getValue().equals(index)) {
                    holdersCache.add(new EntryHolder(entry));
                    break;
                }
            }
            // to observe behavior in visualvm
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

    }

    public static Map<String, Integer> pseudoQueryDatabase() {
        final Map<String, Integer> map = new TreeMap<>();
        IntStream.range(0, MAP_SIZE).forEach(i -> map.put(String.valueOf(i), i));
        return map;
    }

    public static void main(String[] args) throws Exception {
        new LeakTest().run();
    }
}

Dopo ogni pseudoQueryDatabase()chiamata, le mapistanze dovrebbero essere pronte per la raccolta, ma non accadrà, poiché almeno una Entryè memorizzata altrove.

A seconda delle jvmimpostazioni, l'applicazione potrebbe bloccarsi nella fase iniziale a causa di un OutOfMemoryError.

Puoi vedere da questo visualvmgrafico come la memoria continua a crescere.

Memory dump - TreeMap

Lo stesso non accade con una struttura di dati con hash ( HashMap).

Questo è il grafico quando si utilizza a HashMap.

Dump della memoria - HashMap

La soluzione? Basta salvare direttamente la chiave / valore (come probabilmente già fai) invece di salvare il Map.Entry.


Ho scritto un benchmark più ampio qui .


11

I thread non vengono raccolti fino alla loro conclusione. Servono come radici della raccolta dei rifiuti. Sono uno dei pochi oggetti che non saranno recuperati semplicemente dimenticandoli o cancellando i riferimenti ad essi.

Considerare: il modello di base per terminare un thread di lavoro è impostare una variabile di condizione vista dal thread. Il thread può controllare periodicamente la variabile e usarla come segnale per terminare. Se la variabile non viene dichiarata volatile, la modifica alla variabile potrebbe non essere vista dal thread, quindi non saprà terminare. Oppure immagina se alcuni thread vogliono aggiornare un oggetto condiviso, ma eseguono il deadlock mentre provano a bloccarlo.

Se hai solo una manciata di thread, questi bug saranno probabilmente ovvi perché il tuo programma smetterà di funzionare correttamente. Se si dispone di un pool di thread che crea più thread secondo necessità, è possibile che i thread obsoleti / bloccati non vengano notati e si accumulino indefinitamente, causando una perdita di memoria. È probabile che i thread utilizzino altri dati nella tua applicazione, quindi eviterà anche che qualsiasi cosa a cui fanno riferimento direttamente venga mai raccolta.

Come esempio giocattolo:

static void leakMe(final Object object) {
    new Thread() {
        public void run() {
            Object o = object;
            for (;;) {
                try {
                    sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {}
            }
        }
    }.start();
}

Chiama System.gc()tutto ciò che ti piace, ma l'oggetto a cui leakMepasserai non morirà mai.

(*modificato*)


1
@Spidey Nulla è "bloccato". Il metodo chiamante ritorna prontamente e l'oggetto passato non verrà mai recuperato. È proprio una perdita.
Boann,

1
Avrai un thread "in esecuzione" (o inattivo, qualunque cosa) per tutta la durata del tuo programma. Questo non conta come una perdita per me. Così come un pool non conta come una perdita, anche se non lo si utilizza interamente.
Spidey,

1
@Spidey "Avrai una [cosa] per tutta la durata del tuo programma. Per me non è una perdita." Ti senti?
Boann,

3
@Spidey Se consideri la memoria che il processo conosce come non trapelata, tutte le risposte qui sono errate, poiché il processo tiene sempre traccia delle pagine nel suo spazio di indirizzi virtuale mappate. Al termine del processo, il sistema operativo elimina tutte le perdite rimettendo le pagine nello stack di pagine libero. Per portarlo all'estremo successivo, si potrebbe battere a morte qualsiasi perdita discussa sottolineando che nessuno dei bit fisici nei chip RAM o nello spazio di scambio sul disco è stato spostato o distrutto fisicamente, quindi è possibile spegnere il computer e di nuovo per eliminare eventuali perdite.
Boann,

1
La definizione pratica di una perdita è che è la memoria che è stata persa di traccia di tale che non sappiamo e quindi non può eseguire la procedura necessaria per recuperarla solo; dovremmo abbattere e ricostruire l'intero spazio di memoria. Un thread canaglia come questo potrebbe sorgere naturalmente attraverso un deadlock o un'implementazione di threadpool ingannevole. Agli oggetti a cui fanno riferimento tali thread, anche indirettamente, viene ora impedito di essere mai raccolti, quindi abbiamo una memoria che non verrà recuperata o riutilizzata naturalmente durante la vita del programma. Lo definirei un problema; in particolare è una perdita di memoria.
Boann,

10

Penso che un valido esempio potrebbe essere l'utilizzo delle variabili ThreadLocal in un ambiente in cui i thread sono raggruppati.

Ad esempio, usando le variabili ThreadLocal nei servlet per comunicare con altri componenti Web, avendo i thread creati dal contenitore e mantenendo quelli inattivi in ​​un pool. Le variabili ThreadLocal, se non ripulite correttamente, resteranno lì fino a quando, possibilmente, lo stesso componente web non sovrascriverà i loro valori.

Naturalmente, una volta identificato, il problema può essere risolto facilmente.


10

L'intervistatore potrebbe aver cercato una soluzione di riferimento circolare:

    public static void main(String[] args) {
        while (true) {
            Element first = new Element();
            first.next = new Element();
            first.next.next = first;
        }
    }

Questo è un problema classico con riferimento al conteggio dei netturbini. Spiegheresti educatamente che le JVM usano un algoritmo molto più sofisticato che non ha questa limitazione.

-Wes Tarle


12
Questo è un problema classico con riferimento al conteggio dei netturbini. Anche 15 anni fa Java non usava il conteggio degli ref. Ref. il conteggio è anche più lento di GC.
bestsss

4
Non è una perdita di memoria. Solo un ciclo infinito.
Esben Skov Pedersen,

2
@Esben Ad ogni iterazione, la precedente firstnon è utile e dovrebbe essere raccolta dei rifiuti. Nel riferimento al conteggio dei garbage collector, l'oggetto non verrebbe liberato perché c'è un riferimento attivo su di esso (da solo). Il loop infinito è qui per smantellare la perdita: quando si esegue il programma, la memoria si alza indefinitamente.
RDS

@rds @ Wesley Tarle supponiamo che il loop non sia infinito. Ci sarebbe ancora una perdita di memoria?
nz_21
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.