Do Código ao Bytecode — JDK, javac, JVM e o Primeiro Programa

Do Código ao Bytecode — JDK, javac, JVM e o Primeiro Programa

Instalação do JDK 25, a diferença entre JDK, JRE e JVM, o primeiro programa compilado com javac e executado com java, o bytecode visto com javap, o caminho da JVM até o JIT, os arquivos compactos do Java 25 com a classe IO e o jshell para testar ideias.
Java

• • 17 min de leitura

Instalar o Java, escrever o primeiro programa de dois jeitos diferentes e acompanhar cada passo que ele dá até aparecer na sua tela.

Na aula anterior você viu que a grande ideia do Java é compilar o código para uma máquina imaginária e executá-lo numa máquina virtual. Agora vamos tornar isso concreto. Ao fim desta aula você terá o Java instalado e terá escrito, compilado e executado seu primeiro programa. Também vai ter aberto o arquivo compilado para ver o bytecode, e vai entender o papel de cada uma das siglas que aparecem em qualquer conversa sobre Java.

Três siglas: JDK, JRE e JVM

JVM (Java Virtual Machine) é o programa que executa bytecode. Ela lê os arquivos compilados, confere se são válidos, executa as instruções e cuida da memória. Existe uma JVM diferente para cada sistema operacional e processador, mas todas executam o mesmo bytecode.

JRE (Java Runtime Environment) é a JVM mais a biblioteca padrão, ou seja, o necessário para rodar programas Java. Por muitos anos, usuários comuns instalavam só o JRE. Desde o Java 11 a Oracle não distribui mais um JRE separado (algumas distribuições, como o Temurin, ainda oferecem pacotes só de execução). Quem entrega um programa Java hoje empacota junto um ambiente de execução sob medida, como você vai ver em "Do JAR ao Executável".

JDK (Java Development Kit) é tudo isso mais as ferramentas de desenvolvimento: o compilador javac, o jshell, o javap, o jar e várias outras. É o que você, como programador, instala.

Qual JDK instalar

Aqui entra o OpenJDK da aula anterior. O código do Java é aberto e desenvolvido no projeto OpenJDK. Várias organizações pegam esse código, compilam, testam e distribuem pacotes prontos, cada uma com sua política de atualizações. Todas são o mesmo Java, e um programa que roda numa roda nas outras. As mais comuns são:

Distribuição Mantida por Observação
Eclipse Temurin Adoptium (Eclipse Foundation) Gratuita, neutra, muito usada. É a recomendação deste curso.
Oracle OpenJDK Oracle Gratuita, mas só recebe atualizações até a versão seguinte sair.
Oracle JDK Oracle Gratuita sob licença própria por um período em cada LTS. Leia os termos antes de usar em empresas.
Amazon Corretto Amazon Gratuita, usada na AWS.
Azul Zulu Azul Gratuita, com suporte pago opcional.
Microsoft Build of OpenJDK Microsoft Gratuita, usada no Azure.
Pacotes da distribuição Linux Debian, Ubuntu, Fedora... Instalação pelo gerenciador de pacotes do sistema.

A confusão de licenças mencionada na aula anterior vem daqui: em 2018 a Oracle mudou os termos do seu JDK. Isso não afeta o OpenJDK nem as outras distribuições. Se você usar Temurin, ou o pacote do seu Linux, não há nada com que se preocupar.

Instalando

Instale o JDK 25. A forma mais simples depende do sistema.

Windows. Pelo terminal (PowerShell), com o gerenciador de pacotes que já vem no Windows:

winget install EclipseAdoptium.Temurin.25.JDK

Outra opção é baixar o instalador .msi em https://adoptium.net/. Durante a instalação, marque as opções que configuram o PATH e a variável JAVA_HOME. Depois de instalar, feche e abra o terminal de novo, para que ele enxergue as variáveis novas.

macOS e Linux. A ferramenta SDKMAN instala e alterna entre versões do Java (e de outras ferramentas que vamos usar, como o Maven e o Gradle) sem mexer no sistema:

curl -s "https://get.sdkman.io" | bash
# feche e abra o terminal, depois:
sdk list java              # mostra as versões disponíveis
sdk install java 25.0.4-tem   # use o identificador de uma versão 25 que aparecer na lista, terminado em -tem

O identificador exato muda a cada atualização, por isso vale consultar a lista. O sufixo -tem indica Temurin.

Ubuntu e Debian. O pacote do próprio sistema também serve:

sudo apt install openjdk-25-jdk

Com qualquer um dos métodos, confira se deu certo:

java --version
javac --version

A resposta deve mencionar a versão 25, mais ou menos assim (os números depois do 25 e o nome da distribuição variam):

openjdk 25.0.4 2026-07-21 LTS
OpenJDK Runtime Environment Temurin-25.0.4+7 (build 25.0.4+7-LTS)
OpenJDK 64-Bit Server VM Temurin-25.0.4+7 (build 25.0.4+7-LTS, mixed mode, sharing)
javac 25.0.4

Se o terminal disser que não encontra o comando java, o problema quase sempre é o PATH: a pasta bin do JDK não está na lista de lugares onde o sistema procura programas. No Windows, reabrir o terminal costuma resolver. Se não resolver, reinstale marcando a opção de configurar o PATH.

O primeiro programa, do jeito clássico

Entre na pasta aula-02 e crie um arquivo chamado exatamente Ola.java, com este conteúdo:

public class Ola {
    public static void main(String[] args) {
        System.out.println("Olá, mundo!");
    }
}

Por enquanto não se preocupe em entender cada palavra. Todas serão explicadas ao longo do curso, e algumas só farão sentido completo na Fase 2. Basta uma leitura geral: estamos declarando uma classe chamada Ola. Dentro dela há um método chamado main, o ponto por onde a execução começa. Dentro do método há uma instrução que imprime uma linha de texto na saída padrão, System.out.

Agora compile:

javac Ola.java

Se nada aparecer, deu certo. Em programação, silêncio costuma ser boa notícia. Liste a pasta e você verá um arquivo novo:

Ola.class
Ola.java

O Ola.class é o bytecode, o resultado da compilação. Agora execute:

java Ola
Olá, mundo!

Repare que, para executar, você passou o nome da classe (Ola), e não o nome do arquivo (Ola.class). O comando java inicia a JVM e pede que ela carregue a classe Ola e chame seu método main.

Há uma regra que pega todo iniciante: uma classe pública precisa estar num arquivo com o mesmo nome dela, incluindo maiúsculas e minúsculas. Se você salvar a classe Ola num arquivo chamado Nome.java, o compilador recusa:

Nome.java:1: error: class Ola is public, should be declared in a file named Ola.java
public class Ola {
       ^
1 error

Aproveite para ler essa mensagem com calma, porque todas as mensagens do javac seguem o mesmo formato: o arquivo, o número da linha, a descrição do erro, a linha problemática e um ^ apontando o ponto exato. Faça outro teste: apague o ponto e vírgula do fim da linha do println e compile de novo.

Ola.java:3: error: ';' expected
        System.out.println("Olá, mundo!")
                                         ^
1 error

O compilador é o seu primeiro e mais paciente revisor. Criar o hábito de ler as mensagens dele, em vez de sair mudando o código no escuro, economiza horas ao longo do curso.

Dentro do .class: o bytecode

O JDK traz uma ferramenta que desmonta um arquivo .class e mostra as instruções que ele contém. Com o Ola.class compilado (e o ponto e vírgula de volta), execute:

javap -c Ola
Compiled from "Ola.java"
public class Ola {
  public Ola();
    Code:
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return

  public static void main(java.lang.String[]);
    Code:
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #13                 // String Olá, mundo!
         5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
}

Isto é bytecode, a linguagem da JVM, mostrado de forma legível. Olhe o bloco do main. São quatro instruções:

  • getstatic busca o objeto System.out, que representa a saída padrão;
  • ldc carrega a constante de texto "Olá, mundo!";
  • invokevirtual chama o método println desse objeto, passando o texto;
  • return encerra o método.

A JVM é uma máquina de pilha: cada instrução empilha valores ou consome valores da pilha. As duas primeiras instruções empilham o objeto e o texto, e a terceira consome os dois para fazer a chamada. Os números depois do # apontam para uma tabela de constantes dentro do arquivo e podem variar de um compilador para outro. O primeiro bloco, public Ola(), é um construtor que o compilador criou sozinho. Por que ele existe fica claro em "Moldes e Instâncias".

Nada disso é código de máquina de um processador real. É o mesmo arquivo em qualquer sistema. Se você copiar este Ola.class para um computador com outro sistema operacional e outro processador, e ali houver uma JVM, ele roda sem recompilar. Esse é o "escreva uma vez, rode em qualquer lugar" na prática.

Uma curiosidade da aula anterior: os quatro primeiros bytes de todo arquivo .class são CA FE BA BE, o número mágico que identifica o formato. Logo depois vem a versão do formato. Arquivos compilados pelo Java 25 têm a versão principal 69. Se você tentar rodar esse arquivo num Java mais antigo, a JVM recusa com uma mensagem sobre a versão da classe, porque ela não sabe o que versões futuras podem conter. O contrário funciona: uma JVM nova executa bytecode antigo.

O que acontece quando a JVM executa

Quando você digita java Ola, acontece em ordem aproximada o seguinte:

  1. O sistema operacional inicia a JVM, um programa nativo comum.
  2. O class loader encontra e carrega o Ola.class, e depois, sob demanda, as classes da biblioteca padrão de que ele precisa, como System e PrintStream.
  3. O verificador confere o bytecode: se as instruções são válidas, se os tipos batem, se nada tenta acessar memória fora das regras. É a herança direta da preocupação com segurança dos anos 90.
  4. A execução começa pelo main, a princípio num interpretador, que lê e executa uma instrução de bytecode por vez.
  5. O compilador JIT (just-in-time) observa o programa rodando. Quando percebe que um método é executado muitas vezes, compila esse trecho para código de máquina nativo, otimizado com base no comportamento real do programa. Na JVM padrão, a HotSpot, isso acontece em níveis: um compilador rápido (C1) entra primeiro e um compilador mais agressivo (C2) reotimiza o que continua quente.
  6. O coletor de lixo trabalha durante toda a execução, liberando a memória de objetos que ninguém mais usa.

Num programa de uma linha, quase nada disso aparece. Num servidor que roda por semanas, é o JIT que faz o Java atingir desempenho próximo ao de linguagens compiladas: ele otimiza com informações que um compilador tradicional, trabalhando antes da execução, não tem. A Fase 6 inteira é dedicada a abrir essa máquina.

Se você vem do C, a diferença está no momento da tradução. Em C, o compilador gera código de máquina para um processador específico antes da execução, e para outro processador é preciso compilar de novo. Em Java a tradução acontece em duas etapas: para bytecode na compilação e para código de máquina durante a execução. Se você vem do C#, reconheceu o modelo, que é praticamente o mesmo da CLR com sua linguagem intermediária. A aula anterior explicou por quê.

O atalho moderno

Compilar com javac e executar com java em dois passos é o fluxo clássico, e é o que ferramentas como o Maven fazem por baixo dos panos. Para programas pequenos há um atalho, disponível desde o Java 11: passar o arquivo-fonte direto para o java.

java Ola.java
Olá, mundo!

A JVM compila o arquivo em memória e o executa, sem gerar .class nenhum. É ótimo para experimentar.

O Java 25 deu um passo além e simplificou o próprio programa. Crie um arquivo Saudacao.java:

void main() {
    IO.println("Olá, mundo!");
    String nome = IO.readln("Qual é o seu nome? ");
    IO.println("Bem-vindo à JVM, " + nome + "!");
}
java Saudacao.java
Olá, mundo!
Qual é o seu nome? Marina
Bem-vindo à JVM, Marina!

Não há class, public, static nem String[] args. Isso é um arquivo de código compacto, recurso finalizado no Java 25. Quando o arquivo não declara uma classe, o compilador cria uma sozinho, e o main pode ser um método simples. A classe IO, também nova, oferece println para escrever e readln para ler uma linha digitada, sem nenhuma importação. Com isso o programa já conversa com o usuário: readln mostra a pergunta, espera a pessoa digitar e devolve o texto, que guardamos em nome. A linha com String nome usa uma variável, assunto da próxima aula. Por enquanto, basta saber que ela guarda o que foi digitado.

Isso não é outro Java, e nem um modo "de brinquedo". Se você compilar esse arquivo com javac e desmontá-lo com javap, vai encontrar uma classe Saudacao comum, com um método main. O recurso só esconde a cerimônia que um iniciante ainda não tem como entender. É por isso que ele existe: para que a primeira aula de programação não comece com "ignore estas cinco palavras".

Neste curso, os programas das primeiras aulas vão usar a forma compacta, para que você se concentre em um conceito por vez. Na Fase 2, quando as classes forem apresentadas de verdade, cada palavra da forma clássica ganha explicação, e você vai entender exatamente o que a forma compacta esconde.

jshell: o Java de bolso

Às vezes você só quer saber o resultado de uma expressão, sem criar arquivo nenhum. Para isso existe o jshell, um console interativo que acompanha o JDK desde o Java 9:

$ jshell
jshell> 2 + 3 * 4
$1 ==> 14

jshell> int x = 7;
x ==> 7

jshell> x * 6
$3 ==> 42

jshell> "Java".length()
$4 ==> 4

jshell> /exit

Cada linha é avaliada na hora, e o resultado aparece depois de ==>. Os nomes $1, $3 e $4 são variáveis que o jshell cria para guardar os resultados, caso você queira reutilizá-los. Deixe um terminal com o jshell aberto enquanto estuda as próximas aulas. Sempre que surgir uma dúvida do tipo "o que acontece se...", teste ali em dois segundos.

Onde escrever o código

Qualquer editor de texto serve, mas um editor que entende Java ajuda muito, porque aponta erros enquanto você digita e completa nomes. Duas boas opções gratuitas:

  • Visual Studio Code com o Extension Pack for Java, leve e útil para várias linguagens.
  • IntelliJ IDEA, o ambiente mais usado por profissionais Java, com uma versão gratuita.

Uma recomendação: nas primeiras aulas, compile e execute pelo terminal, mesmo usando um editor que tenha botão de "Run". O botão esconde exatamente os passos que você acabou de aprender, e entender o que o javac e o java fazem vai ser importante quando um projeto maior der um erro que o botão não explica.

Uma observação para quem usa Windows: desde o Java 18, arquivos-fonte são lidos como UTF-8 por padrão, então acentos no código funcionam. Se os acentos aparecerem trocados na saída do terminal, o problema está na configuração do terminal, e não no seu programa. O Windows Terminal, que vem nas versões atuais do Windows, costuma exibi-los corretamente.

Com o ambiente pronto, o próximo passo é o que um programa manipula: valores.

Fontes e leituras recomendadas

  • Dev.java, "Getting Started with Java": https://dev.java/learn/getting-started/. É o tutorial oficial de primeiros passos, com instalação e primeiro programa.
  • JEP 512, "Compact Source Files and Instance Main Methods": https://openjdk.org/jeps/512. A proposta que criou os arquivos compactos e a classe IO no Java 25, com a motivação pedagógica explicada pelos próprios autores.
  • JEP 330, "Launch Single-File Source-Code Programs": https://openjdk.org/jeps/330. A proposta que permitiu executar java Arquivo.java diretamente.
  • The Java Virtual Machine Specification, Java SE 25: https://docs.oracle.com/javase/specs/jvms/se25/html/index.html. A especificação da JVM. O capítulo 4 descreve o formato do arquivo .class e o capítulo 6, cada instrução de bytecode. Não é para ler de ponta a ponta, mas vale abrir para consultar as instruções que o javap mostrou.
  • Adoptium: https://adoptium.net/. O site do Temurin, com instaladores e a documentação das versões.

Exercícios

Exercício 1

Instale o JDK 25 e rode java --version e javac --version. Anote a distribuição e a versão completa que apareceram. Por que é importante que as duas versões coincidam?

Ver resposta

✓ Resposta: A saída varia conforme a instalação, por exemplo openjdk 25.0.4 e javac 25.0.4, com o nome da distribuição (Temurin, Corretto, etc.) na segunda linha do java --version. As versões precisam coincidir porque, se o javac for mais novo que o java, ele gera arquivos .class com uma versão de formato que a JVM mais antiga não aceita, e o programa falha ao rodar com um erro sobre a versão da classe. Isso acontece quando há dois JDKs instalados e o PATH aponta cada comando para um diferente. Conferir as duas versões logo no começo evita esse problema.

Exercício 2

Compile e execute o Ola.java clássico. Depois apague o Ola.class e tente rodar java Ola. O que acontece e por quê? E se você rodar java Ola.java?

Ver resposta

✓ Resposta: Sem o .class, o java Ola falha com Error: Could not find or load main class Ola, seguido de Caused by: java.lang.ClassNotFoundException: Ola. O comando recebeu o nome de uma classe e a JVM procurou o arquivo compilado correspondente, que não existe mais. Ela não compila nada por conta própria nesse modo. Já o java Ola.java funciona, porque recebeu um arquivo-fonte: a JVM compila em memória e executa, sem gravar nenhum .class no disco.

Exercício 3

Salve a classe Ola num arquivo chamado ola.java, com a letra inicial minúscula, e compile. O que acontece? O resultado pode ser diferente entre Windows e Linux?

Ver resposta

✓ Resposta: O nome da classe pública (Ola) precisa coincidir exatamente com o nome do arquivo, incluindo maiúsculas. No Linux, o javac recusa com class Ola is public, should be declared in a file named Ola.java. No Windows e no macOS, cujos sistemas de arquivos por padrão não diferenciam maiúsculas de minúsculas, o comportamento pode ser menos previsível, e o erro às vezes só aparece quando o projeto é levado para um servidor Linux. A lição é tratar os nomes de arquivos Java como sensíveis a maiúsculas em qualquer sistema e sempre copiar o nome da classe letra por letra.

Exercício 4

Escreva uma versão clássica do programa com dois println, um com "Primeira linha" e outro com "Segunda linha". Antes de rodar o javap -c, preveja quantas instruções o main terá e quais serão. Depois confira.

Ver resposta

✓ Resposta: Cada println gera o mesmo trio de instruções (getstatic para buscar System.out, ldc para carregar o texto e invokevirtual para chamar o método), e há um return no fim, ou seja, sete instruções:

 0: getstatic     #7    // Field java/lang/System.out:Ljava/io/PrintStream;
 3: ldc           #13   // String Primeira linha
 5: invokevirtual #15   // Method java/io/PrintStream.println:(Ljava/lang/String;)V
 8: getstatic     #7    // Field java/lang/System.out:Ljava/io/PrintStream;
11: ldc           #21   // String Segunda linha
13: invokevirtual #15   // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: return

Repare em duas coisas. O System.out é buscado de novo antes do segundo println: o bytecode não "lembra" o que já buscou, porque a pilha foi consumida pela chamada anterior. E os números à esquerda não são sequenciais porque indicam a posição de cada instrução em bytes: getstatic ocupa três bytes, ldc dois e invokevirtual três.

Exercício 5

Escreva um arquivo compacto Cidade.java que pergunte o nome e a cidade da pessoa e responda com uma frase usando as duas informações. Execute com java Cidade.java.

Ver resposta

✓ Resposta: Uma solução:

void main() {
    String nome = IO.readln("Seu nome: ");
    String cidade = IO.readln("Sua cidade: ");
    IO.println(nome + ", de " + cidade + ", está pronto para a jornada.");
}

Executando e digitando "Ana" e "Recife":

Seu nome: Ana
Sua cidade: Recife
Ana, de Recife, está pronto para a jornada.

Cada IO.readln mostra a pergunta, espera a pessoa digitar e apertar Enter, e devolve o texto digitado, que fica guardado numa variável. O operador + entre textos os junta em um só. As variáveis, os tipos como String e o + com textos são os assuntos das duas próximas aulas, "Cada Valor em sua Caixa" e "Operadores, Texto e Conversa com o Usuário".

Comentários

Mais em Java

Moldes e Instâncias — Classes, Objetos e Construtores
Moldes e Instâncias — Classes, Objetos e Construtores

Classes e objetos em Java: campos, new e referências, o heap, null e a…

Escreva uma Vez para Todo Tipo — Genéricos
Escreva uma Vez para Todo Tipo — Genéricos

Genéricos em Java: classes, records e métodos genéricos, o operador diamante…

Fazendo Duas Coisas ao Mesmo Tempo — Threads
Fazendo Duas Coisas ao Mesmo Tempo — Threads

O que é uma thread e como criar, esperar e interromper várias delas: start e…