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

3224

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 stupide de n'avoir aucune idée de comment commencer à en créer un.

Quel serait un exemple?

Mat B.
la source
275
Je leur dirais que Java utilise un éboueur, et leur demander d'être un peu plus précis sur leur définition de « fuite de mémoire », expliquant que - à l'exception des bugs JVM - Java ne peut pas fuir mémoire tout à fait de la même manière C / C ++ peut. Vous devez avoir une référence à l'objet quelque part .
Darien
371
Je trouve drôle que sur la plupart des réponses, les gens recherchent ces cas et astuces et semblent manquer complètement le point (OMI). Ils pourraient simplement afficher du code qui conserve des références inutiles à des objets qui ne seront plus jamais utilisés, et en même temps ne jamais supprimer ces références; on peut dire que ces cas ne sont pas des "vraies" fuites de mémoire car il y a toujours des références à ces objets, mais si le programme n'utilise plus jamais ces références et ne les supprime jamais, il est complètement équivalent à (et aussi mauvais que) un " véritable fuite de mémoire ".
ehabkost
62
Honnêtement, je ne peux pas croire que la question similaire que j'ai posée à propos de "Go" a été réduite à -1. Ici: stackoverflow.com/questions/4400311/… Fondamentalement, les fuites de mémoire dont je parlais sont celles qui ont obtenu +200 votes positifs pour l'OP et pourtant j'ai été attaqué et insulté pour avoir demandé si "Go" avait le même problème. D'une manière ou d'une autre, je ne suis pas sûr que tout ce qui fonctionne sur wiki fonctionne aussi bien.
SyntaxT3rr0r
74
@ SyntaxT3rr0r - la réponse de darien n'est pas le fanboyisme. il a explicitement admis que certaines machines virtuelles Java peuvent avoir des bogues qui entraînent une fuite de mémoire. c'est différent de la spécification de langue elle-même, ce qui permet des fuites de mémoire.
Peter Recore
31
@ehabkost: Non, ils ne sont pas équivalents. (1) Vous possédez la possibilité de récupérer la mémoire, alors que dans une "vraie fuite" votre programme C / C ++ oublie la plage qui a été allouée, il n'y a aucun moyen sûr de récupérer. (2) Vous pouvez très facilement détecter le problème du profilage, car vous pouvez voir quels objets le "ballonnement" implique. (3) Une "vraie fuite" est une erreur sans équivoque, alors qu'un programme qui conserve beaucoup d'objets jusqu'à sa fin pourrait être une partie délibérée de la façon dont il est censé fonctionner.
Darien

Réponses:

2309

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 (éventuellement personnalisé) ClassLoader.
  3. La classe alloue une grande partie de la mémoire (par exemple new byte[1000000]), stocke une référence forte à elle dans un champ statique, puis stocke une référence à elle-même dans a ThreadLocal. L'allocation de la mémoire supplémentaire est facultative (la fuite de l'instance de classe est suffisante), mais cela rendra la fuite beaucoup plus rapide.
  4. L'application efface toutes les références à la classe personnalisée ou à la ClassLoadersource à partir de laquelle elle a été chargée.
  5. Répéter.

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

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

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

Threadobjet → threadLocalscarte → instance de l'exemple de classe → exemple de classe → ThreadLocalchamp statique → ThreadLocalobjet.

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

Une variation de ce modèle est la raison pour laquelle les conteneurs d'applications (comme Tomcat) peuvent fuir la mémoire comme un tamis si vous redéployez fréquemment des applications qui utilisent des ThreadLocals qui, d'une manière ou d'une autre, pointent vers elles-mêmes. Cela peut se produire pour un certain nombre de raisons subtiles et est souvent difficile à déboguer et / ou à corriger.

Mise à jour : Étant donné que de nombreuses personnes continuent de le demander, voici un exemple de code qui montre ce comportement en action .

Daniel Pryden
la source
186
+1 Les fuites ClassLoader sont parmi les fuites de mémoire les plus douloureuses dans le monde JEE, souvent causées par des bibliothèques tierces qui transforment les données (BeanUtils, codecs XML / JSON). Cela peut se produire lorsque la bibliothèque est chargée en dehors du chargeur de classe racine de votre application mais contient des références à vos classes (par exemple, par mise en cache). Lorsque vous annulez le déploiement / redéploiement de votre application, la machine virtuelle Java n'est pas en mesure de récupérer le chargeur de classe de l'application (et donc toutes les classes chargées par elle), de sorte qu'avec les déploiements répétés, le serveur d'application finit par bork. Si vous avez de la chance, vous obtenez un indice avec ClassCastException zxyAbc ne peut pas être casté en zxyAbc
earcam
7
tomcat utilise des astuces et nils TOUTES les variables statiques dans TOUTES les classes chargées, tomcat a beaucoup de dataraces et de mauvais codage cependant (besoin de gagner du temps et de soumettre des correctifs), plus le tout ConcurrentLinkedQueue époustouflant en tant que cache pour les (petits) objets internes, si petit que même le ConcurrentLinkedQueue.Node prend plus de mémoire.
bestsss
57
+1: Les fuites du chargeur de classe sont un cauchemar. J'ai passé des semaines à essayer de les comprendre. Le plus triste est que, comme l'a dit @earcam, ils sont principalement causés par des bibliothèques tierces et la plupart des profileurs ne peuvent pas détecter ces fuites. Il y a une bonne et claire explication sur ce blog sur les fuites de Classloader. blogs.oracle.com/fkieviet/entry/…
Adrian M
4
@Nicolas: Êtes-vous sûr? JRockit fait les objets GC Class par défaut, et HotSpot non, mais AFAIK JRockit ne peut toujours pas GC une classe ou un ClassLoader référencé par un ThreadLocal.
Daniel Pryden
6
Tomcat essaiera de détecter ces fuites pour vous et vous en avertira: wiki.apache.org/tomcat/MemoryLeakProtection . La version la plus récente corrige parfois même la fuite pour vous.
Matthijs Bierman
1212

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

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

Zones inaccessibles depuis le garbage collector de la JVM , telles que la mémoire allouée via 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 , telles que l' noclassgcoption sur IBM JDK qui empêche le garbage collection inutilisé

Voir Paramètres IBM jdk .

Prashant Bhate
la source
178
Je ne suis pas d'accord pour dire que le contexte et les attributs de session sont des «fuites». Ce ne sont que des variables à longue durée de vie. Et le champ final statique n'est plus ou moins qu'une constante. Peut-être que les grandes constantes devraient être évitées, mais je ne pense pas qu'il soit juste d'appeler cela une fuite de mémoire.
Ian McLaird
80
Les flux ouverts (non fermés) (fichier, réseau, etc.) ne fuient pas pour de vrai, pendant la finalisation (qui sera après le prochain cycle du GC), close () va être planifié ( close()n'est généralement pas invoqué dans le finaliseur). thread car il peut s'agir d'une opération de blocage). C'est une mauvaise pratique de ne pas fermer, mais cela ne provoque pas de fuite. La java.sql.Connection non fermée est la même.
bestsss
33
Dans la plupart des machines virtuelles Java sensées, il semble que la classe String n'ait qu'une faible référence sur son interncontenu de table de hachage. En tant que tel, il s'agit de déchets correctement récupérés et non d'une fuite. (mais IANAJP) mindprod.com/jgloss/interned.html#GC
Matt B.
42
Comment le champ statique contenant la référence d'objet [champ final esp] est une fuite de mémoire ??
Kanagavelu Sugumar
5
@cHao True. Le danger que j'ai rencontré n'est pas les problèmes de fuite de mémoire par Streams. Le problème est qu'ils n'ont pas assez de mémoire. Vous pouvez laisser couler beaucoup de poignées, mais vous avez encore beaucoup de mémoire. Le garbage collector pourrait alors décider de ne pas s'embêter à faire une collection complète car il a encore beaucoup de mémoire. Cela signifie que le finaliseur n'est pas appelé, vous n'avez donc plus de poignées. Le problème est que, les finaliseurs seront (généralement) exécutés avant de manquer de mémoire à partir de flux qui fuient, mais il se peut qu'ils ne soient pas appelés avant que vous n'ayez plus de mémoire.
Patrick M
460

Une chose simple à faire est d'utiliser un HashSet avec un incorrect (ou inexistante) hashCode()ou equals(), puis continuer à ajouter « 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 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.
Peter Lawrey
la source
68
En fait, vous pouvez supprimer les éléments d'un HashSet même si la classe d'élément obtient hashCode et vaut faux; obtenez simplement un itérateur pour l'ensemble et utilisez sa méthode remove, car l'itérateur fonctionne réellement sur les entrées sous-jacentes elles-mêmes et non sur les éléments. (Notez qu'un hashCode / equals non implémenté ne suffit pas pour déclencher une fuite; les valeurs par défaut implémentent l'identité d'objet simple et vous pouvez donc obtenir les éléments et les supprimer normalement.)
Donal Fellows
12
@Donal, ce que j'essaie de dire, je suppose, c'est que je suis en désaccord avec votre définition d'une fuite de mémoire. Je considérerais (pour continuer l'analogie) que votre technique de suppression d'itérateur est un bac d'égouttement sous une fuite; la fuite existe toujours indépendamment du bac d'égouttement.
corsiKa
94
Je suis d'accord, ce n'est pas une "fuite" de mémoire, car vous pouvez simplement supprimer les références au hashset et attendre que le GC se déclenche, et hop! la mémoire remonte.
user541686
12
@ SyntaxT3rr0r, j'ai interprété votre question comme demandant s'il y a quelque chose dans la langue qui mène naturellement à des fuites de mémoire. La réponse est non. Cette question demande s'il est possible de créer une situation pour créer quelque chose comme une fuite de mémoire. Aucun de ces exemples n'est une fuite de mémoire de la manière qu'un programmeur C / C ++ pourrait comprendre.
Peter Lawrey
11
@Peter Lawrey: aussi, qu'en pensez-vous: "Il n'y a rien dans le langage C qui fuit naturellement dans la fuite de mémoire si vous n'oubliez pas de libérer manuellement la mémoire que vous avez allouée" . Comment serait-ce pour la malhonnêteté intellectuelle? Quoi qu'il en soit, je suis fatigué: vous pouvez avoir le dernier mot.
SyntaxT3rr0r
271

Ci-dessous, il y aura un cas non évident où Java fuit, en plus du cas standard d'écouteurs oubliés, de références statiques, de fausses clés / modifiables dans des hashmaps, ou simplement des threads coincé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 caractère sous-jacent [] est également divulgué)- en Java 7, la sous-chaîne copie également la char[], donc la dernière ne s'applique pas ; @Daniel, cependant, pas besoin de votes.

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

  • Runtime.addShutdownHooket 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é, ce qui fuit efficacement le ThreadGroup. JGroup a la fuite dans GossipRouter.

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

  • La création d'un thread hérite de ContextClassLoaderet AccessControlContext, ainsi que de ThreadGroupet any 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 jucExecutor qui dispose d'une ThreadFactoryinterface super simple , mais la plupart des développeurs n'ont aucune idée du danger qui se cache. De nombreuses bibliothèques démarrent également des threads sur demande (beaucoup trop de bibliothèques populaires de l'industrie).

  • ThreadLocalcaches; ce sont des maux dans de nombreux cas. Je suis sûr que tout le monde a vu pas mal de caches simples basés sur ThreadLocal, enfin la mauvaise nouvelle: si le thread continue plus que prévu la vie dans le contexte ClassLoader, c'est une pure belle petite fuite. N'utilisez pas les caches ThreadLocal à moins que cela ne soit vraiment nécessaire.

  • Appel ThreadGroup.destroy()lorsque le ThreadGroup n'a pas de threads lui-même, mais il conserve toujours les ThreadGroups enfants. 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 de la valeur (in) fait directement référence à la clé. C'est difficile à trouver sans vidage de tas. Cela s'applique à toutes les extensions Weak/SoftReferencequi pourraient conserver une référence dure à l'objet gardé.

  • Utilisation java.net.URLavec le protocole HTTP (S) et chargement de la ressource à partir de (!). Celui-ci est spécial, le KeepAliveCachecrée un nouveau thread dans le système ThreadGroup qui fuit le chargeur de classe de contexte du thread actuel. Le thread est créé à la première demande lorsqu'il n'existe aucun thread vivant, vous pouvez donc 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 chargeur de classe de contexte. Il y a peu de cas supplémentaires (comme ImageFetcher, également corrigé ) de créer des threads similaires.

  • Utiliser en InflaterInputStreampassant new java.util.zip.Inflater()le constructeur ( PNGImageDecoderpar exemple) et ne pas appeler end()le gonfleur. Eh bien, si vous passez dans le constructeur avec juste new, aucune chance ... Et oui, appeler close()sur le flux ne ferme pas le gonfleur s'il est passé manuellement en paramètre constructeur. Ce n'est pas une vraie fuite puisqu'elle serait publiée par le finaliseur ... quand elle le jugerait nécessaire. Jusqu'à ce moment, il mange si mal la mémoire native qu'il peut provoquer Linux oom_killer pour tuer le processus en toute impunité. Le principal problème est que la finalisation en Java est très peu fiable et G1 l'a aggravée jusqu'à 7.0.2. Morale de l'histoire: libérez les ressources natives dès que vous le pouvez; le finaliseur 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 (max) et 8 niveaux de mémoire (9 est max) allouant plusieurs centaines de Ko de mémoire native. Heureusement, il Deflatern'est pas largement utilisé et à ma connaissance, JDK ne contient aucun abus. Appelez toujours end()si vous créez manuellement un Deflaterou Inflater. La meilleure partie des deux derniers: vous ne pouvez pas les trouver via les outils de profilage normaux disponibles.

(Je peux ajouter des pertes de temps supplémentaires que j'ai rencontrées sur demande.)

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

bestsss
la source
23
Creating but not starting a Thread...Oui, j'ai été gravement mordu par celui-ci il y a quelques siècles! (Java 1.3)
leonbloy
@leonbloy, avant c'était encore pire car le thread était ajouté directement au groupe de threads, ne pas démarrer signifiait une fuite très dure. Non seulement cela augmente le unstartednombre, mais cela empêche le groupe de threads de détruire (moindre mal mais toujours une fuite)
bestsss
Je vous remercie! "Appeler ThreadGroup.destroy()lorsque le ThreadGroup n'a pas de threads lui-même ..." est un bug incroyablement subtil; Je poursuis cela depuis des heures, induit en erreur parce que l'énumération du thread dans mon interface graphique de contrôle n'a rien montré, mais le groupe de threads et, probablement, au moins un groupe enfant ne s'en iraient pas.
Lawrence Dol
1
@bestsss: Je suis curieux, pourquoi voudriez-vous supprimer un hook d'arrêt, étant donné qu'il s'exécute au niveau de l'arrêt de la JVM?
Lawrence Dol
203

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

Mais il existe des cas vraiment plus simples pour les fuites de mémoire. Le garbage collector 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'allouons en cas de besoin et le laissons être libéré automatiquement. Bien.

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

Bien sûr, toutes sortes d'écouteurs (comme les écouteurs 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, c'est qu'il ne s'agit pas d'un cas d'angle Java ou d'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 dont a besoin 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 les connexions ou les fichiers SQL. Nous devons définir des références appropriées sur 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 temporisateurs). Tous les objets qui permettent à un écouteur d'être notifié doivent fournir à la fois une méthode addListener et removeListener. Et lorsque ces notificateurs ne sont plus utilisés, ils doivent effacer leur liste d'auditeurs.

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 un indicateur que quelque chose manque peut-être ou même des problèmes de conception.

Nicolas Bousquet
la source
24
Je trouve drôle que sur d'autres réponses les gens recherchent ces cas et astuces de bord et semblent manquer complètement le point. Ils pourraient simplement afficher du code qui conserve des références inutiles à des objets qui ne seront plus jamais utilisés et ne supprimera jamais ces références; on peut dire que ces cas ne sont pas de "vraies" fuites de mémoire car il y a toujours des références à ces objets, mais si le programme n'utilise plus jamais ces références et ne les supprime jamais, il est complètement équivalent à (et aussi mauvais que) un " véritable fuite de mémoire ".
ehabkost
@Nicolas Bousquet: "Une fuite de mémoire est vraiment possible" Merci beaucoup. +15 votes positifs. Agréable. J'ai été crié ici pour avoir déclaré ce fait, comme les prémisses d'une question sur la langue Go: stackoverflow.com/questions/4400311 Cette question a toujours des votes négatifs :(
SyntaxT3rr0r
Le GC en Java et .NET est en quelque sorte fondé sur le graphe d'hypothèse d'objets qui contiennent des références à d'autres objets est le même que le graphe d'objets qui "se soucient" d'autres objets. En réalité, il est possible que des arêtes puissent exister dans le graphe de référence qui ne représentent pas des relations de "soin", et il est possible qu'un objet se soucie de l'existence d'un autre objet même si aucun chemin de référence direct ou indirect (même en utilisant WeakReference) n'existe de l'un à l'autre. Si une référence d'objet avait un bit de rechange, il pourrait être utile d'avoir un indicateur "se soucie de la cible" ...
supercat
... et demander au système de fournir des notifications (par des moyens similaires à PhantomReference) s'il s'avère qu'un objet n'a personne qui s'en soucie. WeakReferencese rapproche quelque peu, mais doit être converti en une référence forte avant de pouvoir être utilisé; si un cycle GC se produit alors que la référence forte existe, la cible sera supposée utile.
supercat
C'est à mon avis la bonne réponse. Nous avons écrit une simulation il y a des années. D'une manière ou d'une autre, nous avons accidentellement lié l'état précédent à l'état actuel, créant une fuite de mémoire. En raison d'un délai, nous n'avons jamais résolu la fuite de mémoire mais en avons fait une «fonctionnalité» en la documentant.
2017
163

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 y a plein d'exemples dans les autres réponses.

Mais il y a plusieurs 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 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 une fuite de 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 comme "Quelle réponse aurais-je pu utiliser dans cette situation d'entrevue". Et donc, je vais me concentrer sur les compétences d'entrevue 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 ne devez être en mesure 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 l'entretien consiste à apprendre à écouter activement les questions et à travailler avec l'enquêteur pour en extraire l'intention. Non seulement cela vous permet de répondre à leur question comme ils le souhaitent, mais cela montre également que vous avez des compétences de communication essentielles. Et quand il s'agit de choisir entre de nombreux développeurs tout aussi talentueux, j'engagerai celui qui écoute, pense et comprend avant de répondre à chaque fois.

PlayTank
la source
22
Chaque fois que j'ai posé cette question, je cherche une réponse assez simple - continuez à augmenter la file d'attente, pas de fermeture définitive de la base de données, etc. Cela dépend du travail que vous interviewez, je suppose.
DaveC
Veuillez jeter un œil à ma question, merci stackoverflow.com/questions/31108772/…
Daniel Newtown
130

Ce qui suit est un exemple assez inutile, si vous ne comprenez pas JDBC . Ou au moins comment JDBC attend un développeur à proximité Connection, Statementet des ResultSetcas avant de les jeter ou de perdre des références à eux, au lieu de compter sur la mise en œuvre 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' Connectionobjet n'est pas fermé, et donc la connexion physique restera ouverte, jusqu'à ce que le ramasse-miettes se déplace et voit qu'il est inaccessible. GC invoquera la finalizeméthode, mais il existe des pilotes JDBC qui n'implémentent pas le finalize, du moins pas de la même manière que celle Connection.closeimplémentée. Le comportement résultant est que même si la mémoire est récupérée en raison de la collecte d'objets inaccessibles, les ressources (y compris la mémoire) associées à l' Connectionobjet peuvent tout simplement ne pas être récupérées.

Dans un tel cas où Connectiondes » finalizeva durer plusieurs cycles de collecte des ordures, jusqu'à ce que le serveur de base de données figure finalement que la connexion n'est pas en vie la méthode fait tout up pas propre, on peut effectivement constater que la connexion physique au serveur de base de données (si elle ne) et doit être fermé.

Même si le pilote JDBC devait être implémenté finalize, il est possible que des exceptions soient levées lors de la finalisation. Le comportement résultant est que toute mémoire associée à l'objet désormais "dormant" ne sera pas récupérée, car il finalizeest garanti qu'elle ne sera invoquée qu'une seule fois.

Le scénario ci-dessus de rencontrer des exceptions lors de la finalisation de l'objet est lié à un autre autre scénario qui pourrait éventuellement conduire à une fuite de mémoire - la résurrection des objets. La résurrection d'objet se fait souvent intentionnellement en créant une référence forte à l'objet en cours de finalisation, à partir d'un autre objet. Lorsque la résurrection d'objets est mal utilisée, elle entraînera une fuite de mémoire en combinaison avec d'autres sources de fuites de mémoire.

Il existe de nombreux autres exemples que vous pouvez évoquer - comme

  • Gérer un List instance où vous ne faites qu'ajouter à la liste sans en supprimer (bien que vous devriez vous débarrasser des éléments dont vous n'avez plus besoin), ou
  • Ouvrir Sockets ou Files, mais ne pas les fermer lorsqu'ils ne sont plus nécessaires (similaire à l'exemple ci-dessus impliquant leConnection classe).
  • Ne pas décharger les singletons lors de la fermeture d'une application Java EE. Apparemment, le Classloader qui a chargé la classe singleton conservera une référence à la classe, et donc 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 continuera d'exister en raison du singleton.
Vineet Reynolds
la source
98
Vous atteindrez la limite maximale de connexion ouverte avant d'atteindre généralement les limites de mémoire. Ne me demandez pas pourquoi je sais ...
Hardwareguy
Le pilote Oracle JDBC est connu pour cela.
chotchki
@Hardwareguy J'ai beaucoup atteint les limites de connexion des bases de données SQL jusqu'à ce que je mette Connection.closedans le bloc enfin tous mes appels SQL. Pour plus de plaisir, j'ai appelé des procédures stockées Oracle de longue durée qui nécessitaient des verrous côté Java pour éviter trop d'appels à la base de données.
Michael Shopsin le
@Hardwareguy C'est intéressant mais il n'est pas vraiment nécessaire que les limites de connexion réelles soient atteintes pour tous les environnements. Par exemple, pour une application déployée sur le serveur d'applications weblogic 11g, j'ai vu des fuites de connexion à grande échelle. Mais en raison d'une option de récupération des connexions perdues, les connexions à la base de données sont restées disponibles pendant l'introduction des fuites de mémoire. Je ne suis pas sûr de tous les environnements.
Aseem Bansal du
D'après mon expérience, vous obtenez une fuite de mémoire même si vous fermez la connexion. Vous devez d'abord fermer ResultSet et PreparedStatement. Avait un serveur qui s'est écrasé à plusieurs reprises après des heures ou même des jours de bon fonctionnement, en raison de OutOfMemoryErrors, jusqu'à ce que je commence à le faire.
Bjørn Stenfeldt
119

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

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'aviez implémenté vous-même, auriez-vous pensé à supprimer l'élément de tableau qui n'est plus utilisé ( elementData[--size] = null)? Cette référence pourrait garder un énorme objet en vie ...

meriton
la source
5
Et où est la fuite de mémoire ici?
rds
28
@maniek: Je ne voulais pas laisser entendre que ce code présente une fuite de mémoire. Je l'ai cité pour montrer que du code parfois non évident est nécessaire pour éviter la rétention accidentelle d'objets.
meriton
Qu'est-ce que RangeCheck (index); ?
Koray Tugay
6
Joshua Bloch a donné cet exemple dans Effective Java montrant une implémentation simple de Stacks. Une très bonne réponse.
loue
Mais ce ne serait pas une VRAIE fuite de mémoire, même si on l'oublie. L'élément serait toujours accessible en toute sécurité avec Reflection, il ne serait tout simplement pas évident et directement accessible via l'interface List, mais l'objet et la référence sont toujours là et peuvent être accessibles en toute sécurité.
DGoiko
68

Chaque fois que vous conservez des références à des 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 en Java et ce que vous pouvez faire à ce sujet.

Bill le lézard
la source
14
Je ne pense pas que ce soit une "fuite". C'est un bug, et c'est par la conception du programme et du langage. Une fuite serait un objet qui traîne sans aucune référence à lui.
user541686
29
@Mehrdad: Ce n'est qu'une définition étroite qui ne s'applique pas entièrement à toutes les langues. Je dirais que toute fuite de mémoire est un bug causé par une mauvaise conception du programme.
Bill the Lizard
9
@ Mehrdad: ...then the question of "how do you create a memory leak in X?" becomes meaningless, since it's possible in any language. Je ne vois pas comment vous tirez cette conclusion. Il existe moins de façons de créer une fuite de mémoire en Java, quelle que soit la définition. C'est définitivement une question valable.
Bill the Lizard
7
@ 31eee384: Si votre programme conserve des objets en mémoire qu'il ne pourra jamais utiliser, alors techniquement, il s'agit d'une fuite de mémoire. Le fait que vous ayez de plus gros problèmes ne change pas vraiment cela.
Bill the Lizard
8
@ 31eee384: Si vous savez pertinemment que ce ne sera pas le cas, cela ne peut pas l'être. Le programme, tel qu'il est écrit, n'accédera jamais aux données.
Bill the Lizard
51

Vous pouvez faire 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 directement une instance de cette classe , 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(pendant la compilation, vous obtiendrez 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();
        }
    }

}
stemm
la source
1
La mémoire allouée est invisible pour le garbage collector
stemm
4
La mémoire allouée n'appartient pas non plus à Java.
bestsss
Ce jvm soleil / oracle est-il spécifique? Par exemple, cela fonctionnera-t-il sur IBM?
Berlin Brown
2
La mémoire appartient certainement à Java, au moins dans le sens où i) elle n'est disponible pour personne d'autre, ii) lorsque l'application Java se termine, elle sera renvoyée au système. Il se trouve juste à l'extérieur de la JVM.
Michael Anderson
3
Cela se construira dans eclipse (au moins dans les versions récentes), mais vous devrez modifier les paramètres du compilateur: dans Fenêtre> Préférences> Java> Compilateur> Erreurs / Avertissement> Jeu d'API obsolète et restreint Référence interdite (règles d'accès) à "Avertissement ".
Michael Anderson
43

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

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

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

Jusqu'à présent, toutes les autres réponses ne sont pas vraiment des fuites de mémoire dans ma définition. Ils visent tous à remplir la mémoire de choses 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 -> PAS DE FUITE. la réponse d'Acconrad est assez proche, comme je dois l'admettre, car sa solution consiste à "écraser" simplement le garbage collector 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 du JNI, qui peut avoir une gestion manuelle de la mémoire et donc avoir des fuites de mémoire. Si vous appelez cette bibliothèque, votre processus java perdra de la mémoire. Ou, vous pouvez avoir des bogues dans la JVM, de sorte que la JVM perd de la mémoire. Il y a probablement des bogues dans la JVM, il peut même y en avoir quelques-uns connus, car le ramasse-miettes n'est pas si simple, mais c'est quand même un bogue. De par sa conception, cela 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 pourrait bien ne plus être un bug dans la prochaine version Java.

yankee
la source
12
C'est une définition extrêmement limitée (et pas très utile) des fuites de mémoire. La seule définition qui a du sens à des fins pratiques est "une fuite de mémoire est une condition dans laquelle le programme continue de conserver la mémoire allouée après que les données qu'il contient ne sont plus nécessaires."
Mason Wheeler
1
La réponse de l'acconrad mentionnée a été supprimée?
Tomáš Zato - Rétablir Monica le
1
@ TomášZato: Non, ce n'est pas le cas. J'ai tourné la référence ci-dessus pour créer un lien maintenant, afin que vous puissiez facilement la trouver.
yankee
Qu'est-ce que la résurrection d'objets? Combien de fois un destructeur est-il appelé? Comment ces questions réfutent-elles cette réponse?
autiste
1
Bien sûr, vous pouvez créer une fuite de mémoire à l'intérieur de Java, malgré GC, et toujours remplir la définition ci-dessus. Il suffit d'avoir une structure de données à ajouter uniquement, protégée contre les accès externes afin qu'aucun autre code ne la supprime - le programme ne peut pas libérer la mémoire car il n'a pas le code.
toolforger
39

En voici une 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 d'origine, beaucoup plus longue, l'original reste en mémoire. Ainsi, tant que vous avez un StringLeaker en jeu, vous avez également toute la chaîne d'origine en mémoire, même si vous pensez peut-être que vous vous accrochez à une chaîne à un seul caractère.

La façon d'éviter de stocker une référence indésirable à la chaîne d'origine est de faire quelque chose comme ceci:

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

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

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

Cela gardera à la fois la chaîne longue d'origine et la sous-chaîne dérivée en mémoire même après que l'instance de StringLeaker a été supprimée.

Jon Chambers
la source
4
Je n'appellerais pas cela une fuite de mémoire, en soi . Lorsque muchSmallerStringest libéré (car l' StringLeakerobjet est détruit), la longue chaîne sera également libérée. Ce que j'appelle une fuite de mémoire est une mémoire qui ne peut jamais être libérée dans cette instance de JVM. Cependant, vous vous avez montré comment libérer la mémoire: this.muchSmallerString=new String(this.muchSmallerString). Avec une vraie fuite de mémoire, vous ne pouvez rien faire.
rds
2
@rds, c'est un bon point. Le non- interncas peut être plus une «surprise de mémoire» qu'une «fuite de mémoire». .intern()Cependant, la sous-chaîne crée certainement une situation où la référence à la chaîne plus longue est préservée et ne peut pas être libérée.
Jon Chambers du
15
La méthode substring () crée une nouvelle chaîne dans java7 (c'est un nouveau comportement)
anstarovoyt
Vous n'avez même pas besoin de faire vous-même la sous-chaîne (): utilisez une correspondance regex pour faire correspondre une petite partie d'une énorme entrée et portez la chaîne "extraite" pendant longtemps. L'énorme entrée reste vivante jusqu'à Java 6.
Bananeweizen
37

Un exemple courant de cela dans le code GUI est lors de la création d'un widget / composant et l'ajout d'un écouteur à un objet de portée statique / d'application, puis ne supprimant pas l'écouteur lorsque le widget est détruit. Non seulement vous obtenez une fuite de mémoire, mais aussi une baisse de performance comme lorsque tout ce que vous écoutez se déclenche, tous vos anciens auditeurs sont également appelés.

pauli
la source
1
La plateforme Android donne l'exemple d'une fuite mémoire créée par la mise en cache d'un Bitmap dans le champ statique d'une Vue .
rds
36

Prenez n'importe quelle application Web exécutée dans n'importe quel conteneur de servlet (Tomcat, Jetty, Glassfish, peu importe ...). Redéployez l'application 10 ou 20 fois de suite (il peut suffire de toucher simplement le WAR dans le répertoire de déploiement automatique du serveur.

À moins que quiconque n'ait réellement testé cela, il y a de fortes chances que vous obteniez une OutOfMemoryError après quelques redéploiements, car l'application n'a pas pris soin de nettoyer après 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 la durée de vie de votre application. Vous devez vous assurer que toutes les références que le conteneur peut avoir aux objets ou aux classes de votre application peuvent être récupérées.

S'il n'y a qu'une seule référence survivant au déploiement de votre application Web, le chargeur de classe correspondant et par conséquent toutes les classes de votre application Web ne peuvent pas être récupérés.

Les threads démarrés par votre application, les variables ThreadLocal, les ajouts de journalisation sont certains des suspects habituels à l'origine de fuites du chargeur de classe.

Harald Wellmann
la source
1
Ce n'est pas à cause d'une fuite de mémoire, mais parce que le chargeur de classe ne décharge pas l'ensemble de classes précédent. Il n'est donc pas recommandé de redéployer un serveur d'applications sans redémarrer le serveur (pas la machine physique, mais le serveur d'applications). J'ai vu le même problème avec WebSphere.
Sven
35

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

Rogach
la source
22
Cela dépend de la définition de «fuite de mémoire». Si "la mémoire est conservée mais n'est plus nécessaire", alors c'est facile à faire en Java. Si c'est "de la mémoire allouée mais pas du tout accessible par le code", cela devient un peu plus difficile.
Joachim Sauer
@Joachim Sauer - Je voulais dire le deuxième type. Le premier est assez facile à faire :)
Rogach
6
"Avec du java pur, c'est presque impossible." Eh bien, mon expérience en est une autre, surtout en ce qui concerne la mise en œuvre de caches par des personnes qui ne connaissent pas les pièges ici.
Fabian Barney
4
@Rogach: il y a fondamentalement +400 votes positifs sur les différentes réponses des personnes avec +10 000 répétitions, montrant que dans les deux cas, Joachim Sauer a déclaré que c'était très possible. Votre «presque impossible» n'a donc aucun sens.
SyntaxT3rr0r
32

J'ai eu une belle "fuite de mémoire" en ce qui concerne l'analyse PermGen et XML une fois. L'analyseur XML que nous avons utilisé (je ne me souviens plus lequel était) a fait 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 ou du texte, mais sous forme de variables, nous avions donc un document comme:

<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), qui étaient uniques et arrivaient à 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 jamais GCed (car il est dans PermGen). Nous avions fixé permgen à 512 Mo, il a donc fallu environ deux jours pour que l'exception de mémoire insuffisante (OOME) arrive ...

Ron
la source
4
Juste pour taper votre code d'exemple: je pense que les nombres (ou les chaînes commençant par des nombres) ne sont pas autorisés comme noms d'éléments en XML.
Paŭlo Ebermann
Notez que cela n'est plus vrai pour JDK 7+, où l'internement de chaînes se produit sur le tas. Voir cet article pour un résumé détaillé: java-performance.info/string-intern-in-java-6-7-8
jmiserez
Donc, je pense que l'utilisation de StringBuffer à la place de String résoudrait ce problème? ça ne va pas?
anubhs
24

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

  • Cela est dû à un bug ou à une mauvaise conception.
  • C'est une perte de mémoire.
  • Cela empire avec le temps.
  • Le garbage collector ne peut pas le nettoyer.

Exemple typique:

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

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 bientôt, la base de données entière 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 .
  • ajouter des arbres de référence plus complexes .
  • ou des fuites causées par des bibliothèques tierces .

Ce qui arrive souvent:

Si cet objet Info a des références à d'autres objets, qui ont à nouveau des références à d'autres objets. D'une certaine manière, vous pouvez également considérer qu'il s'agit d'une sorte de fuite de mémoire (causée par une mauvaise conception).

bvdb
la source
22

J'ai trouvé intéressant que personne n'utilise les exemples de classe interne. Si vous avez une classe interne; il conserve de manière inhérente une référence à la classe conteneur. Bien sûr, il ne s'agit pas techniquement d'une fuite de mémoire car Java finira par la nettoyer; mais cela peut faire traîner les classes plus longtemps que prévu.

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

Maintenant, si vous appelez Example1 et obtenez un exemple2 en supprimant l'exemple1, vous aurez toujours un lien vers un objet Example1.

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 également entendu une rumeur selon laquelle si vous avez une variable qui existe depuis plus d'un certain temps; Java suppose qu'il existera toujours et n'essaiera jamais de le nettoyer s'il ne peut plus être atteint dans le code. Mais cela n'est absolument pas vérifié.

Suroot
la source
2
les classes internes sont rarement un problème. C'est un cas simple et très facile à détecter. La rumeur n'est qu'une rumeur aussi.
bestsss
2
La "rumeur" ressemble à quelqu'un à moitié lu sur le fonctionnement du GC générationnel. Les objets à longue durée de vie mais désormais inaccessibles peuvent en effet rester dans les parages et prendre de l'espace pendant un certain temps, car la JVM les a promus hors des générations plus jeunes afin de pouvoir arrêter de les vérifier à chaque passage. Ils échapperont aux passes délicates de "nettoyer mes 5000 cordes temporaires", par conception. Mais ils ne sont pas immortels. Ils sont toujours éligibles pour la collecte, et si la machine virtuelle est limitée pour la RAM, elle exécutera éventuellement un balayage GC complet et reprendra cette mémoire.
cHao
22

J'ai récemment rencontré une situation de fuite de mémoire causée en quelque sorte par log4j.

Log4j possède ce mécanisme appelé Nested Diagnostic Context (NDC) qui est un instrument permettant de distinguer les sorties de journaux entrelacées de différentes sources. La granularité à laquelle le NDC fonctionne est les threads, il distingue donc séparément les sorties de journal des différents threads.

Afin de stocker des balises spécifiques au thread, la classe NDC de log4j utilise une table de hachage qui est saisie par l'objet Thread lui-même (par opposition à l'ID du thread), et donc jusqu'à ce que la balise NDC reste en mémoire 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 journal avec un identifiant de demande pour distinguer les journaux d'une seule demande séparément. Le conteneur qui associe la balise NDC à un thread, la supprime également lors du renvoi de la réponse d'une demande. Le problème s'est produit lorsque, au cours du traitement d'une demande, un thread enfant a été généré, quelque chose comme le 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();
    }
}    

Un contexte NDC a donc été associé au thread en ligne généré. L'objet thread qui était la clé de ce contexte NDC, est le thread en ligne qui a l'objet énormeList suspendu. Par conséquent, même après que le thread a fini de faire ce qu'il faisait, la référence à l'énormeList a été maintenue vivante par le contexte NDC Hastable, provoquant ainsi une fuite de mémoire.

Puneet
la source
Ça craint. Vous devez vérifier cette bibliothèque de journalisation qui alloue la mémoire ZÉRO lors de la connexion à un fichier: mentalog.soliveirajr.com
TraderJoeChicago
+1 Savez-vous par vous-même s'il existe un problème similaire avec le MDC dans slf4j / logback (produits successeurs du même auteur)? Je suis sur le point de faire une plongée profonde sur la source mais je voulais d'abord vérifier. Quoi qu'il en soit, merci d'avoir posté cela.
sparc_spread
20

L'intervieweur était probablement à la recherche d'une référence circulaire comme le code ci-dessous (qui ne laisse d'ailleurs passer que de la mémoire dans les très anciennes machines virtuelles Java qui utilisaient le comptage des références, ce qui n'est plus le cas). Mais c'est une question assez vague, c'est donc une excellente occasion de montrer votre compréhension de la gestion de la mémoire JVM.

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 des références, le code ci-dessus entraînerait une fuite de mémoire. Mais la plupart des machines virtuelles Java modernes n'utilisent plus le comptage des références, la plupart utilisent un ramasse-miettes qui collectera en fait cette mémoire.

Ensuite, vous pourriez expliquer la création d'un objet qui a 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. */
    }
}

Ensuite, vous pouvez expliquer qu'il s'agit techniquement d'une fuite de mémoire, mais en réalité, la fuite est causée par du code natif dans la JVM allouant des ressources natives sous-jacentes, qui n'ont pas été libérées par votre code Java.

À la fin de la journée, avec une JVM moderne, vous devez écrire du code Java qui alloue une ressource native en dehors de la portée normale de la reconnaissance de la JVM.

deltamind106
la source
19

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

  1. Déclarez la méthode native.
  2. Dans la méthode native, appelez malloc. N'appelle pasfree .
  3. Appelez la méthode native.

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

Paul Morie
la source
1
Basé sur une histoire vraie.
Reg
18

Créez une carte statique et continuez d'y ajouter des références matérielles. 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); }
}
2 tours
la source
87
Comment est-ce une fuite? Il fait exactement ce que vous lui demandez de faire. Si c'est une fuite, créer et stocker des objets n'importe où est une fuite.
Falmarri
3
Je suis d'accord avec @Falmarri. Je ne vois pas de fuite là-bas, vous créez juste des objets. Vous pouvez certainement «récupérer» la mémoire que vous venez d'allouer avec une autre méthode appelée «removeFromCache». Une fuite se produit lorsque vous ne pouvez pas récupérer la mémoire.
Kyle
3
Mon point est que quelqu'un qui continue de créer des objets, peut-être en les mettant dans un cache, pourrait se retrouver avec une erreur OOM s'il ne fait pas attention.
duffymo
8
@duffymo: Mais ce n'est pas vraiment ce que la question demandait. Cela n'a rien à voir avec la simple utilisation de toute votre mémoire.
Falmarri
3
Absolument invalide. Vous collectez simplement un tas d'objets dans une collection de cartes. Leurs références seront conservées car la Carte les contient.
gyorgyabraham
16

Vous pouvez créer une fuite de mémoire mobile 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 fuit le tas entier entre quelques secondes et quelques minutes selon 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());
        }
    }
}
sethobrien
la source
15

Je ne pense pas que quelqu'un l'ait encore dit: vous pouvez ressusciter un objet en remplaçant la méthode finalize () de telle sorte que finalize () stocke une référence de ceci quelque part. Le ramasse-miettes ne sera appelé qu'une seule fois sur l'objet, après quoi l'objet ne sera jamais détruit.

Ben
la source
10
C'est faux. finalize()ne sera pas appelé mais l'objet sera collecté une fois qu'il n'y aura plus de références. Le garbage collector n'est pas "appelé" non plus.
bestsss
1
Cette réponse est trompeuse, la finalize()méthode ne peut être appelée qu'une seule fois par la machine virtuelle Java, mais cela ne signifie pas qu'elle ne peut pas être récupérée à nouveau si l'objet est ressuscité, puis déréférencé à nouveau. S'il y a du code de fermeture de ressource dans la finalize()méthode, ce code ne sera pas exécuté à nouveau, cela peut provoquer une fuite de mémoire.
Tom Cammann
15

J'ai récemment rencontré une sorte de fuite de ressources plus subtile. Nous ouvrons les ressources via getResourceAsStream du chargeur de classe et il s'est produit que les descripteurs de flux d'entrée n'étaient pas fermés.

Uhm, vous pourriez dire, quel idiot.

Eh bien, ce qui rend cela intéressant, c'est: de cette façon, vous pouvez divulguer la mémoire de tas du processus sous-jacent, plutôt que du tas de JVM.

Tout ce dont vous avez besoin est un fichier jar avec un fichier à l'intérieur qui sera référencé à partir du code Java. Plus le fichier jar est grand, plus la mémoire est allouée rapidement.

Vous pouvez facilement créer un tel pot 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();
    }
}

Il suffit de coller dans un fichier nommé BigJarCreator.java, de le compiler et de l'exécuter à 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 rien, mais crée des objets InputStream non référencés. Ces objets seront immédiatement récupérés et ne contribuent 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 importe ici!

En cas de doute, essayez de compiler et de démarrer la classe ci-dessus, mais assurez-vous de choisir une taille de segment de mémoire décente (2 Mo):

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

Vous ne rencontrerez pas d'erreur OOM ici, car aucune référence n'est conservée, l'application continuera à fonctionner quelle que soit la taille que vous avez choisie ITERATIONS dans l'exemple ci-dessus. La consommation de mémoire de votre processus (visible en haut (RES / RSS) ou explorateur de processus) augmente à moins que l'application accède à la commande d'attente. Dans la configuration ci-dessus, il allouera environ 150 Mo de mémoire.

Si vous souhaitez que l'application fonctionne en toute sécurité, fermez le flux d'entrée là où il a été 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.

Geai
la source
14

Comme beaucoup de gens l'ont suggéré, les fuites de ressources sont assez faciles à provoquer - comme les exemples JDBC. Les fuites de mémoire réelles sont un peu plus difficiles - surtout si vous ne comptez pas sur des bits cassés de la JVM pour le faire pour vous ...

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, il sera récupéré et si quelque chose peut y accéder, ce n'est pas une fuite ...

Cependant, une façon qui fonctionnait auparavant - et je ne sais pas si c'est toujours le cas - est d'avoir une chaîne circulaire à trois profondeurs. Comme dans l'objet A a une référence à l'objet B, l'objet B a une référence à l'objet C et l'objet C a une référence à l'objet A. Le GC était assez intelligent pour savoir qu'une chaîne à deux profonds - comme dans A <--> B - peut être collecté en toute sécurité si A et B ne sont accessibles par rien d'autre, mais ne peuvent pas gérer la chaîne à trois voies ...

Graham
la source
7
Ce n'est plus le cas depuis un certain temps maintenant. Les GC modernes savent comment gérer les références circulaires.
assylias
13

Une autre façon de créer des fuites de mémoire potentiellement énormes consiste à conserver les références à Map.Entry<K,V>a TreeMap.

Il est difficile d'évaluer pourquoi cela ne s'applique qu'à TreeMaps, mais en regardant l'implémentation, la raison pourrait être que: a TreeMap.Entrystocke les références à ses frères et sœurs, donc si a TreeMapest prêt à être collecté, mais une autre classe détient une référence à l'un des son Map.Entry, puis la carte entière sera conservée en mémoire.


Scénario réel:

Imaginez avoir une requête db qui renvoie une TreeMapstructure de Big Data. Les gens utilisent généralement TreeMaps car l'ordre d'insertion des éléments est conservé.

public static Map<String, Integer> pseudoQueryDatabase();

Si la requête était appelée plusieurs fois et, pour chaque requête (donc, pour chaque Mapretour), vous enregistrez Entryquelque part, la mémoire ne cessait de croître.

Considérez la classe wrapper suivante:

class EntryHolder {
    Map.Entry<String, Integer> entry;

    EntryHolder(Map.Entry<String, Integer> entry) {
        this.entry = entry;
    }
}

Application:

public class LeakTest {

    private final List<EntryHolder> holdersCache = new ArrayList<>();
    private static final int MAP_SIZE = 100_000;

    public void run() {
        // create 500 entries each holding a reference to an Entry of a TreeMap
        IntStream.range(0, 500).forEach(value -> {
            // create map
            final Map<String, Integer> map = pseudoQueryDatabase();

            final int index = new Random().nextInt(MAP_SIZE);

            // get random entry from map
            for (Map.Entry<String, Integer> entry : map.entrySet()) {
                if (entry.getValue().equals(index)) {
                    holdersCache.add(new EntryHolder(entry));
                    break;
                }
            }
            // to observe behavior in visualvm
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

    }

    public static Map<String, Integer> pseudoQueryDatabase() {
        final Map<String, Integer> map = new TreeMap<>();
        IntStream.range(0, MAP_SIZE).forEach(i -> map.put(String.valueOf(i), i));
        return map;
    }

    public static void main(String[] args) throws Exception {
        new LeakTest().run();
    }
}

Après chaque pseudoQueryDatabase()appel, les mapinstances doivent être prêtes pour la collecte, mais cela ne se produira pas, car au moins une Entryest stockée ailleurs.

Selon vos jvmparamètres, l'application peut se bloquer à un stade précoce en raison d'un OutOfMemoryError.

Vous pouvez voir sur ce visualvmgraphique comment la mémoire continue de croître.

Vidage de la mémoire - TreeMap

La même chose ne se produit pas avec une structure de données hachée ( HashMap).

Il s'agit du graphique lors de l'utilisation d'un HashMap.

Vidage de la mémoire - HashMap

La solution? Enregistrez simplement la clé / valeur (comme vous le faites probablement déjà) plutôt que d'enregistrer Map.Entry.


J'ai écrit un point de référence plus complet ici .

Marko Pacak
la source
11

Les threads ne sont pas collectés avant la fin. Ils servent de racines à la collecte des ordures. Ils sont l'un des rares objets qui ne seront pas récupérés simplement en les oubliant ou en effaçant les références à eux.

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 terminer. Si la variable n'est pas déclarée volatile, la modification de la variable peut ne pas être vue par le thread, il ne saura donc pas se terminer. Ou imaginez si certains threads veulent mettre à jour un objet partagé, mais un blocage tout en essayant de le verrouiller.

Si vous n'avez qu'une poignée de threads, ces bogues seront probablement évidents car votre programme cessera de fonctionner correctement. Si vous disposez d'un pool de threads qui crée plus de threads selon les besoins, les threads obsolètes / bloqués peuvent 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, ce qui empêchera également la collecte de tout ce qu'ils référencent directement.

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 est passé àleakMe ne mourra jamais.

(*édité*)

Boann
la source
1
@Spidey Rien n'est "bloqué". La méthode appelante retourne rapidement et l'objet transmis ne sera jamais récupéré. C'est précisément une fuite.
Boann
1
Vous aurez un thread "en cours d'exécution" (ou en veille, peu importe) pour la durée de vie de votre programme. Cela ne compte pas comme une fuite pour moi. Ainsi, une piscine ne compte pas comme une fuite, même si vous ne l'utilisez pas entièrement.
Spidey
1
@Spidey "Vous aurez une [chose] pour la durée de vie de votre programme. Cela ne compte pas comme une fuite pour moi." Vous entendez-vous?
Boann
3
@Spidey Si vous considérez la mémoire que le processus connaît comme non divulguée, toutes les réponses ici sont fausses, car le processus suit toujours les pages de son espace d'adressage virtuel qui sont mappées. Lorsque le processus se termine, le système d'exploitation nettoie toutes les fuites en remettant les pages sur la pile de pages libres. Pour pousser cela à l'extrême suivant, on pourrait battre à mort toute fuite argumentée en soulignant qu'aucun des bits physiques des puces RAM ou de l'espace d'échange sur le disque n'a été physiquement mal placé ou détruit, vous pouvez donc éteindre l'ordinateur et à nouveau pour nettoyer toute fuite.
Boann
1
La définition pratique d'une fuite est que c'est de la mémoire qui a été perdue de telle sorte que nous ne savons pas et ne pouvons donc pas effectuer la procédure nécessaire pour la récupérer uniquement; il faudrait démolir et reconstruire tout l'espace mémoire. Un thread escroc comme celui-ci pourrait survenir naturellement à travers une mise en œuvre de blocage ou threadpool douteux. Les objets référencés par de tels threads, même indirectement, sont désormais empêchés d'être collectés, nous avons donc une mémoire qui ne sera pas naturellement récupérée ou réutilisable pendant la durée de vie du programme. J'appellerais cela un problème; spécifiquement c'est une fuite de mémoire.
Boann
10

Je pense qu'un exemple valide pourrait être d'utiliser des variables ThreadLocal dans un environnement où les threads sont regroupés.

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

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

mschonaker
la source
10

L'intervieweur pourrait avoir recherché 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;
        }
    }

Il s'agit d'un problème classique avec les compteurs de déchets de comptage de référence. Vous expliqueriez alors poliment que les machines virtuelles Java utilisent un algorithme beaucoup plus sophistiqué qui n'a pas cette limitation.

-Wes Tarle

Wesley Tarle
la source
12
Il s'agit d'un problème classique avec les compteurs de déchets de comptage de référence. Il y a encore 15 ans, Java n'utilisait pas le comptage de références. Réf. le comptage est également plus lent que GC.
bestsss
4
Pas une fuite de mémoire. Juste une boucle infinie.
Esben Skov Pedersen
2
@Esben À chaque itération, la précédente firstn'est pas utile et doit être récupérée. Dans le comptage des références des récupérateurs de place, l'objet ne serait pas libéré car il y a une référence active sur lui (par lui-même). La boucle infinie est là pour démontrer la fuite: lorsque vous exécutez le programme, la mémoire augmentera indéfiniment.
rds
@rds @ Wesley Tarle suppose que la boucle n'était pas infinie. Y aurait-il encore une fuite de mémoire?
nz_21