octobre
2008
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
original
is 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
etcount=13
pour la première, ce qui fait que la totalité du tableau est utilisé.offset=6
etcount=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…
15 Commentaires + Ajouter un commentaire
Tutoriels
Discussions
- jre 1.5, tomcat 6.0 et multi processeurs
- Possibilité d'accéder au type générique en runtime
- [REFLEXION] Connaitre toutes les classes qui implémentent une interface
- Difference de performances Unix/Windows d'un programme?
- Classes, méthodes private
- [ fuite ] memoire
- Définition exacte de @Override
- L'apparition du mot-clé const est-il prévu dans une version à venir du JDK?
- Recuperation du nom des parametres
Tiens je suis tombé par hasard sur ce billet, je tiens juste à signaler que le partage du tableau de char a été supprimé en Java 7.
@Nemek : tout à fait !
Désormais chaque String possède son propre tableau interne.
Du coup substring() ne s’exécute plus en temps constant (puisqu’il faut allouer un nouveau tableau), mais on évite ainsi des risques de fuite de mémoire…
A noter en contrepartie qu’il y a une JEP dont l’objectif sera à terme de dé-dupliquer les String identiques : http://openjdk.java.net/jeps/192
a++
Voici l’implémentation du J2RE 1.5.0 de la JVM J9 d’IBM
du constructeur de copie de la classe String.
L’optimisation de la mémoire ne fonctionne donc pas chez IBM.
* 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;
}
merci pour cet article!
Sympa et interressant
lunatix> Oui il y avait également une optimisation avec un partage du char[] entre les classes StringBuffer et String.
Ce comportement a été supprimé avec Java 5.0. Pour plus de détail : http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6219959
a++
Je me rappelle d’un post des développeurs d’Eclipse qui s’étaient rendu compte que de changer les retours de méthodes de
return stringBuffer.toString() a
return new String(stringBuffer.toString())
leur avaient fait gagner pas mal de mémoire.
Je n’avais jamais lu ce billet. Et j’ai une preuve de ma bonne foi
Ce billet a été publié en septembre 2008.
Si finalement mon billet n’a été publié qu’hier, cela fait longtemps que j’en avais écrit les grandes lignes (cela m’arrive parfois et encore maintenant j’ai 2 billets en brouillon :aie:)
Cela peut se vérifier par le numéro d’identifiant du billet : http://blog.developpez.com/?p=6053
Les billets avec les numéros suivants ont été publié en juillet… ce qui fait que j’ai commencé à écrire ce billet en Juillet, pour le finaliser hier :aie:
a++
En cherchant vite fait, je pense que le topic similaire est:
http://kjetilod.blogspot.com/2008/09/string-constructor-considered-useless.html
fmarot> Je ne me suis pas inspiré d’un blog anglais pour ce billet…
Cela fait pas mal de temps que j’ai commencé à écrire ce billet, mais le temps me manquant je ne l’avais jamais finalisé. Aujourd’hui j’ai pris le temps de le finir.
a++
PS : Mais si tu as le lien vers ce billet n’hésite pas
La source de l’article aurait put etre citée, quand meme…
Effectivement c’est interressant, mais j’ai lu récemment un blog en anglais (linké sur dzone.com) qui évoquait ce meme point… L’auteur aurait donc put dire où il a trouvé son inspiration. Dommage !
Mais ca reste intéressant
Cet usage est explicitement prévu dans les commentaires du source ; on trouve, en effet :
public String(String original) {
int size = original.count;
char[] originalValue = original.value;
char[] v;
if (originalValue.length > size) {
// The array representing the String is bigger than the new
// String itself. Perhaps this constructor is being called
// in order to trim the baggage, so make a copy of the array.
int off = original.offset;
v = Arrays.copyOfRange(originalValue, off, off+size);
} else {
// The array representing the String is the same
// size as the String, so no point in making a copy.
v = originalValue;
}
this.offset = 0;
this.count = size;
this.value = v;
}
Ils appellent ça « trim the baggage »
Très intéressant article!
Si l’uilité de new String() n’est que ça, je préfère avoir une méthode explicite dans la classe String qui permet ce type d’optimisation.
Comme ça on ne se pose plus de question
Excellent article!
Je me suis toujours demandé si le constructeur de String pouvait avoir une quelconque utilité sans creuser le sujet… Bravo excellente mise au point.
Simplement merci.
Je suis convaincu que cet exposé va remettre en cause l’existence de fantômes et autres phénomènes inexpliqués.