O que é JVM?

O que é JVM?

Angela Gomes. A JVM é uma máquina virtual que permite a execução de programas Java. Ela interpreta o bytecode gerado pelo compilador Java e executa as instruções em um ambiente independente de plataforma. Isso significa que os programas Java podem ser executados em diferentes sistemas operacionais sem a necessidade de recompilação.

A JVM funciona como uma camada de abstração entre o código Java e o sistema operacional subjacente. Ela é responsável por várias tarefas, como carregar classes, gerenciar memória, realizar o garbage collection (coleta de lixo), executar as instruções Java e lidar com exceções.

Implementa a especificação Java Virtual Machine Specification, que define a estrutura, o comportamento e os requisitos da JVM. Essa especificação permite que várias implementações da JVM sejam criadas por diferentes fornecedores.

Realiza a verificação de bytecode em tempo de carregamento das classes para garantir que o código esteja em conformidade com as regras de segurança e integridade da linguagem Java. Isso inclui a verificação de acesso a campos e métodos, verificação de tipos, detecção de exceções não tratadas e validação de estrutura de código.

Suporta a criação de agentes Java (Java Agents) que podem ser anexados a um programa em execução para instrumentar e monitorar o comportamento do aplicativo. Os agentes Java podem ser usados para realizar tarefas como profiling, monitoramento de desempenho, modificação dinâmica de código e muito mais.

Oferece suporte a recursos avançados de diagnóstico e troubleshooting, como a capacidade de gerar despejos de memória (heap dumps) para análise, perfis de alocação de memória, análise de threads e monitoramento de utilização de recursos.

Possui uma arquitetura de classificação dinâmica (dynamic class loading) que permite carregar classes em tempo de execução com base em condições específicas. Isso possibilita a criação de aplicativos flexíveis e dinâmicos, onde as classes podem ser carregadas sob demanda, reduzindo a sobrecarga de memória.

Suporta a criação e execução de applets Java, que são pequenos aplicativos executados em navegadores da web. Os applets Java foram populares no passado para fornecer funcionalidades interativas nas páginas da web.

Oferece suporte a várias opções de configuração e ajuste de desempenho por meio de argumentos de linha de comando, variáveis de ambiente e arquivos de configuração. Isso permite que os administradores de sistema otimizem a JVM para atender às necessidades específicas do ambiente de execução.

Possui uma API rica, chamada Java Native Interface (JNI), que permite a comunicação entre código Java e código nativo escrito em linguagens como C e C++. Isso possibilita a integração de bibliotecas nativas existentes e o acesso a recursos de baixo nível do sistema operacional.

Principais Componentes da JVM

  1. Class Loader (Carregador de Classes): É responsável por carregar as classes necessárias em tempo de execução.
  2. Memory Area (Área de Memória): É onde a memória é alocada para diferentes fins, como o heap (onde os objetos são armazenados) e a stack (onde os métodos são executados).
  3. Execution Engine (Mecanismo de Execução): É responsável por executar o bytecode Java. Existem diferentes tipos de mecanismos de execução, como interpretação, just-in-time compilation (JIT) e AOT (Ahead-of-Time) compilation.
  4. Garbage Collector (Coletor de Lixo): É responsável por gerenciar a memória, liberando objetos que não estão mais sendo referenciados pelo programa.

Carregamento de Classes

O carregamento de classes ocorre em três fases distintas:

  • Carregamento: Nessa fase, o Class Loader localiza, carrega e lê os bytes de definição da classe. Os bytes da classe geralmente estão armazenados em um arquivo .class no sistema de arquivos ou podem ser obtidos de outras fontes, como um banco de dados ou uma rede.
  • Vinculação:

Verificação: Nessa etapa, a JVM verifica se os bytes da classe estão corretos e consistentes. Isso envolve a verificação da estrutura da classe, como se os campos e métodos referenciados existem e têm acesso apropriado.

Preparação: Nessa etapa, a JVM aloca espaço para os campos estáticos da classe e inicializa-os com valores padrão.

Resolução: Nessa etapa, as referências simbólicas da classe são substituídas por referências diretas a outros tipos (classes, interfaces ou métodos). Isso envolve a resolução de nomes de classes, interfaces e membros.

  • Inicialização: Nessa fase, o código estático da classe é executado para inicializar os campos estáticos e realizar outras inicializações necessárias. Esse código é executado apenas uma vez, quando a classe é carregada pela primeira vez ou quando é referenciada pela primeira vez.
  • Carregamento Dinâmico de Classes: Além do carregamento padrão, é possível carregar classes dinamicamente em tempo de execução usando o método ‘Class.forName()’ou as APIs de Reflection. Isso permite que um programa carregue classes com base em condições específicas ou até mesmo adicione novas classes em tempo de execução.
  • Classpath: O Classpath é um conjunto de locais (diretórios ou arquivos JAR) especificados para a JVM procurar classes durante o carregamento. Ele determina onde o Class Loader deve procurar pelas classes solicitadas. O Classpath pode ser definido por meio de variáveis de ambiente ou especificado na linha de comando ao iniciar um programa Java.

Class Loader: Funcionamento

O Class Loader é acionado quando uma classe é referenciada pela primeira vez durante a execução do programa. Ele busca a classe em diferentes locais, como o sistema de arquivos local, diretórios de bibliotecas ou até mesmo pela rede. Uma vez encontrada, o Class Loader carrega a definição da classe na memória e a prepara para uso.

A JVM possui uma hierarquia de Class Loaders que trabalham em conjunto para carregar as classes. A hierarquia é baseada em pais e filhos, onde cada Class Loader tem um pai, exceto o Class Loader raiz. Quando uma classe é referenciada, o Class Loader atual tenta carregá-la, e se não for capaz, passa a solicitação para seu pai na hierarquia. Essa cadeia continua até que a classe seja encontrada e carregada com sucesso ou ocorra uma exceção, indicando que a classe não pode ser encontrada.

A JVM possui um Class Loader padrão que é usado como ponto de partida para carregar as classes. Ele é responsável por carregar as bibliotecas do sistema, classes principais do Java e outras classes de uso geral. O Class Loader padrão é implementado pela classe java.lang.ClassLoader e pode ser estendido para criar Class Loaders personalizados.

É possível criar Class Loaders personalizados na JVM. Isso permite que desenvolvedores carreguem classes de maneiras personalizadas, como carregar classes a partir de um local específico, baixá-las pela rede ou até mesmo gerá-las dinamicamente em tempo de execução. Os Class Loaders personalizados podem ser úteis em cenários como plugins, ambientes de sandbox e aplicativos de servidor.

O Class Loader utiliza um modelo de delegação para carregar classes. Isso significa que o Class Loader tenta primeiro delegar a responsabilidade de carregamento para o Class Loader pai antes de tentar carregar a classe por conta própria. Essa abordagem ajuda a evitar a duplicação de carregamento de classes e promove a reutilização de código.

O Class Loader desempenha um papel importante no carregamento e gerenciamento de classes em tempo de execução na JVM. Ele permite que o programa Java acesse as classes necessárias dinamicamente, fornecendo flexibilidade e modularidade aos aplicativos.

Existem três tipos principais de ClassLoader no Java:

  • Bootstrap Class Loader: O Bootstrap Class Loader, também conhecido como primordial Class Loader, é o primeiro Class Loader a ser carregado pela JVM. Ele é responsável por carregar as classes principais do Java, como aquelas encontradas nas bibliotecas do sistema (por exemplo, rt.jar). O Bootstrap Class Loader é implementado em código nativo e não é possível substituí-lo ou estendê-lo.
  • Extension Class Loader: O Extension Class Loader é um Class Loader filho do Bootstrap Class Loader. Ele é responsável por carregar as classes localizadas na extensão do diretório de classpath (jre/lib/ext) ou definidas por meio da variável de ambiente java.ext.dirs. Esse Class Loader permite a adição de funcionalidades extras ao Java através de extensões.
  • Application Class Loader: O Application Class Loader, também conhecido como System Class Loader, é um Class Loader filho do Extension Class Loader. Ele é responsável por carregar as classes do aplicativo em execução. O Application Class Loader é configurado pelo parâmetro de linha de comando -classpath ou pela variável de ambiente CLASSPATH. Ele também é responsável por carregar classes personalizadas desenvolvidas pelo usuário.

Runtime Data Areas (Áreas de Dados em Tempo de Execução)

São áreas de memória usadas pela JVM (Java Virtual Machine) durante a execução de um programa Java. Elas são responsáveis por armazenar diferentes tipos de dados e informações necessárias para a execução correta do programa. Aqui estão as principais áreas de dados em tempo de execução:

  • Method Area (Área de Métodos)

A Method Area é uma área de memória compartilhada entre todas as threads em execução na JVM. Ela armazena as informações sobre as classes e métodos carregados, como a estrutura de classe, os campos, os métodos e as constantes. A Method Area também contém as constantes da classe, como strings literais e valores numéricos constantes.

  • Heap

O Heap é a área de memória onde os objetos Java são alocados durante a execução do programa. Ele é compartilhado entre todas as threads e é gerenciado pelo coletor de lixo (garbage collector) da JVM. O Heap é dividido em duas regiões principais: a Young Generation (Geração Jovem) e a Old Generation (Geração Antiga), que são responsáveis pelo gerenciamento de objetos de curta duração e objetos de longa duração, respectivamente.

  • Stack (Pilha)

A Java Stack é uma área de memória associada a cada thread em execução. Ela armazena informações sobre os métodos em execução, como os parâmetros e as variáveis locais. Cada método invocado cria um novo frame na Java Stack, que contém informações específicas do método, como os valores das variáveis locais e a referência para o método atualmente em execução.

  • PC Register (Registrador de Contador de Programa)

O PC Register é um registrador dedicado a cada thread. Ele contém o endereço de memória da próxima instrução a ser executada pela thread. O PC Register é atualizado a cada instrução executada e é usado para controlar o fluxo de execução do programa.

  • Native Method Stack (Pilha de Métodos Nativos)

A Native Method Stack é uma área de memória usada para a execução de métodos nativos, que são implementados em linguagens diferentes do Java, como C ou C++. Cada thread em execução possui sua própria pilha de métodos nativos.

  • Direct Memory (Memória Direta)

A Direct Memory é uma área de memória usada para armazenar dados fora do Heap, geralmente para operações que exigem acesso direto à memória, como operações de E/S (entrada/saída) e operações de rede. A Direct Memory não é gerenciada pelo coletor de lixo e requer uma liberação explícita pelos programas Java.

Essas áreas de dados em tempo de execução são essenciais para o funcionamento adequado da JVM e são usadas para armazenar informações críticas durante a execução do programa Java, como as estruturas de classe, os objetos alocados, os métodos em execução e os registros de controle de fluxo.

O Execution Engine (Mecanismo de Execução)

É uma parte crucial da JVM (Java Virtual Machine) responsável por executar o bytecode Java. Ele interpreta e executa as instruções em bytecode, permitindo que o programa Java seja executado em qualquer plataforma suportada pela JVM.

  • Interpretação

O Execution Engine usa um mecanismo de interpretação para executar o bytecode Java. Ele lê as instruções do bytecode uma por uma e as converte em ações ou chamadas de métodos equivalentes na plataforma subjacente. O processo de interpretação é relativamente lento, pois cada instrução precisa ser interpretada em tempo real.

  • Just-in-Time Compilation (Compilação Just-in-Time - JIT)

Para melhorar o desempenho, muitas JVMs modernas utilizam técnicas de Just-in-Time Compilation (compilação em tempo de execução). O JIT Compiler (compilador JIT) converte partes do bytecode Java em código nativo de máquina durante a execução do programa. Isso permite que as instruções sejam executadas diretamente pela CPU, proporcionando um aumento significativo no desempenho.

  • HotSpot JVM: A HotSpot JVM, uma implementação da JVM fornecida pela Oracle, é conhecida por sua abordagem de otimização dinâmica. Ela combina a interpretação inicial do bytecode com a compilação JIT adaptativa. A HotSpot JVM monitora o desempenho do código em tempo real e identifica trechos de código frequentemente executados (chamados de "hotspots"). Esses hotspots são então compilados em código nativo otimizado para melhorar o desempenho.
  • Profiling (Perfilamento): O Execution Engine realiza o profiling (perfilamento) do programa em execução para coletar informações sobre o comportamento do código. Isso inclui a identificação de trechos de código frequentemente executados, o tempo gasto em cada método, a alocação de objetos, entre outros dados. Essas informações são usadas pelo JIT Compiler para realizar otimizações específicas, como inlining de métodos, eliminação de código morto e otimização de loops.
  • Gerenciamento de Exceções: O Execution Engine lida com o gerenciamento de exceções durante a execução do programa Java. Quando ocorre uma exceção, o Execution Engine interrompe a execução normal do programa e procura um bloco de tratamento adequado para lidar com a exceção. Se nenhum bloco de tratamento é encontrado, o programa termina com uma mensagem de erro.

O Execution Engine é responsável por executar o bytecode Java em uma plataforma específica, garantindo que o programa Java seja executado corretamente e otimizando o desempenho sempre que possível. Sua combinação de interpretação e compilação JIT torna a execução do Java eficiente e portável entre diferentes sistemas operacionais e arquiteturas de hardware.

Garbage Collector (Coletor de Lixo)

É uma parte essencial da JVM (Java Virtual Machine) responsável pela gerência automática da memória e pela recuperação de objetos não utilizados, liberando o espaço ocupado por eles. O Garbage Collector automatiza o processo de liberação de memória, eliminando a necessidade de o programador gerenciar explicitamente a alocação e desalocação de memória.

Gerenciamento de Memória: O Garbage Collector gerencia a memória do programa Java, alocando espaço para objetos durante a execução e liberando automaticamente a memória ocupada por objetos que não são mais referenciados. Isso evita vazamentos de memória e torna o desenvolvimento em Java mais conveniente, pois o programador não precisa se preocupar com a desalocação manual da memória.

O Garbage Collector identifica os objetos que não são mais referenciados pelo programa Java. Um objeto é considerado não referenciado se não pode ser alcançado a partir de nenhum ponto de acesso do programa. Isso é feito por meio de técnicas como contagem de referências, coleta de raiz e análise de alcance.

Algoritmos de Coleta de Lixo: Existem diferentes algoritmos de coleta de lixo implementados pelos Garbage Collectors para determinar quais objetos serão liberados. Os algoritmos mais comuns são a Coleta de Lixo Marcação e Varredura (Mark and Sweep), Coleta de Gerações (Generational Collection) e Coleta de Gerações com Parada de Mundos (Stop-the-World Generational Collection). Cada algoritmo possui suas próprias estratégias para identificar e liberar objetos não utilizados.

  • Coleta de Gerações: A maioria dos Garbage Collectors modernos usa o conceito de coleta de gerações. Nesse modelo, a memória é dividida em diferentes gerações, como a Geração Jovem (Young Generation) e a Geração Antiga (Old Generation). Objetos recém-criados são alocados na Geração Jovem, enquanto objetos que sobrevivem a várias coletas de lixo são promovidos para a Geração Antiga. A coleta de gerações permite otimizações específicas para cada tipo de objeto, resultando em um melhor desempenho.
  • Parada de Mundos (Stop-the-World): Durante a execução da coleta de lixo, a maioria dos Garbage Collectors faz uma pausa temporária na execução do programa Java, conhecida como parada de mundos (stop-the-world). Durante essa pausa, todas as threads são suspensas enquanto o Garbage Collector executa suas operações de coleta de lixo. Embora isso possa causar breves interrupções na execução do programa, a parada de mundos é otimizada para minimizar seu impacto no desempenho geral.

A JVM oferece opções de configuração para ajustar o comportamento do Garbage Collector, permitindo otimizações personalizadas para atender às necessidades específicas do aplicativo. As opções de configuração incluem a escolha do algoritmo de coleta de lixo, tamanho da heap, políticas de promoção e limpeza, entre outros.

O Garbage Collector é uma parte fundamental do ambiente de execução Java, garantindo a gerência eficiente da memória e prevenindo vazamentos de memória. Ele alivia a carga do programador em relação à alocação e desalocação manual da memória, permitindo que se concentre na lógica de negócios do aplicativo.

Vantagens da JVM

  • Portabilidade

A JVM é projetada para fornecer portabilidade de código Java. Isso significa que um programa Java escrito uma vez pode ser executado em qualquer sistema operacional ou arquitetura de hardware compatível com a JVM. Isso permite que os desenvolvedores escrevam aplicativos Java uma vez e os executem em diferentes plataformas sem a necessidade de reescrever o código-fonte.

  • Gerenciamento de Memória

A JVM gerencia automaticamente a alocação e desalocação de memória por meio do Garbage Collector. Isso simplifica o desenvolvimento, eliminando a necessidade de o programador lidar explicitamente com a alocação e desalocação de memória. O Garbage Collector rastreia e libera automaticamente os objetos não utilizados, evitando vazamentos de memória e tornando o desenvolvimento em Java mais eficiente e seguro.

  • Segurança

A JVM é projetada com recursos de segurança robustos. Ela implementa um modelo de segurança baseado em sandbox (caixa de areia) que restringe o acesso não autorizado a recursos do sistema. Isso garante que os aplicativos Java executados na JVM não possam causar danos ao sistema host, protegendo contra ameaças como acesso não autorizado, corrupção de dados ou execução de código malicioso.

  • Gerenciamento de Exceções

A JVM possui um sistema de gerenciamento de exceções robusto e integrado. Ele fornece mecanismos para detectar, lançar, capturar e tratar exceções em tempo de execução. Isso permite que os desenvolvedores identifiquem e lidem com erros e exceções de maneira estruturada, facilitando a depuração e o desenvolvimento de aplicativos mais robustos e confiáveis.

  • Desempenho

Embora a JVM seja uma máquina virtual intermediária, ela oferece um desempenho muito bom. A JVM utiliza técnicas avançadas, como a compilação Just-in-Time (JIT), para converter o bytecode Java em código nativo otimizado em tempo de execução. Isso resulta em um desempenho comparável a linguagens de programação nativas, permitindo que os aplicativos Java executem de forma eficiente e rápida.

  • Facilidade de Manutenção

A JVM e a linguagem Java são conhecidas por promoverem a escrita de código legível e de fácil manutenção. A linguagem Java possui uma sintaxe clara e padronizada, o que torna o código mais compreensível e facilita a colaboração entre desenvolvedores. Além disso, a JVM fornece recursos avançados de depuração, profiling e monitoramento, facilitando a identificação e correção de problemas em tempo de execução.

  • Multithreading

A JVM oferece suporte nativo a programação multithread, permitindo que os desenvolvedores escrevam aplicativos Java altamente concorrentes e eficientes. A JVM gerencia a execução concorrente de threads e fornece mecanismos de sincronização, como locks e semáforos, para garantir a consistência dos dados compartilhados entre as threads. Isso permite a criação de aplicativos escaláveis e de alto desempenho que aproveitam os recursos de processamento paralelo.

  • Integração com outras linguagens

A JVM não é exclusiva para a linguagem Java. Ela suporta outras linguagens de programação, como Kotlin, Scala, Groovy e muitas outras, por meio da compilação dessas linguagens para o bytecode Java. Isso permite que os desenvolvedores aproveitem as vantagens da JVM, como o Garbage Collector, o desempenho e o ecossistema, enquanto usam outras linguagens de programação.

  • Suporte a grande escala

A JVM é capaz de lidar com aplicativos de grande escala e de alto desempenho. Ela oferece suporte a sistemas distribuídos, clustering e balanceamento de carga, permitindo que os aplicativos Java sejam dimensionados horizontalmente para atender a demandas crescentes. Além disso, a JVM possui ferramentas avançadas de monitoramento e gerenciamento de desempenho, que ajudam a otimizar e ajustar o desempenho do aplicativo em ambientes de produção.

  • Ecossistema e Bibliotecas

A JVM possui um ecossistema maduro e uma vasta coleção de bibliotecas e frameworks disponíveis. Isso facilita o desenvolvimento de aplicativos Java, pois os desenvolvedores podem aproveitar bibliotecas existentes para tarefas comuns, como manipulação de arquivos, acesso a bancos de dados, comunicação em rede, interfaces gráficas, entre outros. O ecossistema Java é rico em ferramentas, IDEs (Integrated Development Environments) e recursos de suporte à comunidade, proporcionando um ambiente de desenvolvimento robusto e colaborativo.

Implementações da JVM (Java Virtual Machine)

Existem várias implementações da JVM  disponíveis, oferecendo suporte ao desenvolvimento e execução de aplicativos Java em diferentes plataformas.

  • Oracle HotSpot JVM

O HotSpot JVM é a implementação padrão da Oracle e é amplamente utilizado. Ele é conhecido por seu desempenho e recursos avançados, como o compilador Just-in-Time (JIT) e o Garbage Collector (Coletor de Lixo) de última geração.

  • OpenJDK

O OpenJDK é uma implementação de código aberto da plataforma Java e da JVM. Ele é mantido pela comunidade Java e serve como base para várias outras implementações, incluindo o HotSpot JVM da Oracle. O OpenJDK é amplamente utilizado e oferece suporte a diversas plataformas.

  • IBM J9 JVM

O J9 JVM da IBM é outra implementação popular da JVM. Ele é conhecido por seu baixo consumo de memória e por sua escalabilidade em ambientes corporativos. O J9 JVM é usado em várias soluções IBM, como o WebSphere Application Server.

Essas são apenas algumas das implementações JVM mais populares. Cada uma delas tem suas próprias características e recursos distintos, mas todas são compatíveis com o padrão Java e permitem a execução de aplicativos Java em diferentes plataformas.

Em resumo, a JVM é um componente essencial para a execução de aplicativos Java, oferecendo portabilidade, desempenho, segurança e um conjunto abrangente de recursos para o desenvolvimento e execução de aplicativos robustos e escaláveis.

Ela oferece um ambiente de execução poderoso e flexível para aplicativos Java, fornecendo recursos avançados de gerenciamento de memória, otimização de desempenho, segurança e extensibilidade. A JVM é um componente central da plataforma Java e continua evoluindo para atender às demandas dos aplicativos modernos. É uma tecnologia complexa e versátil que oferece suporte a uma ampla gama de recursos e funcionalidades para a execução de aplicativos Java.

💡
As opiniões e comentários expressos neste artigo são de propriedade exclusiva de seu autor e não representam necessariamente o ponto de vista da Revelo.

A Revelo Content Network acolhe todas as raças, etnias, nacionalidades, credos, gêneros, orientações, pontos de vista e ideologias, desde que promovam diversidade, equidade, inclusão e crescimento na carreira dos profissionais de tecnologia.