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.
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.
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.
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.
É 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? **
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?
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.
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.
domain
controller
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.
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.
Status | CPU | Memória | Disco | Rede |
---|---|---|---|---|
# | # | # | # | # |
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.
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!
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?
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?
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?
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.
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.
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?
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.
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!
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:
Impulsione a sua carreira com os melhores cursos e faça parte da maior comunidade tech.
1 ano de Alura
Assine o PLUS e garanta:
Formações com mais de 1500 cursos atualizados e novos lançamentos semanais, em Programação, Inteligência Artificial, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.
A cada curso ou formação concluído, um novo certificado para turbinar seu currículo e LinkedIn.
No Discord, você tem acesso a eventos exclusivos, grupos de estudos e mentorias com especialistas de diferentes áreas.
Faça parte da maior comunidade Dev do país e crie conexões com mais de 120 mil pessoas no Discord.
Acesso ilimitado ao catálogo de Imersões da Alura para praticar conhecimentos em diferentes áreas.
Explore um universo de possibilidades na palma da sua mão. Baixe as aulas para assistir offline, onde e quando quiser.
Acelere o seu aprendizado com a IA da Alura e prepare-se para o mercado internacional.
1 ano de Alura
Todos os benefícios do PLUS e mais vantagens exclusivas:
Luri é nossa inteligência artificial que tira dúvidas, dá exemplos práticos, corrige exercícios e ajuda a mergulhar ainda mais durante as aulas. Você pode conversar com a Luri até 100 mensagens por semana.
Aprenda um novo idioma e expanda seus horizontes profissionais. Cursos de Inglês, Espanhol e Inglês para Devs, 100% focado em tecnologia.
Transforme a sua jornada com benefícios exclusivos e evolua ainda mais na sua carreira.
1 ano de Alura
Todos os benefícios do PRO e mais vantagens exclusivas:
Mensagens ilimitadas para estudar com a Luri, a IA da Alura, disponível 24hs para tirar suas dúvidas, dar exemplos práticos, corrigir exercícios e impulsionar seus estudos.
Envie imagens para a Luri e ela te ajuda a solucionar problemas, identificar erros, esclarecer gráficos, analisar design e muito mais.
Escolha os ebooks da Casa do Código, a editora da Alura, que apoiarão a sua jornada de aprendizado para sempre.