2009-06-04 42 views
15

Jak obsługiwać StackOverflowError w Javie?Jak radzić sobie z StackOverflowError w Javie?

+25

Crash. Tak zawsze robię. –

+1

Należy opublikować kod, który powoduje przepełnienie stosu. Unikanie przepełnień stosu jest prawie zawsze lepsze niż próba obsłużenia wyjątku. –

+8

Ahh, ironia tego pytania na stronie o tej nazwie ... –

Odpowiedz

14

Nie jestem pewien, co masz na myśli mówiąc "uchwyt".

Można oczywiście nadrobić ten błąd:

public class Example { 
    public static void endless() { 
     endless(); 
    } 

    public static void main(String args[]) { 
     try { 
      endless(); 
     } catch(StackOverflowError t) { 
      // more general: catch(Error t) 
      // anything: catch(Throwable t) 
      System.out.println("Caught "+t); 
      t.printStackTrace(); 
     } 
     System.out.println("After the error..."); 
    } 
} 

ale to najprawdopodobniej jest to zły pomysł, chyba że dokładnie wiesz co robisz.

+3

Obsługa wyjątków oznacza, że ​​chcesz kontynuować dalej po zignorowaniu wyjątku. Ponieważ jest to wyjątek stackOverflowException, myślę, że nie ma miejsca na stos, co jest wymagane przez java. A więc w takim przypadku, jak kontynuować? –

+4

Najprawdopodobniej masz poważny problem w swoim programie. Oceń ślad stosu i sprawdź, czy znajdziesz coś podejrzanego. Możesz kontynuować po przechwyceniu StackOverflowError, ponieważ ten komunikat został opróżniony przez zgłoszony błąd ...Mogę tylko powtórzyć: znaleźć prawdziwy błąd, który powoduje problem! – Huxi

+4

Ankur, jego wyjątek StachOverflowError, a nie StackOverflowException, z tą różnicą, że nazwa "BŁĄD" wskazuje, że nie powinna zostać przechwycona przez próbę ... catch, ale powinieneś przepisać swój kod tak jak ja i większość innych sugeruje. – Avrom

17

Prawdopodobnie trwa nieskończona rekursja.

tj. metoda, która zwraca się w kółko

public void sillyMethod() 
{ 
    sillyMethod(); 
} 

One obsłużyć to naprawić swój kod tak, że rekurencja kończy zamiast kontynuować wiecznie.

+3

Czasami nieskończona rekurencja przebiega bardzo dobrze. Spójrz na stos śledzenia wewnątrz debuggera po przepływie stackoverflow. –

+4

+1 dla nazwy metody. Ponadto: KnightsWhoSayNeet() – kevinarpe

1

Zgaduję, że nie możesz - lub przynajmniej zależy to od jvm, którego używasz. Przepełnienie stosu oznacza, że ​​nie masz miejsca na przechowywanie zmiennych lokalnych i adresów zwrotnych. Jeśli twój jvm robi jakąś formę kompilacji, masz stackoverflow w jvm, a to oznacza, że ​​nie możesz go obsłużyć ani go złapać. Jvm musi się zakończyć.

Może istnieć sposób tworzenia jvm, który pozwala na takie zachowanie, ale byłby powolny.

Nie testowałem zachowania z jvm, ale w .net po prostu nie można obsłużyć stackoverflow. Nawet próba złapania nie pomoże. Ponieważ java i .net opierają się na tej samej koncepcji (maszyny wirtualne z jit) podejrzewam, że java zachowałaby się tak samo. Obecność wyjątku stackoverflow w .NET sugeruje, że może istnieć jakiś vm, który uniemożliwia programowi złapanie go, normalne jednak nie.

0

Ślad stosu powinien wskazywać na rodzaj problemu. Po odczytaniu śladu stosu powinna pojawić się pewna pętla.

Jeśli nie jest to błąd, musisz dodać licznik lub inny mechanizm, aby zatrzymać rekursję, zanim rekursja pójdzie tak głęboko, że spowoduje przepełnienie stosu.

Przykładem tego może być obsługa zagnieżdżonego XML w modelu DOM z rekursywnymi wywołaniami, a kod XML jest zagnieżdżony tak głęboko, że powoduje przepełnienie stosu zagnieżdżonymi połączeniami (mało prawdopodobne, ale możliwe). To musiałoby być dość głębokim zagnieżdżeniem, aby spowodować przepełnienie stosu.

0

Jak wspomniano przez wielu w tym wątku, częstą przyczyną tego jest wywołanie metody rekurencyjnej, która nie kończy się. Tam, gdzie to możliwe, unikaj przelewu z stosu i jeśli to w testowaniu, powinieneś rozważyć to w większości przypadków jako poważny błąd. W niektórych przypadkach możesz skonfigurować rozmiar stosu wątków w Javie, aby był większy, aby poradzić sobie z pewnymi okolicznościami (duże zestawy danych są zarządzane w pamięci podręcznej lokalnego magazynu, długie rekurencyjne wywołania), ale zwiększy to ogólny wskaźnik pamięci, co może prowadzić do problemów w liczbie wątków dostępnych w VM. Zasadniczo, jeśli otrzymasz ten wyjątek, wątek i wszelkie lokalne dane do tego wątku powinny być uważane za toast i nie są używane (tj. Podejrzane i prawdopodobnie uszkodzone).

13

Spójrz na numer Raymond Chen: When debugging a stack overflow, you want to focus on the repeating recursive part. Wyciąg:

If you go hunting through your defect tracking database trying to see whether this is a known issue or not, a search for the top functions on the stack is unlikely to find anything interesting. That's because stack overflows tend to happen at a random point in the recursion; each stack overflow looks superficially different from every other one even if they are the same stack overflow.

Suppose you're singing the song Frère Jacques, except that you sing each verse a few tones higher than the previous one. Eventually, you will reach the top of your singing range, and precisely where that happens depends on where your vocal limit lines up against the melody. In the melody, the first three notes are each a new "record high" (i.e., the notes are higher than any other note sung so far), and new record highs appear in the three notes of the third measure, and a final record high in the second note of the fifth measure.

If the melody represented a program's stack usage, a stack overflow could possibly occur at any of those five locations in the program's execution. In other words, the same underlying runaway recursion (musically represented by an ever-higher rendition of the melody) can manifest itself in five different ways. The "recursion" in this analogy was rather quick, just eight bars before the loop repeated. In real life, the loop can be quite long, leading to dozens of potential points where the stack overflow can manifest itself.

If you are faced with a stack overflow, then, you want to ignore the top of the stack, since that's just focusing on the specific note that exceeded your vocal range. You really want to find the entire melody, since that's what's common to all the stack overflows with the same root cause.

+0

Bardzo pomocne łącze - tego nauczyłem się w przypadku przepełnienia stosu. –

7

Być może chcesz sprawdzić, czy JVM obsługuje opcję "-Xss".Jeśli tak, możesz spróbować ustawić go na wartość 512k (domyślnie jest to 256k w 32-bitowych systemach Windows i Unix) i sprawdzić, czy to działa (poza ustawieniem dłuższego czasu, aż do pojawienia się wyjątku StackOverflowException). Zauważ, że jest to ustawienie dla wątków, więc jeśli masz dużo wątków, możesz także chcieć podnieść ustawienia sterty.

+1

+1, jest to jedyna odpowiedź, która rozwiązuje podstawową przyczynę problemu. Pracuję w środowisku korporacyjnym z setkami usług, a wiele z nich z przerwami nie działa - i nie używają rekursji, po prostu dużo robią. Usługi są stabilne, gdy wielkość stosu zostanie zwiększona z domyślnej wartości 1 MB, do 4 MB lub nawet 16 MB. A kiedy pojawi się "StackOverflowError", nie możemy koniecznie polegać na tym, że pojawi się on w logach, może to być absolutnie ^% £ * 1 problemu do wyśledzenia. – Contango

0

Prosty,

Spójrz na ślad stosu że StackOverflowError produkuje więc wiesz, gdzie w kodzie występuje i użyć go, aby dowiedzieć się, jak przepisać kod tak, że nie nazywa się rekurencyjnie (the prawdopodobną przyczyną twojego błędu), więc to się więcej nie powtórzy.

StackOverflowErrors nie są czymś, z czym trzeba się postąpić poprzez klauzulę try ... catch, ale wskazuje ona na podstawową wadę logiki kodu, którą należy naprawić.

0

java.lang.Error javadoc:

Błąd jest podklasą Throwable wskazująca poważnych problemów, które rozsądny wniosek nie powinien spróbować złapać. Większość takich błędów to nienormalne warunki. Błąd ThreadDeath, chociaż jest "normalnym" stanem, jest także podklasą błędu, ponieważ większość aplikacji nie powinna próbować go przechwycić. Metoda nie musi deklarować w swojej klauzuli throws żadnych podklas Error, które mogą być rzucone podczas wykonywania metody, ale nie zostały przechwycone, ponieważ te błędy są nienormalnymi warunkami, które nigdy nie powinny wystąpić.

Więc nie rób tego. Postaraj się znaleźć, co jest nie tak w logice twojego kodu. Ten wyjątek występuje bardzo często z powodu nieskończonej rekursji.

4

Poprawna odpowiedź to ta, która już została podana. Prawdopodobnie albo a) masz błąd w kodzie prowadzący do nieskończonej rekurencji, który jest zazwyczaj dość łatwy do zdiagnozowania i naprawienia, albo b) posiada kod, który może prowadzić do bardzo głębokich rekursji, na przykład rekursywnie przechodząc przez niezrównoważone drzewo binarne. W tej ostatniej sytuacji musisz zmienić swój kod, aby nie przydzielać informacji na stosie (tj. Aby się nie powtórzył), ale zamiast tego przydzielić go w stercie.

Na przykład w przypadku niezbalansowanego przemierzania drzewa można przechowywać węzły, które będą wymagać ponownego przeglądu w strukturze danych stosu. Aby przejść w kolejności, pętli w dół lewej gałęzi pchania każdego węzła, jak odwiedziłeś go, aż uderzysz w liść, który chcesz przetworzyć, a następnie wyskakuje węzeł z góry stosu, przetwarzaj go, a następnie uruchom ponownie pętlę z prawe dziecko (ustawiając po prostu zmienną pętli na prawy węzeł). ​​Spowoduje to użycie stałej ilości stosów poprzez przeniesienie wszystkiego, co było na stosie, do stosu w strukturze danych stosu. Stert jest zazwyczaj o wiele więcej niż stos.

Jako coś, co zwykle jest bardzo złym pomysłem, ale jest konieczne w przypadkach, gdy użycie pamięci jest wyjątkowo ograniczone, można użyć odwrócenia wskaźnika. W tej technice kodujesz stos do struktury, przez którą przechodzisz, i wykorzystując ponownie używane przez ciebie łącza, możesz to zrobić bez żadnej lub znacznie mniejszej dodatkowej pamięci. Korzystając z powyższego przykładu, zamiast przesuwać węzły podczas pętli, musimy po prostu pamiętać o naszym bezpośrednim obiekcie nadrzędnym, a przy każdej iteracji ustawiamy łącze, które przeszliśmy do bieżącego elementu nadrzędnego, a następnie do bieżącego elementu nadrzędnego względem węzła, z którego wychodzimy. Kiedy dojdziemy do liścia, przetwarzamy go, a następnie udajemy się do naszego rodzica, a następnie mamy zagadkę. Nie wiemy, czy poprawić lewą gałąź, przetworzyć ten węzeł i przejść do właściwej gałęzi, czy poprawić właściwą gałąź i przejść do naszego rodzica. Musimy więc przydzielić dodatkowe informacje w trakcie wykonywania iteracji. Zwykle, dla realizacji niskiego poziomu tej techniki, ten bit będzie przechowywany w samym wskaźniku, co prowadzi do braku dodatkowej pamięci i całkowitej pamięci. Nie jest to opcja w Javie, ale możliwe jest, że uda się ją wyważyć w polach używanych do innych celów.W najgorszym przypadku jest to przynajmniej 32- lub 64-krotna redukcja potrzebnej pamięci. Oczywiście ten algorytm jest niezwykle łatwy do zmylenia z całkowicie dezorientującymi wynikami i może wywołać spustoszenie w przypadku współbieżności. Więc prawie nigdy nie warto mieć koszta utrzymania, chyba że przydzielanie pamięci jest nie do utrzymania. Typowym przykładem jest śmieciarz, w którym podobne algorytmy są powszechne.

Tym, o czym naprawdę chciałem porozmawiać, jest możliwość poradzenia sobie z StackOverflowError. Mianowicie, aby zapewnić eliminację ogłań końcowych na maszynie JVM. Jednym ze sposobów jest użycie stylu trampolinowego, w którym zamiast wykonywania wywołania typu "tail" zwracasz obiekt procedurowy o wartości NULL lub zwracasz wartość zwracaną przez obiekt. [Uwaga: to wymaga pewnych sposobów powiedzenia, że ​​funkcja zwraca A lub B. W Javie prawdopodobnie najlżejszym sposobem, aby to zrobić, jest zwrócenie jednego typu normalnie i wyrzucenie drugiego jako wyjątku.] Wtedy, gdy wywołasz metodę, trzeba wykonać pętlę while, wywołując procedury nullary (które same zwrócą albo procedurę zerową albo wartość), aż do uzyskania wartości. Niekończąca się pętla stanie się pętlą while, która nieustannie wymusza obiekty procedur, które zwracają obiekty procedur. Zaletą stylu trampoliny jest to, że używa tylko stałego współczynnika więcej stosu niż w przypadku implementacji, która właściwie wyeliminowałaby wszystkie wywołania ogonowe, używa zwykłego stosu Java do wywołań niezwiązanych z końcem, tłumaczenie proste, a tylko powiększa kod przez (żmudny) stały czynnik. Wadą jest przydzielenie obiektu na każde wywołanie metody (które natychmiast stanie się śmieciem), a zużywanie tych obiektów wiąże się z kilkoma wywołaniami pośrednimi na każde wywołanie ogona.

Idealną rzeczą do zrobienia byłoby nigdy nie przydzielić tych procedur zerowych lub cokolwiek innego w pierwszej kolejności, co jest dokładnie tym, co udało się uzyskać eliminacji ogona. Pracując z tym, co zapewnia Java, możemy zrobić to normalnie i wykonać tylko te puste procedury, gdy zabraknie nam sterty. Teraz nadal przydziela się te bezużyteczne ramki, ale robimy to na stosie zamiast sterty i rozdziela je hurtowo, a nasze połączenia są normalnymi bezpośrednimi połączeniami Java. Najprostszym sposobem opisania tej transformacji jest najpierw przepisanie wszystkich metod z wieloma wywołaniami instrukcji na metody, które mają dwie instrukcje wywołania, tj. Fgh() {f(); sol(); h(); } staje się fgh() {f(); gh(); } i gh() {g(); h(); }. Dla uproszczenia przyjmuję, że wszystkie metody kończą się wywołaniem ogona, które można zaaranżować, po prostu pakując pozostałą część metody w osobną metodę, chociaż w praktyce, chciałbyś zająć się nimi bezpośrednio. Po tych transformacjach mamy trzy przypadki, albo metoda ma zero wywołań, w którym to przypadku nie ma nic do zrobienia, albo ma jedno wywołanie (ogon), w którym to przypadku zawijamy go w blok try-catch w tym samym my będziemy dla wywołanie ogona w dwóch przypadkach wywołania. Wreszcie, może mieć dwa wywołania, niezwiązane z ogonem połączenie i wywołanie ogona, w którym to przypadku stosujemy następującą transformację zilustrowaną przykładem (z wykorzystaniem zapisu lambda C#, który można łatwo zastąpić anonimową klasą wewnętrzną z pewnym wzrostem):

// top-level handler 
Action tlh(Action act) { 
    return() => { 
     while(true) { 
      try { act(); break; } catch(Bounce e) { tlh(() => e.run())(); } 
     } 
    } 
} 

gh() { 
    try { g(); } catch(Bounce e) { 
     throw new Bounce(tlh(() => { 
      e.run(); 
      try { h(); } catch(StackOverflowError e) { 
       throw new Bounce(tlh(() => h()); 
      } 
     }); 
    } 
    try { h(); } catch(StackOverflowError e) { 
     throw new Bounce(tlh(() => h())); 
    } 
} 

główną korzyścią jest tutaj, jeśli nie jest wyjątek, jest to ten sam kod jak zaczęliśmy po prostu z kilku dodatkowych procedur obsługi wyjątków zainstalowane. Ponieważ wywołania tail (wywołanie h() nie obsługują wyjątku Bounce, ten wyjątek będzie przez nie przepuszczał (niepotrzebne) ramki ze stosu. Połączenia niezwiązane z końcem przechwytują wyjątki odskoku i ponownie je wywołują, dodając pozostały kod. Spowoduje to rozwinięcie stosu aż do najwyższego poziomu, eliminując ramki wywołań końcowych, ale pamiętając o klatkach połączeń bez limitu w procedurze nullary. Kiedy w końcu wykonamy procedurę w wyjątku Odbijanie na najwyższym poziomie, odtworzymy wszystkie ramki klatek bez limitu. W tym momencie, jeśli natychmiast znowu zabraknie nam stosu, to, ponieważ nie przeinstalowujemy procedur obsługi StackOverflowError, będzie on działał niezgodnie z oczekiwaniami, ponieważ naprawdę jesteśmy poza stosem. Jeśli zajmiemy trochę więcej, zostanie automatycznie zainstalowany nowy StackOverflowError. Co więcej, jeśli robimy postępy, ale potem znowu mamy do czynienia z brakiem stosu, nie ma korzyści z ponownego rozwijania ramek, które już rozwinęliśmy, dlatego instalujemy nowe procedury obsługi najwyższego poziomu, aby stos był rozwijany tylko do nich.

Największy problem z tym podejściem polega na tym, że prawdopodobnie będziesz chciał wywoływać zwykłe metody Java, a Ty możesz mieć mało miejsca na stosie, więc możesz mieć wystarczająco dużo miejsca, aby rozpocząć, ale nie zakończyć, a nie możesz wznów je na środku. Istnieją co najmniej dwa rozwiązania tego problemu. Pierwszym z nich jest wysłanie całej takiej pracy do osobnego wątku, który będzie miał własny stos. Jest to całkiem efektywne i całkiem proste i nie wprowadzi żadnej współbieżności (chyba, że ​​tego chcesz). Inną opcją jest po prostu celowe rozwijanie stosu przed wywołaniem dowolnej normalnej metody Java, po prostu wyrzucenie StackOverflowError bezpośrednio przed nimi. Jeśli po wznowieniu nadal brakuje miejsca na stosie, to na początku trzeba było wkręcić.

Podobna rzecz można zrobić, aby kontynuować również w samą porę. Niestety, ta transformacja nie jest możliwa do zrobienia ręcznie w Javie i jest prawdopodobnie granicą języków takich jak C# lub Scala. Tak więc, przekształcenia takie jak te są zwykle wykonywane przez języki, które są ukierunkowane na JVM, a nie na ludzi.

-1
/* 
Using Throwable we can trap any know error in JAVA.. 
*/ 
public class TestRecur { 
    private int i = 0; 


    public static void main(String[] args) { 
     try { 
      new TestRecur().show(); 
     } catch (Throwable err) { 
      System.err.println("Error..."); 
     } 
    } 

    private void show() { 
     System.out.println("I = " + i++); 
     show(); 
    } 
} 

// jednak u może spojrzeć na link: http://marxsoftware.blogspot.in/2009/07/diagnosing-and-resolving.html do // zrozumieć snipets kod, który może podnieść błąd

1

Większość szanse dostać StackOverflowError są za pomocą długich/[nieskończone] rekursji w funkcjach rekursywnych.

Można uniknąć rekursji funkcji, zmieniając projekt aplikacji, tak aby można było przenosić obiekty danych. Istnieją wzorce kodowania służące do przekształcania kodów rekurencyjnych w iteracyjne bloki kodu. Rzucić okiem na poniższy answeres:

Więc uniknąć pamięci układania przez Java dla recesywnych wywołań funkcji, za pomocą własnych stosy danych.

0

w niektórych przypadkach nie można złapać stackoverflowerror. za każdym razem, gdy spróbujesz, napotkasz nowy. ponieważ jest java vm. dobrze jest znaleźć rekursywne bloki kodu, takie jak Andrew Bullock's said.

Powiązane problemy