Bir döngüde kalan işlemi yürüten Java iş parçacığı diğer tüm iş parçacıklarını engeller


123

Aşağıdaki kod parçacığı iki iş parçacığı yürütür, biri her saniyede günlük tutan basit bir zamanlayıcıdır, ikincisi kalan işlemi yürüten sonsuz bir döngüdür:

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);
            }
        }
    }
}

Bu, aşağıdaki sonucu verir:

[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

Sonsuz görevin diğer tüm konuları neden 13,3 saniye engellediğini anlamıyorum. İş parçacığı önceliklerini ve diğer ayarları değiştirmeye çalıştım, hiçbir şey işe yaramadı.

Bunu düzeltmek için herhangi bir öneriniz varsa (işletim sistemi bağlam değiştirme ayarlarının değiştirilmesi dahil) lütfen bana bildirin.


8
@Marthin GC değil. Bu JIT. İle Koşu -XX:+PrintCompilationI anda aşağıdaki olsun genişletilmiş gecikme biter: TestBlockingThread :: lambda 2 (24 bayt) @ $ 0 DERLEME SKIPPED: (farklı kademedeki yeniden deneme) önemsiz sonsuz döngü
Andreas

4
Sistemimde yeniden üretiyor ve tek değişiklikle günlük çağrısını System.out.println ile değiştirdim. Bir zamanlayıcı sorunu gibi görünüyor çünkü Runnable'ın while (true) döngüsünün içine 1 ms uyku eklerseniz, diğer iş parçacığındaki duraklama ortadan kalkar.
JJF

3
Bunu tavsiye ettiğimden değil, ancak JIT kullanımını devre dışı bırakırsanız -Djava.compiler=NONE, bu olmayacak.
Andreas

3
Tek bir yöntem için JIT'yi sözde devre dışı bırakabilirsiniz. Belirli bir yöntem / sınıf için Java JIT'i devre dışı bırakma
Andreas

3
Bu kodda tamsayı bölümü yoktur. Lütfen başlığınızı ve sorunuzu düzeltin.
Marquis of Lorne

Yanıtlar:


94

Buradaki tüm açıklamalardan sonra (teşekkürler Peter Lawrey ), bu duraklamanın ana kaynağının, döngü içindeki kayıt noktasına oldukça nadiren ulaşılması olduğunu, bu nedenle JIT tarafından derlenen kod değişimi için tüm iş parçacıkları durdurmanın uzun zaman aldığını gördük.

Ancak daha derine inmeye ve neden kayıt noktasına nadiren ulaşıldığını bulmaya karar verdim . Biraz kafa karıştırıcı buldum neden geri sıçradıwhileBu durumda döngünün "güvenli" olmadığını .

Bu yüzden -XX:+PrintAssemblytüm ihtişamıyla yardım etmeye çağırıyorum

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

Biraz araştırdıktan sonra, lambda'nın üçüncü yeniden derlemesinden sonra C2 derleyicisinin noktası yoklamalarını döngü içine tamamen attığını .

GÜNCELLEME

Profilleme aşamasında değişken ihiçbir zaman 0'a eşit görülmedi. Bu yüzden C2spekülatif olarak bu dalı optimize etti, böylece döngü şöyle bir şeye dönüştürüldü

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

Başlangıçta sonsuz döngünün bir sayaç ile düzenli bir sonlu döngü şeklinde yeniden şekillendirildiğini unutmayın! Sonlu sayılan döngülerde kayıt noktası yoklamalarını ortadan kaldırmak için JIT optimizasyonu nedeniyle, bu döngüde de kayıt noktası yoklaması yoktu.

Bir süre sonra, igeri sarıldı 0ve alışılmadık tuzak alındı. Yöntem, optimize edilmemiş ve yorumlayıcıda yürütülmeye devam edilmiştir. Yeniden derleme sırasında yeni bir bilgi C2ile sonsuz döngü fark edildi ve derlemeyi bıraktı. Yöntemin geri kalanı tercümanda uygun kayıt noktaları ile devam etti.

Nitsan Wakart tarafından yazılan, güvenli noktaları ve bu özel konuyu kapsayan , okunması gereken harika bir blog yazısı var "Güvenli Noktalar: Anlam, Yan Etkiler ve Genel Giderler" .

Çok uzun sayılan döngülerde kayıt noktasının ortadan kaldırılmasının bir sorun olduğu bilinmektedir. Hata JDK-5014723( Vladimir Ivanov sayesinde ) bu sorunu çözüyor .

Çözüm, hata nihayet giderilene kadar kullanılabilir.

  1. Sen kullanmayı deneyebilirsiniz -XX:+UseCountedLoopSafepoints(o olacak genel performans ceza neden ve JVM çökmesine yol açabilir JDK-8161147 ). C2Derleyiciyi kullandıktan sonra , geri atlamalarda güvenli noktaları tutmaya devam edin ve orijinal duraklama tamamen kaybolur.
  2. Sorunlu yöntemin derlemesini kullanarak açıkça devre dışı bırakabilirsiniz.
    -XX:CompileCommand='exclude,binary/class/Name,methodName'

  3. Veya manuel olarak kayıt noktası ekleyerek kodunuzu yeniden yazabilirsiniz. Örnek için Thread.yield()döngüsünün sonunda çağrı veya hatta değişen int iiçin long i(sayesinde, Nitsan Wakart ) da duraklama çözecektir.


7
Bu, nasıl düzeltileceği sorusunun gerçek cevabıdır .
Andreas

UYARI: JVM'yi çökertebileceğinden-XX:+UseCountedLoopSafepoints üretimde kullanmayın . Şimdiye kadarki en iyi çözüm, uzun döngüyü manuel olarak daha kısa olanlara bölmektir.
apangin

@apangin aah. anladım! teşekkür ederim :) bu yüzden c2kasa noktalarını kaldırıyor! ama anlamadığım bir şey daha sonra ne olacağı. Görebildiğim kadarıyla döngü açtıktan (?) sonra hiçbir kayıt noktası kalmadı ve stw yapmanın bir yolu yok gibi görünüyor. Yani bir tür zaman aşımı oluyor ve optimizasyon yok oluyor?
vsminkov

2
Önceki yorumum doğru değildi. Şimdi ne olacağı tamamen açık. Profil oluşturma aşamasında iasla 0 değildir, bu nedenle döngü spekülatif olarak, yani for (int i = osr_value; i != 0; i++) { if (1 % i == 0) uncommon_trap(); } uncommon_trap();normal sonlu sayılan döngü gibi bir şeye dönüştürülür . i0'a geri döndükten sonra , yaygın olmayan tuzak alınır, yöntem deoptimize edilir ve yorumlayıcıda işleme devam edilir. Yeni bilgiyle yeniden derleme sırasında JIT sonsuz döngüyü tanır ve derlemeden vazgeçer. Yöntemin geri kalanı, uygun kayıt noktaları ile tercümanda yürütülür.
apangin

1
Bir int yerine ia'yı uzun yapabilirsiniz, bu da döngüyü "sayılmamış" hale getirir ve sorunu çözer.
Nitsan Wakart

64

Kısacası, döngünün içinde güvenli bir noktaya sahip değilsiniz. i == 0 ulaşıldığı . Bu yöntem derlendiğinde ve değiştirilecek kodu tetiklediğinde, tüm iş parçacıklarını güvenli bir noktaya getirmesi gerekir, ancak bu çok uzun bir zaman alır ve yalnızca kodu çalıştıran iş parçacığını değil, JVM'deki tüm iş parçacıklarını kilitler.

Aşağıdaki komut satırı seçeneklerini ekledim.

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

Daha uzun sürdüğü görülen kayan noktayı kullanmak için kodu da değiştirdim.

boolean b = 1.0 / i == 0;

Ve çıktıda gördüğüm şey

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

Not: Kodun değiştirilebilmesi için iş parçacıklarının güvenli bir noktada durdurulması gerekir. Bununla birlikte, burada böyle güvenli bir noktaya çok nadiren ulaşıldığı görülmektedir (muhtemelen yalnızca i == 0görevi

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

Benzer bir gecikme görüyorum.

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

Döngüye dikkatlice kod ekleyerek daha uzun bir gecikme elde edersiniz.

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

alır

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

Bununla birlikte, kodu her zaman güvenli bir noktası olan yerel bir yöntemi kullanacak şekilde değiştirin (eğer bu bir içsel değilse)

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

baskılar

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

Not: if (Thread.currentThread().isInterrupted()) { ... }bir döngüye eklemek güvenli bir nokta ekler.

Not: Bu, 16 çekirdekli bir makinede gerçekleşti, bu nedenle CPU kaynakları eksikliği yok.


1
Yani bu bir JVM hatası, değil mi? "Hata", spesifikasyon ihlali değil, ciddi uygulama kalitesi sorunu anlamına gelir.
usr

1
@vsminkov, güvenli noktaların eksikliğinden dolayı dünyayı birkaç dakikalığına durdurabiliyor olması, bir hata gibi ele alınmalı gibi görünüyor. Çalışma zamanı, uzun beklemelerden kaçınmak için güvenlik noktaları sunmaktan sorumludur.
Voo

1
@Voo, ancak diğer yandan her geri atlamada güvenlik noktaları tutmak çok fazla cpu döngüsüne mal olabilir ve tüm uygulamanın gözle görülür performans düşüşüne neden olabilir. ama sana katılıyorum bu özel durumda, kayıt noktasını korumak yasal görünüyor
vsminkov

9
@Voo iyi ... Performans optimizasyonları söz konusu olduğunda bu resmi her zaman hatırlıyorum : D
vsminkov

1
.NET buraya kayıt noktaları ekler (ancak .NET yavaş üretti). Olası bir çözüm, döngüyü parçalamaktır. İki döngüye bölün, iç tarafın 1024 öğelik partileri kontrol etmemesini sağlayın ve dış döngü, grupları ve güvenlik noktalarını yönlendirir. Uygulamada daha az, kavramsal olarak yükü 1024x azaltır.
usr

26

Neden cevabını buldum . Güvenli noktalar olarak adlandırılırlar ve en iyi GC nedeniyle gerçekleşen Dünyayı Durdur olarak bilinir.

Bu makalelere bakın: JVM'de dünyayı durdurma duraklamalarını günlüğe kaydetme

Farklı olaylar, JVM'nin tüm uygulama iş parçacıklarını duraklatmasına neden olabilir. Bu tür duraklamalara Dünyayı Durdur (STW) duraklamaları denir. Bir STW duraklamasının tetiklenmesinin en yaygın nedeni çöp toplamadır (örneğin github'da), ancak farklı JIT eylemleri (örnek), önyargılı kilit iptali (örnek), belirli JVMTI işlemleri ve daha pek çoğu uygulamanın durdurulmasını gerektirir.

Uygulama ipler güvenle durdurulabilir hangi noktaları, sürpriz, denir safepoints . Bu terim sıklıkla tüm STW duraklamalarına atıfta bulunmak için kullanılır.

GC günlüklerinin etkinleştirilmesi az çok yaygındır. Ancak bu, tüm kayıt noktaları hakkında bilgi toplamaz. Hepsini elde etmek için şu JVM seçeneklerini kullanın:

-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime

Açıkça GC'ye atıfta bulunan isimlendirmeyi merak ediyorsanız, paniğe kapılmayın - bu seçenekleri açmak sadece çöp toplama duraklamalarını değil, tüm kayıt noktalarını günlüğe kaydeder. Yukarıda belirtilen bayraklarla aşağıdaki bir örneği (github'da kaynak) çalıştırırsanız.

HotSpot Terimler Sözlüğünü okurken , bunu tanımlar:

safepoint

Program yürütme sırasında tüm GC köklerinin bilindiği ve tüm yığın nesnesi içeriklerinin tutarlı olduğu bir nokta. Global bir bakış açısından, GC çalışmadan önce tüm iş parçacıkları bir kayıt noktasında bloke edilmelidir. (Özel bir durum olarak, JNI kodunu çalıştıran iş parçacıkları, yalnızca tutamaçları kullandıklarından, çalışmaya devam edebilirler. Bir kayıt noktası sırasında, tanıtıcının içeriğini yüklemek yerine engellemeleri gerekir.) Yerel bir bakış açısından, bir kayıt noktası ayırt edici bir noktadır. çalıştıran iş parçacığının GC için engelleyebileceği bir kod bloğunda. Çoğu arama sitesi güvenli nokta olarak nitelendirilir.Her kayıt noktasında geçerli olan ve güvenli olmayan noktalarda göz ardı edilebilecek güçlü değişmezler vardır. Hem derlenmiş Java kodu hem de C / C ++ kodu, kayıt noktaları arasında optimize edilebilir, ancak kayıt noktaları arasında daha az optimize edilir. JIT derleyicisi her kayıt noktasında bir GC haritası yayar. Sanal makinedeki C / C ++ kodu, potansiyel güvenlik noktalarını işaretlemek için stilize edilmiş makro tabanlı kurallar (örneğin, TRAPS) kullanır.

Yukarıda belirtilen bayraklarla çalışarak şu çıktıyı alıyorum:

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

Üçüncü STW olayına dikkat edin:
Toplam
durdurma süresi: 10.7951187 saniye Durdurma konuları: 10.7950774 saniye

JIT'in kendisi neredeyse hiç zaman almadı, ancak JVM bir JIT derlemesi yapmaya karar verdikten sonra STW moduna girdi, ancak derlenecek kodun (sonsuz döngü) bir çağrı sitesi olmadığı için hiçbir kayıt noktasına ulaşılamadı.

STW, JIT sonunda beklemeyi bıraktığında ve kodun sonsuz döngüde olduğu sonucuna vardığında sona erer.


"Kayıt Noktası - Program yürütme sırasında tüm GC köklerinin bilindiği ve tüm yığın nesnesi içeriklerinin tutarlı olduğu bir nokta" - Bu, yalnızca yerel değer türü değişkenlerini ayarlayan / okuyan bir döngüde neden doğru olmasın?
BlueRaja - Dany Pflughoeft

@ BlueRaja-DannyPflughoeft Bu soruyu
cevabımda

5

Yorum dizilerini izledikten ve kendi başıma bazı testler yaptıktan sonra, duraklamanın JIT derleyicisinden kaynaklandığını düşünüyorum. JIT derleyicisinin neden bu kadar uzun sürdüğü, hata ayıklama yeteneğimin ötesinde.

Ancak, sadece bunu nasıl önleyeceğinizi sorduğunuz için bir çözümüm var:

Sonsuz döngünüzü JIT derleyicisinden çıkarılabileceği bir yönteme çekin

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;
            }
        }
    }

Programınızı şu sanal makine bağımsız değişkeniyle çalıştırın:

-XX: CompileCommand = exclude, PACKAGE.TestBlockingThread :: infLoop (PACKAGE'ı paket bilgilerinizle değiştirin)

Yöntemin ne zaman JIT tarafından derleneceğini belirtmek için böyle bir mesaj almalısınız:
### Hariç tutulan derleme: statik engelleme.TestBlockingThread :: infLoop
sınıfı engelleme adlı bir pakete koyduğumu fark edebilirsiniz


1
Derleyici o kadar uzun sürmüyor, sorun kodun güvenli bir noktaya ulaşmaması çünkü döngünün içinde ne zaman dışında hiçbir şey yoki == 0
Peter Lawrey

@PeterLawrey ama döngüdeki whiledöngünün sonu neden bir kayıt noktası değil?
vsminkov

@vsminkov Bir kayıt noktası var gibi görünüyor, if (i != 0) { ... } else { safepoint(); }ancak bu çok nadirdir. yani. döngüden çıkarsanız / kırarsanız hemen hemen aynı zamanlamaları alırsınız.
Peter Lawrey

@PeterLawrey biraz araştırdıktan sonra, döngünün geri atlamasında bir kayıt noktası oluşturmanın yaygın bir uygulama olduğunu öğrendim. Sadece bu özel durumdaki farkın ne olduğunu merak ediyorum. belki
safım

@vsminkov JIT'in döngüde bir kayıt noktası gördüğünden şüpheleniyorum, bu yüzden sonuna bir tane eklemez.
Peter Lawrey
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.