tool Création d'une fuite de mémoire avec Java




permanent generation (24)

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?


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 .


Vous pouvez créer une fuite de mémoire avec la classe sun.misc.Unsafe . En fait, cette classe de service est utilisée dans différentes classes standard (par exemple, dans les classes java.nio ). Vous ne pouvez pas créer d'instance de cette classe directement , mais vous pouvez utiliser la réflexion pour le faire .

Le code ne se compile pas dans Eclipse IDE - compilez-le à l'aide de la commande javac (lors de la compilation, vous recevrez des avertissements)

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

}

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.


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


Qu'est-ce qu'une fuite de mémoire?

  • C'est causé par un bug ou une mauvaise conception.
  • C'est un gaspillage de mémoire.
  • Cela empire avec le temps.
  • Le ramasse-miettes ne peut pas le nettoyer.

Exemple typique:

Un cache d'objets est un bon point de départ pour tout gâcher.

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

Votre cache grandit et grandit. Et très vite, toute la base de données est aspirée en mémoire. Une meilleure conception utilise un LRUMap (conserve uniquement les objets récemment utilisés dans le cache).

Bien sûr, vous pouvez rendre les choses beaucoup plus compliquées:

  • en utilisant des constructions ThreadLocal .
  • ajout d' arbres de référence plus complexes .
  • ou des fuites causées par des bibliothèques tierces .

Qu'est-ce qui se passe souvent:

Si cet objet Info contient des références à d'autres objets, ceux-ci le sont également. D'une certaine manière, vous pourriez aussi considérer qu'il s'agit d'une fuite de mémoire (due à une mauvaise conception).


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.


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.


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

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.

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.


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

Les threads ne sont pas collectés jusqu'à ce qu'ils se terminent. Ils servent de roots collecte des ordures. Ils sont l’un des rares objets qui ne seront pas récupérés simplement en les oubliant ou en supprimant les références.

Considérez: le modèle de base pour terminer un thread de travail consiste à définir une variable de condition vue par le thread. Le thread peut vérifier la variable périodiquement et l'utiliser comme signal pour se terminer. Si la variable n'est pas déclarée volatile, la modification apportée à la variable risque de ne pas être vue par le thread, de sorte qu'il ne saura pas se terminer. Ou imaginez si certains threads souhaitent mettre à jour un objet partagé, mais bloquent en essayant de le verrouiller.

Si vous n'avez qu'une poignée de threads, ces bugs seront probablement évidents car votre programme cessera de fonctionner correctement. Si vous avez un pool de threads qui crée plus de threads selon vos besoins, les threads obsolètes / bloqués risquent de ne pas être remarqués et s'accumuleront indéfiniment, provoquant une fuite de mémoire. Les threads sont susceptibles d'utiliser d'autres données dans votre application. Par conséquent, tout ce à quoi ils font directement référence ne sera jamais collecté.

Comme exemple de jouet:

static void leakMe(final Object object) {
    new Thread() {
        public void run() {
            Object o = object;
            for (;;) {
                try {
                    sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {}
            }
        }
    }.start();
}

Appelez System.gc()tout ce que vous voulez, mais l'objet transmis leakMene mourra jamais.

(*édité*)


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.


J'ai eu une belle "fuite de mémoire" en relation avec PermGen et l'analyse XML une fois. L'analyseur XML que nous avons utilisé (je ne me souviens plus lequel) était un String.intern () sur les noms de balises, pour accélérer la comparaison. Un de nos clients a eu la bonne idée de stocker des valeurs de données non pas dans des attributs XML ni dans du texte, mais sous forme de noms de variables. Nous avions donc un document du type:

<data>
   <1>bla</1>
   <2>foo</>
   ...
</data>

En fait, ils n'utilisaient pas de chiffres, mais des identifiants textuels plus longs (environ 20 caractères), uniques et enregistrés à un rythme de 10 à 15 millions par jour. Cela fait 200 Mo de déchets par jour, ce qui n’est plus jamais nécessaire et qui n’a jamais été transféré (car il est en PermGen). Permgen étant réglé sur 512 Mo, il a fallu environ deux jours pour que l'exception de mémoire insuffisante (OOME) arrive ...


Un exemple courant de ceci dans le code d'interface graphique est lors de la création d'un widget / composant, l'ajout d'un écouteur à un objet couvert d'application statique / application et la suppression de l'écouteur lorsque le widget est détruit. Vous obtenez non seulement une fuite de mémoire, mais également un coup dur pour la performance: lorsque vous écoutez des événements, tous vos anciens auditeurs sont également appelés.


Je peux copier ma réponse à partir d’ici: Le moyen le plus simple de provoquer une fuite de mémoire en Java?

"En informatique, une fuite de mémoire (ou une fuite, dans ce contexte) se produit lorsqu'un programme informatique consomme de la mémoire mais ne peut pas la restituer au système d'exploitation." (Wikipédia)

La réponse facile est: tu ne peux pas. Java gère automatiquement la mémoire et libère des ressources inutiles. Vous ne pouvez pas empêcher cela de se produire. Il sera TOUJOURS capable de libérer les ressources. Dans les programmes avec gestion manuelle de la mémoire, cela est différent. Vous pouvez obtenir de la mémoire en C en utilisant malloc (). Pour libérer la mémoire, vous avez besoin du pointeur renvoyé par malloc et appelez free () dessus. Mais si vous n'avez plus le pointeur (écrasé, ou la durée de vie dépassée), alors vous êtes malheureusement incapable de libérer cette mémoire et vous avez donc une fuite de mémoire.

Toutes les autres réponses à ce jour sont dans ma définition pas vraiment des fuites de mémoire. Ils visent tous à remplir la mémoire avec des trucs inutiles très rapidement. Mais à tout moment, vous pouvez toujours déréférencer les objets que vous avez créés et libérer ainsi la mémoire -> AUCUNE FUITE. La réponse de acconrad est assez proche cependant, comme je dois l'admettre, car sa solution consiste simplement à "écraser" le ramasse-miettes en le forçant dans une boucle sans fin).

La réponse longue est: Vous pouvez obtenir une fuite de mémoire en écrivant une bibliothèque pour Java à l'aide de JNI, qui peut gérer manuellement la mémoire et avoir ainsi des fuites de mémoire. Si vous appelez cette bibliothèque, votre processus Java perdra de la mémoire. Ou bien, vous pouvez avoir des bogues dans la machine virtuelle Java, de sorte que celle-ci perde de la mémoire. Il y a probablement des bogues dans la machine virtuelle, voire même des problèmes connus, car la récupération de place n'est pas si simple, mais c'est quand même un bogue. Par conception, ce n'est pas possible. Vous demandez peut-être du code Java affecté par un tel bogue. Désolé, je n'en connais pas et il est possible que ce ne soit plus un bogue dans la prochaine version de Java.


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.


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.


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.


Je pense qu'un exemple valide pourrait utiliser des variables ThreadLocal dans un environnement où les threads sont mis en pool.

Par exemple, utiliser des variables ThreadLocal dans Servlets pour communiquer avec d'autres composants Web, faire en sorte que les threads soient créés par le conteneur et conserver ceux inactifs dans un pool. Les variables ThreadLocal, si elles ne sont pas correctement nettoyées, y resteront jusqu'à ce que le même composant Web remplace éventuellement leurs valeurs.

Bien sûr, une fois identifié, le problème peut être résolu facilement.


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


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 .


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.


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







memory-leaks