이 메서드는 왜 4를 인쇄합니까?


111

StackOverflowError를 잡으려고 시도하고 다음 방법을 생각해 낼 때 어떤 일이 발생하는지 궁금합니다.

class RandomNumberGenerator {

    static int cnt = 0;

    public static void main(String[] args) {
        try {
            main(args);
        } catch (StackOverflowError ignore) {
            System.out.println(cnt++);
        }
    }
}

이제 내 질문 :

이 메서드는 왜 '4'를 인쇄합니까?

System.out.println()콜 스택에서 3 개의 세그먼트가 필요 하기 때문이라고 생각했는데 3 번이 어디에서 오는지 모르겠습니다. 의 소스 코드 (및 바이트 코드)를 볼 System.out.println()때 일반적으로 3보다 훨씬 많은 메서드 호출이 발생합니다 (따라서 호출 스택의 3 개 세그먼트로는 충분하지 않습니다). 핫스팟 VM이 적용되는 최적화 (메소드 인라인) 때문이라면 다른 VM에서 결과가 다를지 궁금합니다.

편집 :

출력이 매우 JVM 특정인 것처럼
보이므로 Java (TM) SE 런타임 환경 (빌드 1.6.0_41-b02)
Java HotSpot (TM) 64 비트 서버 VM (빌드 20.14-b01, 혼합 모드)을 사용하여 결과 4를 얻습니다.


이 질문이 Java 스택 이해와 다른 이유를 설명 하십시오 .

내 질문은 왜 cnt> 0이 있는지에 대한 것이 아니라 (분명히 System.out.println()스택 크기가 필요하고 StackOverflowError무언가가 인쇄되기 전에 다른 것을 던지기 때문입니다 ), 왜 특정 값이 4, 각각 0,3,8,55 또는 다른 무엇인지에 대한 것입니다. 시스템.


4
내 지역에서는 예상대로 "0"이 표시됩니다.
Reddy

2
이것은 많은 건축 문제를 다룰 수 있습니다. 따라서 jdk 버전으로 출력을 게시하는 것이 좋습니다. For me output is 0 on jdk 1.7
Lokesh

3
나는 5, 6그리고 38자바 1.7.0_10
Kon

8
당신이 기본 구조를 포함하는 트릭을 수행 할 때 @Elist는 동일한 출력을하지 않습니다)
m0skit0

3
@flrnb 중괄호를 정렬하는 데 사용하는 스타일입니다. 조건과 기능이 시작되고 끝나는 위치를 더 쉽게 알 수 있습니다. 원하는 경우 변경할 수 있지만 제 생각에는이 방법이 더 읽기 쉽습니다.
syb0rg

답변:


41

나는 다른 사람들이 cnt> 0의 이유를 잘 설명했다고 생각하지만, 왜 cnt = 4이고, 왜 cnt가 다른 설정에 따라 그렇게 크게 달라지는 지에 대한 세부 사항이 충분하지 않습니다. 나는 여기서 그 공백을 채우려 고 노력할 것입니다.

허락하다

  • X는 총 스택 크기입니다.
  • M은 처음 main에 들어갈 때 사용되는 스택 공간입니다.
  • R은 우리가 메인에 들어갈 때마다 스택 공간이 증가합니다.
  • P는 실행에 필요한 스택 공간입니다. System.out.println

처음 메인에 들어가면 남은 공간은 XM입니다. 각 재귀 호출은 R 메모리를 더 많이 차지합니다. 따라서 1 개의 재귀 호출 (원래보다 1 개 더 많음)의 경우 메모리 사용량은 M + R입니다. C 개의 성공적인 재귀 호출, 즉 M + C * R <= X 및 M + C * (R + 1)> X. 첫 번째 StackOverflowError 시점에 X-M-C * R 메모리가 남아 있습니다.

를 실행할 수 있으려면 System.out.prinln스택에 P 공간이 필요합니다. X-M-C * R> = P가 발생하면 0이 인쇄됩니다. P에 더 많은 공간이 필요하면 스택에서 프레임을 제거하여 cnt ++ 비용으로 R 메모리를 얻습니다.

println마지막으로 실행할 수있을 때 X-M-(C-cnt) * R> = P. 따라서 특정 시스템에 대해 P가 크면 cnt가 커집니다.

몇 가지 예를 들어 살펴 보겠습니다.

예 1 : 가정

  • X = 100
  • M = 1
  • R = 2
  • P = 1

그러면 C = floor ((XM) / R) = 49, cnt = ceiling ((P-(X-M-C * R)) / R) = 0입니다.

예 2 : 다음을 가정합니다.

  • X = 100
  • M = 1
  • R = 5
  • P = 12

그러면 C = 19, cnt = 2입니다.

예 3 : 다음과 같이 가정합니다.

  • X = 101
  • M = 1
  • R = 5
  • P = 12

그러면 C = 20, cnt = 3입니다.

예 4 : 다음과 같이 가정합니다.

  • X = 101
  • M = 2
  • R = 5
  • P = 12

그러면 C = 19, cnt = 2입니다.

따라서 시스템 (M, R 및 P)과 스택 크기 (X)가 모두 cnt에 영향을 미친다는 것을 알 수 있습니다.

참고 catch로 시작 하는 데 필요한 공간 은 중요하지 않습니다 . 에 대한 공간이 충분 catch하지 않으면 cnt가 증가하지 않으므로 외부 효과가 없습니다.

편집하다

나는 내가 말한 것을 되 돌린다 catch. 그것은 역할을합니다. 시작하려면 T 공간이 필요하다고 가정합니다. cnt는 남은 공간이 T보다 클 때 증가하기 시작 println하고 남은 공간이 T + P보다 클 때 실행됩니다. 이것은 계산에 추가 단계를 추가하고 이미 진흙 투성이 분석을 더 혼란스럽게합니다.

편집하다

마침내 내 이론을 뒷받침하기 위해 몇 가지 실험을 실행할 시간을 찾았습니다. 불행히도 이론은 실험과 일치하지 않는 것 같습니다. 실제로 일어나는 일은 매우 다릅니다.

실험 설정 : 기본 java 및 default-jdk를 사용하는 Ubuntu 12.04 서버. Xss는 70,000에서 1 바이트로 시작하여 460,000으로 증가합니다.

결과는 다음에서 확인할 수 있습니다. https://www.google.com/fusiontables/DataSource?docid=1xkJhd4s8biLghe6gZbcfUs3vT5MpS_OnscjWDbM 반복되는 모든 데이터 포인트가 제거되는 다른 버전을 만들었습니다. 즉, 이전과 다른 점만 표시됩니다. 이렇게하면 이상 징후를 더 쉽게 볼 수 있습니다. https://www.google.com/fusiontables/DataSource?docid=1XG_SRzrrNasepwZoNHqEAKuZlHiAm9vbEdwfsUA


좋은 요약을 해주셔서 감사합니다. 모든 질문은 M, R, P에 어떤 영향을 미칩니 까 (X는 VM 옵션 -Xss로 설정 될 수 있기 때문에)?
flrnb

@flrnb M, R 및 P는 시스템에 따라 다릅니다. 쉽게 변경할 수 없습니다. 일부 릴리스 간에도 다를 것으로 예상합니다.
John Tseng 2013

그렇다면 Xss (일명 X)를 변경하여 다른 결과를 얻는 이유는 무엇입니까? M, R 및 P가 동일하게 유지되는 경우 X를 100에서 10000으로 변경하면 공식에 따라 cnt에 영향을주지 않아야합니까, 아니면 제가 잘못 알고 있습니까?
flrnb

@flrnb X는 이러한 변수의 불연속적인 특성으로 인해 cnt를 변경합니다. 예제 2와 3은 X에서만 다르지만 cnt는 다릅니다.
John Tseng 2013

1
어쨌든, 난 정말 스택이 실제로 무엇에 관심이있을 것 - @JohnTseng 나는 또한 지금 쯤 가장 이해하고 완전한 답을 고려해 본다 (가) 순간처럼 StackOverflowError발생하고,이 출력에 영향을 미치지 않는 방법에 대해 설명합니다. 힙의 스택 프레임에 대한 참조 만 포함 된 경우 (Jay가 제안한대로) 출력은 주어진 시스템에 대해 상당히 예측 가능해야합니다.
flrnb 2013

20

이것은 잘못된 재귀 호출의 희생자입니다. cnt 의 값이 왜 다른지 궁금한 것은 스택 크기가 플랫폼에 따라 다르기 때문입니다. Windows의 Java SE 6의 기본 스택 크기는 32 비트 VM에서 320k이고 64 비트 VM에서 1024k입니다. 여기에서 자세한 내용을 읽을 수 있습니다 .

다른 스택 크기를 사용하여 실행할 수 있으며 스택이 오버플로되기 전에 cnt의 다른 값을 볼 수 있습니다.

java -Xss1024k RandomNumberGenerator

인쇄 문이 Eclipse 또는 다른 IDE를 통해 확실히 디버그 할 수있는 오류를 던지기 때문에 값이 1보다 크더라도 cnt 값이 여러 번 인쇄되는 것을 볼 수 없습니다 .

원하는 경우 문 실행별로 디버그하기 위해 코드를 다음과 같이 변경할 수 있습니다.

static int cnt = 0;

public static void main(String[] args) {                  

    try {     

        main(args);   

    } catch (Throwable ignore) {

        cnt++;

        try { 

            System.out.println(cnt);

        } catch (Throwable t) {   

        }        
    }        
}

최신 정보:

이것이 훨씬 더 많은 관심을 받고 있으므로 더 명확하게하기위한 또 다른 예를 들어 보겠습니다.

static int cnt = 0;

public static void overflow(){

    try {     

      overflow();     

    } catch (Throwable t) {

      cnt++;                      

    }

}

public static void main(String[] args) {

    overflow();
    System.out.println(cnt);

}

잘못된 재귀를 수행하기 위해 overflow 라는 또 다른 메서드를 만들고 catch 블록에서 println 문을 제거하여 인쇄를 시도하는 동안 다른 오류 집합이 발생하지 않도록했습니다. 이것은 예상대로 작동합니다. 당신은 퍼팅 시도 할 수 있습니다 에서 System.out.println (CNT)를; 위의 cnt ++ 뒤에 문을 작성 하고 컴파일하십시오. 그런 다음 여러 번 실행하십시오. 플랫폼에 따라 cnt 값이 다를 수 있습니다 .

이것이 코드의 미스터리가 환상이 아니기 때문에 일반적으로 오류를 포착하지 않는 이유입니다.


13

동작은 스택 크기에 따라 다릅니다 (을 사용하여 수동으로 설정할 수 있습니다 Xss. 스택 크기는 아키텍처에 따라 다릅니다. From JDK 7 소스 코드 :

// Windows의 기본 스택 크기는 실행 파일에 의해 결정됩니다 (java.exe
// 기본값은 320K / 1MB [32bit / 64bit]). Windows 버전에 따라
// ThreadStackSize를 0이 아닌 값으로 변경 하면 메모리 사용량에 상당한 영향을 미칠 수 있습니다.
// os_windows.cpp의 주석을 참조하십시오.

따라서 StackOverflowError가 발생하면 오류가 catch 블록에서 잡 힙니다. 다음 println()은 예외를 다시 발생시키는 또 다른 스택 호출입니다. 이것은 반복됩니다.

몇 번 반복됩니까? -JVM이 더 이상 스택 오버플로가 아니라고 생각하는시기에 따라 다릅니다. 그리고 그것은 각 함수 호출 (찾기 어려움)의 스택 크기와 Xss. 위에서 언급했듯이 각 함수 호출의 기본 총 크기와 크기 (메모리 페이지 크기 등에 따라 다름)는 플랫폼에 따라 다릅니다. 따라서 다른 행동.

호출 java에 전화를 -Xss 4M나를 수 있습니다 41. 따라서 상관 관계입니다.


4
cnt 값을 인쇄하려고 할 때 이미 초과 되었기 때문에 스택 크기가 결과에 영향을 미치는 이유를 알 수 없습니다. 따라서 유일한 차이는 "각 함수 호출의 스택 크기"에서 비롯 될 수 있습니다. 그리고 이것이 동일한 JVM 버전을 실행하는 두 대의 컴퓨터간에 왜 달라야 하는지를 이해하지 못합니다.
flrnb 2013

정확한 동작은 JVM 소스에서만 얻을 수 있습니다. 그러나 그 이유는 이것 일 수 있습니다. 조차도 catch블록이고 스택의 메모리를 차지 한다는 것을 기억하십시오 . 각 메서드 호출에 걸리는 메모리 양은 알 수 없습니다. 스택이 지워지면 블록을 하나 더 추가하는 것 catch입니다. 이것은 동작 일 수 있습니다. 이것은 추측 일뿐입니다.
Jatin 2013-07-24

스택 크기는 두 대의 다른 컴퓨터에서 다를 수 있습니다. 스택 크기는 많은 OS 기반 요소, 즉 메모리 페이지 크기 등에 따라 달라집니다
Jatin 2013-07-24

6

표시된 숫자는 System.out.println호출이 Stackoverflow예외를 던진 횟수라고 생각합니다 .

아마도의 구현 println및 스택 호출 수에 따라 달라집니다 .

그림으로 :

main()호출을 트리거 Stackoverflow호출 전에서 예외입니다. main의 i-1 호출은 println두 번째를 트리거하는 예외와 호출 을 포착합니다 Stackoverflow. cnt1로 증가합니다. main의 i-2 호출은 이제 예외를 포착하고 println. 에서 println하는 방법 제 3 예외를 트리거라고합니다. cnt2로 증가합니다. 이것은 println필요한 모든 호출을 수행하고 마지막으로 값을 표시 할 수 있을 때까지 계속 됩니다 cnt.

이는의 실제 구현에 따라 다릅니다 println.

JDK7의 경우 순환 호출을 감지하고 더 일찍 예외를 발생 시키거나 스택 리소스를 유지하고 한계에 도달하기 전에 예외를 발생시켜 수정 논리를위한 공간을 제공하거나 println구현이 호출을하지 않거나 ++ 작업이 완료됩니다. 따라서 println호출은 예외에 의해 전달됩니다.


이것이 제가 의미하는 바입니다. "System.out.println은 호출 스택에서 3 개의 세그먼트가 필요하기 때문이라고 생각했습니다."-하지만 정확히이 숫자 인 이유에 대해 의아해했으며 지금은 숫자가 서로 크게 다른 이유가 더 궁금합니다. (가상) 기계
flrnb

나는 그것에 부분적으로 동의하지만 내가 동의하지 않는 것은`println의 실제 구현에 의존적`이라는 진술에있다. 구현보다는 각 jvm의 스택 크기와 관련이있다.
Jatin 2013-07-24

6
  1. main재귀 깊이에서 스택이 오버플로 될 때까지 자체적으로 반복됩니다 R.
  2. 재귀 깊이의 catch 블록 R-1이 실행됩니다.
  3. 재귀 깊이에서 catch 블록 R-1평가됩니다 cnt++.
  4. 깊이 R-1호출 의 catch 블록 은 스택에의 이전 값을 println배치 cnt합니다. println내부적으로 다른 메서드를 호출하고 지역 변수 등을 사용합니다. 이러한 모든 프로세스에는 스택 공간이 필요합니다.
  5. 스택이 이미 한계를 파악하고 있고 호출 / 실행 println에 스택 공간이 필요하기 때문에 새로운 스택 오버플로가 depth R-1대신 depth 에서 트리거됩니다 R.
  6. 2-5 단계가 다시 발생하지만 재귀 깊이에서 발생합니다 R-2.
  7. 2-5 단계가 다시 발생하지만 재귀 깊이에서 발생합니다 R-3.
  8. 2-5 단계가 다시 발생하지만 재귀 깊이에서 발생합니다 R-4.
  9. 2-4 단계가 다시 발생하지만 재귀 깊이에서 발생합니다 R-5.
  10. 이제 println완료 하기 에 충분한 스택 공간 이 있습니다 (이는 구현 세부 사항이며 다를 수 있음에 유의하십시오).
  11. cnt깊이 후 증가 된 R-1, R-2, R-3, R-4, 그리고 마지막에 R-5. 다섯 번째 post-increment는 인쇄 된 것 인 4를 반환했습니다.
  12. 함께 main깊이 성공적으로 완료 R-5, 더 catch 블록없이 전체 스택 풀려 실행하고 프로그램이 완료된다.

1

한동안 샅샅이 뒤져 답을 찾았다 고는 말할 수 없지만 지금은 꽤 가깝다고 생각합니다.

첫째, 언제 StackOverflowError유언장이 던져 졌는지 알아야 합니다. 사실 자바 스레드의 스택은 메소드를 호출하고 재개하는 데 필요한 모든 데이터를 포함하는 프레임을 저장합니다. Java Language Specifications for JAVA 6 에 따르면 메서드를 호출 할 때

이러한 활성화 프레임을 만드는 데 사용할 수있는 메모리가 충분하지 않으면 StackOverflowError가 발생합니다.

둘째, " 이러한 활성화 프레임을 생성하는 데 사용할 수있는 메모리가 충분하지 않습니다 " 무엇인지 명확히해야합니다 . JAVA 6 용 Java Virtual Machine 사양에 따르면 ,

프레임에 힙이 할당 될 수 있습니다.

따라서 프레임이 생성 될 때 스택 프레임을 생성하기에 충분한 힙 공간과 프레임이 힙 할당 된 경우 새 스택 프레임을 가리키는 새 참조를 저장하기에 충분한 스택 공간이 있어야합니다.

이제 질문으로 돌아 갑시다. 위에서 우리는 메서드가 실행될 때 동일한 양의 스택 공간이 필요할 수 있음을 알 수 있습니다. 그리고 호출 System.out.println(아마도)에는 5 단계의 메소드 호출이 필요하므로 5 개의 프레임을 생성해야합니다. 그런 StackOverflowError다음를 버리면 5 프레임의 참조를 저장할 수있는 충분한 스택 공간을 확보하기 위해 5 번 뒤로 돌아 가야합니다. 따라서 4가 인쇄됩니다. 5 개가 아닌 이유는 무엇입니까? 사용하기 때문에 cnt++. 로 변경 ++cnt하면 5를 얻을 수 있습니다.

그리고 스택의 크기가 높은 수준으로 올라가면 가끔 50 개를 얻게됩니다. 사용 가능한 힙 공간의 양을 고려해야하기 때문입니다. 스택의 크기가 너무 크면 스택 전에 힙 공간이 부족할 수 있습니다. 그리고 (아마도)의 스택 프레임의 실제 크기 System.out.println는의 약 51 배 main이므로 51 번 뒤로 돌아가서 50을 인쇄합니다.


내 첫 번째 생각은 또한 메서드 호출 수준을 세는 것이 었습니다 (그리고 당신이 맞습니다, 나는 증가 cnt를 게시한다는 사실에주의를 기울이지 않았습니다).하지만 솔루션이 그렇게 간단하다면 왜 결과가 플랫폼에 따라 많이 다를까요? 및 VM 구현?
flrnb

@flrnb 다른 플랫폼이 스택 프레임의 크기에 영향을 미칠 수 있고 다른 버전의 jre가 구현 System.out.print또는 메서드 실행 전략에 영향을 미칠 수 있기 때문 입니다. 위에서 설명한 것처럼 VM 구현은 스택 프레임이 실제로 저장되는 위치에도 영향을줍니다.
Jay

0

이것은 질문에 대한 정확한 답은 아니지만 내가 만난 원래 질문과 문제를 어떻게 이해했는지에 무언가를 추가하고 싶었습니다.

원래 문제에서 가능한 경우 예외가 포착됩니다.

예를 들어 jdk 1.7을 사용하면 처음에 발생합니다.

그러나 이전 버전의 jdk에서는 예외가 발생하는 첫 번째 장소에서 포착되지 않으므로 4, 50 등으로 보입니다.

이제 다음과 같이 try catch 블록을 제거하면

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

그러면 cntant의 모든 값이 throw 된 예외 (jdk 1.7에서)를 볼 수 있습니다.

cmd가 모든 출력과 예외를 표시하지 않기 때문에 netbeans를 사용하여 출력을 확인했습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.