Scala JVM Bytecode ile Ellerinizi Kirletin
Yayınlanan: 2022-03-11Scala dili, işlevsel ve nesne yönelimli yazılım geliştirme ilkelerinin mükemmel kombinasyonu ve kanıtlanmış Java Sanal Makinesi (JVM) üzerinde uygulanması sayesinde son birkaç yılda popülerlik kazanmaya devam etti.
Scala, Java bayt kodunu derlemesine rağmen, Java dilinin algılanan eksikliklerinin çoğunu iyileştirmek için tasarlanmıştır. Tam işlevsel programlama desteği sunan Scala'nın çekirdek sözdizimi, Java programcıları tarafından açıkça oluşturulması gereken, bazıları önemli ölçüde karmaşıklık içeren birçok örtük yapı içerir.
Java bayt kodunu derleyen bir dil oluşturmak, Java Sanal Makinesinin iç işleyişini derinlemesine anlamayı gerektirir. Scala'nın geliştiricilerinin başardıklarını takdir etmek için, kaputun altına girmek ve verimli ve etkili JVM bayt kodu üretmek için Scala'nın kaynak kodunun derleyici tarafından nasıl yorumlandığını keşfetmek gerekir.
Tüm bunların nasıl uygulandığına bir göz atalım.
Önkoşullar
Bu makaleyi okumak, Java Sanal Makinesi bayt kodunun temel düzeyde anlaşılmasını gerektirir. Eksiksiz sanal makine özellikleri Oracle'ın resmi belgelerinden elde edilebilir. Bu makaleyi anlamak için tüm özellikleri okumak kritik değildir, bu nedenle, temel bilgilere hızlı bir giriş için makalenin sonunda kısa bir kılavuz hazırladım.
Aşağıda verilen örnekleri yeniden oluşturmak ve daha fazla araştırmaya devam etmek için Java bayt kodunu sökmek için bir yardımcı program gereklidir. Java Geliştirme Kiti, burada kullanacağımız kendi komut satırı yardımcı programı olan javap
sağlar. javap
nasıl çalıştığının hızlı bir gösterimi, alttaki kılavuzda yer almaktadır.
Ve elbette, örneklerle birlikte takip etmek isteyen okuyucular için Scala derleyicisinin çalışan bir kurulumu gereklidir. Bu makale Scala 2.11.7 kullanılarak yazılmıştır. Scala'nın farklı sürümleri biraz farklı bayt kodu üretebilir.
Varsayılan Alıcılar ve Ayarlayıcılar
Java kuralı her zaman ortak nitelikler için alıcı ve ayarlayıcı yöntemler sağlasa da, her birinin modelinin onlarca yıldır değişmemiş olmasına rağmen, Java programcılarının bunları kendilerinin yazması gerekir. Scala, aksine, varsayılan alıcılar ve ayarlayıcılar sağlar.
Aşağıdaki örneğe bakalım:
class Person(val name:String) { }
Şimdi Person
sınıfının içine bir göz atalım. Bu dosyayı scalac
ile derlersek, $ javap -p Person.class
çalıştırmak bize şunu verir:
Compiled from "Person.scala" public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }
Scala sınıfındaki her alan için bir alan ve onun getter yönteminin üretildiğini görebiliriz. Alan özel ve nihaidir, yöntem ise geneldir.
Person
kaynağında val
var
ile değiştirir ve yeniden derlersek, alanın final
değiştiricisi bırakılır ve ayarlayıcı yöntemi de eklenir:
Compiled from "Person.scala" public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }
Sınıf gövdesi içinde herhangi bir val
veya var
tanımlanırsa, karşılık gelen özel alan ve erişimci yöntemleri oluşturulur ve örnek oluşturma üzerine uygun şekilde başlatılır.
Sınıf düzeyinde val
ve var
alanlarının böyle bir uygulamasının, ara değerleri depolamak için sınıf düzeyinde bazı değişkenler kullanılıyorsa ve programcı tarafından hiçbir zaman doğrudan erişilmiyorsa, bu tür her bir alanın başlatılmasının bir ila iki yöntem ekleyeceği anlamına geldiğini unutmayın. sınıf ayak izi Bu tür alanlar için private
bir değiştirici eklemek, ilgili erişimcilerin bırakılacağı anlamına gelmez. Sadece özel olacaklar.
Değişken ve Fonksiyon Tanımları
Bir m()
yöntemimiz olduğunu ve bu işleve üç farklı Scala stili referans oluşturduğumuzu varsayalım:
class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }
m
yapılan bu referansların her biri nasıl inşa edilmiştir? Her durumda m
ne zaman yürütülür? Ortaya çıkan bayt koduna bir göz atalım. Aşağıdaki çıktı, javap -v Person.class
(birçok gereksiz çıktıyı atlayarak) sonuçlarını gösterir:
Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object."<init>":()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return
Sabit havuzda, m()
yöntemine yapılan başvurunun #30
dizininde saklandığını görüyoruz. Yapıcı kodunda, başlatma sırasında bu yöntemin iki kez çağrıldığını görüyoruz, komut invokevirtual #30
önce bayt ofset 11'de, sonra ofset 19'da görünüyor. İlk çağrıyı, sonucunu atayan putfield #22
komutu takip ediyor. bu yöntem, sabit havuzda #22
numaralı dizin tarafından başvurulan m1
alanına. İkinci çağrıyı aynı model takip eder, bu sefer değeri sabit havuzda #24
indekslenen m2
alanına atar.
Başka bir deyişle, val
veya var
ile tanımlanan bir değişkene yöntem atamak, yalnızca yöntemin sonucunu o değişkene atar. Oluşturulan m1()
ve m2()
yöntemlerinin bu değişkenler için basitçe alıcılar olduğunu görebiliriz. var m2
durumunda, diğer ayarlayıcılar gibi davranan ve alandaki değerin üzerine yazan m2_$eq(int)
ayarlayıcısının oluşturulduğunu da görüyoruz.
Ancak, def
anahtar sözcüğünü kullanmak farklı bir sonuç verir. Döndürülecek bir alan değeri getirmek yerine, m3()
yöntemi ayrıca invokevirtual #30
komutunu da içerir. Yani, bu yöntem her çağrıldığında m()
öğesini çağırır ve bu yöntemin sonucunu döndürür.
Gördüğümüz gibi, Scala sınıf alanlarıyla çalışmak için üç yol sağlar ve bunlar val
, var
ve def
anahtar sözcükleri aracılığıyla kolayca belirtilir. Java'da, gerekli ayarlayıcıları ve alıcıları açıkça uygulamamız gerekecek ve bu tür manuel olarak yazılmış ortak kod çok daha az anlamlı ve daha fazla hataya açık olacaktır.
Tembel Değerler
Tembel bir değer bildirilirken daha karmaşık kod üretilir. Aşağıdaki alanı önceden tanımlanmış sınıfa eklediğimizi varsayalım:
lazy val m4 = m
javap -p -v Person.class
çalıştırmak şimdi aşağıdakileri ortaya çıkaracaktır:
Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn
Bu durumda m4
alanının değeri ihtiyaç duyulana kadar hesaplanmaz. Tembel değeri hesaplamak için özel, özel yöntem m4$lzycompute()
ve durumunu izlemek için bitmap$0
alanı üretilir. m4()
yöntemi, bu alanın değerinin 0 olup olmadığını kontrol ederek m4
henüz başlatılmadığını belirtir, bu durumda m4$lzycompute()
çağrılır, m4
doldurulur ve değeri döndürülür. Bu özel yöntem ayrıca bitmap$0
değerini 1'e ayarlar, böylece m4()
bir sonraki çağrıldığında başlatma yöntemini çağırmayı atlar ve bunun yerine basitçe m4
değerini döndürür.
Scala'nın burada ürettiği bayt kodu, hem iş parçacığı için güvenli hem de etkili olacak şekilde tasarlanmıştır. İş parçacığı güvenliği için, tembel hesaplama yöntemi, monitorenter
/ monitorexit
komut çiftini kullanır. Bu senkronizasyonun performans yükü yalnızca tembel değerin ilk okumasında gerçekleştiğinden, yöntem etkili kalır.
Tembel değerin durumunu belirtmek için sadece bir bit gereklidir. Yani 32'den fazla tembel değer yoksa, tek bir int alanı hepsini izleyebilir. Kaynak kodda birden fazla tembel değer tanımlanırsa, yukarıdaki bayt kodu, bu amaç için bir bit maskesi uygulamak için derleyici tarafından değiştirilecektir.
Yine Scala, Java'da açıkça uygulanması gereken belirli bir davranış türünden kolayca yararlanmamızı sağlayarak, çaba tasarrufu sağlar ve yazım hatası riskini azaltır.
Değer olarak işlev
Şimdi aşağıdaki Scala kaynak koduna bir göz atalım:
class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output("Hello"); } }
Printer
sınıfında, String => Unit
türünde bir output
alanı vardır: String
alan ve Unit
türünde bir nesne döndüren bir işlev (Java'daki void
benzer). Ana yöntemde, bu nesnelerden birini oluşturuyoruz ve bu alanı belirli bir dizeyi yazdıran anonim bir işlev olarak atadık.
Bu kodun derlenmesi dört sınıf dosyası oluşturur:
Hello.class
, ana yöntemi yalnızca Hello$.main()
öğesini çağıran bir sarmalayıcı sınıftır:
public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return
Gizli Hello$.class
, ana yöntemin gerçek uygulamasını içerir. Bayt koduna bir göz atmak için, özel karakter olarak yorumlanmasını önlemek için komut kabuğunuzun kurallarına göre $
doğru bir şekilde çıktığınızdan emin olun:
public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1."<init>":()V 11: invokespecial #22 // Method Printer."<init>":(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string "Hello" 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return
Yöntem bir Printer
oluşturur. Ardından, anonim işlevimizi s => println(s)
içeren bir Hello$$anonfun$1
oluşturur. Printer
, output
alanı olarak bu nesneyle başlatılır. Bu alan daha sonra yığına yüklenir ve "Hello"
işleneni ile yürütülür.
Aşağıdaki anonim işlev sınıfına bir göz atalım, Hello$$anonfun$1.class
. apply()
Scala'nın Function1
( AbstractFunction1
olarak) genişlettiğini görebiliriz. Aslında, biri diğerini saran, birlikte tür denetimi yapan (bu durumda girdinin bir String
olduğu) ve anonim işlevi yürüten (girdiyi println()
ile yazdıran) iki application( apply()
yöntemi oluşturur.
public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1<java.lang.String, scala.runtime.BoxedUnit> implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn
Yukarıdaki Hello$.main()
yöntemine geri dönüp baktığımızda, konum 21'de anonim işlevin yürütülmesinin apply( Object )
yöntemine yapılan bir çağrı tarafından tetiklendiğini görebiliriz.
Son olarak, eksiksiz olması için Printer.class
bayt koduna bakalım:
public class Printer // ... // field private final scala.Function1<java.lang.String, scala.runtime.BoxedUnit> output; // field getter public scala.Function1<java.lang.String, scala.runtime.BoxedUnit> output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1<java.lang.String, scala.runtime.BoxedUnit>); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object."<init>":()V 9: return
Burada anonim fonksiyonun herhangi bir val
değişkeni gibi ele alındığını görebiliriz. output
sınıf alanında saklanır ve alıcı output()
oluşturulur. Tek fark, bu değişkenin şimdi Scala arabirimi scala.Function1
( AbstractFunction1
yaptığı) uygulaması gerektiğidir.
Bu nedenle, bu zarif Scala özelliğinin maliyeti, değer olarak kullanılabilecek tek bir anonim işlevi temsil etmek ve yürütmek için oluşturulan temel yardımcı program sınıflarıdır. Özel uygulamanız için ne anlama geldiğini anlamak için bu tür işlevlerin sayısını ve VM uygulamanızın ayrıntılarını dikkate almalısınız.
Skala Özellikleri
Scala'nın özellikleri Java'daki arayüzlere benzer. Aşağıdaki özellik, iki yöntem imzasını tanımlar ve ikincisinin varsayılan uygulamasını sağlar. Bakalım nasıl uygulanıyor:
trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }
İki varlık üretilir: Similarity.class
, her iki yöntemi de bildiren arabirim ve varsayılan uygulamayı sağlayan Similarity$class.class
sentetik sınıfı:
public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); }
public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return
Bir sınıf bu özelliği uyguladığında ve isNotSimilar
yöntemini çağırdığında, Scala derleyicisi, eşlik eden sınıf tarafından sağlanan statik yöntemi çağırmak için invokestatic
bayt kodu talimatını oluşturur.
Özelliklerden karmaşık polimorfizm ve kalıtım yapıları oluşturulabilir. Örneğin, uygulayan sınıfın yanı sıra birden çok özelliğin tümü, denetimi bir sonraki özelliğe geçirmek için super.methodName()
çağırarak aynı imzaya sahip bir yöntemi geçersiz kılabilir. Scala derleyicisi bu tür çağrılarla karşılaştığında:

- Bu çağrı tarafından tam olarak hangi özelliğin varsayıldığını belirler.
- Özellik için tanımlanan statik yöntem bayt kodunu sağlayan eşlik eden sınıfın adını belirler.
- Gerekli
invokestatic
yönergesini üretir.
Böylece, güçlü nitelik kavramının, önemli bir ek yüke yol açmayacak şekilde JVM düzeyinde uygulandığını görebiliriz ve Scala programcıları, çalışma zamanında çok pahalı olacağından endişe etmeden bu özelliğin keyfini çıkarabilir.
Singleton'lar
Scala, object
anahtar sözcüğünü kullanarak tekil sınıfların açık tanımını sağlar. Aşağıdaki singleton sınıfını ele alalım:
object Config { val home_dir = "/home/user" }
Derleyici iki sınıf dosyası üretir:
Config.class
oldukça basit bir tanesidir:
public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn
Bu yalnızca, singleton'ın işlevselliğini gömen sentetik Config$
sınıfı için bir dekoratördür. Bu sınıfı javap -p -c
ile incelemek aşağıdaki bayt kodunu üretir:
public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method "<init>":()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object."<init>":()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value "/home/user" and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return
Aşağıdakilerden oluşur:
- Diğer nesnelerin bu tekil nesneye eriştiği sentetik değişken
MODULE$
. -
MODULE$
'ı başlatmak ve alanlarını varsayılan değerlere ayarlamak için kullanılan statik başlatıcı{}
(<clinit>
olarak da bilinir, sınıf başlatıcı) veConfig$
özel yöntemi - Statik alan
home_dir
için bir alıcı yöntemi. Bu durumda, sadece bir yöntemdir. Singleton'da daha fazla alan varsa, daha fazla alıcıya ve değiştirilebilir alanlar için ayarlayıcılara sahip olacaktır.
Singleton, popüler ve kullanışlı bir tasarım desenidir. Java dili, onu dil düzeyinde belirtmek için doğrudan bir yol sağlamaz; bunun yerine, onu Java kaynağında uygulamak geliştiricinin sorumluluğundadır. Öte yandan Scala, object
anahtar sözcüğünü kullanarak bir singleton'u açıkça bildirmek için açık ve kullanışlı bir yol sağlar. Kaputun altına baktığımızda da görebileceğimiz gibi, ekonomik ve doğal bir şekilde uygulanmaktadır.
Çözüm
Şimdi Scala'nın çeşitli örtük ve işlevsel programlama özelliklerini karmaşık Java bayt kodu yapılarında nasıl derlediğini gördük. Scala'nın iç işleyişine bu kısa bakışla, Scala'nın gücünü daha derinden takdir ederek bu güçlü dilden en iyi şekilde yararlanmamıza yardımcı olabiliriz.
Artık dili kendimiz keşfetmek için araçlara da sahibiz. Scala sözdiziminin vaka sınıfları, currying ve liste anlamaları gibi bu makalede ele alınmayan birçok yararlı özelliği vardır. Scala'nın bu yapıları uygulamasını kendiniz araştırmanızı tavsiye ederim, böylece nasıl bir sonraki seviye Scala ninjası olunacağını öğrenebilirsiniz!
Java Sanal Makinesi: Bir Hızlandırılmış Kurs
Java derleyicisi gibi, Scala derleyicisi de kaynak kodunu Java Sanal Makinesi tarafından yürütülecek Java bayt kodunu içeren .class
dosyalarına dönüştürür. Başlık altında iki dilin nasıl farklılaştığını anlamak için her ikisinin de hedeflediği sistemi anlamak gerekir. Burada, Java Sanal Makinesi mimarisinin bazı ana unsurlarına, sınıf dosyası yapısına ve montajcı temellerine kısa bir genel bakış sunuyoruz.
Bu kılavuzun, yukarıdaki makaleyle birlikte aşağıdakileri etkinleştirmek için yalnızca minimumu kapsayacağını unutmayın. JVM'nin birçok ana bileşeni burada tartışılmasa da, tüm ayrıntılar burada, resmi belgelerde bulunabilir.
javap
ile Sınıf Dosyalarının Derlenmesi
Sabit Havuz
Alan ve Yöntem Tabloları
JVM Bayt Kodu
Yöntem Çağrıları ve Çağrı Yığını
İşlenen Yığınında Yürütme
Yerel Değişkenler
Başa dön
javap
ile Sınıf Dosyalarının Derlenmesi
Java, .class
dosyalarını insan tarafından okunabilir bir forma dönüştüren javap
komut satırı yardımcı programıyla birlikte gelir. Scala ve Java sınıf dosyalarının her ikisi de aynı JVM'yi hedeflediğinden, javap
, Scala tarafından derlenen sınıf dosyalarını incelemek için kullanılabilir.
Aşağıdaki kaynak kodunu derleyelim:
// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( "Calculating perimeter..." ) return sideLength * this.numSides } }
Bunu scalac RegularPolygon.scala
ile derlemek RegularPolygon.class
üretecektir. Daha sonra javap RegularPolygon.class
çalıştırırsak, aşağıdakileri göreceğiz:
$ javap RegularPolygon.class Compiled from "RegularPolygon.scala" public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
Bu, sınıfın genel üyelerinin adlarını ve türlerini gösteren sınıf dosyasının çok basit bir dökümüdür. -p
seçeneğinin eklenmesi özel üyeleri içerecektir:
$ javap -p RegularPolygon.class Compiled from "RegularPolygon.scala" public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }
Bu hala çok fazla bilgi değil. Java bayt kodunda yöntemlerin nasıl uygulandığını görmek için -c
seçeneğini ekleyelim:
$ javap -p -c RegularPolygon.class Compiled from "RegularPolygon.scala" public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object."<init>":()V 9: return }
Bu biraz daha ilginç. Ancak, tüm hikayeyi gerçekten anlamak için, javap -p -v RegularPolygon.class
olduğu gibi -v
veya -verbose
seçeneğini kullanmalıyız:
Burada nihayet sınıf dosyasında gerçekte ne olduğunu görüyoruz. Bütün bunlar ne anlama geliyor? En önemli kısımlardan bazılarına bir göz atalım.
Sabit Havuz
C++ uygulamaları için geliştirme döngüsü, derleme ve bağlantı aşamalarını içerir. Java için geliştirme döngüsü, açık bir bağlantı aşamasını atlar çünkü bağlantı çalışma zamanında gerçekleşir. Sınıf dosyası bu çalışma zamanı bağlantısını desteklemelidir. Bu, kaynak kodu herhangi bir alana veya yönteme atıfta bulunduğunda, elde edilen bayt kodunun ilgili referansları sembolik biçimde tutması gerektiği, uygulama belleğe yüklendikten ve gerçek adreslerin çalışma zamanı bağlayıcısı tarafından çözümlenebildiğinde başvurudan kaldırılmaya hazır olması gerektiği anlamına gelir. Bu sembolik form şunları içermelidir:
- sınıf adı
- alan veya yöntem adı
- tür bilgisi
Sınıf dosya formatı belirtimi, dosyanın sabit havuz adı verilen bir bölümünü, bağlayıcı tarafından ihtiyaç duyulan tüm referansların bir tablosunu içerir. Farklı türlerde girdiler içerir.
// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...
Her girişin ilk baytı, giriş türünü gösteren sayısal bir etikettir. Kalan baytlar, girdinin değeri hakkında bilgi sağlar. Bayt sayısı ve yorumlanması için kurallar, ilk bayt tarafından belirtilen türe bağlıdır.
Örneğin, sabit bir 365
tamsayısını kullanan bir Java sınıfı, aşağıdaki bayt koduyla sabit bir havuz girişine sahip olabilir:
x03 00 00 01 6D
İlk bayt x03
, CONSTANT_Integer
giriş türünü tanımlar. Bu, bağlayıcıya sonraki dört baytın tamsayının değerini içerdiğini bildirir. (Onaltılı olarak x16D
olduğunu unutmayın). Bu, sabit havuzdaki 14. girişse, javap -v
onu şöyle yapacaktır:
#14 = Integer 365
Birçok sabit tür, sabit havuzun başka yerlerinde daha "ilkel" sabit türlere yapılan referanslardan oluşur. Örneğin, örnek kodumuz şu ifadeyi içerir:
println( "Calculating perimeter..." )
Bir dize sabitinin kullanılması, sabit havuzda iki giriş üretecektir: CONSTANT_Utf8
türünde bir giriş ve CONSTANT_String
türünde başka bir giriş. Constant_UTF8
türündeki giriş, dize değerinin gerçek UTF8 temsilini içerir. CONSTANT_String
türündeki giriş, CONSTANT_Utf8
girişine bir başvuru içerir:
#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...
Bu tür bir karmaşıklık gereklidir, çünkü Utf8
girdilere başvuran ve String
türünde olmayan başka sabit havuz girdileri vardır. Örneğin, bir sınıf özniteliğine yapılan herhangi bir başvuru, sınıf adına, öznitelik adına ve öznitelik türüne bir dizi başvuru içeren bir CONSTANT_Fieldref
türü üretecektir:
#1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I
Sabit havuz hakkında daha fazla ayrıntı için JVM belgelerine bakın.
Alan ve Yöntem Tabloları
Bir sınıf dosyası, sınıfta tanımlanan her bir alan (yani nitelik) hakkında bilgi içeren bir alan tablosu içerir. Bunlar, alanın adını ve türünü ve ayrıca erişim kontrol bayraklarını ve diğer ilgili verileri tanımlayan sabit havuz girişlerine yapılan referanslardır.
Sınıf dosyasında benzer bir yöntem tablosu mevcuttur. Ancak, soyut olmayan her yöntem için ad ve tür bilgilerine ek olarak, JVM tarafından yürütülecek gerçek bayt kodu talimatlarının yanı sıra yöntemin yığın çerçevesi tarafından kullanılan veri yapılarını içerir.
JVM Bayt Kodu
JVM, derlenmiş kodu yürütmek için kendi dahili komut setini kullanır. javap
-c
seçeneğiyle çalıştırmak, çıktıda derlenmiş yöntem uygulamalarını içerir. RegularPolygon.class
dosyamızı bu şekilde incelersek, getPerimeter()
yöntemimiz için aşağıdaki çıktıyı göreceğiz:
public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn
Gerçek bayt kodu şöyle görünebilir:
xB2 00 17 x12 19 xB6 00 1D x27 ...
Her talimat, JVM talimatını tanımlayan bir baytlık bir işlem kodu ile başlar, ardından spesifik talimatın formatına bağlı olarak üzerinde çalıştırılacak sıfır veya daha fazla talimat işleneni gelir. Bunlar tipik olarak ya sabit değerlerdir ya da sabit havuza yapılan referanslardır. javap
, bayt kodunu yararlı bir şekilde aşağıdakileri görüntüleyen insan tarafından okunabilir bir forma çevirir:
- Kodun içindeki talimatın ilk baytının ofseti veya konumu.
- Talimatın insan tarafından okunabilir adı veya anımsatıcısı .
- Varsa işlenenin değeri.
#23
gibi bir sayı işaretiyle görüntülenen işlenenler, sabit havuzdaki girişlere başvurulardır. Gördüğümüz gibi, javap
çıktıda havuzdan tam olarak neyin referans alındığını belirleyen faydalı yorumlar da üretir.
Aşağıda ortak talimatlardan birkaçını tartışacağız. JVM komut setinin tamamı hakkında ayrıntılı bilgi için belgelere bakın.
Yöntem Çağrıları ve Çağrı Yığını
Her yöntem çağrısı, yerel olarak bildirilen değişkenler veya yönteme geçirilen bağımsız değişkenler gibi şeyleri içeren kendi bağlamıyla çalışabilmelidir. Bunlar birlikte bir yığın çerçevesi oluşturur. Bir yöntemin çağrılması üzerine, yeni bir çerçeve oluşturulur ve çağrı yığınının üstüne yerleştirilir. Yöntem döndüğünde, geçerli çerçeve çağrı yığınından çıkarılır ve atılır ve yöntem çağrılmadan önce yürürlükte olan çerçeve geri yüklenir.
Bir yığın çerçevesi birkaç farklı yapı içerir. İki önemli olan, işlenen yığını ve daha sonra tartışılacak olan yerel değişken tablosudur .
İşlenen Yığınında Yürütme
Birçok JVM talimatı, çerçevelerinin işlenen yığını üzerinde çalışır. Bu komutlar, bayt kodunda açıkça sabit bir işlenen belirtmek yerine, işlenen yığınının üstündeki değerleri girdi olarak alır. Tipik olarak, bu değerler işlem sırasında yığından kaldırılır. Bazı talimatlar, yığının üstüne yeni değerler de yerleştirir. Bu şekilde, karmaşık işlemleri gerçekleştirmek için JVM talimatları birleştirilebilir. Örneğin, ifade:
sideLength * this.numSides
getPerimeter()
yöntemimizde aşağıdaki şekilde derlenir:
8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul
- İlk talimat,
dload_1
, nesne referansını yerel değişken tablosunun (aşağıda tartışılacak) yuva 1'den işlenen yığınına iter. Bu durumda,sideLength
yöntem argümanı budur. - Sonraki talimat,aload_0
, yerel değişken tablosunun 0 yuvasındaki nesne referansını işlenen yığınına iter. Pratikte, bu hemen hemen her zamanthis
, mevcut sınıfa referanstır. - Bu,
numSides()
örnek yöntemini yürüten bir sonraki çağrı olaninvokevirtual #31
için yığını ayarlar.invokevirtual
, yöntemi hangi sınıftan çağırması gerektiğini belirlemek için üst işleneni (this
öğesine referans) yığından çıkarır. Yöntem geri döndüğünde, sonucu yığına itilir. - Bu durumda, döndürülen değer (
numSides
) tamsayı biçimindedir. Başka bir çift değerle çarpmak için çift kayan nokta biçimine dönüştürülmesi gerekir.i2d
komutu, tamsayı değerini yığından çıkarır, kayan nokta biçimine dönüştürür ve yığına geri iter. - Bu noktada yığın, üstte
this.numSides
kayan nokta sonucunu ve ardından yönteme iletilensideLength
bağımsız değişkeninin değerini içerir.dmul
bu ilk iki değeri yığından çıkarır, üzerlerinde kayan nokta çarpması gerçekleştirir ve sonucu yığına iletir.
Bir yöntem çağrıldığında, yığın çerçevesinin bir parçası olarak işlemlerin gerçekleştirileceği yeni bir işlenen yığını oluşturulur. We must be careful with terminology here: the word “stack” may refer to the call stack , the stack of frames providing context for method execution, or to a particular frame's operand stack , upon which JVM instructions operate.
Local Variables
Each stack frame keeps a table of local variables . This typically includes a reference to this
object, any arguments that were passed when the method was called, and any local variables declared within the method body. Running javap
with the -v
option will include information about how each method's stack frame should be set up, including its local variable table:
public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D
In this example, there are two local variables. The variable in slot 0 is named this
, with the type RegularPolygon
. This is the reference to the method's own class. The variable in slot 1 is named sideLength
, with the type D
(indicating a double). This is the argument that is passed to our getPerimeter()
method.
Instructions such as iload_1
, fstore_2
, or aload [n]
, transfer different types of local variables between the operand stack and the local variable table. Since the first item in the table is usually the reference to this
, the instruction aload_0
is commonly seen in any method that operates on its own class.
This concludes our walkthrough of JVM basics.