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?
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?
Risposte:
Ecco un buon modo per creare una vera perdita di memoria (oggetti inaccessibili eseguendo codice ma ancora memorizzati in memoria) in Java puro:
ClassLoader
.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.ClassLoader
è stata caricata.A causa del modo ThreadLocal
è implementato in Oracle JDK, questo crea una perdita di memoria:
Thread
ha un campo privato threadLocals
, che in realtà memorizza i valori thread-local.ThreadLocal
oggetto, quindi dopo che l' ThreadLocal
oggetto è stato immesso nella spazzatura, la sua voce viene rimossa dalla mappa.ThreadLocal
all'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:
Thread
oggetto → threadLocals
mappa → istanza della classe esempio → classe esempio → staticoThreadLocal
campo → ThreadLocal
oggetto.
( ClassLoader
Non 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 ThreadLocal
s 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 .
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' noclassgc
opzione su IBM JDK che impedisce la garbage collection di classe non utilizzata
Vedi le impostazioni IBM jdk .
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.
intern
contenuto hashtable. Come tale, è spazzatura raccolta correttamente e non una perdita. (ma IANAJP) mindprod.com/jgloss/interned.html#GC
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.
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, 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.addShutdownHook
e 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, Thread
entra nella stessa categoria di cui sopra.
La creazione di un thread eredita 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 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).
ThreadLocal
cache; 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/SoftReference
che potrebbero mantenere un riferimento rigido all'oggetto protetto.
Utilizzo java.net.URL
con il protocollo HTTP (S) e caricamento della risorsa da (!). Questo è speciale, KeepAliveCache
crea 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 InflaterInputStream
passare new java.util.zip.Inflater()
nel costruttore ( PNGImageDecoder
per 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, Deflater
non è ampiamente usato e per quanto ne so JDK non contiene abusi. Chiama sempre end()
se crei manualmente un Deflater
o 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!
Creating but not starting a Thread...
Yikes, sono stato gravemente morso da questo alcuni secoli fa! (Java 1.3)
unstarted
conteggio, ma ciò impedisce alla distruzione del gruppo di thread (minore male ma comunque una perdita)
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.
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.
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" ...
PhantomReference
) se un oggetto è stato trovato non avere a nessuno interessarsene. WeakReference
si 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.
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?
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.
Il seguente è un esempio abbastanza inutile, se non capisci JDBC . O almeno come JDBC si aspetta che uno sviluppatore chiuda Connection
, Statement
e le ResultSet
istanze 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' Connection
oggetto 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 finalize
metodo, 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 Connection
all'oggetto potrebbero semplicemente non essere recuperate.
In tal caso in cui il metodo del Connection
' finalize
non 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
List
un'istanza in cui si sta solo aggiungendo all'elenco e non si elimina da esso (anche se è necessario eliminare gli elementi non più necessari), oppureSocket
s o File
s, ma non chiuderli quando non sono più necessari (simile all'esempio sopra che coinvolge la Connection
classe).Connection.close
il 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.
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 ...
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.
...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.
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();
}
}
}
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.
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.
muchSmallerString
viene liberato (poiché l' StringLeaker
oggetto 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.
intern
caso 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.
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.
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.
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.
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 ...
Che cos'è una perdita di memoria:
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:
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).
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.
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.
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.
Tutti dimenticano sempre la route del codice nativo. Ecco una semplice formula per una perdita:
malloc
. Non chiamare free
.Ricorda, le allocazioni di memoria nel codice nativo provengono dall'heap JVM.
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); }
}
È 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());
}
}
}
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.
finalize()
non verrà chiamato ma l'oggetto verrà raccolto una volta che non ci saranno più riferimenti. Nemmeno il garbage collector viene "chiamato".
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.
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.
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 ...
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 TreeMap
s, ma osservando l'implementazione la ragione potrebbe essere che: a TreeMap.Entry
memorizza 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 TreeMap
struttura di big data. Le persone di solito usano TreeMap
s 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 Map
restituita) ne salvassi un Entry
posto, 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 map
istanze dovrebbero essere pronte per la raccolta, ma non accadrà, poiché almeno una Entry
è memorizzata altrove.
A seconda delle jvm
impostazioni, l'applicazione potrebbe bloccarsi nella fase iniziale a causa di un OutOfMemoryError
.
Puoi vedere da questo visualvm
grafico come la memoria continua a crescere.
Lo stesso non accade con una struttura di dati con hash ( HashMap
).
Questo è il grafico quando si utilizza a 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 .
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 leakMe
passerai non morirà mai.
(*modificato*)
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.
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
first
non è 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.