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?
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?
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:
ClassLoader
.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.ClassLoader
source à partir de laquelle elle a été chargée.En raison de la façon dont ThreadLocal
est implémenté dans le JDK d'Oracle, cela crée une fuite de mémoire:
Thread
a un champ privé threadLocals
, qui stocke en fait les valeurs locales du thread.ThreadLocal
objet, donc après que cet ThreadLocal
objet a été récupéré, son entrée est supprimée de la carte.ThreadLocal
objet 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:
Thread
objet → threadLocals
carte → instance de l'exemple de classe → exemple de classe → ThreadLocal
champ statique → ThreadLocal
objet.
(Le ClassLoader
ne 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 ClassLoader
s 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 ThreadLocal
s 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 .
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' noclassgc
option sur IBM JDK qui empêche le garbage collection inutilisé
Voir Paramètres IBM jdk .
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.
intern
contenu 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
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.
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, 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.addShutdownHook
et 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 Thread
entre dans la même catégorie que ci-dessus.
La création d'un thread hérite de ContextClassLoader
et AccessControlContext
, ainsi que de ThreadGroup
et 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 ThreadFactory
interface 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).
ThreadLocal
caches; 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/SoftReference
qui pourraient conserver une référence dure à l'objet gardé.
Utilisation java.net.URL
avec le protocole HTTP (S) et chargement de la ressource à partir de (!). Celui-ci est spécial, le KeepAliveCache
crée un nouveau thread dans le système ThreadGroup qui 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 InflaterInputStream
passant new java.util.zip.Inflater()
le constructeur ( PNGImageDecoder
par 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 Deflater
n'est pas largement utilisé et à ma connaissance, JDK ne contient aucun abus. Appelez toujours end()
si vous créez manuellement un Deflater
ou 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!
Creating but not starting a Thread...
Oui, j'ai été gravement mordu par celui-ci il y a quelques siècles! (Java 1.3)
unstarted
nombre, mais cela empêche le groupe de threads de détruire (moindre mal mais toujours une fuite)
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.
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.
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" ...
PhantomReference
) s'il s'avère qu'un objet n'a personne qui s'en soucie. WeakReference
se 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.
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?
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.
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
, Statement
et des ResultSet
cas 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' Connection
objet 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 finalize
mé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.close
implé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' Connection
objet peuvent tout simplement ne pas être récupérées.
Dans un tel cas où Connection
des » finalize
va 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 finalize
est 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
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), ouSocket
s ou File
s, mais ne pas les fermer lorsqu'ils ne sont plus nécessaires (similaire à l'exemple ci-dessus impliquant leConnection
classe).Connection.close
dans 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.
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 ...
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.
...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.
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();
}
}
}
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.
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.
muchSmallerString
est libéré (car l' StringLeaker
objet 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.
intern
cas 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.
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.
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.
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.
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 ...
Qu'est-ce qu'une fuite de mémoire:
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:
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).
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é.
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.
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.
Tout le monde oublie toujours la route du code natif. Voici une formule simple pour une fuite:
malloc
. N'appelle pasfree
.N'oubliez pas que les allocations de mémoire en code natif proviennent du tas JVM.
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); }
}
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());
}
}
}
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.
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.
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.
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.
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 ...
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'à TreeMap
s, mais en regardant l'implémentation, la raison pourrait être que: a TreeMap.Entry
stocke les références à ses frères et sœurs, donc si a TreeMap
est 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 TreeMap
structure de Big Data. Les gens utilisent généralement TreeMap
s 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 Map
retour), vous enregistrez Entry
quelque 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 map
instances doivent être prêtes pour la collecte, mais cela ne se produira pas, car au moins une Entry
est stockée ailleurs.
Selon vos jvm
paramètres, l'application peut se bloquer à un stade précoce en raison d'un OutOfMemoryError
.
Vous pouvez voir sur ce visualvm
graphique comment la mémoire continue de croître.
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
.
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 .
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é*)
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.
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
first
n'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.