Inspector de archivos .class de Java
Un archivo .class de Java no es un contenedor comprimido sino el formato propio de la JVM: una clase por archivo, big-endian desde el primer byte, y cada nombre, tipo y constante escrito como un índice al único grupo de constantes que hay al principio. Este inspector lee esa estructura dentro del navegador y muestra lo mismo que informa javap -v, sin necesidad de tener un JDK y sin que el archivo salga del equipo. La cabecera da la versión del archivo, y el número mayor se corresponde con una versión de Java: 45 es Java 1.1, 49 es Java 5, 52 es Java 8 y a partir de 8 cada release suma exactamente uno, de modo que 55 es Java 11, 61 es Java 17, 65 es Java 21 y 69 es Java 25. Ese número es del que se queja un UnsupportedClassVersionError, y señala la JVM más antigua capaz de cargar el archivo, nunca el JDK que lo compiló: javac --release 8 sobre un JDK 21 sigue emitiendo un archivo con mayor 52. Los indicadores de acceso se decodifican para saber de un vistazo si se trata de una clase, una interfaz, una enumeración, una anotación o un registro; un registro no tiene indicador propio y se reconoce por su atributo Record, mientras que una enumeración se reconoce por ACC_ENUM. El grupo de constantes se cuenta por etiqueta, que es la vía más rápida para entender por qué una clase generada es enorme: unos miles de entradas Utf8, o un muro de entradas InvokeDynamic y MethodHandle procedentes de lambdas y concatenación de cadenas, saltan a la vista. Los campos y los métodos aparecen con sus descriptores traducidos a tipos de Java, y cada método con max_stack, max_locals y la longitud del código tomados de su atributo Code; los métodos abstractos y nativos no tienen Code y muestran un guion. La lista de clases referenciadas son todas las entradas CONSTANT_Class salvo la propia clase, lo más parecido a una lista de dependencias que cabe en un solo archivo. Lo que no puede decirte importa igual: los descriptores están borrados, así que un campo List<String> se lee como java.util.List salvo que exista el atributo Signature; una clase compilada con -g:none no lleva SourceFile ni números de línea ni nombres de variables, por lo que sus trazas quedan vacías; y nada de esto ve una clase cargada por nombre en tiempo de ejecución.
Cómo usar
- Suelta un archivo .class en el recuadro o haz clic para elegirlo. Tiene que ser una sola clase compilada: un .jar es un zip, así que descomprímelo antes y elige la clase que te interesa.
- Lee la fila de resumen: la versión del archivo, la versión de Java que le corresponde, la clase de tipo y el tamaño que alcanzó el grupo de constantes.
- Revisa el bloque de declaración para ver la superclase, las interfaces, los atributos de nivel de clase y el nombre del archivo fuente. Si no hay SourceFile ni información de depuración, se compiló con -g:none.
- Repasa la tabla de métodos. La pila máxima, las locales máximas y los bytes de código salen del atributo Code de cada método, por lo que un método abstracto, nativo o de interfaz muestra un guion.
- Abre el desglose del grupo de constantes cuando una clase parezca mucho mayor que el código que contiene; el recuento por etiqueta suele señalar al culpable en una línea.
Preguntas frecuentes
- ¿Qué significa "class file version 65.0" y por qué aparece UnsupportedClassVersionError?
- Los dos números son las versiones mayor y menor que ocupan los bytes 5 a 8 del archivo. La mayor 45 fue Java 1.1 y 46, 47 y 48 fueron 1.2, 1.3 y 1.4; desde la mayor 49 (Java 5) la aritmética es simplemente mayor menos 44, así que 52 es Java 8, 55 es Java 11, 61 es Java 17, 65 es Java 21 y 69 es Java 25. UnsupportedClassVersionError significa que la JVM en ejecución es más antigua que la versión mayor del archivo: el mensaje cita ambos números y la solución es un runtime más nuevo o recompilar con --release fijado a la versión sobre la que realmente despliegas. Una versión menor de 65535 es especial: marca un archivo que usa funciones preliminares y solo carga en esa release exacta con --enable-preview.
- ¿Por qué el grupo de constantes salta un índice después de un long o un double?
- Porque lo dice la especificación, y es uno de los fallos clásicos de un lector de clases escrito a mano. Una entrada CONSTANT_Long o CONSTANT_Double ocupa dos ranuras consecutivas y la segunda es inutilizable: nada puede apuntar a ella y la siguiente entrada real empieza un índice más allá. Los diseñadores de Java admitieron después que fue una mala decisión, pero está grabada en todos los archivos de clase escritos hasta hoy. Un lector que avance de uno en uno tras un long desalinea en silencio el resto del grupo, y a partir de ahí lee cada nombre de campo, nombre de método y referencia de clase de la entrada equivocada: en vez de un error produce un listado verosímil pero completamente falso. Esta herramienta avanza de dos en dos, y por eso el recuento del grupo que muestra es mayor que el número de entradas que enumera.
- ¿Las cadenas de un archivo de clase son UTF-8?
- Casi, y la diferencia muerde. CONSTANT_Utf8 usa UTF-8 modificado: el carácter NUL se codifica como los dos bytes C0 80 en lugar de un único cero, de modo que ninguna cadena contiene un cero incrustado y el código en C puede tratar los nombres como terminados en nulo. Un carácter fuera del plano multilingüe básico no se escribe en la forma de cuatro bytes del UTF-8 real: se parte en su par suplente de UTF-16 y cada suplente se escribe en la forma de tres bytes, seis en total. Pasar esos bytes a un decodificador UTF-8 estándar da un carácter de reemplazo o una excepción. Esta herramienta decodifica cada grupo a una unidad de código UTF-16 y deja que el par vuelva a unirse, que es lo que hace la JVM.
- ¿Por qué mi campo List<String> aparece como java.util.List?
- Porque los descriptores están borrados. La estructura field_info guarda un descriptor, y un descriptor no tiene sitio para un argumento de tipo: Ljava/util/List; es todo lo que hay. La información genérica se guarda aparte en un atributo opcional Signature, que el compilador emite junto al descriptor para clases, campos y métodos genéricos, y que usan la reflexión y javap al imprimir una declaración. Por eso la lista de atributos incluye a menudo Signature en una clase o un miembro que por lo demás parece corriente. Si necesitas saber que un campo es List<String> y no un List crudo, el atributo Signature es el único lugar del archivo que lo dice.
- ¿Cómo distingo una clase, una enumeración, un registro y una anotación?
- Por los indicadores de acceso y un atributo. ACC_INTERFACE marca una interfaz, y un tipo de anotación es una interfaz que además lleva ACC_ANNOTATION, razón por la que javap imprime una anotación como una interfaz que extiende java.lang.annotation.Annotation. ACC_ENUM marca una enumeración, cuya superclase es java.lang.Enum y cuyas constantes aparecen como campos static final que también llevan ACC_ENUM, junto a un array sintético $VALUES. Un registro no tiene ningún indicador propio: es una clase final que extiende java.lang.Record y lleva un atributo Record con la lista de componentes, así que el atributo es la única prueba fiable. ACC_MODULE marca un archivo module-info, que no tiene campos ni métodos y no es una clase en absoluto.
- ¿La lista de clases referenciadas equivale a las dependencias de la clase?
- Se acerca, y conviene conocer sus huecos. La lista son todas las entradas CONSTANT_Class del grupo, lo que cubre la superclase, las interfaces, todo lo que se construye, se convierte, se captura o actúa como propietario de un campo o un método, y cada tipo de array mencionado en el bytecode. No cubre un tipo que solo aparece dentro de un descriptor: un método que recibe un String y no devuelve nada deja (Ljava/lang/String;)V en el grupo como entrada Utf8, y java/lang/String puede no aparecer nunca como CONSTANT_Class. Tampoco ve una clase nombrada solo en una cadena y cargada con Class.forName, ni un servicio resuelto en tiempo de ejecución. Tómala como el conjunto de referencias de compilación, no como el cierre completo de ejecución.
Herramientas relacionadas
Inspector de bytecode .pyc de Python
Abre un .pyc de __pycache__ en el navegador: qué CPython lo escribió, cómo se invalida, el árbol de objetos de código y el desensamblado.
Inspector de catálogos .mo de gettext
Lee en el navegador un catálogo .mo compilado de GNU gettext: orden de bytes, cabecera, cada msgid y traducción, contextos, plurales y cadenas sin traducir.
Herramientas Semver (Parser, Comparador, Incremento)
Parsea, compara e incrementa versiones semánticas lado a lado, con un expansor de rango que muestra qué cubren realmente ^x.y.z y ~x.y.z.
Inspector de pickle de Python
Desensambla un .pkl en el navegador: cada opcode, el memo, el valor que reconstruye y si cargarlo ejecutaría código.
Inspector de binarios ELF
Abre un ejecutable de Linux, un .so o un .o en tu navegador: arquitectura, bibliotecas que necesita, Build ID y si trae PIE, NX y RELRO.
Analizador de Java .properties
Analiza un archivo .properties igual que java.util.Properties.load: separadores, escapes y continuaciones, con las líneas que te van a sorprender.