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

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 또는 다른 무엇인지에 대한 것입니다. 시스템.



답변

나는 다른 사람들이 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


답변

이것은 잘못된 재귀 호출의 희생자입니다. 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 값이 다를 수 있습니다 .

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


답변

동작은 스택 크기에 따라 다릅니다 (을 사용하여 수동으로 설정할 수 있습니다 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. 따라서 상관 관계입니다.


답변

표시된 숫자는 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호출은 예외에 의해 전달됩니다.


답변

  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 블록없이 전체 스택 풀려 실행하고 프로그램이 완료된다.

답변

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

첫째, 언제 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을 인쇄합니다.


답변

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

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

예를 들어 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를 사용하여 출력을 확인했습니다.