Tout ce que vous devez savoir sur les fuites de mémoire dans Android.

Ali Asadi
30 juin 2019 – 16 min de lecture

L’un des principaux avantages de Java, ou pour être plus précis, de la JVM (Java Virtual Machine), est le garbage collector (GC). Nous pouvons créer de nouveaux objets sans nous soucier de les libérer de la mémoire. Le ramasseur d’ordures se chargera d’allouer et de libérer la mémoire pour nous.

Pas exactement ! Nous pouvons empêcher le garbage collector de libérer la mémoire pour nous si nous ne comprenons pas complètement le fonctionnement du GC.

Écrire un code sans une bonne compréhension du fonctionnement du GC pourrait faire des fuites de mémoire dans l’app. Ces fuites peuvent affecter notre app en gaspillant la mémoire non libérée et provoque éventuellement des exceptions hors mémoire et des décalages.

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

L’échec de la libération des objets inutilisés de la mémoire

L’échec de la libération des objets inutilisés de la mémoire signifie qu’il y a des objets inutilisés dans l’application que le GC ne peut pas effacer de la mémoire.

Lorsque le GC ne peut pas effacer les objets inutilisés de la mémoire, nous avons des problèmes. L’unité de mémoire qui contient les objets inutilisés sera occupée jusqu’à la fin de l’application ou (jusqu’à la fin de la méthode).

Jusqu’à la fin de la méthode ? Oui, c’est ça. Nous avons deux types de fuites, les fuites qui occupent l’unité de mémoire jusqu’à la fin de l’application et les fuites qui occupent l’unité de mémoire jusqu’à la fin de la méthode. La première est claire. La seconde nécessite plus de clarification. Prenons un exemple pour l’expliquer ! Supposons que nous ayons une méthode X. La méthode X effectue une tâche longue en arrière-plan, et il lui faudra une minute pour la terminer. De plus, la méthode X conserve des objets inutilisés pendant cette tâche. Dans ce cas, l’unité de mémoire sera occupée, et les objets inutilisés ne pourront pas être effacés pendant une minute jusqu’à la fin de la tâche. Après la fin de la méthode, le GC peut effacer les objets inutilisés et récupérer la mémoire.

C’est ce que je veux que vous sachiez pour l’instant nous y reviendrons plus tard avec du code et de la visualisation. CE SERA AMUSANT. 👹😜

Attendez une minute!!🥴

Avant de sauter au contexte, commençons par les bases.

La RAM, ou mémoire à accès aléatoire, est la mémoire des appareils androïdes ou des ordinateurs utilisée pour stocker les applications en cours d’exécution et leurs données.

Je vais expliquer deux personnages principaux de la RAM, le premier est le Heap, et le second est la Pile. Passons à la partie amusante 🤩🍻.

Heap &Pile

Je ne vais pas être trop long. Allons droit au but, une courte description, le Stack est utilisé pour l’allocation de mémoire statique tandis que le Heap est utilisé pour l’allocation de mémoire dynamique. Gardez juste à l’esprit que le Heap et le Stack sont tous deux stockés dans la RAM.

Plus d’informations sur la mémoire Heap

La mémoire Heap de Java est utilisée par la machine virtuelle pour allouer des objets. Chaque fois que vous créez un objet, il est toujours créé dans le tas. Les machines virtuelles, comme JVM ou DVM, effectuent une collecte régulière des déchets (GC), rendant la mémoire du tas de tous les objets qui ne sont plus référencés disponible pour de futures allocations.

Pour offrir une expérience utilisateur fluide, Android fixe une limite stricte à la taille du tas pour chaque application en cours d’exécution. La limite de la taille du tas varie selon les appareils et est basée sur la quantité de RAM dont dispose un appareil. Si votre application atteint cette limite de tas et tente d’allouer plus de mémoire, elle recevra un OutOfMemoryError et se terminera.

Vous êtes-vous déjà demandé quelle est la taille du tas pour votre application ?

Découvrons cela ensemble. Dans android, nous avons la VM Dalvik (DVM). La DVM est une machine virtuelle Java unique, optimisée pour les appareils mobiles. Elle optimise la machine virtuelle pour la mémoire, l’autonomie de la batterie et les performances, et elle est responsable de la distribution de la quantité de mémoire pour chaque application.

Parlons de deux lignes dans la DVM:

  1. dalvik.vm.heapgrowthlimit : Cette ligne est basée sur la façon dont Dalvik va commencer dans la taille du tas de votre application. Il s’agit de la taille de tas par défaut pour chaque application. Le maximum que votre application peut atteindre!
  2. dalvik.vm.heapsize : Cette ligne représente la taille maximale du tas pour un tas plus grand. Vous pouvez y parvenir en demandant à android un plus grand tas dans votre manifeste d’application (android:largeHeap= »true »).

N’utilisez pas un plus grand tas dans votre application. Faites-le UNIQUEMENT si vous connaissez exactement l’effet secondaire de cette étape. Ici, je vais vous donner suffisamment d’informations pour continuer à faire des recherches sur le sujet.

Voici un tableau montrant quelle taille de tas vous avez obtenu en fonction de la RAM de votre appareil:

+==========================+=========+=========+===================+
| DVM | 1GB RAM | 2GB RAM | 3GB RAM OR HIGHER |
+==========================+=========+=========+===================+
| DEFAULT(heapgrowthlimit) | 64m | 128m | 256m |
+--------------------------+---------+---------+-------------------+
| LARGE(heapsize) | 128m | 256m | 512m |
+--------------------------+---------+---------+-------------------+

Souvenez-vous que plus vous avez de ram, plus la taille de tas sera élevée. Gardez à l’esprit que tous les appareils avec une ram plus élevée ne vont pas au-delà de 512m faites vos recherches sur votre appareil si votre appareil a plus de 3GB pour voir si votre taille de tas est plus grande que 512m.

Comment pouvez-vous vérifier la taille de tas d’applications pour votre appareil?

En utilisant le ActivityManager. Vous pouvez vérifier la taille maximale du tas au moment de l’exécution en utilisant les méthodes getMemoryClass() ou getLargeMemoryClass() (lorsqu’un grand tas est activé).

  • getMemoryClass() : Renvoie la taille maximale du tas par défaut.
  • getLargeMemoryClass() : Renvoie la taille maximale du tas disponible après avoir activé le drapeau de grand tas dans le manifeste.
ActivityManager am = getSystemService(ACTIVITY_SERVICE);
Log.d("XXX", "dalvik.vm.heapgrowthlimit: " + am.getMemoryClass());
Log.d("XXX", "dalvik.vm.heapsize: " + am.getLargeMemoryClass());

Comment cela fonctionne dans le monde réel ?

Nous utiliserons cette application simple pour comprendre quand nous utilisons le tas et quand nous utilisons la pile.

application

L’image ci-dessous montre une représentation du tas et de la pile de l’application et où chaque objet pointe et est stocké lorsque nous exécutons l’application.

Nous allons reprendre l’exécution de l’app, arrêter chaque ligne, expliquer quand l’application alloue les objets, et les stocke dans le tas ou la pile. Nous verrons également quand l’app libère les objets de la pile et du tas.

  • Ligne 1 – La JVM crée un bloc de mémoire de pile pour la méthode main.

  • Ligne 2 – Dans cette ligne, nous créons une variable locale primitive. La variable sera créée et stockée dans la mémoire de pile de la méthode principale.

  • Ligne 3 -Ici je demande votre attention ! !! Dans cette ligne, nous créons un nouvel objet. L’objet est créé dans la pile de la méthode main et stocké dans le heap. La pile stocke la référence, l’adresse mémoire objet-heap (pointeur), tandis que le heap stocke l’objet original.

  • Ligne 4 – La même chose que la ligne 3.

  • Ligne 5 – La JVM crée un bloc de mémoire de pile pour la méthode foo.

  • Ligne 6 -Créer un nouvel objet. L’objet est créé dans la mémoire de la pile de la méthode foo, et nous stockons dans la pile l’adresse en mémoire de tas de l’objet que nous avons créé à la ligne 3. La valeur (adresse en mémoire de tas de l’objet de la ligne 3) que nous avons transmise à la ligne 5. Gardez à l’esprit que Java passe toujours les références par valeur.

  • Ligne 7 – Nous créons un nouvel objet. L’objet créé dans la pile et pointé vers le pool de chaînes dans le tas.

  • Ligne 8 – Dans la dernière ligne de la méthode foo, la méthode s’est terminée. Et les objets seront libérés du bloc de pile de la méthode foo.

  • Ligne 9- La même que la ligne 8, Dans la ligne finale de la méthode main, la méthode s’est terminée. Et le bloc de pile de la méthode principale devient libre.

Qu’en est-il de la libération de la mémoire du tas ? Nous allons y arriver bientôt. Prenez un café☕️, et continuez 😼.

Que se passe-t-il quand les méthodes se terminent ?

Chaque méthode a sa propre portée. Lorsque la méthode est terminée, les objets sont libérés et récupérés automatiquement de la pile.

figure 1

Dans la figure 1, lorsque la méthode foo s’est terminée. La mémoire de pile ou le bloc de pile de la méthode foo sera libéré et récupéré automatiquement.

figure 2

Dans la figure 2, la même. Lorsque la méthode main s’est terminée. La mémoire de la pile ou le bloc de pile de la méthode principale sera libéré et récupéré automatiquement.

Conclusion

Maintenant, c’est clair pour nous que les objets dans la pile sont temporaires. Une fois que la méthode se termine, les objets seront libérés et récupérés.

La pile est une structure de données LIFO (Last-In-First-Out). Vous pouvez la voir comme une boîte. En utilisant cette structure, le programme peut facilement gérer toutes ses opérations en utilisant deux opérations simples : push et pop.

Chaque fois que vous devez sauvegarder quelque chose comme une variable ou une méthode, il pousse et déplace le pointeur de la pile vers le haut. Chaque fois que vous sortez d’une méthode, tout est retiré du pointeur de la pile jusqu’au retour à l’adresse de la méthode précédente. Dans notre exemple le retour de la méthode foo à la méthode main.

Qu’en est-il du tas ?

Le tas est différent de la pile. Pour libérer et réclamer les objets de la mémoire du tas, nous avons besoin d’aide.

Pour cela, Java, ou pour être plus précis, la JVM a fait un super-héros pour nous aider. Nous l’avons appelé le Garbage Collector. Il va faire le travail difficile pour nous. Et s’occuper de détecter les objets inutilisés, les libérer, et récupérer plus d’espace dans la mémoire.

Comment fonctionne le garbage collector?

Simple. Le ramasseur d’ordures recherche les objets inutilisés ou inaccessibles. Lorsqu’il y a un objet dans le tas qui n’a aucune référence pointée vers lui, le ramasseur d’ordures se charge de le libérer de la mémoire et de récupérer plus d’espace.

GC

Les racinesGC sont des objets référencés par la JVM. Ils constituent les objets initiaux de l’arbre. Chaque objet de l’arbre possède un ou plusieurs objets racines. Tant que l’application ou les racines GC peuvent atteindre ces racines ou ces objets, l’ensemble de l’arbre est accessible. Une fois qu’ils deviennent inatteignables par l’application ou les racines GC, ils seront considérés comme des objets inutilisés ou des objets inatteignables.

Que se passe-t-il lorsque le ramasseur d’ordures s’exécute ?

Pour l’instant, c’est l’état actuel de la mémoire de l’application. La pile est vide, et le tas est plein d’objets inutilisés.

Avant GC

Après avoir exécuté le GC, les résultats seront les suivants :

Après la GC

La GC va libérer et effacer tous les objets inutilisés du tas.

Man ! Qu’en est-il de la fuite de mémoire que nous attendons ? LOL, juste un peu plus, et nous y serons. Dans notre application simple, l’application a été écrite super et simple. Il n’y avait rien de mal dans le code qui puisse empêcher le GC de libérer les objets du tas. Et pour cette raison, le GC libère et récupère tous les objets de la mémoire du tas. Continuez. Nous avons beaucoup d’exemples de fuites de mémoire dans la section suivante😃.

Quand et comment se produisent les fuites de mémoire ?

Une fuite de mémoire se produit lorsque la pile fait encore référence à des objets inutilisés dans le tas.

Il existe une représentation visuelle simple dans l’image ci-dessous pour une meilleure compréhension du concept.

Dans la représentation visuelle, nous voyons que lorsque nous avons des objets référencés depuis la pile mais qui ne sont plus utilisés. Le ramasseur d’ordures ne va jamais les libérer ou les libérer de la mémoire parce qu’il montre que ces objets sont utilisés alors qu’ils ne le sont pas.

Comment pouvons-nous provoquer une fuite ?

Il existe différentes façons de provoquer une fuite de mémoire dans Android. Et cela peut être fait facilement en utilisant AsyncTasks, Handlers, Singleton, Threads, et plus encore.

Je vais montrer quelques exemples en utilisant les threads, singleton, et listeners pour expliquer comment nous pouvons provoquer une fuite et les éviter et les réparer.

Consultez mon dépôt Github. J’ai quelques exemples de code.

Comment pouvons-nous provoquer une fuite en utilisant un thread ?

Dans cet exemple, nous allons démarrer une activité qui exécute un thread en arrière-plan. Le thread va faire une tâche qui prend 20 secondes pour se terminer.

Comme on le sait, Les classes internes détiennent une référence implicite à leur classe englobante.

Dans les coulisses, voici en fait à quoi ressemble l’activité.

La tâche de téléchargement détient une référence de l’activité ThreadActivity.

Donc, que se passe-t-il après le démarrage de la tâche ou du thread ?

Il existe deux flux possibles pour utiliser l’application. Un flux régulier qui fonctionne comme prévu sans erreur et un flux de fuite qui provoque une fuite de mémoire.

Flux régulier

Dans l’image, nous présentons le tas et la pile de l’application.

L’utilisateur démarre l’application, ouvre l’activité ThreadActivity, et a attendu à l’écran jusqu’à la fin de la tâche de téléchargement. L’utilisateur a attendu pendant 20 secondes. Pourquoi 20 secondes ? Parce que c’est le temps que prend le thread pour terminer la tâche.

Tâche en cours d’exécution

La tâche s’exécute en arrière-plan. L’utilisateur a attendu pendant 20 secondes l’achèvement de la tâche de téléchargement. Lorsque la tâche est terminée, la pile libère le bloc de méthode run().

Il n’y a pas de référence détenant la DownloadTask. Le GC a considéré l’objet DownladTask comme un objet inutilisé, et pour cette raison, le prochain cycle du GC l’effacera de la mémoire du tas.

Le GC efface les objets inutilisés du tas. Maintenant, lorsque l’utilisateur ferme l’activité. La méthode principale sera libérée de la pile, et dans le prochain cycle du GC, le GC effacera le ThreadActivity de la mémoire du tas.

Parfait!