Ir para o conteúdo
AZ Tools

Inspetor de arquivos .class do Java

Um arquivo .class do Java não é um contêiner compactado e sim o formato da própria JVM: uma classe por arquivo, big-endian desde o primeiro byte, e cada nome, tipo e constante escrito como um índice para o único pool de constantes que fica no início. Este inspetor lê essa estrutura dentro do navegador e mostra o mesmo que o javap -v relata, sem precisar de um JDK instalado e sem que o arquivo saia da máquina. O cabeçalho traz a versão do arquivo, e o número maior corresponde a uma versão do Java: 45 é Java 1.1, 49 é Java 5, 52 é Java 8 e, a partir do 8, cada release soma exatamente um, de modo que 55 é Java 11, 61 é Java 17, 65 é Java 21 e 69 é Java 25. É desse número que reclama um UnsupportedClassVersionError, e ele indica a JVM mais antiga capaz de carregar o arquivo, nunca o JDK que o compilou: javac --release 8 em um JDK 21 continua gerando um arquivo com maior 52. Os sinalizadores de acesso são decodificados para você ver de relance se o arquivo guarda uma classe, uma interface, uma enumeração, uma anotação ou um registro; um registro não tem sinalizador próprio e é identificado pelo atributo Record, enquanto uma enumeração é identificada por ACC_ENUM. O pool de constantes é contado por tag, o caminho mais rápido para entender por que uma classe gerada ficou enorme: alguns milhares de entradas Utf8, ou uma parede de entradas InvokeDynamic e MethodHandle vindas de lambdas e concatenação de strings, aparecem na hora. Campos e métodos são listados com os descritores traduzidos para tipos Java, e cada método com max_stack, max_locals e o tamanho do código tirados do seu atributo Code; métodos abstratos e nativos não têm Code e mostram um traço. A lista de classes referenciadas são todas as entradas CONSTANT_Class exceto a própria classe, o mais próximo de uma lista de dependências que cabe em um único arquivo. O que ele não consegue dizer importa igual: os descritores são apagados, então um campo List<String> aparece como java.util.List a menos que exista o atributo Signature; uma classe compilada com -g:none não carrega SourceFile, nem números de linha, nem nomes de variáveis locais, e por isso seus rastreamentos de pilha ficam vazios; e nada aqui enxerga uma classe carregada por nome em tempo de execução.

Como usar

  1. Solte um arquivo .class na caixa ou clique para escolher. Precisa ser uma única classe compilada: um .jar é um zip, então descompacte antes e escolha a classe desejada.
  2. Leia a linha de visão geral: a versão do arquivo, a versão do Java correspondente, a espécie de tipo e o tamanho que o pool de constantes alcançou.
  3. Confira o bloco de declaração para ver a superclasse, as interfaces, os atributos no nível da classe e o nome do arquivo de origem. Sem SourceFile e sem informação de depuração, a classe foi compilada com -g:none.
  4. Percorra a tabela de métodos. Pilha máxima, locais máximas e bytes de código vêm do atributo Code de cada método, então um método abstrato, nativo ou de interface mostra um traço.
  5. Abra o detalhamento do pool de constantes quando uma classe parecer muito maior do que o código que contém; a contagem por tag costuma apontar o culpado em uma linha.

Perguntas frequentes

O que significa "class file version 65.0" e por que aparece UnsupportedClassVersionError?
Os dois números são as versões maior e menor gravadas nos bytes 5 a 8 do arquivo. A maior 45 foi o Java 1.1 e 46, 47 e 48 foram 1.2, 1.3 e 1.4; da maior 49 (Java 5) em diante a conta é simplesmente maior menos 44, então 52 é Java 8, 55 é Java 11, 61 é Java 17, 65 é Java 21 e 69 é Java 25. UnsupportedClassVersionError quer dizer que a JVM em execução é mais antiga que a versão maior do arquivo: a mensagem cita os dois números, e a correção é um runtime mais novo ou recompilar com --release apontando para a versão em que você realmente implanta. Uma versão menor igual a 65535 é especial: marca um arquivo que usa recursos de prévia e só carrega naquela release exata com --enable-preview.
Por que o pool de constantes pula um índice depois de um long ou um double?
Porque a especificação manda, e esse é um dos erros clássicos de um leitor de classes escrito à mão. Uma entrada CONSTANT_Long ou CONSTANT_Double ocupa duas posições consecutivas e a segunda é inutilizável: nada pode apontar para ela e a próxima entrada real começa um índice adiante. Os projetistas do Java depois admitiram que foi uma escolha ruim, mas ela está gravada em todo arquivo de classe já produzido. Um leitor que avance de um em um depois de um long desalinha em silêncio o resto do pool, e dali em diante lê cada nome de campo, nome de método e referência de classe da entrada errada: em vez de um erro, sai uma listagem plausível e completamente falsa. Esta ferramenta avança de dois em dois, e por isso a contagem do pool que ela mostra é maior que o número de entradas listadas.
As strings dentro de um arquivo de classe são UTF-8?
Quase, e a diferença morde. CONSTANT_Utf8 usa UTF-8 modificado: o caractere NUL é codificado como os dois bytes C0 80 em vez de um único zero, para que nenhuma string contenha um zero embutido e o código em C possa tratar nomes como terminados em nulo. Um caractere fora do plano multilíngue básico não é escrito na forma de quatro bytes do UTF-8 real: ele é dividido no seu par substituto de UTF-16 e cada substituto é escrito na forma de três bytes, seis no total. Entregar esses bytes a um decodificador UTF-8 padrão gera um caractere de substituição ou uma exceção. Esta ferramenta decodifica cada grupo para uma unidade de código UTF-16 e deixa o par se recombinar, que é o que a JVM faz.
Por que meu campo List<String> aparece como java.util.List?
Porque descritores são apagados. A estrutura field_info guarda um descritor, e um descritor não tem espaço para um argumento de tipo: Ljava/util/List; é tudo o que existe. A informação genérica fica separada em um atributo opcional Signature, que o compilador emite ao lado do descritor para classes, campos e métodos genéricos, e que é usado pela reflexão e pelo javap ao imprimir uma declaração. Por isso a lista de atributos aqui costuma incluir Signature em uma classe ou membro que de resto parece comum. Se você precisa saber que um campo é List<String> e não um List cru, o atributo Signature é o único lugar do arquivo que diz isso.
Como distinguir uma classe, uma enumeração, um registro e uma anotação?
Pelos sinalizadores de acesso e por um atributo. ACC_INTERFACE marca uma interface, e um tipo de anotação é uma interface que também traz ACC_ANNOTATION, motivo pelo qual o javap imprime uma anotação como interface que estende java.lang.annotation.Annotation. ACC_ENUM marca uma enumeração, cuja superclasse é java.lang.Enum e cujas constantes aparecem como campos static final que também trazem ACC_ENUM, ao lado de um array sintético $VALUES. Um registro não tem sinalizador algum: é uma classe final que estende java.lang.Record e carrega um atributo Record listando os componentes, então o atributo é o único teste confiável. ACC_MODULE marca um arquivo module-info, que não tem campos nem métodos e nem sequer é uma classe.
A lista de classes referenciadas é a mesma coisa que as dependências da classe?
É próxima, e vale conhecer as lacunas. A lista são todas as entradas CONSTANT_Class do pool, o que cobre a superclasse, as interfaces, tudo que é construído, convertido, capturado ou usado como dono de campo ou método, e todo tipo de array citado no bytecode. Ela não cobre um tipo que aparece apenas dentro de um descritor: um método que recebe um String e não devolve nada coloca (Ljava/lang/String;)V no pool como entrada Utf8, e java/lang/String pode nunca aparecer como CONSTANT_Class. Ela também não enxerga uma classe nomeada só em uma string e carregada com Class.forName, nem um serviço resolvido em tempo de execução. Trate-a como o conjunto de referências de compilação, não como o fecho completo de execução.

Ferramentas relacionadas