Não é segredo que, no momento, o Java é uma das linguagens de programação mais populares do mundo. A data oficial de lançamento do Java é 23 de maio de 1995.
Este artigo é dedicado aos princípios básicos: descreve os recursos básicos da linguagem, que serão úteis para os "javists" iniciantes, e desenvolvedores Java experientes poderão atualizar seus conhecimentos.
* O artigo foi preparado com base em um relatório de Eugene Freiman - desenvolvedor Java do IntexSoft.
O artigo contém links para materiais externos .

1. JDK, JRE, JVM
O Java Development Kit é um kit de desenvolvimento de aplicativos
Java . Inclui o
Java Development Tools e o Java Runtime Environment (
JRE ).
As ferramentas de desenvolvimento Java incluem cerca de 40 ferramentas diferentes: javac (compilador), java (iniciador de aplicativos), javap (desmontador de arquivo de classe java), jdb (depurador de java), etc.
O tempo de execução do JRE é um pacote de tudo o necessário para executar um programa Java compilado. Inclui a máquina virtual da
JVM e a
Java Class Library .
JVM é um programa projetado para executar bytecode. A primeira vantagem da JVM é o princípio de
"Escreva uma vez, execute em qualquer lugar" . Isso significa que um aplicativo escrito em Java funcionará da mesma maneira em todas as plataformas. Essa é uma grande vantagem da JVM e do próprio Java.
Antes do advento do Java, muitos programas de computador eram escritos para sistemas de computador específicos, e a preferência era dada ao gerenciamento manual de memória, por ser mais eficiente e previsível. Desde a segunda metade da década de 90, após o advento do Java, o gerenciamento automático de memória tornou-se uma prática comum.
Existem muitas implementações da JVM, comercial e de código aberto. Um dos objetivos da criação de novas JVMs é aumentar o desempenho de uma plataforma específica. Cada JVM é gravada separadamente para a plataforma, enquanto é possível gravá-la para que ela funcione mais rapidamente em uma plataforma específica. A implementação mais comum da JVM é o
OpenJDK JVM Hotspot. Também existem implementações do
IBM J9 ,
Excelsior JET .
2. Execução do código da JVM
De acordo com
a especificação Java SE , para obter o código em execução na JVM, é necessário concluir 3 etapas:
- Carregando bytecode e instanciando a classe Class
Grosso modo, para acessar a JVM, a classe deve ser carregada. Existem classes de carregador separadas para isso, retornaremos a elas um pouco mais tarde. - Vincular ou vincular
Após o carregamento da classe, o processo de vinculação é iniciado, no qual o bytecode é analisado e verificado. O processo de vinculação, por sua vez, ocorre em três etapas:
- verificação ou verificação do bytecode: a correção das instruções, a possibilidade de estouro de pilha nesta seção do código, a compatibilidade dos tipos de variáveis são verificadas; a verificação ocorre uma vez para cada classe;
- preparação ou preparação: nesta etapa, de acordo com a especificação, a memória é alocada para campos estáticos e sua inicialização ocorre;
- resolução ou resolução: permissão de links simbólicos (quando no bytecode abrimos arquivos com a extensão .class, vemos valores numéricos em vez de links simbólicos). - Inicializando o objeto de classe resultante
No último estágio, a classe que criamos é inicializada e a JVM pode começar a executá-la.
3. Carregadores de classes e sua hierarquia
De volta aos carregadores de classes, são classes especiais que fazem parte da JVM. Eles carregam classes na memória e as disponibilizam para execução. Os carregadores trabalham com todas as classes: tanto as nossas quanto as diretamente necessárias para Java.
Imagine a situação: escrevemos nosso aplicativo e, além das classes padrão, existem nossas classes e existem muitas. Como a JVM trabalhará com isso? Java implementa carregamento diferido de classe, ou seja, carregamento lento. Isso significa que o carregamento das classes não será realizado até que no aplicativo não haja chamada para a classe.
Hierarquia do carregador de classes

O carregador de primeira classe é o
carregador de classe Bootstrap . Está escrito em C ++. Este é o carregador básico que carrega todas as classes do sistema do arquivo
rt.jar . Ao mesmo tempo, há uma pequena diferença entre carregar classes do
rt.jar e nossas classes: quando a JVM carrega classes do
rt.jar , ela não executa todas as etapas de verificação que são executadas ao carregar qualquer outro arquivo de classe desde A JVM está inicialmente ciente de que todas essas classes já estão validadas. Portanto, você não deve incluir nenhum dos seus arquivos neste arquivo.
O próximo carregador de inicialização é o
carregador de classes Extension. Carrega classes de extensão da pasta
jre / lib / ext . Suponha que você queira que uma classe seja carregada toda vez que a máquina Java iniciar. Para fazer isso, você pode copiar o arquivo de classe de origem para esta pasta e ele será carregado automaticamente.
Outro carregador de inicialização é o
carregador de classes do
sistema . Carrega as classes do caminho de classe que especificamos quando o aplicativo foi iniciado.
O processo de carregamento de classes ocorre em uma hierarquia:
- Primeiro, solicitamos uma pesquisa no cache do System Class Loader (o cache do carregador do sistema contém classes que já foram carregadas por ele);
- Se a classe não foi encontrada no cache do carregador do sistema, examinamos o carregador da classe Extension;
- Se a classe não for encontrada no cache do carregador de extensão, a classe será solicitada no carregador do Bootstrap.
Se a classe não for encontrada no cache do Bootstrap, ela tentará carregar essa classe. Se o Bootstrap não pôde carregar a classe, ele delega o carregamento da classe no carregador de extensão. Se, nesse ponto, a classe estiver carregada, ela permanecerá no cache do carregador de classes Extension e o carregamento da classe estará concluído.
4. Estrutura do arquivo da classe e processo de inicialização
Prosseguimos diretamente para a estrutura dos arquivos de classe.
Uma classe escrita em Java é compilada em um único arquivo com a extensão .class. Se houver várias classes em nosso arquivo Java, um arquivo Java poderá ser compilado em vários arquivos com os arquivos de extensão .class - bytecode dessas classes.
Todos os números, seqüências de caracteres, ponteiros para classes, campos e métodos são armazenados no
pool Constant - a área de memória do
Meta space . A descrição da classe é armazenada no mesmo local e contém o nome, modificadores, superclasse, superinterfaces, campos, métodos e atributos. Os atributos, por sua vez, podem conter qualquer informação adicional.
Assim, ao carregar as classes:
- leitura do arquivo de classe, ou seja, validação de formato
- representação de classe é criada no pool constante (meta espaço)
- super classes e super interfaces são carregadas; se eles não estiverem carregados, a própria classe não será carregada
5. Execução de bytecode na JVM
Antes de tudo, para executar o bytecode, a JVM pode
interpretá-lo . A interpretação é um processo bastante lento. No processo de interpretação, o intérprete “executa” linha por linha através do arquivo de classe e o converte em comandos que são compreensíveis pela JVM.
Além disso, a JVM pode
transmiti-lo , ou seja, compilar no código da máquina que será executado diretamente na CPU.
Os comandos executados com frequência não serão interpretados, mas serão transmitidos imediatamente.
6. Compilação
Um compilador é um programa que converte as partes de origem dos programas gravados em uma linguagem de programação de alto nível em um programa de linguagem de máquina que é "compreensível" em um computador.
Os compiladores são divididos em:
- Não otimizando
- Otimização simples (Hotspot Client): trabalhe rapidamente, mas gere código não ideal
- Otimização complexa (Hotspot Server): execute transformações complexas de otimização antes de gerar o bytecode
Os compiladores também podem ser classificados por tempo de compilação:
- Compiladores dinâmicos
Eles trabalham simultaneamente com o programa, o que afeta o desempenho. É importante que esses compiladores sejam executados em código que geralmente é executado. Durante a execução do programa, a JVM sabe qual código é executado com mais freqüência e, para não interpretá-lo constantemente, a máquina virtual o traduz imediatamente em comandos que já serão executados diretamente no processador. - Compiladores estáticos
Compile mais, mas gere o código ideal para execução. Dos profissionais: eles não exigem recursos durante a execução do programa, cada método é compilado usando otimizações.
7. Organização da memória em Java
Uma pilha é uma área de memória em Java que funciona de acordo com o esquema LIFO - “
Última entrada - saída de saída ” ou “
Última entrada, saída primeiro ”.

É necessário para armazenar métodos. As variáveis na pilha existem desde que o método em que elas foram criadas seja executado.
Quando qualquer método é chamado em Java, um quadro ou área de memória é criado na pilha e o método é colocado no topo. Quando um método conclui a execução, ele é removido da memória, liberando memória para os seguintes métodos. Se a memória da pilha estiver cheia, o Java lançará uma exceção
java.lang.StackOverFlowError . Por exemplo, isso pode acontecer se tivermos uma função recursiva que se chamará e não houver memória suficiente na pilha.
Principais recursos da pilha:
- A pilha é preenchida e liberada à medida que novos métodos são chamados e concluídos.
- O acesso a essa área de memória é mais rápido que o heap.
- O tamanho da pilha é determinado pelo sistema operacional.
- É seguro para threads, pois cada pilha possui sua própria pilha separada.
Outra área de memória em Java é
Heap ou
heap . É usado para armazenar objetos e classes. Novos objetos sempre são criados na pilha e referências a eles são armazenadas na pilha. Todos os objetos na pilha têm acesso global, ou seja, eles podem ser acessados de qualquer lugar no aplicativo.
O heap é dividido em várias partes menores chamadas gerações:
- Geração jovem - a área onde estão localizados os objetos criados recentemente
- Geração antiga (tenured) - a área em que objetos de longa duração são armazenados
- Antes do Java 8, havia outra área - Geração permanente - que contém metainformações sobre classes, métodos e variáveis estáticas. Após o advento do Java 8, decidiu-se armazenar essas informações separadamente, fora do heap, nomeadamente no Meta space

Por que abandonar a geração permanente? Antes de tudo, isso ocorre devido a um erro associado ao estouro da área: como o Perm tinha um tamanho constante e não podia se expandir dinamicamente, mais cedo ou mais tarde a memória acabou, um erro foi lançado e o aplicativo travou.
O meta espaço possui um tamanho dinâmico e, em tempo de execução, pode ser expandido para tamanhos de memória da JVM.
Principais recursos da pilha:
- Quando essa área de memória está cheia, o Java lança java.lang.OutOfMemoryError
- O acesso ao heap é mais lento que o acesso à pilha
- Coletor de lixo trabalha para coletar objetos não utilizados
- Um heap, diferente de uma pilha, não é seguro para threads, pois qualquer thread pode acessá-lo
Com base nas informações acima, considere como o gerenciamento de memória é realizado usando um exemplo simples:
public class App { public static void main(String[] args) { int id = 23; String pName = "Jon"; Person p = null; p = new Person(id, pName); } } class Person { int pid; String name; // constructors, getters/setters }
Temos uma classe App na qual o único método
principal consiste em:
- variável de
identificação primitiva do tipo
int com o valor
23- variável de referência
pName do tipo
String com o valor
Jon- variável de referência
p do tipo
pessoa
Como já mencionado, quando um método é chamado, uma área de memória é criada na parte superior da pilha na qual os dados necessários para que esse método seja armazenado são armazenados.
No nosso caso, isso é uma referência à classe
person : o próprio objeto é armazenado no heap e o link é armazenado na pilha. Um link para a sequência também é enviado para a pilha e a própria sequência é armazenada na pilha no pool de Sequências. O primitivo é armazenado diretamente na pilha.
Para chamar o construtor com os parâmetros
Person (String) do método
main () na pilha, na parte superior da chamada
main () anterior, um quadro separado é criado na pilha que armazena:
-
this - link para o objeto atual
- valor do
ID primitivo
- a variável de referência
personName , que aponta para uma string no Pool de String.
Depois que chamamos o construtor,
setPersonName () é chamado, após o qual um novo quadro é criado na pilha novamente, onde os mesmos dados são armazenados: referência ao objeto, referência de linha, valor da variável.
Assim, quando o método
setter é executado, o quadro desaparece, a pilha é limpa. Em seguida, o construtor é executado, o quadro que foi criado para o construtor é limpo, após o qual o método
main () termina seu trabalho e também é removido da pilha.
Se outros métodos forem chamados, novos quadros também serão criados para eles com o contexto desses métodos específicos.
8. coletor de lixo
O coletor de lixo está trabalhando no heap - um programa em execução na máquina virtual Java que se livra dos objetos que não podem ser acessados.
JVMs diferentes podem ter algoritmos diferentes de coleta de lixo; também existem coletores de lixo diferentes.
Falaremos sobre o coletor mais simples, o
Serial GC . Solicitamos a coleta de lixo usando
System.gc () .

Como mencionado acima, o heap é dividido em 2 áreas: nova geração e geração antiga.
A nova geração (geração mais jovem) inclui 3 regiões:
Eden ,
Survivor 0 e
Survivor 1 .
A geração antiga inclui a região
Tenured .
O que acontece quando criamos um objeto em Java?
Primeiro de tudo, o objeto cai no
Éden . Se já criamos muitos objetos e não há mais espaço no
Eden , o coletor de lixo dispara e libera memória. Essa é a chamada
pequena coleta de lixo - na primeira passagem, limpa a área de
Eden e coloca os objetos “sobreviventes” na região
Survivor 0 . Assim, a região do
Éden é completamente libertada.
Se acontecer que a área de
Eden tenha sido preenchida novamente, o coletor de lixo começará a trabalhar com a área de
Eden e o
Survivor 0 , que atualmente está ocupado. Após a limpeza, os objetos sobreviventes cairão em outra região -
Survivor 1 , e os outros dois permanecerão limpos. Após a coleta de lixo subsequente, o
Survivor 0 será novamente selecionado como a região de destino. É por isso que é importante que uma das regiões do
Survivor esteja sempre vazia.
A JVM monitora objetos que são constantemente copiados e movidos de uma região para outra. E, para otimizar esse mecanismo, após um certo limite, o coletor de lixo move esses objetos para a região
Tenured .
Quando não há espaço suficiente para novos objetos no
Tenured , há uma coleta de lixo completa -
Mark-Sweep-Compact .

Durante esse mecanismo, é determinado quais objetos não são mais usados, a região é limpa desses objetos e a
área de memória
Tenured é desfragmentada, ou seja, sequencialmente preenchido com os objetos necessários.
Conclusão
Neste artigo, examinamos as ferramentas básicas da linguagem Java: JVM, JRE, JDK, o princípio e os estágios da execução do código da JVM, compilação, organização da memória e o princípio do coletor de lixo.