java - tool - permanent generation




Création d'une fuite de mémoire avec Java (20)

Chaque fois que vous gardez des références à des objets dont vous n’avez plus besoin, vous avez une fuite de mémoire. Reportez-vous à la rubrique Gestion des fuites de mémoire dans les programmes Java pour obtenir des exemples montrant comment les fuites de mémoire se manifestent en Java et ce que vous pouvez faire.

Je viens d'avoir une interview et on m'a demandé de créer une fuite de mémoire avec Java. Inutile de dire que je me sentais stupide de n'avoir aucune idée de la façon de commencer à en créer un.

Que serait un exemple?


Ci-dessous, nous verrons un cas non évident où Java fuit, outre le cas standard des auditeurs oubliés, des références statiques, des clés fictives / modifiables dans hashmaps, ou simplement des threads bloqués sans aucune chance de mettre fin à leur cycle de vie.

  • File.deleteOnExit() - toujours la chaîne fuit, si la chaîne est une sous-chaîne, la fuite est encore pire (le caractère sous-jacent [] est également divulgué) - en Java 7, la sous-chaîne copie également le caractère char[] ; @ Daniel, pas besoin de votes, cependant.

Je vais me concentrer sur les discussions pour montrer le danger que représentent les discussions non gérées, sans même vouloir toucher au swing.

  • Runtime.addShutdownHook et ne pas supprimer ... et même, même avec removeShutdownHook en raison d'un bogue dans la classe ThreadGroup concernant les threads non démarrés, il est possible que le thread ThreadGroup ne soit pas récupéré. JGroup a la fuite dans GossipRouter.

  • Créer, mais ne pas commencer, un Thread va dans la même catégorie que ci-dessus.

  • La création d'un fil hérite des AccessControlContext et AccessControlContext , du ThreadGroup et de tout InheritedThreadLocal , toutes ces références sont des fuites potentielles, de même que l'ensemble des classes chargées par le chargeur de classes et toutes les références statiques, et ja-ja. L'effet est particulièrement visible avec l'ensemble de la structure jucExecutor qui propose une interface ThreadFactory super simple, mais la plupart des développeurs n'ont aucune idée du danger qui les guette. De plus, de nombreuses bibliothèques démarrent des threads sur demande (beaucoup trop de bibliothèques populaires dans l’industrie).

  • ThreadLocal caches; ceux-ci sont mal dans beaucoup de cas. Je suis sûr que tout le monde a vu un peu de caches simples basés sur ThreadLocal, et bien la mauvaise nouvelle: si le fil continue plus que prévu dans la vie du contexte ClassLoader, il s’agit d’une belle petite fuite. N'utilisez pas de caches ThreadLocal sauf si cela est vraiment nécessaire.

  • Appel de ThreadGroup.destroy() lorsque le ThreadGroup n'a pas de thread lui-même, mais qu'il conserve toujours des ThreadGroups enfants. Mauvaise fuite empêchant le ThreadGroup de disparaître de son parent, mais tous les enfants deviennent non énumérables.

  • L'utilisation de WeakHashMap et de la valeur (in) référence directement la clé. C’est difficile à trouver sans fichier mémoire. Cela s'applique à toutes les références Weak/SoftReference susceptibles de garder une référence dure à l'objet protégé.

  • Utilisation de java.net.URL avec le protocole HTTP (S) et chargement de la ressource à partir de (!). Celui-ci est spécial, le KeepAliveCache crée un nouveau thread dans le système ThreadGroup qui laisse filtrer le chargeur de classes de contexte du thread en cours. Le thread est créé à la première demande lorsqu'aucun thread vivant n'existe, vous pouvez donc avoir de la chance ou tout simplement fuir. La fuite est déjà corrigée dans Java 7 et le code qui crée le thread supprime correctement le chargeur de classes de contexte. Il y a peu d'autres cas ( comme ImageFetcher , également corrigé ) de la création de threads similaires.

  • Utilisation de InflaterInputStream transmettant la new java.util.zip.Inflater() dans le constructeur ( PNGImageDecoder par exemple) et en n’appelant pas end() de l’inflater. Eh bien, si vous passez dans le constructeur avec simplement new , aucune chance ... Et oui, appeler close() sur le flux ne ferme pas l'inflater s'il est passé manuellement en tant que paramètre du constructeur. Ce n'est pas une vraie fuite car elle serait libérée par le finaliseur ... quand elle le jugera nécessaire. Jusqu'à ce moment-là, il consomme tellement de mémoire native que Linux oom_killer peut tuer le processus en toute impunité. Le problème principal est que la finalisation en Java est très peu fiable et que G1 l'a aggravée jusqu'à la version 7.0.2. Morale de l'histoire: libérez les ressources natives dès que vous le pouvez; le finaliseur est trop pauvre.

  • Même cas avec java.util.zip.Deflater . Celui-ci est bien pire car Deflater a besoin de beaucoup de mémoire en Java, c’est-à-dire qu’il utilise toujours 15 bits (maximum) et 8 niveaux de mémoire (9 au maximum) en allouant plusieurs centaines de Ko de mémoire native. Heureusement, Deflater n'est pas largement utilisé et, à ma connaissance, JDK ne contient aucun abus. Appelez toujours end() si vous créez manuellement un Deflater ou un Inflater . La meilleure partie des deux derniers: vous ne pouvez pas les trouver via les outils de profilage normaux disponibles.

(Je peux ajouter d'autres pertes de temps que j'ai rencontrées sur demande.)

Bonne chance et rester en sécurité; les fuites sont mauvaises!


La plupart des exemples ici sont "trop ​​complexes". Ce sont des cas extrêmes. Avec ces exemples, le programmeur a commis une erreur (comme ne redéfinissant pas equals / hashcode) ou a été mordu par un cas de la JVM / JAVA (chargement de classe avec static ...). Je pense que ce n'est pas le type d'exemple qu'un intervieweur veut ou même le cas le plus courant.

Mais il existe des cas vraiment plus simples pour les fuites de mémoire. Le ramasse-miettes ne libère que ce qui n'est plus référencé. En tant que développeurs Java, nous ne nous soucions pas de la mémoire. Nous l'attribuons en cas de besoin et laissons le libérer automatiquement. Bien.

Mais toute application de longue durée a tendance à avoir un état partagé. Cela peut être n'importe quoi, la statique, les singletons ... Souvent, les applications non triviales ont tendance à créer des graphes d'objets complexes. Le simple fait d'oublier de définir une référence sur null ou plus souvent d'oublier de supprimer un objet d'une collection suffit à provoquer une fuite de mémoire.

Bien sûr, tous les types d'écouteurs (comme les auditeurs d'interface utilisateur), les caches ou tout état partagé de longue durée ont tendance à produire une fuite de mémoire s'ils ne sont pas gérés correctement. Ce qui doit être compris est que ce n'est pas un cas de coin Java, ou un problème avec le ramasse-miettes. C'est un problème de conception. Nous concevons que nous ajoutons un auditeur à un objet de longue durée, mais nous ne supprimons pas l'auditeur lorsqu'il n'est plus nécessaire. Nous mettons en cache des objets, mais nous n'avons aucune stratégie pour les supprimer du cache.

Nous avons peut-être un graphique complexe qui stocke l'état précédent nécessaire à un calcul. Mais l'état précédent est lui-même lié à l'état d'avant et ainsi de suite.

Comme nous devons fermer des connexions ou des fichiers SQL. Nous devons définir les références appropriées sur null et supprimer des éléments de la collection. Nous aurons des stratégies de cache appropriées (taille maximale de la mémoire, nombre d'éléments ou minuteries). Tous les objets permettant à un auditeur d'être notifié doivent fournir une méthode addListener et une méthode removeListener. Et lorsque ces notificateurs ne sont plus utilisés, ils doivent effacer leur liste d'écouteurs.

Une fuite de mémoire est en effet vraiment possible et parfaitement prévisible. Pas besoin de fonctionnalités linguistiques spéciales ou de cas d'angle. Les fuites de mémoire sont soit un signe de manque, soit des problèmes de conception.


La réponse dépend entièrement de ce que l'intervieweur pensait demander.

Est-il possible en pratique de faire fuir Java? Bien sûr que oui, et il existe de nombreux exemples dans les autres réponses.

Mais il y a plusieurs méta-questions qui peuvent avoir été posées?

  • Une implémentation Java théoriquement "parfaite" est-elle vulnérable aux fuites?
  • Le candidat comprend-il la différence entre théorie et réalité?
  • Le candidat comprend-il le fonctionnement de la collecte des ordures?
  • Ou comment la collecte des ordures est-elle censée fonctionner dans un cas idéal?
  • Savent-ils qu'ils peuvent appeler d'autres langues via des interfaces natives?
  • Savent-ils qu'ils perdent de la mémoire dans ces autres langues?
  • Le candidat sait-il même ce qu'est la gestion de la mémoire et ce qui se passe dans les coulisses de Java?

Je lis votre méta-question en tant que "Quelle réponse j'aurais pu utiliser dans cette situation d'entrevue". Et par conséquent, je vais me concentrer sur les compétences en entretien plutôt qu'en Java. Je crois que vous êtes plus susceptible de répéter la situation de ne pas connaître la réponse à une question dans une interview que de vous retrouver dans une situation où vous devez savoir comment faire fuir Java. Donc, espérons-le, cela aidera.

L'une des compétences les plus importantes que vous puissiez développer pour un entretien consiste à apprendre à écouter activement les questions et à travailler avec l'intervieweur pour en dégager le but. Cela vous permet non seulement de répondre à leur question comme ils le souhaitent, mais montre également que vous possédez des compétences de communication vitales. Et quand il s'agit de choisir entre de nombreux développeurs tout aussi talentueux, j'engage celui qui écoute, qui pense et qui comprend avant de répondre à chaque fois.


Voici un bon moyen de créer une véritable fuite de mémoire (objets inaccessibles en exécutant du code mais toujours stockés en mémoire) en Java pur:

  1. L'application crée un thread de longue durée (ou utilisez un pool de threads pour une fuite encore plus rapide).
  2. Le thread charge une classe via un ClassLoader (éventuellement personnalisé).
  3. La classe alloue une grande quantité de mémoire (par exemple, un new byte[1000000] ), stocke une référence forte à celle-ci dans un champ statique, puis stocke une référence à elle-même dans un ThreadLocal. L'affectation de la mémoire supplémentaire est facultative (la fuite de l'instance de classe suffit), mais la fuite fonctionnera beaucoup plus rapidement.
  4. Le thread efface toutes les références à la classe personnalisée ou au ClassLoader à partir duquel elle a été chargée.
  5. Répéter.

Cela fonctionne car le ThreadLocal conserve une référence à l'objet, qui conserve une référence à sa classe, qui à son tour garde une référence à son classLoader. Le ClassLoader, à son tour, conserve une référence à toutes les classes qu'il a chargées.

(C’était pire dans de nombreuses implémentations de machine virtuelle Java, en particulier avant Java 7, car Classes et ClassLoaders étaient alloués directement dans permgen et n’étaient jamais du tout enregistrés. Cependant, quelle que soit la manière dont la machine virtuelle gère le déchargement des classes, un ThreadLocal empêchera tout L'objet de classe n'est pas récupéré.)

Une variante de ce modèle est la raison pour laquelle les conteneurs d'applications (tels que Tomcat) peuvent perdre de la mémoire comme un tamis si vous redéployez fréquemment des applications utilisant de manière quelconque ThreadLocals. (Le conteneur de l'application utilisant les threads comme décrit et chaque fois que vous redéployez l'application, un nouveau ClassLoader est utilisé.)

Mise à jour : Comme de nombreuses personnes ne cessent de le demander, voici un exemple de code montrant ce comportement en action .


Voici un exemple assez inutile si vous ne comprenez pas JDBC . Ou du moins comment JDBC attend un développeur qu'il ferme ResultSet instances de Connection , Statement et ResultSet avant de les supprimer ou de perdre leurs références, au lieu de compter sur l'implémentation de 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);
   }
}

Le problème avec ce qui précède est que l'objet Connection n'est pas fermé et que, par conséquent, la connexion physique restera ouverte, jusqu'à ce que le ramasse-miettes arrive et constate qu'il est inaccessible. GC invoquera la méthode finalize , mais certains pilotes JDBC n'implémentent pas la finalize , du moins pas de la même manière que Connection.close . Le comportement qui en résulte est que, même si la mémoire sera récupérée en raison de la collecte d'objets inaccessibles, les ressources (y compris la mémoire) associées à l'objet Connection risquent tout simplement de ne pas être récupérées.

Dans un tel cas où la méthode de finalize Connection ne nettoiera pas tout, on pourrait même constater que la connexion physique au serveur de base de données durera plusieurs cycles de récupération de place, jusqu'à ce que le serveur de base de données comprenne finalement que la connexion n'est pas vivante ( si c'est le cas) et devrait être fermé.

Même si le pilote JDBC implémentait la finalize , il est possible que des exceptions soient générées lors de la finalisation. Le comportement qui en résulte est que toute mémoire associée à l'objet désormais "dormant" ne sera pas récupérée, étant donné que finalize est invoqué une seule fois.

Le scénario ci-dessus consistant à rencontrer des exceptions lors de la finalisation d'un objet est lié à un autre scénario pouvant éventuellement conduire à une fuite de mémoire: la résurrection d'objet. La résurrection d'objet est souvent faite intentionnellement en créant une forte référence à l'objet en cours de finalisation, à partir d'un autre objet. Lorsque la résurrection d'objet est mal utilisée, une fuite de mémoire est associée à d'autres sources de fuites de mémoire.

Il y a beaucoup plus d'exemples que vous pouvez évoquer - comme

  • Gérer une instance de List où vous ne faites qu'ajouter à la liste et ne pas la supprimer (bien que vous deviez vous débarrasser des éléments dont vous n'avez plus besoin), ou
  • Ouverture de Socket ou de File , mais ne les ferme pas lorsqu'ils ne sont plus nécessaires (comme dans l'exemple ci-dessus impliquant la classe Connection ).
  • Ne pas décharger des singletons lors de la fermeture d'une application Java EE. Apparemment, le chargeur de classe qui a chargé la classe singleton conservera une référence à la classe et, par conséquent, l'instance singleton ne sera jamais collectée. Lorsqu'une nouvelle instance de l'application est déployée, un nouveau chargeur de classes est généralement créé et l'ancien chargeur de classes continue d'exister en raison du singleton.

Champ statique contenant la référence d'objet [esp final field]

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

Appel de String.intern() sur une chaîne longue

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

Flux non ouverts (fichiers, réseau, etc ...)

try {
    BufferedReader br = new BufferedReader(new FileReader(inputFile));
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

Connexions non fermées

try {
    Connection conn = ConnectionFactory.getConnection();
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

Zones inaccessibles depuis le collecteur de mémoire de la machine virtuelle Java , telles que la mémoire allouée via des méthodes natives

Dans les applications Web, certains objets sont stockés dans le champ d'application jusqu'à ce que l'application soit explicitement arrêtée ou supprimée.

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

Options JVM incorrectes ou inappropriées , telles que l'option noclassgc sur le JDK IBM qui empêche le noclassgc la classe inutilisé

Voir Paramètres IBM jdk .


J'ai récemment rencontré une situation de fuite de mémoire causée d'une manière par log4j.

Log4j a ce mécanisme appelé Contexte de diagnostic imbriqué (NDC) qui est un instrument permettant de distinguer la sortie de journal entrelacée de différentes sources. La granularité à laquelle le NDC fonctionne est les threads, ce qui permet de distinguer les sorties de journal de différents threads séparément.

Afin de stocker les balises spécifiques aux threads, la classe NDC de log4j utilise une table de hachage qui est indexée par l'objet Thread lui-même (contrairement à l'ID de thread), et jusqu'à ce que la balise NDC reste en mémoire avec tous les objets qui pendent du thread. l'objet reste également en mémoire. Dans notre application Web, nous utilisons NDC pour baliser les sorties de connexion avec un ID de demande afin de distinguer les journaux d'une seule demande séparément. Le conteneur qui associe la balise NDC à un thread, la supprime également tout en renvoyant la réponse d'une requête. Le problème est survenu lors du traitement d'une demande, un thread enfant a été créé, quelque chose qui ressemble au code suivant:

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

Ainsi, un contexte NDC a été associé à un thread en ligne qui a été généré. L'objet thread qui était la clé de ce contexte NDC est le thread en ligne auquel se trouve l'objet énormeList. Ainsi, même après que le thread ait fini de faire ce qu'il faisait, la référence à la liste énorme a été maintenue en vie par le contexte NDC Hastable, provoquant ainsi une fuite de mémoire.


L’intervieweur a peut-être cherché une solution de référence circulaire:

    public static void main(String[] args) {
        while (true) {
            Element first = new Element();
            first.next = new Element();
            first.next.next = first;
        }
    }

Ceci est un problème classique du comptage de références. Vous expliqueriez alors poliment que les machines virtuelles utilisent un algorithme beaucoup plus sophistiqué qui n’a pas cette limitation.

-Nous Tarle


Peut-être en utilisant du code natif externe via JNI?

Avec Java pur, c'est presque impossible.

Mais il s’agit d’un type de fuite de mémoire "standard", lorsque vous ne pouvez plus accéder à la mémoire, mais qu’elle appartient toujours à l’application. Vous pouvez plutôt conserver des références à des objets inutilisés ou ouvrir des flux sans les fermer par la suite.


Comme beaucoup de gens l'ont suggéré, les fuites de ressources sont assez faciles à provoquer - comme dans les exemples JDBC. Les fuites de mémoire réelles sont un peu plus difficiles, en particulier si vous ne comptez pas sur des fragments cassés de la machine virtuelle Java pour le faire à votre place ...

Les idées de créer des objets qui ont une très grande empreinte et de ne pas pouvoir y accéder ne sont pas non plus de véritables fuites de mémoire. Si rien ne peut y accéder, les déchets seront collectés, et si quelque chose peut y accéder, il ne s'agit pas d'une fuite ...

Une façon que l' habitude de travailler bien - et je ne sais pas si cela ne fonctionne toujours - est d'avoir une chaîne circulaire trois profonde. Comme dans l'objet A une référence à l'objet B, l'objet B a une référence à l'objet C et l'objet C à une référence à l'objet A. Le GC était assez intelligent pour savoir qu'une chaîne de deux profondeurs - comme dans A <-> B - peut être collecté en toute sécurité si A et B ne sont pas accessibles par autre chose, mais ne peuvent pas gérer la chaîne à trois voies ...


Créez une carte statique et continuez à y ajouter des références exactes. Ceux-ci ne seront jamais 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); }
}

J'ai trouvé intéressant de noter que personne n'a utilisé les exemples de classe internes. Si vous avez une classe interne; elle maintient intrinsèquement une référence à la classe contenante. Bien entendu, il ne s’agit techniquement pas d’une fuite de mémoire, car Java finira par le nettoyer. mais cela peut amener les classes à rester plus longtemps que prévu.

public class Example1 {
  public Example2 getNewExample2() {
    return this.new Example2();
  }
  public class Example2 {
    public Example2() {}
  }
}

Maintenant, si vous appelez Exemple1 et obtenez un Exemple2 éliminant Exemple1, vous aurez toujours un lien vers un objet Exemple1.

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

J'ai aussi entendu une rumeur disant que si vous avez une variable qui existe depuis plus longtemps, Java suppose qu'il existera toujours et n'essaiera jamais de le nettoyer s'il n'est plus possible d'atteindre le code. Mais c'est complètement non vérifié.


Je ne pense pas que quiconque ait déjà dit cela: vous pouvez ressusciter un objet en redéfinissant la méthode finalize () de manière à ce que finalize () stocke une référence quelque part. Le ramasse-miettes ne sera appelé qu'une fois sur l'objet, après quoi l'objet ne sera jamais détruit.


Je suis récemment tombé sur un type de fuite de ressources plus subtil. Nous ouvrons des ressources via getResourceAsStream du chargeur de classe et il est arrivé que les descripteurs de flux d'entrée ne soient pas fermés.

Uhm, pourriez-vous dire, quel idiot.

Eh bien, ce qui rend cette question intéressante est la suivante: de cette manière, vous pouvez perdre de la mémoire du processus sous-jacent, plutôt que de celle de la machine virtuelle Java.

Tout ce dont vous avez besoin est un fichier jar contenant un fichier à l'intérieur duquel sera référencé le code Java. Plus le fichier jar est volumineux, plus la mémoire allouée est rapide.

Vous pouvez facilement créer un tel bocal avec la classe suivante:

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

Collez simplement dans un fichier nommé BigJarCreator.java, compilez-le et exécutez-le à partir de la ligne de commande:

javac BigJarCreator.java
java -cp . BigJarCreator

Et voilà: vous trouvez une archive JAR dans votre répertoire de travail actuel avec deux fichiers à l'intérieur.

Créons une deuxième 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);
    }

}

Cette classe ne fait fondamentalement rien mais crée des objets InputStream non référencés. Ces objets seront immédiatement récupérés et ne contribueront donc pas à la taille du tas. Il est important pour notre exemple de charger une ressource existante à partir d'un fichier jar, et la taille compte ici!

Si vous avez des doutes, essayez de compiler et de démarrer la classe ci-dessus, mais assurez-vous de choisir une taille de segment acceptable (2 Mo):

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

Vous ne rencontrerez pas d'erreur de MOO ici, car aucune référence n'est conservée, l'application continuera à fonctionner quelle que soit la taille choisie par ITERATIONS dans l'exemple ci-dessus. La consommation de mémoire de votre processus (visible dans l'en-tête (RES / RSS) ou l'explorateur de processus) augmente si l'application ne parvient pas à la commande wait. Dans la configuration ci-dessus, environ 150 Mo de mémoire seront alloués.

Si vous voulez que l'application joue en toute sécurité, fermez le flux d'entrée à l'endroit où il est créé:

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

et votre processus ne dépassera pas 35 Mo, indépendamment du nombre d'itérations.

Assez simple et surprenant.


L'intervieweur cherchait probablement une référence circulaire comme le code ci-dessous (qui ne contient d'ailleurs que de la mémoire dans de très anciennes machines virtuelles utilisant le comptage de références, ce qui n'est plus le cas). Mais comme la question est plutôt vague, il s’agit donc d’une excellente occasion de démontrer votre compréhension de la gestion de la mémoire de la machine virtuelle Java.

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

Ensuite, vous pouvez expliquer qu'avec le comptage de références, le code ci-dessus perdrait de la mémoire. Cependant, la plupart des machines virtuelles modernes n'utilisent plus le comptage de références. La plupart utilise un ramasse-miettes à balayage, qui collectera en fait cette mémoire.

Ensuite, vous expliquez comment créer un objet ayant une ressource native sous-jacente, comme ceci:

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

Vous pouvez ensuite expliquer qu'il s'agit techniquement d'une fuite de mémoire, mais c'est en réalité le code natif de la JVM qui alloue les ressources natives sous-jacentes, qui n'ont pas été libérées par votre code Java.

En fin de compte, avec une machine virtuelle Java moderne, vous devez écrire du code Java qui alloue une ressource native en dehors de la portée normale de la reconnaissance de la machine virtuelle Java.


Prenez n'importe quelle application Web s'exécutant dans n'importe quel conteneur de servlets (Tomcat, Jetty, Glassfish, peu importe ...). Redéployez l'application 10 ou 20 fois de suite (il suffit parfois de toucher le fichier WAR dans le répertoire de déploiement automatique du serveur.

À moins que quelqu'un n'ait réellement testé cela, il y a de fortes chances que vous obteniez une erreur OutOfMemoryError après quelques redéploiements, car l'application n'a pas pris soin de se nettoyer elle-même. Vous pouvez même trouver un bogue sur votre serveur avec ce test.

Le problème est que la durée de vie du conteneur est plus longue que celle de votre application. Vous devez vous assurer que toutes les références du conteneur aux objets ou aux classes de votre application peuvent être nettoyées.

Si une seule référence survit au non-déploiement de votre application Web, le chargeur de classes correspondant et, par conséquent, toutes les classes de votre application Web ne peuvent pas être nettoyés.

Les threads lancés par votre application, les variables ThreadLocal, l'enregistrement des ajouts sont quelques-uns des suspects habituels à l'origine des fuites du chargeur de classes.


Tout le monde oublie toujours la route de code natif. Voici une formule simple pour une fuite:

  1. Déclarez la méthode native.
  2. En méthode native, appelez malloc. N'appelle pas free.
  3. Appelez la méthode native.

N'oubliez pas que les allocations de mémoire en code natif proviennent de la pile JVM.


Voici un simple / sinistre via 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);
    }
}

Étant donné que la sous-chaîne fait référence à la représentation interne de la chaîne originale, chaîne beaucoup plus longue, l'original reste en mémoire. Ainsi, tant que vous avez un StringLeaker en jeu, vous avez également la chaîne entière en mémoire, même si vous pensez peut-être que vous ne tenez qu'une chaîne à un seul caractère.

Pour éviter de stocker une référence indésirable à la chaîne d'origine, procédez comme suit:

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

Pour plus de mal, vous pouvez également .intern()utiliser la sous-chaîne:

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

Cela permet de conserver en mémoire la chaîne longue d'origine et la sous-chaîne dérivée, même après la suppression de l'instance de StringLeaker.


Vous pouvez créer une fuite de mémoire en déplacement en créant une nouvelle instance d'une classe dans la méthode finalize de cette classe. Points bonus si le finaliseur crée plusieurs instances. Voici un programme simple qui divise tout le tas en quelques secondes à quelques minutes, en fonction de la taille de votre tas:

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




memory-leaks