Domanda Creare una perdita di memoria con Java


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

Quale sarebbe un esempio?


2634


origine


risposte:


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

  1. L'applicazione crea un thread di lunga durata (o utilizza un pool di thread per perdite ancora più veloci).
  2. Il thread carica una classe tramite un ClassLoader (opzionale).
  3. La classe alloca un grosso blocco di memoria (ad es. new byte[1000000]), memorizza un forte riferimento ad esso in un campo statico e quindi memorizza un riferimento a se stesso in un ThreadLocal. L'allocazione della memoria extra è facoltativa (la perdita dell'istanza di classe è sufficiente), ma renderà la perdita più veloce.
  4. Il thread cancella tutti i riferimenti alla classe personalizzata o al ClassLoader da cui è stato caricato.
  5. Ripetere.

Questo funziona perché ThreadLocal mantiene un riferimento all'oggetto, che mantiene un riferimento alla sua Classe, che a sua volta conserva un riferimento al suo ClassLoader. ClassLoader, a sua volta, mantiene un riferimento a tutte le Classi che ha caricato.

(E 'stato peggio in molte implementazioni JVM, specialmente prima di Java 7, perché Classi e ClassLoaders sono stati allocati direttamente in permgen e non sono mai stati GC. Tuttavia, indipendentemente da come la JVM gestisce lo scaricamento della classe, un ThreadLocal impedirà comunque un Oggetto di classe dall'essere recuperato.)

Una variazione su questo modello è il motivo per cui i contenitori di applicazioni (come Tomcat) possono perdere memoria come un setaccio se si ricolloca spesso le applicazioni che utilizzano ThreadLocals in qualche modo. (Poiché il contenitore dell'applicazione utilizza Thread come descritto e ogni volta che si ridistribuisce l'applicazione viene utilizzato un nuovo ClassLoader.)

Aggiornare: Dal momento che molte persone continuano a chiederlo, ecco un esempio di codice che mostra questo comportamento in azione.


1929



Riferimento dell'oggetto statico contenente il campo [esp finale field]

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

chiamata String.intern() sulla lunga stringa

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

(Unclosed) open stream (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 irraggiungibili dal garbage collector di JVM, ad esempio memoria allocata tramite metodi nativi

Nelle applicazioni Web, alcuni oggetti vengono archiviati nell'ambito dell'applicazione finché l'applicazione non viene arrestata o rimossa esplicitamente.

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

Opzioni JVM errate o inappropriate, come il noclassgc opzione su IBM JDK che impedisce la garbage collection della classe inutilizzata

Vedere Impostazioni jdk di IBM.


1058



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

Se vuoi aggirare queste chiavi / elementi sbagliati 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.

390



Di seguito ci sarà un caso non ovvio in cui Java perde, oltre al caso standard di ascoltatori dimenticati, riferimenti statici, chiavi false / modificabili nelle 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 (viene anche trapelata la variabile sottostante [] - nella sottostringa Java 7 copia anche il file char[], quindi il più tardi non si applica; @ Daniel, non c'è bisogno di voti, però.

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

  • Runtime.addShutdownHook e non rimuovere ... e quindi anche con removeShutdownHook a causa di un bug nella classe ThreadGroup riguardante i thread non avviati che potrebbero non essere raccolti, in modo efficace perdono il ThreadGroup. JGroup ha la perdita in GossipRouter.

  • Creare, ma non iniziare, a Thread entra nella stessa categoria di cui sopra.

  • La creazione di un thread eredita il ContextClassLoader e AccessControlContext, più il ThreadGroup e 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 j.u.c.Executor che presenta un super semplice ThreadFactory interfaccia, ma la maggior parte degli sviluppatori non ha idea del pericolo in agguato. Inoltre molte librerie avviano i thread su richiesta (troppe librerie popolari di settore).

  • ThreadLocal cache; quelli sono cattivi in ​​molti casi. Sono sicuro che tutti hanno visto un po 'di semplici cache basate su ThreadLocal, beh, le cattive notizie: se il thread continua ad andare oltre le aspettative la vita del contesto ClassLoader, si tratta di una pura piccola perdita. Non utilizzare le cache ThreadLocal se non strettamente necessario.

  • chiamata ThreadGroup.destroy() quando ThreadGroup non ha thread in sé, ma mantiene comunque ThreadGroups figlio. Una perdita grave che impedirà al ThreadGroup di rimuoverlo dal suo genitore, ma tutti i bambini diventano non enumerabili.

  • Usando WeakHashMap e il valore (in) fa direttamente riferimento alla chiave. Questo è difficile da trovare senza una discarica di heap. Questo vale per tutti esteso Weak/SoftReference quello potrebbe mantenere un riferimento duro all'oggetto custodito.

  • utilizzando java.net.URL con il protocollo HTTP (S) e caricando la risorsa da (!). Questo è speciale, il KeepAliveCache crea un nuovo thread nel sistema ThreadGroup che perde il classloader di contesto corrente del thread. 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 creare thread simili.

  • utilizzando InflaterInputStream passaggio new java.util.zip.Inflater() nel costruttore (PNGImageDecoder per esempio) e non chiamando end() del gonfiatore. Bene, se passi il costruttore con giusto new, nessuna possibilità ... E sì, chiamando close() nello stream non chiude il gonfiatore se viene passato manualmente come parametro costruttore. Questa non è una vera perdita dal momento che sarebbe stata rilasciata dal finalizzatore ... quando lo riterrà necessario. Fino a quel momento mangia così male la memoria nativa che può causare a Linux oom_killer di uccidere il processo impunemente. Il problema principale è che la finalizzazione in Java è molto inaffidabile e G1 ha peggiorato la situazione fino alla 7.0.2. Morale della trama: rilascia le risorse native non appena puoi; il finalizzatore è troppo povero.

  • Lo stesso caso con java.util.zip.Deflater. Questo è molto peggiore da quando Deflater ha fame di memoria in Java, cioè usa sempre 15 bit (max) e 8 livelli di memoria (9 è massimo) allocando diverse centinaia di KB di memoria nativa. Fortunatamente, Deflater non è ampiamente utilizzato e, a mia conoscenza, JDK non contiene abusi. Chiama sempre end() se si crea manualmente a Deflater o Inflater. La parte migliore degli ultimi due: non puoi trovarli tramite normali strumenti di profilazione disponibili.

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

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


228



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

Ma ci sono casi davvero più semplici per perdite di memoria. Il garbage collector libera solo ciò che non è più referenziato. Noi come sviluppatori Java non ci interessa la memoria. Lo allociamo quando necessario e lo lasciamo automaticamente libero. Belle.

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

Ovviamente tutti i tipi di ascoltatori (come gli ascoltatori dell'interfaccia utente), le cache o qualsiasi stato condiviso di lunga durata tendono a produrre perdite di memoria se non gestite correttamente. Ciò che deve essere compreso è che questo non è un caso angolare Java o un problema con il garbage collector. È un problema di progettazione. Progettiamo di aggiungere un listener a un oggetto longevo, ma non rimuoviamo il listener quando non è più necessario. Memorizziamo gli oggetti nella cache, ma non abbiamo alcuna 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 prima e così via.

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

Una perdita di memoria è davvero possibile ed è perfettamente prevedibile. Non sono necessarie caratteristiche linguistiche speciali o casi angolari. Le perdite di memoria sono o un indicatore che qualcosa è forse mancante o anche di problemi di progettazione.


150



La risposta dipende interamente da ciò che l'intervistatore pensava di chiedere.

È possibile in pratica fare fuoriuscire 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 si suppone che la raccolta dei rifiuti funzioni in un caso ideale?
  • Sanno che possono chiamare altre lingue attraverso interfacce native?
  • Sanno di perdere memoria in quelle altre lingue?
  • Il candidato sa anche cosa sia 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 invece di Java. Credo che sia più probabile che tu ripeta la situazione di non conoscere la risposta a una domanda in un'intervista di quanto tu non sia nel posto in cui devi sapere come fare a fugare Java. Quindi, si spera, questo aiuterà.

Una delle abilità più importanti che puoi sviluppare per intervistare è imparare ad ascoltare attivamente le domande e lavorare con l'intervistatore per estrarre le loro intenzioni. Ciò non solo ti consente di rispondere alla domanda nel modo desiderato, ma mostra anche che hai delle capacità comunicative vitali. E quando si tratta di scegliere tra molti sviluppatori di talento, assumerò quello che ascolta, pensa e capisce prima che rispondano ogni volta.


133



Quello che segue è un esempio piuttosto inutile, se non capisci JDBC. O almeno come JDBC si aspetta che uno sviluppatore si chiuda Connection, Statement e ResultSet istanze prima di scartarle o di perdere riferimenti a esse, invece di affidarsi all'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 il Connection l'oggetto non è chiuso e quindi la connessione fisica rimarrà aperta, finché il garbage collector non si avvicina e vede che è irraggiungibile. GC invocherà il finalize metodo, ma ci sono driver JDBC che non implementano il finalize, almeno non nello stesso modo in cui Connection.close è implementato. Il comportamento risultante è che mentre la memoria verrà recuperata a causa di oggetti irraggiungibili raccolti, le risorse (inclusa la memoria) associate al Connection l'oggetto potrebbe semplicemente non essere reclamato.

In un tale evento in cui il Connection'S finalize il metodo non ripulisce tutto, in realtà si potrebbe scoprire che la connessione fisica al server del database durerà diversi cicli di garbage collection, finché il server del database non capirà che la connessione non è attiva (se lo fa), e dovrebbe essere chiusa.

Anche se il driver JDBC doveva essere implementato finalize, è possibile che vengano lanciate eccezioni durante la finalizzazione. Il comportamento risultante è che qualsiasi memoria associata all'oggetto "dormiente" ora non sarà recuperata, come finalize è garantito per essere invocato una sola volta.

Lo scenario sopra riportato relativo alle eccezioni durante la finalizzazione degli oggetti è correlato a un altro scenario che potrebbe portare a una perdita di memoria: la risurrezione degli oggetti. La resurrezione dell'oggetto viene spesso eseguita intenzionalmente creando un forte riferimento all'oggetto da essere finalizzato, da un altro oggetto. Quando la risurrezione dell'oggetto viene utilizzata in modo errato, 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 a List istanza in cui si sta solo aggiungendo alla lista e non eliminando da essa (anche se dovresti liberarti degli elementi che non ti servono più), o
  • Apertura Sockets o Files, ma non chiudendoli quando non sono più necessari (simile al precedente esempio che coinvolge il Connection classe).
  • Non scaricare Singletons quando si abbassa 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 generalmente creato un nuovo programma di caricamento classi e il precedente programma di caricamento classi continuerà ad esistere a causa del singleton.

113