루프에서 나머지 작업을 실행하는 Java 스레드는 다른 모든 스레드를 차단합니다.


123

다음 코드 조각은 두 개의 스레드를 실행합니다. 하나는 매초 간단한 타이머 로깅이고, 두 번째는 나머지 작업을 실행하는 무한 루프입니다.

public class TestBlockingThread {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestBlockingThread.class);

    public static final void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            int i = 0;
            while (true) {
                i++;
                if (i != 0) {
                    boolean b = 1 % i == 0;
                }
            }
        };

        new Thread(new LogTimer()).start();
        Thread.sleep(2000);
        new Thread(task).start();
    }

    public static class LogTimer implements Runnable {
        @Override
        public void run() {
            while (true) {
                long start = System.currentTimeMillis();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // do nothing
                }
                LOGGER.info("timeElapsed={}", System.currentTimeMillis() - start);
            }
        }
    }
}

결과는 다음과 같습니다.

[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1004
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1003
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=13331
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1006
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1003
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1004
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1004

무한 작업이 다른 모든 스레드를 13.3 초 동안 차단하는 이유를 이해할 수 없습니다. 스레드 우선 순위 및 기타 설정을 변경하려고 시도했지만 아무것도 작동하지 않았습니다.

이 문제를 해결하기위한 제안 (OS 컨텍스트 전환 설정 조정 포함)이 있으면 알려주세요.


8
@Marthin 아니 GC. JIT입니다. -XX:+PrintCompilation확장 된 지연이 끝날 때 다음과 같이 실행합니다 . TestBlockingThread :: lambda $ 0 @ 2 (24 바이트) COMPILE SKIPPED : trivial infinite loop (retry at different tier)
Andreas

4
로그 호출을 System.out.println으로 바꾼 유일한 변경 사항으로 내 시스템에서 재현됩니다. Runnable의 while (true) 루프 내부에 1ms 절전을 도입하면 다른 스레드의 일시 중지가 사라지기 때문에 스케줄러 문제처럼 보입니다.
JJF

3
권장하지는 않지만을 사용 하여 JIT를 비활성화 하면 -Djava.compiler=NONE발생하지 않습니다.
Andreas

3
단일 메서드에 대해 JIT를 비활성화 할 수 있습니다. 특정 메서드 / 클래스에 대해서는 Java JIT 비활성화를
Andreas

3
이 코드에는 정수 나눗셈이 없습니다. 제목과 질문을 수정하십시오.
Marquis of Lorne

답변:


94

여기에서 모든 설명을 마친 후 ( Peter Lawrey 덕분에 )이 일시 중지의 주요 원인은 루프 내부의 safepoint에 거의 도달하지 않기 때문에 JIT 컴파일 된 코드 교체를 위해 모든 스레드를 중지하는 데 오랜 시간이 걸린다는 사실을 발견했습니다.

그러나 나는 더 깊이 들어가서 safepoint에 거의 도달하지 않는지 찾기로 결정 했습니다. while이 경우 루프 의 역 점프 가 "안전"하지 않은 이유가 약간 혼란 스러웠습니다 .

그래서 나는 -XX:+PrintAssembly모든 영광을 소환 하여

-XX:+UnlockDiagnosticVMOptions \
-XX:+TraceClassLoading \
-XX:+DebugNonSafepoints \
-XX:+PrintCompilation \
-XX:+PrintGCDetails \
-XX:+PrintStubCode \
-XX:+PrintAssembly \
-XX:PrintAssemblyOptions=-Mintel

몇 가지 조사 후 람다 C2컴파일러 의 세 번째 재 컴파일 후 루프 내부에서 safepoint 폴링이 완전히 사라 졌다는 것을 발견했습니다 .

최신 정보

프로파일 링 단계 변수 중 i 는 0과 같지 않은 것으로 나타났습니다 C2. 그래서이 분기를 추측 적으로 최적화하여 루프가 다음과 같은 것으로 변환되었습니다.

for (int i = OSR_value; i != 0; i++) {
    if (1 % i == 0) {
        uncommon_trap();
    }
}
uncommon_trap();

원래 무한 루프는 카운터가있는 일반 유한 루프로 재구성되었습니다! 유한 카운트 루프에서 safepoint 폴링을 제거하기위한 JIT 최적화로 인해이 루프에서도 safepoint 폴링이 없었습니다.

몇 시간 후, i 돌아가서 0흔하지 않은 함정을 가져갔습니다. 이 메서드는 최적화되지 않았고 인터프리터에서 계속 실행되었습니다. 새로운 지식 C2으로 재 컴파일하는 동안 무한 루프를 인식하고 컴파일을 포기했습니다. 나머지 방법은 적절한 safepoint를 사용하여 인터프리터에서 진행되었습니다.

반드시 읽어야 할 훌륭한 블로그 게시물 "Safepoints : 의미, 부작용 및 오버 헤드"가 있습니다.Safepoint와이 특정 문제를 다루는 Nitsan Wakart 가 있습니다.

매우 긴 카운트 루프에서 Safepoint 제거는 문제로 알려져 있습니다. 버그 JDK-5014723(감사합니다 Vladimir Ivanov )는이 문제를 해결합니다.

버그가 최종적으로 수정 될 때까지 해결 방법을 사용할 수 있습니다.

  1. 사용을 시도 할 수 있습니다 -XX:+UseCountedLoopSafepoints( 전체 성능 저하 유발 하고 JVM 충돌로 이어질 수 있습니다 JDK-8161147 ). 사용 후C2 컴파일러는 뒤로 점프에서 safepoints를 계속 유지하고 원래 일시 중지는 완전히 사라집니다.
  2. 다음을 사용하여 문제가있는 메서드의 컴파일을 명시 적으로 비활성화 할 수 있습니다.
    -XX:CompileCommand='exclude,binary/class/Name,methodName'

  3. 또는 수동으로 safepoint를 추가하여 코드를 다시 작성할 수 있습니다. 예를 들어 Thread.yield()주기의 끝에서 호출 또는 변경 int ilong i(덕분에, Nitsan Wakart는 )도 일시 정지가 해결됩니다.


7
이것이 어떻게 고치는 지에 대한 진정한 답 입니다.
Andreas

경고 : JVM과 충돌-XX:+UseCountedLoopSafepoints 할 수 있으므로 프로덕션에서 사용하지 마십시오 . 지금까지 가장 좋은 해결 방법은 긴 루프를 짧은 루프로 수동으로 분할하는 것입니다.
apangin

@apangin 아아. 알았다! 감사합니다 :) 그래서 c2safepoints를 제거합니다! 그러나 내가 얻지 못한 또 하나의 것은 다음에 무엇을 할 것인지입니다. 내가 볼 수있는 한 루프 언 롤링 (?) 후에 남은 safepoint가없고 stw를 수행 할 방법이없는 것 같습니다. 그래서 어떤 종류의 시간 초과가 발생하고 최적화 해제가 발생합니까?
vsminkov

2
내 이전 의견이 정확하지 않았습니다. 이제 무슨 일이 일어나는지 완전히 분명합니다. 프로파일 링 단계 i에서 절대 0이 아니므로 루프는 추측에 for (int i = osr_value; i != 0; i++) { if (1 % i == 0) uncommon_trap(); } uncommon_trap();따라 예를 들어 일반 유한 계수 루프 와 같은 것으로 변환 됩니다. 일단 i랩 0으로 백업, 드문 트랩, 촬영 방법은 deoptimized되고 인터프리터에서 진행. 새로운 지식으로 재 컴파일하는 동안 JIT는 무한 루프를 인식하고 컴파일을 포기합니다. 나머지 메서드는 적절한 safepoint가있는 인터프리터에서 실행됩니다.
apangin

1
int 대신 ia를 길게 만들면 루프가 "계산되지 않음"으로되어 문제를 해결할 수 있습니다.
Nitsan Wakart

64

요컨대, 당신이 가지고있는 루프는 i == 0 도달 는 . 이 메소드가 컴파일되고 교체 될 코드를 트리거 할 때 모든 스레드를 안전한 지점으로 가져와야하지만 이는 코드를 실행하는 스레드뿐만 아니라 JVM의 모든 스레드를 잠그는 데 매우 오랜 시간이 걸립니다.

다음 명령 줄 옵션을 추가했습니다.

-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime -XX:+PrintCompilation

또한 시간이 오래 걸리는 부동 소수점을 사용하도록 코드를 수정했습니다.

boolean b = 1.0 / i == 0;

그리고 출력에서 ​​보는 것은

timeElapsed=100
Application time: 0.9560686 seconds
  41423  280 %     4       TestBlockingThread::lambda$main$0 @ -2 (27 bytes)   made not entrant
Total time for which application threads were stopped: 40.3971116 seconds, Stopping threads took: 40.3967755 seconds
Application time: 0.0000219 seconds
Total time for which application threads were stopped: 0.0005840 seconds, Stopping threads took: 0.0000383 seconds
  41424  281 %     3       TestBlockingThread::lambda$main$0 @ 2 (27 bytes)
timeElapsed=40473
  41425  282 %     4       TestBlockingThread::lambda$main$0 @ 2 (27 bytes)
  41426  281 %     3       TestBlockingThread::lambda$main$0 @ -2 (27 bytes)   made not entrant
timeElapsed=100

참고 : 코드를 교체하려면 안전한 지점에서 스레드를 중지해야합니다. 그러나 여기에서는 그러한 안전한 지점에 매우 드물게 도달하는 것으로 보입니다 ( i == 0작업을 다음으로 변경하는 경우에만 가능).

Runnable task = () -> {
    for (int i = 1; i != 0 ; i++) {
        boolean b = 1.0 / i == 0;
    }
};

비슷한 지연이 있습니다.

timeElapsed=100
Application time: 0.9587419 seconds
  39044  280 %     4       TestBlockingThread::lambda$main$0 @ -2 (28 bytes)   made not entrant
Total time for which application threads were stopped: 38.0227039 seconds, Stopping threads took: 38.0225761 seconds
Application time: 0.0000087 seconds
Total time for which application threads were stopped: 0.0003102 seconds, Stopping threads took: 0.0000105 seconds
timeElapsed=38100
timeElapsed=100

루프에 코드를 신중하게 추가하면 지연이 더 길어집니다.

for (int i = 1; i != 0 ; i++) {
    boolean b = 1.0 / i / i == 0;
}

얻다

 Total time for which application threads were stopped: 59.6034546 seconds, Stopping threads took: 59.6030773 seconds

그러나 항상 안전한 지점이있는 네이티브 메서드를 사용하도록 코드를 변경하십시오 (내재적이지 않은 경우).

for (int i = 1; i != 0 ; i++) {
    boolean b = Math.cos(1.0 / i) == 0;
}

인쇄물

Total time for which application threads were stopped: 0.0001444 seconds, Stopping threads took: 0.0000615 seconds

참고 : if (Thread.currentThread().isInterrupted()) { ... }루프에 추가하면 안전한 지점이 추가됩니다.

참고 : 이는 16 코어 머신에서 발생하므로 CPU 리소스가 부족하지 않습니다.


1
그래서 그것은 JVM 버그입니다. 여기서 "버그"는 사양 위반이 아닌 심각한 구현 품질 문제를 의미합니다.
usr

1
@vsminkov는 safepoints 부족으로 인해 몇 분 동안 세계를 멈출 수 있다는 것은 버그로 취급되어야하는 것처럼 들립니다. 런타임은 긴 대기를 피하기 위해 safepoint를 도입해야합니다.
Voo

1
@Voo 그러나 다른 한편으로 모든 백 점프에서 safepoint를 유지하면 많은 CPU 사이클이 소모되고 전체 애플리케이션의 성능이 눈에 띄게 저하 될 수 있습니다. 하지만 동의합니다. 이 특별한 경우에는
safepoint

9
@Voo 음 ... 난 항상 기억 D : 그것은 성능 최적화에 올 때 사진을
vsminkov

1
.NET은 여기에 safepoint를 삽입합니다 (하지만 .NET은 느리게 생성 된 코드를 가지고 있습니다). 가능한 해결책은 루프를 청크하는 것입니다. 두 개의 루프로 분할하고 내부가 1024 요소의 배치를 확인하지 않도록하고 외부 루프는 배치와 Safepoint를 구동합니다. 개념적으로 오버 헤드를 1024 배로 줄여서 실제로는 더 적습니다.
usr

26

이유에 대한 답을 찾았습니다 . 그들은 safepoints라고 불리며 GC로 인해 발생하는 Stop-The-World로 가장 잘 알려져 있습니다.

이 기사를 참조하십시오 : JVM에서 stop-the-world 일시 중지 로깅

다른 이벤트로 인해 JVM이 모든 애플리케이션 스레드를 일시 중지 할 수 있습니다. 이러한 일시 중지를 STW (Stop-The-World) 일시 중지라고합니다. STW 일시 중지가 트리거되는 가장 일반적인 원인은 가비지 수집 (github의 예)이지만 다른 JIT 작업입니다. (예), 편향된 잠금 취소 (예), 특정 JVMTI 작업 등도 애플리케이션을 중지해야합니다.

애플리케이션 스레드가 안전하게 중지 될 수있는 지점을 놀라움, safepoints 라고 합니다. 이 용어는 또한 모든 STW 일시 중지를 나타내는 데 자주 사용됩니다.

GC 로그를 사용하는 것은 다소 일반적입니다. 그러나 이것은 모든 Safepoint에 대한 정보를 캡처하지는 않습니다. 모든 것을 얻으려면 다음 JVM 옵션을 사용하십시오.

-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime

GC를 명시 적으로 참조하는 이름 지정에 대해 궁금한 경우 놀라지 마십시오.이 옵션을 켜면 가비지 수집 일시 중지뿐만 아니라 모든 safepoint가 기록됩니다. 위에 지정된 플래그를 사용하여 다음 예제 (github의 소스)를 실행하는 경우.

HotSpot 용어집을 읽고 다음을 정의합니다.

Safepoint

프로그램 실행 중 모든 GC 루트가 알려져 있고 모든 힙 개체 내용이 일치하는 지점입니다. 글로벌 관점에서 모든 스레드는 GC가 실행되기 전에 안전한 지점에서 차단되어야합니다. (특별한 경우로 JNI 코드를 실행하는 스레드는 핸들 만 사용하기 때문에 계속 실행될 수 있습니다. safepoint 중에는 핸들의 내용을로드하는 대신 차단해야합니다.) 로컬 관점에서 Safepoint는 구별 지점입니다. 실행 스레드가 GC에 대해 차단 될 수있는 코드 블록에서.대부분의 통화 사이트는 Safepoint로 인정됩니다.안전하지 않은 지점에서는 무시 될 수있는 모든 안전 지점에서 적용되는 강력한 불변성이 있습니다. 컴파일 된 Java 코드와 C / C ++ 코드는 모두 safepoint 사이에서 최적화되지만 safepoint에서는 덜 최적화됩니다. JIT 컴파일러는 각 safepoint에서 GC 맵을 내 보냅니다. VM의 C / C ++ 코드는 양식화 된 매크로 기반 규칙 (예 : TRAPS)을 사용하여 잠재적 인 Safepoint를 표시합니다.

위에서 언급 한 플래그로 실행하면 다음과 같은 출력이 나타납니다.

Application time: 0.9668750 seconds
Total time for which application threads were stopped: 0.0000747 seconds, Stopping threads took: 0.0000291 seconds
timeElapsed=1015
Application time: 1.0148568 seconds
Total time for which application threads were stopped: 0.0000556 seconds, Stopping threads took: 0.0000168 seconds
timeElapsed=1015
timeElapsed=1014
Application time: 2.0453971 seconds
Total time for which application threads were stopped: 10.7951187 seconds, Stopping threads took: 10.7950774 seconds
timeElapsed=11732
Application time: 1.0149263 seconds
Total time for which application threads were stopped: 0.0000644 seconds, Stopping threads took: 0.0000368 seconds
timeElapsed=1015

세 번째 STW 이벤트를 확인하십시오.
중지 된 총 시간 : 10.7951187 초
스레드 중지 시간 : 10.7950774 초

JIT 자체는 사실상 시간이 걸리지 않았지만 JVM이 JIT 컴파일을 수행하기로 결정한 후 STW 모드에 들어 갔지만 컴파일 할 코드 (무한 루프)에 호출 사이트 가 없으므로 Safepoint에 도달 하지 못했습니다 .

STW는 JIT가 결국 대기를 포기하고 코드가 무한 루프에 있다고 결론을 내릴 때 종료됩니다.


"Safepoint-모든 GC 루트가 알려져 있고 모든 힙 객체 내용이 일관된 프로그램 실행 중 지점" -로컬 값 유형 변수 만 설정 / 읽는 루프에서 이것이 사실이 아닌 이유는 무엇입니까?
BlueRaja-Danny Pflughoeft

@ BlueRaja-DannyPflughoeft 나는 내 대답
vsminkov

5

주석 스레드와 일부 테스트를 직접 수행 한 후 일시 중지가 JIT 컴파일러에 의해 발생했다고 생각합니다. JIT 컴파일러가 그렇게 오래 걸리는 이유는 디버깅 능력을 넘어선 것입니다.

그러나 이것을 방지하는 방법 만 요청했기 때문에 해결책이 있습니다.

무한 루프를 JIT 컴파일러에서 제외 할 수있는 메서드로 가져옵니다.

public class TestBlockingThread {
    private static final Logger LOGGER = Logger.getLogger(TestBlockingThread.class.getName());

    public static final void main(String[] args) throws InterruptedException     {
        Runnable task = () -> {
            infLoop();
        };
        new Thread(new LogTimer()).start();
        Thread.sleep(2000);
        new Thread(task).start();
    }

    private static void infLoop()
    {
        int i = 0;
        while (true) {
            i++;
            if (i != 0) {
                boolean b = 1 % i == 0;
            }
        }
    }

다음 VM 인수를 사용하여 프로그램을 실행합니다.

-XX : CompileCommand = exclude, PACKAGE.TestBlockingThread :: infLoop (PACKAGE를 패키지 정보로 대체)

메소드가 JIT 컴파일되었을 때를 나타내는 다음과 같은 메시지가 표시되어야합니다.
### Excluding compile : static blocking.TestBlockingThread :: infLoop
클래스를 blocking이라는 패키지에 넣었 음을 알 수 있습니다.


1
컴파일러는 그렇게 오래 걸리지 않습니다. 문제는 다음 경우를 제외하고 루프 내부에 아무것도 없기 때문에 코드가 안전한 지점에 도달하지 않는다는 것입니다i == 0
Peter Lawrey

@PeterLawrey하지만 while루프 의 순환 종료가 Safepoint가 아닌 이유 는 무엇입니까?
vsminkov

@vsminkov 안에 안전한 지점이있는 것 if (i != 0) { ... } else { safepoint(); }같지만 이것은 매우 드뭅니다. 즉. 루프를 종료 / 중단하면 거의 동일한 타이밍을 얻습니다.
Peter Lawrey

@PeterLawrey 약간의 조사 후 루프의 백 점프에서 safepoint를 만드는 것이 일반적인 관행이라는 것을 알았습니다. 이 특별한 경우의 차이점이 무엇인지 궁금합니다. 어쩌면 내가 순진 해요하지만 난 다시 점프가 "안전"하지 왜 아무 이유도 볼
vsminkov

@vsminkov 나는 JIT가 safepoint가 루프에 있다고 생각하므로 끝에 하나를 추가하지 마십시오.
Peter Lawrey
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.