Rien n'est complètement inutile, malgré des apparences qui pourraient laisser présager le contraire...
L'API Java propose un constructeur de copie String(String) pour la classe String. Pour rappel le constructeur de copie est une notion très importante en C++ où des copies d'objets sont obligatoires afin de gérer proprement la mémoire (lorsqu'on n'utilise pas de GC), sinon on ne saurait plus si l'objet est libérable ou pas. A l'inverse, en Java ce concept est rarement utilisé puisque le GC couplé à la notion d'immuabilité des classes permet de partager des instances entre plusieurs classes sans que cela ne cause de problème. Cela permet d'éviter d'avoir à faire de multiples copies de protection...
La classe String étant immuable, la création de copie de protection est donc inutile. De ce fait pendant longtemps j'ai pensé que la présence de ce constructeur de copie String(String) était une bizarrerie de l'API, que l'on se traine pour des raisons de compatibilité ascendante.
D'ailleurs, même la documentation officiel met en avant le peu d'intérêt de ce constructeur :
Unless an explicit copy of
originalis needed, use of this constructor is unnecessary since Strings are immutable.
Il existe pourtant un cas où cela peut se reveler utile, et c'est justement lié à l'implémentation de la classe String en Java. Comme vous le savez surement, et puisque la classe String est immuable, la plupart des méthodes "modifiant" la chaine de caractères renvoyent en fait une nouvelle instance de la classe, ceci afin de préserver l'immuabilité de la classe. Ces méthodes sont toutefois optimisés afin de ne pas utiliser trop de mémoire, en partageant un maximum d'information.
Ainsi, en interne, une String est représentée par trois attributs :
char value[], un tableau stockant les caractères de la chaîne.int offset, qui indique l'index du premier caractère de la chaîne dans le tableau.int count, qui indique le nombre de caractère de la chaîne.L'astuce vient du fait que le tableau de caractère value peut être partagé par plusieurs String différentes afin d'éviter de dupliquer du code. Ainsi, si on prend le code suivant :
String helloWorld = "Hello World !"; String world = helloWorld.substring(6, 11);
Nous avons bien deux objets de type String, mais ils utilisent tous les deux le même tableau value avec des positions différentes :
offset=0 et count=13 pour la première, ce qui fait que la totalité du tableau est utilisé.offset=6 et count=5 pour la seconde, ce qui fait que seul le mot "World" est utilisé.Cela permet d'éviter de dupliquer inutilement les tableaux et donc d'économiser la mémoire. La plupart des temps cette implémentation est bénéfique, mais il arrive des cas où cela peut poser problème. Et c'est justement dans ces cas là que le constructeur par copie prend de l'intérêt.
Pour bien mettre en évidence cette problématique, je vais utiliser deux méthodes toutes simples.
La première permet de générer aléatoirement une chaine de caractère de taille quelconque :
/** * Crée une chaine aléatoire de la taille indiqué en paramètre * @param size Taille de la chaine à créer * @return Une chaine aléatoire. */ public static String createString(int size) { final String str = "abcdefghijklmnopqrstuvwxyz0123456789"; final int length = str.length(); final Random random = new Random(); final StringBuilder sb = new StringBuilder(); for (int i=0; i<size; i++) { sb.append( str.charAt(random.nextInt(length)) ); } return sb.toString(); }
La seconde "force" la collecte des objets par le GC, et affiche les informations sur la mémoire utilisée par l'application :
/** * "Force" l'exécution du GC et affiche la taille du heap. */ public static void tryToForceGC() { // On fait le bourrin en appelant le GC 5 fois au cas où for (int i=0; i<5; i++) { System.gc(); Thread.yield(); } System.out.println(ManagementFactory.getMemoryMXBean().getHeapMemoryUsage()); }
Attention : le GC ne devrait pas être appelé explicitement dans une application normale. Si je le fais ici c'est uniquement pour mettre en évidence un comportement. Dans une vrai application l'appel explicite au GC peut poser des problèmes de performances car il y a de forte chance que cela génère un cycle complet inutilement...
Mon premier test se contentera de mettre en évidence les optimisations de substring(), en créant une chaîne de très grande taille (10 millions d'éléments), puis en la découpant en deux sous-chaines :
tryToForceGC(); String str = createString(10000000); tryToForceGC(); String str1 = str.substring(0, 5000000); String str2 = str.substring(5000001); tryToForceGC();
Cela nous génère le résultat suivant :
init = 0(0K) used = 132944(129K) committed = 5177344(5056K) max = 66650112(65088K) init = 0(0K) used = 20116464(19644K) committed = 66650112(65088K) max = 66650112(65088K) init = 0(0K) used = 20116512(19645K) committed = 66650112(65088K) max = 66650112(65088K)
Avec la création de notre chaîne d'un millions d'éléments, la mémoire utilisée passe logiquement de 129K à près de 20 Mo (pour rappel les char en Java sont stocké en UTF-16 et occupe ainsi 2 octets). Mais on voit également que la création des deux sous-chaines via subString() n'entraine quasiment aucune consommation mémoire, puisqu'on utilise en fait les mêmes données...
On voit bien là tout l'intérêt de cette optimisation : théoriquement l'occupation mémoire aurait dû doubler entre le deuxième et le troisième affichage de la mémoire. Or il n'en ait rien car ici les trois instances de la classe String utilisent toutes le même espace mémoire.
A l'inverse prennons maintenant le code suivant :
tryToForceGC(); String str = createString(10000000); tryToForceGC(); str = str.substring(0, 5); tryToForceGC();
De notre chaine de 20 Mo, nous ne conservons désormais que les 5 premiers caractères, ce qui devrait représenter 10 petits octets (si on prend uniquement en compte les caractères).
Pourtant la sortie du programme nous donne une toute autre information :
init = 0(0K) used = 132832(129K) committed = 5177344(5056K) max = 66650112(65088K) init = 0(0K) used = 20116496(19645K) committed = 66650112(65088K) max = 66650112(65088K) init = 0(0K) used = 20116496(19645K) committed = 66650112(65088K) max = 66650112(65088K)
Nous utilisons toujours 20 Mo de mémoire, alors que notre programme ne référence plus qu'une toute petite chaine de 10 octets !
Le GC aurait-il du mal à faire son travail ?
Pas du tout ! La référence vers l'instance de String de 10 millions de lettres a bien été nettoyé, mais pas son tableau interne qui est désormais également référencé par la nouvelle instance renvoyée par le substring(), malgré le fait qu'elle n'en utilise qu'une infime partie...
On est typiquement dans un cas (extrême) où le partage des données engendre une utilisation mémoire inutile.
C'est là que survient l'intérêt du constructeur String(String) :
tryToForceGC(); String str = createString(10000000); tryToForceGC(); str = new String( str.substring(0, 5) ); tryToForceGC();
En effet, ce constructeur va créer une nouvelle instance de la même valeur, mais sans utiliser le mécanisme de partage des données : elle utilisera son propre espace mémoire. Ainsi le tableau interne de 10 millions d'éléments est désormais libérable par le GC, puisqu'il n'est plus référencé nulle-part, et on se retrouve bien à la fin avec une utilisation mémoire "normale" :
init = 0(0K) used = 132832(129K) committed = 5177344(5056K) max = 66650112(65088K) init = 0(0K) used = 20116496(19645K) committed = 66650112(65088K) max = 66650112(65088K) init = 0(0K) used = 116504(113K) committed = 5181440(5060K) max = 66650112(65088K)
Bref : le constructeur String(String) n'est pas si inutile que cela, puisqu'il permet de passer outre le partage des données entre différentes instances de String. Si ces dernières sont très utile dans 95% des cas, il reste quelque cas particulier où cela peut être problématique...
Faut-il l'utiliser massivement pour autant ? Non puisque cela ne représente que quelques cas particuliers, mais il peut être important de bien comprendre ce comportement, en particulier lorsque on crée des sous-chaines qui auront une durée de vie plus importante que la chaine originale...
http://blog.developpez.com/htsrv/trackback.php?tb_id=6053
/**
* Creates a string that is a copy of another string
*
* @param string the String to copy
*/
public String (String string) {
value = string.value;
offset = string.offset;
count = string.count;
}
Vous devez être identifié pour poster un commentaire.

Ce blog est mis à disposition sous un contrat Creative Commons BY-NC-SA.
System.gc() a encore frappé : jre 1.5, tomcat 6.0 et multi processeurs
Tutoriels Java SE
Tutoriels Java EE
Sélection du blog
Java SE/EE et Web en général
| Lun | Mar | Mer | Jeu | Ven | Sam | Dim |
|---|---|---|---|---|---|---|
| 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 |
Copyright © 2000-2012 - www.developpez.com