Tutoriel sur les fuites de mémoire sous Android

Il est important au sein d'une application de toujours vérifier que nous n'avons pas de fuites mémoire générées. Pour cela nous avons quelques outils sous Android.

Article lu   fois.

L'auteur

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

I-A. La mémoire Native

La mémoire native permet de stocker des données depuis la mémoire physique ou autre dispositifs physique (exemple: disques, mémoire flash, etc..). De ce fait la JVM se basera sur cette mémoire en premier lieu.

I-B. La mémoire et JAVA

Depuis la JVM, le stockage des données s'opère dans deux grandes zones de mémoires :

  • les zones mémoires qui ont une existence de vie très courte, par exemple celles liées à un thread
  • et les zones mémoires qui ont une vie longue, soit celles crées au lancement de l'application


La JVM utilise ces espaces mémoires que nous verrons par la suite :

  • le tas (heap)
  • la pile (stack)
  • les registres, uniquement lors l'exécution des instructions du byte code
  • des zones de méthode

I-B-1. La pile (Stack)

Chaque thread possède sa propre pile, seule des données de types primitifs peuvent être stockées dans celles-ci, seul le thread propriétaire de cette pile pourra accéder à ces données. De ce fait logiquement la taille de la pile restera faible. Si vous arrivez à faire déborder cette zone mémoire, une exception sera levée de type StackOverflowError.

Par exemple nous retrouvons les appels des procédures et classes dans la pile propre au thread. Pour ce faire nous pouvons les récupérer directement par la méthode getStackTrace() de la classe Thread ou plus facilement si vous voulez afficher la pile d'appel dans la console, via la méthode statique Log.getStackTraceString(Throwable t);

Image non disponible

Stack 1 <----> 1  Thread,  que les types primitifs.

I-B-2. Le tas (Heap)

Cette zone mémoire est partagée par l'ensemble de l'application, celle-ci contient les objets. Par conséquent tout objets créés depuis l'application est sauvegardés dans cet espace mémoire et est accessible à l'ensemble de l'application. La libération de cet espace mémoire se fait par le ramasse miette  ( Garbage Collector). Elle est réalisée lorsque l'objet en question n'a pas de raison valable d'être garder en mémoire. Celle-ci étant déterminée par la non utilisation de la donnée en mémoire.

On pourrait décomposer cette zone mémoire comme tel :

  • La Young Generation

    • Eden Space : L'espace depuis lequel la mémoire est initialisée pour allouer la plupart des objets.
    • Survivor Space: L'espace qui contient les objets qui ont survécu au garbage de l'Eden Space.
  • La Old Generation

    • Tenured Generation : L'espace qui contient les objets qui ont existé pendant un moment dans la Survivor Space

Si vous arrivez à faire déborder cette zone mémoire, une exception sera levée de type OutOfMemoryError.

Le ramasse-miette pourra de ce fait bouger la mémoire des objets comme bon lui semble, ne vous inquiétez pas, cela est complètement transparent pour le développeur JAVA.

Par exemple si nous appelons la méthode hashCode d'un objet qui nous retourne l'adresse interne de l'objet, celui-ci ne bougera pas car sa valeur est sauvegardée lors d'une ré-allocation faite par le ramasse miette.

Partagé à l'ensemble de l'application, contient les objets.

I-B-3. Concept

Nous avons deux concepts pour la mémoire sous Java :

  • ShallowHeap : est la mémoire prise par un objet simple, par exemple pour un Integer cela sera 4 octets, pour un long 8, etc..Bien sûr pour les objets complexes la taille de l'objet sera ajusté par un alignement de 8 octets.
  • RetainedHeap : est la somme de la mémoire des objets retenus par un objet. En d'autres termes, quand l'objet sera supprimé il libérera la retainedHeap.

Pour plus de compréhension, imaginez le graphe d'objets suivants, où chaque objet possède une shallowHeap de 100 et pointe les uns vers les autres selon le schéma ci-dessous. Nous mettons alors la valeur (R) de la retainedHeap de chaque objet.

Image non disponible

I-B-4. Les autres espaces

 

Il existe deux autres espaces qui ne sont pas présents dans la Heap, mais qui font partie de la gestion de la mémoire de Java (HotSpot Java JM):

  • La Permanent Generation: Elle contient la représentation des données comme par exemple les classes et les méthodes des objets. Elle contient le byteCode et les données lues du fichier .class. C'est sur cet espace que s'effectue la réflexion.
  • Code Cache : Elle contient la mémoire qui est utilisée pour la compilation et le stockage du code natif.

Attention : Depuis Java 8 , nous avons  une nouvelle représentation de la HotSpot, la Permanent Generation a disparu pour laisser place a la MetaSpace. Voici les améliorations :

http://mail.openjdk.java.net/pipermail/hotspot-dev/2012-September/006679.html

I-C. Représentation

Image non disponible

I-D. Fuites de mémoire

La libération de la mémoire par le ramasse-miette se fait lorsqu'il n'y a plus de référence sur un objet alloué. Si on oublie de bien déreféncer l'objet, le ramasse-miette ne pourra pas libérer cette mémoire, du coup une fuite mémoire sera générée.  Un exemple typique serait de lancer un thread qui tournerait en boucle et de ne jamais l'arrêter.

Voici un schéma représentant une fuite mémoire:

Image non disponible
Illustration 1:http://blog.naviso.fr/wordpress/?p=1080

II. Analyse de la mémoire

Justement pour détecter ce genre de cas, nous avons à notre disposition différents outils.

II-A. Les logs

Les logs nous permettent de pouvoir récupérer à un instant “t” la représentation de la mémoire. Pour ce faire nous avons plusieurs moyens de la récupérer que nous pourrons déclarer au sein d'une classe statique qui pourra être accessible depuis tout endroit de votre application (par exemple depuis une classe statique Log), ces trois méthodes renvoient les mêmes données de base:

  • via la classe ActivityManager (une classe qui interagie avec toutes les activités exécutées sur le système, nous avons besoin d'ici du contexte de l'application) :
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
/**
* log heap by the ActivityManager
* @param context : Context to have access to the function getSystemService
* @param df : the format display
*/
private static void logActivityManagerHeap(Context context, DecimalFormat df) {
   Log.d("mytag", "ActivityManager. logHeap =================================");
   ActivityManager actManager = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE);
   MemoryInfo memInfo = new ActivityManager.MemoryInfo();
   actManager.getMemoryInfo(memInfo);
   if (memInfo != null) {
      Log.w("mytag", "ActivityManager.heap : total " +       df.format(memInfo.totalMem / 1048576.0) + "MB of " + 
      df.format(memInfo.availMem / 1048576.0) + "MB (" +
      df.format(memInfo.threshold / 1048576.0) + "MB threshold  and " +
      (memInfo.lowMemory ? "is low memory" : "is not low memory"));
   }
}
  • via la classe Runtime (Permet d'accéder à l'environnement sur lequel l'application est éxécutée) :
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
/**
* log heap by Runtime
* @param df : the format display
*/
private static void logRuntimeHeap(DecimalFormat df) {
   Log.d("mytag", "Runtime. logHeap =================================");
   Log.d("mytag", "Runtime.Memory: allocated: " + df.format(new Double(Runtime.getRuntime().totalMemory()/1048576)) + "MB of " + 
        df.format(new Double(Runtime.getRuntime().maxMemory()/1048576))+ "MB (" + 
        df.format(new Double(Runtime.getRuntime().freeMemory()/1048576)) +"MB free)");
}
  • via la classe Debug (Une classe permettant de pouvoir accéder à plusieurs méthode de débogue, ainsi que les allocations et traces):
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
/**
* log heap by Debug
* @param df : the format display
*/
private static void logDebugHeap(DecimalFormat df) {
      Log.d("mytag", "Debug. logHeap =================================");
       Double allocated = new Double(Debug.getNativeHeapAllocatedSize())/ 1048576.0;
       Double available = new Double(Debug.getNativeHeapSize())/1048576.0;
       Double free = new Double(Debug.getNativeHeapFreeSize())/1048576.0;
       Log.d("mytag", "Debug.heap native: allocated " + df.format(allocated) + "MB of " + 
        df.format(available) + "MB (" + 
        df.format(free) + "MB free)");
       
       // can get  the global allocation by function Debug.getGlobal... or binder by Debug.getBinder..., thread by Debug.getThread...
       
       // get memInfo
android.os.Debug.MemoryInfo memInfoDebug = new Debug.MemoryInfo();
Debug.getMemoryInfo(memInfoDebug);
      if (memInfoDebug != null) {
         Log.w("mytag", "Debug.heap : totalPrivateDirty " + df.format((memInfoDebug.getTotalPrivateDirty()/ 1024.0) + "MB"));
         Log.w("mytag", "Debug.heap : totalPss " + df.format((memInfoDebug.getTotalPss()/ 1024.0) + "MB"));
         Log.w("mytag", "Debug.heap : totalSharedDirty " + df.format((memInfoDebug.getTotalSharedDirty()/ 1024.0) + "MB"));
         int currentapiVersion = android.os.Build.VERSION.SDK_INT;
         if (currentapiVersion >= android.os.Build.VERSION_CODES.KITKAT){
            Log.w("mytag", "Debug.heap : totalSharedClean " + df.format((memInfoDebug.getTotalSharedClean()/ 1024.0) + "MB"));
            Log.w("mytag", "Debug.heap : totalPrivateClean " + df.format((memInfoDebug.getTotalPrivateClean()/ 1024.0) + "MB"));
            Log.w("mytag", "Debug.heap : totalSwappablePss " + df.format((memInfoDebug.getTotalSwappablePss()/ 1024.0) + "MB"));
         }
      }
}

Ces méthodes nous permettent de nous donner une représentation générale de la mémoire actuelle,  mais cela nous ne donne pas assez d'information sur réellement ce que nous avons. Cela nous permet par exemple de savoir rapidement si une fuite mémoire importante est présente ou pas (style les bitmaps).

Astuce : Les trois méthodes peuvent être utilisées, seule la classe ActivityManager à besoin d'un contexte. La classe Debug étant la méthode qui vous renverra le plus d'information sur l'utilisation de la mémoire en cours.

Les données seront bien entendu récupérer depuis le logcat sous cette forme :

11-01 16:13:37.774: D/mytag(875): Debug. logHeap =================================

11-01 16:13:37.794: D/mytag(875): Debug.heap native: allocated 6.806MB of 6.844MB (0.038MB free)

11-01 16:13:37.844: D/mytag(875): Runtime. logHeap =================================

11-01 16:13:37.903: D/mytag(875): Runtime.Memory: allocated: 2.000MB of 32.000MB (0.000MB free)

11-01 16:13:37.903: D/mytag(875): ActivityManager. logHeap =================================

11-01 16:13:37.913: W/mytag(875): ActivityManager.heap : total 336.359MB of 218.617MB (46.000MB threshold  and is not lowmemory

II-B. En ligne de commande

Vous pouvez depuis la console accéder aux informations mémoires de votre appareil.

Pour cela il vous suffit d'exécuter cette ligne de commande :

 
Sélectionnez
adb shell dumpsys meminfo <processid>

Cela vous retournera sous format de tableau les informations mémoires relatives au processus demandé.

II-C. Le Heap Dump

Un Dump de la mémoire est une capture de l'image complète de l'utilisation de la mémoire en cours. Cela se réalise via la génération d'un fichier .hrpof contenant les données sous format binaire de l'image que nous analyserons ensuite.

II-C-1. Génération du fichier

Pour générer ce fichier, il existe plusieurs méthodes que nous allons voir.

II-C-1-a. Code

Cette méthode est très utile pour générer le fichier lorsque vous aurez une exception OutOfMemoryError qui sera levée. De ce fait vous serez capable d'analyser plus rapidement les causes de cette exception.

Pour cela il vous faut rajouter dans votre classe Application :

 
Sélectionnez
@Override
public void onCreate() {
   super.onCreate();
   /**
   * Full path to a directory assigned to the package for its    persistent data. 
   * catch any error in the current Thread and dump the memory if an    outofmemoryerror 
   * can save the dump file in other path, as sdcard
   */
   Thread.currentThread().setUncaughtExceptionHandler(
new HeapDumpingUncaughtExceptionHandler(getApplicationInfo().dataDir));
}

De ce fait toutes exceptions levées depuis l'application sera interceptées via cette méthode.

Puis ensuite la classe HeapDumpingUncaughtExceptionHandler :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
/**
* Save the dump file in the given path
*
*/
public class HeapDumpingUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {

   private static final String HPROF_DUMP_BASENAME = "Android2ee.dalvik-hprof";
   private final String dataDir;

   public HeapDumpingUncaughtExceptionHandler(String dataDir) {
      this.dataDir = dataDir;
   }

   @Override
   public void uncaughtException(Thread thread, Throwable ex) {
      Log.w(getClass().getSimpleName(), "uncaughtException :" + dataDir);
      String absPath = new File(dataDir,       HPROF_DUMP_BASENAME).getAbsolutePath();
      // Dump the memory when we have an OutOfMemoryError
      if(ex.getClass().equals(OutOfMemoryError.class)) {
         try {
            // Dump "hprof" data to the specified file. This may cause a GC.
            Debug.dumpHprofData(absPath);
         } catch (IOException e) {
            Log.e(getClass().getSimpleName(),e.getMessage()) ;
         }
      }
   }
}

Cette classe vous permet de pouvoir faire un dump de la mémoire que si l'exception est une OutOfMemory, ce dump est réalisé par la fonction Debug.dumpHprofData. Dans cet exemple nous avons pris le chemin de getApplicationInfo().dataDir qui est le chemin des données persistantes, nous aurions pu prendre un autre chemin relatif, soit la sdcard, pour faciliter la récupération du fichier depuis l'appareil.

Suite à la récupération de ce fichier vous devrez utiliser l'outil de conversion fourni par la SDK d' Android, pour pouvoir lire le fichier hprof par la suite. Voici la commande :

 
Sélectionnez
platform-tools/hprof-conv infile outfile

II-C-1-b. DDMS

Une méthode plus simple existe et consiste a utiliser le DDMS fourni par le SDK d' Android. Depuis le DDMS vous trouverez deux actions qui sont Update Heap et Dump HPROF File. Il vous suffira alors de d'abord mettre à jour la Heap (via le bouton Update Heap) puis après de cliquer sur le bouton Dump HPROF File. Ce fichier sera alors ensuite généré , par besoin de le convertir pour ce cas là.

Image non disponible

Avant de récupérer la capture d'image de votre mémoire, vous pouvez provoquer un GC grâce au bouton “Cause GC” que vous trouverez depuis le DDMS.

 
Image non disponible

II-C-2. Analyse du fichier HPROF

Pour analyser ce fichier vous aurez besoin de l'outil MAT (Memory Analyser Tool), qui est un plugin pour Eclipse.

Pour installer cet outil il vous faudra récupérer le lien de la dernière release ici.

Ensuite depuis votre eclipse, aller dans Help→ Instal New Software... Entrez ensuite cet URL http://download.eclipse.org/mat/1.4/update-site/.

Vous aurez également besoin de BIRT pour pouvoir afficher les graphiques depuis MAT. Pour cela vous devrez entrez cet URL : http://download.eclipse.org/birt/update-site/4.3. BIRT est un outil open source pour créer des visualisations de données et des rapports qui peuvent être intégrés dans des applications client enrichi et Web.

II-C-2-a. Fonctionnement de MAT

Il vous suffit d'ouvrir le fichier hprof (généré ou non généré : MAT se chargera de cela pour vous) depuis Eclipse. Vous aurez alors une demande du type de rapport que vous voulez avoir comme ceci :

Image non disponible

Leak Suspects Report : Permet de vérifier que vous n'avez pas de fuite mémoire.

Component Report : Permet d'analyser un ensemble de mémoire (duplication, collection , etc..).

Pas de stress, vous pourrez basculer entre les types depuis MAT.

MAT vous générera ensuite les données et vous obtiendrez quelques chose du genre :

Image non disponible

Depuis cette page vous avez différentes options accessible depuis le menu :

Image non disponible
   
  • OverView : Permet de voir une vue générale sur le rapport généré.
  • Histogram : Permet de voir l'ensemble des objets alloués sous forme de tableau.
  • Dominator Tree: Permet de voir l'ensemble des classes présentées dans le tas sous forme de tableau.
  • Open Object Query Language : Permet sous forme de requêtes SQL de pouvoir rechercher des objets.
  • Thread name, etc. : Si possible nous renvoie sous forme de tableau la liste des threads vivants.
  • Run Expert System : est un sous menu disponible vous permettant de générer d'autre rapport, les ouvrir,  ou les exécuter.
  • Open Query Browser : Des requêtes SQL prédéfinies pour vous faciliter la vie dans votre recherche dans le tas.

III. Exemple

Prenons un exemple de fuite de mémoire :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
private static Drawable sBackground;

@Override
protected void onCreate(Bundle state) {
  super.onCreate(state);
  TextView label = new TextView(this);
  label.setText("test");
  if (sBackground == null) {
    sBackground = getDrawable(R.drawable.large_bitmap);
  }
  label.setBackgroundDrawable(sBackground);
  setContentView(label);
}

Nous pouvons voir ici que nous chargeons une TextView au démarrage de l'Activity. Ce chargement se fait dynamiquement avec une image de fond assignée à celle-ci. Lors d'une rotation de l'écran le système détruira l'Activity pour la reconstruire.  De ce fait il reppassera dans le onCreate de l'Activity et recréera une TextView avec une image de fond, nous avons gardé l'image de fond dans une statique pour ne pas recharger celle-ci à chaque fois. Nous avons généré une fuite mémoire via l'image de fond car la fonction setBackgroundDrawable attache le callback du drawable à sa propre vue. Cela implique donc que l'image possède une référence sur la vue, ce qui provoquera une fuite mémoire. Cette fuite de mémoire explosera la mémoire du smartphone qui est actuellement de 64Mo pour les récents (dont une grande partie est déjà réservée au démarrage de l'application pour avoir accès aux ressources plus rapidement depuis les 4.x).

Image non disponible
Illustration: Nous pouvons voir par le schéma que l'objet drawable gardera en mémoire l'objet TextView depuis sa callBack.

Regardons maintenant le fichier hprof que nous aurons généré suite à plusieurs rotations du smartphone.

Image non disponible
 

Voici la fuite que nous détectons via l'outil MAT, nous voyons qu'il nous informe que nous avons une fuite de mémoire sur une classe Bitmap. Nous pouvons retrouver ces instances dans le dominator tree :

Image non disponible

On retrouve bien nos onze instances que l'outil nous avaient informées.

Attention l'outil est un analyseur est peu raté également des fuites de mémoires, par exemple si nous exécutons un thread depuis l' Activity sans l'annuler lorsque l' Activity est détruite alors nous aurons une succession de thread lancé.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
@Override
protected void onResume() {
  super.onResume();
  // do a leak that's no detect by MAT
  myThread thread = new myThread();
  thread.start();
}

/**
* Thread to create a leak
*
*/
private class myThread extends Thread {
  @Override
  public void run() {
    super.run();
    while (true) {
      try {
        sleep(1000);
      } catch (InterruptedException e) {
        Log.e(“mytag”,e.getMessage();
      }
    }
  }
}

Sans annuler le thread depuis l' Activity nous aurons x threads lancés qui ne se termineront jamais à cause de la condition while(true). L'outil Mat ne trouvera pas cette fuite de mémoire mais vous pourrez retrouver cette fuite depuis la page des threads comme cela :

Nous pouvons voir ici que le thread myThread a été lancé quatre fois !

Image non disponible

IV. Conclusion

N'ayez jamais une confiance absolu au GC (Garbage Collector), il n'évite pas entièrement les fuites de mémoires.

  • Respecter les cycles de vie (Activity, Fragment, etc.) où vos objets ont été alloués.
  • Si vous créer des inner classes, pensez bien au WeakReference si vous gardez un lien sur l' Activity, Fragment.
  • Essayer d'utiliser au possible le contexte de l'application et non celui de l'activité.
  • Vérifier bien que vos images soit bien recyclé.
  • Abuser des rotations d'écrans pour contrôler votre mémoire.
  • Faites extrêmement attention avec l'usage des variables statiques !

Le projet sous github : https://github.com/ffournier/MemoryLeak

Vous pouvez aussi regarder une conférence de Romain Guy au PAUG qui parle des fuites de mémoires: ici

Vous pouvez aussi lire cet article sur les Handler, les Asynctask et les fuites mémoires http://www.android2ee.com/Tutoriaux/handlers-lifecycle.html.

 

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2015 Florian. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.