Ferramentas para ativar e desenvolver aplicativos Java, compilação, execução na JVM

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.

Source: https://habr.com/ru/post/pt471772/


All Articles