Olá Pessoal, tudo bom?
O objetivo principal desmistificar muitos aspectos relacionado a alocação de memória no Java. Como isso é realizado por debaixo dos panos? Por que existe a exception NullPointerException sendo que o Java não possui ponteiros? Entre outras dúvidas que incomodam muitos desenvolvedores.
Remember… Divisão da Memória de um Processo em um Sistema Operacional
A memória de um computador é um tipo de armazenamento de informações volátil, caso ocorra uma queda de energia as informações ali contidas serão apagadas. Aprofundando um pouco mais, uma memória é um conjunto de bytes que armazenam informações em um computador, conforme figura abaixo:
Para a utilização desse conjunto de bytes nos dias atuais é necessário a utilização de um Sistema Operacional o qual divide todas as utilizações do hardware em Processos. De acordo com MAZIERO ((Sistemas Operacionais – Capítulo 5 – http://dainf.ct.utfpr.edu.br/~maziero/lib/exe/fetch.php/so:so-cap05.pdf)), um Processo possui sua memória organizada nas seguintes partes:
- TEXT: contém o código a ser executado pelo processo, gerado durante a compilação e a ligação com as bibliotecas. Esta área tem tamanho fixo, calculado durante a compilação, e normalmente só deve estar acessível para leitura e execução.
- DATA : esta área contém os dados estáticos usados pelo programa, ou seja, suas variáveis globais e as variáveis locais estáticas (na linguagem C, são as variáveis definidas como static dentro das funções). Como o tamanho dessas variáveis pode ser determinado durante a compilação, esta área tem tamanho fixo; deve estar acessível para leituras e escritas, mas não para execução.
- HEAP : área usada para armazenar dados através de alocação dinâmica, usando operadores como malloc e free ou similares. Esta área tem tamanho variável, podendo aumentar/diminuir conforme as alocações/liberações de memória feitas pelo processo. Ao longo do uso, esta área pode se tornar fragmentada, ou seja, pode conter lacunas entre os blocos de memória alocados. São necessários então algoritmos de alocação que minimizem sua fragmentação.
- STACK : área usada para manter a pilha de execução do processo, ou seja, a estrutura responsável por gerenciar o fluxo de execução nas chamadas de função e também para armazenar os parâmetros, variáveis locais e o valor de retorno das funções. Geralmente a pilha cresce “para baixo”, ou seja, inicia em endereços elevados e cresce em direção aos endereços menores da memória. No caso de programas com múltiplas threads, esta área contém somente a pilha do programa principal. Como threads podem ser criadas e destruídas dinamicamente, a pilha de cada thread é mantida em uma área própria, geralmente alocada no heap.
O autor ainda representa essa organização da memória de um Processo nessa imagem:
Para melhor entender o post é necessário prestar atenção nas seguintes camadas:
- Data – Armazena variáveis globais e locais estáticas
- Heap – Armazena as alocações dinâmicas
- Stack – Armazena variáveis locais não estáticas e parâmetros
Essas camadas serão muito interessantes para entendermos os exemplos das próximas partes do post.
Remember… Alocação de Memória em Linguagens de 3ª Geração Estruturadas
As linguagens de 3ª geração estruturadas, dentre elas estão o Fortran, Cobol e a linguagem C, possuíam o conceito de alocação de memória a médio nível, ou seja, o desenvolvedor não precisava diretamente utilizar do recurso de system calls para o sistema operacional, pois as linguagens ajudavam a mascarar essas chamadas através de funções específicas. Vamos observar um código C para entender melhor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include<stdio.h> int main() { int *ptr_one; ptr_one = (int *)malloc(sizeof(int)); if (ptr_one == 0) { printf("ERROR: Out of memory\n"); return 1; } *ptr_one = 25; printf("%d\n", *ptr_one); free(ptr_one); return 0; } |
Esse código realiza as seguintes tarefas:
- Alocar memória para uma instância do tipo int, passando a quantidade de bytes que são utilizados por essa instância (linha 7)
- Verificar se realmente havia espaço na memória (linha 9)
- Atribuir um valor inteiro para essa instância (linha 15)
- Imprimir o conteúdo desta instância (linha 16)
- Liberar a memória alocada para essa instância (linha 18)
O método malloc(int) da linha 7, recebe um inteiro que representa a quantidade de bytes da qual um tipo é composto. Para um tipo simples, como int, é mais fácil saber esse valor, mas quando você possuía estruturas mais complexas, chamadas de Structs (um conceito muito parecido com as classes em Java, mas sem os métodos), era mais difícil calcular o tamanho a ser alocado. Dessa forma a linguagem C possui o método sizeof(type), que recebe o tipo de um tipo primitivo ou struct, e calcula a quantidade de bytes que este type utiliza. Além disso na linha 18, existe a utilização do método free(*), o qual liberava a memória alocada pelo método malloc(). Vamos analisar graficamente como isso se comporta:
O programa começa com a memória sem utilização* já que não há memórias alocadas ou programas inicializados. Podemos ver nas células de rodapé, os endereços de memória.
* É sempre bom lembrar que existem várias outras alocações feitas pelo compilador. desta forma a memória não está completamente sem utilização.
O programa representado no código visto anteriormente começa na linha 3. O compilador insere no primeiro endereço da Stack, a referencia para o método int main().
Na linha 5 do código é realizada a inserção do ponteiro ptr_one dentro da Stack
A linha 7 do código, contém várias partes a serem explicadas. A primeira delas é referente ao método sizeof(). Esse método é inserido dentro da Stack, para realizar o calculo do tamanho de uma variável integer.
Esse valor, 4, é retornado para a execução do método malloc()
O método malloc() irá alocar um espaço de 4 bytes para a variável integer e retorna o endereço de memória para a avariável ptr_one.
Após isso, na linha 15, é inserido o valor 25, dentro do espaço de memória reservado pelo malloc()
Por fim a linha 18, realiza o método free() do espaço de memória, removendo o valor, se houver, contido no espaço de memória HEAP e o endereço inserido dentro da variável passada como parâmetro. Por fim, caso houvesse mais uma variável apontando para o endereço de memória, este não seria removido.
Por fim, como deve ter sido notado, a alocação de memória em C ocorre através do processo em execução do Sistema Operacional, isso é muito importante, pois em Java torna-se um pouco diferente. Para maiores detalhes sobre alocação de memória em C, o link da wikipedia((Wikipedia – C dynamic memory allocation – http://en.wikipedia.org/wiki/C_dynamic_memory_allocation)), descreve com bons detalhes esse procedimento.
Alocação de Memória em Java, mas antes a palavra chave “new”
A palavra chave ou palavra reservada new é utilizada para criar uma nova instância de uma classe. A sintaxe da palavra em Java é:
A palavra new + uma chamada para o construtor de uma classe.
12 //ExemploInteger num = new Integer(10);
Mas o que realmente faz o “new”? Ele é o método responsável por reservar o espaço de memória para a nova instância do objeto que será criada, como o método malloc() em C faz, mas com a diferença que a parte que o método sizeof() faz não é necessária. O java verifica qual é o tipo que você deseja criar um novo objeto e faz o calculo automaticamente.
Outro aspecto muito interessante é que o Java é uma máquina virtual (JVM – Java Virtual Machine). Dessa forma uma alocação de memória tem certas peculiaridades. Apesar de ser uma máquina virtual, o Java perante o Sistema Operacional é um processo como qualquer outro processo. Assim é para cada aplicativo Java que você rodar. Dessa forma se você está rodando em sua máquina um programa Java Swing, um servidor JBoss e um emulador Android pelo Eclipse, você terá em memória 4 instâncias de JVM rodando em seu Sistema Operacional, uma delas é que o Eclipse roda diretamente em Java.
Por ser um processo, logo este possui uma área de memória parecida com as áreas de memória descritas no tópico anterior. Dessa forma ao realizar uma chamada do new a JVM realiza a alocação de um espaço de memória no HEAP, garantindo assim o espaço necessário para o armazenamento do valor. Após essa alocação, o endereço de memória inicial da alocação pode ser retornado para uma variável, que atua como um ponteiro, pois ela é alocada na STACK, igual ao exemplo do tópico anterior.
E a função free(), quem faz no Java?
A linguagem Java não possui o método free(), pois não é possível liberar memória em Java. Para isso existe o Garbage Collection, veremos em mais detalhes esse item muito importante do Java no futuro.
finally
Esse post não falou tanto de Java, quanto estamos acostumados em outros posts, mas é um ponto muito importante que todo desenvolvedor deveria saber, pois muitos erros como OutOfMemoryError: Java Heap Space ou OutOfMemoryError: PermGen Space, podem ocorrer e saber o que pode estar acontecendo é sempre bom para não ficar perdido.
No futuro serão analisados 2 pontos da memória do Java: A arquitetura do Java e o Garbage Collection.
Até lá!
Leave a Reply