O StackOverflowError em Java
1. Visão geral
StackOverflowError pode ser irritante para desenvolvedores Java, pois é um dos erros de tempo de execução mais comuns que podemos encontrar.
Neste artigo, veremos como esse erro pode ocorrer examinando uma variedade de exemplos de código e também como podemos lidar com ele.
2. Empilhar frames e comoStackOverflowError ocorre
Vamos começar com o básico. When a method is called, a new stack frame gets created on the call stack. Este frame de pilha contém parâmetros do método invocado, suas variáveis locais e o endereço de retorno do método, ou seja, o ponto em que a execução do método deve continuar após o retorno do método invocado.
A criação de quadros de pilha continuará até atingir o final das invocações de métodos encontradas nos métodos aninhados.
Durante este processo, se a JVM encontrar uma situação em que não há espaço para um novo quadro de pilha a ser criado, ela lançará umStackOverflowError.
A causa mais comum para a JVM encontrar essa situação éunterminated/infinite recursion - a descrição Javadoc paraStackOverflowError menciona que o erro é lançado como resultado de recursão muito profunda em um fragmento de código específico.
No entanto, a recursão não é a única causa desse erro. Isso também pode acontecer em uma situação em que um aplicativo mantémcalling methods from within methods until the stack is exhausted. Esse é um caso raro, pois nenhum desenvolvedor seguiria intencionalmente práticas de codificação ruins. Outra causa rara éhaving a vast number of local variables inside a method.
OStackOverflowError também pode ser lançado quando um aplicativo é projetado para tercyclic relationships between classes. Nessa situação, os construtores um do outro são chamados repetidamente, o que causa esse erro. Isso também pode ser considerado como uma forma de recursão.
Outro cenário interessante que causa esse erro é se aclass is being instantiated within the same class as an instance variable of that class. Isso fará com que o construtor da mesma classe seja chamado repetidamente (recursivamente), o que eventualmente resulta em umStackOverflowError.
Na próxima seção, veremos alguns exemplos de código que demonstram esses cenários.
3. StackOverflowError em ação
No exemplo mostrado abaixo, umStackOverflowError será lançado devido à recursão não intencional, onde o desenvolvedor se esqueceu de especificar uma condição de término para o comportamento recursivo:
public class UnintendedInfiniteRecursion {
public int calculateFactorial(int number) {
return number * calculateFactorial(number - 1);
}
}
Aqui, o erro é gerado em todas as ocasiões para qualquer valor passado para o método:
public class UnintendedInfiniteRecursionManualTest {
@Test(expected = StackOverflowError.class)
public void givenPositiveIntNoOne_whenCalFact_thenThrowsException() {
int numToCalcFactorial= 1;
UnintendedInfiniteRecursion uir
= new UnintendedInfiniteRecursion();
uir.calculateFactorial(numToCalcFactorial);
}
@Test(expected = StackOverflowError.class)
public void givenPositiveIntGtOne_whenCalcFact_thenThrowsException() {
int numToCalcFactorial= 2;
UnintendedInfiniteRecursion uir
= new UnintendedInfiniteRecursion();
uir.calculateFactorial(numToCalcFactorial);
}
@Test(expected = StackOverflowError.class)
public void givenNegativeInt_whenCalcFact_thenThrowsException() {
int numToCalcFactorial= -1;
UnintendedInfiniteRecursion uir
= new UnintendedInfiniteRecursion();
uir.calculateFactorial(numToCalcFactorial);
}
}
No entanto, no próximo exemplo, uma condição de terminação é especificada, mas nunca será atendida se um valor de-1 for passado para o métodocalculateFactorial(), o que causa recursão não terminada / infinita:
public class InfiniteRecursionWithTerminationCondition {
public int calculateFactorial(int number) {
return number == 1 ? 1 : number * calculateFactorial(number - 1);
}
}
Este conjunto de testes demonstra este cenário:
public class InfiniteRecursionWithTerminationConditionManualTest {
@Test
public void givenPositiveIntNoOne_whenCalcFact_thenCorrectlyCalc() {
int numToCalcFactorial = 1;
InfiniteRecursionWithTerminationCondition irtc
= new InfiniteRecursionWithTerminationCondition();
assertEquals(1, irtc.calculateFactorial(numToCalcFactorial));
}
@Test
public void givenPositiveIntGtOne_whenCalcFact_thenCorrectlyCalc() {
int numToCalcFactorial = 5;
InfiniteRecursionWithTerminationCondition irtc
= new InfiniteRecursionWithTerminationCondition();
assertEquals(120, irtc.calculateFactorial(numToCalcFactorial));
}
@Test(expected = StackOverflowError.class)
public void givenNegativeInt_whenCalcFact_thenThrowsException() {
int numToCalcFactorial = -1;
InfiniteRecursionWithTerminationCondition irtc
= new InfiniteRecursionWithTerminationCondition();
irtc.calculateFactorial(numToCalcFactorial);
}
}
Nesse caso específico, o erro poderia ter sido completamente evitado se a condição de rescisão fosse simplesmente colocada como:
public class RecursionWithCorrectTerminationCondition {
public int calculateFactorial(int number) {
return number <= 1 ? 1 : number * calculateFactorial(number - 1);
}
}
Este é o teste que mostra este cenário na prática:
public class RecursionWithCorrectTerminationConditionManualTest {
@Test
public void givenNegativeInt_whenCalcFact_thenCorrectlyCalc() {
int numToCalcFactorial = -1;
RecursionWithCorrectTerminationCondition rctc
= new RecursionWithCorrectTerminationCondition();
assertEquals(1, rctc.calculateFactorial(numToCalcFactorial));
}
}
Agora, vamos examinar um cenário ondeStackOverflowError acontece como resultado de relacionamentos cíclicos entre as classes. Vamos considerarClassOne eClassTwo, que se instanciam dentro de seus construtores causando uma relação cíclica:
public class ClassOne {
private int oneValue;
private ClassTwo clsTwoInstance = null;
public ClassOne() {
oneValue = 0;
clsTwoInstance = new ClassTwo();
}
public ClassOne(int oneValue, ClassTwo clsTwoInstance) {
this.oneValue = oneValue;
this.clsTwoInstance = clsTwoInstance;
}
}
public class ClassTwo {
private int twoValue;
private ClassOne clsOneInstance = null;
public ClassTwo() {
twoValue = 10;
clsOneInstance = new ClassOne();
}
public ClassTwo(int twoValue, ClassOne clsOneInstance) {
this.twoValue = twoValue;
this.clsOneInstance = clsOneInstance;
}
}
Agora, digamos que tentamos instanciarClassOne como visto neste teste:
public class CyclicDependancyManualTest {
@Test(expected = StackOverflowError.class)
public void whenInstanciatingClassOne_thenThrowsException() {
ClassOne obj = new ClassOne();
}
}
Isso termina com umStackOverflowError, pois o construtor deClassOne está instanciandoClassTwo, e o construtor deClassTwo novamente está instanciandoClassOne. E isso acontece repetidamente até que transborde a pilha.
A seguir, veremos o que acontece quando uma classe está sendo instanciada dentro da mesma classe que uma variável de instância dessa classe.
Conforme visto no próximo exemplo,AccountHolder se instancia como uma variável de instânciajointAccountHolder:
public class AccountHolder {
private String firstName;
private String lastName;
AccountHolder jointAccountHolder = new AccountHolder();
}
Quando a classeAccountHolder é instanciada em,, umStackOverflowError é lançado devido à chamada recursiva do construtor, conforme visto neste teste:
public class AccountHolderManualTest {
@Test(expected = StackOverflowError.class)
public void whenInstanciatingAccountHolder_thenThrowsException() {
AccountHolder holder = new AccountHolder();
}
}
4. Lidando comStackOverflowError
A melhor coisa a fazer quando umStackOverflowError é encontrado é inspecionar o rastreamento de pilha com cuidado para identificar o padrão de repetição de números de linha. Isso nos permitirá localizar o código com recursão problemática.
Vamos examinar alguns rastreamentos de pilha causados pelos exemplos de código que vimos anteriormente.
Este rastreamento de pilha é produzido porInfiniteRecursionWithTerminationConditionManualTest se omitirmos a declaração de exceçãoexpected:
java.lang.StackOverflowError
at c.b.s.InfiniteRecursionWithTerminationCondition
.calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5)
at c.b.s.InfiniteRecursionWithTerminationCondition
.calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5)
at c.b.s.InfiniteRecursionWithTerminationCondition
.calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5)
at c.b.s.InfiniteRecursionWithTerminationCondition
.calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5)
Aqui, a linha número 5 pode ser vista repetindo. É aqui que a chamada recursiva está sendo feita. Agora é só examinar o código para ver se a recursão é feita de maneira correta.
Aqui está o rastreamento de pilha que obtemos ao executarCyclicDependancyManualTest (novamente, sem exceçãoexpected):
java.lang.StackOverflowError
at c.b.s.ClassTwo.(ClassTwo.java:9)
at c.b.s.ClassOne.(ClassOne.java:9)
at c.b.s.ClassTwo.(ClassTwo.java:9)
at c.b.s.ClassOne.(ClassOne.java:9)
Esse rastreamento de pilha mostra os números de linha que causam o problema nas duas classes que estão em um relacionamento cíclico. A linha número 9 deClassTwoe linha número 9 deClassOne apontam para o local dentro do construtor onde ele tenta instanciar a outra classe.
Depois que o código estiver sendo minuciosamente inspecionado e se nenhum dos seguintes (ou qualquer outro erro de lógica do código) for a causa do erro:
-
Recursão incorretamente implementada (ou seja, sem condição de término)
-
Dependência cíclica entre classes
-
Instanciando uma classe dentro da mesma classe que uma variável de instância dessa classe
Seria uma boa ideia tentar aumentar o tamanho da pilha. Dependendo da JVM instalada, o tamanho da pilha padrão pode variar.
O sinalizador-Xss pode ser usado para aumentar o tamanho da pilha, seja a partir da configuração do projeto ou da linha de comando.
5. Conclusão
Neste artigo, examinamos mais de pertoStackOverflowError, incluindo como o código Java pode causar isso e como podemos diagnosticar e corrigir.
O código-fonte relacionado a este artigo pode ser encontradoover on GitHub.