Eliminação de bloqueio + análise de escape

 Se você pode confirmar que um objeto bloqueado não escapará do escopo local, poderá excluir o bloqueio. Isso significa que esse objeto pode ser acessado apenas por um thread por vez, portanto, não há necessidade de impedir que outros threads o acessem. Nesse caso, o bloqueio pode ser excluído. Isso é chamado de eliminação de bloqueio.Este artigo é uma série de artigos sobre o mecanismo de implementação da JVM, que é exatamente o tópico de hoje.

Como todos sabemos, java.lang.StringBuffer é uma classe segura para threads que usa métodos síncronos, e pode ser usada para interpretar bem a eliminação de bloqueios. O StringBuffer foi introduzido no Java 1.0 e pode ser usado para costurar com eficiência objetos de sequência imutáveis. Ele sincroniza todos os métodos de acréscimo para garantir que, quando vários encadeamentos gravem no mesmo objeto StringBuffer ao mesmo tempo, ele possa garantir que a sequência na construção possa ser criada com segurança.

Muitos programas, na verdade, não precisam dessa camada de garantia de segurança de encadeamento, portanto, no Java 5, foi introduzida uma classe java.lang.StringBuilder não sincronizada como alternativa. Ambas as classes herdam a classe java.lang.AbstractStringBuilder do pacote private (note: em termos simples, a classe sem modificadores) e a implementação do método append também é muito semelhante.

A diferença está na operação de sincronização do StringBuffer:

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

Compare com StringBuilder:

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

O thread que chama o método de acréscimo de StringBuffer deve adquirir o bloqueio interno (também chamado de bloqueio do monitor) deste objeto para inserir o método.O bloqueio também deve ser liberado antes de sair do método. O StringBuilder não precisa executar esta operação, portanto, seu desempenho de execução é superior ao do StringBuffer - pelo menos à primeira vista.

No entanto, depois que a máquina virtual HotSpot introduziu a análise de escape, quando o método de sincronização de um objeto como StringBuffer foi chamado, o bloqueio pode ser eliminado automaticamente. Isso aparecerá apenas nos objetos criados dentro do domínio do método e somente então poderá ser garantido que nenhuma fuga ocorrerá.

O teste de desempenho Java geralmente usa o Java Microbenchmark Harness (JMH). Vamos usar o JMH para testar, quando a JVM moderna pode confirmar que o objeto StringBuffer só pode ser acessado por um encadeamento, como reduz a diferença de desempenho ao eliminar o bloqueio no StringBuffer.

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

A eliminação de bloqueio é uma otimização muito eficaz, pois é ativada por padrão no Java 8, mas você também pode desativá-lo pelo parâmetro da VM -XX: -DoEscapeAnalysis, para poder ver o efeito da otimização. Depois de ativar a análise de escape (padrão), o desempenho do StringBuffer e do StringBuilder é basicamente o mesmo. (O relatório de resultados conta o número de operações realizadas por segundo. Uma pontuação mais alta indica melhor desempenho.)

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

Como mostrado acima, após a análise de escape ser desativada, o código StringBuffer fica cerca de 15% mais lento - e essa diferença se deve principalmente à operação de bloqueio quando o método append () é chamado.

Bloqueio de bloqueio

As máquinas virtuais HotSpot também possuem algumas técnicas adicionais de otimização de bloqueios, embora não façam parte tecnicamente do subsistema de análise de escape, mas também melhoram o desempenho dos bloqueios internos ao analisar o escopo. Ao adquirir sucessivamente bloqueios do mesmo objeto, a máquina virtual HotSpot verifica se várias áreas de bloqueio podem ser combinadas em uma área de bloqueio maior. Essa agregação é chamada de bloqueio de bloqueio, que pode reduzir o consumo de bloqueio e desbloqueio.

Quando a JVM do HotSpot descobrir que precisa estar bloqueada, tentará localizar a operação de desbloqueio do mesmo objeto para a frente. Se for possível, ele considerará se as duas áreas de bloqueio serão mescladas e excluir um conjunto de operações de desbloqueio / bloqueio.

Vejamos um programa que adquire continuamente bloqueios de monitor para o mesmo objeto:

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

Seu bytecode é o seguinte, que parece muito detalhado:

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

[O comentário no final do código corresponde à seguinte linha de saída. - Ed.]

Vamos revisar primeiro, os códigos de bytes correspondentes ao bloqueio interno da operação são monitorenter e monitorexit.

Cada instrução do monitorenter no bytecode corresponde a duas instruções de monitorexit, que correspondem respectivamente a diferentes caminhos de execução. O motivo é que a primeira instrução de monitorexit libera o bloqueio do monitor quando sai normalmente da área de bloqueio e a segunda instrução libera-o quando sai anormalmente.

Esse bytecode pode parecer estranho porque há apenas uma operação de incremento da variável int no bloco de sincronização no programa de origem. Não há nenhuma exceção lançada no código, mas pode realmente sair da área de bloqueio de forma anormal. (Isso pode acontecer se o encadeamento capturar uma InterruptedException, como chamar o método stop () do encadeamento em execução. Portanto, é necessário um segundo caminho de execução para garantir que o bloqueio do monitor possa ser liberado, mesmo se for desmarcado. O mesmo vale para exceções não verificadas. Você pode aprender mais com a especificação da JVM.) O bloqueio de bloqueio é ativado por padrão, mas também pode ser desativado iniciando o parâmetro -XX: -EliminateLocks.

Bloqueio aninhado

Os blocos de sincronização podem ser aninhados um por um, e também é possível que dois blocos usem o bloqueio do monitor do mesmo objeto para sincronização. Essa situação é chamada de bloqueio aninhado.A máquina virtual HotSpot pode reconhecer e excluir o bloqueio no bloco interno. Um encadeamento já adquiriu a trava ao entrar no bloco externo; portanto, quando tenta entrar no bloco interno, ainda deve reter a trava, para que seja possível excluir a trava nesse momento.

No momento da gravação, a exclusão do bloqueio aninhado no Java 8 só pode ocorrer quando o bloqueio é declarado como estático final ou o bloqueio é esse objeto.

A seguir, é apresentado um exemplo de exclusão de um bloqueio interno ao encontrar um bloco de sincronização aninhado:

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

A máquina virtual do HotSpot exclui o bloqueio aninhado interno; portanto, esse código acabará se tornando assim:

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

A otimização de bloqueio aninhado é ativada por padrão, mas também pode ser desativada iniciando o parâmetro -XX: -EliminateNestedLocks.

Análise de matriz e escape

O espaço alocado no não heap é armazenado na pilha ou apenas nos registros da CPU.Esses são recursos relativamente escassos, portanto a análise de escape, como outras otimizações, definitivamente enfrentará comprometimento (na implementação). Uma limitação padrão na JVM do HotSpot é que matrizes maiores que 64 elementos não serão otimizadas para análise de escape. Esse tamanho pode ser controlado pelo parâmetro de inicialização -XX: EliminateAllocationArraySizeLimit = n, em que n é o tamanho da matriz.

Supondo um código de ponto de acesso, ele alocará uma matriz temporária para ler dados do cache. Se a análise de escape descobrir que o escopo dessa matriz não escapa ao corpo do método, ele não alocará memória no heap. No entanto, se o tamanho da matriz exceder 64 (mesmo que nem todos sejam usados), ele ainda será armazenado no heap. Dessa maneira, a otimização da análise de escape da matriz não funcionará e a memória ainda será alocada do heap.

No teste de benchmark JMH a seguir, o método de teste criará novas matrizes sem escape dos tamanhos 63, 64 e 65, respectivamente. (A matriz de tamanho 63 também participou do teste para provar que a matriz 64 é mais rápida que 65, não por causa do alinhamento da memória.)

Cada rodada de teste usava apenas os dois primeiros elementos da matriz, ou seja, um [0] e um [1]. Deve-se notar que a análise de escape é limitada apenas pelo comprimento da matriz e não importa quantos elementos são realmente usados.

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

A partir dos resultados, quando a alocação do array não puder se beneficiar da otimização da análise de escape, o desempenho cairá significativamente. (A pontuação aqui também é o número de operações por segundo, quanto maior a pontuação, melhor o desempenho.)

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

Se você precisar alocar uma matriz maior no código do ponto de acesso, poderá configurar a máquina virtual para otimizar a matriz grande. Ajuste o limite superior do elemento para 65 e execute o benchmark novamente para descobrir que o desempenho também está alinhado.

Linha de comando:

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

O resultado da execução é:

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

Você pode ver o resultado é o mesmo.

Conclusão

Este artigo e o artigo anterior sobre análise de escape mostram algumas das mágicas por trás da JVM do HotSpot. Ao mesmo tempo, você também pode ver a complexidade por trás dessas otimizações. Todo release principal do Java adiciona alguns novos recursos à JVM.

De fato, a Oracle também está estudando a próxima geração de tecnologia de compilação. É o Graal, que é um compilador just-in-time (JIT) implementado em Java, extensível e plugável. É uma parte importante do projeto Metropolis.O objetivo deste projeto é usar a linguagem Java o máximo possível para criar programas de tempo de execução da JVM.

Conforme mencionado no JEP 317, o compilador Graal é um novo recurso experimental do Java 10. Seu principal objetivo é permitir que desenvolvedores e líderes de plataformas profissionais escrevam seus próprios compiladores JIT que atendam às suas próprias necessidades especiais. Para a conclusão do projeto e protótipo de novas tecnologias de otimização, o Graal é uma plataforma muito adequada.

Os métodos de análise de escopo mencionados neste artigo e no artigo anterior podem ser usados ​​para implementar muitas técnicas de otimização. A primeira é a eliminação da alocação (ou seja, a substituição escalar. Nota: refere-se à decomposição de objetos em tipos básicos, como int, aloca espaço na pilha e nos registradores, para que você não possa alocar memória na pilha e não precisa do GC Reciclado) e as tecnologias relacionadas a bloqueios que discutimos. Estes são apenas alguns exemplos da tecnologia de compilação JIT fornecida pelo compilador C2 maduro na JVM HotSpot. Os artigos subsequentes também apresentarão algumas outras técnicas usadas para melhorar o desempenho do código na JVM do HotSpot.

 Se você pode confirmar que um objeto bloqueado não escapará do escopo local, poderá excluir o bloqueio. Isso significa que esse objeto pode ser acessado apenas por um thread por vez, portanto, não há necessidade de impedir que outros threads o acessem. Nesse caso, o bloqueio pode ser excluído. Isso é chamado de eliminação de bloqueio.Este artigo é uma série de artigos sobre o mecanismo de implementação da JVM, que é exatamente o tópico de hoje.

Como todos sabemos, java.lang.StringBuffer é uma classe segura para threads que usa métodos síncronos, e pode ser usada para interpretar bem a eliminação de bloqueios. O StringBuffer foi introduzido no Java 1.0 e pode ser usado para costurar com eficiência objetos de sequência imutáveis. Ele sincroniza todos os métodos de acréscimo para garantir que, quando vários encadeamentos gravem no mesmo objeto StringBuffer ao mesmo tempo, ele possa garantir que a sequência na construção possa ser criada com segurança.

Muitos programas, na verdade, não precisam dessa camada de garantia de segurança de encadeamento, portanto, no Java 5, foi introduzida uma classe java.lang.StringBuilder não sincronizada como alternativa. Ambas as classes herdam a classe java.lang.AbstractStringBuilder do pacote private (note: em termos simples, a classe sem modificadores) e a implementação do método append também é muito semelhante.

A diferença está na operação de sincronização do StringBuffer:

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

Compare com StringBuilder:

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

O thread que chama o método de acréscimo de StringBuffer deve adquirir o bloqueio interno (também chamado de bloqueio do monitor) deste objeto para inserir o método.O bloqueio também deve ser liberado antes de sair do método. O StringBuilder não precisa executar esta operação, portanto, seu desempenho de execução é superior ao do StringBuffer - pelo menos à primeira vista.

No entanto, depois que a máquina virtual HotSpot introduziu a análise de escape, quando o método de sincronização de um objeto como StringBuffer foi chamado, o bloqueio pode ser eliminado automaticamente. Isso aparecerá apenas nos objetos criados dentro do domínio do método e somente então poderá ser garantido que nenhuma fuga ocorrerá.

O teste de desempenho Java geralmente usa o Java Microbenchmark Harness (JMH). Vamos usar o JMH para testar, quando a JVM moderna pode confirmar que o objeto StringBuffer só pode ser acessado por um encadeamento, como reduz a diferença de desempenho ao eliminar o bloqueio no StringBuffer.

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

A eliminação de bloqueio é uma otimização muito eficaz, pois é ativada por padrão no Java 8, mas você também pode desativá-lo pelo parâmetro da VM -XX: -DoEscapeAnalysis, para poder ver o efeito da otimização. Depois de ativar a análise de escape (padrão), o desempenho do StringBuffer e do StringBuilder é basicamente o mesmo. (O relatório de resultados conta o número de operações realizadas por segundo. Uma pontuação mais alta indica melhor desempenho.)

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

Como mostrado acima, após a análise de escape ser desativada, o código StringBuffer fica cerca de 15% mais lento - e essa diferença se deve principalmente à operação de bloqueio quando o método append () é chamado.

Bloqueio de bloqueio

As máquinas virtuais HotSpot também possuem algumas técnicas adicionais de otimização de bloqueios, embora não façam parte tecnicamente do subsistema de análise de escape, mas também melhoram o desempenho dos bloqueios internos ao analisar o escopo. Ao adquirir sucessivamente bloqueios do mesmo objeto, a máquina virtual HotSpot verifica se várias áreas de bloqueio podem ser combinadas em uma área de bloqueio maior. Essa agregação é chamada de bloqueio de bloqueio, que pode reduzir o consumo de bloqueio e desbloqueio.

Quando a JVM do HotSpot descobrir que precisa estar bloqueada, tentará localizar a operação de desbloqueio do mesmo objeto para a frente. Se for possível, ele considerará se as duas áreas de bloqueio serão mescladas e excluir um conjunto de operações de desbloqueio / bloqueio.

Vejamos um programa que adquire continuamente bloqueios de monitor para o mesmo objeto:

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

Seu bytecode é o seguinte, que parece muito detalhado:

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

[O comentário no final do código corresponde à seguinte linha de saída. - Ed.]

Vamos revisar primeiro, os códigos de bytes correspondentes ao bloqueio interno da operação são monitorenter e monitorexit.

Cada instrução do monitorenter no bytecode corresponde a duas instruções de monitorexit, que correspondem respectivamente a diferentes caminhos de execução. O motivo é que a primeira instrução de monitorexit libera o bloqueio do monitor quando sai normalmente da área de bloqueio e a segunda instrução libera-o quando sai anormalmente.

Esse bytecode pode parecer estranho porque há apenas uma operação de incremento da variável int no bloco de sincronização no programa de origem. Não há nenhuma exceção lançada no código, mas pode realmente sair da área de bloqueio de forma anormal. (Isso pode acontecer se o encadeamento capturar uma InterruptedException, como chamar o método stop () do encadeamento em execução. Portanto, é necessário um segundo caminho de execução para garantir que o bloqueio do monitor possa ser liberado, mesmo se for desmarcado. O mesmo vale para exceções não verificadas. Você pode aprender mais com a especificação da JVM.) O bloqueio de bloqueio é ativado por padrão, mas também pode ser desativado iniciando o parâmetro -XX: -EliminateLocks.

Bloqueio aninhado

Os blocos de sincronização podem ser aninhados um por um, e também é possível que dois blocos usem o bloqueio do monitor do mesmo objeto para sincronização. Essa situação é chamada de bloqueio aninhado.A máquina virtual HotSpot pode reconhecer e excluir o bloqueio no bloco interno. Um encadeamento já adquiriu a trava ao entrar no bloco externo; portanto, quando tenta entrar no bloco interno, ainda deve reter a trava, para que seja possível excluir a trava nesse momento.

No momento da gravação, a exclusão do bloqueio aninhado no Java 8 só pode ocorrer quando o bloqueio é declarado como estático final ou o bloqueio é esse objeto.

A seguir, é apresentado um exemplo de exclusão de um bloqueio interno ao encontrar um bloco de sincronização aninhado:

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

A máquina virtual do HotSpot exclui o bloqueio aninhado interno; portanto, esse código acabará se tornando assim:

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

A otimização de bloqueio aninhado é ativada por padrão, mas também pode ser desativada iniciando o parâmetro -XX: -EliminateNestedLocks.

Análise de matriz e escape

O espaço alocado no não heap é armazenado na pilha ou apenas nos registros da CPU.Esses são recursos relativamente escassos, portanto a análise de escape, como outras otimizações, definitivamente enfrentará comprometimento (na implementação). Uma limitação padrão na JVM do HotSpot é que matrizes maiores que 64 elementos não serão otimizadas para análise de escape. Esse tamanho pode ser controlado pelo parâmetro de inicialização -XX: EliminateAllocationArraySizeLimit = n, em que n é o tamanho da matriz.

Supondo um código de ponto de acesso, ele alocará uma matriz temporária para ler dados do cache. Se a análise de escape descobrir que o escopo dessa matriz não escapa ao corpo do método, ele não alocará memória no heap. No entanto, se o tamanho da matriz exceder 64 (mesmo que nem todos sejam usados), ele ainda será armazenado no heap. Dessa maneira, a otimização da análise de escape da matriz não funcionará e a memória ainda será alocada do heap.

No teste de benchmark JMH a seguir, o método de teste criará novas matrizes sem escape dos tamanhos 63, 64 e 65, respectivamente. (A matriz de tamanho 63 também participou do teste para provar que a matriz 64 é mais rápida que 65, não por causa do alinhamento da memória.)

Cada rodada de teste usava apenas os dois primeiros elementos da matriz, ou seja, um [0] e um [1]. Deve-se notar que a análise de escape é limitada apenas pelo comprimento da matriz e não importa quantos elementos são realmente usados.

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

A partir dos resultados, quando a alocação do array não puder se beneficiar da otimização da análise de escape, o desempenho cairá significativamente. (A pontuação aqui também é o número de operações por segundo, quanto maior a pontuação, melhor o desempenho.)

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

Se você precisar alocar uma matriz maior no código do ponto de acesso, poderá configurar a máquina virtual para otimizar a matriz grande. Ajuste o limite superior do elemento para 65 e execute o benchmark novamente para descobrir que o desempenho também está alinhado.

Linha de comando:

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

O resultado da execução é:

 

Falta e preenchimento de lacunas, otimização de JVM, eliminação de bloqueio + análise de escape

 

 

Você pode ver o resultado é o mesmo.

Conclusão

Este artigo e o artigo anterior sobre análise de escape mostram algumas das mágicas por trás da JVM do HotSpot. Ao mesmo tempo, você também pode ver a complexidade por trás dessas otimizações. Todo release principal do Java adiciona alguns novos recursos à JVM.

De fato, a Oracle também está estudando a próxima geração de tecnologia de compilação. É o Graal, que é um compilador just-in-time (JIT) implementado em Java, extensível e plugável. É uma parte importante do projeto Metropolis.O objetivo deste projeto é usar a linguagem Java o máximo possível para criar programas de tempo de execução da JVM.

Conforme mencionado no JEP 317, o compilador Graal é um novo recurso experimental do Java 10. Seu principal objetivo é permitir que desenvolvedores e líderes de plataformas profissionais escrevam seus próprios compiladores JIT que atendam às suas próprias necessidades especiais. Para a conclusão do projeto e protótipo de novas tecnologias de otimização, o Graal é uma plataforma muito adequada.

Os métodos de análise de escopo mencionados neste artigo e no artigo anterior podem ser usados ​​para implementar muitas técnicas de otimização. A primeira é a eliminação da alocação (ou seja, a substituição escalar. Nota: refere-se à decomposição de objetos em tipos básicos, como int, aloca espaço na pilha e nos registradores, para que você não possa alocar memória na pilha e não precisa do GC Reciclado) e as tecnologias relacionadas a bloqueios que discutimos. Estes são apenas alguns exemplos da tecnologia de compilação JIT fornecida pelo compilador C2 maduro na JVM HotSpot. Os artigos subsequentes também apresentarão algumas outras técnicas usadas para melhorar o desempenho do código na JVM do HotSpot.

Acho que você gosta

Origin www.cnblogs.com/zhuyeshen/p/12735734.html
Recomendado
Clasificación