Question Créer une fuite de mémoire avec Java


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 assez bête n'ayant aucune idée sur la façon de commencer même à en créer un.

Quel serait un exemple?


2634


origine


Réponses:


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 à exécution longue (ou utilise un pool de threads pour une fuite encore plus rapide).
  2. Le thread charge une classe via un ClassLoader (facultatif).
  3. La classe alloue un gros morceau de mémoire (par ex. new byte[1000000]), stocke une référence forte dans un champ statique, puis stocke une référence à lui-même dans un ThreadLocal. L'allocation de la mémoire supplémentaire est facultative (la fuite de l'instance de classe est suffisante), 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 il a été chargé.
  5. Répéter.

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

(Il était pire dans de nombreuses implémentations JVM, en particulier avant Java 7, parce que Classes et ClassLoaders étaient directement alloués dans permgen et n'étaient jamais du tout GC'd.Cependant, indépendamment de la façon dont la JVM gère le déchargement de classe, un ThreadLocal empêchera toujours Objet de classe d'être récupéré.)

Une variation sur ce modèle est pourquoi les conteneurs d'applications (comme Tomcat) peuvent fuir la mémoire comme un tamis si vous redéployez fréquemment des applications qui utilisent ThreadLocals de quelque façon que ce soit. (Puisque le conteneur d'application utilise des Threads comme décrit, et chaque fois que vous redéployez l'application, un nouveau ClassLoader est utilisé.)

Mettre à jour: Depuis beaucoup de gens ne cessent de le demander, voici un exemple de code qui montre ce comportement en action.


1931



Référence d'objet de maintien de champ statique [champ final esp]

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

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

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 ouverts (non closes) (fichier, 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 garbage collector de JVM, comme la mémoire allouée par des méthodes natives

Dans les applications Web, certains objets sont stockés dans la portée de l'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, comme le noclassgc option sur IBM JDK qui empêche la récupération de place inutilisée

Voir Paramètres IBM jdk.


1059



Une chose simple à faire est d'utiliser un HashSet avec un incorrect (ou inexistant) hashCode() ou equals(), puis continuez d'ajouter des "doublons". Au lieu d'ignorer les doublons comme il se doit, l'ensemble ne fera que croître et vous ne serez pas en mesure de les supprimer.

Si vous voulez que ces mauvaises clés / éléments traînent, vous pouvez utiliser un champ statique comme

class BadKey {
   // no hashCode or equals();
   public final String key;
   public BadKey(String key) { this.key = key; }
}

Map map = System.getProperties();
map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.

390



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

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

Je vais me concentrer sur les threads pour montrer le danger des threads non gérés la plupart du temps, ne veulent même pas toucher le swing.

  • Runtime.addShutdownHook et ne pas supprimer ... et même avec removeShutdownHook en raison d'un bogue dans la classe ThreadGroup concernant les threads non démarrés, il peut ne pas être collecté, fuir efficacement le ThreadGroup. JGroup a la fuite dans GossipRouter.

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

  • La création d'un thread hérite de ContextClassLoader et AccessControlContextplus le ThreadGroup et n'importe quel InheritedThreadLocal, toutes ces références sont des fuites potentielles, ainsi que les classes entières chargées par le chargeur de classe et toutes les références statiques, et ja-ja. L'effet est particulièrement visible avec l'ensemble du framework j.u.c.Executor qui dispose d'un super simple ThreadFactory interface, mais la plupart des développeurs n'ont aucune idée du danger caché. De nombreuses bibliothèques démarrent également des threads à la demande (bien trop de bibliothèques populaires de l'industrie).

  • ThreadLocal des caches; ce sont des maux dans beaucoup de cas. Je suis sûr que tout le monde a vu pas mal de caches simples basés sur ThreadLocal, et bien la mauvaise nouvelle: si le thread continue à aller plus que prévu la vie du ClassLoader, c'est une pure petite fuite. N'utilisez pas de caches ThreadLocal, sauf si nécessaire.

  • Appel ThreadGroup.destroy() lorsque le ThreadGroup n'a pas de threads, mais qu'il conserve les fils ThreadGroups. Une mauvaise fuite qui empêchera le ThreadGroup de se retirer de son parent, mais tous les enfants deviennent non énumérables.

  • L'utilisation de WeakHashMap et la valeur (in) référence directement la clé. C'est difficile à trouver sans une décharge de tas. Cela vaut pour toutes les extensions Weak/SoftReference cela pourrait garder une référence difficile à l'objet gardé.

  • En utilisant java.net.URL avec le protocole HTTP (S) et le chargement de la ressource à partir de (!). Celui-ci est spécial, le KeepAliveCache crée un nouveau thread dans le système ThreadGroup qui fuit le chargeur de classe de contexte du thread en cours. Le thread est créé lors de la première requête lorsqu'il n'existe aucun thread vivant, donc vous pouvez avoir de la chance ou simplement fuir. La fuite est déjà corrigée dans Java 7 et le code qui crée le thread supprime correctement le classloader de contexte. Il y a quelques autres cas (comme ImageFetcher, également corrigé) de créer des threads similaires.

  • En utilisant InflaterInputStream qui passe new java.util.zip.Inflater() dans le constructeur (PNGImageDecoder par exemple) et ne pas appeler end() de l'inflateur. Eh bien, si vous passez dans le constructeur avec juste new, pas de chance ... Et oui, en appelant close() sur le flux ne ferme pas l'inflateur s'il est passé manuellement en tant que paramètre constructeur. Ce n'est pas une vraie fuite car elle serait libérée par le finaliseur ... quand il le jugera nécessaire. Jusqu'à ce moment-là, il mange si mal la 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 G1 l'a fait pire jusqu'à 7.0.2. Morale de l'histoire: libérer des ressources natives dès que vous le pouvez; le finaliseur est juste trop pauvre.

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

(Je peux ajouter plus de gaspilleurs de temps que j'ai rencontrés sur demande.)

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


228



La plupart des exemples sont "trop ​​complexes". Ce sont des cas de bordure. Avec ces exemples, le programmeur a fait une erreur (comme ne pas redéfinir equals / hashcode), ou a été mordu par un casse de la JVM / JAVA (charge de classe avec statique ...). Je pense que ce n'est pas le type d'exemple qu'un intervieweur veut ou même le cas le plus commun.

Mais il y a des cas vraiment plus simples pour les fuites de mémoire. Le garbage collector libère seulement 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'allouons en cas de besoin et le libérons automatiquement. Bien.

Mais toute application de longue durée tend à avoir un état partagé. Il peut s'agir de n'importe quoi, de statique, de singletons ... Souvent, les applications non triviales tendent à faire des graphes d'objets complexes. Le simple fait d'oublier de définir une référence à null ou, plus souvent, d'oublier de supprimer un objet d'une collection est suffisant pour provoquer une fuite de mémoire.

Bien sûr, tous les types d'auditeurs (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 correctement gérés. Ce qui doit être compris est que ce n'est pas un cas de coin de Java, ou un problème avec le garbage collector. C'est un problème de conception. Nous concevons que nous ajoutons un écouteur à un objet à longue durée de vie, mais nous ne supprimons pas l'écouteur 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 graphe 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 avant et ainsi de suite.

Comme nous devons fermer les connexions ou les fichiers SQL. Nous devons définir des références correctes à null et supprimer des éléments de la collection. Nous aurons des stratégies de mise en cache appropriées (taille maximale de la mémoire, nombre d'éléments ou minuteurs). Tous les objets qui permettent à un écouteur d'être averti doivent fournir une méthode addListener et 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 indicateur que quelque chose est peut-être manquant ou même des problèmes de conception.


150



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

Est-il possible en pratique de faire fuir Java? Bien sûr, et il y a beaucoup d'exemples dans les autres réponses.

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

  • Une implémentation Java "parfaite" est-elle vulnérable aux fuites?
  • Est-ce que le candidat comprend la différence entre la théorie et la réalité?
  • Est-ce que le candidat comprend comment fonctionne la collecte des ordures?
  • Ou comment la collecte des ordures est censée fonctionner dans un cas idéal?
  • Savent-ils qu'ils peuvent appeler d'autres langues via des interfaces natives?
  • Savent-ils qu'il y a une fuite de mémoire dans ces autres langues?
  • Est-ce que le candidat sait même ce qu'est la gestion de la mémoire, et ce qui se passe derrière la scène en Java?

Je lis votre méta-question comme "Quelle est la réponse que j'aurais pu utiliser dans cette situation d'entrevue". Et par conséquent, je vais me concentrer sur les techniques d'interview au lieu de 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 vous devez être dans un endroit où il faut savoir comment faire fuir Java. Donc, j'espère, cela aidera.

L'une des compétences les plus importantes que vous pouvez développer pour l'entretien consiste à apprendre à écouter activement les questions et à travailler avec l'intervieweur pour en extraire l'intention. Non seulement cela vous permet de répondre à leur question comme ils le veulent, mais cela montre aussi que vous avez des compétences de communication essentielles. Et quand il s'agit de choisir entre de nombreux développeurs tout aussi talentueux, je recruterai celui qui écoute, pense et comprend avant de répondre à chaque fois.


133



Ce qui suit est un exemple assez inutile, si vous ne comprenez pas JDBC. Ou du moins comment JDBC s'attend à ce qu'un développeur ferme Connection, Statement et ResultSet instances avant de les rejeter ou de les perdre, au lieu de s'appuyer sur la mise en œuvre 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 le Connection l'objet n'est pas fermé, et par conséquent la connexion physique restera ouverte, jusqu'à ce que le garbage collector arrive et voit qu'il est inaccessible. GC invoquera le finalize méthode, mais il existe des pilotes JDBC qui ne mettent pas en œuvre le finalize, du moins pas de la même manière que Connection.close est implémenté. Le comportement résultant est que, tandis que 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 à la Connection l'objet peut simplement ne pas être récupéré.

Dans un tel cas où le Connectionde finalize La méthode ne nettoie pas tout, on peut en effet constater que la connexion physique au serveur de base de données durera plusieurs cycles de récupération de la mémoire, jusqu'à ce que le serveur de base de données découvre que la connexion n'est pas active.

Même si le pilote JDBC devait mettre en œuvre finalize, il est possible que des exceptions soient levées pendant la finalisation. Le comportement résultant est que toute mémoire associée à l'objet maintenant "dormant" ne sera pas récupérée, comme finalize est garanti pour être invoqué seulement une fois.

Le scénario ci-dessus de rencontrer des exceptions lors de la finalisation de l'objet est lié à un autre scénario qui pourrait entraîner une fuite de mémoire - résurrection de l'objet. La résurrection d'objet est souvent faite intentionnellement en créant une référence forte à l'objet d'être finalisé, à partir d'un autre objet. Lorsque la résurrection de l'objet est mal utilisée, cela entraîne une fuite de mémoire en combinaison avec d'autres sources de fuites de mémoire.

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

  • Gérer un List exemple où vous ajoutez seulement à la liste et ne la supprimez pas (bien que vous devriez vous débarrasser des éléments dont vous n'avez plus besoin), ou
  • Ouverture Sockets ou Files, mais ne les ferme pas quand ils ne sont plus nécessaires (similaire à l'exemple ci-dessus impliquant le Connection classe).
  • Ne pas décharger des singletons lors de la suppression d'une application Java EE. Apparemment, le Classloader 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 classe est généralement créé et l'ancien chargeur de classe continue d'exister en raison du singleton.

113



Probablement l'un des exemples les plus simples d'une fuite de mémoire potentielle, et comment l'éviter, est l'implémentation de ArrayList.remove (int):

public E remove(int index) {
    RangeCheck(index);

    modCount++;
    E oldValue = (E) elementData[index];

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index,
                numMoved);
    elementData[--size] = null; // (!) Let gc do its work

    return oldValue;
}

Si vous l'implémentiez vous-même, auriez-vous pensé à effacer l'élément de tableau qui n'est plus utilisé (elementData[--size] = null)? Cette référence pourrait garder un énorme objet en vie ...


102



Chaque fois que vous gardez des références autour d'objets dont vous n'avez plus besoin, vous avez une fuite de mémoire. Voir Gestion des fuites de mémoire dans les programmes Java pour des exemples de la façon dont les fuites de mémoire se manifestent dans Java et ce que vous pouvez faire à ce sujet.


63



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

Le code ne compile pas dans Eclipse IDE - compilez-le en utilisant la commande javac (pendant la compilation, vous aurez 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();
        }
    }

}

43