Bu yöntem neden 4 yazdırır?


111

Bir StackOverflowError'ı yakalamaya çalıştığınızda ve aşağıdaki yöntemi bulduğunuzda ne olacağını merak ediyordum:

class RandomNumberGenerator {

    static int cnt = 0;

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

Şimdi sorum:

Bu yöntem neden '4' yazdırıyor?

Bunun System.out.println()çağrı yığınında 3 segmente ihtiyaç duyduğundan olabileceğini düşündüm , ancak 3 numarasının nereden geldiğini bilmiyorum. Kaynak koduna (ve bayt koduna) baktığınızda, System.out.println()normalde 3'ten çok daha fazla yöntem çağrısına yol açar (bu nedenle çağrı yığınındaki 3 segment yeterli olmaz). Hotspot VM'nin uyguladığı optimizasyonlar nedeniyle (yöntem satır içi), sonucun başka bir VM'de farklı olup olmayacağını merak ediyorum.

Düzenle :

Çıktı son derece JVM'ye özgü göründüğü için,
Java (TM) SE Çalışma Zamanı Ortamı (derleme 1.6.0_41-b02)
Java HotSpot (TM) 64-Bit Sunucu VM (derleme 20.14-b01, karma mod) kullanarak sonuç 4'ü alıyorum


Bu sorunun Java yığınını anlamaktan neden farklı olduğunu düşündüğümün açıklaması :

Sorum neden bir cnt> 0 olduğu (açıkçası System.out.println()yığın boyutu gerektirdiği ve bir StackOverflowErrorşey yazdırılmadan önce bir başkasını attığı için ) değil, neden 4, sırasıyla 0,3,8,55 veya diğerinde başka bir özel değere sahip olduğu ile ilgili değil. sistemleri.


4
Yerelimde beklendiği gibi "0" alıyorum.
Reddy

2
Bu pek çok mimari şeyle ilgilenebilir. Çıktınızı jdk sürümüyle daha iyi yayınlayın Benim için çıktı jdk 1.7'de 0
Lokesh

3
Ben ele verdik 5, 6ve 38Java 1.7.0_10 ile
Kon

8
@Elist Altta yatan mimariyi içeren hileler yaptığınızda aynı çıktı olmayacak;)
m0skit0

3
@flrnb Bu sadece parantezleri sıralamak için kullandığım bir stil. Koşulların ve işlevlerin nerede başladığını ve bittiğini bilmemi kolaylaştırıyor. İsterseniz değiştirebilirsiniz, ancak bence bu şekilde daha okunaklı.
syb0rg

Yanıtlar:


41

Bence diğerleri neden cnt> 0'ı açıklamakta iyi bir iş çıkardılar, ancak neden cnt = 4 ve farklı ayarlar arasında neden cnt'nin bu kadar geniş bir şekilde değiştiğine dair yeterli ayrıntı yok. Bu boşluğu burada doldurmaya çalışacağım.

İzin Vermek

  • X toplam yığın boyutu olabilir
  • M main'e ilk girdiğimizde kullanılan yığın alanı olsun
  • R, ana bölüme her girdiğimizde yığın alanı artabilir
  • P çalıştırmak için gerekli yığın alanı olun System.out.println

Ana yola ilk girdiğimizde, kalan alan XM. Her özyinelemeli arama daha fazla hafıza alır. Dolayısıyla, 1 özyinelemeli çağrı için (orijinalden 1 fazla), bellek kullanımı M + R'dir. 1)> X. İlk StackOverflowError sırasında, X - M - C * R belleği kaldı.

Çalıştırabilmek System.out.prinlniçin yığında P kadar boş alana ihtiyacımız var. Böyle bir durumda, X - M - C * R> = P, o zaman 0 yazdırılacaktır. P daha fazla alana ihtiyaç duyuyorsa, kareleri yığından kaldırarak cnt ++ pahasına R belleği kazanırız.

Tüm printlnson olarak çalıştırmak için, X - M - (Cı - CNT) * R> = S. yüzden P belirli bir sistem için büyük ise, o zaman pulse büyük olacaktır.

Buna bazı örneklerle bakalım.

Örnek 1: Varsayalım

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

O zaman C = kat ((XM) / R) = 49 ve cnt = tavan ((P - (X - M - C * R)) / R) = 0.

Örnek 2: Varsayalım ki

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

O zaman C = 19 ve cnt = 2.

Örnek 3: Varsayalım ki

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

O zaman C = 20 ve cnt = 3.

Örnek 4: Varsayalım ki

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

O zaman C = 19 ve cnt = 2.

Böylece, hem sistemin (M, R ve P) hem de yığın boyutunun (X) cnt'yi etkilediğini görüyoruz.

Bir yan not olarak, catchbaşlamak için ne kadar alan gerektiğinin önemi yoktur . Yeterli alan olmadığı sürece catchcnt artmayacaktır, bu nedenle dış etkiler olmayacaktır.

DÜZENLE

Hakkında söylediklerimi geri alıyorum catch . Bir rol oynar. Başlamak için T miktarda alan gerektirdiğini varsayalım. Kalan boşluk T'den printlnbüyük olduğunda cnt artmaya başlar ve kalan boşluk T + P'den büyük olduğunda çalışır. Bu, hesaplamalara fazladan bir adım ekler ve zaten çamurlu olan analizi daha da çamurlu hale getirir.

DÜZENLE

Sonunda teorimi desteklemek için bazı deneyler yapmak için zaman buldum. Ne yazık ki, teori deneylerle uyuşmuyor. Gerçekte olan şey çok farklı.

Deney kurulumu: Varsayılan java ve varsayılan jdk ile Ubuntu 12.04 sunucusu. Xss, 1 baytlık artışlarla 460.000'e 70.000'den başlar.

Sonuçlar şu adreste mevcuttur: https://www.google.com/fusiontables/DataSource?docid=1xkJhd4s8biLghe6gZbcfUs3vT5MpS_OnscjWDbM Tekrarlanan her veri noktasının kaldırıldığı başka bir sürüm oluşturdum. Başka bir deyişle, yalnızca öncekinden farklı olan noktalar gösterilir. Bu, anormallikleri görmeyi kolaylaştırır. https://www.google.com/fusiontables/DataSource?docid=1XG_SRzrrNasepwZoNHqEAKuZlHiAm9vbEdwfsUA


İyi özet için teşekkür ederim, sanırım hepsi şu soruyu özetliyor: M, R ve P'yi ne etkiler (X, VM seçeneği -Xss ile ayarlanabildiğinden)?
flrnb

@flrnb M, R ve P sisteme özeldir. Bunları kolayca değiştiremezsiniz. Bazı sürümler arasında da farklılık göstermelerini bekliyorum.
John Tseng

Öyleyse neden Xss'i (X) değiştirerek farklı sonuçlar alıyorum? M, R ve P'nin aynı kaldığı göz önüne alındığında, X'i 100'den 10000'e değiştirmek, formülünüze göre cnt'yi etkilememeli, yoksa yanılıyor muyum?
flrnb

@flrnb X, bu değişkenlerin ayrık doğası nedeniyle tek başına cnt'yi değiştirmez. Örnek 2 ve 3 yalnızca X'te farklılık gösterir, ancak cnt farklıdır.
John Tseng

1
@JohnTseng Ayrıca cevabınızın şimdiye kadar en anlaşılır ve eksiksiz olduğunu düşünüyorum - her neyse, yığının fırlatıldığı anda gerçekte neye benzediğini ve StackOverflowErrorbunun çıktıyı nasıl etkilediğini gerçekten merak ediyorum . Yığın üzerindeki bir yığın çerçevesine yalnızca bir referans içeriyorsa (Jay'in önerdiği gibi), çıktı belirli bir sistem için oldukça tahmin edilebilir olmalıdır.
flrnb

20

Bu, kötü özyinelemeli aramanın kurbanı. Cnt değerinin neden değiştiğini merak ettiğiniz gibi , bunun nedeni yığın boyutunun platforma bağlı olmasıdır. Windows'ta Java SE 6'nın varsayılan yığın boyutu 32 bit VM'de 320k ve 64 bit VM'de 1024k'dir. Daha fazlasını buradan okuyabilirsiniz .

Farklı yığın boyutları kullanarak çalıştırabilirsiniz ve yığın taşmadan önce farklı cnt değerleri göreceksiniz.

java -Xss1024k RandomNumberGenerator

Değer 1'den büyük olsa bile cnt değerinin birden çok kez yazdırıldığını görmezsiniz, çünkü bazen print deyiminiz Eclipse veya diğer IDE'ler aracılığıyla emin olmak için hata ayıklayabileceğiniz bir hata da atıyor.

İsterseniz ifade yürütme başına hata ayıklamak için kodu aşağıdaki şekilde değiştirebilirsiniz:

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) {   

        }        
    }        
}

GÜNCELLEME:

Bu daha çok dikkat çekerken, olayları daha net hale getirmek için başka bir örnek verelim.

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

}

Kötü bir özyineleme yapmak için overflow adında başka bir yöntem oluşturduk ve println ifadesini catch bloğundan kaldırdık, böylece yazdırmaya çalışırken başka bir hata kümesi atmaya başlamaz. Bu beklendiği gibi çalışıyor. System.out.println (cnt) koymayı deneyebilirsiniz ; cnt ++ 'dan sonraki ifade ve derleyin. Ardından birden çok kez çalıştırın. Platformunuza bağlı olarak, farklı cnt değerleri alabilirsiniz .

Bu nedenle, genellikle hata yakalamıyoruz çünkü koddaki gizem fantezi değildir.


13

Davranış, yığın boyutuna bağlıdır (kullanılarak manuel olarak ayarlanabilir Xss. Yığın boyutu mimariye özeldir. JDK 7 kaynak kodundan :

// Windows'daki varsayılan yığın boyutu, yürütülebilir dosya tarafından belirlenir (java.exe
// varsayılan değeri 320K / 1MB [32bit / 64bit]). Windows sürümüne bağlı olarak,
// ThreadStackSize değerini sıfırdan farklı olarak değiştirmek bellek kullanımı üzerinde önemli bir etkiye sahip olabilir.
// os_windows.cpp'deki yorumlara bakın.

Yani StackOverflowErroratıldığında hata catch bloğunda yakalanır. İşte println()yine istisna atan başka bir yığın çağrısı. Bu tekrarlanır.

Kaç kez tekrar ediyor? - JVM'nin artık yığın aşımı olmadığını düşünmesine bağlı. Ve bu, her işlev çağrısının yığın boyutuna (bulunması zor) ve Xss. Yukarıda belirtildiği gibi, her bir işlev çağrısının varsayılan toplam boyutu ve boyutu (bellek sayfa boyutuna vb. Bağlıdır) platforma özeldir. Dolayısıyla farklı davranış.

Çağrı javaile görüşmesi -Xss 4Mbana verir 41. Bu nedenle korelasyon.


4
Cnt değerini yazdırmaya çalıştığımızda zaten aşıldığından, yığın boyutunun neden sonucu etkilemesi gerektiğini anlamıyorum. Bu nedenle, tek fark "her işlev çağrısının yığın boyutundan" gelebilir. Ve bunun neden aynı JVM sürümünü çalıştıran 2 makine arasında değişmesi gerektiğini anlamıyorum.
flrnb

Kesin davranış yalnızca JVM kaynağından elde edilebilir. Ama nedeni bu olabilir. Unutmayın bile catchbir bloktur ve bu yığın üzerinde hafızayı işgal eder. Her yöntem çağrısının ne kadar bellek aldığı bilinemez. Yığın temizlendiğinde, bir blok daha ekliyorsunuz catchve benzeri. Bu davranış olabilir. Bu sadece bir spekülasyon.
Jatin

Ve yığın boyutu iki farklı makinede farklılık gösterebilir. Yığın boyutu, işletim sistemi tabanlı birçok faktöre, yani bellek sayfa boyutuna vb.
Bağlıdır

6

Sanırım görüntülenen sayı, System.out.printlnaramanın Stackoverflowistisnayı attığı sayıdır .

Muhtemelen uygulamasına printlnve içinde yapılan yığınlama çağrısının sayısına bağlıdır .

Örnek olarak:

main()Çağrı tetik Stackoverflowçağrı i de istisna. Ana i-1 çağrısı istisnayı yakalar ve printlnbir saniyeyi tetikleyen çağrıdır Stackoverflow. cnt1'e artış alın. Ana yakalama i-2 çağrısı şimdi istisna ve çağrıdır println. Gelen printlnbir yöntem 3. istisna tetikleme denir. cnt2'ye yükseltin. bu printlngerekli tüm çağrıyı yapana ve sonunda değerini görüntüleyene kadar devam edin cnt.

Bu, daha sonra fiili uygulamasına bağlıdır println.

JDK7 için ya döngü çağrısını algılar ve istisnayı daha önce fırlatır ya da bazı yığın kaynaklarını tutar ve sınıra ulaşmadan önce istisnayı fırlatarak iyileştirme mantığına biraz yer verir ya printlnuygulama arama yapmaz ya da ++ işlemi sonra yapılır. bu printlnnedenle çağrı, istisna tarafından atlanır.


"Belki de System.out.println çağrı yığınında 3 segmente ihtiyaç duyduğundan kaynaklanıyordu" derken kastettiğim buydu - ama neden tam olarak bu sayı olduğuna şaşırmıştım ve şimdi sayının neden bu kadar büyük ölçüde farklılık gösterdiğini daha da şaşırdım. (sanal) makineler
flrnb

Kısmen katılıyorum, ancak katılmadığım şey `` println'in gerçek uygulamasına bağlı '' ifadesine bağlı.
Jatin

6
  1. mainözyineleme derinliğinde yığından taşana kadar kendi kendine yinelenir R.
  2. Özyineleme derinliğindeki catch bloğu R-1çalıştırılır.
  3. Özyineleme derinliğindeki catch bloğu R-1değerlendirilir cnt++.
  4. Derinlikte catch bloğu R-1aramaları printlnyerleştirmek cnt'yığında eski değer s. printlndahili olarak diğer yöntemleri çağırır ve yerel değişkenleri ve şeyleri kullanır. Tüm bu işlemler yığın alanı gerektirir.
  5. Yığın zaten sınırı aştığı ve çağırma / yürütme printlniçin yığın alanı gerektirdiğinden, derinlik R-1yerine derinlikte yeni bir yığın taşması tetiklenir R.
  6. Adım 2-5 tekrar gerçekleşir, ancak özyineleme derinliğindedir R-2.
  7. Adım 2-5 tekrar gerçekleşir, ancak özyineleme derinliğindedir R-3.
  8. Adım 2-5 tekrar gerçekleşir, ancak özyineleme derinliğindedir R-4.
  9. Adım 2-4 tekrar gerçekleşir, ancak özyineleme derinliğindedir R-5.
  10. Öyle ki, artık printlntamamlamak için yeterli yığın alanı var (bunun bir uygulama ayrıntısı olduğunu ve değişiklik gösterebileceğini unutmayın).
  11. cntderinliklerde sonrası artırıldığına R-1, R-2, R-3, R-4ve son olarak en R-5. Beşinci artımlı artış dört döndürdü, bu da basılan şeydi.
  12. İle mainderinlikte başarıyla tamamlandı R-5, daha catch blokları olmadan bütün yığını çözülmeler çalıştırmak ve program tamamlamalar ediliyor.

1

Bir süre etrafta dolaştıktan sonra cevabı bulduğumu söyleyemem ama şimdi oldukça yakın olduğunu düşünüyorum.

İlk olarak, bir StackOverflowErroriradenin ne zaman atılacağını bilmemiz gerekir . Aslında, bir java iş parçacığı yığını, bir yöntemi çağırmak ve devam ettirmek için gereken tüm verileri içeren çerçeveleri depolar. JAVA 6 için Java Dil Spesifikasyonlarına göre , bir yöntem çağrıldığında,

Böyle bir etkinleştirme çerçevesi oluşturmak için yeterli bellek yoksa, bir StackOverflowError atılır.

İkinci olarak, " böyle bir aktivasyon çerçevesi yaratmak için yeterli hafıza olmadığını " netleştirmeliyiz . JAVA 6 için Java Sanal Makine Şartnamelerine göre ,

çerçeveler yığın tahsis edilebilir.

Bu nedenle, bir çerçeve oluşturulduğunda, bir yığın çerçevesi oluşturmak için yeterli yığın alanı ve çerçeve ayrılmışsa yeni yığın çerçevesini işaret eden yeni referansı depolamak için yeterli yığın alanı olmalıdır.

Şimdi soruya geri dönelim. Yukarıdakilerden, bir yöntem çalıştırıldığında, aynı miktarda yığın alanına mal olabileceğini bilebiliriz. Ve çağırmak System.out.println(olabilir) 5 seviyeli yöntem çağırmaya ihtiyaç duyar, bu yüzden 5 çerçevenin oluşturulması gerekir. Daha sonra StackOverflowErroratıldığında, 5 karenin referanslarını depolamak için yeterli yığın alanı elde etmek için 5 kez geri gitmesi gerekir. Dolayısıyla 4 çıktı alınır. Neden 5 değil? Çünkü kullanıyorsun cnt++. Olarak değiştirin ++cnt, sonra 5 alırsınız.

Ve istif boyutu yüksek bir seviyeye çıktığında, bazen 50 alacağınızı fark edeceksiniz. Bunun nedeni, kullanılabilir yığın alanı miktarının o zaman dikkate alınması gerektiğidir. Yığının boyutu çok büyük olduğunda, yığından önce yığın alanı tükenebilir. Ve (belki) yığın çerçevelerinin gerçek boyutu System.out.printlnyaklaşık 51 katıdır main, bu nedenle 51 kez geriye gider ve 50 yazdırır.


İlk düşüncem, yöntem çağrılarının düzeylerini de saymaktı (ve haklısın, artış cnt gönderiyorum gerçeğine dikkat etmedim), ancak çözüm bu kadar basitse, sonuçlar platformlar arasında neden bu kadar değişsin? ve sanal makine uygulamaları?
flrnb

@flrnb Bunun nedeni, farklı platformların yığın çerçevesinin boyutunu etkileyebilmesi ve jre'nin farklı sürümlerinin System.out.print, yöntem yürütme stratejisinin uygulanmasını veya uygulanmasını etkileyeceğidir . Yukarıda açıklandığı gibi, VM uygulaması ayrıca yığın çerçevesinin nerede depolanacağını da etkiler.
Jay

0

Bu tam olarak sorunun cevabı değil ama sadece karşılaştığım orijinal soruya ve sorunu nasıl anladığıma bir şeyler eklemek istedim:

Orijinal problemde istisna, mümkün olduğu yerde yakalanır:

Örneğin jdk 1.7 ile ilk görüldüğü yerde yakalanır.

ancak jdk'nin önceki sürümlerinde, istisna ilk görüldüğü yerde yakalanmıyor gibi görünüyor, dolayısıyla 4, 50 vb.

Şimdi, try catch bloğunu aşağıdaki gibi kaldırırsanız

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

Daha sonra cntatılan istisnaların tüm değerlerini göreceksiniz (jdk 1.7'de).

Çıktıyı görmek için netbeans kullandım, çünkü cmd tüm çıktıyı ve atılan istisnayı göstermeyecektir.

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.