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);
|
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.
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▲
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:
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) :
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) :
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):
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 :
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 :
@
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 :
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 :
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à.
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, 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 :
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 :
Depuis cette page vous avez différentes options accessible 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 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 :
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).
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 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é.
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 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.