Sizeof para Java

26 de diciembre de 2003

P: ¿Java tiene un operador como sizeof () en C?

R: Una respuesta superficial es que Java no proporciona nada por el estilo C de sizeof(). Sin embargo, consideremos por qué un programador de Java podría quererlo ocasionalmente.

El programador de CA administra la mayoría de las asignaciones de memoria de la estructura de datos él mismo, y sizeof()es indispensable para conocer los tamaños de los bloques de memoria a asignar. Además, los asignadores de memoria C malloc()no hacen casi nada en lo que respecta a la inicialización de objetos: un programador debe establecer todos los campos de objeto que apuntan a otros objetos. Pero cuando todo está dicho y codificado, la asignación de memoria C / C ++ es bastante eficiente.

En comparación, la asignación y la construcción de objetos Java están unidas (es imposible utilizar una instancia de objeto asignada pero no inicializada). Si una clase Java define campos que son referencias a otros objetos, también es común establecerlos en el momento de la construcción. Por lo tanto, la asignación de un objeto Java asigna con frecuencia numerosas instancias de objetos interconectados: un gráfico de objeto. Junto con la recolección automática de basura, esto es muy conveniente y puede hacer que sienta que nunca tiene que preocuparse por los detalles de asignación de memoria de Java.

Por supuesto, esto solo funciona para aplicaciones Java simples. En comparación con C / C ++, las estructuras de datos Java equivalentes tienden a ocupar más memoria física. En el desarrollo de software empresarial, acercarse al máximo de memoria virtual disponible en las JVM actuales de 32 bits es una restricción de escalabilidad común. Por lo tanto, un programador de Java podría beneficiarse de sizeof()algo similar para vigilar si sus estructuras de datos son demasiado grandes o contienen cuellos de botella de memoria. Afortunadamente, la reflexión de Java le permite escribir una herramienta de este tipo con bastante facilidad.

Antes de continuar, prescindiré de algunas respuestas frecuentes pero incorrectas a la pregunta de este artículo.

Falacia: Sizeof () no es necesario porque los tamaños de los tipos básicos de Java son fijos

Sí, un Java inttiene 32 bits en todas las JVM y en todas las plataformas, pero esto es solo un requisito de especificación de lenguaje para el ancho perceptible por el programador de este tipo de datos. Este intes esencialmente un tipo de datos abstracto y puede respaldarse, por ejemplo, con una palabra de memoria física de 64 bits en una máquina de 64 bits. Lo mismo ocurre con los tipos no primitivos: la especificación del lenguaje Java no dice nada sobre cómo se deben alinear los campos de clase en la memoria física o que una matriz de valores booleanos no se puede implementar como un vector de bits compacto dentro de la JVM.

Falacia: puede medir el tamaño de un objeto serializándolo en una secuencia de bytes y observando la longitud de la secuencia resultante

La razón por la que esto no funciona es porque el diseño de serialización es solo un reflejo remoto del verdadero diseño en memoria. Una forma fácil de verlo es observando cómo Stringse serializan los correos electrónicos: en la memoria cada uno chartiene al menos 2 bytes, pero en forma serializada Stringlos correos electrónicos están codificados en UTF-8, por lo que cualquier contenido ASCII ocupa la mitad de espacio.

Otro enfoque de trabajo

Quizás recuerde el "Consejo 130 de Java: ¿Conoce el tamaño de sus datos?" que describía una técnica basada en la creación de una gran cantidad de instancias de clase idénticas y midiendo cuidadosamente el aumento resultante en el tamaño del montón usado de JVM. Cuando sea aplicable, esta idea funciona muy bien y, de hecho, la usaré para iniciar el enfoque alternativo en este artículo.

Tenga en cuenta que la Sizeofclase de Java Tip 130 requiere una JVM inactiva (de modo que la actividad del montón se deba solo a las asignaciones de objetos y las recolecciones de basura solicitadas por el hilo de medición) y requiere una gran cantidad de instancias de objetos idénticas. Esto no funciona cuando desea ajustar el tamaño de un solo objeto grande (tal vez como parte de una salida de seguimiento de depuración) y especialmente cuando desea examinar qué lo hizo realmente tan grande.

¿Cuál es el tamaño de un objeto?

La discusión anterior destaca un punto filosófico: dado que normalmente se trabaja con gráficos de objetos, ¿cuál es la definición de tamaño de objeto? ¿Es solo el tamaño de la instancia del objeto que está examinando o el tamaño de todo el gráfico de datos arraigado en la instancia del objeto? Esto último es lo que suele importar más en la práctica. Como verá, las cosas no siempre son tan claras, pero para empezar, puede seguir este enfoque:

  • Una instancia de objeto se puede dimensionar (aproximadamente) sumando todos sus campos de datos no estáticos (incluidos los campos definidos en superclases)
  • A diferencia de, digamos, C ++, los métodos de clase y su virtualidad no tienen impacto en el tamaño del objeto
  • Las superinterfaces de clase no tienen ningún impacto en el tamaño del objeto (consulte la nota al final de esta lista)
  • El tamaño completo del objeto se puede obtener como un cierre sobre todo el gráfico del objeto enraizado en el objeto inicial
Nota: La implementación de cualquier interfaz Java simplemente marca la clase en cuestión y no agrega ningún dato a su definición. De hecho, la JVM ni siquiera valida que una implementación de interfaz proporcione todos los métodos requeridos por la interfaz: esto es estrictamente responsabilidad del compilador en las especificaciones actuales.

Para iniciar el proceso, para los tipos de datos primitivos, utilizo tamaños físicos medidos por la Sizeofclase de Java Tip 130 . Como resultado, para las JVM comunes de 32 bits, un plano java.lang.Objectocupa 8 bytes, y los tipos de datos básicos generalmente son del menor tamaño físico que puede adaptarse a los requisitos del idioma (excepto que booleanocupa un byte completo):

// java.lang.Object shell tamaño en bytes: 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;

(Es importante darse cuenta de que estas constantes no están codificadas para siempre y deben medirse de forma independiente para una JVM determinada.) Por supuesto, la totalización ingenua de los tamaños de los campos de objetos descuida los problemas de alineación de la memoria en la JVM. La alineación de la memoria importa (como se muestra, por ejemplo, para los tipos de matrices primitivas en Java Tip 130), pero creo que no es rentable perseguir detalles de tan bajo nivel. Estos detalles no solo dependen del proveedor de JVM, sino que no están bajo el control del programador. Nuestro objetivo es obtener una buena suposición del tamaño del objeto y, con suerte, obtener una pista de cuándo un campo de clase podría ser redundante; o cuando un campo debería ser poblado de manera perezosa; o cuando es necesaria una estructura de datos anidada más compacta, etc. Para una precisión física absoluta, siempre puede volver a la Sizeofclase en Java Tip 130.

Para ayudar a perfilar lo que constituye una instancia de objeto, nuestra herramienta no solo calculará el tamaño, sino que también creará una estructura de datos útil como subproducto: un gráfico compuesto por IObjectProfileNodes:

interfaz IObjectProfileNode {Objeto objeto (); Nombre de cadena (); int tamaño (); int refcount (); IObjectProfileNode parent (); IObjectProfileNode [] hijos (); IObjectProfileNode shell (); IObjectProfileNode [] ruta (); IObjectProfileNode root (); int pathlength (); recorrido booleano (filtro INodeFilter, visitante INodeVisitor); String dump (); } // Fin de la interfaz

IObjectProfileNodeLos s están interconectados casi exactamente de la misma manera que el gráfico de objetos original, y IObjectProfileNode.object()devuelven el objeto real que representa cada nodo. IObjectProfileNode.size()devuelve el tamaño total (en bytes) del subárbol del objeto enraizado en la instancia del objeto de ese nodo. Si una instancia de objeto se vincula a otros objetos a través de campos de instancia no nulos o mediante referencias contenidas dentro de los campos de matriz, IObjectProfileNode.children()habrá una lista correspondiente de nodos de gráficos secundarios, ordenados en orden de tamaño decreciente. Por el contrario, para cada nodo que no sea el inicial, IObjectProfileNode.parent()devuelve su padre. Así, toda la colección de IObjectProfileNodes corta y divide el objeto original y muestra cómo se particiona el almacenamiento de datos dentro de él. Además, los nombres de los nodos del gráfico se derivan de los campos de clase y examinan la ruta de un nodo dentro del gráfico (IObjectProfileNode.path()) le permite rastrear los enlaces de propiedad desde la instancia del objeto original hasta cualquier dato interno.

Es posible que haya notado al leer el párrafo anterior que la idea hasta ahora todavía tiene cierta ambigüedad. Si, mientras atraviesa el gráfico de objetos, encuentra la misma instancia de objeto más de una vez (es decir, más de un campo en algún lugar del gráfico apunta a ella), ¿cómo asigna su propiedad (el puntero principal)? Considere este fragmento de código:

 Objeto 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.

Pensar en recorridos de gráficos y rutas más cortas debería sonar una campana en este punto: la búsqueda primero en amplitud es un algoritmo de recorrido de gráficos que garantiza encontrar la ruta más corta desde el nodo de inicio a cualquier otro nodo de gráfico accesible.

Después de todos estos preliminares, aquí hay una implementación de libro de texto de tal recorrido de gráfico. (Se han omitido algunos detalles y métodos auxiliares; consulte la descarga de este artículo para obtener detalles completos):