Máquina Virtual JAVA
Uma máquina virtual Java (JVM) é uma máquina virtual que permite que um computador execute programas Java, bem como programas escritos em outras linguagens que também são compiladas para Java bytecode. A JVM é detalhada por uma especificação que descreve formalmente o que é necessário em uma implementação de JVM. Ter uma especificação garante a interoperabilidade de programas Java em diferentes implementações para que os autores de programas que usam o Java Development Kit (JDK) não precisem se preocupar com idiossincrasias da plataforma de hardware subjacente.
A implementação de referência JVM é desenvolvida pelo projeto OpenJDK como código-fonte aberto e inclui um compilador JIT chamado HotSpot. As versões Java com suporte comercial disponíveis na Oracle são baseadas no tempo de execução OpenJDK. Eclipse OpenJ9 é outra JVM de software livre para OpenJDK.
Especificação da JVM
A máquina virtual Java é um computador abstrato (virtual) definido por uma especificação. Faz parte do Java Runtime Environment. O algoritmo de coleta de lixo usado e qualquer otimização interna das instruções da máquina virtual Java (sua tradução em código de máquina) não são especificados. A principal razão para esta 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.
Começando com Java Platform, Standard Edition (J2SE) 5.0, as mudanças na especificação JVM foram desenvolvidas sob o Java Community Process como JSR 924. A partir de 2006, mudanças na especificação para suportar mudanças propostas no formato de arquivo de classe (JSR 202) estão sendo feitas como uma versão de manutenção da JSR 924. A especificação para a JVM foi publicada como o livro azul, cujo prefácio declara:
Pretendemos que esta especificação deve documentar suficientemente a Máquina Virtual Java para tornar possíveis implementações de salas limpas compatíveis. A Oracle fornece testes que verificam o funcionamento adequado das implementações da Máquina Virtual Java.
Uma das JVMs da Oracle chama-se HotSpot; o outro, herdado da BEA Systems, é o JRockit. A Oracle é proprietária da marca registrada Java e pode permitir seu uso para certificar suítes de implementação como totalmente compatíveis com as especificações da Oracle.
Carregador de classe
Uma das unidades organizacionais do código de bytes JVM é uma classe. Uma implementação de carregador de classe deve ser capaz de reconhecer e carregar qualquer coisa que esteja em conformidade com o formato de arquivo de classe Java. Qualquer implementação é livre para reconhecer outras formas binárias além dos arquivos class, mas deve reconhecer arquivos class.
O carregador de classe executa três atividades básicas nesta ordem estrita:
- Carregamento: encontra e importa os dados binários para um tipo
- Linking: executa a resolução de verificação, preparação e (opcionalmente)
- Verificação: garante a correção do tipo importado
- Preparação: aloca memória para variáveis de classe e inicializa a memória para valores padrão
- Resolução: transforma referências simbólicas do tipo em referências diretas.
- Inicialização: invoca o código Java que inicializa variáveis de classe para seus valores iniciais adequados.
Em geral, existem três tipos de carregador de classes: carregador de classes bootstrap, carregador de classes de extensão e carregador de classes de sistema/aplicativo.
Toda implementação de Java virtual machine deve ter um carregador de classes de bootstrap que seja capaz de carregar classes confiáveis, bem como um carregador de classes de extensão ou carregador de classes de aplicativo. A especificação da máquina virtual Java não especifica como um carregador de classes deve localizar as classes.
Arquitetura de máquina virtual
A JVM opera em tipos específicos de dados, conforme especificado nas especificações da Java Virtual Machine. Os tipos de dados podem ser divididos em tipos primitivos (inteiros, ponto flutuante, longo etc.) e tipos de referência. As JVM anteriores eram apenas máquinas de 32 bits. Os tipos long
e double
, que são de 64 bits, são suportados nativamente, mas consomem duas unidades de armazenamento nas variáveis locais de um quadro ou na pilha de operandos, pois cada unidade é de 32 bits. Os tipos boolean
, byte
, short
e char
são todos com sinal estendido (exceto char
que é estendido para zero) e operado como inteiros de 32 bits, o mesmo que os tipos int
. Os tipos menores têm apenas algumas instruções específicas de tipo para carregamento, armazenamento e conversão de tipo. boolean
é operado como valores byte
de 8 bits, com 0 representando false
e 1 representando true
. (Embora boolean
tenha sido tratado como um tipo desde que The Java Virtual Machine Specification, Second Edition esclareceu esse problema, no código compilado e executado há pouca diferença entre um boolean
e um byte
, exceto para alteração de nome em assinaturas de método e o tipo de matrizes booleanas. boolean
s em assinaturas de método são mutilados como Z
enquanto byte
s são mutilados como B
. Arrays booleanos carregam o tipo boolean[]
mas usam 8 bits por elemento, e a JVM tem nenhum recurso interno para empacotar booleanos em uma matriz de bits, portanto, exceto pelo tipo, eles executam e se comportam da mesma forma que as matrizes de byte
. Em todos os outros usos, o tipo boolean
é efetivamente desconhecido para a JVM, pois todas as instruções para operar em booleanos também são usadas para operar em byte
s.) No entanto, as versões mais recentes da JVM (OpenJDK HotSpot JVM) suportam 64 bits, portanto, você pode JVM de 32 bits/64 bits em um 64-bit bit SO. A principal vantagem de executar o Java em um ambiente de 64 bits é o maior espaço de endereço. Isso permite um tamanho de heap Java muito maior e um número máximo aumentado de Java Threads, que é necessário para certos tipos de aplicativos grandes; no entanto, há um impacto no desempenho ao usar JVM de 64 bits em comparação com JVM de 32 bits.
A JVM tem um heap de coleta de lixo para armazenar objetos e arrays. Código, constantes e outros dados de classe são armazenados na "área de método". A área do método é logicamente parte do heap, mas as implementações podem tratar a área do método separadamente do heap e, por exemplo, podem não fazer a coleta de lixo. Cada encadeamento JVM também possui sua própria pilha de chamadas (chamada de "pilha de máquina virtual Java" para maior clareza), que armazena quadros. 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 uma matriz de "variáveis locais". A pilha de operandos é usada para operandos para 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étodo. Assim, a JVM é uma máquina de pilha e uma máquina de registro. Na prática, o HotSpot elimina totalmente todas as pilhas além da pilha nativa de thread/chamada, mesmo quando executado no modo Interpretado, pois seu Interpretador de modelo funciona tecnicamente como um compilador.
Instruções de bytecode
A JVM possui instruções para os seguintes grupos de tarefas:
- Carga e loja
- Aritmética
- Conversão de tipo
- Criação e manipulação de objetos
- Gestão de pilha de operando (push / pop)
- Transferência de controle (branching)
- Método invocação e retorno
- Excepções de lançamento
- Confiança baseada em monitor
O objetivo é a compatibilidade binária. Cada sistema operacional de host específico precisa de sua própria implementação da JVM e tempo de execução. Essas JVMs interpretam o bytecode semanticamente da mesma maneira, mas a implementação real pode ser diferente. Mais complexo do que apenas emular bytecode é implementar de forma compatível e eficiente a API central Java que deve ser mapeada para cada sistema operacional host.
Essas instruções operam em um conjunto de tipos de dados abstratos em vez dos tipos de dados nativos de qualquer arquitetura específica de conjunto de instruções.
Idiomas JVM
Uma linguagem JVM é qualquer linguagem com funcionalidade que pode ser expressa em termos de um arquivo de classe válido que pode ser hospedado pela Java Virtual Machine. Um arquivo de classe contém instruções da Java Virtual Machine (código de byte Java) e uma tabela de símbolos, bem como outras informações auxiliares. O formato de arquivo de classe é o formato binário independente de hardware e sistema operacional usado para representar classes e interfaces compiladas.
Existem vários idiomas JVM, tanto idiomas antigos portados para JVM quanto idiomas completamente novos. JRuby e Jython são talvez as portas mais conhecidas das linguagens existentes, ou seja, Ruby e Python, respectivamente. Das novas linguagens que foram criadas do zero para compilar em bytecode Java, Clojure, Apache Groovy, Scala e Kotlin podem ser as mais populares. Uma característica notável das linguagens JVM é que elas são compatíveis entre si, de modo que, por exemplo, as bibliotecas Scala podem ser usadas com programas Java e vice-versa.
Java 7 JVM implementa JSR 292: Suporte a linguagens de tipagem dinâmica na plataforma Java, um novo recurso que suporta linguagens de tipagem dinâmica na JVM. Esta funcionalidade é desenvolvida dentro do projeto Da Vinci Machine cuja missão é estender a JVM para que ela suporte outras linguagens além de Java.
Verificador de bytecode
Uma filosofia básica de Java é que é inerentemente seguro do ponto de vista de que nenhum programa de usuário pode travar a máquina host ou interferir de forma inadequada em outras operações na máquina host e que é possível proteger certos métodos e estruturas de dados pertencer ao código confiável de acesso ou corrupção por código não confiável em execução na mesma JVM. Além disso, erros comuns do programador que geralmente levam à corrupção de dados ou comportamento imprevisível, como acessar o final de uma matriz ou usar um ponteiro não inicializado, não podem ocorrer. Vários recursos de Java se combinam para fornecer essa segurança, incluindo o modelo de classe, a pilha de lixo coletada e o verificador.
A JVM verifica todo o bytecode antes de ser executado. Esta verificação consiste principalmente em três tipos de verificações:
- Os ramos são sempre locais válidos
- Os dados são sempre inicializados e as referências são sempre seguras
- O acesso a dados e métodos privados ou de pacotes é rigidamente controlado
As duas primeiras dessas verificações ocorrem principalmente durante a etapa de verificação que ocorre quando uma classe é carregada e qualificada para uso. A terceira é executada principalmente dinamicamente, quando itens de dados ou métodos de uma classe são acessados pela primeira vez por outra classe.
O verificador permite apenas algumas sequências de bytecode em programas válidos, por exemplo uma instrução de salto (ramificação) só pode ter como alvo uma instrução dentro do mesmo método. Além disso, o verificador garante que qualquer instrução opera em um local fixo da pilha, permitindo que o compilador JIT transforme os acessos à pilha em acessos fixos ao registro. Por causa disso, o fato de a JVM ser uma arquitetura de pilha não implica em uma penalidade de velocidade para emulação em arquiteturas baseadas em registro ao usar um compilador JIT. Diante da arquitetura JVM verificada por código, não faz diferença para um compilador JIT se ele recebe registros imaginários nomeados ou posições de pilha imaginárias que devem ser alocadas aos registros da arquitetura de destino. De fato, a verificação de código torna a JVM diferente de uma arquitetura de pilha clássica, cuja emulação eficiente com um compilador JIT é mais complicada e normalmente realizada por um interpretador mais lento. Além disso, o interpretador usado pela JVM padrão é um tipo especial conhecido como Template Interpreter, que traduz bytecode diretamente em linguagem de máquina nativa baseada em registro, em vez de emular uma pilha como um interpretador típico. Em muitos aspectos, o HotSpot Interpreter pode ser considerado um compilador JIT em vez de um verdadeiro interpretador, o que significa que a arquitetura de pilha que o bytecode visa não é realmente usada na implementação, mas apenas uma especificação para a representação intermediária que pode ser implementada em um registrador arquitetura baseada. Outra instância de uma arquitetura de pilha sendo meramente uma especificação e implementada em uma máquina virtual baseada em registradores é o Common Language Runtime.
A especificação original para o verificador de bytecode usava linguagem natural incompleta ou incorreta em alguns aspectos. Várias tentativas foram feitas para especificar a JVM como um sistema formal. Ao fazer isso, a segurança das implementações atuais da JVM pode ser analisada mais detalhadamente e possíveis explorações de segurança evitadas. Também será possível otimizar a JVM ignorando verificações de segurança desnecessárias, se o aplicativo em execução for comprovado como seguro.
Execução segura de código remoto
Uma arquitetura de máquina virtual permite um controle muito refinado sobre as ações que o código dentro da máquina pode executar. Ele assume que o código é "semanticamente" correto, ou seja, passou com sucesso no processo de verificação de bytecode (formal), materializado por uma ferramenta, possivelmente off-board da máquina virtual. Isso é projetado para permitir a execução segura de código não confiável de fontes remotas, um modelo usado por applets Java e outros downloads de código seguro. Após a verificação do bytecode, o código baixado é executado em uma "caixa de proteção" restrita, projetada para proteger o usuário contra mau comportamento ou código malicioso. Além do processo de verificação de bytecode, os editores podem adquirir um certificado com o qual assinam digitalmente os applets como seguros, dando-lhes permissão para solicitar ao usuário que saia da caixa de areia e acesse o sistema de arquivos local, a área de transferência, execute softwares externos, ou rede.
A prova formal de verificadores de bytecode foi feita pela indústria Javacard (Formal Development of an Embedded Verifier for Java Card Byte Code)
Interpretador de bytecode e compilador just-in-time
Para cada arquitetura de hardware é necessário um interpretador de bytecode Java diferente. Quando um computador possui um interpretador de bytecode Java, ele pode executar qualquer programa de bytecode Java, e o mesmo programa pode ser executado em qualquer computador que possua tal interpretador.
Quando o bytecode Java é executado por um interpretador, a execução sempre será mais lenta que a execução do mesmo programa compilado em linguagem de máquina nativa. Esse problema é mitigado por compiladores just-in-time (JIT) para execução de bytecode Java. Um compilador JIT pode traduzir o bytecode Java em linguagem de máquina nativa durante a execução do programa. As partes traduzidas do programa podem então ser executadas muito mais rapidamente do que poderiam ser interpretadas. Essa técnica é aplicada às partes de um programa executado com frequência. Dessa forma, um compilador JIT pode acelerar significativamente o tempo de execução geral.
Não há conexão necessária entre a linguagem de programação Java e o bytecode Java. Um programa escrito em Java pode ser compilado diretamente na linguagem de máquina de um computador real e programas escritos em outras linguagens além de Java podem ser compilados em bytecode Java.
O bytecode Java destina-se a ser independente de plataforma e seguro. Algumas implementações de JVM não incluem um interpretador, mas consistem apenas em um compilador just-in-time.
JVM no navegador da web
No início da vida útil da plataforma Java, a JVM era comercializada como uma tecnologia da Web para a criação de Rich Web Applications. A partir de 2018, a maioria dos navegadores da web e sistemas operacionais que agrupam navegadores da web não são fornecidos com um plug-in Java, nem permitem o carregamento lateral de qualquer plug-in não Flash. O plug-in do navegador Java foi preterido no JDK 9.
O plug-in do navegador NPAPI Java foi projetado para permitir que a JVM execute os chamados applets Java incorporados em páginas HTML. Para navegadores com o plug-in instalado, o applet pode desenhar em uma região retangular na página atribuída a ele. Como o plug-in inclui uma JVM, os applets Java não estão restritos à linguagem de programação Java; qualquer idioma destinado à JVM pode ser executado no plug-in. Um conjunto restrito de APIs permite que os applets acessem o microfone do usuário ou a aceleração 3D, embora os applets não sejam capazes de modificar a página fora de sua região retangular. O Adobe Flash Player, principal tecnologia concorrente, funciona da mesma forma nesse aspecto.
Em junho de 2015, de acordo com a W3Techs, o uso de applet Java e Silverlight caiu para 0,1% cada para todos os sites, enquanto o Flash caiu para 10,8%.
JavaScript JVMs e interpretadores
A partir de maio de 2016, o JavaPoly permite que os usuários importem bibliotecas Java não modificadas e as invoquem diretamente do JavaScript. O JavaPoly permite que os sites usem bibliotecas Java não modificadas, mesmo que o usuário não tenha o Java instalado em seu computador.
Compilação para JavaScript
Com as melhorias contínuas na velocidade de execução do JavaScript, combinadas com o aumento do uso de dispositivos móveis cujos navegadores da Web não implementam suporte para plug-ins, há esforços para direcionar esses usuários por meio da compilação para JavaScript. É possível compilar o código-fonte ou bytecode JVM para JavaScript.
A compilação do bytecode da JVM, que é universal entre as linguagens da JVM, permite a construção do compilador existente da linguagem para o bytecode. Os principais bytecode JVM para compiladores JavaScript são TeaVM, o compilador contido no Dragome Web SDK, Bck2Brwsr e j2js-compiler.
Os principais compiladores de linguagens JVM para JavaScript incluem o compilador Java-to-JavaScript contido no Google Web Toolkit, Clojurescript (Clojure), GrooScript (Apache Groovy), Scala.js (Scala) e outros.
Contenido relacionado
Representação e raciocínio do conhecimento
David Winer
AWK