3148 votes

Comment créer une fuite de mémoire en Java ?

Je viens d'avoir un entretien, et on m'a demandé de créer un fuite de mémoire avec Java.

Inutile de dire que je me suis sentie très bête, n'ayant aucune idée de la façon de commencer à en créer une.

Quel serait un exemple ?

2269voto

Daniel Pryden Points 22167

Voici un bon moyen de créer une véritable fuite de mémoire (objets inaccessibles par le code en cours d'exécution mais toujours stockés en mémoire) en Java pur :

  1. L'application crée un thread de longue durée (ou utilise un pool de threads pour fuir encore plus rapidement).
  2. Le fil d'exécution charge une classe par l'intermédiaire d'une commande (éventuellement personnalisée) ClassLoader .
  3. La classe alloue une grande partie de la mémoire (par ex. new byte[1000000] ), stocke une référence forte à celui-ci dans un champ statique, puis stocke une référence à lui-même dans un champ statique. ThreadLocal . L'allocation de la mémoire supplémentaire est facultative (la fuite de l'instance de la classe est suffisante), mais elle rendra la fuite encore plus rapide.
  4. L'application efface toutes les références à la classe personnalisée ou à l'élément ClassLoader à partir duquel il a été chargé.
  5. Répète.

En raison de la façon dont ThreadLocal est implémenté dans le JDK d'Oracle, cela crée une fuite de mémoire :

  • Chaque Thread a un champ privé threadLocals qui stocke en fait les valeurs locales du thread.
  • Chaque clé dans cette carte est une référence faible à un ThreadLocal donc après cela ThreadLocal est collecté, son entrée est supprimée de la carte.
  • Mais chaque valeur est une référence forte, donc lorsqu'une valeur (directement ou indirectement) pointe vers l'objet ThreadLocal qui est son clé cet objet ne sera ni collecté ni retiré de la carte tant que le thread sera en vie.

Dans cet exemple, la chaîne de références fortes ressemble à ceci :

Thread objet → threadLocals map → instance de la classe d'exemple → classe d'exemple → statique. ThreadLocal champ → ThreadLocal objet.

(Le ClassLoader ne joue pas vraiment un rôle dans la création de la fuite, elle ne fait qu'empirer la fuite à cause de cette chaîne de référence supplémentaire : exemple de la classe →. ClassLoader → toutes les classes qu'il a chargées. C'était encore pire dans de nombreuses implémentations de la JVM, notamment avant Java 7, car les classes et les ClassLoader ont été alloués directement dans permgen et n'ont jamais été collectés dans les poubelles).

Une variation de ce schéma explique pourquoi les conteneurs d'applications (comme Tomcat) peuvent perdre de la mémoire comme une passoire si vous redéployez fréquemment des applications qui utilisent ThreadLocal qui, d'une certaine manière, renvoient à eux-mêmes. Cela peut se produire pour un certain nombre de raisons subtiles et est souvent difficile à déboguer et/ou à réparer.

Mise à jour : Puisque beaucoup de gens continuent à le demander, voici un exemple de code qui montre ce comportement en action .

1194voto

Prashant Bhate Points 4669

Champ statique contenant une référence à un objet [en particulier un final champ]

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

Les zones qui sont inaccessibles au ramasseur de déchets de la 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 sur le JDK d'IBM qui empêche la récupération des classes inutilisées.

Voir Paramètres du JDK d'IBM .

451voto

Peter Lawrey Points 229686

Une chose simple à faire est d'utiliser un HashSet avec une valeur incorrecte (ou inexistante) de la variable hashCode() ou equals() puis continuer à ajouter des "doublons". Au lieu d'ignorer les doublons comme il se doit, l'ensemble ne fera que croître et vous ne pourrez pas les supprimer.

Si vous voulez que ces mauvaises clés/éléments restent en place, vous pouvez utiliser un champ statique tel que

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.

268voto

bestsss Points 6403

Vous trouverez ci-dessous un cas non évident de fuite de Java, en plus du cas standard des listeners oubliés, des références statiques, des clés bidon/modifiables dans les hashmaps, ou simplement des threads bloqués sans aucune chance de terminer leur cycle de vie.

  • File.deleteOnExit() - laisse toujours échapper la chaîne de caractères, Si la chaîne est une sous-chaîne, la fuite est encore pire (le char[] sous-jacent est également divulgué). - Dans Java 7, la sous-chaîne copie également le char[] donc la dernière ne s'applique pas ; @Daniel, pas besoin de votes, cependant.

Je me concentrerai sur les threads pour montrer le danger des threads non gérés, je ne souhaite même pas aborder le swing.

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

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

  • La création d'un thread hérite du ContextClassLoader et AccessControlContext plus le ThreadGroup et tout InheritedThreadLocal toutes ces références sont des fuites potentielles, tout comme les classes entières 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 du framework j.u.c.Executor qui comporte une fonction super simple de type ThreadFactory Pourtant, la plupart des développeurs n'ont aucune idée du danger qui les guette. De plus, de nombreuses bibliothèques lancent des threads sur demande (beaucoup trop de bibliothèques populaires dans l'industrie).

  • ThreadLocal caches ; ceux-ci sont diaboliques dans de nombreux 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 loin que prévu la vie le contexte ClassLoader, c'est une pure petite fuite sympathique. N'utilisez pas les caches ThreadLocal à moins d'en avoir vraiment besoin.

  • Appel à ThreadGroup.destroy() lorsque le ThreadGroup n'a pas de threads lui-même, mais qu'il conserve des ThreadGroups enfants. Une mauvaise fuite qui empêchera le ThreadGroup de se retirer de son parent, mais tous les enfants deviennent indénombrables.

  • Utilisation de WeakHashMap et la valeur (in)fait directement référence à la clé. C'est difficile à trouver sans un vidage du tas. Cela s'applique à tous les Weak/SoftReference qui pourrait garder une référence dure à l'objet gardé.

  • Utilisation de java.net.URL avec le protocole HTTP(S) et en chargeant la ressource depuis( !). Celui-ci est spécial, le KeepAliveCache crée un nouveau thread dans le ThreadGroup du système qui fuit le classloader du contexte du thread actuel. Le thread est créé à la première demande lorsqu'il n'existe pas de thread vivant, donc vous pouvez avoir de la chance ou simplement fuir. Cette fuite est déjà corrigée dans Java 7 et le code qui crée le thread supprime correctement le classloader du contexte. Il existe quelques autres cas ( comme ImageFetcher , également fixé ) de créer des fils similaires.

  • Utilisation de InflaterInputStream en passant par new java.util.zip.Inflater() dans le constructeur ( PNGImageDecoder par exemple) et ne pas appeler end() du gonfleur. Eh bien, si vous passez dans le constructeur avec juste new aucune chance... Et oui, appeler close() sur le flux ne ferme pas l'inflateur s'il est passé manuellement comme paramètre du constructeur. Ce n'est pas une vraie fuite puisqu'elle sera libérée par le finaliseur... quand il le jugera nécessaire. Jusqu'à ce moment-là, il consomme de la mémoire native à tel point 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 a empiré la situation jusqu'à 7.0.2. Morale de l'histoire : libérez les ressources natives dès que vous le pouvez ; le finalisateur est tout simplement 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 est le 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 dernières : 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 soyez prudents ; les fuites sont diaboliques !

157voto

PlayTank Points 230

La réponse dépend entièrement de ce que l'enquêteur 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 de multiples méta-questions qui ont pu être posées ?

  • Une implémentation Java théoriquement "parfaite" est-elle vulnérable aux fuites ?
  • Le candidat comprend-il la différence entre la théorie et la réalité ?
  • Le candidat comprend-il le fonctionnement du garbage collection ?
  • Ou comment le garbage collection est censé fonctionner dans un cas idéal ?
  • Savent-ils qu'ils peuvent appeler d'autres langues via des interfaces natives ?
  • Savent-ils faire fuir la mémoire dans ces autres langues ?
  • Le candidat sait-il seulement ce qu'est la gestion de la mémoire et ce qui se passe derrière la scène en Java ?

J'interprète votre méta-question comme étant "Quelle est la réponse que j'aurais pu utiliser dans cette situation d'entretien". Et donc, je vais me concentrer sur les compétences d'entretien plutôt que sur Java. Je pense que vous êtes plus susceptible de répéter la situation de ne pas connaître la réponse à une question lors d'un entretien que d'être dans une situation où vous avez besoin de savoir comment faire fuir Java. J'espère donc que cela vous aidera.

L'une des compétences les plus importantes que vous pouvez développer pour les entretiens est d'apprendre à écouter activement les questions et à travailler avec l'intervieweur pour extraire son intention. Cela vous permettra non seulement de répondre à sa question de la manière qu'il souhaite, mais aussi de montrer que vous avez des compétences essentielles en matière de communication. Et lorsqu'il faudra choisir entre plusieurs développeurs de talent égal, j'engagerai toujours celui qui écoute, réfléchit et comprend avant de répondre.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X