I. Introduction▲
I-A. La mémoire native▲
La mémoire native permet de stocker des données depuis la mémoire physique ou autres dispositifs physiques (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émoire :
- les zones mémoire qui ont une existence de vie très courte, par exemple celles liées à un thread ;
- et les zones mémoire qui ont une vie longue, soit celles créées au lancement de l'application.
La JVM utilise ces espaces mémoire que nous verrons par la suite :
- le tas (heap) ;
- la pile (stack) ;
- les registres, uniquement lors de 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, seules des données de types primitifs peuvent être stockées dans celle-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'appels dans la console, via la méthode statique Log.getStackTraceString
(
Throwable t);
|
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 objet créé depuis l'application est sauvegardé 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 gardé 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 ceci :
-
la Young Generation
- Eden Space : l'espace depuis lequel la mémoire est initialisée pour allouer la plupart des objets ;
- le 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ée par un alignement de 8 octets ;
- RetainedHeap : est la somme de la mémoire des objets retenue 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.
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 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 ;
- la 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 à la MetaSpace. Voici les améliorations :
http://mail.openjdk.java.net/pipermail/hotspot-dev/2012-September/006679.html
I-C. Représentation▲
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éréférencer 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 :
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 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 notre 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 interagit avec toutes les activités exécutées sur le système, nous avons besoin ici du contexte de l'application) :
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 exécutée) :
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 d'accéder à plusieurs méthodes de débogage, ainsi que les allocations et traces) :
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'informations 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 a besoin d'un contexte. La classe Debug étant la méthode qui nous renverra le plus d'informations sur l'utilisation de la mémoire en cours.
Les données seront bien entendu récupérées 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émoire de votre appareil.
Pour cela, il vous suffit d'exécuter cette ligne de commande :
adb shell dumpsys meminfo <
processid>
Cela vous retournera sous format de tableau les informations mémoire 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 :
@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 seront interceptées via cette méthode.
Ensuite la classe HeapDumpingUncaughtExceptionHandler :
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 ne 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 :
platform-tools/hprof-conv infile outfile
II-C-1-b. DDMS▲
Une méthode plus simple existe et consiste à 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 de cliquer sur le bouton Dump HPROF File. Ce fichier sera alors généré , pas besoin de le convertir pour ce cas-là.
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.
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, allez dans Help→ Instal New Software… Entrez ensuite cette 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 entrer cette 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 :
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émoires (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 quelque chose du genre :
Depuis cette page, vous avez différentes options accessibles depuis le menu :
- 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 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'autres rapports, 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 :
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 repassera 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 64 Mo 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).
Regardons maintenant le fichier hprof que nous aurons généré suite à plusieurs rotations du smartphone.
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 :
On retrouve bien nos onze instances que l'outil nous avait informées.
Attention l'outil est un analyseur et peut rater également des fuites de mémoire, 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.
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 !
IV. Conclusion▲
N'ayez jamais une confiance absolue au GC (Garbage Collector), il n'évite pas entièrement les fuites de mémoire.
- Respectez les cycles de vie (Activity, Fragment, etc.) où vos objets ont été alloués.
- Si vous créez des inner classes, pensez bien au WeakReference si vous gardez un lien sur l'Activity, Fragment.
- Essayez d'utiliser au possible le contexte de l'application et non celui de l'activité.
- Vérifiez bien que vos images sont bien recyclées.
- Abusez des rotations d'écran pour contrôler votre mémoire.
- Faites extrêmement attention à 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émoire : ici
Vous pouvez aussi lire cet article sur les Handler, les Asynctask et les fuites mémoire http://www.android2ee.com/Tutoriaux/handlers-lifecycle.html.
V. Remerciements Developpez▲
Mes remerciements à Claude Leloup pour sa relecture orthographique et également à Mickael Baron pour sa relecture technique
N'hésitez pas à commenter cet article ! 3 commentaires