StackOverflowError di Java

1. Ikhtisar

StackOverflowError dapat mengganggu pengembang Java, karena ini adalah salah satu kesalahan runtime paling umum yang dapat kita temui.

Dalam artikel ini, kita akan melihat bagaimana kesalahan ini dapat terjadi dengan melihat berbagai contoh kode serta cara mengatasinya.

2. Stack Frames dan Bagaimana StackOverflowError Terjadi

Mari kita mulai dengan dasar-dasarnya. Saat metode dipanggil, bingkai tumpukan baru dibuat di tumpukan panggilan. Kerangka tumpukan ini menampung parameter dari metode yang dipanggil, variabel lokalnya dan alamat kembalian dari metode tersebut, yaitu titik dari mana eksekusi metode harus dilanjutkan setelah metode yang dipanggil kembali.

Pembuatan bingkai tumpukan akan berlanjut hingga mencapai akhir pemanggilan metode yang ditemukan di dalam metode bertingkat.

Selama proses ini, jika JVM menghadapi situasi di mana tidak ada ruang untuk bingkai tumpukan baru yang akan dibuat, itu akan memunculkan StackOverflowError .

Penyebab paling umum bagi JVM untuk menghadapi situasi ini adalah rekursi yang tidak dapat diselesaikan / tidak terbatas - deskripsi Javadoc untuk StackOverflowError menyebutkan bahwa kesalahan terjadi sebagai akibat dari rekursi yang terlalu dalam di cuplikan kode tertentu.

Namun, rekursi bukan satu-satunya penyebab kesalahan ini. Ini juga dapat terjadi dalam situasi di mana aplikasi terus memanggil metode dari dalam metode hingga tumpukan habis . Ini adalah kasus yang jarang terjadi karena tidak ada pengembang yang dengan sengaja mengikuti praktik pengkodean yang buruk. Penyebab langka lainnya adalah memiliki sejumlah besar variabel lokal di dalam sebuah metode .

The StackOverflowError juga dapat dibuang bila ada aplikasi yang dirancang untuk memiliki c hubungan yclic antara kelas . Dalam situasi ini, konstruktor satu sama lain dipanggil berulang kali yang menyebabkan kesalahan ini dilemparkan. Ini juga dapat dianggap sebagai bentuk rekursi.

Skenario menarik lainnya yang menyebabkan kesalahan ini adalah jika sebuah kelas dibuat dalam kelas yang sama sebagai variabel instan dari kelas itu . Ini akan menyebabkan konstruktor dari kelas yang sama dipanggil berulang kali (secara rekursif) yang pada akhirnya menghasilkan StackOverflowError.

Di bagian selanjutnya, kita akan melihat beberapa contoh kode yang mendemonstrasikan skenario ini.

3. StackOverflowError dalam Tindakan

Dalam contoh yang ditunjukkan di bawah ini, StackOverflowError akan muncul karena rekursi yang tidak diinginkan, di mana pengembang lupa menentukan kondisi penghentian untuk perilaku rekursif:

public class UnintendedInfiniteRecursion { public int calculateFactorial(int number) { return number * calculateFactorial(number - 1); } }

Di sini, kesalahan terjadi pada semua kesempatan untuk nilai apa pun yang diteruskan ke dalam metode:

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); } }

Namun, dalam contoh berikutnya, kondisi penghentian ditentukan tetapi tidak pernah terpenuhi jika nilai -1 diteruskan ke metode countFactorial () , yang menyebabkan rekursi tidak ditentukan / tak terbatas:

public class InfiniteRecursionWithTerminationCondition { public int calculateFactorial(int number) { return number == 1 ? 1 : number * calculateFactorial(number - 1); } }

Serangkaian pengujian ini mendemonstrasikan skenario ini:

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); } }

Dalam kasus khusus ini, kesalahan dapat dihindari sepenuhnya jika kondisi penghentian hanya dibuat sebagai:

public class RecursionWithCorrectTerminationCondition { public int calculateFactorial(int number) { return number <= 1 ? 1 : number * calculateFactorial(number - 1); } }

Berikut tes yang menunjukkan skenario ini dalam praktiknya:

public class RecursionWithCorrectTerminationConditionManualTest { @Test public void givenNegativeInt_whenCalcFact_thenCorrectlyCalc() { int numToCalcFactorial = -1; RecursionWithCorrectTerminationCondition rctc = new RecursionWithCorrectTerminationCondition(); assertEquals(1, rctc.calculateFactorial(numToCalcFactorial)); } }

Sekarang mari kita lihat skenario di mana StackOverflowError terjadi sebagai hasil dari hubungan siklik antar kelas. Mari pertimbangkan ClassOne dan ClassTwo , yang membuat instance satu sama lain di dalam konstruktornya yang menyebabkan hubungan siklik:

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; } }

Sekarang katakanlah kita mencoba membuat instance ClassOne seperti yang terlihat dalam tes ini:

public class CyclicDependancyManualTest { @Test(expected = StackOverflowError.class) public void whenInstanciatingClassOne_thenThrowsException() { ClassOne obj = new ClassOne(); } }

Ini berakhir dengan StackOverflowError karena konstruktor ClassOne membuat instance ClassTwo, dan konstruktor ClassTwo lagi membuat instance ClassOne. Dan ini berulang kali terjadi hingga tumpukan meluap.

Selanjutnya, kita akan melihat apa yang terjadi ketika sebuah kelas dibuat dalam kelas yang sama sebagai variabel instan dari kelas itu.

Seperti yang terlihat pada contoh berikutnya, AccountHolder membuat dirinya sendiri sebagai variabel instance jointAccountHolder :

public class AccountHolder { private String firstName; private String lastName; AccountHolder jointAccountHolder = new AccountHolder(); }

Ketika Rekening kelas dipakai , sebuah StackOverflowError dilemparkan karena pemanggilan rekursif dari konstruktor seperti yang terlihat dalam tes ini:

public class AccountHolderManualTest { @Test(expected = StackOverflowError.class) public void whenInstanciatingAccountHolder_thenThrowsException() { AccountHolder holder = new AccountHolder(); } }

4. Berurusan Dengan StackOverflowError

Hal terbaik yang harus dilakukan saat StackOverflowError ditemukan adalah memeriksa pelacakan tumpukan dengan hati-hati untuk mengidentifikasi pola nomor baris yang berulang. Ini akan memungkinkan kami untuk menemukan kode yang memiliki rekursi bermasalah.

Mari kita periksa beberapa jejak tumpukan yang disebabkan oleh contoh kode yang kita lihat sebelumnya.

Pelacakan tumpukan ini dihasilkan oleh InfiniteRecursionWithTerminationConditionManualTest jika kita menghilangkan deklarasi pengecualian yang diharapkan :

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)

Di sini, baris nomor 5 terlihat berulang. Di sinilah panggilan rekursif dilakukan. Sekarang tinggal memeriksa kode untuk melihat apakah rekursi dilakukan dengan cara yang benar.

Berikut adalah pelacakan tumpukan yang kita dapatkan dengan menjalankan CyclicDependancyManualTest (sekali lagi, tanpa pengecualian yang diharapkan ):

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)

Pelacakan tumpukan ini menunjukkan nomor baris yang menyebabkan masalah di dua kelas yang berada dalam hubungan siklik. Baris nomor 9 dari ClassTwo dan nomor baris 9 dari ClassOne menunjuk ke lokasi di dalam konstruktor di mana ia mencoba untuk membuat instance kelas lain.

Setelah kode diperiksa secara menyeluruh dan jika tidak ada yang berikut (atau kesalahan logika kode lainnya) yang menjadi penyebab kesalahan:

  • Rekursi yang diterapkan secara salah (yaitu tanpa kondisi penghentian)
  • Ketergantungan siklik antar kelas
  • Membuat instance kelas dalam kelas yang sama sebagai variabel instance kelas itu

Akan menjadi ide yang bagus untuk mencoba dan meningkatkan ukuran tumpukan. Bergantung pada JVM yang diinstal, ukuran tumpukan default dapat bervariasi.

The -Xss bendera dapat digunakan untuk meningkatkan ukuran stack, baik dari konfigurasi proyek atau baris perintah.

5. Kesimpulan

Dalam artikel ini, kami melihat lebih dekat pada StackOverflowError termasuk bagaimana kode Java dapat menyebabkannya dan bagaimana kami dapat mendiagnosis dan memperbaikinya.

Kode sumber yang terkait dengan artikel ini dapat ditemukan di GitHub.