1. Gambaran keseluruhan
StackOverflowError boleh mengganggu pemaju Java, kerana ini adalah salah satu kesalahan runtime yang paling biasa yang dapat kita hadapi.
Dalam artikel ini, kita akan melihat bagaimana ralat ini dapat terjadi dengan melihat pelbagai contoh kod dan bagaimana kita dapat mengatasinya.
2. Stack Frames dan Bagaimana StackOverflowError Berlaku
Mari mulakan dengan asasnya. Apabila kaedah dipanggil, bingkai timbunan baru akan dibuat pada timbunan panggilan. Rangka timbunan ini menyimpan parameter kaedah yang dipanggil, pemboleh ubah lokalnya dan alamat kembali kaedah iaitu titik dari mana pelaksanaan kaedah harus diteruskan setelah kaedah yang dipanggil telah kembali.
Pembuatan susunan bingkai akan berterusan sehingga mencapai akhir kaedah pemanggilan yang terdapat dalam kaedah bersarang.
Semasa proses ini, jika JVM menghadapi situasi di mana tidak ada ruang untuk bingkai timbunan baru dibuat, ia akan membuang StackOverflowError .
Sebab yang paling biasa bagi JVM untuk menghadapi situasi ini adalah pengulangan yang tidak dapat diselesaikan / tidak terhingga - keterangan Javadoc untuk StackOverflowError menyebutkan bahawa kesalahan dilemparkan sebagai akibat daripada pengulangan yang terlalu mendalam dalam coretan kod tertentu.
Walau bagaimanapun, rekursi bukan satu-satunya penyebab ralat ini. Hal ini juga dapat terjadi dalam situasi di mana aplikasi terus memanggil kaedah dari dalam metode hingga tumpukan habis . Ini adalah kes yang jarang berlaku kerana tidak ada pemaju yang sengaja mengikuti amalan pengkodan yang buruk. Sebab lain yang jarang berlaku ialah terdapat sebilangan besar pemboleh ubah tempatan di dalam satu kaedah .
The StackOverflowError juga boleh dibuang apabila aplikasi direka untuk mempunyai c hubungan yclic antara kelas . Dalam keadaan ini, pembangun antara satu sama lain dipanggil berulang-ulang yang menyebabkan kesalahan ini dilemparkan. Ini juga boleh dianggap sebagai bentuk rekursi.
Senario lain yang menarik yang menyebabkan ralat ini adalah jika kelas dibuat dalam kelas yang sama dengan pemboleh ubah instance kelas tersebut . Ini akan menyebabkan pembangun kelas yang sama dipanggil berulang kali (berulang kali) yang akhirnya menghasilkan StackOverflowError.
Di bahagian seterusnya, kita akan melihat beberapa contoh kod yang menunjukkan senario ini.
3. StackOverflowError dalam Tindakan
Dalam contoh yang ditunjukkan di bawah, StackOverflowError akan dilemparkan kerana pengulangan yang tidak disengajakan, di mana pembangun lupa untuk menentukan syarat penamatan untuk tingkah laku rekursif:
public class UnintendedInfiniteRecursion { public int calculateFactorial(int number) { return number * calculateFactorial(number - 1); } }
Di sini, ralat dilontarkan setiap kali untuk sebarang nilai yang dimasukkan ke dalam kaedah:
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); } }
Walau bagaimanapun, dalam contoh seterusnya, syarat penamatan ditentukan tetapi tidak pernah dipenuhi jika nilai -1 diteruskan ke kaedah calculatorFactorial () , yang menyebabkan pengulangan yang tidak berakhir / tidak terhingga:
public class InfiniteRecursionWithTerminationCondition { public int calculateFactorial(int number) { return number == 1 ? 1 : number * calculateFactorial(number - 1); } }
Set ujian ini menunjukkan senario 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 kes ini, kesalahan dapat dihindari sepenuhnya sekiranya keadaan penamatan hanya dinyatakan sebagai:
public class RecursionWithCorrectTerminationCondition { public int calculateFactorial(int number) { return number <= 1 ? 1 : number * calculateFactorial(number - 1); } }
Inilah ujian yang menunjukkan senario ini dalam praktik:
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 senario di mana StackOverflowError berlaku akibat hubungan kitaran antara kelas. Mari kita pertimbangkan ClassOne dan ClassTwo , yang saling menunjukkan di dalam pembina mereka 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 katakan bahawa kami cuba memberi contoh ClassOne seperti yang dilihat dalam ujian ini:
public class CyclicDependancyManualTest { @Test(expected = StackOverflowError.class) public void whenInstanciatingClassOne_thenThrowsException() { ClassOne obj = new ClassOne(); } }
Ini berakhir dengan StackOverflowError kerana pembangun ClassOne sedang mengasaskan ClassTwo, dan pembangun ClassTwo sekali lagi menunjukkan ClassOne. Dan ini berulang kali berlaku sehingga melimpah timbunan.
Seterusnya, kita akan melihat apa yang berlaku ketika kelas dibuat dalam kelas yang sama dengan pemboleh ubah instance kelas tersebut.
Seperti yang dilihat dalam contoh seterusnya, AccountHolder menunjukkan dirinya sebagai pemboleh ubah instance jointAccountHolder :
public class AccountHolder { private String firstName; private String lastName; AccountHolder jointAccountHolder = new AccountHolder(); }
Apabila pemegang akaun kelas adalah instantiated , yang StackOverflowError dibuang kerana panggilan rekursi pembina seperti yang dilihat dalam ujian ini:
public class AccountHolderManualTest { @Test(expected = StackOverflowError.class) public void whenInstanciatingAccountHolder_thenThrowsException() { AccountHolder holder = new AccountHolder(); } }
4. Berurusan dengan StackOverflowError
Perkara terbaik yang perlu dilakukan ketika StackOverflowError dihadapi adalah dengan memeriksa jejak timbunan dengan berhati-hati untuk mengenal pasti corak nombor baris yang berulang. Ini akan membolehkan kita mencari kod yang mempunyai masalah berulang.
Mari kita periksa beberapa jejak tumpukan yang disebabkan oleh contoh kod yang kita lihat sebelumnya.
Jejak timbunan ini dihasilkan oleh InfiniteRecursionWithTerminationConditionManualTest jika kita menghilangkan pernyataan 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, garis nombor 5 dapat dilihat berulang. Di sinilah panggilan rekursif sedang dilakukan. Sekarang hanya perlu memeriksa kod untuk melihat apakah pengulangan dilakukan dengan cara yang betul.
Inilah jejak timbunan yang kami dapat dengan melaksanakan 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)
Jejak timbunan ini menunjukkan nombor garis yang menyebabkan masalah dalam dua kelas yang berada dalam hubungan kitaran. Baris nombor 9 dari ClassTwo dan nombor baris 9 dari ClassOne menunjuk ke lokasi di dalam konstruktor di mana ia cuba menunjukkan kelas yang lain.
Setelah kod tersebut diperiksa dengan teliti dan jika tidak ada yang berikut (atau kesalahan logik kod lain) yang menjadi penyebab ralat:
- Pengulangan yang tidak betul dilaksanakan (iaitu tanpa syarat penamatan)
- Pergantungan kitaran antara kelas
- Membuat kelas dalam kelas yang sama dengan pemboleh ubah contoh kelas tersebut
Adalah idea yang baik untuk mencuba dan meningkatkan ukuran timbunan. Bergantung pada JVM yang dipasang, ukuran timbunan lalai mungkin berbeza.
The -Xss bendera boleh digunakan untuk meningkatkan saiz timbunan, sama ada dari konfigurasi projek atau baris arahan.
5. Kesimpulan
Dalam artikel ini, kami melihat StackOverflowError dengan lebih dekat termasuk bagaimana kod Java boleh menyebabkannya dan bagaimana kita dapat mendiagnosis dan memperbaikinya.
Kod sumber yang berkaitan dengan artikel ini boleh didapati di GitHub.