memory how - Creare una perdita di memoria con Java




to solve (25)

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?


Answers

Ecco uno semplice / sinistro attraverso 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 dell'originale, una stringa molto più lunga, l'originale rimane in memoria. Quindi, finché hai un StringLeaker in gioco, hai anche tutta la stringa originale in memoria, anche se potresti pensare di stare semplicemente aggrappando una stringa di un singolo carattere.

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

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

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

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

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


Di recente mi sono imbattuto in un tipo più sottile di perdita di risorse. Apriamo le risorse tramite getResourceAsStream del programma di caricamento classi e si è verificato che gli handle del flusso di input non sono stati chiusi.

Uhm, potresti dire, che idiota.

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

Tutto ciò di cui hai bisogno è un file jar con un file all'interno del quale verrà fatto riferimento dal codice Java. Più grande è il file jar, la memoria più veloce viene allocata.

Puoi facilmente creare un vaso simile 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, compilarlo 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 all'interno due file.

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 fondamentalmente non fa nulla, ma crea oggetti InputStream senza riferimento. Questi oggetti saranno raccolti immediatamente e quindi non contribuiranno alla dimensione dell'heap. Per il nostro esempio è importante caricare una risorsa esistente da un file jar e le dimensioni contano qui!

Se sei dubbi, prova a compilare e avviare la classe sopra, ma assicurati di scegliere una dimensione heap accettabile (2 MB):

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

Qui non si verificherà un errore OOM, in quanto non viene mantenuto alcun riferimento, l'applicazione continuerà a funzionare indipendentemente dall'ampiezza di scelta di ITERATIONS nell'esempio sopra riportato. Il consumo di memoria del processo (visibile in alto (RES / RSS) o in Process Explorer) aumenta a meno che l'applicazione non arrivi al comando di attesa. Nella configurazione sopra, allocherà circa 150 MB in memoria.

Se vuoi che l'applicazione funzioni correttamente, chiudi il flusso di input nel punto in cui è stato creato:

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

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

Abbastanza semplice e sorprendente.


Cos'è una perdita di memoria:

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

Esempio tipico:

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

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. Un design migliore utilizza un LRUMap (mantiene solo gli oggetti usati di recente nella cache).

Certo, puoi rendere le cose molto più complicate:

  • utilizzando le costruzioni ThreadLocal .
  • aggiungendo alberi di riferimento più 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 modo si potrebbe anche considerare che si tratti di una perdita di memoria (causata da un cattivo design).


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

In un evento del genere in cui il metodo finalize Connection non ripulisce tutto, si potrebbe effettivamente 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 chiuso.

Anche se il driver JDBC doveva implementare la finalize , è possibile che vengano lanciate eccezioni durante la finalizzazione. Il comportamento risultante è che qualsiasi memoria associata all'oggetto "dormiente" ora non sarà recuperata, poiché è garantito che il finalize venga richiamato 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 un'istanza di List cui si sta solo aggiungendo all'elenco e non eliminando da esso (sebbene sia necessario eliminare elementi che non sono più necessari) o
  • Aprendo Socket o File , ma non chiudendoli quando non sono più necessari (simile all'esempio sopra che riguarda la classe Connection ).
  • 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.

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

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

Chiama String.intern() sulla 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();

(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 l'opzione noclassgc su IBM JDK che impedisce la garbage collection della classe inutilizzata

Vedi Impostazioni jdk di IBM .


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 riferimento forte 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.)

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


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 char[] , quindi la versione successiva 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.

  • Creando, ma non avviando, un Thread entra nella stessa categoria di cui sopra.

  • La creazione di un thread eredita ContextClassLoader e ContextClassLoader , oltre a ThreadGroup e qualsiasi InheritedThreadLocal , tutti i 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 ThreadFactory molto semplice, 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 ; 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.

  • Chiamando 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 i Weak/SoftReference che potrebbero mantenere un riferimento rigido all'oggetto protetto.

  • Utilizzando java.net.URL con il protocollo HTTP (S) e caricando la risorsa da (!). Questo è speciale, 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.

  • Utilizzo di InflaterInputStream passando il new java.util.zip.Inflater() nel costruttore (per esempio PNGImageDecoder ) e non chiamando end() del filtro. Bene, se passi il costruttore con solo new , nessuna possibilità ... E sì, chiamare close() sul flusso 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 è affamato 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. Chiamare sempre end() se si crea manualmente un Deflater o Inflater . La parte migliore degli ultimi due: non è possibile 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!


ci sono molte situazioni diverse che potrebbero perdere memoria. Uno che ho incontrato, che espone una mappa che non dovrebbe essere esposta e utilizzata in altro luogo.

public class ServiceFactory {

private Map<String, Service> services;

private static ServiceFactory singleton;

private ServiceFactory() {
    services = new HashMap<String, Service>();
}

public static synchronized ServiceFactory getDefault() {

    if (singleton == null) {
        singleton = new ServiceFactory();
    }
    return singleton;
}

public void addService(String name, Service serv) {
    services.put(name, serv);
}

public void removeService(String name) {
    services.remove(name);
}

public Service getService(String name, Service serv) {
    return services.get(name);
}

// the problematic api, which expose the map.
//and user can do quite a lot of thing from this api.
//for example, create service reference and forget to dispose or set it null
//in all this is a dangerous api, and should not expose 
public Map<String, Service> getAllServices() {
    return services;
}

}

// resource class is a heavy class
class Service {

}

Come molti hanno suggerito, Resource Leaks è abbastanza facile da causare, come gli esempi JDBC. Le perdite di memoria effettive sono un po 'più difficili, specialmente se non si fa affidamento su parti spezzate della JVM per farlo per voi ...

Le idee di creare oggetti che hanno un ingombro molto grande e quindi di non poterle accedere non sono nemmeno vere e proprie perdite di memoria. Se nulla può accedervi, verrà raccolto dei rifiuti, e se qualcosa può accedervi, allora non è una perdita ...

Un modo che abituato a lavorare anche se - e non so se lo fa ancora - è quello di avere una catena circolare di tre profonda. 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 è stato abbastanza intelligente da sapere che una catena profonda due - come in A <-> B - può essere raccolto in sicurezza se A e B non sono accessibili da altro, ma non possono gestire la catena a tre vie ...


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 garbage collector. Dovresti quindi educatamente spiegare che le JVM usano un algoritmo molto più sofisticato che non ha questa limitazione.

-Siamo Tarle


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

"Una perdita di memoria, in informatica (o perdita, in questo contesto), si verifica quando un programma per computer consuma memoria, ma non è in grado di rilasciarlo di nuovo al sistema operativo." (Wikipedia)

La risposta facile è: non puoi. Java esegue la gestione automatica della memoria e libererà risorse che non sono necessarie per te. Non puoi impedire che ciò accada. Sarà SEMPRE in grado di rilasciare le risorse. Nei programmi con gestione manuale della memoria, questo è diverso. Si può ottenere un po 'di memoria in C usando malloc (). Per liberare la memoria, è necessario il puntatore che malloc ha restituito e chiama free () su di esso. Ma se non si ha più il puntatore (sovrascritto o superato per sempre), si è purtroppo incapaci di liberare questa memoria e quindi si ha una perdita di memoria.

Tutte le altre risposte finora sono nella mia definizione, non in realtà perdite di memoria. Tutti mirano a riempire la memoria di cose inutili in fretta. Ma in qualsiasi momento puoi ancora dereferenziare gli oggetti che hai creato e liberare così la memoria -> NO LEAK. La risposta di Acconrad arriva piuttosto vicina, come devo ammettere, poiché la sua soluzione è effettivamente solo "schiantare" il garbage collector forzandolo in un loop infinito).

La risposta lunga è: è possibile ottenere una perdita di memoria scrivendo una libreria per Java utilizzando JNI, che può avere la gestione manuale della memoria e quindi avere perdite di memoria. Se chiami questa libreria, il tuo processo java perderà memoria. Oppure, puoi avere bug nella JVM, in modo che la JVM perda memoria. Probabilmente ci sono bug nella JVM, potrebbero essercene anche alcuni noti, dal momento che la garbage collection non è così banale, ma è comunque un bug. Di progettazione questo non è possibile. Potresti chiederti qualche codice Java che è stato risolto da un tale errore. Scusa se non ne conosco uno e potrebbe non essere più un bug nella prossima versione di Java comunque.


Recentemente ho riscontrato una situazione di perdita di memoria causata in un modo da log4j.

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

Per poter archiviare tag specifici del thread, la classe NDC di log4j utilizza un Hashtable che è codificato dall'oggetto Thread stesso (anziché dire l'id del thread), e quindi finché il tag NDC rimane in memoria tutti gli oggetti che rimangono fuori dal thread anche l'oggetto rimane in memoria. Nella nostra applicazione web utilizziamo NDC per etichettare i logoutput con un ID di richiesta per distinguere i log da una singola richiesta separatamente. Il contenitore che associa il tag NDC a un thread, lo rimuove anche mentre restituisce la risposta da una richiesta. Il problema si è verificato durante il processo di elaborazione di una richiesta, è stato generato un thread figlio, ad esempio il 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();
    }
}    

Pertanto, 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 che ne sta fuori. Quindi, anche dopo che il thread ha terminato di fare ciò che stava facendo, il riferimento alla lista enorme è stato tenuto in vita dal contesto NDC Hastable, causando così una perdita di memoria.


Crea una mappa statica e continua ad aggiungervi riferimenti rigidi. 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); }
}

Porta 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 WAR nella directory dei punti di pagamento del server.

A meno che qualcuno non abbia effettivamente provato questo, è molto probabile che otterrai un OutOfMemoryError dopo un paio di ridistribuzioni, perché l'applicazione non ha avuto cura di ripulire se stessa. Potresti anche trovare un bug nel tuo server con questo test.

Il problema è che la durata del contenitore è più lunga della durata della tua applicazione. È necessario assicurarsi che tutti i riferimenti che il contenitore possa avere agli oggetti o alle classi della propria applicazione possano essere raccolti.

Se esiste un solo riferimento che sopravvive alla disattivazione della tua app Web, il classloader corrispondente e di conseguenza tutte le classi della tua app Web non possono essere raccolte automaticamente.

I thread avviati dall'applicazione, le variabili ThreadLocal, gli allegati di log sono alcuni dei soliti sospetti che causano perdite di classloader.


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.


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 stavi implementando da solo, avresti pensato di cancellare l'elemento dell'array che non è più usato ( elementData[--size] = null )? Quel riferimento potrebbe mantenere vivo un enorme oggetto ...


Tutti dimenticano sempre il percorso del codice nativo. Ecco una semplice formula per una perdita:

  1. Dichiara 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.


Un esempio comune di questo nel codice GUI è quando si crea un widget / componente e si aggiunge un listener ad un oggetto con scope statico / applicativo e quindi non si rimuove il listener quando il widget viene distrutto. Non solo ottieni una perdita di memoria, ma anche un successo in termini di prestazioni come quando qualsiasi cosa tu stia ascoltando genera eventi, vengono chiamati anche tutti i vecchi ascoltatori.


Sei in grado di fare una perdita di memoria con la classe sun.misc.Unsafe . In realtà questa classe di servizio viene utilizzata in diverse classi standard (ad esempio nelle classi java.nio ). Non è possibile creare istanze di questa classe direttamente , ma è possibile utilizzare la riflessione per farlo .

Il codice non viene compilato in Eclipse IDE - compila usando il comando javac (durante la compilazione riceverai avvertimenti)

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

}

Ogni volta che mantieni i riferimenti intorno agli oggetti che non ti servono più hai una perdita di memoria. Consulta Gestione delle perdite di memoria nei programmi Java per esempi di come le perdite di memoria si manifestano in Java e cosa puoi fare al riguardo.


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.


Forse utilizzando il 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 mantenere riferimenti a oggetti non utilizzati o aprire flussi senza chiuderli in seguito.


È possibile creare una perdita di memoria in movimento 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 in un tempo compreso tra pochi secondi e alcuni minuti 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());
        }
    }
}

Probabilmente l'intervistatore stava cercando un riferimento circolare come il seguente codice (che tra l'altro solo perdite di memoria in JVM molto vecchi che utilizzavano il conteggio dei riferimenti, che non è più il caso). Ma è una domanda piuttosto vaga, quindi è una grande opportunità 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 è possibile spiegare che con il conteggio dei riferimenti, il codice precedente potrebbe perdere memoria. Ma le JVM più moderne non usano più il conteggio dei riferimenti, la maggior parte usa un garbage collector sweep, che in effetti raccoglierà questa memoria.

Successivamente potresti spiegare la creazione di un oggetto che ha una risorsa nativa sottostante, come questa:

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 è possibile 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 codice Java.

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


Elimina la finestra di dialogo quando l'attività distrugge

@Override
protected void onDestroy()
{
    super.onDestroy();
    if (pDialog!=null && pDialog.isShowing()){
        pDialog.dismiss();
    }
}




java memory memory-leaks