Compartilhamento de tecnologia

Artigo científico popular: Entenda o combate real do JVM em um artigo (4) Compreensão aprofundada da análise de fuga Análise de fuga

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Visão geral

Os objetos em Java são alocados na memória heap?

Ok, é muito abstrato. Vamos ser mais específicos. Vamos ver onde a memória do objeto a seguir está alocada.

  1. public void test() {
  2. Object object = new Object();
  3. }
  4. 这个方法中的object对象,是在堆中分配内存么?

Diga o resultado: o objeto pode alocar memória na pilha ou no heap.

Aqui está o ponto principal: na implementação da JVM, para melhorar o desempenho da JVM e economizar espaço de memória, a JVM fornece um recurso chamado “análise de escape”. A análise de escape é uma tecnologia de otimização relativamente de ponta na atualidade. Máquina virtual Java, e também é um JIT Uma técnica de otimização muito importante. O jdk6 apenas começou a introduzir esta tecnologia, o jdk7 começou a habilitar a análise de escape por padrão, o jdk8 começou a melhorar a análise de escape e a habilitou por padrão. Até o JDK 9, a análise de escape será usada como método de otimização padrão e nenhum parâmetro de compilação especial. é requerido.

Agora entenda a frase "o objeto pode alocar memória na pilha ou alocar memória na pilha". Antes do jdk7, o objeto aqui deve alocar memória na pilha no jdk7 e 8, é possível alocar memória na pilha, porque o jdk7; apenas a análise de escape começou a ser suportada; o jdk9 provavelmente está alocado na pilha (o objeto aqui é muito pequeno), porque o jdk9 apenas suporta e permite verdadeiramente a análise de escape por padrão.

Com o desenvolvimento de compiladores JIT (compiladores just-in-time) e a maturidade gradual da tecnologia de análise de escape, a alocação de pilha e a tecnologia de otimização de substituição escalar farão com que "todos os objetos serão alocados no heap" se tornem menos absolutos. Na máquina virtual, os objetos são alocados em memória no heap, mas há um caso especial, ou seja, se após análise de escape for constatado que um objeto não escapa do método, ele poderá ser otimizado para ser alocado na pilha. Quando o método é executado Quando concluído, o quadro da pilha é exibido e o objeto é liberado. Isso elimina a necessidade de alocar memória no heap e passar pela coleta de lixo (.O Hotspot atualmente não faz isso). Essa também é a tecnologia de armazenamento fora do heap mais comum.

Após o JDK 6u23 (versão principal memorável do JDK7), a análise de escape é habilitada por padrão no Hotspot. Se você usar uma versão anterior, poderá exibir a análise de escape por meio da opção "-XX:+DoEscapeAnalysis Pass "-XX:+PrintEscapeAnalysis". Visualize os resultados do filtro para análise de escape.

O Hotspot implementa a substituição escalar por meio da análise de escape (objetos sem escape são substituídos por escalares e agregados, o que pode melhorar a eficiência do código), mas os objetos sem escape ainda alocarão memória no heap, então ainda pode ser dito que todos os objetos são Alocados memória na pilha.

Além disso, profundamente personalizado com base no Open JDKMáquina virtual TaoBao, entre os quais a tecnologia inovadora GCIH (GC invisível heap) implementa fora do heap, movendo objetos com longos ciclos de vida do heap para fora do heap, e o GC não gerencia objetos Java dentro do GCIH, reduzindo assim a frequência de reciclagem do GC e melhorando o propósito da eficiência de recuperação do GC.

Pilha: Quando cada método é executado, um quadro de pilha será criado ao mesmo tempo para armazenar informações como tabelas de variáveis ​​locais, pilhas de operações, conexões dinâmicas, saídas de métodos, etc. O processo de cada método chamado até a execução ser concluída corresponde ao processo de um quadro de pilha, desde ser empurrado para dentro da pilha até ser retirado da pilha na pilha da máquina virtual.

pilha:Quando um objeto é instanciado, o objeto é alocado no heap e uma referência ao heap é colocada na pilha.

escapar:Quando um ponteiro para um objeto é referenciado por vários métodos ou threads, dizemos que o ponteiro escapa. Geralmente, objetos de retorno e variáveis ​​globais geralmente escapam.

Análise de fuga:O método usado para analisar este fenômeno de fuga é chamado de análise de fuga

Otimização da análise de escape - alocação na pilha:A alocação na pilha significa que a instância gerada pela variável local no método (nenhum escape ocorre) é alocada na pilha e não precisa ser alocada no heap. Após a conclusão da alocação, a execução continua na pilha de chamadas. Finalmente, o thread termina, o espaço da pilha é reciclado e os objetos variáveis ​​locais também são reciclados.

processo de alocação de memória de objeto java

  1. Se a alocação na pilha (análise de escape) estiver habilitada, a JVM alocará primeiro na pilha.
  2. Se a alocação na pilha não estiver habilitada ou as condições não forem atendidas, a alocação TLAB será inserida.
  3. Se a alocação TLAB não for bem-sucedida ou for inconsistente, determine se ela pode entrar na alocação da geração antiga.
  4. Se não puder entrar na geração antiga, entrará na alocação do Éden.
  5. Nem todos os objetos são alocados no heap. Além do heap, os objetos também podem ser alocados na pilha e no TLAB. (A maioria dos objetos são alocados no heap)

Os objetos em Java são necessariamente alocados no heap?

Resposta: Não necessariamente.

Se as condições da análise de escape forem atendidas, um objeto poderá ser alocado na pilha.Reduza a alocação de memória heap e a pressão do GC.Como a memória da pilha é limitada, se o objeto atender às condições para substituição escalar,Uma outra operação é realizada no assunto para quebrá-lo em partes.O método específico de substituição escalar é: a JVM irá dividir ainda mais o objeto e decompor o objeto em várias variáveis ​​de membro usadas por este método.Assim, o objetivo de melhor utilização da memória da pilha e dos registros é alcançado.

Como alocar objetos na pilha para a pilha requer análise de escape.

Este é um algoritmo de análise de fluxo de dados global multifuncional que pode reduzir efetivamente a carga de sincronização e a pressão de alocação de heap de memória em programas Java. Através da análise de escape, o compilador Java Hotspot pode analisar o intervalo de uso da referência de um novo objeto e decidir se deve alocar o objeto ao heap.

O comportamento básico da análise de escape é analisar o escopo dinâmico dos objetos:

  1. Quando um objeto é definido em um método e o objeto é usado apenas dentro do método, considera-se que não ocorreu nenhum escape.
  2. Quando um objeto é definido em um método e é referenciado por um método externo, ocorre um escape. Por exemplo, passado como parâmetro de chamada para outros locais.

No princípio de otimização do compilador de linguagem de computador, a análise de escape refere-se ao método de análise da faixa dinâmica de ponteiros. Está relacionada à análise de ponteiro e à análise de forma do princípio de otimização do compilador. Quando uma variável (ou objeto) é alocada em um método, seu ponteiro pode ser retornado ou referenciado globalmente, que será referenciado por outros métodos ou threads. Esse fenômeno é chamado de escape de ponteiro (ou referência). Em termos leigos, se o ponteiro de um objeto for referenciado por vários métodos ou threads, então chamamos o ponteiro (ou objeto) do objeto de Escape (porque neste momento, o objeto escapa do escopo local do método ou thread).

O que é análise de fuga?

Breve descrição: "Análise de escape: uma análise estática que determina o intervalo dinâmico de ponteiros. Ela pode analisar onde no programa o ponteiro pode ser acessado. No contexto da compilação just-in-time da JVM, a análise de escape determinará se o ponteiro pode ser acessado." o objeto recém-criado escapa.

A base para a compilação just-in-time para determinar se um objeto escapa: uma é se o objeto está armazenado no heap (campo estático ou campo de instância do objeto no heap) e a outra é se o objeto é passado para código desconhecido.

A análise de escape é atualmente uma tecnologia de otimização relativamente avançada em máquinas virtuais Java. Assim como a análise de relacionamento de herança de tipo, não é um meio de otimizar código diretamente, mas uma tecnologia de análise que fornece uma base para outros meios de otimização.

Análise de Escape: É uma tecnologia de otimização JIT muito importante, utilizada para determinar se o objeto será acessado fora do método, ou seja, para escapar do escopo do método. A análise de escape é uma etapa do compilador JIT. Através do JIT podemos determinar quais objetos podem ser restritos para uso dentro do método e não escaparão para o exterior. . Ou execute a substituição escalar para dividir um objeto em vários tipos básicos para armazenamento. É um algoritmo de análise de fluxo de dados global multifuncional que pode efetivamente reduzir a carga de sincronização, a alocação de heap de memória e a pressão de coleta de lixo em programas Java. Através da análise de escape, o compilador Java Hotspot pode analisar o intervalo de uso da referência de um novo objeto e decidir se deve alocar esse objeto ao heap.

A análise de escape concentra-se principalmente em variáveis ​​locais para determinar se os objetos alocados no heap escaparam do escopo do método. Está associado à análise de ponteiro e à análise de forma dos princípios de otimização do compilador. Quando uma variável (ou objeto) é alocada em um método, seu ponteiro pode ser retornado ou referenciado globalmente, que será referenciado por outros métodos ou threads. Esse fenômeno é chamado de escape de ponteiro (ou referência). Em termos leigos, se o ponteiro de um objeto for referenciado por vários métodos ou threads, então dizemos que o ponteiro do objeto escapou. Projetar adequadamente a estrutura do código e o uso de dados pode utilizar melhor a análise de escape para otimizar o desempenho do programa. Também podemos reduzir a sobrecarga de alocação de objetos no heap e melhorar a utilização da memória por meio da análise de escape.

A análise de escape é uma técnica usada para determinar se um objeto escapou fora do escopo de um método durante seu tempo de vida. No desenvolvimento Java, a análise de escape é usada para determinar o ciclo de vida e o escopo dos objetos, a fim de realizar a otimização correspondente e melhorar o desempenho do programa e a eficiência de utilização da memória.

Quando um objeto é criado, ele pode ser usado dentro de um método ou pode ser passado para outros métodos ou threads e continuar a existir fora do método. Se o objeto não escapar do escopo do método, a JVM poderá alocá-lo na pilha em vez de no heap, evitando assim a sobrecarga de alocação de memória heap e coleta de lixo.

  1. 关于逃逸分析的论文在1999年就已经发表,但直到Sun JDK 1.6才实现了逃逸分析,而且直到现在这项优化尚未足够成熟,仍有很大的改进余地。不成熟的原因主要是不能保证逃逸分析的性能收益必定高于它的消耗。如果要完全准确的判断一个对象是否会逃逸,需要进行数据流敏感的一系列复杂分析,从而确定程序各个分支执行时对此对象的影响。这是一个相对高耗时的过程,如果分析完后发现没有几个不逃逸的对象,那这些运行期耗用的时间就白白浪费了,所以目前虚拟机只能采用不那么准确,但时间压力相对较小的算法来完成逃逸分析。还有一点是,基于逃逸分析的一些优化手段,如上面提到的“栈上分配”,由于HotSpot虚拟机目前的实现方式导致栈上分配实现起来比较复杂,因此在HotSpot中暂时还没有做这项优化。
  2. 在测试结果中,实施逃逸分析后的程序在MicroBenchmarks中往往能运行出不错的成绩,但是在实际的应用程序,尤其是大型程序中反而发现实施逃逸分析可能出现效果不稳定的情况,或因分析过程耗时但却无法有效判别出非逃逸对象而导致性能(即使编译的收益)有所下降,所以在很长的一段时间里,即使是Server Compiler,也默认不开启逃逸分析(在JDK 1.6 Update 23的Server Compiler中才开始默认开启了逃逸分析),甚至在某些版本(如JDK 1.6 Update 18)中还曾经短暂的完全禁止了这项优化。
  3. 如果有需要,并且确认对程序运行有益,用户可以使用参数-XX:+DoEscapeAnalysis来手动开启逃逸分析,开启之后可以通过参数-XX:+PrintEscapeAnalysis来查看分析结果。有了逃逸分析支持之后,用户可以使用参数-XX:EliminateAllocations来开启标量替换,使用+XX:+EliminateLocks来开启同步消除,使用参数-XX:PrintEliminateAllocations查看标量的替换情况。
  4. 尽管目前逃逸分析的技术仍不是十分成熟,但是他却是即时编译器优化技术的一个重要的方向,在今后的虚拟机中,逃逸分析技术肯定会支撑起一系列使用有效的优化技术。

Princípios básicos de análise de fuga

O princípio básico da análise de escape JVM é determinar a situação de escape do objeto através de dois métodos de análise: estático e dinâmico.

No sistema de compilação Java, o processo de transformar um arquivo de código-fonte Java em uma instrução de máquina executável por computador requer dois estágios de compilação:

A primeira seção da compilação refere-se ao compilador front-endarquivo .javaconvertido paraarquivo .class (arquivo de bytecode). Os produtos do compilador front-end podem ser Javac do JDK ou o compilador incremental no Eclipse JDT.

No segundo estágio de compilação, a JVM interpreta o bytecode e o traduz em instruções de máquina correspondentes, lê o bytecode um por um e o interpreta e traduz em código de máquina um por um.

Obviamente, devido ao processo intermediário de interpretação, sua velocidade de execução será inevitavelmente muito mais lenta do que a de um programa executável de bytecode binário. Esta é a função do intérprete JVM tradicional (Interpreter).

Como eliminar intermediários e melhorar a eficiência?

Para resolver este problema de eficiência, foi introduzida a tecnologia JIT (Just In Time Compiler).

Após a introdução da tecnologia JIT, os programas Java ainda são interpretados e executados através do interpretador. Ou seja, o corpo principal ainda é interpretado e executado, mas os links intermediários são parcialmente removidos.

Compilador JIT (Just-in-timeCompiler) compilação just-in-time. A primeira solução de implementação Java consistia em um conjunto de tradutores (intérpretes) que traduziam cada instrução Java em uma instrução de microprocessador equivalente e as executavam sequencialmente de acordo com a ordem das instruções traduzidas, porque uma instrução Java poderia ser traduzida em uma dúzia ou dezenas de instruções equivalentes do microprocessador, este modo é executado muito lentamente.

Como remover parcialmente os links intermediários?

Quando a JVM descobre que um determinado método ou bloco de código está sendo executado com particular frequência, ela o considerará como um "código de ponto quente". Em seguida, o JIT traduzirá parte do "código ativo" em código de máquina relacionado à máquina local, otimizará e armazenará em cache o código de máquina traduzido para o próximo uso.

Onde armazenar em cache o código de máquina traduzido? Esse cache é chamado de Cache de Código. Pode-se observar que os métodos para alcançar alta simultaneidade entre aplicações JVM e WEB são semelhantes e ainda utilizam arquitetura de cache.

Quando a JVM encontrar o mesmo código quente na próxima vez, ela pulará o link intermediário de interpretação, carregará o código de máquina diretamente do cache de código e o executará diretamente, sem compilar novamente.

Portanto, o objetivo geral do JIT é descobrir o código quente, e o código quente se tornou a chave para melhorar o desempenho. Foi assim que surgiu o nome hotspot JVM. É uma busca constante para identificar o código quente e escrevê-lo no nome.

Portanto, a estratégia geral da JVM é:

  • Para a maioria dos códigos incomuns, não precisamos gastar tempo compilando-os em código de máquina, mas executando-os por meio de interpretação e execução;

  • Por outro lado, para código quente que ocupa apenas uma pequena parte, podemos compilá-lo em código de máquina para atingir a velocidade de execução ideal.

O surgimento do JIT (compilação just in time) e a diferença entre intérpretes

(1) O intérprete interpreta o bytecode em código de máquina. Mesmo que encontre o mesmo bytecode na próxima vez, ele ainda realizará interpretações repetidas.

(2) JIT compila alguns bytecodes em códigos de máquina e os armazena no cache de código. Na próxima vez, ele será executado diretamente, sem compilar novamente.

(3) O intérprete interpreta o bytecode em código de máquina comum a todas as plataformas.

(4) O JIT gerará código de máquina específico da plataforma com base no tipo de plataforma.

JVM contém vários compiladores just-in-time, principalmente C1 e C2, e Graal (experimental).

Vários compiladores just-in-time otimizarão o bytecode e gerarão código de máquina

  • C1 executará otimização simples e confiável de bytecode, incluindo inlining de método, desvirtualização, eliminação de redundância, etc. A velocidade de compilação é mais rápida. Você pode especificar a compilação C1 por meio de -client.
  • C2 realizará otimizações radicais no bytecode, incluindo previsão de frequência de ramificação, apagamento síncrono, etc. A compilação C2 pode ser forçada a ser especificada por meio de -server

A JVM divide o status de execução em 5 níveis:

  • Nível 0, Intérprete

  • Nível 1, compilado e executado usando o compilador just-in-time C1 (sem criação de perfil)

  • Camada 2, compilada e executada usando o compilador just-in-time C1 (com perfil básico)

  • Camada 3, compilada e executada usando o compilador just-in-time C1 (com perfil completo)

  • Nível 4, compilado e executado usando o compilador just-in-time C2

A JVM não habilitará C2 diretamente. Em vez disso, ela primeiro coleta o status de execução do programa por meio da compilação C1 e, em seguida, determina se deve habilitar C2 com base nos resultados da análise.

No modo de compilação em camadas, o status de execução da máquina virtual é dividido em cinco camadas, do simples ao complexo, do rápido ao lento.

Durante a compilação, além de armazenar em cache o código quente para acelerar o processo, o JIT também realizará muitas otimizações no código.

O objetivo de algumas das otimizações éReduza a pressão de alocação de heap de memória , uma das técnicas importantes na otimização JIT é chamada de análise de escape. De acordo com a análise de escape, o compilador just-in-time otimizará o código da seguinte forma durante o processo de compilação:

  • Eliminação de bloqueio: quando um objeto de bloqueio é bloqueado por apenas um thread, o compilador just-in-time removerá o bloqueio
  • Alocação na pilha: quando um objeto não escapa, o objeto será alocado diretamente na pilha. À medida que o thread é reciclado, uma vez que uma grande quantidade de código JVM é alocação de heap, a JVM atual não suporta alocação na pilha. mas usa substituição escalar.
  • Substituição escalar: quando um objeto não escapa, o objeto atual será dividido em diversas variáveis ​​locais e alocado na tabela de variáveis ​​locais da pilha da máquina virtual.

1. A análise estática é uma análise realizada em tempo de compilação

Ele verifica a estrutura estática do código para determinar se o objeto pode escapar. Por exemplo, quando um objeto é atribuído a uma variável membro de uma classe ou retornado a um método externo, pode ser determinado que o objeto escapa.

2. A análise dinâmica é a análise realizada em tempo de execução

Ele determina se um objeto escapa observando o comportamento de chamadas de métodos e referências de objetos. Por exemplo, quando um objeto é referenciado por vários threads, pode-se considerar que o objeto escapou.

A análise de escape realiza uma análise aprofundada do código para determinar se o objeto escapou fora do escopo do método durante o tempo de vida do método. Se o objeto não escapar, a JVM poderá alocá-lo na pilha em vez de no heap.

Status de escape: escape global, escape de parâmetro, sem escape

Um objeto possui três estados de escape: escape global, escape de parâmetro e sem escape.

fuga global(GlobalEscape): Ou seja, o escopo de um objeto escapa do método ou thread atual.

Geralmente existem os seguintes cenários:
① O objeto é uma variável estática
② O objeto é um objeto que escapou
③ O objeto é usado como valor de retorno do método atual

Escape de parâmetro(ArgEscape): Ou seja, um objeto é passado como parâmetro de método ou referenciado por um parâmetro, mas nenhum escape global ocorre durante o processo de chamada. Este estado é determinado pelo bytecode do método chamado.

nenhuma escapatória: Ou seja, o objeto no método não escapa.

O código de amostra do estado de escape é o seguinte:

  1. public class EscapeAnalysisTest {
  2. public static Object globalVariableObject;
  3. public Object instanceObject;
  4. public void globalVariableEscape(){
  5. globalVariableObject = new Object(); // 静态变量,外部线程可见,发生逃逸
  6. }
  7. public void instanceObjectEscape(){
  8. instanceObject = new Object(); // 赋值给堆中实例字段,外部线程可见,发生逃逸
  9. }
  10. public Object returnObjectEscape(){
  11. return new Object(); // 返回实例,外部线程可见,发生逃逸
  12. }
  13. public void noEscape(){
  14. Object noEscape = new Object(); // 仅创建线程可见,对象无逃逸
  15. }
  16. }

Formas de escape: escape de método e escape de thread


1. Escape de método: No corpo de um método, defina uma variável local, que pode ser referenciada por um método externo, como ser passada para um método como parâmetro de chamada ou retornada diretamente como um objeto. Ou pode-se entender que o objeto salta do método.

Os escapes de método incluem:

  • Passe o endereço do objeto para outros métodos chamando parâmetros,
  • O objeto retorna o ponteiro do objeto para outros métodos por meio da instrução return
  • etc.
  1. 我们可以用下面的代码来表示这个现象。
  2. //StringBuffer对象发生了方法逃逸
  3. public static StringBuffer createStringBuffer(String s1, String s2) {
  4. StringBuffer sb = new StringBuffer();
  5. sb.append(s1);
  6. sb.append(s2);
  7. return sb;
  8. }
  9. 上面的例子中,StringBuffer 对象通过return语句返回。
  10. StringBuffer sb是一个方法内部变量,上述代码中直接将sb返回,这样这个StringBuffer有可能被其他方法所改变,这样它的作用域就不只是在方法内部,虽然它是一个局部变量,称其逃逸到了方法外部。
  11. 甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。
  12. 不直接返回 StringBuffer,那么StringBuffer将不会逃逸出方法。
  13. 具体的代码如下:
  14. // 非方法逃逸
  15. public static String createString(String s1, String s2) {
  16. StringBuffer sb = new StringBuffer();
  17. sb.append(s1);
  18. sb.append(s2);
  19. return sb.toString();
  20. }
  21. 可以看出,想要逃逸方法的话,需要让对象本身被外部调用,或者说, 对象的指针,传递到了 方法之外。

Como determinar rapidamente se a análise de escape ocorreu. Vejamos se a nova entidade do objeto é chamada fora do método.

  1. public class EscapeAnalysis {
  2.  
  3.     public EscapeAnalysis obj;
  4.  
  5.     /**
  6.      * 方法返回EscapeAnalysis对象,发生逃逸
  7.      * @return
  8.      */
  9.     public EscapeAnalysis getInstance() {
  10.         return obj == null ? new EscapeAnalysis():obj;
  11.     }
  12.  
  13.     /**
  14.      * 为成员属性赋值,发生逃逸
  15.      */
  16.     public void setObj() {
  17.         this.obj = new EscapeAnalysis();
  18.     }
  19.  
  20.     /**
  21.      * 对象的作用于仅在当前方法中有效,没有发生逃逸
  22.      */
  23.     public void useEscapeAnalysis() {
  24.         EscapeAnalysis e = new EscapeAnalysis();
  25.     }
  26.  
  27.     /**
  28.      * 引用成员变量的值,发生逃逸
  29.      */
  30.     public void useEscapeAnalysis2() {
  31.         EscapeAnalysis e = getInstance();
  32.     }
  33. }

2. Escape de thread: este objeto é acessado por outros threads, como atribuído a uma variável de instância e acessado por outros threads. O objeto escapou do thread atual.

Estratégias de otimização para análise de fuga

A análise de escape pode trazer as seguintes estratégias de otimização para programas Java: alocação na pilha, eliminação de sincronização, substituição escalar e inlining de método;

Parâmetros relacionados à análise de escape:

  1. -XX:+DoEscapeAnalysis 开启逃逸分析
  2. -XX:+PrintEscapeAnalysis 开启逃逸分析后,可通过此参数查看分析结果。
  3. -XX:+EliminateAllocations 开启标量替换
  4. -XX:+EliminateLocks 开启同步消除
  5. -XX:+PrintEliminateAllocations 开启标量替换后,查看标量替换情况。

1. Alocação de pilha

A análise de escape pode determinar quais objetos não escaparão do escopo do método e alocar esses objetos na pilha em vez de no heap. Os objetos alocados na pilha são criados e destruídos dentro do ciclo de vida da chamada de método sem coleta de lixo, melhorando assim a eficiência de execução do programa.

Em circunstâncias normais, os objetos que não podem escapar ocupam um espaço relativamente grande. Se o espaço na pilha puder ser usado, um grande número de objetos será destruído quando o método terminar, reduzindo a pressão do GC.

        Ideias de alocação na pilhaA alocação na pilha é uma tecnologia de otimização fornecida pela JVM.
A ideia é:

  1. Para objetos privados de thread (objetos que não podem ser acessados ​​por outros threads), eles podem ser alocados para memória de pilha em vez de memória heap, que é uma solução para substituir variáveis ​​agregadas por escalares.
  2. A vantagem de ser alocado na pilha é que ele pode ser destruído automaticamente após o término do método, sem a necessidade de intervenção do GC, melhorando o desempenho do sistema.
  3. Para um grande número de objetos dispersos, a alocação na pilha fornece uma boa estratégia de alocação de objetos. A alocação de blocos de velocidade na pilha pode efetivamente evitar o impacto negativo da reciclagem do GC.

Problema: Como a memória da pilha é relativamente pequena, objetos grandes não podem e não são adequados para alocação na pilha.
        Habilitar alocação na pilha
A alocação na pilha é baseada na análise de escape e na substituição escalar, portanto, a análise de escape e a substituição escalar devem estar habilitadas. Obviamente, o JDK1.8 está habilitado por padrão.

  1. 开启逃逸分析:-XX:+DoEscapeAnalysis
  2. 关闭逃逸分析:-XX:-DoEscapeAnalysis
  3. 显示分析结果:-XX:+PrintEscapeAnalysis
  4. 开启标量替换:-XX:+EliminateAllocations
  5. 关闭标量替换:-XX:-EliminateAllocations
  6. 显示标量替换详情:-XX:+PrintEliminateAllocations

Exemplo de alocação na pilha:

  1. 示例1
  2. import java.lang.management.ManagementFactory;
  3. import java.util.List;
  4. /**
  5. * 逃逸分析优化-栈上分配
  6. * 栈上分配,意思是方法内局部变量(未发生逃逸)生成的实例在栈上分配,不用在堆中分配,分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。
  7. * 一般生成的实例都是放在堆中的,然后把实例的指针或引用压入栈中。
  8. *虚拟机参数设置如下,表示做了逃逸分析 消耗时间在10毫秒以下
  9. * -server -Xmx10M -Xms10M
  10. -XX:+DoEscapeAnalysis -XX:+PrintGC
  11. *
  12. *虚拟机参数设置如下,表示没有做逃逸分析 消耗时间在1000毫秒以上
  13. * -server -Xmx10m -Xms10m
  14. -XX: -DoEscapeAnalysis -XX:+PrintGC
  15. * @author 734621
  16. *
  17. */
  18. public class OnStack{
  19. public static void alloc(){
  20. byte[] b=new byte[2];
  21. b[0]=1;
  22. }
  23. public static void main(String [] args){
  24. long b=System.currentTimeMillis();
  25. for(int i=0;i<100000000;i++){
  26. alloc();
  27. }
  28. long e=System.currentTimeMillis();
  29. System.out.println("消耗时间为:" + (e - b));
  30. List<String> paramters = ManagementFactory.getRuntimeMXBean().getInputArguments();
  31. for(String p : paramters){
  32. System.out.println(p);
  33. }
  34. }
  35. }
  36. 加逃逸分析的结果
  37. [GC (Allocation Failure) 2816K->484K(9984K), 0.0013117 secs]
  38. 消耗时间为:7
  39. -Xmx10m
  40. -Xms10m
  41. -XX:+DoEscapeAnalysis
  42. -XX:+PrintGC
  43. 没有加逃逸分析的结果如下:
  44. [GC (Allocation Failure) 3320K->504K(9984K), 0.0003174 secs]
  45. [GC (Allocation Failure) 3320K->504K(9984K), 0.0002524 secs]
  46. 消耗时间为:1150
  47. -Xmx10m
  48. -Xms10m
  49. -XX:-DoEscapeAnalysis
  50. -XX:+PrintGC
  51. 以上测试可以看出,栈上分配可以明显提高效率: 效率是不开启的1150/7= 160
  52. 示例2
  53. 我们通过举例来说明 开启逃逸分析 和 未开启逃逸分析时候的情况
  54. class User {
  55. private String name;
  56. private String age;
  57. private String gender;
  58. private String phone;
  59. }
  60. public class StackAllocation {
  61. public static void main(String[] args) throws InterruptedException {
  62. long start = System.currentTimeMillis();
  63. for (int i = 0; i < 100000000; i++) {
  64. alloc();
  65. }
  66. long end = System.currentTimeMillis();
  67. System.out.println("花费的时间为:" + (end - start) + " ms");
  68. // 为了方便查看堆内存中对象个数,线程sleep
  69. Thread.sleep(10000000);
  70. }
  71. private static void alloc() {
  72. // 未发生逃逸
  73. User user = new User();
  74. }
  75. }
  76. 设置JVM参数,表示未开启逃逸分析
  77. -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
  78. 花费的时间为:664 ms
  79. 然后查看内存的情况,发现有大量的User存储在堆中
  80. 开启逃逸分析
  81. -Xmx1G -Xms1G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails
  82. 然后查看运行时间,我们能够发现花费的时间快速减少,同时不会发生GC操作
  83. 花费的时间为:5 ms
  84. 在看内存情况,我们发现只有很少的User对象,说明User未发生逃逸,因为它存储在栈中,随着栈的销毁而消失。

Em comparação, podemos ver

  • Ative a alocação de pilha e aloque objetos sem escape na memória da pilha, o que obviamente será executado com mais eficiência.
  • Após fechar a alocação na pilha, o GC realiza frequentemente a coleta de lixo.

2. Eliminação de bloqueio

A análise de escape pode detectar que determinados objetos são acessados ​​apenas por um único thread e não escapam para outros threads. Portanto, operações desnecessárias de sincronização podem ser eliminadas e a sobrecarga de execução de programas multithread é reduzida.

Os bloqueios de sincronização consomem muito desempenho; portanto, quando o compilador determinar que um objeto não escapou, ele removerá o bloqueio de sincronização do objeto. O JDK1.8 habilita bloqueios de sincronização por padrão, mas é baseado na habilitação da análise de escape.

  1. -XX:+EliminateLocks #开启同步锁消除(JVM默认状态)
  2. -XX:-EliminateLocks #关闭同步锁消除
  1. 通过示例: 明显可以看到“逃逸分析和锁消除” 对性能的提升
  2. public void testLock(){
  3. long t1 = System.currentTimeMillis();
  4. for (int i = 0; i < 100_000_000; i++) {
  5. locketMethod();
  6. }
  7. long t2 = System.currentTimeMillis();
  8. System.out.println("耗时:"+(t2-t1));
  9. }
  10. public static void locketMethod(){
  11. EscapeAnalysis escapeAnalysis = new EscapeAnalysis();
  12. synchronized(escapeAnalysis) {
  13. escapeAnalysis.obj2="abcdefg";
  14. }
  15. }
  16. 设置JVM参数,开启逃逸分析, 耗时:
  17. java -Xmx64m -Xms64m -XX:+DoEscapeAnalysis
  18. 设置JVM参数,关闭逃逸分析, 耗时:
  19. java -Xmx64m -Xms64m -XX:-DoEscapeAnalysis
  20. 设置JVM参数,关闭锁消除,再次运行
  21. java -Xmx64m -Xms15m -XX:+DoEscapeAnalysis -XX:-EliminateLocks
  22. 设置JVM参数,开启锁消除,再次运行
  23. java -Xmx64m -Xms15m -XX:+DoEscapeAnalysis -XX:+EliminateLocks

O custo da sincronização de threads é bastante alto e a consequência da sincronização é a redução da simultaneidade e do desempenho.

Ao compilar dinamicamente um bloco sincronizado, o compilador JIT pode usar a análise de escape para determinar se o objeto de bloqueio usado pelo bloco sincronizado só pode ser acessado por um thread e não foi liberado para outros threads. Caso contrário, o compilador JIT irá dessincronizar esta parte do código ao compilar este bloco sincronizado. Isso pode melhorar muito a simultaneidade e o desempenho. Este processo de cancelamento da sincronização é chamado de omissão de sincronização, também chamado de eliminação de bloqueio.

  1. 例如下面的代码
  2. public void f() {
  3.     Object hellis = new Object();
  4.     synchronized(hellis) {
  5.         System.out.println(hellis);
  6.     }
  7. }
  8. 代码中对hellis这个对象加锁,但是hellis对象的生命周期只在f()方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉,优化成:
  9. public void f() {
  10.     Object hellis = new Object();
  11.     System.out.println(hellis);
  12. }
  13. 我们将其转换成字节码,此处发现,还是有同步锁的身影,是因为优化是在编译阶段的,在加载进内存后发生。

3. Substituição escalar

A análise de escape pode dividir um objeto em vários escalares, como tipos primitivos ou outros objetos, e atribuí-los em locais diferentes. Isso pode reduzir a fragmentação da memória e a sobrecarga de acesso a objetos, além de melhorar a eficiência da utilização da memória.

Em primeiro lugar, devemos compreender escalares e agregados. As referências a tipos e objetos básicos podem ser entendidas como escalares e não podem ser decompostas posteriormente. A quantidade que pode ser decomposta ainda mais é a quantidade agregada, como: objeto.

O objeto é uma quantidade agregada, que pode ser decomposta em escalares e suas variáveis-membro em variáveis ​​discretas. Isso é chamado de substituição escalar.

Dessa forma, se um objeto não escapar, não há necessidade de criá-lo. Apenas os escalares membros usados ​​por ele serão criados na pilha ou no registro, o que economiza espaço de memória e melhora o desempenho do aplicativo.

A substituição escalar também é habilitada por padrão no JDK1.8, mas também deve ser baseada na habilitação da análise de escape.

Um escalar é um dado que não pode ser dividido em dados menores. O tipo de dados primitivo em Java é escalar.

Por outro lado, os dados que podem ser decompostos são chamados de agregados. Um objeto em Java é um agregado porque pode ser decomposto em outros agregados e escalares.

  1. public static void main(String args[]) {
  2.     alloc();
  3. }
  4. class Point {
  5.     private int x;
  6.     private int y;
  7. }
  8. private static void alloc() {
  9.     Point point = new Point(1,2);
  10.     System.out.println("point.x" + point.x + ";point.y" + point.y);
  11. }
  12. 以上代码,经过标量替换后,就会变成
  13. private static void alloc() {
  14.     int x = 1;
  15.     int y = 2;
  16.     System.out.println("point.x = " + x + "; point.y=" + y);
  17. }

No estágio JIT, se for constatado através da análise de escape que um objeto não será acessado pelo mundo externo, então, após a otimização JIT, o objeto será desmontado em diversas variáveis-membro contidas nele e substituído. Este processo é uma substituição escalar.
Percebe-se que após análise de escape, constatou-se que a grandeza agregada Ponto não escapou, sendo substituída por dois escalares. Então, quais são os benefícios da substituição escalar? Ou seja, pode reduzir bastante o uso de memória heap. Porque uma vez que não há necessidade de criar objetos, não há necessidade de alocar memória heap. A substituição escalar fornece uma boa base para alocação na pilha.

Teste de análise de escape

  1. 逃逸分析测试
  2. 代码如下,大致思路就是 for 循环 1 亿次,循环体内调用外部的 allot() 方法,而 allot() 方法的作用就是简单创建一个对象,但是这个对象是内部的,所以是未逃逸的,所以理论上 JVM 是会进行优化的,我们拭目以待。并且我们会对比开启和关闭逃逸分析之后各自程序的运行时间:
  3. /**
  4. * @ClassName: EscapeAnalysisTest
  5. * @Description: http://www.jetchen.cn 逃逸分析 demo
  6. * @Author: Jet.Chen
  7. * @Date: 2020/11/23 14:26
  8. * @Version: 1.0
  9. **/
  10. public class EscapeAnalysisTest {
  11. public static void main(String[] args) {
  12. long t1 = System.currentTimeMillis();
  13. for (int i = 0; i < 100000000; i++) {
  14. allot();
  15. }
  16. long t2 = System.currentTimeMillis();
  17. System.out.println(t2-t1);
  18. }
  19. private static void allot() {
  20. Jet jet = new Jet();
  21. }
  22. static class Jet {
  23. public String name;
  24. }
  25. }
  26. 上面就是我们进行逃逸分析测试的代码, mian() 方法末尾有一个线程暂停,目的是为了观察此时 JVM 中的内存情况。
  27. Step 1:测试开启逃逸
  28. 由于环境是 jdk1.8,默认开启了逃逸分析,所以直接运行,得到结果如下,程序耗时 3 毫秒:
  29. 此时线程是处于睡眠状态的,我们观察下内存情况,发现堆内存中一共新建了 11 万个 Jet 对象。
  30. Step 2:测试关闭逃逸
  31. 我们关闭逃逸分析再来运行一次(使用 java -XX:-DoEscapeAnalysis EscapeAnalysisTest 来运行代码即可),得到结果如下,程序耗时 400 毫秒:
  32. 此时我们观察下内存情况,发现堆内存中一共新建了 3 千多万个 Jet 对象。
  33. 所以,无论是从代码的执行时间(3 毫秒 VS 400 毫秒),还是从堆内存中对象的数量(11 万个 VS 3 千万个)来分析,在上述场景下,开启逃逸分析是有正向益的。
  34. Step 3:测试标量替换
  35. 我们测试下开启和关闭 标量替换,如下图:
  36. 由上图我们可以看出,在上述极端场景下,开启和关闭标量替换对于性能的影响也是满巨大的,另外,同时也验证了标量替换功能生效的前提是逃逸分析已经开启,否则没有意义。
  37. Step 4:测试锁消除
  38. 测试锁消除,我们需要简单调整下代码,即给 allot() 方法中的内容加锁处理,如下:
  39. private static void allot() {
  40. Jet jet = new Jet();
  41. synchronized (jet) {
  42. jet.name = "jet Chen";
  43. }
  44. }
  45. 然后我们运行测试代码,测试结果也很明显,在上述场景下,开启和关闭锁消除对程序性能的影响也是巨大的。
  46. /**
  47. * 进行两种测试
  48. * 关闭逃逸分析,同时调大堆空间,避免堆内GC的发生,如果有GC信息将会被打印出来
  49. * VM运行参数:-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
  50. *
  51. * 开启逃逸分析 jdk8默认开启
  52. * VM运行参数:-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
  53. *
  54. * 执行main方法后
  55. * jps 查看进程
  56. * jmap -histo 进程ID
  57. *
  58. */
  59. @Slf4j
  60. public class EscapeTest {
  61. public static void main(String[] args) {
  62. long start = System.currentTimeMillis();
  63. for (int i = 0; i < 500000; i++) {
  64. alloc();
  65. }
  66. long end = System.currentTimeMillis();
  67. log.info("执行时间:" + (end - start) + " ms");
  68. try {
  69. Thread.sleep(Integer.MAX_VALUE);
  70. } catch (InterruptedException e1) {
  71. e1.printStackTrace();
  72. }
  73. }
  74. /**
  75. * JIT编译时会对代码进行逃逸分析
  76. * 并不是所有对象存放在堆区,有的一部分存在线程栈空间
  77. * Ponit没有逃逸
  78. */
  79. private static String alloc() {
  80. Point point = new Point();
  81. return point.toString();
  82. }
  83. /**
  84. *同步省略(锁消除) JIT编译阶段优化,JIT经过逃逸分析之后发现无线程安全问题,就会做锁消除
  85. */
  86. public void append(String str1, String str2) {
  87. StringBuffer stringBuffer = new StringBuffer();
  88. stringBuffer.append(str1).append(str2);
  89. }
  90. /**
  91. * 标量替换
  92. *
  93. */
  94. private static void test2() {
  95. Point point = new Point(1,2);
  96. System.out.println("point.x="+point.getX()+"; point.y="+point.getY());
  97. // int x=1;
  98. // int y=2;
  99. // System.out.println("point.x="+x+"; point.y="+y);
  100. }
  101. }
  102. @Data
  103. @AllArgsConstructor
  104. @NoArgsConstructor
  105. class Point{
  106. private int x;
  107. private int y;
  108. }

4. Método embutido

A análise de escape pode determinar que certas chamadas de método não escaparão do escopo do método atual. Portanto, esses métodos podem ser otimizados em linha para reduzir o custo das chamadas de métodos e melhorar a eficiência de execução do programa.

Por meio dessas estratégias de otimização, a análise de escape pode ajudar a JVM a otimizar melhor o código, reduzir a sobrecarga de coleta de lixo, melhorar a eficiência e a capacidade de resposta da execução do programa e reduzir o uso de memória.
 

Cenários práticos de aplicação

A análise de escape possui uma ampla variedade de cenários de aplicativos em aplicativos Java reais. A seguir estão alguns cenários de aplicativos comuns:

  1. Quando um objeto é passado como parâmetro de método, a análise de escape pode determinar se o objeto escapa, determinando assim se o objeto está alocado no heap ou na pilha
    1. Quando um objeto é usado como valor de retorno de método, a análise de escape pode determinar se o objeto escapa, determinando assim se o objeto está alocado no heap ou na pilha.
    1. Quando um objeto é compartilhado por threads, a análise de escape pode determinar se o objeto escapa, determinando assim se as operações de sincronização são necessárias.
    1. Quando um objeto temporário é criado em um loop, a análise de escape pode determinar se o objeto escapa, determinando assim se o objeto precisa ser criado e destruído com frequência.

Desvantagens da análise de fuga

O artigo sobre análise de fuga foi publicado em 1999, mas não foi implementado até o JDK1.6, e essa tecnologia ainda não está muito madura.

A razão fundamental é que não há garantia de que o consumo de desempenho da análise de escape será superior ao seu consumo. Embora a análise de escape possa fazer substituição escalar, alocação de pilha e eliminação de bloqueio. No entanto, a própria análise de fuga também requer uma série de análises complexas, o que é, na verdade, um processo relativamente demorado.

Um exemplo extremo é que após a análise de escape, verifica-se que nenhum objeto não escapa. Então o processo de análise de fuga é desperdiçado.

Embora esta tecnologia não esteja muito madura, é também um meio muito importante na tecnologia de otimização de compiladores just-in-time. Percebi que existem algumas opiniões de que através da análise de escape, a JVM irá alocar objetos na pilha que não irão escapar. Isso é teoricamente possível, mas depende da escolha do designer da JvM. Pelo que eu sei, o Oracle Hotspot JVM não faz isso. Isso foi explicado nos documentos relacionados à análise de escape, portanto, está claro que todas as instâncias de objetos são criadas no heap.

Atualmente, muitos livros ainda são baseados em versões anteriores ao JDK7. O cache de strings internas e variáveis ​​​​estáticas já foram alocados na geração permanente, e a geração permanente foi substituída pela área de metadados. No entanto, o cache de string interno e as variáveis ​​estáticas não são transferidos para a área de metadados, mas são alocados diretamente no heap, portanto, isso também é consistente com a conclusão do ponto anterior: as instâncias do objeto são alocadas no heap. O exemplo acima é acelerado devido à substituição escalar.

Benefícios da análise de fuga


Se um objeto não escapar dentro do corpo do método ou dentro do thread (ou for determinado que ele falhou ao escapar após a análise de escape), as seguintes otimizações poderão ser feitas:

Alocação na pilha:


Em circunstâncias normais, os objetos que não podem escapar ocupam um espaço relativamente grande. Se o espaço na pilha puder ser usado, um grande número de objetos será destruído quando o método terminar, reduzindo a pressão do GC.


Eliminação síncrona:


Se houver um bloqueio de sincronização no método da classe que você define, mas apenas um thread estiver acessando-o em tempo de execução, o código de máquina após a análise de escape será executado sem o bloqueio de sincronização.


Substituição escalar:


Os tipos de dados primitivos na máquina virtual Java (tipos numéricos como int, long e tipos de referência, etc.) não podem ser decompostos posteriormente e podem ser chamados de escalares. Por outro lado, se um dado puder continuar a ser decomposto, ele será chamado de agregado. O agregado mais típico em Java é um objeto. Se a análise de escape provar que um objeto não será acessado externamente e que o objeto é decomponível, o objeto não pode ser criado quando o programa for realmente executado, mas sim criar diretamente várias de suas variáveis ​​de membro usadas por este método para substituir. As variáveis ​​desmontadas podem ser analisadas e otimizadas separadamente. Depois que os atributos são nivelados, não há necessidade de estabelecer relacionamentos por meio de ponteiros de referência. Eles podem ser armazenados de forma contínua e compacta, o que é mais amigável para vários armazenamentos e economiza muito manuseio de dados. execução. Ao mesmo tempo, você também pode alocar espaço no quadro de pilha ou no registro, respectivamente, para que o objeto original não precise alocar espaço como um todo.

resumo


A geração mais jovem é a área onde os objetos nascem, crescem e morrem. Um objeto é gerado e utilizado aqui, e finalmente é coletado pelo coletor de lixo e termina sua vida.

Objetos com ciclos de vida longos colocados na geração antiga são geralmente objetos Java copiados da área sobrevivente. Claro, também existem casos especiais. Sabemos que objetos comuns serão alocados no TLAB, se o objeto for grande, a JVM tentará alocá-lo diretamente para outros locais no Eden; ser capaz de encontrar um espaço livre contínuo suficientemente longo na nova geração, a JVM irá alocá-lo diretamente para a geração antiga. Quando a GC ocorre apenas na geração mais jovem, o ato de reciclar objetos da geração mais jovem é denominado MinorGc.

Quando o GC ocorre na geração antiga, ele é denominado MajorGc ou FullGC. Geralmente, a frequência de ocorrência do MinorGc é muito maior que a do MajorGC, ou seja, a frequência de coleta de lixo na geração antiga será muito menor do que na geração mais jovem.

A análise de escape da JVM usa dois métodos de análise, estático e dinâmico, para determinar se um objeto pode escapar do escopo de um método. Pode ajudar a JVM a otimizar o código e melhorar o desempenho e a eficiência de utilização da memória dos programas Java.

As estratégias de otimização para análise de escape incluem alocação na pilha, eliminação de sincronização, substituição escalar e inlining de método. Essas estratégias de otimização podem reduzir a sobrecarga da coleta de lixo, melhorar a eficiência e a capacidade de resposta da execução do programa e reduzir o uso de memória.

referir-se:

https://zhuanlan.zhihu.com/p/693382698

Blog JVM-Heap-Escape-08-CSDN

Análise de escape de memória JIT_java desativa substituição escalar-CSDN Blog

Veja todos os valores de parâmetros configurados da JVM

java -XX:+PrintFlagsFinal  #输出打印所有参数jvm参数

Insira a descrição da imagem aqui
Insira a descrição da imagem aqui
Insira a descrição da imagem aqui
Insira a descrição da imagem aqui