Recherche…


Remarques

En Java, les objets sont alloués dans le tas et la mémoire de tas est récupérée par la récupération de place automatique. Un programme d'application ne peut pas supprimer explicitement un objet Java.

Les principes de base de la récupération de la mémoire Java sont décrits dans l'exemple de la récupération de la mémoire . D'autres exemples décrivent la finalisation, comment déclencher manuellement le ramasse-miettes et le problème des fuites de stockage.

Finalisation

Un objet Java peut déclarer une méthode de finalize . Cette méthode est appelée juste avant que Java ne libère la mémoire pour l'objet. Il ressemblera généralement à ceci:

public class MyClass {
  
    //Methods for the class

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

Cependant, il existe des mises en garde importantes sur le comportement de la finalisation Java.

  • Java ne garantit pas qu'une méthode finalize() sera appelée.
  • Java ne garantit même pas qu'une méthode finalize() sera appelée quelque temps pendant la durée de vie de l'application en cours d'exécution.
  • La seule chose garantie est que la méthode sera appelée avant que l'objet soit supprimé ... si l'objet est supprimé.

Les mises en garde ci-dessus signifient que c'est une mauvaise idée de compter sur la méthode finalize pour effectuer des actions de nettoyage (ou autres) qui doivent être effectuées en temps opportun. Dépendre de la finalisation peut entraîner des fuites de stockage, des fuites de mémoire et d’autres problèmes.

En bref, il existe très peu de situations où la finalisation est en fait une bonne solution.

Les finaliseurs ne s'exécutent qu'une seule fois

Normalement, un objet est supprimé après sa finalisation. Cependant, cela n'arrive pas tout le temps. Prenons l'exemple suivant 1 :

public class CaptainJack {
    public static CaptainJack notDeadYet = null;

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

Lorsqu'une instance de CaptainJack devient inaccessible et que le ramasse-miette tente de le récupérer, la méthode notDeadYet finalize() attribue une référence à l'instance à la variable notDeadYet . Cela rendra l'instance accessible une fois de plus, et le ramasse-miettes ne le supprimera pas.

Question: Le capitaine Jack est-il immortel?

Réponse: non

Le catch est que la JVM exécutera uniquement un finaliseur sur un objet une fois dans sa vie. Si vous attribuez la valeur null à notDeadYet provoquant une inaccessibilité de l'instance notDeadYet , le ramasse-miettes n'appelle pas finalize() sur l'objet.

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

Déclenchement manuel du GC

Vous pouvez déclencher manuellement le garbage collector en appelant

System.gc();

Toutefois, Java ne garantit pas l’exécution du Garbage Collector au retour de l’appel. Cette méthode "suggère simplement" à la machine virtuelle Java (Java Virtual Machine) que vous voulez qu’elle exécute le ramasse-miettes, mais ne le force pas à le faire.

Il est généralement considéré comme une mauvaise pratique de tenter de déclencher manuellement la récupération de place. La JVM peut être exécutée avec l'option -XX:+DisableExplicitGC pour désactiver les appels à System.gc() . Le déclenchement de la récupération de place en appelant System.gc() peut perturber les activités normales de gestion des ordures / de promotion d'objet de l'implémentation spécifique du ramasse-miettes utilisée par la JVM.

Collecte des ordures

L'approche C ++ - new and delete

Dans un langage comme le C ++, le programme d'application est responsable de la gestion de la mémoire utilisée par la mémoire allouée dynamiquement. Lorsqu'un objet est créé dans le tas C ++ à l'aide de l'opérateur new , il doit y avoir une utilisation correspondante de l'opérateur delete pour éliminer l'objet:

  • Si le programme oublie de delete un objet et ne fait que l'oublier, la mémoire associée est perdue pour l'application. Le terme pour cette situation est une fuite de mémoire , et trop de mémoire fuit, une application est susceptible d’utiliser de plus en plus de mémoire et finit par tomber en panne.

  • D'un autre côté, si une application tente de delete le même objet deux fois ou d'utiliser un objet après sa suppression, l'application risque de se bloquer en raison de problèmes de corruption de mémoire.

Dans un programme C ++ compliqué, l'implémentation de la gestion de la mémoire en utilisant new et delete peut prendre beaucoup de temps. En effet, la gestion de la mémoire est une source commune de bogues.

L'approche Java - garbage collection

Java adopte une approche différente. Au lieu d'un opérateur de delete explicite, Java fournit un mécanisme automatique appelé récupération de place pour récupérer la mémoire utilisée par les objets devenus inutiles. Le système d'exécution Java prend la responsabilité de trouver les objets à éliminer. Cette tâche est effectuée par un composant appelé « garbage collector» ou «GC».

A tout moment pendant l'exécution d'un programme Java, nous pouvons diviser l'ensemble de tous les objets existants en deux sous-ensembles distincts 1 :

  • Les objets accessibles sont définis par le JLS comme suit:

    Un objet accessible est tout objet auquel on peut accéder dans tout calcul continu potentiel à partir d'un thread en direct.

    En pratique, cela signifie qu’il existe une chaîne de références à partir d’une variable locale dans la portée ou d’une variable static permettant à un code d’atteindre l’objet.

  • Les objets inaccessibles sont des objets qui ne peuvent pas être atteints comme ci-dessus.

Tout objet inaccessible est éligible pour la récupération de la mémoire. Cela ne signifie pas qu'ils seront collectés. En réalité:

  • Un objet inaccessible n'est pas collecté immédiatement après être devenu inaccessible 1 .
  • Un objet inaccessible peut ne jamais être récupéré.

La spécification du langage Java donne beaucoup de latitude à une implémentation JVM pour décider quand collecter des objets inaccessibles. Il donne également (en pratique) l'autorisation à une implémentation JVM d'être prudente dans la manière dont elle détecte les objets inaccessibles.

La seule chose que JLS garantit, c'est qu'aucun objet accessible ne sera jamais récupéré.

Que se passe-t-il lorsqu'un objet devient inaccessible

Tout d'abord, rien ne se produit spécifiquement lorsqu'un objet devient inaccessible. Les choses ne se produisent que lorsque le ramasse-miettes s'exécute et qu'il détecte que l'objet est inaccessible. De plus, il est fréquent qu'un cycle de CPG ne détecte pas tous les objets inaccessibles.

Lorsque le CPG détecte un objet inaccessible, les événements suivants peuvent se produire.

  1. S'il existe des objets Reference faisant référence à l'objet, ces références seront effacées avant la suppression de l'objet.

  2. Si l'objet est définissable , il sera finalisé. Cela se produit avant que l'objet soit supprimé.

  3. L'objet peut être supprimé et la mémoire qu'il occupe peut être récupérée.

Notez qu'il existe une séquence claire dans laquelle les événements ci-dessus peuvent se produire, mais rien n'oblige le ramasse-miettes à effectuer la suppression finale d'un objet spécifique dans un délai spécifique.

Exemples d'objets accessibles et inaccessibles

Prenons les exemples de classes suivants:

// 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
    }
}

Examinons ce qui se passe quand on appelle test() . Les instructions T1, T2 et T3 créent des objets Node , et tous les objets sont accessibles via les variables n1 , n2 et n3 , respectivement. L'instruction T4 assigne la référence à l'objet 2nd Node au champ next du premier. Lorsque cela est fait, le 2ème Node est accessible via deux chemins:

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

Dans l'instruction T5, nous affectons null à n2 . Cela brise la première des chaînes d'accessibilité pour Node2 , mais la seconde reste intacte, donc Node2 est toujours accessible.

Dans l'instruction T6, nous affectons null à n3 . Cela casse la seule chaîne d'accessibilité pour Node3 , ce qui rend inaccessible Node3 . Cependant, Node1 et Node2 sont tous deux encore accessibles via la n1 variable.

Enfin, lorsque la méthode test() retourne, ses variables locales n1 , n2 et n3 sont hors de portée et ne peuvent donc pas être consultées. Cela brise les chaînes restantes pour joignabilité Node1 et Node2 , et tous les Node objets sont ni inaccessibles et admissibles à la collecte des ordures.


1 - Ceci est une simplification qui ignore la finalisation et les classes de Reference . 2 - Hypothétiquement, une implémentation Java pourrait le faire, mais le coût de la performance le rend peu pratique.

Réglage de la taille du tas, du permGen et de la pile

Lorsqu'une machine virtuelle Java démarre, elle doit connaître la taille du tas et la taille par défaut des piles de threads. Celles-ci peuvent être spécifiées en utilisant des options de ligne de commande sur la commande java . Pour les versions de Java antérieures à Java 8, vous pouvez également spécifier la taille de la région PermGen du tas.

Notez que PermGen a été supprimé dans Java 8 et que si vous tentez de définir la taille de PermGen, l'option sera ignorée (avec un message d'avertissement).

Si vous ne spécifiez pas explicitement les tailles de tas et de piles, la machine virtuelle Java utilisera les valeurs par défaut calculées de manière spécifique à la version et à la plate-forme. Cela peut entraîner votre application utilisant trop peu ou trop de mémoire. Ceci est généralement OK pour les piles de threads, mais cela peut être problématique pour un programme qui utilise beaucoup de mémoire.

Définition des tailles de tas, PermGen et par défaut:

Les options JVM suivantes définissent la taille de segment de mémoire:

  • -Xms<size> - définit la taille initiale du tas
  • -Xmx<size> - définit la taille maximale du tas
  • -XX:PermSize<size> - définit la taille initiale du PermGen
  • -XX:MaxPermSize<size> : définit la taille maximale de PermGen
  • -Xss<size> - définit la taille de la pile de threads par défaut

Le paramètre <size> peut être un nombre d'octets ou peut avoir un suffixe de k , m ou g . Ces derniers spécifient la taille en kilo-octets, mégaoctets et gigaoctets respectivement.

Exemples:

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

Trouver les tailles par défaut:

L'option -XX:+printFlagsFinal peut être utilisée pour imprimer les valeurs de tous les indicateurs avant de démarrer la machine virtuelle Java. Cela peut être utilisé pour imprimer les valeurs par défaut pour le tas et les paramètres de taille de pile comme suit:

  • Pour Linux, Unix, Solaris et Mac OSX

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

  • Pour les fenêtres:

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

La sortie des commandes ci-dessus ressemblera à ceci:

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

Les tailles sont données en octets.

Fuites de mémoire en Java

Dans l'exemple de la récupération de données , nous avons sous-entendu que Java résout le problème des fuites de mémoire. Ce n'est pas vraiment vrai. Un programme Java peut perdre de la mémoire, même si les causes des fuites sont assez différentes.

Les objets accessibles peuvent fuir

Considérez l'implémentation de pile naïve suivante.

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

Lorsque vous push un objet et que vous push faites pop immédiatement, il y aura toujours une référence à l'objet dans le tableau de la stack .

La logique de l'implémentation de la pile signifie que cette référence ne peut pas être renvoyée à un client de l'API. Si un objet a été sauté, alors nous pouvons prouver qu'il ne peut être «accessible dans aucun calcul continu potentiel à partir d'un thread en direct» . Le problème est qu'une JVM de génération actuelle ne peut pas le prouver. Les JVM de génération actuelle ne prennent pas en compte la logique du programme pour déterminer si les références sont accessibles. (Pour commencer, ce n'est pas pratique.)

Mais mettre de côté la question de savoir ce que signifie réellement l' accessibilité , nous avons clairement une situation où l'implémentation de NaiveStack est "accrochée" à des objets qui doivent être récupérés. C'est une fuite de mémoire.

Dans ce cas, la solution est simple:

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

Les caches peuvent être des fuites de mémoire

Une stratégie commune pour améliorer les performances du service consiste à mettre en cache les résultats. L'idée est de conserver un enregistrement des requêtes courantes et de leurs résultats dans une structure de données en mémoire appelée cache. Ensuite, chaque fois qu'une demande est faite, vous recherchez la requête dans le cache. Si la recherche réussit, vous renvoyez les résultats enregistrés correspondants.

Cette stratégie peut être très efficace si elle est correctement mise en œuvre. Cependant, si elle est implémentée de manière incorrecte, un cache peut être une fuite de mémoire. Prenons l'exemple suivant:

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

Le problème avec ce code est que, bien que tout appel à doRequest puisse ajouter une nouvelle entrée au cache, rien ne les supprime. Si le service reçoit continuellement des tâches différentes, le cache finira par consommer toute la mémoire disponible. Ceci est une forme de fuite de mémoire.

Une solution à ce problème consiste à utiliser un cache avec une taille maximale et à supprimer les anciennes entrées lorsque le cache dépasse le maximum. (Jeter la dernière entrée récemment utilisée est une bonne stratégie.) Une autre approche consiste à créer le cache à l’aide de WeakHashMap afin que la JVM puisse expulser les entrées de cache si le tas commence à être trop plein.



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow