2008-10-18 19 views

Odpowiedz

307

Parametry i zmienne lokalne są przydzielane na stosie (z typami odniesienia obiekt znajduje się na stercie, a zmienna odwołuje się do tego obiektu). Stos zwykle znajduje się w górnej części przestrzeni adresowej, a ponieważ jest zużyty, kieruje się w kierunku dolnej części przestrzeni adresowej (tj. W kierunku zera).

Proces ma również stertę, która znajduje się na końcu procesu. Podczas przydzielania pamięci ta kupa może rosnąć w kierunku górnej krawędzi przestrzeni adresowej. Jak widać, istnieje możliwość "zderzenia" sterty ze stosem (trochę jak płyty tektoniczne !!!).

Powszechną przyczyną przepełnienia stosu jest złe wywołanie rekurencyjne. Zazwyczaj dzieje się tak, gdy funkcje rekursywne nie mają poprawnego warunku zakończenia, a więc kończy się samo wywoływanie na zawsze. Jednak przy programowaniu GUI możliwe jest generowanie pośredniej rekursji. Na przykład Twoja aplikacja może obsługiwać komunikaty o malowaniu i, podczas przetwarzania, może wywoływać funkcję, która powoduje, że system wysyła kolejną wiadomość z farbą. Nie nazwaliście się tu bezpośrednio, ale system operacyjny/maszyna wirtualna zrobiły to za was.

Aby sobie z nimi poradzić, musisz sprawdzić swój kod. Jeśli masz funkcje, które się nazywają, sprawdź, czy masz warunek zakończenia. Jeśli następnie sprawdziłeś, niż wywołując funkcję, przynajmniej zmodyfikowałeś jeden z argumentów, w przeciwnym razie nie będzie widocznej zmiany dla rekursywnie nazywanej funkcji, a warunek kończący jest bezużyteczny.

Jeśli nie masz żadnych oczywistych funkcji rekursywnych, sprawdź, czy wywołujesz funkcje biblioteczne, które pośrednio spowodują wywołanie funkcji (jak ukryty przypadek powyżej).

+1

Oryginalny plakat: hej, to jest świetne. Czy rekursja zawsze jest odpowiedzialna za przepełnienie stosu?Czy też inne rzeczy mogą być za nie odpowiedzialne? Niestety używam biblioteki ... ale nie takiej, którą rozumiem. – Ziggy

+4

Ha ha ha, więc tutaj jest: while (points <100) {addMouseListeners(); moveball(); checkforcollision(); pauza (prędkość);} Wow, czuję się kulawy, bo nie zdaję sobie sprawy, że skończę ze stosem słuchaczy myszy ... Dzięki chłopaki! – Ziggy

+2

Nie, przepełnienia stosu mogą również pochodzić od zmiennych, które są zbyt duże, aby je przydzielić, jeśli przejrzysz artykuł w Wikipedii pod adresem http://en.wikipedia.org/wiki/Stack_overflow. –

6

Tak jak mówisz, musisz pokazać kod. :-)

Błąd przepełnienia stosu zwykle występuje, gdy twoja funkcja wywołuje zagnieżdżenie zbyt głęboko. Zajrzyj do wątku Stack Overflow Code Golf, aby dowiedzieć się, jak to się dzieje (chociaż w przypadku tego pytania odpowiedzi celowo powodują przepełnienie stosu).

+1

Chciałbym całkowicie dodać kod, ale ponieważ nie wiem, co powoduje przepełnienie stosu, nie jestem pewien, jaki kod dodać. dodanie całego kodu byłoby lame, nie? – Ziggy

+0

Czy twój projekt jest open-source? Jeśli tak, zrób konto Sourceforge lub github i prześlij tam cały swój kod. :-) –

+0

to brzmi jak świetny pomysł, ale jestem takim noobem, że nawet nie wiem, co musiałbym załadować. Podobnie jak biblioteka, do której importuję klasy, które rozszerzam itp. ... są dla mnie nieznane. Och, stary: złe czasy. – Ziggy

8

Przepełnienie stosu jest zwykle wywoływane przez zagnieżdżanie wywołań funkcji zbyt głęboko (szczególnie łatwo, gdy używana jest rekurencja, tj. Funkcja, która sama się nazywa) lub przydzielanie dużej ilości pamięci na stosie, w którym użycie sterty byłoby bardziej odpowiednie.

+1

Ups, nie widziałem tag Java – Greg

+0

Również z oryginalnego plakatu tutaj: zagnieżdżanie funkcjonuje zbyt głęboko w czym? Inne funkcje? Oraz: w jaki sposób przypisać pamięć do stosu lub sterty (ponieważ, wiesz, jasno zrobiłem jedną z tych rzeczy bez wiedzy). – Ziggy

+0

@Ziggy: Tak, jeśli jedna funkcja wywoła inną funkcję, która wywoła jeszcze inną funkcję, i tak dalej, po wielu poziomach, twój program będzie miał przepełnienie stosu. [kontynuuje] –

59

Jeśli funkcja jak:

int foo() 
{ 
    // more stuff 
    foo(); 
} 

Wtedy foo() będzie trzymać nazywająca siebie, coraz głębiej i głębiej, a gdy przestrzeń wykorzystywane do śledzenia jakie funkcje jesteś w jest wypełniona , pojawi się błąd przepełnienia stosu.

+8

Niepoprawnie. Twoja funkcja jest rekursywna. Większość skompilowanych języków posiada optymalizacje rekurencji ogona. Oznacza to, że rekurencja sprowadza się do prostej pętli i nigdy nie trafisz na przepełnienie stosu tym kodem na niektórych systemach. – Cheery

+57

nie w java. – Chii

+0

Wesoły, które języki niefunkcjonalne obsługują rekursję ogonową? – horseyguy

22

Przepełnienie stosu oznacza dokładnie to: stos przepełnia się. Zazwyczaj w programie znajduje się jeden stos zawierający zmienne o zasięgu lokalnym i adresy, z których można powrócić po zakończeniu wykonywania procedury. Ten stos zwykle jest ustalonym zakresem pamięci w pamięci, dlatego jest ograniczony tym, ile może zawierać wartości.

Jeśli stos jest pusty, nie można go otworzyć, jeśli wystąpi błąd niedomiaru stosu.

Jeśli stos jest pełny, nie można go popchnąć, jeśli wystąpi błąd przepełnienia stosu.

Pojawi się przepełnienie stosu, w którym zbyt dużo miejsca zostanie przydzielone do stosu. Na przykład we wspomnianej rekursji.

Niektóre implementacje optymalizują niektóre formy rekursji. W szczególności rekursja ogona. Rekurencyjne rutyny ogonowe są formą rutyny, w której rekurencyjne wywołanie wydaje się ostateczną rzeczą, którą robi rutyna. Takie rutynowe połączenie zostaje po prostu zredukowane do skoku.

Niektóre implementacje idą tak daleko, jak implementują własne stosy dla rekurencji, w związku z tym umożliwiają rekursję, aż do wyczerpania pamięci.

Najprostszą rzeczą, którą możesz spróbować, to zwiększenie rozmiaru stosu, jeśli możesz. Jeśli jednak nie możesz tego zrobić, najlepszą rzeczą byłoby sprawdzić, czy jest coś, co wyraźnie powoduje przepełnienie stosu. Spróbuj, drukując coś przed i po wywołaniu w rutynę. Pomaga to ustalić nieudaną procedurę.

+4

Czy istnieje coś takiego, jak stos * niedomiaru *? – Pacerier

+4

Możliwe niedopełnienie stosu (popping więcej niż naciśnięto), chociaż w językach skompilowanych byłoby prawie niemożliwe. Nie jestem pewien, może być w stanie znaleźć implementację C alloca(), która "obsługuje" negatywne rozmiary. –

+1

Przepełnienie stosu oznacza dokładnie to: stos przepełnia się. Zwykle w programie znajduje się jeden stos zawierający zmienne o zasięgu lokalnym -> Nie, każdy wątek ma własny stos, który zawiera ramki stosów dla każdego wywołania metody zawierającego lokalne zmienne. –

4

wyniku dałoby StackOverflowError:

class StackOverflowDemo 
{ 
    public static void badRecursiveCall() { 
    badRecursiveCall(); 
    } 

    public static void main(String[] args) 
    { 
     badRecursiveCall(); 
    } 
} 
3

Oto przykład algorytm rekursywnego odwrócenie pojedynczo związany listę. Na laptopie z następującą specyfikacją (pamięć 4G, procesor Intel Core i5 2,3 GHz, 64-bitowy system Windows 7), ta funkcja będzie działać z błędem StackOverflow dla połączonej listy o wielkości blisko 10 000.

Chodzi mi o to, że powinniśmy używać rekursji rozważnie, zawsze biorąc pod uwagę skalę systemu. Często rekurencję można przekonwertować na program iteracyjny, który lepiej się skaluje. (Jeden iteracyjny wersja tego samego algorytmu jest podany na dole strony, to odwraca się pojedynczo połączonej listy wielkości 1 mln w 9 milisekund.)

private static LinkedListNode doReverseRecursively(LinkedListNode x, LinkedListNode first){ 

    LinkedListNode second = first.next; 

    first.next = x; 

    if(second != null){ 
     return doReverseRecursively(first, second); 
    }else{ 
     return first; 
    } 
} 

public static LinkedListNode reverseRecursively(LinkedListNode head){ 
    return doReverseRecursively(null, head); 
} 

Iterative wersja tego samego algorytmu:

public static LinkedListNode reverseIteratively(LinkedListNode head){ 
    return doReverseIteratively(null, head); 
} 

private static LinkedListNode doReverseIteratively(LinkedListNode x, LinkedListNode first) { 

    while (first != null) { 
     LinkedListNode second = first.next; 
     first.next = x; 
     x = first; 

     if (second == null) { 
      break; 
     } else { 
      first = second; 
     } 
    } 
    return first; 
} 


public static LinkedListNode reverseIteratively(LinkedListNode head){ 
    return doReverseIteratively(null, head); 
} 
+0

Myślę, że z JVM to naprawdę nie ma znaczenia jaki jest twój spec laptopa. – kevin

1

Termin "przekroczenie stosu (przepełnienie)" jest często używany, ale jest mylący; ataki nie przepełniają stosu, ale buforują na stosie.

62

Aby to opisać, najpierw niech nam zrozumieć, w jaki sposób lokalna zmienna i obiekt są przechowywane, zmienne lokalne są przechowywane w stosie enter image description here

jeśli spojrzeć na obraz powinien być w stanie zrozumieć, jak to wszystko działa.

Gdy wywołanie funkcji jest wywoływane przez aplikację Java, ramka stosu jest przydzielana na stosie wywołań. Ramka stosu zawiera parametry wywoływanej metody, jej parametry lokalne i adres powrotu metody. Adres zwrotny oznacza punkt wykonawczy, z którego wykonanie programu będzie kontynuowane po powrocie zwróconej metody. Jeśli nie ma miejsca na nową ramkę stosu, komunikat StackOverflowError jest generowany przez wirtualną maszynę Java (JVM). Najczęstszym przypadkiem, który może wyczerpać stos aplikacji Java, jest rekursja. W rekurencji metoda wywołuje się podczas jej wykonywania. Rekurencja jest uważana za potężną technikę programowania ogólnego przeznaczenia, ale musi być używana z ostrożnością, aby uniknąć StackOverflowError. Przykładem, który generuje StackOverflowError przedstawiono poniżej:

StackOverflowErrorExample.java:

public class StackOverflowErrorExample { 

    public static void recursivePrint(int num) { 
     System.out.println("Number: " + num); 

     if(num == 0) 
      return; 
     else 
      recursivePrint(++num); 
    } 

    public static void main(String[] args) { 
     StackOverflowErrorExample.recursivePrint(1); 
    } 
} 

W tym przykładzie określić metodą rekurencyjną, zwany recursivePrint że drukuje liczbę całkowitą, a następnie wywołuje się, z następną liczbą całkowitą jako argumentem. Rekurencja kończy się po wywołaniu metody, przekazując 0 jako parametr. Jednak w naszym przykładzie zaczynamy drukować liczby od 1, a więc rekursja nigdy się nie zakończy. Execution próbki z flagą -Xss1M określający wielkość stosu gwintu równa 1 MB, jest przedstawiony poniżej:

Number: 1 
Number: 2 
Number: 3 
... 
Number: 6262 
Number: 6263 
Number: 6264 
Number: 6265 
Number: 6266 
Exception in thread "main" java.lang.StackOverflowError 
     at java.io.PrintStream.write(PrintStream.java:480) 
     at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221) 
     at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291) 
     at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104) 
     at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185) 
     at java.io.PrintStream.write(PrintStream.java:527) 
     at java.io.PrintStream.print(PrintStream.java:669) 
     at java.io.PrintStream.println(PrintStream.java:806) 
     at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:4) 
     at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:9) 
     at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:9) 
     at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.java:9) 
     ... 

zależności od konfiguracji początkowej JVM, wyniki mogą się różnić, ale ostatecznie StackOverflowError zostanie rzucony. Ten przykład jest bardzo dobrym przykładem tego, w jaki sposób rekurencja może powodować problemy, jeśli nie jest wdrażana z ostrożnością.

Jak radzić sobie z StackOverflowError

  1. Najprostszym rozwiązaniem jest, aby dokładnie zbadać ślad stosu i wykryć powtarzający się wzór z numerami linii. Te numery linii wskazują kod wywoływany rekursywnie. Po wykryciu tych linii należy dokładnie sprawdzić kod i zrozumieć, dlaczego rekursja nigdy się nie kończy.

  2. Jeśli sprawdziłeś, że rekurencja została poprawnie zaimplementowana, możesz zwiększyć rozmiar stosu, w celu uzyskania większej liczby wywołań w kolejności . W zależności od zainstalowanej wirtualnej maszyny Java JVM domyślny rozmiar stosu wątków może wynosić równy 512 KB lub 1 MB. Można zwiększyć rozmiar stosu gwintów o rozmiar , używając flagi -XSS. Tę flagę można określić za pomocą konfiguracji projektu lub za pomocą wiersza polecenia. Format -Xss argumentem jest: -Xss [g | G | m | M | k | K]

+0

Wygląda na to, że w niektórych wersjach java występuje błąd podczas korzystania z systemu Windows, w którym argument -Xss działa tylko na nowe wątki – goerlibe

0

Oto przykład

public static void main(String[] args) { 
    System.out.println(add5(1)); 
} 

public static int add5(int a) { 
    return add5(a) + 5; 
} 

StackOverflowError zasadzie jest podczas próby zrobić coś, co najprawdopodobniej wywołuje samą siebie i przechodzi do nieskończoności (lub do momentu, w którym wywoła to StackOverflowError).

add5(a) będzie się sam wywoływał, a następnie ponownie się nawiąże i tak dalej.

2

StackOverflowError w błąd runtime w java. Jest on generowany, gdy aplikacja nawraca się zbyt głęboko. Za każdym razem, gdy wykonywane jest wykonanie metody, przydzielany jest stos wywołań z ograniczoną ilością miejsca w pamięci. Ta ilość pamięci jest przydzielana przez JVM. Na podstawie liczby wykonania metody rozmiar stosu wywołań rośnie i maleje.

W większości przypadków komunikat StackOverError jest generowany, gdy stos wywołań przekracza nadmierną głęboką lub nieskończoną rekursję. Czas, przez który potrzeba przechowywania zmiennej lokalnej w metodzie przekracza przydzielony rozmiar stosu. W takim wystąpi StackOverFlowError.Jest to pokazane w poniższym programie:

public class Factorial { 
    public static int factorial(int n){ 
     if(n == 1){ 
      return 1; 
     } 
     else{ 
      return n * factorial(n-1); 
     } 
    } 

    public static void main(String[] args){ 
     System.out.println("Main method started"); 
     int result = Factorial.factorial(-1); 
     System.out.println("Factorial ==>"+result); 
     System.out.println("Main method ended"); 
    } 
} 

Tutaj mieliśmy zaimplementowaną silnię z wykorzystaniem rekursji w języku Java. Kod działa poprawnie, gdy dodatnia liczba jest przekazywana jako argument do metody "czynnikowej". Ale problem występuje, gdy podajemy liczbę ujemną jako argument. W takim przypadku rekurencja nigdy nie przejdzie do warunku wyjścia, co spowoduje przepełnienie stosu i spowoduje odrzucenie tego wyjątku.

Main method started 
Exception in thread "main" java.lang.StackOverflowError 
at com.program.stackoverflow.Factorial.factorial(Factorial.java:9) 
at com.program.stackoverflow.Factorial.factorial(Factorial.java:9) 
at com.program.stackoverflow.Factorial.factorial(Factorial.java:9) 

W powyższym przypadku można uniknąć dokonywania zmian programowych. Ale jeśli logika programu jest poprawna i nadal występuje, rozmiar stosu musi zostać zwiększony.

Możesz zwiększyć rozmiar stosu używając opcji -Xss4m.