Sizeof pour Java

26 décembre 2003

Q: Java a-t-il un opérateur comme sizeof () en C?

A: Une réponse superficielle est que Java ne fournit pas quelque chose comme C de sizeof(). Cependant, voyons pourquoi un programmeur Java peut le vouloir occasionnellement.

Le programmeur AC gère lui-même la plupart des allocations de mémoire de structure de données et sizeof()est indispensable pour connaître la taille des blocs de mémoire à allouer. De plus, les allocateurs de mémoire C malloc()ne font presque rien en ce qui concerne l'initialisation des objets: un programmeur doit définir tous les champs d'objet qui sont des pointeurs vers d'autres objets. Mais quand tout est dit et codé, l'allocation de mémoire C / C ++ est assez efficace.

Par comparaison, l'allocation et la construction d'objets Java sont liées (il est impossible d'utiliser une instance d'objet allouée mais non initialisée). Si une classe Java définit des champs qui font référence à d'autres objets, il est également courant de les définir au moment de la construction. L'allocation d'un objet Java alloue donc fréquemment de nombreuses instances d'objets interconnectés: un graphe d'objets. Couplé au ramasse-miettes automatique, cela est trop pratique et peut vous donner l'impression de ne jamais avoir à vous soucier des détails d'allocation de mémoire Java.

Bien sûr, cela ne fonctionne que pour les applications Java simples. Par rapport au C / C ++, les structures de données Java équivalentes ont tendance à occuper plus de mémoire physique. Dans le développement de logiciels d'entreprise, se rapprocher de la mémoire virtuelle maximale disponible sur les JVM 32 bits d'aujourd'hui est une contrainte d'évolutivité courante. Ainsi, un programmeur Java pourrait bénéficier de sizeof()ou de quelque chose de similaire pour savoir si ses structures de données deviennent trop volumineuses ou contiennent des goulots d'étranglement de mémoire. Heureusement, la réflexion Java vous permet d'écrire un tel outil assez facilement.

Avant de continuer, je vais me passer de quelques réponses fréquentes mais incorrectes à la question de cet article.

Erreur: Sizeof () n'est pas nécessaire car les tailles des types de base Java sont fixes

Oui, un Java intest 32 bits dans toutes les JVM et sur toutes les plates-formes, mais ce n'est qu'une exigence de spécification de langage pour la largeur perceptible par le programmeur de ce type de données. Ce inttype de données est essentiellement un type de données abstrait et peut être sauvegardé, par exemple, par un mot de mémoire physique de 64 bits sur une machine 64 bits. Il en va de même pour les types non primitifs: la spécification du langage Java ne dit rien sur la façon dont les champs de classe doivent être alignés dans la mémoire physique ou sur le fait qu'un tableau de booléens n'a pas pu être implémenté en tant que vecteur de bits compact dans la JVM.

Erreur: vous pouvez mesurer la taille d'un objet en le sérialisant dans un flux d'octets et en regardant la longueur du flux résultant

La raison pour laquelle cela ne fonctionne pas est que la disposition de sérialisation n'est qu'un reflet distant de la véritable disposition en mémoire. Un moyen simple de le voir est de regarder comment Strings être sérialisé: en mémoire, chacun charfait au moins 2 octets, mais sous forme sérialisée, les Strings sont encodés en UTF-8 et donc tout contenu ASCII prend la moitié moins d'espace.

Une autre approche de travail

Vous vous souvenez peut-être du "Conseil Java 130: Connaissez-vous la taille de vos données?" qui décrit une technique basée sur la création d'un grand nombre d'instances de classe identiques et la mesure attentive de l'augmentation résultante de la taille de tas utilisée par la JVM. Le cas échéant, cette idée fonctionne très bien et je vais en fait l'utiliser pour amorcer l'approche alternative dans cet article.

Notez que la Sizeofclasse de Java Tip 130 nécessite une JVM au repos (de sorte que l'activité du tas est uniquement due aux allocations d'objets et aux garbage collection demandés par le thread de mesure) et nécessite un grand nombre d'instances d'objets identiques. Cela ne fonctionne pas lorsque vous souhaitez dimensionner un seul objet volumineux (peut-être dans le cadre d'une sortie de trace de débogage) et en particulier lorsque vous souhaitez examiner ce qui l'a rendu si grand.

Quelle est la taille d'un objet?

La discussion ci-dessus met en évidence un point philosophique: étant donné que vous traitez généralement avec des graphes d'objets, quelle est la définition d'une taille d'objet? S'agit-il uniquement de la taille de l'instance d'objet que vous examinez ou de la taille de l'ensemble du graphique de données enraciné dans l'instance d'objet? Ce dernier est généralement ce qui compte le plus dans la pratique. Comme vous le verrez, les choses ne sont pas toujours aussi claires, mais pour commencer, vous pouvez suivre cette approche:

  • Une instance d'objet peut être (approximativement) dimensionnée en totalisant tous ses champs de données non statiques (y compris les champs définis dans les superclasses)
  • Contrairement, par exemple, au C ++, les méthodes de classe et leur virtualité n'ont aucun impact sur la taille de l'objet
  • Les super-interfaces de classe n'ont aucun impact sur la taille de l'objet (voir la note à la fin de cette liste)
  • La taille totale de l'objet peut être obtenue comme une fermeture sur l'ensemble du graphe d'objet enraciné à l'objet de départ
Remarque: l' implémentation d'une interface Java marque simplement la classe en question et n'ajoute aucune donnée à sa définition. En fait, la JVM ne valide même pas qu'une implémentation d'interface fournit toutes les méthodes requises par l'interface: c'est strictement de la responsabilité du compilateur dans les spécifications actuelles.

Pour amorcer le processus, pour les types de données primitifs, j'utilise des tailles physiques mesurées par la Sizeofclasse de Java Tip 130 . En fin de compte, pour les JVM 32 bits communes, un plain java.lang.Objectprend 8 octets, et les types de données de base sont généralement de la taille physique la plus faible pouvant s'adapter aux exigences linguistiques (sauf booleanprend un octet entier):

// java.lang.Object shell size en octets: public static final int OBJECT_SHELL_SIZE = 8; public static final int OBJREF_SIZE = 4; public static final int LONG_FIELD_SIZE = 8; public static final int INT_FIELD_SIZE = 4; public static final int SHORT_FIELD_SIZE = 2; public static final int CHAR_FIELD_SIZE = 2; public static final int BYTE_FIELD_SIZE = 1; public static final int BOOLEAN_FIELD_SIZE = 1; public static final int DOUBLE_FIELD_SIZE = 8; public static final int FLOAT_FIELD_SIZE = 4;

(Il est important de réaliser que ces constantes ne sont pas codées en dur pour toujours et doivent être mesurées indépendamment pour une JVM donnée.) Bien sûr, le total naïf des tailles de champ d'objets néglige les problèmes d'alignement de la mémoire dans la JVM. L'alignement de la mémoire est important (comme indiqué, par exemple, pour les types de tableaux primitifs dans Java Tip 130), mais je pense qu'il n'est pas rentable de rechercher des détails de bas niveau. Non seulement ces détails dépendent du fournisseur JVM, mais ils ne sont pas sous le contrôle du programmeur. Notre objectif est d'obtenir une bonne estimation de la taille de l'objet et, espérons-le, avoir une idée du moment où un champ de classe peut être redondant; ou quand un champ doit être peuplé paresseusement; ou lorsqu'une structure de données imbriquée plus compacte est nécessaire, etc. Pour une précision physique absolue, vous pouvez toujours revenir à la Sizeofclasse dans Java Tip 130.

Pour aider à profiler ce qui constitue une instance d'objet, notre outil ne calculera pas seulement la taille, mais construira également une structure de données utile en tant que sous-produit: un graphique composé de IObjectProfileNodes:

interface IObjectProfileNode {Objet objet (); Nom de chaîne (); taille int (); int refcount (); IObjectProfileNode parent (); IObjectProfileNode [] enfants (); Shell IObjectProfileNode (); IObjectProfileNode [] chemin (); Racine IObjectProfileNode (); int pathlength (); cheminement booléen (filtre INodeFilter, visiteur INodeVisitor); Dump de chaîne (); } // Fin de l'interface

IObjectProfileNodeLes s sont interconnectés presque exactement de la même manière que le graphe d'objets d'origine, avec le IObjectProfileNode.object()retour de l'objet réel que chaque nœud représente. IObjectProfileNode.size()renvoie la taille totale (en octets) du sous-arbre d'objet enraciné dans l'instance d'objet de ce nœud. Si une instance d'objet est liée à d'autres objets via des champs d'instance non nuls ou via des références contenues dans des champs de tableau, alors il y IObjectProfileNode.children()aura une liste correspondante de nœuds de graphe enfants, triés par ordre de taille décroissante. Inversement, pour chaque nœud autre que le nœud de départ, IObjectProfileNode.parent()renvoie son parent. L'ensemble de la collection de IObjectProfileNodes coupe et découpe ainsi l'objet d'origine et montre comment le stockage des données est partitionné en son sein. De plus, les noms des nœuds du graphe sont dérivés des champs de classe et examinent le chemin d'un nœud dans le graphe (IObjectProfileNode.path()) vous permet de tracer les liens de propriété de l'instance d'objet d'origine vers n'importe quelle donnée interne.

Vous avez peut-être remarqué en lisant le paragraphe précédent que l'idée jusqu'à présent a encore une certaine ambiguïté. Si, en parcourant le graphe d'objets, vous rencontrez la même instance d'objet plus d'une fois (c'est-à-dire que plus d'un champ quelque part dans le graphe pointe vers elle), comment attribuez-vous sa propriété (le pointeur parent)? Considérez cet extrait de code:

 Object obj = new String [] {new String ("JavaWorld"), new String ("JavaWorld")}; 

Each java.lang.String instance has an internal field of type char[] that is the actual string content. The way the String copy constructor works in Java 2 Platform, Standard Edition (J2SE) 1.4, both String instances inside the above array will share the same char[] array containing the {'J', 'a', 'v', 'a', 'W', 'o', 'r', 'l', 'd'} character sequence. Both strings own this array equally, so what should you do in cases like this?

If I always want to assign a single parent to a graph node, then this problem has no universally perfect answer. However, in practice, many such object instances could be traced back to a single "natural" parent. Such a natural sequence of links is usually shorter than the other, more circuitous routes. Think about data pointed to by instance fields as belonging more to that instance than to anything else. Think about entries in an array as belonging more to that array itself. Thus, if an internal object instance can be reached via several paths, we choose the shortest path. If we have several paths of equal lengths, well, we just pick the first discovered one. In the worst case, this is as good a generic strategy as any.

Penser aux traversées de graphes et aux chemins les plus courts devrait sonner une cloche à ce stade: la recherche en largeur d'abord est un algorithme de traversée de graphe qui garantit de trouver le chemin le plus court du nœud de départ à tout autre nœud de graphe accessible.

Après tous ces préliminaires, voici une implémentation classique d'un tel parcours de graphe. (Certains détails et méthodes auxiliaires ont été omis; voir le téléchargement de cet article pour plus de détails.):