1. Prefácio
O Java é uma plataforma cruzada orientada para a linguagem de programação de alto nível. Os programas Java são executados nas máquinas virtuais Java (JVMS) e gerenciam memória pelo JVMS. Esta é a maior diferença de C ++. Embora a memória seja gerenciada pelo JVMS, também devemos entender como a JVM gerencia a memória. Atualmente, não existem apenas uma JVM, e pode haver dezenas de máquinas virtuais atualmente, mas um design de máquina virtual que está em conformidade com a especificação deve seguir a "especificação da máquina virtual Java". Este artigo é baseado na descrição da máquina virtual do ponto de acesso e será mencionado se houver diferenças com outras máquinas virtuais. Este artigo descreve principalmente como a memória é distribuída na JVM, como os objetos do programa Java são armazenados e acessados e possíveis exceções em várias áreas de memória.
2. Distribuição de memória (região) na JVM
Ao executar os programas Java, a JVM divide a memória em várias áreas de dados diferentes para gerenciamento. Essas áreas têm funções diferentes, tempos de criação e destruição. Algumas áreas são alocadas quando o processo da JVM é iniciado, enquanto outras estão relacionadas ao ciclo de vida do encadeamento do usuário (o thread do próprio programa). De acordo com a especificação da JVM, as áreas de memória gerenciadas pela JVM são divididas nas seguintes áreas de dados de tempo de execução:
1. Pilha de máquina virtual
Essa área de memória é privada pelo fio e é criada quando o fio começa e destruído quando é destruído. O modelo de memória para a execução dos métodos Java descritos pela pilha de máquinas virtuais: cada método criará um quadro de pilha (quadro de pilha) no início da execução, que é usado para armazenar tabelas de variáveis locais, pilhas de operando, links dinâmicos, método de saída, etc. A execução e a devolução de cada método são concluídos e existe um quadro de pilha, etc.
Como o nome sugere, a tabela variável local é uma área de memória que armazena variáveis locais: ele armazena os tipos de dados básicos (8 tipos de dados básicos de Java), tipos de referência e endereços de retorno que podem ser encontrados durante o período do compilador; Os tipos longos e duplos que ocupam 64 bits ocuparão 2 espaço variável local e outros tipos de dados ocuparem apenas 1; Como o tamanho do tipo é determinado e o número de variáveis pode ser conhecido durante o período de compilação, a tabela variável local tem um tamanho conhecido quando é criado. Esta parte do espaço de memória pode ser alocada durante o período de compilação e não há necessidade de modificar o tamanho da tabela variável local durante a execução do método.
Na especificação da máquina virtual, duas exceções são especificadas para esta área de memória:
1. Se a profundidade da pilha solicitada pelo encadeamento for maior que a profundidade permitida (?), StackOverflowError será lançada;
2. Se a máquina virtual puder se expandir dinamicamente, quando a expansão não puder solicitar memória suficiente, será lançada uma exceção ou exceção OutOfMemory ;
2. Pilha de métodos locais
A pilha de métodos local também é privada de threads e sua função é quase a mesma da pilha de máquinas virtuais: a pilha de máquinas virtuais fornece serviços de pilha dentro e fora da execução do método Java, enquanto a pilha de métodos local fornece serviços para a máquina virtual executar métodos nativos.
Na especificação da máquina virtual, não há regulamentação obrigatória no método de implementação da pilha de métodos local e pode ser implementada livremente pela máquina virtual específica; A máquina virtual do hotspot combina diretamente a pilha de máquina virtual e o método local empilham em uma; Para outras máquinas virtuais implementarem esse método, os leitores podem consultar informações relevantes se estiverem interessadas;
Como a pilha de máquinas virtuais, a pilha de métodos local também lançará exceções StackOverflowError和OutOfMemory .
3. Calculadora de programas
A calculadora do programa também é uma área de memória privada dos threads. Pode ser considerado como um indicador de número de linha (apontando para uma instrução) para que os threads executem bytecode. Quando o Java é executado, ele obtém a próxima instrução a ser executada alterando o valor do contador. As ordens de execução de galhos, loops, saltos, manipulação de exceções, recuperação de threads etc. todos confiam nesse contador para concluir. O multithreading de uma máquina virtual é alcançado com o tempo alternando e alocando o tempo de execução do processador. O processador (um núcleo para um processador de vários núcleos) pode executar apenas um comando por vez. Portanto, depois que o thread executa a comutação, ele precisa ser restaurado na posição de execução correta. Cada thread possui uma calculadora de programa independente.
Ao executar um método Java, a calculadora do programa registra (aponta) o endereço da instrução ByteCode que o encadeamento atual está executando. Se o método nativo estiver sendo executado, o valor desta calculadora será indefinido. Isso ocorre porque o modelo de encadeamento da máquina virtual do ponto de acesso é um modelo de encadeamento nativo, ou seja, cada thread java mapeia diretamente o encadeamento do sistema operacional (sistema operacional). Ao executar o método nativo, ele é executado diretamente pelo sistema operacional. O valor desse contador da máquina virtual é inútil; Como esta calculadora é uma área de memória com espaço muito pequeno, privado e não requer expansão. É a única área na especificação da máquina virtual que não especifica nenhuma exceção OutOfMemoryError .
4. Memória de heap (heap)
O heap java é uma área de memória compartilhada por threads. Pode -se dizer que é a maior área de memória gerenciada pela máquina virtual e é criada quando a máquina virtual é iniciada. A memória Java Heap armazena principalmente instâncias de objetos, e quase todas as instâncias de objetos (incluindo matrizes) são armazenadas aqui. Portanto, esta também é a principal área de memória da coleta de lixo (GC). O conteúdo sobre o GC não será descrito aqui;
De acordo com a especificação da máquina virtual, a memória Java Heap pode estar em memória física descontínua. Desde que seja logicamente contínuo e não haja limite para a expansão do espaço, pode ser um tamanho fixo ou uma árvore estendida. Se a memória da heap não tiver espaço suficiente para concluir a alocação de instância e não puder ser expandida, será lançada uma exceção ou exceção OutOfMemoryError .
5. Área do método
A área do método é a área de memória compartilhada por threads, assim como a memória da heap, ele armazena informações de tipo, constantes, variáveis estáticas, código compilado durante o período de compilação instantânea e outros dados. A especificação da máquina virtual não possui muitas restrições à implementação da área do método e, como a memória da pilha, não requer espaço contínuo de memória física, o tamanho pode ser corrigido ou escalável e também pode ser escolhido para não implementar a coleta de lixo; Quando a área do método não pode atender aos requisitos de alocação de memória, a exceção OutOfMemoryError será lançada.
6. Memória direta
A memória direta não faz parte da memória gerenciada da máquina virtual, mas essa parte da memória ainda pode ser usada com frequência; Quando os programas Java usam métodos nativos (como NIO, NIO, nenhuma descrições são fornecidos aqui), a memória pode ser alocada diretamente fora de heap, mas o espaço total da memória é limitado e haverá memória insuficiente, e uma exceção ou uma exceção OutOfMemoryError também será lançada.
2. Acesso ao armazenamento de objetos da instância
O primeiro ponto acima tem uma descrição geral da memória em cada área da máquina virtual. Para cada área, há problemas com a forma como os dados são criados, dispostos e acessados. Vamos usar a memória de heap mais usada como exemplo para falar sobre esses três aspectos baseados no ponto de acesso.
1. Criação de objetos da instância
Quando a máquina virtual executa uma nova instrução, primeiro, localiza primeiro a referência de símbolo de classe do objeto de criação do pool constante e julga se a classe foi carregada e inicializada. Se não estiver carregado, o processo de inicialização de carga da classe será executado (a descrição não será feita aqui sobre o carregamento da classe). Se essa classe não puder ser encontrada, uma exceção comum ClassNotFoundException será lançada;
Após a verificação de carregamento da classe, a memória física (memória heap) é realmente alocada ao objeto. O espaço de memória exigido pelo objeto é determinado pela classe correspondente. Após o carregamento da classe, o espaço de memória exigido pelo objeto desta classe é corrigido; Alocar o espaço de memória para o objeto é equivalente a dividir uma peça da pilha e alocá -lo para esse objeto;
De acordo com se o espaço de memória é contínuo (alocada e não alocada é dividida em duas partes completas), é dividido em duas maneiras de alocar memória:
1. Memória contínua: um ponteiro é usado como um ponto de divisão entre a memória alocada e não alocada. A alocação de memória do objeto requer apenas o ponteiro para mover o tamanho do espaço para o segmento de memória não alocado; Este método é chamado de "colisão do ponteiro".
2. Memória descontínua: a máquina virtual precisa manter (registrar) uma lista que registra os blocos de memória na pilha que não são alocados. Ao alocar a memória do objeto, selecione uma área de memória de tamanho apropriado para alocá -lo ao objeto e atualize esta lista; Este método é chamado de "lista gratuita".
A alocação da memória do objeto também encontrará problemas de simultaneidade. A máquina virtual usa duas soluções para resolver esse problema de segurança de thread: primeiro, use CAS (compare e set)+ para identificar e voltar para garantir a atomicidade da operação de alocação; Segundo, a alocação de memória é dividida em diferentes espaços de acordo com os threads, ou seja, cada thread pré-alocado uma peça de memória privada de rosca na pilha, chamada de tampão alocado de rosca local (TLAB); Quando esse thread deseja alocar memória, ele é diretamente alocado do TLAB. Somente quando o TLAB do thread é alocado após a re-alocação, a operação síncrona pode ser alocada a partir da pilha. Esta solução reduz efetivamente a simultaneidade da memória de alocação de objetos entre os threads; Se a máquina virtual usa o TLAB é definida através do parâmetro JVM -xx: +/- uSetLab.
Após a conclusão da alocação de memória, além das informações do cabeçalho do objeto, a máquina virtual inicializa o espaço de memória alocado para o valor zero para garantir que os campos da instância do objeto possam ser usados diretamente ao valor zero correspondente ao tipo de dados sem atribuir valores; Em seguida, execute o método init para concluir a inicialização de acordo com o código antes que a criação de um objeto de instância seja concluída;
2. O layout de objetos na memória
Na máquina virtual do hotspot, os objetos são divididos em três partes na memória: cabeçalho do objeto, dados da instância e alinhamento e preenchimento:
O cabeçalho do objeto é dividido em duas partes: parte dele armazena os dados do tempo de execução do objeto, incluindo código de hash, idade de geração de lixo, status de bloqueio do objeto, bloqueio de manutenção da linha, ID de encadeamento tendencioso, registro de data e hora tendencioso, etc.; Em máquinas virtuais de 32 e 64 bits, essa parte dos dados ocupa 32 bits e 64 bits, respectivamente; Como existem muitos dados de tempo de execução, não é suficiente para armazenar completamente todos os dados, portanto, essa peça foi projetada para armazenar dados de tempo de execução em um formato não fixado, mas usa bits diferentes para armazenar dados de acordo com o estado do objeto; A outra parte armazena o ponteiro do tipo de objeto, apontando para a classe desse objeto, mas isso não é necessário, e os metadados da classe do objeto não precisam necessariamente ser determinados usando esta parte do armazenamento (será discutido abaixo);
Os dados da instância são o conteúdo de vários tipos de dados definidos pelo objeto, e os dados definidos por esses programas não são armazenados na ordem definida. Eles são determinados na ordem das políticas e definições de alocação de máquinas virtuais: Long/Double, Int, Short/Char, Byte/Booleano, OOP (Ponint comuns de objeto) , pode -se observar que as políticas são alocadas de acordo com o número de espaços reservados do tipo, e os mesmos tipos alocarão a memória; e, sob a satisfação dessas condições, a ordem das variáveis da classe pai é precedida pela subclasse;
A peça de preenchimento do objeto não existe necessariamente. Ele apenas desempenha um papel no alinhamento de espaço reservado. No hotspot, o gerenciamento da memória da máquina virtual é gerenciado em unidades de 8 bytes. Portanto, quando a memória é alocada, o tamanho do objeto não é um múltiplo de 8 e o preenchimento de alinhamento é concluído;
3. Acesso ao objeto <r /> No programa Java, criamos um objeto e, na verdade, obtemos uma variável de tipo de referência, através da qual realmente operamos uma instância na memória da heap; Na especificação da máquina virtual, é estipulado apenas que o tipo de referência é uma referência apontando para o objeto e não especifica como essa referência localiza e acessa as instâncias na pilha; Atualmente, em máquinas virtuais convencionais, existem duas maneiras principais de implementar o acesso ao objeto:
1. Método da alça: Uma região é dividida na memória da heap como um pool de alça. A variável de referência armazena o endereço do identificador do objeto e o identificador armazena as informações específicas do endereço do objeto de amostra e do tipo de objeto. Portanto, o cabeçalho do objeto não pode conter o tipo de objeto:
2. Acesso direto ao ponteiro: o tipo de referência armazena diretamente as informações de endereço do objeto de instância na pilha, mas isso exige que o layout do objeto de instância deve conter o tipo de objeto:
Esses dois métodos de acesso têm suas próprias vantagens: quando o endereço do objeto é alterado (classificação de memória, coleta de lixo), o objeto de acesso à alça, a variável de referência não precisa ser alterada, mas apenas o valor do endereço do objeto no identificador é alterado; Ao usar o método de acesso direto do ponteiro, todas as referências desse objeto precisam ser modificadas; Mas o método do ponteiro pode reduzir uma operação de endereçamento e, no caso de um grande número de acessos de objetos, as vantagens desse método são mais óbvias; A máquina virtual do hotspot usa esse método de acesso direto de ponteiro.
3. Exceção de memória de tempo de execução
Existem duas exceções principais que podem ocorrer ao executar no programa Java: OrofMemoryError e StackOverflowerRor; O que acontecerá nessa área de memória? Como mencionado brevemente antes, exceto o contador do programa, outras áreas de memória ocorrerão; Esta seção demonstra principalmente as exceções em cada área de memória através do código da instância, e muitos parâmetros de inicialização de máquina virtual comumente usados serão usados para explicar melhor a situação. (Como executar o programa com parâmetros não é descrito aqui)
1. O transbordamento de memória de heap java
O excesso de memória da heap ocorre quando os objetos são criados após a capacidade da heap atingir a capacidade máxima da pilha. No programa, os objetos são criados continuamente e esses objetos são garantidos para não serem coletados de lixo:
/** * Parâmetros da máquina virtual: * -xms20m Capacidade mínima de heap * -xmx20m Capacidade máxima de heap * @Author hwz * */public classe headoutOfMemoryError {public static void main (string [] args) {// use o contêiner para salvar o objeto para garantir que o objeto não seja a lista de coletos <sweetflem). ArrayList <HeadOutOfMemoryError> (); enquanto (true) {// crie continuamente objetos e adicione -os à lista de contêineres para toundobj.add (new headoutOfMemoryError ()); }}} Você pode adicionar parâmetros da máquina virtual :-XX:HeapDumpOnOutOfMemoryError . Ao enviar uma exceção de OOM, deixe a máquina virtual despejar o arquivo de instantâneo da pilha atual. Você pode usar esse problema de exceção de segmentação do Word Word no futuro. Isso não será descrito em detalhes. Vou escrever um blog para descrever em detalhes usando a ferramenta MAT para analisar problemas de memória.
2. Pilha de máquinas virtuais e transbordamento de pilha de métodos locais
Na máquina virtual do hotspot, essas duas pilhas de método não são implementadas juntas. De acordo com a especificação da máquina virtual, essas duas exceções ocorrerão nessas duas áreas de memória:
1. Se o encadeamento solicitar a profundidade da pilha maior que a profundidade máxima permitida pela máquina virtual, jogue uma exceção de StackOverFlowerRor;
2. Se a máquina virtual não puder aplicar um grande espaço de memória ao expandir o espaço da pilha, uma exceção ou exceção do MemoryError será lançada;
Na verdade, há sobreposição entre essas duas situações: quando o espaço da pilha não pode ser alocado, é impossível distinguir se a memória é muito pequena ou a profundidade de pilha usada é muito grande.
Use duas maneiras de testar o código
1. Use o parâmetro -xss para reduzir o tamanho da pilha, chame um método infinitamente recursivamente e aumente a profundidade da pilha infinitamente:
/** * Parâmetros da máquina virtual: <br> * -xss128k Capacidade de pilha * @author hwz * */public class StackOverflowerRor {private int stackdeep = 1; / *** Recursão infinita, aumente infinitamente a profundidade da pilha de chamadas*/ public void RecursiveInVoke () {Stackdeep ++; recursiveInvoke (); } public static void main (string [] args) {StackOverfLowerRor Soe = new StackOverfLowerRor (); tente {soe.recursiveInVoke (); } catch (throwable e) {System.out.println ("Stack Deep =" + Soe.stackdeep); jogar e; }}} Um grande número de variáveis locais é definido no método, o comprimento da tabela variável local na pilha de métodos também é chamado de infinitamente recursivamente:
/** * @Author hwz * */public class Stackoomeerror {private int stackdeep = 1; / *** Defina um grande número de variáveis locais, aumente a tabela de variáveis local na recursão infinita da pilha*, aumente infinitamente a profundidade da pilha de chamadas*/ public void RecursiveInVoke () {Double i; Duplo i2; // ... recursiveInvoke (); } public static void main (string [] args) {sTackoomeerror soe = new StackoomeError (); tente {soe.recursiveInVoke (); } catch (throwable e) {System.out.println ("Stack Deep =" + Soe.stackdeep); jogar e; }}}O teste de código acima mostra que, independentemente de a pilha de quadros ser muito grande ou a capacidade da máquina virtual é muito pequena, quando a memória não pode ser alocada, todo o StackOverflowerRor é jogado;
3. Área do método e transbordamento constante de tempo de execução
Aqui, primeiro descreveremos o método estagiário da string: se o pool constante de string já contiver uma string igual a esse objeto String, ele retornará um objeto String que representa essa string. Caso contrário, adicione este objeto String ao pool constante e retorne uma referência a este objeto String; Através deste método, ele adicionará continuamente um objeto String ao pool constante, resultando em transbordamento:
/** * Parâmetros da máquina virtual: <br> * -xx: PermSize = 10m Tamanho da área permanente * -xx: maxPermsize = 10m Área permanente Capacidade máxima * @author hwz * */public class RunTeConstancePoolOom {public static void Main (string [] Is) {// Use Container para SAVE para salvar o objeto para salvar o objeto para o objeto. ArrayList <String> (); // Use o método string.intern para adicionar o objeto do pool constante para (int i = 1; true; i ++) {list.add (string.valueof (i) .intern ()); }}}No entanto, esse código de teste não transborda durante o pool constante de tempo de execução no JDK1.7, mas acontecerá no JDK1.6. Por esse motivo, escreva outro código de teste para verificar esse problema:
/** * String.Irtern O método é testado em diferentes JDKs * @author hwz * */public class stringInterntest {public static void main (string [] args) {string str1 = new StringBuilder ("teste"). Append ("01"). ToString (); System.out.println (str1.intern () == str1); String str2 = new StringBuilder ("Test"). Append ("02"). ToString (); System.out.println (str2.intern () == str2); }} Os resultados de execução sob JDK1.6 são: false, false;
O resultado da corrida sob o JDK1.7 é: verdadeiro, verdadeiro;
Acontece que no JDK1.6, o método Intern () copia a primeira instância de string encontrada para a geração permanente, que por sua vez é uma referência à instância na geração permanente, e as instâncias de string criadas pelo StringBuilder estão no heap, para que não sejam iguais;
No JDK1.7, o método Intern () não copia a instância, mas apenas registra a referência da primeira instância que aparece no pool constante. Portanto, a referência retornada pelo estagiário é a mesma da instância criada pelo StringBuilder, para que retorne true;
Portanto, o código de teste para transbordamento constante do pool não terá uma exceção constante de estouro de pool, mas pode ter uma exceção insuficiente da memória de heap após a execução contínua;
Em seguida, você precisa testar o estouro da área do método, continue adicionando coisas à área do método, como nomes de classes, modificadores de acesso, pools constantes etc. Podemos deixar o programa carregar um grande número de classes para preencher continuamente a área do método, o que leva ao transbordamento. Usamos o CGLIB para manipular diretamente o bytecode para gerar um grande número de classes dinâmicas:
/** * Método a classe de teste de memória da área da área de transbordamento * @author hwz * */public class Methodareaom {public static void main (string [] args) {// use gclib para criar subclasses infinitamente enquanto (true) {intensificador intensificador = new aprimor (); intensificador.SetSuperclass (maoomclass.class); intensificador.setUseCache (false); intensancer.setCallback (new MethodIntercept () {@Override public Object Intercept (objeto Obj, método do método, objeto [] args, MethodProxy Proxy) lança arremesso {return proxy.invokesuper (obj, args);}}); intensificador.create (); }} classe estática maomclass {}} Através da observação do VisualVM, podemos ver que o número de classes carregadas de JVM aumenta em linha reta com o uso do Pergen:
4. Memória direta transbordando
O tamanho da memória direta pode ser definido através dos parâmetros da máquina virtual : -xx: maxDirectMemorySize . Para fazer o excesso de memória direta, você só precisa solicitar continuamente a memória direta. O seguinte é o mesmo que o teste de cache de memória direta em Java Nio:
/** * Parâmetros da máquina virtual: <br> * -xx: maxDirectMemorySize = 30m Tamanho da memória direta * @Author hwz * */public class DirectMemoryOom {public static void main (string [] args) {list> buffers = new Arraylist <buffer> (); int i = 0; while (true) {// imprima o sistema atual.out.println (++ i); // consumo direto de memória, solicitando continuamente o consumo de memória de buffer direto no buffer de cache.Add (bytebuffer.alocatedirect (1024*1024)); // Contabilidade 1m de cada vez}}} No loop, cada vez que a memória direta 1M é aplicada, a memória direta máxima é definida como 30m e uma exceção é lançada quando o programa é executado 31 vezes: java.lang.OutOfMemoryError: Direct buffer memory
4. Resumo
O exposto acima é todo o conteúdo deste artigo. Este artigo descreve principalmente a estrutura de layout da memória, armazenamento de objetos e exceções de memória que podem ocorrer em várias áreas de memória na JVM; O principal livro de referência "Compreensão aprofundada da Java Virtual Machine (segunda edição)". Se houver alguma incorreção, aponte -o nos comentários; Obrigado pelo seu apoio ao wulin.com.