StackOverflowError 진단 및 해결

최근 JavaWorld 커뮤니티 포럼 메시지 (새 객체를 인스턴스화 한 후 Stack Overflow)는 Java를 처음 사용하는 사람들이 StackOverflowError의 기본 사항을 항상 잘 이해하지 못한다는 사실을 상기 시켰습니다. 다행히 StackOverflowError는 디버깅하기 쉬운 런타임 오류 중 하나이며이 블로그 게시물에서는 StackOverflowError를 진단하는 것이 얼마나 쉬운 지 보여줄 것입니다. 스택 오버플로의 가능성은 Java에만 국한되지 않습니다.

결과 스택 추적에서 줄 번호를 사용할 수 있도록 디버그 옵션을 켠 상태에서 코드를 컴파일 한 경우 StackOverflowError의 원인을 진단하는 것이 매우 간단 할 수 있습니다. 이러한 경우 일반적으로 스택 추적에서 반복되는 행 번호 패턴을 찾는 것은 간단합니다. 반복되는 줄 번호 패턴은 StackOverflowError가 종종 종료되지 않은 재귀로 인해 발생하기 때문에 유용합니다. 반복되는 줄 번호는 직접 또는 간접적으로 재귀 적으로 호출되는 코드를 나타냅니다. 스택 오버플로가 발생할 수있는 무제한 재귀 이외의 상황이 있지만이 블로그 게시물은 제한 StackOverflowError되지 않은 재귀 로 인한 것으로 제한됩니다.

재귀 관계가 나빠진 것은 StackOverflowErrorStackOverflowError에 대한 Javadoc 설명에이 오류가 "애플리케이션이 너무 많이 반복되어 스택 오버플로가 발생할 때 발생합니다."라고 명시되어 있습니다. 것이 중요하다 StackOverflowError단어로 끝 오류 및 오류 (상위를 통해 상위를 확장)가 아닌 체크 또는 런타임 예외입니다. 그 차이는 중요합니다. ErrorException각각이 Throwable를 전문 있지만, 의도 처리는 상당히 다르다. Java Tutorial은 오류가 일반적으로 Java 응용 프로그램 외부에 있으므로 일반적으로 응용 프로그램에서 포착하거나 처리 할 수 ​​없으며 처리해서는 안된다고 지적합니다.

StackOverflowError세 가지 다른 예 를 통해 무제한 재귀 를 통해 실행하는 방법을 보여 드리겠습니다 . 이 예제에 사용 된 코드는 세 개의 클래스에 포함되어 있으며 첫 번째 클래스 (및 기본 클래스)가 다음에 표시됩니다. .NET Framework를 디버깅 할 때 줄 번호가 중요하기 때문에 세 클래스를 모두 나열합니다 StackOverflowError.

StackOverflowErrorDemonstrator.java

package dustin.examples.stackoverflow; import java.io.IOException; import java.io.OutputStream; /** * This class demonstrates different ways that a StackOverflowError might * occur. */ public class StackOverflowErrorDemonstrator { private static final String NEW_LINE = System.getProperty("line.separator"); /** Arbitrary String-based data member. */ private String stringVar = ""; /** * Simple accessor that will shown unintentional recursion gone bad. Once * invoked, this method will repeatedly call itself. Because there is no * specified termination condition to terminate the recursion, a * StackOverflowError is to be expected. * * @return String variable. */ public String getStringVar() { // // WARNING: // // This is BAD! This will recursively call itself until the stack // overflows and a StackOverflowError is thrown. The intended line in // this case should have been: // return this.stringVar; return getStringVar(); } /** * Calculate factorial of the provided integer. This method relies upon * recursion. * * @param number The number whose factorial is desired. * @return The factorial value of the provided number. */ public int calculateFactorial(final int number) { // WARNING: This will end badly if a number less than zero is provided. // A better way to do this is shown here, but commented out. //return number <= 1 ? 1 : number * calculateFactorial(number-1); return number == 1 ? 1 : number * calculateFactorial(number-1); } /** * This method demonstrates how unintended recursion often leads to * StackOverflowError because no termination condition is provided for the * unintended recursion. */ public void runUnintentionalRecursionExample() { final String unusedString = this.getStringVar(); } /** * This method demonstrates how unintended recursion as part of a cyclic * dependency can lead to StackOverflowError if not carefully respected. */ public void runUnintentionalCyclicRecusionExample() { final State newMexico = State.buildState("New Mexico", "NM", "Santa Fe"); System.out.println("The newly constructed State is:"); System.out.println(newMexico); } /** * Demonstrates how even intended recursion can result in a StackOverflowError * when the terminating condition of the recursive functionality is never * satisfied. */ public void runIntentionalRecursiveWithDysfunctionalTermination() { final int numberForFactorial = -1; System.out.print("The factorial of " + numberForFactorial + " is: "); System.out.println(calculateFactorial(numberForFactorial)); } /** * Write this class's main options to the provided OutputStream. * * @param out OutputStream to which to write this test application's options. */ public static void writeOptionsToStream(final OutputStream out) { final String option1 = "1. Unintentional (no termination condition) single method recursion"; final String option2 = "2. Unintentional (no termination condition) cyclic recursion"; final String option3 = "3. Flawed termination recursion"; try { out.write((option1 + NEW_LINE).getBytes()); out.write((option2 + NEW_LINE).getBytes()); out.write((option3 + NEW_LINE).getBytes()); } catch (IOException ioEx) { System.err.println("(Unable to write to provided OutputStream)"); System.out.println(option1); System.out.println(option2); System.out.println(option3); } } /** * Main function for running StackOverflowErrorDemonstrator. */ public static void main(final String[] arguments) { if (arguments.length < 1) { System.err.println( "You must provide an argument and that single argument should be"); System.err.println( "one of the following options:"); writeOptionsToStream(System.err); System.exit(-1); } int option = 0; try { option = Integer.valueOf(arguments[0]); } catch (NumberFormatException notNumericFormat) { System.err.println( "You entered an non-numeric (invalid) option [" + arguments[0] + "]"); writeOptionsToStream(System.err); System.exit(-2); } final StackOverflowErrorDemonstrator me = new StackOverflowErrorDemonstrator(); switch (option) { case 1 : me.runUnintentionalRecursionExample(); break; case 2 : me.runUnintentionalCyclicRecusionExample(); break; case 3 : me.runIntentionalRecursiveWithDysfunctionalTermination(); break; default : System.err.println("You provided an unexpected option [" + option + "]"); } } } 

위의 클래스는 세 가지 유형의 무제한 재귀를 보여줍니다. 우발적이고 완전히 의도하지 않은 재귀, 의도적 순환 관계와 관련된 의도하지 않은 재귀, 불충분 한 종료 조건이있는 의도 된 재귀입니다. 이들 각각과 그 출력은 다음에 논의됩니다.

완전히 의도하지 않은 재귀

어떤 의도도없이 재귀가 발생하는 경우가있을 수 있습니다. 일반적인 원인은 메서드가 실수로 자신을 호출하는 것입니다. 예를 들어, 너무 부주의하게해서 동일한 메서드에 대한 호출이 될 수있는 "get"메서드의 반환 값에 대한 IDE의 첫 번째 권장 사항을 선택하는 것은 그리 어렵지 않습니다! 이것은 실제로 위의 클래스에 표시된 예입니다. getStringVar()가 될 때까지 방법은 반복적으로 자신을 호출 StackOverflowError발생합니다. 출력은 다음과 같이 나타납니다.

Exception in thread "main" java.lang.StackOverflowError at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at 

위에 표시된 스택 추적은 실제로 위에 배치 한 것보다 몇 배 더 길지만 단순히 동일한 반복 패턴입니다. 패턴이 반복되기 때문에 클래스의 34 행이 문제의 원인인지 쉽게 진단 할 수 있습니다. 우리가 그 줄을 볼 때 우리는 return getStringVar()반복적으로 자신을 부르는 것이 실제로 진술 임을 알 수 있습니다. 이 경우 의도 한 동작이 대신 return this.stringVar;.

순환 관계가있는 의도하지 않은 재귀

클래스간에 주기적 관계를 갖는 데는 특정 위험이 있습니다. 이러한 위험 중 하나는 스택이 오버플로 될 때까지 개체간에 순환 종속성이 지속적으로 호출되는 의도하지 않은 재귀가 발생할 가능성이 더 크다는 것입니다. 이를 증명하기 위해 두 개의 클래스를 더 사용합니다. State클래스와 City때문에 클래스는 순환 relationshiop이 State인스턴스가 자본에 대한 참조가 City와가 City받는 참조가 State이 위치한합니다.

State.java

package dustin.examples.stackoverflow; /** * A class that represents a state and is intentionally part of a cyclic * relationship between City and State. */ public class State { private static final String NEW_LINE = System.getProperty("line.separator"); /** Name of the state. */ private String name; /** Two-letter abbreviation for state. */ private String abbreviation; /** City that is the Capital of the State. */ private City capitalCity; /** * Static builder method that is the intended method for instantiation of me. * * @param newName Name of newly instantiated State. * @param newAbbreviation Two-letter abbreviation of State. * @param newCapitalCityName Name of capital city. */ public static State buildState( final String newName, final String newAbbreviation, final String newCapitalCityName) { final State instance = new State(newName, newAbbreviation); instance.capitalCity = new City(newCapitalCityName, instance); return instance; } /** * Parameterized constructor accepting data to populate new instance of State. * * @param newName Name of newly instantiated State. * @param newAbbreviation Two-letter abbreviation of State. */ private State( final String newName, final String newAbbreviation) { this.name = newName; this.abbreviation = newAbbreviation; } /** * Provide String representation of the State instance. * * @return My String representation. */ @Override public String toString() { // WARNING: This will end badly because it calls City's toString() // method implicitly and City's toString() method calls this // State.toString() method. return "StateName: " + this.name + NEW_LINE + "StateAbbreviation: " + this.abbreviation + NEW_LINE + "CapitalCity: " + this.capitalCity; } } 

City.java