Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[CPBR14] Desvendando a JVM : arquitetura e funcionamento

[CPBR14] Desvendando a JVM : arquitetura e funcionamento

More Decks by Kamila de fatima santos oliveira

Other Decks in Programming

Transcript

  1. Kamila Santos TECH LEAD NA ZUP INNOVATION MICROSOFT MVP DEVELOPER

    TECHNOLOGIES CONTENT CREATOR @KAMILA_CODE CO-AUTORA DOS LIVROS JORNADA JAVA E JORNADA MICROSSERVIÇOS
  2. O que é a JVM? JVM (Java Virtual Machine) foi

    criada nos anos 90 e é a base da plataforma Java. Também responsável por tratar todos os SOs e plataformas para a linguagem, ela não conhece a linguagem Java, somente o seu bytecode.
  3. O que é a JVM? O bytecode vem no formato

    .class e são instruções que indicam para a JVM o que deve ser realizado naquela parte da aplicação.
  4. Linguagens compiladas Compilam e transformam o código em binário (linguagem

    de máquina) seguindo a arquitetura do sistema. Tem como vantagem velocidade de execução pela compilação ser focada na arquitetura. Ex: C e C++
  5. Linguagens interpretadas Traduzem cada linha de código em binário conforme

    a linha é executada. A vantagem dessa abordagem é a portabilidade, que permite executar o código em diversos tipos de arquiteturas sem precisar fazer nenhum tipo de pré-compilação para a arquitetura. Por ex: Ruby e Python
  6. E O JAVAAAA? Ela é um misto de linguagem compilada

    e interpretada. Primeiramente o código é compilado para um formato portátil e intermediário (chamado de bytecode), para somente depois ser interpretado. Dentre as vantagens dessa abordagem podemos citar: type checking (verificação da tipagem), otimização da compilação do código, o bytecode só necessita ser compilado uma vez para chegar o mais próximo do código de máquina e mantém a portabilidade.
  7. Class Loader Quando você compila um arquivo java, ele é

    convertido em bytecode (arquivo .class). Quando você tenta usar esta classe em seu aplicativo, o class loader o carrega na memória principal.
  8. Class Loader - Loading Normalmente, a primeira classe a ser

    carregada na memória é a classe principal (que contém o método principal). Jvm armazena as seguintes informações no method area:
  9. Class Loader - Loading O nome da classe carregada e

    sua classe pai. Todos os arquivos .class relacionados a esta classe Modificadores, Métodos e Variáveis. 1. 2. 3. Temos 3 carregadores de classes integrados disponíveis em Java:
  10. Class Loader - Loading Bootstrap class loader: O carregador de

    classes raiz. É a superclasse do Extension Class Loader e carrega os pacotes Java padrão (lang, util, net, io ..). Esses pacotes padrão estão presentes no arquivo rt.jar e em outras bibliotecas centrais no diretório $ JAVA_HOME / jre / lib.
  11. Class Loader - Loading Extension Class Loader: é a subclasse

    do Bootstrap Class Loader e a superclasse do Application Class Loader. Carrega as extensões das bibliotecas Java padrão presentes no diretório $ JAVA_HOME / jre / lib / ext
  12. Class Loader - Loading Application Class Loader: O último class

    loader e a subclasse do Extension Class Loader. Carrega os arquivos presentes no caminho de classe. O caminho de classe é definido como o diretório atual do aplicativo.
  13. Class Loader - Loading Se um class loader pai não

    puder localizar uma classe, delega o trabalho a um class loader filho. Se o filho não for capaz de carregar a classe, ele lança noClassDefFoundError ou ClassNotFoundException.
  14. Class Loader - Linking Depois que uma classe é carregada

    na memória, ela vai para o processo de linking . O processo de linking envolve combinar os diferentes elementos e dependências do programa.
  15. Class Loader - Linking Verificação: Verifica se um arquivo está

    corretamente formatado e se foi gerado por compilador válido ou não. Se essa verificação falhar, nós recebemos uma runtime exception java.lang.VerifyError. Essa verificação é feita pelo componente ByteCodeVerifier. Quando essa atividade é concluída a classe está pronta para compilação.
  16. Class Loader - Linking Preparação: JVM aloca memória para variáveis

    ​ ​ de classe e inicializa a memória para valores padrão.
  17. Class Loader - Linking Resolução: O processo de substituição de

    referências simbólicas, o processo é feito pesquisando no method area para alocar a entidade de referência.
  18. Class Loader - Initialization Nesta etapa, todas as variáveis ​

    ​ estáticas são atribuídas com seus valores definidos no código e no bloco estático. Esta etapa é executada de cima para baixo em uma classe de pai para filho na hierarquia de classes.
  19. JVM Memory Method area Heap JVM language stacks PC registers

    Native Method stacks Nós temos 5 componentes nessa area:
  20. JVM Memory - Method Area Se a memória disponível nesta

    área não for suficiente para a inicialização do aplicativo, a JVM lança um OutOfMemoryError.
  21. JVM Memory - Method Area Existe apenas uma method area

    por JVM e é um recurso compartilhado.
  22. JVM Memory - Heap Area Aqui estão todos os objetos,

    suas variáveis ​ ​ de instância e matrizes relacionadas. Esta memória é compartilhada por vários threads.
  23. JVM Memory - JVM language Stacks (stack area) Quando uma

    nova thread é criada na JVM, uma pilha de tempo de execução separada também é criada neste momento. (armazena informações específicas da thread criada, que será destruída assim que a thread for finalizada.
  24. JVM Memory - JVM language Stacks Variáveis ​ ​ locais,

    chamadas de método e resultados parciais são armazenados aqui.
  25. JVM Memory - JVM language Stacks Um stackOverFlowError ocorre quando

    um processo que está sendo executado em uma thread requer um tamanho de pilha muito grande que não está disponível.
  26. JVM Memory - JVM language Stacks Para cada chamada de

    método, uma entrada é feita na pilha de memória (stack frame) quando essa chamada de método é concluída, a stack frame é destruído.
  27. JVM Memory - JVM language Stacks Local Variables Operand stack

    Frame Data A stack frame é dividida em 3 partes:
  28. JVM Memory - Local Variables cada frame contém um array

    de variáveis chamado local variables. Todas local variables e seus valores são armazenados aqui. No tempo de compilação o tamanho desse array é determinado.
  29. Heap -> armazena objetos complexos -> em um aplicativo, ele

    é compartilhado por todas as threads -> os dados armazenados na heap podem ser acessados ​ ​ por threads multiplas -> ponteiro para aquele objeto, que é a referência da variável e que está armazenado na pilha
  30. Stack -> cada thread tem sua própria stack -> pode

    ser definida como uma estrutura de dados gerenciada pela JVM -> todas as variáveis ​ ​ locais são criadas na pilha e são automaticamente retiradas da pilha quando você chega ao fechamento do bloco que criou aquela variável -> o dado na stack é restrito para a thread , não pode ser acessada por outras threads da aplicação.
  31. JVM Memory - Operand stack Cada frame contém uma pilha

    LIFO chamada operand stack. Atua como um workspace em tempo de execução para performar operações intermediárias.
  32. JVM Memory - Frame Data Armazena todos os símbolos correspondentes

    ao method area e armazena as informações do bloco catch em caso de exceptions.
  33. PC (Program Counter) Registers Cada thread tem o seu próprio

    PC registers para armazenar o endereço do que está sendo executado no momento na JVM.
  34. PC (Program Counter) Registers Quando a instrução é executada, o

    PC register é atualizado com a próxima instrução.
  35. JVM Memory - Native Method Stacks Para cada nova thread,

    uma nova native method stack é alocada.
  36. Execution Engine A execution engine executa os bytecodes (arquivo .class)

    Lê o bytecode linha a linha Então usa os dados e informação presentes na área de memória para executar instruções.
  37. Execution Engine - Interpreter O interpreter é responsável por ler

    e executar o bytecode linha a linha. Devido a esse processo linha a linha o interpreter é uma das etapas mais demoradas.
  38. Execution Engine - Interpreter Quando um método é chamado múltiplas

    vezes,a cada vez é necessário uma nova interpretação.
  39. Execution Engine - JIT Compiler O JIT compiler ve quais

    partes do código são executadas com maior frequência. (Principalmente quais métodos são chamados com maior frequência). A execução desse código pode ser acelerada se o método já for compilado para código nativo de máquina. A parte do código que já estiver em código de máquina vai rodar mais rápido que o bytecode interpretado.
  40. O que é o código de máquina nativo mesmo? O

    código executável que é compreendido pelo SO, na compilação o bytecode é convertido para o código de máquina nativo (isso em um thread separada), enquanto isso a JVM continua usando a versão interpretada.
  41. Vamos voltar no funcionamento do JIT Quando compilamos nosso programa

    Java, terminamos com nosso código- fonte compilado na representação de um bytecode JVM. Esse bytecode é mais simples e compacto que nosso código-fonte, porém os processadores convencionais em nossos computadores não podem executá-lo.
  42. Vamos voltar no funcionamento do JIT Para poder executar um

    programa Java, a JVM interpreta o bytecode, como os interpretadores geralmente são muito mais lentos do que o código nativo executado em um processador real, a JVM pode executar outro compilador, que agora compilará nosso bytecode no código de máquina, que pode ser executado pelo processador.
  43. Vamos voltar no funcionamento do JIT Esse chamado compilador just-in-time

    é bem mais sofisticado que o compilador javac e executa otimizações complexas para gerar código de máquina de alta qualidade. A implementação do JDK pela Oracle foi baseada no projeto OpenJDK de código aberto. Isso inclui a máquina virtual HotSpot, disponível desde a versão 1.3 do Java. E contém dois compiladores JIT convencionais: o compilador cliente, também chamado C1, e o compilador servidor, chamado opto ou C2.
  44. Vamos voltar no funcionamento do JIT C1 é projetado para

    rodar mais rápido e produzir códigos menos otimizados. Enquanto C2, leva um pouco mais de tempo para rodar, porém produz um código melhor otimizado. O compilador C1 é mais adequado para aplicativos de desktop, pois não queremos longas pausas para a compilação JIT. O compilador de C2 é indicado para aplicativos de servidor de execução longa que podem gastar mais tempo na compilação.
  45. Vamos voltar no funcionamento do JIT Um programa Java, compilado

    por javac, inicia sua execução em modo interpretado. A JVM rastreia cada método chamado com frequência e os compila. Para fazer isso, ele utiliza C1 para a compilação. Porém, o HotSpot ainda observa as chamadas futuras desses métodos. Se o número de chamadas aumentar, a JVM recompilará esses métodos mais uma vez, porém desta vez usando C2.
  46. Vamos voltar no funcionamento do JIT C2 foi extremamente otimizado

    e produz código capaz de competir com C++ ou ser ainda mais rápido. O próprio compilador do servidor é escrito em um dialeto específico de C++.
  47. Segmented Code Cache Surgiu no Java 9 com maneiras de

    organizar e separar os tipos de código compilados em cache, é uma área onde JVM armazena seu bytecode compilado em código nativo. Chamamos cada bloco de código nativo de nmethod , ele pode ser um método Java completo ou parte dele.
  48. Segmented Code Cache Ele tem um tamanho fixo, quando encher

    o JIT será desligado (a app não vai parar). Caso isso aconteça iremos receber o erro: "CodeCache is full… The compiler has been disabled" e teremos uma grande queda de desempenho. Para melhorar isso temporariamente, podemos mudar seu tamanho tendo as seguintes opções:
  49. Segmented Code Cache InitialCodeCacheSize – tamanho inicial do code cache,

    160K default ReservedCodeCacheSize – valor padrão é 48MB CodeCacheExpansionSize – quantidade que pode ser adicionada ao code cache, 32KB or 64KB
  50. Segmented Code Cache Para saber como está o uso do

    code cache temos a opção – XX:+PrintCodeCache e teremos uma saída semelhante a essa: CodeCache: size=xyzKb used=xyz max_used=xyz free=xyzKb
  51. Segmented Code Cache É dividido em 3 partes: non-method segment:

    aramzenda o código interno relacionado à JVM, como o interpretador de bytecode. Por default, este segmento tem cerca de 5 MB. Além disso, é possível configurar o tamanho do segmento por meio do argumento -XX: NonNMethodCodeHeapSize
  52. Segmented Code Cache É dividido em 3 partes: profiled-code segment:

    armazenda o código ligeiramente otimizado com tempos de vida potencialmente curtos. Tem como tamanho padrão 122 MB , podemos alterá-lo por meio do argumento -XX: ProfiledCodeHeapSize
  53. Segmented Code Cache É dividido em 3 partes: non-profiled segment

    : armazena o código totalmente otimizado com tempos de vida potencialmente longos. Tem cerca de 122 MB por padrão. Este valor é, obviamente, configurável por meio do argumento -XX: NonProfiledCodeHeapSize
  54. Execution Engine - Garbage Collector O Garbage Collector (GC) é

    responsável gerenciar de modo automático a alocação de memória da aplicação coordenando junto ao SO a quantidade de memória utilizada, a eliminação de objetos que já não estão mais sendo utilizados e assim determinar quando será necessário realizar uma limpeza para disponibilizar mais recursos.
  55. Execution Engine - Garbage Collector O Garbage Collector (GC) é

    responsável por gerenciar, de modo automático, a alocação de memória da aplicação coordenando junto ao SO a quantidade de memória utilizada e a eliminação de objetos que já não estão mais sendo utilizados.
  56. Mark and Sweep Basicamente, o GC funciona em duas etapas

    simples, conhecidas como Mark and Sweep: Marcação – é aqui que o coletor de lixo identifica quais pedaços de memória estão em uso e quais não estão. Varredura – esta etapa remove objetos identificados durante a fase de “marcação”.
  57. Vantagens Sem manipulação manual de alocação/desalocação de memória, porque o

    espaço de memória não utilizado é tratado automaticamente pelo GC; Gerenciamento automático de vazamento de memória
  58. Desvantagem Como a JVM precisa acompanhar a criação/exclusão de referência

    de objeto, essa atividade requer mais poder de CPU do que o aplicativo original. Isso pode afetar o desempenho de solicitações que exigem grande memória;
  59. Execution Engine - Garbage Collector Serial GC: é destinado para

    aplicações pequenas que executam em ambiente single-thread,é a implementação mais simples do GC. O argumento da JVM para usar o Serial Garbage Collector é -XX:+UseSerialGC
  60. Execution Engine - Garbage Collector Parallel GC: é o tipo

    default de GC da JVM. Usa múltiplas threads para garbage collection, mas permanece pausado quando a aplicação está em execução. O argumento da JVM para usar o Parallel Garbage Collector é - XX:+UseParallelGC.
  61. Execution Engine - Garbage Collector Garbage First (G1): recomendado para

    aplicações multi-thread que tem grande espaço de heap disponível.A heap é dividida em espaços de tamanho iguais, G1 identifica as regiões com maior “lixo” e faz a coleta nessa região O argumento da JVM para usar o G1 Garbage Collector é -XX:+UseG1GC
  62. Native Method Interface É uma interface (como uma ponte) Que

    interage com as Native Method Libraries e provem as native libraries (C, C++).
  63. Native Method Interface Isso permite que a JVM chame bibliotecas

    C/C++ e seja chamada por bibliotecas C/C++
  64. Novidade do Java 19 Foreign Function & Memory API (Preview)

    424: fornece uma API mais fácil de usar e mais geral para trabalhar com código e dados fora da JVM. Veio do projeto Panamá, permite acesso à memória nativa (ou seja, memória fora do heap Java) e acesso ao código nativo (por exemplo, bibliotecas C) diretamente do Java
  65. Native Method Libraries São bibliotecas que são escritas em outras

    linguagens de programação (como assembly, C e C++). Essas bibliotecas geralmente estão presentes na forma de arquivos .dll ou .so.
  66. Threads virtuais As threads virtuais resolvem o problema de ter

    um grande número de threads concorrentes de uma maneira que nos permite escrever código de fácil leitura e manutenção. As threads virtuais parecem threads normais de uma perspectiva de código Java, mas não são mapeadas 1:1 para as threads do sistema operacional. Existe um conjunto dos chamados threads de transporte nos quais uma thread virtual é mapeado temporariamente.
  67. Threads virtuais Quando a thread virtual encontra uma operação de

    bloqueio, a thread virtual é removida da thread transportadora e a thread transportadora pode executar outra thread virtual (uma nova ou bloqueada anteriormente):
  68. Threads virtuais Logo, as operações bloqueantes não bloqueiam a thread

    em execução , então podemos processar um grande número de solicitações em paralelo com somente um pequeno número de threads operadoras
  69. Referências https://www.freecodecamp.org/news/jvm-tutorial-java-virtual-machine-architecture-explained-for- beginners/ https://www.geeksforgeeks.org/jvm-works-jvm-architecture/ https://www.guru99.com/java-virtual-machine-jvm.html Imergindo na JVM - Otávio

    Santana Jornada Java - Capítulos 43 e 44 https://www.baeldung.com/jvm-code-cache https://deviniciative.wordpress.com/2020/02/06/desmistificando-otimizacao-de-jvm/ https://www.udemy.com/course/java-application-performance-and-memory-management/ https://www.baeldung.com/jvm-garbage-collectors https://www.geeksforgeeks.org/garbage-collection-java/ https://www.baeldung.com/graal-java-jit-compiler https://www.happycoders.eu/java/virtual-threads/