Compreendendo a memória na JVM
Como você já sabe, a JVM executa programas Java dentro dela mesma. Como qualquer máquina virtual, ela possui seu próprio sistema de organização de memória.
O layout da memória interna indica como seu aplicativo Java funciona. Dessa forma, gargalos na operação de aplicativos e algoritmos podem ser identificados. Vamos ver como isso funciona.
Importante! O modelo Java original não era bom o suficiente, então foi revisado no Java 1.5. Esta versão é usada até hoje (Java 14+).
Pilha de Tópicos
O modelo de memória Java usado internamente pela JVM divide a memória em pilhas de encadeamento e heaps. Vejamos o modelo de memória Java, logicamente dividido em blocos:
Todos os encadeamentos em execução na JVM têm sua própria pilha . A pilha, por sua vez, contém informações sobre quais métodos o thread chamou. Vou chamar isso de “pilha de chamadas”. A pilha de chamadas é retomada assim que o thread executa seu código.
A pilha do thread contém todas as variáveis locais necessárias para executar métodos na pilha do thread. Um thread só pode acessar sua própria pilha. As variáveis locais não são visíveis para outros threads, apenas para o thread que as criou. Em uma situação em que dois threads estão executando o mesmo código, ambos criam suas próprias variáveis locais. Assim, cada thread tem sua própria versão de cada variável local.
Todas as variáveis locais de tipos primitivos ( boolean , byte , short , char , int , long , float , double ) são armazenadas inteiramente na pilha de threads e não são visíveis para outras threads. Um thread pode passar uma cópia de uma variável primitiva para outro thread, mas não pode compartilhar uma variável local primitiva.
pilha
A pilha contém todos os objetos criados em seu aplicativo, independentemente de qual thread criou o objeto. Isso inclui wrappers de tipos primitivos (por exemplo, Byte , Integer , Long e assim por diante). Não importa se o objeto foi criado e atribuído a uma variável local ou criado como uma variável membro de outro objeto, ele é armazenado no heap.
Abaixo está um diagrama que ilustra a pilha de chamadas e variáveis locais (elas são armazenadas em pilhas), bem como objetos (elas são armazenadas no heap):
Caso a variável local seja de tipo primitivo, ela é armazenada na pilha da thread.
Uma variável local também pode ser uma referência a um objeto. Nesse caso, a referência (variável local) é armazenada na pilha de threads, mas o próprio objeto é armazenado no heap.
Um objeto contém métodos, esses métodos contêm variáveis locais. Essas variáveis locais também são armazenadas na pilha de encadeamentos, mesmo que o objeto que possui o método esteja armazenado no heap.
As variáveis de membro de um objeto são armazenadas no heap junto com o próprio objeto. Isso é verdade tanto quando a variável de membro é de um tipo primitivo quanto quando é uma referência de objeto.
As variáveis de classe estáticas também são armazenadas no heap junto com a definição de classe.
Interação com objetos
Os objetos no heap podem ser acessados por todos os encadeamentos que tenham uma referência ao objeto. Se um thread tiver acesso a um objeto, ele poderá acessar as variáveis do objeto. Se duas threads chamarem um método no mesmo objeto ao mesmo tempo, ambas terão acesso às variáveis de membro do objeto, mas cada thread terá sua própria cópia das variáveis locais.
Duas threads possuem um conjunto de variáveis locais.Variável local 2aponta para um objeto compartilhado na pilha (Objeto 3). Cada um dos threads tem sua própria cópia da variável local com sua própria referência. Suas referências são variáveis locais e, portanto, são armazenadas em pilhas de threads. No entanto, duas referências diferentes apontam para o mesmo objeto na pilha.
Por favor, note que o generalObjeto 3tem links paraObjeto 2EObjeto 4como variáveis de membro (mostradas por setas). Através desses links, dois threads podem acessarObjeto 2EObjeto4.
O diagrama também mostra uma variável local (variável local 1do métodoDois ). Cada cópia dele contém referências diferentes que apontam para dois objetos diferentes (Objeto 1EObjeto 5) e não o mesmo. Teoricamente, ambos os threads podem acessar ambosObjeto 1, então paraObjeto 5se eles tiverem referências a ambos os objetos. Mas no diagrama acima, cada thread possui apenas uma referência a um dos dois objetos.
Um exemplo de interação com objetos
Vamos ver como podemos demonstrar o trabalho em código:
public class MySomeRunnable implements Runnable() {
public void run() {
one();
}
public void one() {
int localOne = 1;
Shared localTwo = Shared.instance;
//... do something with local variables
two();
}
public void two() {
Integer localOne = 2;
//... do something with local variables
}
}
public class Shared {
// store an instance of our object in a variable
public static final Shared instance = new Shared();
// member variables pointing to two objects on the heap
public Integer object2 = new Integer(22);
public Integer object4 = new Integer(44);
}
O método run() chama o método one() , e one() por sua vez chama two() .
O método one() declara uma variável local primitiva (localOne) do tipo int e uma variável local (localDois), que é uma referência a um objeto.
Cada thread executando o método one() criará sua própria cópialocalOneElocalDoisem sua pilha. VariáveislocalOneserão completamente separadas umas das outras, ficando na pilha de cada thread. Um encadeamento não pode ver as alterações que outro encadeamento faz em sua cópialocalOne.
Cada thread executando o método one() também cria sua própria cópialocalDois. No entanto, duas cópias diferenteslocalDoisacabam apontando para o mesmo objeto na pilha. O fato é quelocalDoisaponta para o objeto referenciado pela variável estáticainstância. Há apenas uma cópia de uma variável estática e essa cópia é armazenada no heap.
Então, ambas as cópiaslocalDoisacabam apontando para a mesma instância compartilhada . A instância compartilhada também é armazenada no heap. CorrespondeObjeto 3no diagrama acima.
Observe que a classe Shared também contém duas variáveis de membro. As próprias variáveis de membro são armazenadas no heap junto com o objeto. Duas variáveis de membro apontam para dois outros objetosinteiro. Esses objetos inteiros correspondem aObjeto 2EObjeto 4no diagrama.
Observe também que o método two() cria uma variável local chamadalocalOne. Essa variável local é uma referência a um objeto do tipo Integer . O método define o linklocalOnepara apontar para uma nova instância de Integer . O link será armazenado em sua cópialocalOnepara cada fio. Duas instâncias Integer serão armazenadas no heap e, como o método cria um novo objeto Integer cada vez que é executado, os dois threads que executam esse método criarão instâncias Integer separadas . Eles combinamObjeto 1EObjeto 5no diagrama acima.
Observe também as duas variáveis de membro na classe Shared do tipo Integer , que é um tipo primitivo. Como essas variáveis são variáveis de membro, elas ainda são armazenadas no heap junto com o objeto. Somente variáveis locais são armazenadas na pilha de threads.
GO TO FULL VERSION