{"slug": "o-que-tem-em-um-class-do-java", "title": "O que tem em um .class do Java?", "summary": "A developer explains the binary structure of Java .class files, including the CAFEBABE magic number, version info, and constant pool, as part of a series on compiling Brainfuck to JVM bytecode. The post demonstrates reading a compiled Hello.class with xxd and outlines the big-endian byte ordering required when generating class files in Node.js.", "body_md": "Na [parte 1](https://codesilva.com/programacao/2026/03/16/compilando-brainfuck-pra-jvm-parte-1-o-interpretador) a gente construiu um interpretador de Brainfuck com tokenizer, parser e uma Representação Intermediária. Agora a gente precisa entender o formato que a JVM espera receber pra poder gerar nosso próprio bytecode.\n\nQuando você roda `javac Hello.java`\n\n, o compilador gera um `Hello.class`\n\n. Esse arquivo é binário - não é texto, não é JSON, não é XML. São bytes crus numa estrutura muito específica definida na [especificação da JVM](https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-4.html).\n\nNesse post, a gente vai abrir um `.class`\n\ncom `xxd`\n\n, entender cada byte, e construir um gerador que monta essa estrutura do zero em Node.js.\n\nA JVM usa **big endian** pra representar números de múltiplos bytes. Isso é importante porque o processador do seu computador (x86/ARM) provavelmente usa little endian, que é a ordem inversa.\n\nQual a diferença? Imagina o número 30.000 (em hex: `0x7530`\n\n). Ele ocupa 2 bytes:\n\n| Formato | Byte 1 | Byte 2 |\n|---|---|---|\n| Big endian | `0x75` |\n`0x30` |\n| Little endian | `0x30` |\n`0x75` |\n\nBig endian coloca o byte mais significativo primeiro. Little endian coloca o menos significativo primeiro.\n\nNo nosso gerador, toda vez que a gente escrever um número de 2 ou 4 bytes, precisa respeitar essa ordem. A função pra converter um número de 16 bits (2 bytes) pra big endian:\n\n```\nfunction intTo2Bytes(num) {\n  return [(num >> 8) & 0xFF, num & 0xFF];\n}\n```\n\nO `>> 8`\n\ndesloca 8 bits pra direita, pegando o byte alto. O `& 0xFF`\n\nmascara o byte baixo. Pra 4 bytes, a lógica é a mesma mas com mais shifts.\n\nSe você errar a ordem dos bytes, a JVM vai ler valores completamente errados. Um `0x7530`\n\n(30.000) vira `0x3075`\n\n(12.405) se os bytes ficarem invertidos. Então se o seu `.class`\n\ngerado dá erros estranhos no `constant pool`\n\n, confere se você tá escrevendo big endian.\n\nVamos criar o programa Java mais simples possível:\n\n```\npublic class Hello {\n  public static void main(String[] args) {\n    return;\n  }\n}\n```\n\nCompila com `javac Hello.java`\n\ne abre o binário com `xxd Hello.class`\n\n:\n\n```\n00000000: cafe babe 0000 0034 000d 0a00 0200 0307  .......4........\n00000010: 0004 0c00 0500 0601 0010 6a61 7661 2f6c  ..........java/l\n00000020: 616e 672f 4f62 6a65 6374 0100 063c 696e  ang/Object...<in\n00000030: 6974 3e01 0003 2829 5607 0008 0100 0548  it>...()V......H\n00000040: 656c 6c6f 0100 0443 6f64 6501 0004 6d61  ello...Code...ma\n00000050: 696e 0100 1628 5b4c 6a61 7661 2f6c 616e  in...([Ljava/lan\n00000060: 672f 5374 7269 6e67 3b29 5600 2100 0700  g/String;)V.!...\n```\n\nParece caótico, mas tem uma estrutura definida. Vamos ler byte por byte.\n\nOs primeiros 4 bytes de todo `.class`\n\nsão sempre `CA FE BA BE`\n\n. É o magic number que identifica o arquivo como um ClassFile da JVM. Se esses bytes não estiverem lá, a JVM recusa o arquivo imediatamente.\n\n```\ncafe babe\n```\n\nA história diz que os criadores do Java escolheram `CAFEBABE`\n\nporque lembravam de um café que frequentavam. Verdade ou não, é memorável.\n\nOs 4 bytes seguintes indicam a versão do formato:\n\n```\n0000 0034\n```\n\n`0000`\n\n- minor version: 0`0034`\n\n- major version: 52 (decimal)Major version 52 é Java 8. A versão é importante porque determina quais features o `.class`\n\npode usar. A partir da versão 50 (Java 6), por exemplo, a `StackMapTable`\n\né obrigatória - mas esse é assunto da [parte 3](https://codesilva.com/programacao/2026/03/16/compilando-brainfuck-pra-jvm-parte-3-gerando-bytecode).\n\nAqui é onde mora a complexidade. O `constant pool`\n\né uma tabela que armazena todas as constantes do programa: nomes de classes, nomes de métodos, strings, descritores de tipo. Tudo que o bytecode referencia é guardado aqui.\n\nOs próximos 2 bytes indicam o tamanho:\n\n```\n000d\n```\n\n`0x000d`\n\n= 13. Mas atenção: o constant pool usa indexação começando em 1, e o count é sempre `n + 1`\n\n. Então temos 12 entries (indices 1 a 12).\n\nCada entry começa com um byte de tag que indica o tipo:\n\n| Tag | Tipo | O que armazena |\n|---|---|---|\n| 1 | CONSTANT_Utf8 | String UTF-8 (nomes, descritores) |\n| 7 | CONSTANT_Class | Referência a uma classe (aponta pra um Utf8) |\n| 9 | CONSTANT_Fieldref | Referência a um campo (classe + nome/tipo) |\n| 10 | CONSTANT_Methodref | Referência a um método (classe + nome/tipo) |\n| 12 | CONSTANT_NameAndType | Par nome + descritor de tipo |\n\nVou destrinchar as primeiras entries do nosso `Hello.class`\n\n:\n\n**Entry 1** (começa no byte 0x0a):\n\n```\n0a 00 02 00 03\n```\n\n`0x0a`\n\n= 10 = CONSTANT_Methodref`0x0002`\n\n= 2`0x0003`\n\n= 3Isso é uma referência ao método `Object.<init>()V`\n\n- o construtor da classe pai.\n\n**Entry 2**:\n\n```\n07 00 04\n```\n\n`0x07`\n\n= 7 = CONSTANT_Class`0x0004`\n\n= 4 (aponta pra um Utf8 com o nome da classe)**Entry 4**:\n\n```\n01 0010 6a617661 2f6c616e 672f4f62 6a656374\n```\n\n`0x01`\n\n= 1 = CONSTANT_Utf8`0x0010`\n\n= 16 bytes`java/lang/Object`\n\nPercebe o padrão? O Methodref aponta pro Class, que aponta pro Utf8. É uma estrutura de referências indiretas. O bytecode nunca guarda strings diretamente - tudo passa pelo constant pool.\n\nUma coisa que confunde no começo: a JVM usa uma notação própria pra tipos. Não é `void main(String[] args)`\n\n, é `([Ljava/lang/String;)V`\n\n.\n\nAs regras:\n\n| Tipo Java | Descritor JVM |\n|---|---|\n`int` |\n`I` |\n`byte` |\n`B` |\n`char` |\n`C` |\n`void` |\n`V` |\n`String` |\n`Ljava/lang/String;` |\n`int[]` |\n`[I` |\n`byte[]` |\n`[B` |\n\nPra métodos, o formato é `(parâmetros)retorno`\n\n. Então:\n\n| Java | Descritor JVM |\n|---|---|\n`void main(String[] args)` |\n`([Ljava/lang/String;)V` |\n`void print(char c)` |\n`(C)V` |\n`int read()` |\n`()I` |\n\nEsses descritores aparecem no constant pool e o bytecode referencia eles pelo índice.\n\nO resto do ClassFile segue:\n\n```\n0021              - Access flags (ACC_PUBLIC | ACC_SUPER)\n0007              - This class (índice no constant pool)\n0002              - Super class (java/lang/Object)\n0000              - Interfaces count: 0\n0000              - Fields count: 0\n0002              - Methods count: 2\n```\n\nDepois vêm os métodos (cada um com seus atributos de código) e por fim os atributos da classe.\n\nUma coisa que eu não esperava: mesmo o programa mais simples tem **2 métodos**. O `main`\n\nque a gente escreveu e o construtor `<init>`\n\nque o Java gera automaticamente. No nosso gerador, a gente também precisa criar esse construtor.\n\nAgora que a gente entende a estrutura, vamos construir um gerador em Node.js. A ideia é montar o `.class`\n\nbyte por byte num buffer.\n\nPrimeiro, as primitivas de escrita:\n\n```\nclass ClassFileGenerator {\n  constructor() {\n    this.buffer = [];\n    this.constantPool = [];\n    this.constantPoolMap = {};\n  }\n\n  writeU1(value) {\n    this.buffer.push(value & 0xFF);\n  }\n\n  writeU2(value) {\n    this.buffer.push((value >> 8) & 0xFF);\n    this.buffer.push(value & 0xFF);\n  }\n\n  writeU4(value) {\n    this.buffer.push((value >> 24) & 0xFF);\n    this.buffer.push((value >> 16) & 0xFF);\n    this.buffer.push((value >> 8) & 0xFF);\n    this.buffer.push(value & 0xFF);\n  }\n\n  writeBytes(bytes) {\n    for (const b of bytes) {\n      this.buffer.push(b);\n    }\n  }\n}\n```\n\n`U1`\n\n, `U2`\n\n, `U4`\n\n- 1, 2 e 4 bytes sem sinal. Tudo em big endian, que é o que a JVM espera. Repara que `writeU2`\n\ne `writeU4`\n\nusam shifts pra separar os bytes na ordem correta - byte mais significativo primeiro.\n\nO constant pool precisa de deduplicação. Se dois métodos referenciam a mesma string `\"java/lang/Object\"`\n\n, ela deve aparecer uma vez só. A gente usa um mapa pra controlar isso:\n\n``` js\naddUtf8Constant(str) {\n  const key = `utf8:${str}`;\n  if (this.constantPoolMap[key]) {\n    return this.constantPoolMap[key];\n  }\n\n  this.constantPool.push({ tag: 1, value: str });\n  const index = this.constantPool.length;\n  this.constantPoolMap[key] = index;\n  return index;\n}\n\naddClassConstant(nameIndex) {\n  const key = `class:${nameIndex}`;\n  if (this.constantPoolMap[key]) {\n    return this.constantPoolMap[key];\n  }\n\n  this.constantPool.push({ tag: 7, nameIndex });\n  const index = this.constantPool.length;\n  this.constantPoolMap[key] = index;\n  return index;\n}\n```\n\nO mesmo padrão se repete pra `addMethodrefConstant`\n\n, `addFieldrefConstant`\n\n, `addNameAndTypeConstant`\n\n. Cada tipo tem seu tag e seus campos, mas a lógica de deduplicação é a mesma.\n\nO índice retornado é a posição no array + 1, porque o constant pool da JVM é 1-indexed.\n\nPra gerar o `.class`\n\ndo nosso compilador Brainfuck, a gente precisa de várias entries no constant pool. Especificamente, pra fazer `System.out.print(char)`\n\ne `System.in.read()`\n\n, que são as operações de I/O do Brainfuck:\n\n``` js\n// System.out (pra output do brainfuck)\nconst systemClassName = this.addUtf8Constant('java/lang/System');\nconst systemClass = this.addClassConstant(systemClassName);\nconst outFieldName = this.addUtf8Constant('out');\nconst printStreamDesc = this.addUtf8Constant('Ljava/io/PrintStream;');\nconst outNaT = this.addNameAndTypeConstant(outFieldName, printStreamDesc);\nconst outFieldRef = this.addFieldrefConstant(systemClass, outNaT);\n\n// PrintStream.print(char)\nconst printStreamClassName = this.addUtf8Constant('java/io/PrintStream');\nconst printStreamClass = this.addClassConstant(printStreamClassName);\nconst printName = this.addUtf8Constant('print');\nconst printDesc = this.addUtf8Constant('(C)V');\nconst printNaT = this.addNameAndTypeConstant(printName, printDesc);\nconst printMethodRef = this.addMethodrefConstant(printStreamClass, printNaT);\n```\n\nParece bastante coisa, e é. Cada referência de método ou campo na JVM exige essa cadeia: Utf8, depois Class, depois NameAndType, depois Methodref ou Fieldref. Mas depois que você monta uma vez, o padrão fica automático.\n\nTodo `.class`\n\nprecisa de um construtor, mesmo que ele não faça nada. O construtor default em bytecode é:\n\n```\naload_0             // carrega 'this' (local_0)\ninvokespecial #X    // chama Object.<init>()V\nreturn              // retorna\n```\n\nOnde `#X`\n\né o índice do Methodref pro construtor de `Object`\n\nno constant pool.\n\nEm bytes:\n\n``` js\n// bytecode do construtor\nconst constructorCode = [\n  0x2a,       // aload_0\n  0xb7,       // invokespecial\n  ...intTo2Bytes(objectInitMethodRef),\n  0xb1        // return\n];\n```\n\nO main é onde o bytecode do Brainfuck vai ficar. A declaração dele no ClassFile:\n\n```\n// access flags: ACC_PUBLIC | ACC_STATIC\nthis.writeU2(0x0009);\n// name index: \"main\"\nthis.writeU2(mainNameIndex);\n// descriptor index: \"([Ljava/lang/String;)V\"\nthis.writeU2(mainDescIndex);\n// attributes count: 1 (o atributo Code)\nthis.writeU2(1);\n```\n\nO atributo `Code`\n\ncontém o bytecode real:\n\n```\n// atributo Code\nthis.writeU2(codeAttrNameIndex);   // nome \"Code\" no constant pool\nthis.writeU4(codeAttrLength);      // tamanho total do atributo\nthis.writeU2(4);                   // max_stack\nthis.writeU2(3);                   // max_locals (args, cells, pointer)\nthis.writeU4(codeLength);          // tamanho do bytecode\nthis.writeBytes(jvmInstructions);  // o bytecode em si\nthis.writeU2(0);                   // exception table length\nthis.writeU2(stackMapEntries);     // attributes count (0 ou 1)\n// se tiver StackMapTable, escreve aqui\n```\n\nO `max_locals`\n\né 3 porque temos: slot 0 pro `String[] args`\n\n, slot 1 pro `byte[]`\n\n(memória do Brainfuck), e slot 2 pro `int`\n\n(ponteiro). Eu descobri isso na marra - quando coloquei 1, a JVM deu `VerifyError: Local variable table overflow`\n\n.\n\nNo final, a gente junta tudo e escreve:\n\n``` js\nconst classBytes = generator.generateHelloWorldClass(\n  className,\n  ({ symbolicConstantPool }) => {\n    return brainfuckIRToJVM(ir, {\n      input: { /* refs do System.in */ },\n      output: { /* refs do System.out */ }\n    });\n  }\n);\n\nfs.writeFileSync(`${className}.class`, new Uint8Array(classBytes));\n```\n\nO callback `makeInstructions`\n\nrecebe o constant pool já montado e retorna o bytecode gerado. Essa separação permite que o gerador de ClassFile não saiba nada sobre Brainfuck - ele só sabe montar a estrutura do `.class`\n\n.\n\nDepois de gerar o `.class`\n\n, você pode inspecionar ele com `javap -v`\n\npra confirmar que a estrutura tá correta:\n\n```\njavap -v CompiledBrainfuck.class\n```\n\nIsso mostra o constant pool, os métodos, o bytecode decodificado, e os atributos. É a melhor ferramenta de debug que você vai ter nesse projeto. Quando algo der errado (e vai dar), roda o `javap`\n\ne compara com um `.class`\n\ngerado pelo `javac`\n\n.\n\nOutra ferramenta que eu usei muito: `xxd`\n\ncom offsets específicos. Quando o `javap`\n\nreclamava de algo, eu ia direto no byte:\n\n```\nxxd -s 270 -l 4 CompiledBrainfuck.class\n```\n\nIsso mostra 4 bytes a partir da posição 270. Útil pra conferir se um valor específico tá sendo escrito certo.\n\nNesse post a gente viu:\n\n`([Ljava/lang/String;)V`\n\n)`.class`\n\ndo zero em Node.jsNa [parte 3](https://codesilva.com/programacao/2026/03/16/compilando-brainfuck-pra-jvm-parte-3-gerando-bytecode), a gente vai gerar bytecode JVM a partir da IR do Brainfuck. E vai ter que lidar com a StackMapTable, que quase me fez desistir.\n\nCommits relevantes: [ 234b285 (class generator creates base structure)](https://github.com/geeksilva97/brainjuck/commit/234b285),\n\n`31dd14d`\n\n(massive refactoring in the JVM bytecode reader)`1231ffc`\n\n(beat u jvm)Por hoje é só. Abraços.", "url": "https://wpnews.pro/news/o-que-tem-em-um-class-do-java", "canonical_source": "https://dev.to/edysilva/o-que-tem-em-um-class-do-java-2agm", "published_at": "2026-08-13 01:47:10+00:00", "updated_at": "2026-08-13 02:15:09.695054+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Java", "JVM", "Node.js", "Brainfuck"], "alternates": {"html": "https://wpnews.pro/news/o-que-tem-em-um-class-do-java", "markdown": "https://wpnews.pro/news/o-que-tem-em-um-class-do-java.md", "text": "https://wpnews.pro/news/o-que-tem-em-um-class-do-java.txt", "jsonld": "https://wpnews.pro/news/o-que-tem-em-um-class-do-java.jsonld"}}