# O que tem em um .class do Java?

> Source: <https://dev.to/edysilva/o-que-tem-em-um-class-do-java-2agm>
> Published: 2026-08-13 01:47:10+00:00

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.

Quando você roda `javac Hello.java`

, o compilador gera um `Hello.class`

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

Nesse post, a gente vai abrir um `.class`

com `xxd`

, entender cada byte, e construir um gerador que monta essa estrutura do zero em Node.js.

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

Qual a diferença? Imagina o número 30.000 (em hex: `0x7530`

). Ele ocupa 2 bytes:

| Formato | Byte 1 | Byte 2 |
|---|---|---|
| Big endian | `0x75` |
`0x30` |
| Little endian | `0x30` |
`0x75` |

Big endian coloca o byte mais significativo primeiro. Little endian coloca o menos significativo primeiro.

No 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:

```
function intTo2Bytes(num) {
  return [(num >> 8) & 0xFF, num & 0xFF];
}
```

O `>> 8`

desloca 8 bits pra direita, pegando o byte alto. O `& 0xFF`

mascara o byte baixo. Pra 4 bytes, a lógica é a mesma mas com mais shifts.

Se você errar a ordem dos bytes, a JVM vai ler valores completamente errados. Um `0x7530`

(30.000) vira `0x3075`

(12.405) se os bytes ficarem invertidos. Então se o seu `.class`

gerado dá erros estranhos no `constant pool`

, confere se você tá escrevendo big endian.

Vamos criar o programa Java mais simples possível:

```
public class Hello {
  public static void main(String[] args) {
    return;
  }
}
```

Compila com `javac Hello.java`

e abre o binário com `xxd Hello.class`

:

```
00000000: cafe babe 0000 0034 000d 0a00 0200 0307  .......4........
00000010: 0004 0c00 0500 0601 0010 6a61 7661 2f6c  ..........java/l
00000020: 616e 672f 4f62 6a65 6374 0100 063c 696e  ang/Object...<in
00000030: 6974 3e01 0003 2829 5607 0008 0100 0548  it>...()V......H
00000040: 656c 6c6f 0100 0443 6f64 6501 0004 6d61  ello...Code...ma
00000050: 696e 0100 1628 5b4c 6a61 7661 2f6c 616e  in...([Ljava/lan
00000060: 672f 5374 7269 6e67 3b29 5600 2100 0700  g/String;)V.!...
```

Parece caótico, mas tem uma estrutura definida. Vamos ler byte por byte.

Os primeiros 4 bytes de todo `.class`

são sempre `CA FE BA BE`

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

```
cafe babe
```

A história diz que os criadores do Java escolheram `CAFEBABE`

porque lembravam de um café que frequentavam. Verdade ou não, é memorável.

Os 4 bytes seguintes indicam a versão do formato:

```
0000 0034
```

`0000`

- minor version: 0`0034`

- major version: 52 (decimal)Major version 52 é Java 8. A versão é importante porque determina quais features o `.class`

pode usar. A partir da versão 50 (Java 6), por exemplo, a `StackMapTable`

é obrigatória - mas esse é assunto da [parte 3](https://codesilva.com/programacao/2026/03/16/compilando-brainfuck-pra-jvm-parte-3-gerando-bytecode).

Aqui é onde mora a complexidade. O `constant pool`

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

Os próximos 2 bytes indicam o tamanho:

```
000d
```

`0x000d`

= 13. Mas atenção: o constant pool usa indexação começando em 1, e o count é sempre `n + 1`

. Então temos 12 entries (indices 1 a 12).

Cada entry começa com um byte de tag que indica o tipo:

| Tag | Tipo | O que armazena |
|---|---|---|
| 1 | CONSTANT_Utf8 | String UTF-8 (nomes, descritores) |
| 7 | CONSTANT_Class | Referência a uma classe (aponta pra um Utf8) |
| 9 | CONSTANT_Fieldref | Referência a um campo (classe + nome/tipo) |
| 10 | CONSTANT_Methodref | Referência a um método (classe + nome/tipo) |
| 12 | CONSTANT_NameAndType | Par nome + descritor de tipo |

Vou destrinchar as primeiras entries do nosso `Hello.class`

:

**Entry 1** (começa no byte 0x0a):

```
0a 00 02 00 03
```

`0x0a`

= 10 = CONSTANT_Methodref`0x0002`

= 2`0x0003`

= 3Isso é uma referência ao método `Object.<init>()V`

- o construtor da classe pai.

**Entry 2**:

```
07 00 04
```

`0x07`

= 7 = CONSTANT_Class`0x0004`

= 4 (aponta pra um Utf8 com o nome da classe)**Entry 4**:

```
01 0010 6a617661 2f6c616e 672f4f62 6a656374
```

`0x01`

= 1 = CONSTANT_Utf8`0x0010`

= 16 bytes`java/lang/Object`

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

Uma coisa que confunde no começo: a JVM usa uma notação própria pra tipos. Não é `void main(String[] args)`

, é `([Ljava/lang/String;)V`

.

As regras:

| Tipo Java | Descritor JVM |
|---|---|
`int` |
`I` |
`byte` |
`B` |
`char` |
`C` |
`void` |
`V` |
`String` |
`Ljava/lang/String;` |
`int[]` |
`[I` |
`byte[]` |
`[B` |

Pra métodos, o formato é `(parâmetros)retorno`

. Então:

| Java | Descritor JVM |
|---|---|
`void main(String[] args)` |
`([Ljava/lang/String;)V` |
`void print(char c)` |
`(C)V` |
`int read()` |
`()I` |

Esses descritores aparecem no constant pool e o bytecode referencia eles pelo índice.

O resto do ClassFile segue:

```
0021              - Access flags (ACC_PUBLIC | ACC_SUPER)
0007              - This class (índice no constant pool)
0002              - Super class (java/lang/Object)
0000              - Interfaces count: 0
0000              - Fields count: 0
0002              - Methods count: 2
```

Depois vêm os métodos (cada um com seus atributos de código) e por fim os atributos da classe.

Uma coisa que eu não esperava: mesmo o programa mais simples tem **2 métodos**. O `main`

que a gente escreveu e o construtor `<init>`

que o Java gera automaticamente. No nosso gerador, a gente também precisa criar esse construtor.

Agora que a gente entende a estrutura, vamos construir um gerador em Node.js. A ideia é montar o `.class`

byte por byte num buffer.

Primeiro, as primitivas de escrita:

```
class ClassFileGenerator {
  constructor() {
    this.buffer = [];
    this.constantPool = [];
    this.constantPoolMap = {};
  }

  writeU1(value) {
    this.buffer.push(value & 0xFF);
  }

  writeU2(value) {
    this.buffer.push((value >> 8) & 0xFF);
    this.buffer.push(value & 0xFF);
  }

  writeU4(value) {
    this.buffer.push((value >> 24) & 0xFF);
    this.buffer.push((value >> 16) & 0xFF);
    this.buffer.push((value >> 8) & 0xFF);
    this.buffer.push(value & 0xFF);
  }

  writeBytes(bytes) {
    for (const b of bytes) {
      this.buffer.push(b);
    }
  }
}
```

`U1`

, `U2`

, `U4`

- 1, 2 e 4 bytes sem sinal. Tudo em big endian, que é o que a JVM espera. Repara que `writeU2`

e `writeU4`

usam shifts pra separar os bytes na ordem correta - byte mais significativo primeiro.

O constant pool precisa de deduplicação. Se dois métodos referenciam a mesma string `"java/lang/Object"`

, ela deve aparecer uma vez só. A gente usa um mapa pra controlar isso:

``` js
addUtf8Constant(str) {
  const key = `utf8:${str}`;
  if (this.constantPoolMap[key]) {
    return this.constantPoolMap[key];
  }

  this.constantPool.push({ tag: 1, value: str });
  const index = this.constantPool.length;
  this.constantPoolMap[key] = index;
  return index;
}

addClassConstant(nameIndex) {
  const key = `class:${nameIndex}`;
  if (this.constantPoolMap[key]) {
    return this.constantPoolMap[key];
  }

  this.constantPool.push({ tag: 7, nameIndex });
  const index = this.constantPool.length;
  this.constantPoolMap[key] = index;
  return index;
}
```

O mesmo padrão se repete pra `addMethodrefConstant`

, `addFieldrefConstant`

, `addNameAndTypeConstant`

. Cada tipo tem seu tag e seus campos, mas a lógica de deduplicação é a mesma.

O índice retornado é a posição no array + 1, porque o constant pool da JVM é 1-indexed.

Pra gerar o `.class`

do nosso compilador Brainfuck, a gente precisa de várias entries no constant pool. Especificamente, pra fazer `System.out.print(char)`

e `System.in.read()`

, que são as operações de I/O do Brainfuck:

``` js
// System.out (pra output do brainfuck)
const systemClassName = this.addUtf8Constant('java/lang/System');
const systemClass = this.addClassConstant(systemClassName);
const outFieldName = this.addUtf8Constant('out');
const printStreamDesc = this.addUtf8Constant('Ljava/io/PrintStream;');
const outNaT = this.addNameAndTypeConstant(outFieldName, printStreamDesc);
const outFieldRef = this.addFieldrefConstant(systemClass, outNaT);

// PrintStream.print(char)
const printStreamClassName = this.addUtf8Constant('java/io/PrintStream');
const printStreamClass = this.addClassConstant(printStreamClassName);
const printName = this.addUtf8Constant('print');
const printDesc = this.addUtf8Constant('(C)V');
const printNaT = this.addNameAndTypeConstant(printName, printDesc);
const printMethodRef = this.addMethodrefConstant(printStreamClass, printNaT);
```

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

Todo `.class`

precisa de um construtor, mesmo que ele não faça nada. O construtor default em bytecode é:

```
aload_0             // carrega 'this' (local_0)
invokespecial #X    // chama Object.<init>()V
return              // retorna
```

Onde `#X`

é o índice do Methodref pro construtor de `Object`

no constant pool.

Em bytes:

``` js
// bytecode do construtor
const constructorCode = [
  0x2a,       // aload_0
  0xb7,       // invokespecial
  ...intTo2Bytes(objectInitMethodRef),
  0xb1        // return
];
```

O main é onde o bytecode do Brainfuck vai ficar. A declaração dele no ClassFile:

```
// access flags: ACC_PUBLIC | ACC_STATIC
this.writeU2(0x0009);
// name index: "main"
this.writeU2(mainNameIndex);
// descriptor index: "([Ljava/lang/String;)V"
this.writeU2(mainDescIndex);
// attributes count: 1 (o atributo Code)
this.writeU2(1);
```

O atributo `Code`

contém o bytecode real:

```
// atributo Code
this.writeU2(codeAttrNameIndex);   // nome "Code" no constant pool
this.writeU4(codeAttrLength);      // tamanho total do atributo
this.writeU2(4);                   // max_stack
this.writeU2(3);                   // max_locals (args, cells, pointer)
this.writeU4(codeLength);          // tamanho do bytecode
this.writeBytes(jvmInstructions);  // o bytecode em si
this.writeU2(0);                   // exception table length
this.writeU2(stackMapEntries);     // attributes count (0 ou 1)
// se tiver StackMapTable, escreve aqui
```

O `max_locals`

é 3 porque temos: slot 0 pro `String[] args`

, slot 1 pro `byte[]`

(memória do Brainfuck), e slot 2 pro `int`

(ponteiro). Eu descobri isso na marra - quando coloquei 1, a JVM deu `VerifyError: Local variable table overflow`

.

No final, a gente junta tudo e escreve:

``` js
const classBytes = generator.generateHelloWorldClass(
  className,
  ({ symbolicConstantPool }) => {
    return brainfuckIRToJVM(ir, {
      input: { /* refs do System.in */ },
      output: { /* refs do System.out */ }
    });
  }
);

fs.writeFileSync(`${className}.class`, new Uint8Array(classBytes));
```

O callback `makeInstructions`

recebe 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`

.

Depois de gerar o `.class`

, você pode inspecionar ele com `javap -v`

pra confirmar que a estrutura tá correta:

```
javap -v CompiledBrainfuck.class
```

Isso 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`

e compara com um `.class`

gerado pelo `javac`

.

Outra ferramenta que eu usei muito: `xxd`

com offsets específicos. Quando o `javap`

reclamava de algo, eu ia direto no byte:

```
xxd -s 270 -l 4 CompiledBrainfuck.class
```

Isso mostra 4 bytes a partir da posição 270. Útil pra conferir se um valor específico tá sendo escrito certo.

Nesse post a gente viu:

`([Ljava/lang/String;)V`

)`.class`

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

Commits relevantes: [ 234b285 (class generator creates base structure)](https://github.com/geeksilva97/brainjuck/commit/234b285),

`31dd14d`

(massive refactoring in the JVM bytecode reader)`1231ffc`

(beat u jvm)Por hoje é só. Abraços.
