Ricerca…


Osservazioni

In Java, gli oggetti vengono allocati nell'heap e la memoria heap viene recuperata dalla garbage collection automatica. Un programma applicativo non può cancellare esplicitamente un oggetto Java.

I principi fondamentali della raccolta dei rifiuti Java sono descritte nella Garbage Collection esempio. Altri esempi descrivono la finalizzazione, come attivare manualmente il garbage collector e il problema delle perdite di archiviazione.

finalizzazione

Un oggetto Java può dichiarare un metodo finalize . Questo metodo viene chiamato poco prima che Java rilasci la memoria per l'oggetto. In genere sarà simile a questo:

public class MyClass {
  
    //Methods for the class

    @Override
    protected void finalize() throws Throwable {
        // Cleanup code
    }
}

Tuttavia, ci sono alcune avvertenze importanti sul comportamento della finalizzazione di Java.

  • Java non garantisce quando verrà chiamato un metodo finalize() .
  • Java non garantisce nemmeno che un metodo finalize() venga chiamato un po 'di tempo durante la vita dell'applicazione in esecuzione.
  • L'unica cosa che è garantita è che il metodo verrà chiamato prima che l'oggetto sia cancellato ... se l'oggetto è cancellato.

Le avvertenze precedenti indicano che è una cattiva idea affidarsi al metodo finalize per eseguire azioni di pulizia (o altro) che devono essere eseguite in modo tempestivo. L'eccessivo affidamento sulla finalizzazione può portare a perdite di memoria, perdite di memoria e altri problemi.

In breve, ci sono pochissime situazioni in cui la finalizzazione è in realtà una buona soluzione.

I finalizzatori vengono eseguiti una volta sola

Normalmente, un oggetto viene cancellato dopo che è stato finalizzato. Tuttavia, questo non accade tutto il tempo. Considera il seguente esempio 1 :

public class CaptainJack {
    public static CaptainJack notDeadYet = null;

    protected void finalize() {
        // Resurrection!
        notDeadYet = this;
    }
}

Quando un'istanza di CaptainJack diventa irraggiungibile e il garbage collector tenta di recuperarlo, il metodo finalize() assegnerà un riferimento all'istanza alla variabile notDeadYet . Ciò renderà l'istanza nuovamente raggiungibile e il garbage collector non lo eliminerà.

Domanda: Il Capitano Jack è immortale?

Risposta: No.

Il problema è che JVM eseguirà un finalizzatore su un oggetto solo una volta nella sua vita. Se assegni null a notDeadYet facendo sì che un'istanza resurected diventi irraggiungibile ancora una volta, il garbage collector non chiamerà finalize() sull'oggetto.

1 - Vedi https://en.wikipedia.org/wiki/Jack_Harkness .

Attivazione manuale di GC

È possibile attivare manualmente il Garbage Collector chiamando

System.gc();

Tuttavia, Java non garantisce che il Garbage Collector sia eseguito quando la chiamata ritorna. Questo metodo semplicemente "suggerisce" alla JVM (Java Virtual Machine) che si desidera che esegua il garbage collector, ma non lo obbliga a farlo.

Generalmente è considerata una cattiva pratica tentare di attivare manualmente la garbage collection. La JVM può essere eseguita con l'opzione -XX:+DisableExplicitGC per disabilitare le chiamate a System.gc() . L'attivazione della garbage collection chiamando System.gc() può interrompere le normali attività di garbage management / object promotion dell'implementazione del garbage collector specifico in uso da parte della JVM.

Raccolta dei rifiuti

L'approccio C ++: nuovo e cancella

In un linguaggio come C ++, il programma applicativo è responsabile della gestione della memoria utilizzata dalla memoria allocata dinamicamente. Quando un oggetto viene creato nell'heap C ++ utilizzando il new operatore, è necessario che ci sia un corrispondente uso dell'operatore delete per disporre dell'oggetto:

  • Se il programma si dimentica di delete un oggetto e semplicemente "dimentica" su di esso, la memoria associata viene persa per l'applicazione. Il termine per questa situazione è una perdita di memoria , e troppa memoria perde una domanda è suscettibile di utilizzare sempre più memoria, e alla fine crash.

  • D'altra parte, se un'applicazione tenta di delete due volte lo stesso oggetto, o di usare un oggetto dopo che è stato cancellato, l'applicazione rischia di bloccarsi a causa di problemi con il danneggiamento della memoria

In un complicato programma in C ++, l'implementazione della gestione della memoria utilizzando new e delete può richiedere molto tempo. In effetti, la gestione della memoria è una fonte comune di bug.

L'approccio Java - garbage collection

Java ha un approccio diverso. Invece di un operatore di delete esplicita, Java fornisce un meccanismo automatico noto come garbage collection per recuperare la memoria utilizzata da oggetti che non sono più necessari. Il sistema di runtime Java si assume la responsabilità di trovare gli oggetti da smaltire. Questa attività viene eseguita da un componente chiamato garbage collector o GC in breve.

In qualsiasi momento durante l'esecuzione di un programma Java, possiamo dividere l'insieme di tutti gli oggetti esistenti in due sottoinsiemi distinti 1 :

  • Gli oggetti raggiungibili sono definiti da JLS come segue:

    Un oggetto raggiungibile è qualsiasi oggetto a cui è possibile accedere in qualsiasi potenziale calcolo continuo da qualsiasi thread attivo.

    In pratica, ciò significa che esiste una catena di riferimenti che parte da una variabile locale in-scope o da una variabile static tramite la quale alcuni codici potrebbero essere in grado di raggiungere l'oggetto.

  • Gli oggetti non raggiungibili sono oggetti che non possono essere raggiunti come sopra.

Qualsiasi oggetto irraggiungibile è idoneo per la garbage collection. Questo non significa che saranno raccolti dalla spazzatura. Infatti:

  • Un oggetto irraggiungibile non viene raccolto immediatamente quando diventa irraggiungibile 1 .
  • Un oggetto non raggiungibile non può mai essere sottoposto a garbage collection.

La specifica del linguaggio Java dà molta libertà a un'implementazione JVM per decidere quando raccogliere oggetti non raggiungibili. Inoltre (in pratica) dà il permesso che un'implementazione JVM sia conservativa nel modo in cui rileva oggetti non raggiungibili.

L'unica cosa che garantisce JLS è che nessun oggetto raggiungibile sarà mai raccolto.

Cosa succede quando un oggetto diventa irraggiungibile

Prima di tutto, nulla accade in modo specifico quando un oggetto diventa irraggiungibile. Le cose accadono solo quando il garbage collector viene eseguito e rileva che l'oggetto è irraggiungibile. Inoltre, è comune che una esecuzione GC non rilevi tutti gli oggetti non raggiungibili.

Quando il GC rileva un oggetto irraggiungibile, possono verificarsi i seguenti eventi.

  1. Se ci sono Reference oggetti che fanno riferimento all'oggetto, tali riferimenti verranno cancellati prima che l'oggetto viene eliminato.

  2. Se l'oggetto è definibile , sarà finalizzato. Questo succede prima che l'oggetto sia cancellato.

  3. L'oggetto può essere cancellato e la memoria che occupa può essere recuperata.

Si noti che esiste una sequenza chiara in cui possono verificarsi gli eventi precedenti, ma nulla richiede che il garbage collector esegua la cancellazione finale di qualsiasi oggetto specifico in un intervallo di tempo specifico.

Esempi di oggetti raggiungibili e non raggiungibili

Considera le seguenti classi di esempio:

// A node in simple "open" linked-list.
public class Node {
    private static int counter = 0;

    public int nodeNumber = ++counter;
    public Node next;
}

public class ListTest {
    public static void main(String[] args) {
        test();                    // M1
        System.out.prinln("Done"); // M2
    }
    
    private static void test() {
        Node n1 = new Node();      // T1
        Node n2 = new Node();      // T2
        Node n3 = new Node();      // T3
        n1.next = n2;              // T4
        n2 = null;                 // T5
        n3 = null;                 // T6
    }
}

Esaminiamo cosa succede quando viene chiamato test() . Le dichiarazioni T1, T2 e T3 creano oggetti Node e gli oggetti sono tutti raggiungibili tramite le variabili n1 , n2 e n3 rispettivamente. L'istruzione T4 assegna il riferimento all'oggetto 2nd Node al campo next del primo. Quando ciò è fatto, il 2o Node è raggiungibile attraverso due percorsi:

 n2 -> Node2
 n1 -> Node1, Node1.next -> Node2

Nell'istruzione T5, assegniamo null a n2 . Questo interrompe la prima delle catene di raggiungibilità per Node2 , ma il secondo rimane ininterrotto, quindi il Node2 è ancora raggiungibile.

Nell'istruzione T6, assegniamo null a n3 . Questo rompe l'unica catena di raggiungibilità per Node3 , il che rende Node3 irraggiungibile. Tuttavia, Node1 e Node2 sono entrambi ancora raggiungibili tramite la variabile n1 .

Infine, quando il metodo test() ritorna, le sue variabili locali n1 , n2 e n3 escono dall'ambito e quindi non possono essere accessibili da nulla. Ciò interrompe le restanti catene di raggiungibilità per Node1 e Node2 e tutti gli oggetti Node non sono né irraggiungibili e sono idonei per la garbage collection.


1 - Questa è una semplificazione che ignora la finalizzazione e le classi di Reference . 2 - Ipoteticamente, un'implementazione di Java potrebbe farlo, ma il costo delle prestazioni di fare ciò lo rende poco pratico.

Impostazione delle dimensioni di heap, PermGen e stack

Quando viene avviata una macchina virtuale Java, è necessario sapere quanto è grande l'heap e le dimensioni predefinite per gli stack di thread. Questi possono essere specificati usando le opzioni della riga di comando sul comando java . Per le versioni di Java precedenti a Java 8, è anche possibile specificare la dimensione della regione PermGen dell'heap.

Si noti che PermGen è stato rimosso in Java 8 e, se si tenta di impostare la dimensione PermGen, l'opzione verrà ignorata (con un messaggio di avviso).

Se non si specificano le dimensioni di heap e stack in modo esplicito, la JVM utilizzerà i valori predefiniti calcolati in una versione e in una modalità specifica della piattaforma. Ciò potrebbe comportare che l'applicazione utilizzi troppo poca o troppa memoria. Questo è in genere OK per gli stack di thread, ma può essere problematico per un programma che utilizza molta memoria.

Impostazione delle dimensioni heap, permGen e stack predefinito:

Le seguenti opzioni JVM impostano la dimensione heap:

  • -Xms<size> - imposta la dimensione heap iniziale
  • -Xmx<size> : imposta la dimensione massima dell'heap
  • -XX:PermSize<size> - imposta la dimensione iniziale di PermGen
  • -XX:MaxPermSize<size> : imposta la dimensione massima di PermGen
  • -Xss<size> - imposta la dimensione predefinita dello stack di thread

Il parametro <size> può essere un numero di byte o può avere un suffisso di k , m o g . Questi ultimi specificano le dimensioni in kilobyte, megabyte e gigabyte rispettivamente.

Esempi:

$ java -Xms512m -Xmx1024m JavaApp
$ java -XX:PermSize=64m -XX:MaxPermSize=128m JavaApp
$ java -Xss512k JavaApp

Trovare le dimensioni predefinite:

L'opzione -XX:+printFlagsFinal può essere utilizzata per stampare i valori di tutti i flag prima di avviare la JVM. Questo può essere usato per stampare i valori di default per le impostazioni di heap e stack come segue:

  • Per Linux, Unix, Solaris e Mac OSX

    $ java -XX: + PrintFlagsFinal -version | grep -iE 'HeapSize | PermSize | ThreadStackSize'

  • Per Windows:

    java -XX: + PrintFlagsFinal -version | findstr / i "HeapSize PermSize ThreadStackSize"

L'output dei comandi precedenti sarà simile al seguente:

uintx InitialHeapSize                          := 20655360        {product}
uintx MaxHeapSize                              := 331350016       {product}
uintx PermSize                                  = 21757952        {pd product}
uintx MaxPermSize                               = 85983232        {pd product}
 intx ThreadStackSize                           = 1024            {pd product}

Le dimensioni sono espresse in byte.

Perdite di memoria in Java

Nell'esempio della Garbage collection , abbiamo implicito che Java risolva il problema delle perdite di memoria. Questo non è in realtà vero. Un programma Java può perdere memoria, anche se le cause delle perdite sono piuttosto diverse.

Gli oggetti raggiungibili possono perdere

Si consideri la seguente implementazione dello stack ingenuo.

public class NaiveStack {
    private Object[] stack = new Object[100];
    private int top = 0;

    public void push(Object obj) {
        if (top >= stack.length) {
            throw new StackException("stack overflow");
        }
        stack[top++] = obj;
    }

    public Object pop() {
        if (top <= 0) {
            throw new StackException("stack underflow");
        }
        return stack[--top];
    }

    public boolean isEmpty() {
        return top == 0;
    }
}

Quando si push un oggetto e poi subito pop , ci sarà ancora un riferimento all'oggetto nella stack matrice.

La logica dell'implementazione dello stack significa che quel riferimento non può essere restituito a un client dell'API. Se un oggetto è stato scoppiato, possiamo provare che non è "possibile accedere a qualsiasi potenziale calcolo continuo da qualsiasi thread live" . Il problema è che una JVM di generazione attuale non può dimostrarlo. Le JVM di generazione corrente non considerano la logica del programma nel determinare se i riferimenti sono raggiungibili. (Per cominciare, non è pratico).

Ma mettendo da parte il problema di cosa significhi veramente la raggiungibilità , abbiamo chiaramente una situazione in cui l'implementazione di NaiveStack è "aggrappata" a oggetti che dovrebbero essere recuperati. Questa è una perdita di memoria.

In questo caso, la soluzione è semplice:

    public Object pop() {
        if (top <= 0) {
            throw new StackException("stack underflow");
        }
        Object popped = stack[--top];
        stack[top] = null;              // Overwrite popped reference with null.
        return popped;
    }

Le cache possono essere perdite di memoria

Una strategia comune per migliorare le prestazioni del servizio è memorizzare i risultati nella cache. L'idea è di tenere un registro delle richieste comuni e dei loro risultati in una struttura di dati in memoria nota come cache. Quindi, ogni volta che viene effettuata una richiesta, si cerca la richiesta nella cache. Se la ricerca ha esito positivo, si restituiscono i risultati salvati corrispondenti.

Questa strategia può essere molto efficace se implementata correttamente. Tuttavia, se implementata in modo errato, una cache può essere una perdita di memoria. Considera il seguente esempio:

public class RequestHandler {
    private Map<Task, Result> cache = new HashMap<>();

    public Result doRequest(Task task) {
        Result result = cache.get(task);
        if (result == null) {
            result == doRequestProcessing(task);
            cache.put(task, result);
        }
        return result;
    }
}

Il problema con questo codice è che mentre qualsiasi chiamata a doRequest potrebbe aggiungere una nuova voce alla cache, non c'è nulla da rimuovere. Se il servizio riceve continuamente compiti diversi, alla fine la cache consumerà tutta la memoria disponibile. Questa è una forma di perdita di memoria.

Un approccio per risolvere questo è usare una cache con una dimensione massima e buttare fuori vecchie voci quando la cache supera il massimo. (Lanciare la voce meno utilizzata di recente è una buona strategia.) Un altro approccio è creare la cache usando WeakHashMap modo che la JVM possa sfrattare le voci della cache se l'heap inizia ad essere troppo pieno.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow