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

JVM: o que é? Para que ela serve ? E quais os componentes da sua arquitetura?

JVM: o que é? Para que ela serve ? E quais os componentes da sua arquitetura?

More Decks by Kamila de fatima santos oliveira

Other Decks in Programming

Transcript

  1. zup.com.br @zupinnovation >_< TDC Future JVM: o que é? Para

    que ela serve ? E quais os componentes da sua arquitetura?
  2. @zupinnovation zup.com.br <> 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. @zupinnovation zup.com.br <> 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. @zupinnovation zup.com.br <> Linguagens compiladas Compilam e transformam o código

    em binário 🚀. Ou seja, transformam o código em linguagem de máquina, seguindo a arquitetura do sistema. Tem como vantagem a velocidade de execução pela compilação ser focada na arquitetura. Ex: C e C++
  5. @zupinnovation zup.com.br <> 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. Ex: Ruby e Python
  6. @zupinnovation zup.com.br <> E o Java? 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.
  7. @zupinnovation zup.com.br <> E o Java? 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 manter a portabilidade.
  8. @zupinnovation zup.com.br <> É responsável por 3 atividades: ➔ Loading

    ➔ Linking ➔ Initialization Class Loader 🔐 Esse conteúdo é confidencial e não deve ser compartilhado fora da Zup!
  9. @zupinnovation zup.com.br <> 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. Jvm armazena as seguintes informações no method area: ➔ O nome da classe carregada e sua classe pai. ➔ Todos os arquivos .class relacionados a esta classe ➔ Modificadores, Métodos e Variáveis. Class Loader
  10. @zupinnovation zup.com.br <> Bootstrap Class Loader Class Loader - Loading

    Extension Class Loader Application Class Loader 1 2 3 Temos 3 carregadores de classes integrados disponíveis em Java:
  11. @zupinnovation zup.com.br <> 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. 1. Bootstrap class loader: Class Loader - Loading
  12. @zupinnovation zup.com.br <> É 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. 2. Extension Class Loader: Class Loader - Loading
  13. @zupinnovation zup.com.br <> 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. 3. Application Class Loader: Class Loader - Loading
  14. @zupinnovation zup.com.br <> 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 noC lassDefFoundError ou ClassNotFoundException. Class Loader - Loading
  15. @zupinnovation zup.com.br <> 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. Class Loader - Linking ➔ Linking inclui as seguintes etapas: - Verificação - Preparação - Resolução
  16. @zupinnovation zup.com.br <> Verificação Preparação Resoluçã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. JVM aloca memória para variáveis de classe e inicializa a memória para valores padrã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. Class Loader - Linking
  17. @zupinnovation zup.com.br <> 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.
  18. @zupinnovation zup.com.br <> Nós temos 5 componentes nessa área: ➔

    Method area ➔ Heap ➔ JVM language stacks ➔ PC registers ➔ Native Method stacks JVM Memory
  19. @zupinnovation zup.com.br <> ➔ Contém todas as informações das classes,

    como nome, métodos e etc. ➔ Se a memória disponível nesta área não for suficiente para a inicialização do aplicativo, a JVM lança um OutOfMemoryError. ➔ Existe apenas uma method area por JVM e é um recurso compartilhado. JVM Memory - Method Area
  20. @zupinnovation zup.com.br <> ➔ Contém todas as informações das classes,

    como nome, métodos e etc. ➔ Se a memória disponível nesta área não for suficiente para a inicialização do aplicativo, a JVM lança um OutOfMemoryError. ➔ Existe apenas uma method area por JVM e é um recurso compartilhado. JVM Memory - Method Area
  21. @zupinnovation zup.com.br <> ➔ Contém todas as informações das classes,

    como nome, métodos e etc. ➔ Se a memória disponível nesta área não for suficiente para a inicialização do aplicativo, a JVM lança um OutOfMemoryError. ➔ Existe apenas uma method area por JVM e é um recurso compartilhado. JVM Memory - Method Area
  22. @zupinnovation zup.com.br <> JVM Memory ➔ Aqui estão todos os

    objetos, suas variáveis de instância e matrizes relacionadas. ➔ Esta memória é compartilhada por vários threads. Heap Area
  23. @zupinnovation zup.com.br <> ➔ 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). ➔ Variáveis locais, chamadas de método e resultados parciais são armazenados aqui. ➔ 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. ➔ 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ída. JVM Memory - JVM language Stacks (stack area)
  24. @zupinnovation zup.com.br <> ➔ 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). ➔ Variáveis locais, chamadas de método e resultados parciais são armazenados aqui. ➔ 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. ➔ 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ída. JVM Memory - JVM language Stacks (stack area)
  25. @zupinnovation zup.com.br <> ➔ 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). ➔ Variáveis locais, chamadas de método e resultados parciais são armazenados aqui. ➔ 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. ➔ 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ída. JVM Memory - JVM language Stacks (stack area)
  26. @zupinnovation zup.com.br <> ➔ 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). ➔ Variáveis locais, chamadas de método e resultados parciais são armazenados aqui. ➔ 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. ➔ 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ída. JVM Memory - JVM language Stacks (stack area)
  27. @zupinnovation zup.com.br <> Operand stack Frame Data Local Variables A

    stack frame é dividida em 3 partes: JVM Memory - JVM language Stacks (stack area)
  28. @zupinnovation zup.com.br <> JVM Memory ➔ Cada frame contém um

    array de variáveis chamada local variables. ➔ Todas as local variables e seus valores são armazenados aqui. Local Variables
  29. ➔ Armazena objetos complexos; ➔ Em um aplicativo, ele é

    compartilhado por todas as threads; ➔ Os dados armazenados na heap podem ser acessados por threads múltiplas; ➔ Ponteiro para aquele objeto, que é a referência da variável e que está armazenado na pilha. Heap ➔ 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. Stack
  30. 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. JVM Memory - Operand stack Armazena todos os símbolos correspondentes ao method area e armazena as informações do bloco catch em caso de exceptions. JVM Memory - Frame Data
  31. @zupinnovation zup.com.br <> PC Cada thread tem o seu próprio

    PC registers para armazenar o endereço do que está sendo executado no momento na JVM. (Program Counter) Registers
  32. @zupinnovation zup.com.br <> JVM Memory Para cada nova thread, uma

    nova native method stack é alocada. Native Method Stacks
  33. 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. Execution Engine - Interpreter ➔ A execution engine executa os bytecodes (arquivo .class); ➔ Lê o bytecode linha a linha; ➔ Então usa os dados e informações presentes na área de memória para executar instruções. Execution Engine
  34. O JIT compiler vê 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. Execution Engine - JIT Compiler
  35. @zupinnovation zup.com.br <> 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.
  36. @zupinnovation zup.com.br <> Vamos voltar no detalhe do 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.
  37. @zupinnovation zup.com.br <> Vamos voltar no detalhe do 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.
  38. @zupinnovation zup.com.br <> Vamos voltar no detalhe do 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.
  39. @zupinnovation zup.com.br <> Vamos voltar no detalhe do 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.
  40. @zupinnovation zup.com.br <> Vamos voltar no detalhe do 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. ➔ 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++.
  41. @zupinnovation zup.com.br <> ➔ É um projeto de pesquisa criado

    pela Oracle. Podemos olhar para o Graal como vários projetos conectados: um novo compilador JIT que se baseia no HotSpot e uma nova máquina virtual poliglota. ➔ Ele fornece um ecossistema abrangente que suporta um grande conjunto de linguagens (Java e outras linguagens baseadas em JVM; JavaScript, Ruby, Python, R, C/C++) Graal
  42. @zupinnovation zup.com.br <> ➔ Existem várias vantagens importantes de escrever

    um compilador em Java. Dentre elas, a segurança, que significa que não há travamentos, mas exceções, e nenhum vazamento de memória real. ➔ E o compilador pode ser independente do HotSpot e seria capaz de produzir uma versão compilada por JIT mais rápida de si mesmo. Graal
  43. @zupinnovation zup.com.br <> ➔ Ele utiliza a nova JVM Compiler

    Interface – JVMCI para se comunicar com a VM. Para habilitar o uso do novo compilador JIT, precisamos definir as seguintes opções ao executar o Java a partir da linha de comando: -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler Graal
  44. @zupinnovation zup.com.br <> Ou seja, podemos executar um programa simples

    de três maneiras diferentes: 1. Com os compiladores regulares; 2. Com a versão JVMCI do Graal no Java 10; 3. Ou com o próprio GraalVM. Graal
  45. @zupinnovation zup.com.br <> Surgiu no Java 9 com formas de

    organizar e separar os tipos de código compilados em cache. É uma área onde JVM armazena seu bytecode compilado em código nativo. Segmented Code Cache
  46. @zupinnovation zup.com.br <> • 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: - 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. Segmented Code Cache
  47. @zupinnovation zup.com.br <> 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 Segmented Code Cache
  48. @zupinnovation zup.com.br <> Segmented Code Cache Non-method segment: Armazena 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 1 É dividido em 3 partes: Profiled-code segment: Armazena 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 2 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 3
  49. @zupinnovation zup.com.br <> 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. 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”.
  50. @zupinnovation zup.com.br <> Garbage Collector 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 Desvantagens: • 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;
  51. @zupinnovation zup.com.br <> Serial Garbage Collector Esta é a implementação

    de GC mais simples, porque funciona basicamente com uma única thread. Como resultado, essa implementação de GC congela todos os encadeamentos do aplicativo quando é executado. O Serial GC é o GC preferido para a maioria dos aplicativos que não têm requisitos de tempo de pausa pequenos e são executados em máquinas de estilo cliente. Para habilitar o Serial Garbage Collector, podemos usar o seguinte argumento: • Java -XX:+UseSerialGC -jar Application.java
  52. @zupinnovation zup.com.br <> Parallel Garbage Collector É o GC default

    da JVM. Ao contrário do Serial Garbage Collector, ele usa várias threads para gerenciar o espaço de heap, mas também congela outros threads de aplicativos enquanto executa o GC . Se usarmos este GC, podemos especificar o máximo de threads de GC e tempo de pausa e área de cobertura (tamanho do heap). Os números de encadeamentos do coletor de lixo podem ser controlados com a opção de linha de comando -XX:ParallelGCThreads=<N>.
  53. @zupinnovation zup.com.br <> Parallel Garbage Collector O tempo máximo de

    pausa (intervalo, em milissegundos, entre dois GCs) é especificado com a linha de comando -XX:MaxGCPauseMillis=<N> O tempo gasto na coleta de lixo versus o tempo gasto fora da coleta de lixo é chamado de destino de rendimento máximo e pode ser especificado pela linha de comando -XX:GCTimeRatio=<N>
  54. @zupinnovation zup.com.br <> Parallel Garbage Collector O espaço de heap

    máximo (a quantidade de memória heap que um programa necessita durante a execução) é especificado usando a opção -Xmx<N>. Para habilitar o Parallel Garbage Collector, podemos usar o seguinte argumento: • java -XX:+UseParallelGC -jar Application.java
  55. @zupinnovation zup.com.br <> G1 Garbage Collector G1 (Garbage First) Garbage

    Collector é projetado para aplicativos executados em máquinas com vários processadores com grande espaço de memória. Está disponível no JDK7 Update 4 e em versões posteriores. Ao contrário de outros coletores, o coletor G1 divide o heap em um conjunto de regiões de heap de tamanho igual, cada uma delas um intervalo contíguo de memória virtual. Para habilitar o G1 Garbage Collector, podemos usar o seguinte comando: • java -XX:+UseG1GC -jar Application.java
  56. @zupinnovation zup.com.br <> Z Garbage Collector ZGC (Z Garbage Collector)

    é um coletor de lixo escalável de baixa latência que estreou no Java 11 como uma opção experimental para Linux. O JDK 14 introduziu o ZGC nos sistemas operacionais Windows e macOS. O ZGC obteve o status de produção do Java 15 em diante. O ZGC executa todo o trabalho simultaneamente, sem interromper a execução de threads de aplicativos por mais de 10 ms, o que o torna adequado para aplicativos que exigem baixa latência. A coloração de referência é o conceito central do ZGC. Isso significa que o ZGC usa alguns bits (bits de metadados) de referência para marcar o estado do objeto.
  57. @zupinnovation zup.com.br <> Z Garbage Collector Para habilitar o Z

    Garbage Collector, podemos usar o seguinte argumento nas versões do JDK inferiores a 15: • java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC Application.java A partir da versão 15: • java -XX:+UseZGC Application.java
  58. @zupinnovation zup.com.br <> Native Method Interface É uma interface (como

    uma ponte) que interage com as Native Method Libraries e provem as native libraries (C, C++).
  59. @zupinnovation zup.com.br <> Novidade 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.
  60. @zupinnovation zup.com.br <> Novidade Java 19 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
  61. @zupinnovation zup.com.br <> 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.
  62. @zupinnovation zup.com.br <> Threads Virtuais : Java 19 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.
  63. @zupinnovation zup.com.br <> Threads Virtuais : Java 19 Existe um

    conjunto dos chamados threads de transporte nos quais uma thread virtual é mapeado temporariamente.
  64. @zupinnovation zup.com.br <> Threads Virtuais : Java 19 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):
  65. @zupinnovation zup.com.br <> Threads Virtuais : Java 19 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
  66. @zupinnovation zup.com.br <> 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/ • Referências: <a target="_blank" href="https://icons8.com/icon/98964/youtube">YouTube</a> icon by <a target="_blank" href="https://icons8.com">Icons8</a> • https://www.baeldung.com/graal-java-jit-compiler • https://www.happycoders.eu/java/virtual-threads/