Máquina virtual Java () é um programa que carrega e executa os aplicativos Java, convertendo os bytecodes em código executável de máquina. A JVM é responsável pelo gerenciamento dos aplicativos, à medida que são executados.

Graças à máquina virtual Java, os programas escritos em Java podem funcionar em qualquer plataforma de hardware e software que possua uma versão da JVM, tornando assim essas aplicações independentes da plataforma onde funcionam.

Especificação da JVM A máquina virtual Java é um computador abstrato (virtual) definido por uma especificação. Ela é parte do Ambiente de Execução Java (JRE). O algoritmo de coleta de lixo (garbage collection) utilizado e qualquer otimização interna das instruções da máquina virtual Java (sua tradução para linguagem de máquina) não são especificados. O principal motivo para essa omissão é não restringir desnecessariamente os implementadores. Qualquer aplicativo Java pode ser executado apenas dentro de alguma implementação concreta da especificação abstrata da máquina virtual Java.Bill Venners, Inside the Java Virtual Machine Capítulo 5, arquivado em 25 de janeiro de 2021 no Wayback Machine.

Começando com o Java Platform, Standard Edition (J2SE) 5.0, as mudanças na especificação da JVM têm sido desenvolvidas sob o Java Community Process como JSR 924. A partir de 2006, as mudanças na especificação para suportar alterações propostas ao formato de arquivo class (JSR 202) estão sendo realizadas como uma versão de manutenção da JSR 924. A especificação para a JVM foi publicada como o livro azul (blue book),The Java Virtual Machine Specification (a primeira e a segunda edições também estão disponíveis online), arquivado em 9 de julho de 2008 no Wayback Machine. cujo prefácio afirma:

A máquina virtual Java mais comumente utilizada é a HotSpot da Oracle.

A Oracle detém a marca registrada Java e pode permitir seu uso para certificar suítes de implementação como totalmente compatíveis com a especificação da Oracle.

Coletores de lixo

Os coletores de lixo disponíveis na máquina virtual (JVM) do OpenJDK do Java incluem: Serial Parallel CMS (Concurrent Mark Sweep) G1 (Garbage-First) ZGC (Z Garbage Collector) Epsilon Shenandoah GenZGC (Generational ZGC) GenShen (Generational Shenandoah) IBM Metronome (apenas no OpenJDK da IBM) SAP (apenas no OpenJDK da SAP) Azul C4 (Continuously Concurrent Compacting Collector) (apenas no OpenJDK da Azul Systems)

+ Versões do Java e seus Coletores de Lixo Versão GC Padrão GCs Disponíveis 6u14 Serial /Parallel (MP) Serial, Parallel, CMS, G1 (E) 7u4–8 Serial, Parallel, CMS, G1 9–10 G1 11 Serial, Parallel, CMS, G1, Epsilon (E), ZGC (E) 12–13 Serial, Parallel, CMS, G1, Epsilon (E), ZGC (E), Shenandoah (E) 14 Serial, Parallel, G1, Epsilon (E), ZGC (E), Shenandoah (E) 15–20 Serial, Parallel, G1, Epsilon (E), ZGC, Shenandoah 21–22 Serial, Parallel, G1, Epsilon (E), ZGC, Shenandoah, GenZGC (E) 23 Serial, Parallel, G1, Epsilon (E), ZGC, Shenandoah, GenZGC (ZGC padrão) 24 Serial, Parallel, G1, Epsilon (E), Shenandoah, GenZGC, GenShen (E) 25 Serial, Parallel, G1, Epsilon (E), Shenandoah, GenZGC, GenShen (E) = experimental

A filosofia de design do Java gira em torno da suposição de um coletor de lixo. Ao contrário de linguagens como C++ e Rust, o gerenciamento determinístico de memória através de uma palavra-chave delete (como no C++) não é possível. Mesmo introduzir tal recurso não seria viável, devido à falta de propriedade (ownership), exceto pelo uso de ou através da para alocar/desalocar memória fora do heap Java. Como o Java utiliza principalmente a alocação baseada em heap, os objetos são armazenados como referências, e a exclusão manual resultaria em ponteiros pendentes.

Foo a = new Foo(); Foo b = a;

// Em Java, não existe 'delete a' // Se existisse, 'b' se tornaria um ponteiro pendente System.out.println(b);

Arquitetura da máquina virtual

A JVM opera em tipos específicos de dados, conforme especificado nas especificações da Máquina Virtual Java. Os tipos de dados podem ser divididos em tipos primitivos (valores inteiros e de ponto flutuante) e tipos de referência. Os tipos long e double, que possuem 64 bits, são suportados nativamente, mas consomem duas unidades de armazenamento nas variáveis locais de um quadro (frame) ou na pilha de operandos, uma vez que cada unidade possui 32 bits. Os tipos boolean, byte, short e char são todos estendidos por sinal (exceto o char, que é estendido por zero) e operados como inteiros de 32 bits, da mesma forma que os tipos int. Os tipos menores possuem apenas algumas instruções específicas para carregamento, armazenamento e conversão de tipo. O boolean é operado como valores byte de 8 bits, com o 0 representando false e o 1 representando true. (Embora o boolean tenha sido tratado como um tipo desde que The Java Virtual Machine Specification, Second Edition esclareceu essa questão, no código compilado e executado há pouca diferença entre um boolean e um byte, exceto pela codificação de nomes (name mangling) nas assinaturas de métodos e no tipo de arranjos (arrays) booleanos. Os booleans em assinaturas de métodos são codificados como Z, enquanto os bytes são codificados como B. Arranjos booleanos carregam o tipo boolean[], mas usam 8 bits por elemento, e a JVM não possui capacidade embutida para empacotar booleanos em um arranjo de bits, portanto, exceto pelo tipo, eles funcionam e se comportam da mesma forma que arranjos de byte. Em todos os outros usos, o tipo boolean é efetivamente desconhecido para a JVM, já que todas as instruções para operar em booleanos também são usadas para operar em bytes.)

A JVM possui um heap com coleta de lixo para armazenar objetos e arranjos. O código, as constantes e outros dados da classe são armazenados na "área de métodos". A área de métodos é logicamente parte do heap, mas as implementações podem tratar a área de métodos separadamente do heap e, por exemplo, não realizar a coleta de lixo nela. Cada thread da JVM também tem sua própria pilha de chamadas (chamada de "pilha da Máquina Virtual Java" por clareza), que armazena quadros (frames). Um novo quadro é criado cada vez que um método é chamado, e o quadro é destruído quando esse método é encerrado.

Cada quadro fornece uma "pilha de operandos" e um arranjo de "variáveis locais". A pilha de operandos é usada para operandos executarem cálculos e para receber o valor de retorno de um método chamado, enquanto as variáveis locais servem ao mesmo propósito que os registradores e também são usadas para passar argumentos de métodos. Dessa forma, a JVM é ao mesmo tempo uma máquina de pilha e uma máquina de registradores. Na prática, a HotSpot elimina todas as pilhas, exceto a pilha de chamadas/thread nativa, mesmo ao rodar no modo Interpretado, já que o seu Interpretador de Templates atua tecnicamente como um compilador.

A JVM usa referências e índices de pilha/arranjos para endereçar dados; ela não utiliza o endereçamento por bytes como a maioria das máquinas físicas faz, portanto não se enquadra perfeitamente na categorização usual de máquinas de 32 bits ou 64 bits. Em um sentido, ela poderia ser classificada como uma máquina de 32 bits, já que esse é o tamanho do maior valor que ela armazena nativamente: um número inteiro de 32 bits, um valor de ponto flutuante ou uma referência de 32 bits. Como uma referência tem 32 bits, cada programa é limitado a no máximo 2 referências únicas e, portanto, no máximo 2 objetos. No entanto, cada objeto pode ter mais de um byte e ser potencialmente muito grande; o maior objeto possível é um arranjo de long com tamanho de 2 - 1, o que consumiria 16 GiB de memória, e poderia haver potencialmente 2 destes se houvesse memória suficiente disponível. Isso resulta em limites superiores mais comparáveis a uma máquina típica de 64 bits endereçável por byte. Uma implementação da JVM pode ser projetada para rodar em um processador que usa nativamente qualquer largura de bits, desde que implemente corretamente a matemática inteira (8, 16, 32 e 64 bits) e de ponto flutuante (32 e 64 bits) exigida pela JVM. Dependendo do método usado para implementar as referências (ponteiros nativos, ponteiros comprimidos ou uma tabela de indireção), isso pode limitar o número de objetos a algo menor do que o máximo teórico. Uma implementação da JVM em uma plataforma de 64 bits tem acesso a um espaço de endereçamento muito maior do que uma em plataforma de 32 bits, o que permite um tamanho de heap muito maior e um aumento no número máximo de threads, algo necessário para certos tipos de aplicações grandes; no entanto, pode haver uma queda de desempenho ao utilizar uma implementação de 64 bits em comparação com uma de 32 bits.

Linguagens da JVM

Uma linguagem da JVM é qualquer linguagem com funções que podem ser expressas em um arquivo class válido, o qual pode ser hospedado por uma JVM. Um arquivo class contém instruções da JVM (Bytecode Java), uma tabela de símbolos e outras informações complementares. O formato de arquivo class é o formato binário independente de hardware e sistema operacional usado para representar classes e interfaces compiladas.

Existem diversas linguagens da JVM, tanto linguagens estabelecidas e mais antigas portadas para a JVM, quanto linguagens muito mais recentes criadas do zero. O JRuby e o Jython talvez sejam os ports mais conhecidos de linguagens antigas, ou seja, Ruby e Python, respectivamente. Entre as novas linguagens criadas do zero para compilar em bytecode Java, Clojure, Apache Groovy, Scala e Kotlin são as mais populares. Uma característica notável das linguagens da JVM é sua interoperabilidade de linguagens; elas são compatíveis umas com as outras, para que, por exemplo, bibliotecas Scala possam ser usadas com programas Java e vice-versa.

A JVM do Java 7 implementa a JSR 292: Supporting Dynamically Typed Languages na Plataforma Java, um novo recurso que suporta linguagens dinamicamente tipadas na JVM. Esse recurso é desenvolvido dentro do projeto Máquina Da Vinci (Da Vinci Machine), cuja missão é estender a JVM para suportar outras linguagens além do Java.

Carregador de classes

Uma das unidades organizacionais do código de bytes (bytecode) da JVM é a classe. Uma implementação de carregador de classes (class loader) deve ser capaz de reconhecer e carregar qualquer item que esteja em conformidade com o formato de arquivo class do Java. Qualquer implementação é livre para reconhecer outros formatos binários além de arquivos class, mas deve obrigatoriamente reconhecer os arquivos class.

O carregador de classes executa três atividades básicas nesta ordem estrita:

Carregamento (Loading): localiza e importa os dados binários para um tipo. Vinculação (Linking): executa a verificação, preparação e (opcionalmente) resolução. Verificação: assegura a correção do tipo importado. Preparação: aloca memória para variáveis de classe e inicializa a memória com valores padrão. Resolução: transforma referências simbólicas do tipo em referências diretas. Inicialização (Initialization): invoca o código Java que inicializa as variáveis de classe em seus devidos valores iniciais.

Em geral, existem três tipos de carregadores de classe: carregador de classes de inicialização (bootstrap class loader), carregador de classes de extensão (extension class loader) e carregador de classes de sistema / aplicativo (System / Application class loader).

Cada implementação da máquina virtual Java deve possuir um carregador de classes de inicialização capaz de carregar classes confiáveis, bem como um carregador de classes de extensão ou de aplicativo. A especificação da máquina virtual Java não estipula como um carregador de classes deve localizar as classes.

Instruções de bytecode

A JVM tem instruções para os seguintes grupos de tarefas: