Entre para a LISTA VIP da Black Friday

00

DIAS

00

HORAS

00

MIN

00

SEG

Clique para saber mais
Alura > Cursos de Programação > Cursos de Java > Conteúdos de Java > Primeiras aulas do curso Java e Gestão de Memória: crie aplicações performáticas e robustas

Java e Gestão de Memória: crie aplicações performáticas e robustas

Pilha, Heap e Referências - Apresentação

Você já enfrentou algum erro de NullPointerException enquanto programava em Java? Neste curso, vamos discutir sobre esse e outros erros relacionados à gestão de memória.

Me chamo Iasmin Araújo, faço parte da escola de programação aqui na Alura. Para fins de acessibilidade, vou me autodescrever.

Audiodescrição: Iasmin se declara uma mulher branca, tem olhos verdes e cabelo castanho escuro na altura dos ombros. Veste uma camiseta preta e, ao fundo, há uma parede iluminada com luz azul.

Contextualizando o curso

Neste curso, vamos explorar bastante sobre gestão de memória em Java.

Por que esse é um tópico tão importante? Porque nós, enquanto pessoas desenvolvedoras, desejamos que nossas aplicações sejam sempre o mais otimizadas possível, e que elas tenham poucos erros inesperados. A gestão de memória nos fornecerá ferramentas para alcançar esse objetivo.

Entendendo qual o Projeto

Vamos utilizar a aplicação da API Vollmed para nos aprofundarmos nesses conceitos. Este foi um projeto que já foi utilizado na formação de Spring Boot, mas agora vamos continuar evoluindo esse projeto com foco na gestão de memória.

O que vamos aprender?

  1. Interação do Java com o computador
  2. Organização dos dados na memória
  3. Ação do garbage collector
  4. Erros comuns de gestão de memória
  5. Funcionamento interno da JVM.
  6. Utilizar a GraalVM no projeto

Quais pontos vamos ver ao longo do curso? Neste curso, você vai aprender sobre a interação do Java com o computador. Ou seja, vamos entender como o computador entende que estamos executando uma aplicação Java e aloca diversos recursos para poder executar essa aplicação.

Quando estamos executando nossa aplicação, os dados serão organizados na memória. Teremos variáveis, objetos, referências que precisarão ser organizados. Então, vamos entender como funciona essa organização.

Vamos aprender sobre como o garbage collector (coletor de lixo) é executado, o que ele faz para coletar todo o lixo produzido pela nossa aplicação. Também vamos ver sobre os erros comuns relacionados à gestão de memória, como podemos causar exceções no nosso programa e como podemos resolver todos esses erros.

Vamos conhecer sobre o funcionamento da arquitetura da JVM (Máquina Virtual Java). Tudo que ela precisa armazenar, tudo que ela precisa fazer para poder executar nosso código. Por fim, vamos poder utilizar todo esse conhecimento do funcionamento da JVM para utilizar uma GraalVM, que é uma JVM diferenciada, no nosso projeto.

Pré-requisitos

É interessante que você já conheça sobre a API REST em Java com Spring Boot como pré-requisito. Esse conhecimento não é tão essencial; se você já domina o básico de Java, consegue entender como aplicamos a gestão de memória. No entanto, entender como funcionam as APIs REST facilitará sua compreensão do que está acontecendo ao longo do curso.

Além disso, é interessante que você tenha acompanhado a formação com todos os cursos anteriores, principalmente os cursos de Threads (Linhas de Execução) e de Maven (Ferramenta de Automação de Compilação). Vamos então ver vários conceitos bastante complexos que exploram o que acontece no funcionamento dessas aplicações.

Se tiver alguma dúvida ou sugestão relacionada ao curso, fique à vontade para enviar para nós no fórum e no Discord da Comunidade Alura.

**Vamos começar? **

Pilha, Heap e Referências - Interagindo com o Sistema Operacional

Durante o curso, estaremos focando na API da Clínica Vollmed. É provável que você já esteja familiarizado com esse projeto do Spring Boot, no qual uma API bastante completa foi desenvolvida. Mas, quais são os pontos que precisamos aprimorar nessa API?

Aprimoramentos Essenciais na API da Clínica Vollmed

Com o decorrer do tempo, acumulamos uma grande quantidade de médicos e pacientes, e esses profissionais e pacientes interagem por meio de consultas. Temos muitos médicos e pacientes, resultando em centenas de consultas ao final do dia.

Ao longo deste curso, vamos focar na gestão da memória, pois teremos que lidar com uma quantidade significativa de dados sendo salvos, devido às centenas de consultas feitas diariamente, ao longo de 2 ou 3 anos.

Com o nosso projeto aberto no IntelliJ, podemos analisar as classes e os pacotes do projeto para relembrar.

Relembrando as classes e pacotes do projeto

Do lado esquerdo do IntelliJ, temos o pacote de domínio, que contém consulta, endereço, médico e paciente. Esse domínio será tratado nos controllers, que são os controllers de consulta, médico e paciente.

Também temos um pacote para lidar com infraestrutura (arquivo infra), que é a parte de validação, exceções, documentação.

Qual seria o nosso caminho padrão nos cursos da Alura? Conhecendo as nossas classes, testaríamos a nossa aplicação, veríamos o que acontece quando a executamos, quais são as requisições que podemos fazer para a API.

Vamos parar um pouco para pensar antes disso. Estamos tão focados no desenvolvimento do código Java que esquecemos que, no meio disso tudo, tem um computador executando o nosso programa Java.

Como funciona quando clicarmos para executar um programa Java e vermos o resultado na tela? O que acontece no meio disso tudo? Como a memória do nosso programa Java e do computador se comportam nesse processo? Entender esses aspectos é crucial para garantir a robustez, eficiência e outros fatores importantes da nossa aplicação.

Para vermos isso na prática, podemos executar o gerenciador de tarefas, que mostrará como o nosso computador está funcionando exatamente nesse momento.

Entendendo como o computador funciona

Para abrir o gerenciador de tarefas no Windows, clicamos com o botão direito na barra de tarefas na parte inferior e selecionamos "Gerenciador de Tarefas". Ele exibirá todos os processos em execução, incluindo programas como IntelliJ, Google Chrome, Slack, OBS e outros que estão atualmente rodando no computador. Note que o Java ainda não está em execução neste momento.

Ao clicarmos no botão de Executar ("Run") na linha 7 do IntelliJ ("Ctrl + Shift + F10"), abrimos nossa aplicação e podemos observar que um processo Java começa a aparecer entre as tarefas iniciais no Gerenciador de Tarefas. O que isso significa? O que está ocorrendo em nosso computador?

No computador, encontramos o gerenciador de tarefas e processos. Cada processo corresponde a uma aplicação em execução. É possível ter vários programas instalados, mas nem todos estão necessariamente em execução ao mesmo tempo.

Quando um programa está em execução e possui dados associados, ele se torna um processo. Isso é exatamente o que aconteceu com o Java. Os processos consomem diversos recursos do computador, e o Java utiliza muitos desses recursos, como pode ser visto no gerenciador de tarefas.

StatusCPUMemóriaDiscoRede
#####

Da direita para a esquerda, encontramos a rede, responsável pela comunicação entre dispositivos como a internet. Logo em seguida, o disco, onde armazenamos informações de longo prazo, como arquivos .java e .class. Por fim, a CPU, representada como a primeira coluna nessa disposição.

A CPU será responsável por processar todas as nossas informações.

O cérebro do computador é a CPU, responsável por realizar os cálculos para nós. Por quê? Porque, no computador, tudo se resume ao binário. Embora possamos observar várias ações na tela, é o binário que o computador utiliza para executá-las.

A CPU processa informações toda vez que executamos um programa, lidando com os binários por meio de cálculos.

Essas informações que a CPU processa vêm da memória de curto prazo, a memória RAM, que é a segunda coluna. Na RAM, estão armazenados os dados que usamos constantemente, os quais são usados no programa em execução no momento atual.

Podemos ver isso rodando o nosso programa Java de novo. Por quê? Temos a nossa CPU em 30%, a nossa memória em 70%, porque existe algo alocado já para o Java, que ele está rodando. Se executarmos de novo, veremos que a nossa CPU, que estava em 30%, sobe para 82, 85, faz um tanto de conta, que é relacionado à aplicação, ao Spring. Assim, aloca certa memória.

Como antes tínhamos um outro programa que estava executando a memória, estava executando e tinha alocado a memória, aqui não mudamos tanto o que estava sendo utilizado.

Então, tínhamos 73%, mas rodamos a mesma aplicação e alocamos a mesma memória para ela. Por isso que a memória sempre se mantém. Mas a CPU modifica porque estamos sempre fazendo cálculos.

Conclusão e Próximos Passos

Compreendendo esses conceitos, temos uma ideia do que ocorre dentro do computador. No entanto, podemos explorar o funcionamento da memória em detalhes, incluindo como o Java gerencia e aloca sua memória. Iremos abordar esse tema no próximo vídeo!

Pilha, Heap e Referências - Conhecendo a pilha de execução

O time de pessoas desenvolvedoras da VollMed vinha evoluindo a API que atualmente já está pronta. Contudo, ao tentar incluir uma nova funcionalidade na classe Paciente, surgiu um bug. Vamos analisar o que está acontecendo?

Entendendo o bug

Abrimos a classe Paciente.java.

// código omitido

@Transient
public List<Consulta> consultas;
public Paciente(DadosCadastroPaciente dados) {
        this.ativo = true;
        this.nome = dados.nome();
        this.email = dados.email();
        this.telefone = dados.telefone();
        this.cpf = dados.cpf();
        this.endereco = new Endereco(dados.endereco());
}

// código omitido

    public List<Consulta> consultas(){
        return consultas();
    }
}

Temos um atributo consultas, que é a lista de consultas. Este atributo está transiente, pois não está sendo salvo no banco. Além disso, temos um método consultas que retorna essas consultas, sendo um getter.

Foi relatado que, ao executar esse método, ocorreu um problema e uma exceção.

Criaremos uma classe de teste para entender o que está acontecendo, qual é esse erro e como podemos resolvê-lo, já que não temos nenhum método da API que precise acessar esse método das consultas.

Vamos voltar e abrir nosso projeto. Nele, criaremos um novo pacote para realizar nossos testes.

Com o pacote med.voll.api selecionado, teclamos "Alt + Insert" e selecionamos "Package", e vamos nomeá-lo como testes.memoria, onde faremos os testes relacionados aos bugs de memória.

Dentro do nosso pacote testes.memoria, vamos pressionar "Alt + Insert" novamente e clicar para criar um "Java Class". Vamos nomear essa classe como Principal, pois ela será uma classe main onde faremos os testes.

Será exibida uma janela perguntando "Do you want to add the following file to Git?" ("Você deseja adicionar o seguinte arquivo ao Git?"). Clicamos no botão "Add" na parte inferior direita.

Seremos redirecionados para o arquivo Principal.java:

package med.voll.api.testes.memoria;

public class Principal {
}

Dentro das chaves, digitamos "psvm" e teclamos "Tab".

package med.voll.api.testes.memoria;

public class Principal {
    public static void main(String[] args) {
    
    }
}

Podemos criar uma variável paciente e solicitar a lista de consultas desse paciente. Tecnicamente, não veremos nada, pois ainda não adicionamos consultas a esse paciente. Assim, criamos um Paciente paciente que receberá new Paciente();. Na linha de baixo, chamamos o método consultas. Então, paciente.consultas().

package med.voll.api.testes.memoria;

public class Principal {
    public static void main(String[] args) {
        Paciente paciente = new Paciente();
        paciente.consultas()
        
    
    }
}

Podemos executar essa classe e verificar o que está acontecendo. Para isso, clicamos no ícone de play à esquerda da linha 5 e selecionamos a opção "Run 'Principal.main()'" ("Ctrl + Shift + F10").

Como retorno, obtemos:

O retorno abaixo foi parcialmente transcrito:

Exception in thread "main" java.lang.StackOverFlowError

at med.voll.api.domain.paciente.Paciente.consultas(Paciente.java:65)

No console do IntelliJ, ao executar a classe, um grande erro ocorre. O que aconteceu? No início, percebemos um erro, que é uma exceção na thread main. Essa exceção é um Stack Overflow Error. O que isso significa? O que aconteceu?

Investigando a Causa do Erro

Analisaremos algumas coisas na nossa classe Paciente.

Paciente

// código omitido

    public List<Consulta> consultas(){
        return consultas();
    }
}

No final do arquivo, encontramos o método consultas(), que está chamando a si mesmo. Por que isso está acontecendo? O método consultas() está se chamando repetidamente, o que resulta em um loop infinito. Isso é claramente um bug causado por alguém que adicionou um parêntese acidentalmente, levando a esse comportamento inesperado. Podemos resolver o problema ao apagar o parêntese.

Paciente

// código omitido

    public List<Consulta> consultas(){
        return consultas;
    }
}

Com o parênteses, notamos no IntelliJ, na linha 65, um ícone que sugere atualização, indicando que estamos alternando repetidamente no mesmo método consultas(). Portanto, o IntelliJ já aponta o problema.

Executamos para verificar o que acontece. Para isso, voltamos ao arquivo Principal.java e selecionamos o ícone de play à esquerda na linha 5.

Como retorno, obtemos:

Process finished with exit code 0

Ao executar novamente, não vimos nenhuma saída porque não tínhamos consultas para mostrar. O problema foi simples: um método se chamando a si mesmo, causando um loop infinito e, consequentemente, uma exceção. Qual foi a razão para essa exceção? O que estava acontecendo?

Entendendo o erro de exceção

Vamos abrir o slide para entendermos como o Stack Overflow Error ocorreu na memória. No slide, há um retângulo grande contendo um retângulo menor dentro dele.

Dentro desse retângulo menor, temos o método consultas(). O que aconteceu por trás é que, quando temos os métodos em Java, a cada chamada de métodos, levamos essas chamadas de métodos para a pilha, para a stack.

Chamamos consultas() uma vez, e apareceu um retângulo com consultas() uma vez. Mas então chamamos de novo, porque tinha uma consulta dentro da outra. Então, armazenou de novo esse consultas() lá dentro.

Chamamos o método repetidamente, várias vezes. Chega um ponto em que essa pilha de memória, onde alocamos todos os métodos, não consegue mais conter todos os métodos que foram chamados. Como era um loop infinito, chegamos a um ponto em que a pilha fica cheia.

Slide mostrando um retângulo preto que contém cinco retângulos menores verdes dentro dele. Cada retângulo menor tem o rótulo "Consultas()" em preto. Os retângulos menores estão alinhados verticalmente e centralizados dentro do retângulo maior, com espaçamento uniforme entre eles.

Tivemos um estouro de pilha porque colocamos tantos métodos consulta() que não couberam. Isso causou um Stack Overflow Error.

No nosso caso, resolver o bug foi simples ao remover o parêntese. No entanto, devemos ter cuidado com métodos recursivos, que podem inadvertidamente gerar loops infinitos e, consequentemente, estouros de pilha.

Deixaremos mais informações na atividade "Para saber mais".

Portanto, devemos ter cuidado em muitos outros casos para evitar estouros de pilha. É por isso que conhecer a Stack do Java é importante. Na Stack do Java, todos os métodos chamados ao longo do tempo são registrados. Dentro desses métodos, há várias variáveis e referências que também ocupam espaço na pilha.

No próximo slide, temos uma descrição da Stack do Java.

Slide com a descrição da Stack do Java. A imagem é composta por um retângulo maior azul que contém dois retângulos menores brancos dentro dele, representando os métodos. Na parte inferior, temos o método 'MAIN' com um retângulo menor acima com as seguintes informações: "Variável local 3", "Referência", "Variável local 2" e "Variável local 2". Subindo, temos o método 'FUNÇÃO' com um retângulo menor com as informações "Referência" e "Variável local 1" dentro.

Começamos com o método main() na parte inferior, que chama outra função. Na parte superior, temos essa função que o main() chamou. Dentro do main(), várias variáveis locais são criadas, como a 1, a 2 e a 3 e uma referência.

Dentro do método, além das variáveis locais, também empilhamos as referências ao longo do tempo. É como se tivéssemos uma grande pilha para os métodos e, dentro de cada método, subpilhas para os dados que estão sendo utilizados.

No método função na parte superior, temos uma "variável local 1" e uma referência, ambas empilhadas corretamente. Aqui estão as variáveis e as referências. Mas qual é a diferença entre elas? O que as distingue?

Diferença entre variável e referência

Uma variável recebe um valor diretamente, como um int sendo igual a 5. Assim, empilhamos o valor 5 diretamente. O mesmo se aplica ao valor 2. Já uma referência aponta para um objeto que está localizado em outra parte da memória.

Próximos Passos

No próximo vídeo, veremos como a referência aponta para o objeto e como isso funciona. Esperamos você lá para entender melhor a memória do Java!

Sobre o curso Java e Gestão de Memória: crie aplicações performáticas e robustas

O curso Java e Gestão de Memória: crie aplicações performáticas e robustas possui 141 minutos de vídeos, em um total de 53 atividades. Gostou? Conheça nossos outros cursos de Java em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Java acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas