: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

Зацапайте ръцете си с байт кода на Scala JVM



Езикът Scala продължава да набира популярност през последните няколко години, благодарение на отличната си комбинация от функционални и обектно-ориентирани принципи за разработване на софтуер и нейното внедряване върху доказаната Java Virtual Machine (JVM).

Макар че Стълба компилира се в байт код на Java, той е предназначен да подобри много от възприетите недостатъци на езика Java. Предлагайки пълна функционална поддръжка за програмиране, основният синтаксис на Scala съдържа много неявни структури, които трябва да бъдат изградени изрично от програмистите на Java, някои от които включват значителна сложност.



Създаването на език, който се компилира в байт код на Java, изисква задълбочено разбиране на вътрешната работа на Java Virtual Machine. За да оценим постигнатото от разработчиците на Scala, е необходимо да отидем под капака и да проучим как изходният код на Scala се интерпретира от компилатора, за да се получи ефективен и ефективен JVM байт код.



Нека да разгледаме как се изпълняват всички тези неща.



Предпоставки

Четенето на тази статия изисква известно разбиране на байт кода на Java Virtual Machine. Пълна спецификация на виртуалната машина може да бъде получена от Официалната документация на Oracle . Четенето на цялата спецификация не е от решаващо значение за разбирането на тази статия, така че за кратко въведение в основите съм подготвил кратко ръководство в долната част на статията.

Щракнете тук, за да прочетете сривен курс за основите на JVM.

Необходима е помощна програма за разглобяване на байт кода на Java, за да се възпроизведат примерите, предоставени по-долу, и да се продължи с допълнително проучване. Java Development Kit предоставя своя собствена помощна програма за команден ред, javap, която ще използваме тук. Бърза демонстрация на това как javap работи е включен в ръководството отдолу .



И разбира се, работеща инсталация на компилатора Scala е необходима за читателите, които искат да следват заедно с примерите. Тази статия е написана с помощта на Скала 2.11.7 . Различните версии на Scala могат да генерират малко по-различен байт код.

По подразбиране гетери и сетери

Въпреки че Java конвенцията винаги осигурява методи за получаване и задаване за публични атрибути, Java програмистите са длъжни да ги напишат сами, въпреки факта, че моделът за всеки не се е променил от десетилетия. Scala, за разлика от това, предлага по подразбиране гетери и сетери.



Нека разгледаме следния пример:

class Person(val name:String) { }

Нека да разгледаме вътре в класа Person. Ако компилираме този файл с scalac, тогава стартираме $ javap -p Person.class дава ни:



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 се генерира поле и неговият метод за получаване. Полето е частно и окончателно, докато методът е публичен.

Ако заменим val с var в Person източник и прекомпилирайте, след това полето final модификаторът отпада и се добавя и методът на задаване:



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 }

Ако има val или var се дефинира вътре в тялото на класа, след което се създават съответните частни полета и методи за достъп и се инициализират по подходящ начин при създаване на екземпляр.

Имайте предвид, че такова изпълнение на ниво клас val и var полета означава, че ако някои променливи се използват на ниво клас за съхранение на междинни стойности и никога не са достъпни директно от програмиста, инициализацията на всяко такова поле ще добави един до два метода към отпечатъка на класа. Добавяне на private модификатор за такива полета не означава, че съответните аксесоари ще бъдат отпаднали. Те просто ще станат частни.



Определения на променливи и функции

Нека приемем, че имаме метод, m(), и създаваме три различни препратки в стил Scala към тази функция:

class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }

Как са всяка от тези препратки към m конструиран? Кога m да се изпълни във всеки случай? Нека да разгледаме получения байт код. Следващият изход показва резултатите от javap -v Person.class (пропускайки много излишни резултати):

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.'':()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

В константния пул виждаме, че препратката към метода m() се съхранява в индекс #30. В кода на конструктора виждаме, че този метод се извиква два пъти по време на инициализация, с инструкцията invokevirtual #30 появява се първо при изместване на байта 11, след това при изместване 19. Първото извикване е последвано от инструкцията putfield #22 което присвоява резултата от този метод на полето m1, посочено чрез индекс #22 в постоянния басейн. Второто извикване е последвано от същия модел, този път присвоявайки стойността на полето m2, индексирано на #24 в постоянния басейн.

С други думи, присвояване на метод на променлива, дефинирана с val или var само присвоява резултат на метода към тази променлива. Виждаме, че методите m1() и m2() които са създадени са просто гетери за тези променливи. В случая на var m2, ние също виждаме, че задателят m2_$eq(int) се създава, който се държи точно като всеки друг сетер, като презаписва стойността в полето.

Използването на ключовата дума обаче def дава различен резултат. Вместо да извлича стойност на полето за връщане, методът m3() включва също инструкцията invokevirtual #30. Тоест, всеки път, когато се извика този метод, той извиква m() и връща резултата от този метод.

Така че, както виждаме, Scala предоставя три начина за работа с полета на класа и те лесно се определят чрез ключовите думи val, var и def. В Java ще трябва да внедрим необходимите сетери и гетери изрично и такъв ръчно написан код на шаблон ще бъде много по-малко изразителен и по-податлив на грешки.

Мързеливи ценности

По-сложен код се получава при деклариране на мързелива стойност. Да приемем, че сме добавили следното поле към предварително дефинирания клас:

lazy val m4 = m

Изпълнява се javap -p -v Person.class сега ще разкрие следното:

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

В този случай стойността на полето m4 не се изчислява, докато не е необходимо. Специалният, частен метод m4$lzycompute() се създава за изчисляване на мързеливата стойност, а полето bitmap$0 за проследяване на състоянието му. Метод m4() проверява дали стойността на това поле е 0, което показва, че m4 все още не е инициализиран, в този случай m4$lzycompute() се извиква, попълващ m4 и връщане на стойността му. Този частен метод също така задава стойността на bitmap$0 до 1, така че следващия път m4() се нарича, той ще пропусне извикването на метода за инициализация и вместо това просто ще върне стойността на m4

Резултатите от първото извикване на Scala мързелива стойност.

Байтовият код, който Scala произвежда тук, е проектиран да бъде едновременно безопасен и ефективен. За да бъде безопасен за нишки, методът на мързеливи изчисления използва monitorenter / monitorexit чифт инструкции. Методът остава ефективен, тъй като режийните разходи за тази синхронизация се появяват само при първото четене на мързеливата стойност.

За да се посочи състоянието на мързеливата стойност е необходим само един бит. Така че, ако няма повече от 32 мързеливи стойности, едно поле int може да ги проследи всички. Ако в изходния код е дефинирана повече от една мързелива стойност, горният байт код ще бъде модифициран от компилатора, за да приложи битова маска за тази цел.

Отново Scala ни позволява лесно да се възползваме от специфичен вид поведение, което би трябвало да бъде внедрено изрично в Java, спестявайки усилия и намалявайки риска от печатни грешки.

Функция като стойност

Сега нека да разгледаме следния код на Scala:

class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }

Printer class има едно поле, output, с типа String => Unit: функция, която приема String и връща обект от тип Unit (подобно на void в Java). В основния метод създаваме един от тези обекти и задаваме това поле като анонимна функция, която отпечатва даден низ.

Компилирането на този код генерира четири класа файлове:

Изходният код се компилира в четири класа файлове.

Hello.class е клас на обвивка, чийто основен метод просто извиква Hello$.main():

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

Скритото Hello$.class съдържа реалното изпълнение на основния метод. За да разгледате байт кода му, уверете се, че сте избягали правилно $ според правилата на вашата командна обвивка, за да избегнете нейното тълкуване като специален знак:

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.'':()V 11: invokespecial #22 // Method Printer.'':(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

Методът създава Printer. След това създава Hello$$anonfun$1, която съдържа нашата анонимна функция s => println(s). Printer се инициализира с този обект като output поле. След това това поле се зарежда в стека и се изпълнява с операнда 'Hello'.

Нека да разгледаме анонимния функционален клас, Hello$$anonfun$1.class, по-долу. Виждаме, че разширява Scala’s Function1 (като AbstractFunction1) чрез прилагане на apply() метод. Всъщност той създава две apply() методи, едната обвива другата, които заедно извършват проверка на типа (в този случай, че входът е String) и изпълняват анонимната функция (отпечатване на входа с println()).

public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 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

Поглеждайки назад към Hello$.main() метод по-горе, можем да видим, че при отместване 21, изпълнението на анонимната функция се задейства от извикване на нейния apply( Object ) метод.

И накрая, за пълнота, нека разгледаме байт кода за Printer.class:

public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return

Виждаме, че анонимната функция тук се третира точно като всяка val променлива. Той се съхранява в полето на класа output и гетерът output() е създаден. Единствената разлика е, че тази променлива вече трябва да реализира интерфейса Scala scala.Function1 (което AbstractFunction1 прави).

И така, цената на тази елегантна функция Scala са основните класове помощни програми, създадени да представят и изпълнят една анонимна функция, която може да се използва като стойност. Трябва да вземете предвид броя на тези функции, както и подробности за вашата реализация на VM, за да разберете какво означава това за вашето конкретно приложение.

Преминаване под капака със Scala: Разгледайте как този мощен език е реализиран в JVM байт код. Tweet

Скала черти

Характеристиките на Scala са подобни на интерфейсите в Java. Следващата характеристика дефинира два метода и осигурява изпълнение по подразбиране на втория. Нека да видим как се прилага:

trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

Изходният код се компилира в два класа файлове.

Произвеждат се две обекти: Similarity.class, интерфейсът, деклариращ двата метода, и синтетичният клас, Similarity$class.class, осигуряващ изпълнението по подразбиране:

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

Когато клас изпълнява тази черта и извиква метода isNotSimilar, компилаторът Scala генерира инструкция за байт код invokestatic да извика статичния метод, предоставен от придружаващия клас.

Сложните полиморфизъм и наследствени структури могат да бъдат създадени от черти. Например, множество черти, както и класът на внедряване, могат всички да заменят метод с един и същ подпис, извикващ super.methodName() за да предадете контрола на следващата черта. Когато компилаторът Scala срещне такива повиквания, той:

По този начин можем да видим, че мощната концепция за чертите е реализирана на ниво JVM по начин, който не води до значителни режийни разходи, и програмистите на Scala могат да се насладят на тази функция, без да се притесняват, че тя ще бъде твърде скъпа по време на изпълнение.

Единични

Scala предоставя изричната дефиниция на единични класове, използвайки ключовата дума object Нека разгледаме следния сингъл клас:

object Config { val home_dir = '/home/user' }

Компилаторът създава два класа файлове:

Изходният код се компилира в два класа файлове.

Config.class е доста проста:

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

Това е просто декоратор за синтетичния Config$ клас, който вгражда функционалността на синглона. Изследване на този клас с javap -p -c произвежда следния байт код:

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 '':()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.'':()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

Състои се от следното:

Сингълтънът е популярен и полезен модел на дизайн. Езикът Java не осигурява директен начин за неговото определяне на езиково ниво; по-скоро отговорността на разработчика е да го внедри в Java source. Scala, от друга страна, предоставя ясен и удобен начин за деклариране на сингълтон изрично с помощта на object ключова дума. Както виждаме, гледайки под капака, той е изпълнен по достъпен и естествен начин.

Заключение

Сега видяхме как Scala компилира няколко неявни и функционални функции за програмиране в сложни Java байтови структури. С този поглед към вътрешната работа на Scala, ние можем да разберем по-задълбочено силата на Scala, помагайки ни да се възползваме максимално от този мощен език.

Вече разполагаме и с инструментите, за да изследваме самия език. Има много полезни функции на синтаксиса на Scala, които не са обхванати в тази статия, като класове на случаи, currying и разбиране на списъци. Препоръчвам ви сами да проучите изпълнението на Scala от тези структури, за да можете да научите как да бъдете нинджа от следващо ниво Scala!


Виртуалната машина Java: Crash Course

Подобно на Java компилатора, Scala компилаторът преобразува изходния код в .class файлове, съдържащи Java байт код, който да бъде изпълнен от Java Virtual Machine. За да се разбере как двата езика се различават под капака, е необходимо да се разбере системата, към която и двамата са насочени. Тук представяме кратък преглед на някои основни елементи от архитектурата на Java Virtual Machine, файловата структура на класа и основите на асемблера.

Имайте предвид, че това ръководство ще обхваща само минимума, който да позволи последващо, заедно с горната статия. Въпреки че много основни компоненти на JVM не са обсъдени тук, пълни подробности можете да намерите в официалните документи, тук .

Декомпилиране на файлове от клас с javap
Постоянен басейн
Таблици за полета и методи
JVM байт код
Метод повиквания и стек повиквания
Изпълнение на стека на операндите
Локални променливи
Върнете се в началото

Декомпилиране на файлове от клас с javap

Java се доставя с javap помощна програма за команден ред, която декомпилира .class файлове в разбираема за човека форма. Тъй като Scala и Java класните файлове и двата са насочени към една и съща JVM, javap може да се използва за изследване на файлове с класове, съставени от Scala.

Нека компилираме следния изходен код:

// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }

Компилиране на това с scalac RegularPolygon.scala ще произведе RegularPolygon.class. Ако тогава стартираме javap RegularPolygon.class ще видим следното:

$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Това е много проста разбивка на файла на класа, която просто показва имената и типовете публични членове на класа. Добавяне на -p опцията ще включва частни членове:

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

Това все още не е много информация. За да видим как методите са внедрени в байтовия код на Java, нека добавим -c опция:

$ 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.'':()V 9: return }

Това е малко по-интересно. За да разберем цялата история обаче, трябва да използваме -v или -verbose опция, както в javap -p -v RegularPolygon.class:

Пълното съдържание на файл с клас на Java.

Тук най-накрая виждаме какво всъщност е във файла на класа. Какво означава всичко това? Нека да разгледаме някои от най-важните части.

Постоянен басейн

Цикълът на разработка на приложения C ++ включва етапи на компилация и свързване. Цикълът на разработка за Java прескача явен етап на свързване, тъй като свързването се случва по време на изпълнение. Файлът на класа трябва да поддържа това свързване по време на изпълнение. Това означава, че когато изходният код се отнася до което и да е поле или метод, полученият байт код трябва да съхранява съответните препратки в символна форма, готови за дереферентиране, след като приложението се зареди в паметта и действителните адреси могат да бъдат разрешени от свързващия механизъм за изпълнение. Тази символична форма трябва да съдържа:

Спецификацията на файловия формат на класа включва раздел от файла, наречен постоянен басейн , таблица на всички референции, необходими на линкера. Той съдържа записи от различен тип.

// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

Първият байт на всеки запис е цифров маркер, указващ вида на записа. Останалите байтове предоставят информация за стойността на записа. Броят на байтовете и правилата за тяхното тълкуване зависи от вида, посочен от първия байт.

Например Java клас, който използва константно цяло число 365 може да има постоянен запис в пула със следния байт код:

x03 00 00 01 6D

Първият байт, x03, идентифицира вида на записа, CONSTANT_Integer. Това информира линкера, че следващите четири байта съдържат стойността на цялото число. (Обърнете внимание, че 365 в шестнадесетичен е x16D). Ако това е 14-и запис в константния пул, javap -v ще го направи по този начин:

#14 = Integer 365

Много константни типове са съставени от препратки към по-„примитивни“ константни типове другаде в константния пул. Например нашият примерен код съдържа изявлението:

println( 'Calculating perimeter...' )

Използването на низова константа ще доведе до два записа в пула от константи: един запис с тип CONSTANT_String , и друг запис от тип CONSTANT_Utf8. Въвеждането на тип Constant_UTF8 съдържа действителното представяне на UTF8 на низовата стойност. Въвеждането на тип CONSTANT_String съдържа препратка към CONSTANT_Utf8 запис:

#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

Такова усложнение е необходимо, тъй като има други видове постоянни записи в пула, които се отнасят до записи от тип Utf8 и които не са записи от тип String. Например всяко позоваване на атрибут на клас ще създаде CONSTANT_Fieldref тип, който съдържа серия препратки към името на класа, името на атрибута и типа на атрибута:

#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

За повече подробности относно постоянния пул вижте документацията на JVM .

Таблици за полета и методи

Файлът на класа съдържа полева маса който съдържа информация за всяко поле (т.е. атрибут), дефинирано в класа. Това са препратки към записи в константния пул, които описват името и типа на полето, както и знамена за контрол на достъпа и други съответни данни.

Подобен таблица на методите присъства във файла на класа. Въпреки това, освен информация за име и тип, за всеки абстрактен метод той съдържа действителните инструкции за байт код, които трябва да бъдат изпълнени от JVM, както и структури от данни, използвани от стека на метода, описан по-долу.

JVM байт код

JVM използва собствен вътрешен набор от инструкции за изпълнение на компилиран код. Изпълнява се javap с -c Опцията включва компилираните реализации на метода в изхода. Ако разгледаме нашите RegularPolygon.class файл по този начин, ще видим следния изход за нашите getPerimeter() метод:

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

Действителният байт код може да изглежда по следния начин:

xB2 00 17 x12 19 xB6 00 1D x27 ...

Всяка инструкция започва с еднобайтово opcode идентифициране на инструкцията на JVM, последвана от нула или повече операнди с инструкции, с които да се работи, в зависимост от формата на конкретната инструкция. Това обикновено са или константни стойности, или препратки към константния пул. javap услужливо превежда байт кода в разбираема за човека форма, показваща:

Операндите, които се показват със знак за паунд, като #23, са препратки към записи в константния пул. Както виждаме, javap също така дава полезни коментари в изхода, идентифицирайки какво точно се препраща от пула.

Ще обсъдим няколко от често срещаните инструкции по-долу. За подробна информация относно пълния набор от инструкции на JVM вижте документация .

Метод повиквания и стек повиквания

Всяко извикване на метод трябва да може да се изпълнява със собствен контекст, който включва неща като локално декларирани променливи или аргументи, предадени на метода. Заедно те съставляват a стекова рамка . При извикване на метод се създава нов кадър и се поставя върху стек повиквания . Когато методът се върне, текущият кадър се премахва от стека на повикванията и се изхвърля, а кадърът, който е бил в сила преди извикването на метода, се възстановява.

Рамката на стека включва няколко различни структури. Две важни са стек от операнди и локална променлива таблица , обсъдени по-нататък.

Стека на повикванията на JVM.

Изпълнение на стека на операндите

Много инструкции на JVM работят върху техните рамки стек от операнди . Вместо да посочват изрично постоянен операнд в байт кода, вместо това тези инструкции приемат за вход стойностите в горната част на стека на операндите. Обикновено тези стойности се премахват от стека в процеса. Някои инструкции също поставят нови стойности в горната част на стека. По този начин инструкциите на JVM могат да се комбинират за извършване на сложни операции. Например изразът:

sideLength * this.numSides

се компилира към следното в нашия getPerimeter() метод:

8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul

Инструкциите на JVM могат да работят върху стека на операндите, за да изпълняват сложни функции.

Когато се извика метод, се създава нов стек на операнд като част от неговата рамка на стека, където ще се извършват операции. Трябва да бъдем внимателни с терминологията тук: думата „стек“ може да се отнася до стек повиквания , стекът от рамки, осигуряващи контекст за изпълнение на метод, или към конкретен кадър стек от операнди , при което действат инструкциите на JVM.

Локални променливи

Всяка рамка на стека поддържа таблица на локални променливи . Това обикновено включва препратка към this обект, всички аргументи, които са били предадени при извикване на метода, и всички локални променливи, декларирани в тялото на метода. Изпълнява се javap с -v опцията ще включва информация за това как трябва да се настрои рамката на стека на всеки метод, включително неговата локална таблица на променливите:

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

В този пример има две локални променливи. Променливата в слот 0 се нарича this, с типа RegularPolygon. Това е препратката към собствения клас на метода. Променливата в слот 1 се нарича sideLength, с типа D (посочва двойно). Това е аргументът, който се предава на нашите getPerimeter() метод.

Инструкции като iload_1, fstore_2 или aload [n], прехвърлят различни видове локални променливи между стека на операндите и таблицата с локални променливи. Тъй като първият елемент в таблицата обикновено е препратката към this, инструкцията aload_0 се среща често при всеки метод, който оперира със собствен клас.

С това завършваме нашето разглеждане на основите на JVM. Щракнете тук, за да се върнете към основната статия.

; 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

Зацапайте ръцете си с байт кода на Scala JVM



Езикът Scala продължава да набира популярност през последните няколко години, благодарение на отличната си комбинация от функционални и обектно-ориентирани принципи за разработване на софтуер и нейното внедряване върху доказаната Java Virtual Machine (JVM).

Макар че Стълба компилира се в байт код на Java, той е предназначен да подобри много от възприетите недостатъци на езика Java. Предлагайки пълна функционална поддръжка за програмиране, основният синтаксис на Scala съдържа много неявни структури, които трябва да бъдат изградени изрично от програмистите на Java, някои от които включват значителна сложност.



Създаването на език, който се компилира в байт код на Java, изисква задълбочено разбиране на вътрешната работа на Java Virtual Machine. За да оценим постигнатото от разработчиците на Scala, е необходимо да отидем под капака и да проучим как изходният код на Scala се интерпретира от компилатора, за да се получи ефективен и ефективен JVM байт код.



Нека да разгледаме как се изпълняват всички тези неща.



Предпоставки

Четенето на тази статия изисква известно разбиране на байт кода на Java Virtual Machine. Пълна спецификация на виртуалната машина може да бъде получена от Официалната документация на Oracle . Четенето на цялата спецификация не е от решаващо значение за разбирането на тази статия, така че за кратко въведение в основите съм подготвил кратко ръководство в долната част на статията.

Щракнете тук, за да прочетете сривен курс за основите на JVM.

Необходима е помощна програма за разглобяване на байт кода на Java, за да се възпроизведат примерите, предоставени по-долу, и да се продължи с допълнително проучване. Java Development Kit предоставя своя собствена помощна програма за команден ред, javap, която ще използваме тук. Бърза демонстрация на това как javap работи е включен в ръководството отдолу .



И разбира се, работеща инсталация на компилатора Scala е необходима за читателите, които искат да следват заедно с примерите. Тази статия е написана с помощта на Скала 2.11.7 . Различните версии на Scala могат да генерират малко по-различен байт код.

По подразбиране гетери и сетери

Въпреки че Java конвенцията винаги осигурява методи за получаване и задаване за публични атрибути, Java програмистите са длъжни да ги напишат сами, въпреки факта, че моделът за всеки не се е променил от десетилетия. Scala, за разлика от това, предлага по подразбиране гетери и сетери.



Нека разгледаме следния пример:

class Person(val name:String) { }

Нека да разгледаме вътре в класа Person. Ако компилираме този файл с scalac, тогава стартираме $ javap -p Person.class дава ни:



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 се генерира поле и неговият метод за получаване. Полето е частно и окончателно, докато методът е публичен.

Ако заменим val с var в Person източник и прекомпилирайте, след това полето final модификаторът отпада и се добавя и методът на задаване:



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 }

Ако има val или var се дефинира вътре в тялото на класа, след което се създават съответните частни полета и методи за достъп и се инициализират по подходящ начин при създаване на екземпляр.

Имайте предвид, че такова изпълнение на ниво клас val и var полета означава, че ако някои променливи се използват на ниво клас за съхранение на междинни стойности и никога не са достъпни директно от програмиста, инициализацията на всяко такова поле ще добави един до два метода към отпечатъка на класа. Добавяне на private модификатор за такива полета не означава, че съответните аксесоари ще бъдат отпаднали. Те просто ще станат частни.



Определения на променливи и функции

Нека приемем, че имаме метод, m(), и създаваме три различни препратки в стил Scala към тази функция:

class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }

Как са всяка от тези препратки към m конструиран? Кога m да се изпълни във всеки случай? Нека да разгледаме получения байт код. Следващият изход показва резултатите от javap -v Person.class (пропускайки много излишни резултати):

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.'':()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

В константния пул виждаме, че препратката към метода m() се съхранява в индекс #30. В кода на конструктора виждаме, че този метод се извиква два пъти по време на инициализация, с инструкцията invokevirtual #30 появява се първо при изместване на байта 11, след това при изместване 19. Първото извикване е последвано от инструкцията putfield #22 което присвоява резултата от този метод на полето m1, посочено чрез индекс #22 в постоянния басейн. Второто извикване е последвано от същия модел, този път присвоявайки стойността на полето m2, индексирано на #24 в постоянния басейн.

С други думи, присвояване на метод на променлива, дефинирана с val или var само присвоява резултат на метода към тази променлива. Виждаме, че методите m1() и m2() които са създадени са просто гетери за тези променливи. В случая на var m2, ние също виждаме, че задателят m2_$eq(int) се създава, който се държи точно като всеки друг сетер, като презаписва стойността в полето.

Използването на ключовата дума обаче def дава различен резултат. Вместо да извлича стойност на полето за връщане, методът m3() включва също инструкцията invokevirtual #30. Тоест, всеки път, когато се извика този метод, той извиква m() и връща резултата от този метод.

Така че, както виждаме, Scala предоставя три начина за работа с полета на класа и те лесно се определят чрез ключовите думи val, var и def. В Java ще трябва да внедрим необходимите сетери и гетери изрично и такъв ръчно написан код на шаблон ще бъде много по-малко изразителен и по-податлив на грешки.

Мързеливи ценности

По-сложен код се получава при деклариране на мързелива стойност. Да приемем, че сме добавили следното поле към предварително дефинирания клас:

lazy val m4 = m

Изпълнява се javap -p -v Person.class сега ще разкрие следното:

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

В този случай стойността на полето m4 не се изчислява, докато не е необходимо. Специалният, частен метод m4$lzycompute() се създава за изчисляване на мързеливата стойност, а полето bitmap$0 за проследяване на състоянието му. Метод m4() проверява дали стойността на това поле е 0, което показва, че m4 все още не е инициализиран, в този случай m4$lzycompute() се извиква, попълващ m4 и връщане на стойността му. Този частен метод също така задава стойността на bitmap$0 до 1, така че следващия път m4() се нарича, той ще пропусне извикването на метода за инициализация и вместо това просто ще върне стойността на m4

Резултатите от първото извикване на Scala мързелива стойност.

Байтовият код, който Scala произвежда тук, е проектиран да бъде едновременно безопасен и ефективен. За да бъде безопасен за нишки, методът на мързеливи изчисления използва monitorenter / monitorexit чифт инструкции. Методът остава ефективен, тъй като режийните разходи за тази синхронизация се появяват само при първото четене на мързеливата стойност.

За да се посочи състоянието на мързеливата стойност е необходим само един бит. Така че, ако няма повече от 32 мързеливи стойности, едно поле int може да ги проследи всички. Ако в изходния код е дефинирана повече от една мързелива стойност, горният байт код ще бъде модифициран от компилатора, за да приложи битова маска за тази цел.

Отново Scala ни позволява лесно да се възползваме от специфичен вид поведение, което би трябвало да бъде внедрено изрично в Java, спестявайки усилия и намалявайки риска от печатни грешки.

Функция като стойност

Сега нека да разгледаме следния код на Scala:

class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }

Printer class има едно поле, output, с типа String => Unit: функция, която приема String и връща обект от тип Unit (подобно на void в Java). В основния метод създаваме един от тези обекти и задаваме това поле като анонимна функция, която отпечатва даден низ.

Компилирането на този код генерира четири класа файлове:

Изходният код се компилира в четири класа файлове.

Hello.class е клас на обвивка, чийто основен метод просто извиква Hello$.main():

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

Скритото Hello$.class съдържа реалното изпълнение на основния метод. За да разгледате байт кода му, уверете се, че сте избягали правилно $ според правилата на вашата командна обвивка, за да избегнете нейното тълкуване като специален знак:

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.'':()V 11: invokespecial #22 // Method Printer.'':(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

Методът създава Printer. След това създава Hello$$anonfun$1, която съдържа нашата анонимна функция s => println(s). Printer се инициализира с този обект като output поле. След това това поле се зарежда в стека и се изпълнява с операнда 'Hello'.

Нека да разгледаме анонимния функционален клас, Hello$$anonfun$1.class, по-долу. Виждаме, че разширява Scala’s Function1 (като AbstractFunction1) чрез прилагане на apply() метод. Всъщност той създава две apply() методи, едната обвива другата, които заедно извършват проверка на типа (в този случай, че входът е String) и изпълняват анонимната функция (отпечатване на входа с println()).

public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 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

Поглеждайки назад към Hello$.main() метод по-горе, можем да видим, че при отместване 21, изпълнението на анонимната функция се задейства от извикване на нейния apply( Object ) метод.

И накрая, за пълнота, нека разгледаме байт кода за Printer.class:

public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return

Виждаме, че анонимната функция тук се третира точно като всяка val променлива. Той се съхранява в полето на класа output и гетерът output() е създаден. Единствената разлика е, че тази променлива вече трябва да реализира интерфейса Scala scala.Function1 (което AbstractFunction1 прави).

И така, цената на тази елегантна функция Scala са основните класове помощни програми, създадени да представят и изпълнят една анонимна функция, която може да се използва като стойност. Трябва да вземете предвид броя на тези функции, както и подробности за вашата реализация на VM, за да разберете какво означава това за вашето конкретно приложение.

Преминаване под капака със Scala: Разгледайте как този мощен език е реализиран в JVM байт код. Tweet

Скала черти

Характеристиките на Scala са подобни на интерфейсите в Java. Следващата характеристика дефинира два метода и осигурява изпълнение по подразбиране на втория. Нека да видим как се прилага:

trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

Изходният код се компилира в два класа файлове.

Произвеждат се две обекти: Similarity.class, интерфейсът, деклариращ двата метода, и синтетичният клас, Similarity$class.class, осигуряващ изпълнението по подразбиране:

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

Когато клас изпълнява тази черта и извиква метода isNotSimilar, компилаторът Scala генерира инструкция за байт код invokestatic да извика статичния метод, предоставен от придружаващия клас.

Сложните полиморфизъм и наследствени структури могат да бъдат създадени от черти. Например, множество черти, както и класът на внедряване, могат всички да заменят метод с един и същ подпис, извикващ super.methodName() за да предадете контрола на следващата черта. Когато компилаторът Scala срещне такива повиквания, той:

По този начин можем да видим, че мощната концепция за чертите е реализирана на ниво JVM по начин, който не води до значителни режийни разходи, и програмистите на Scala могат да се насладят на тази функция, без да се притесняват, че тя ще бъде твърде скъпа по време на изпълнение.

Единични

Scala предоставя изричната дефиниция на единични класове, използвайки ключовата дума object Нека разгледаме следния сингъл клас:

object Config { val home_dir = '/home/user' }

Компилаторът създава два класа файлове:

Изходният код се компилира в два класа файлове.

Config.class е доста проста:

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

Това е просто декоратор за синтетичния Config$ клас, който вгражда функционалността на синглона. Изследване на този клас с javap -p -c произвежда следния байт код:

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 '':()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.'':()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

Състои се от следното:

Сингълтънът е популярен и полезен модел на дизайн. Езикът Java не осигурява директен начин за неговото определяне на езиково ниво; по-скоро отговорността на разработчика е да го внедри в Java source. Scala, от друга страна, предоставя ясен и удобен начин за деклариране на сингълтон изрично с помощта на object ключова дума. Както виждаме, гледайки под капака, той е изпълнен по достъпен и естествен начин.

Заключение

Сега видяхме как Scala компилира няколко неявни и функционални функции за програмиране в сложни Java байтови структури. С този поглед към вътрешната работа на Scala, ние можем да разберем по-задълбочено силата на Scala, помагайки ни да се възползваме максимално от този мощен език.

Вече разполагаме и с инструментите, за да изследваме самия език. Има много полезни функции на синтаксиса на Scala, които не са обхванати в тази статия, като класове на случаи, currying и разбиране на списъци. Препоръчвам ви сами да проучите изпълнението на Scala от тези структури, за да можете да научите как да бъдете нинджа от следващо ниво Scala!


Виртуалната машина Java: Crash Course

Подобно на Java компилатора, Scala компилаторът преобразува изходния код в .class файлове, съдържащи Java байт код, който да бъде изпълнен от Java Virtual Machine. За да се разбере как двата езика се различават под капака, е необходимо да се разбере системата, към която и двамата са насочени. Тук представяме кратък преглед на някои основни елементи от архитектурата на Java Virtual Machine, файловата структура на класа и основите на асемблера.

Имайте предвид, че това ръководство ще обхваща само минимума, който да позволи последващо, заедно с горната статия. Въпреки че много основни компоненти на JVM не са обсъдени тук, пълни подробности можете да намерите в официалните документи, тук .

Декомпилиране на файлове от клас с javap
Постоянен басейн
Таблици за полета и методи
JVM байт код
Метод повиквания и стек повиквания
Изпълнение на стека на операндите
Локални променливи
Върнете се в началото

Декомпилиране на файлове от клас с javap

Java се доставя с javap помощна програма за команден ред, която декомпилира .class файлове в разбираема за човека форма. Тъй като Scala и Java класните файлове и двата са насочени към една и съща JVM, javap може да се използва за изследване на файлове с класове, съставени от Scala.

Нека компилираме следния изходен код:

// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }

Компилиране на това с scalac RegularPolygon.scala ще произведе RegularPolygon.class. Ако тогава стартираме javap RegularPolygon.class ще видим следното:

$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Това е много проста разбивка на файла на класа, която просто показва имената и типовете публични членове на класа. Добавяне на -p опцията ще включва частни членове:

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

Това все още не е много информация. За да видим как методите са внедрени в байтовия код на Java, нека добавим -c опция:

$ 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.'':()V 9: return }

Това е малко по-интересно. За да разберем цялата история обаче, трябва да използваме -v или -verbose опция, както в javap -p -v RegularPolygon.class:

Пълното съдържание на файл с клас на Java.

Тук най-накрая виждаме какво всъщност е във файла на класа. Какво означава всичко това? Нека да разгледаме някои от най-важните части.

Постоянен басейн

Цикълът на разработка на приложения C ++ включва етапи на компилация и свързване. Цикълът на разработка за Java прескача явен етап на свързване, тъй като свързването се случва по време на изпълнение. Файлът на класа трябва да поддържа това свързване по време на изпълнение. Това означава, че когато изходният код се отнася до което и да е поле или метод, полученият байт код трябва да съхранява съответните препратки в символна форма, готови за дереферентиране, след като приложението се зареди в паметта и действителните адреси могат да бъдат разрешени от свързващия механизъм за изпълнение. Тази символична форма трябва да съдържа:

Спецификацията на файловия формат на класа включва раздел от файла, наречен постоянен басейн , таблица на всички референции, необходими на линкера. Той съдържа записи от различен тип.

// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

Първият байт на всеки запис е цифров маркер, указващ вида на записа. Останалите байтове предоставят информация за стойността на записа. Броят на байтовете и правилата за тяхното тълкуване зависи от вида, посочен от първия байт.

Например Java клас, който използва константно цяло число 365 може да има постоянен запис в пула със следния байт код:

x03 00 00 01 6D

Първият байт, x03, идентифицира вида на записа, CONSTANT_Integer. Това информира линкера, че следващите четири байта съдържат стойността на цялото число. (Обърнете внимание, че 365 в шестнадесетичен е x16D). Ако това е 14-и запис в константния пул, javap -v ще го направи по този начин:

#14 = Integer 365

Много константни типове са съставени от препратки към по-„примитивни“ константни типове другаде в константния пул. Например нашият примерен код съдържа изявлението:

println( 'Calculating perimeter...' )

Използването на низова константа ще доведе до два записа в пула от константи: един запис с тип CONSTANT_String , и друг запис от тип CONSTANT_Utf8. Въвеждането на тип Constant_UTF8 съдържа действителното представяне на UTF8 на низовата стойност. Въвеждането на тип CONSTANT_String съдържа препратка към CONSTANT_Utf8 запис:

#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

Такова усложнение е необходимо, тъй като има други видове постоянни записи в пула, които се отнасят до записи от тип Utf8 и които не са записи от тип String. Например всяко позоваване на атрибут на клас ще създаде CONSTANT_Fieldref тип, който съдържа серия препратки към името на класа, името на атрибута и типа на атрибута:

#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

За повече подробности относно постоянния пул вижте документацията на JVM .

Таблици за полета и методи

Файлът на класа съдържа полева маса който съдържа информация за всяко поле (т.е. атрибут), дефинирано в класа. Това са препратки към записи в константния пул, които описват името и типа на полето, както и знамена за контрол на достъпа и други съответни данни.

Подобен таблица на методите присъства във файла на класа. Въпреки това, освен информация за име и тип, за всеки абстрактен метод той съдържа действителните инструкции за байт код, които трябва да бъдат изпълнени от JVM, както и структури от данни, използвани от стека на метода, описан по-долу.

JVM байт код

JVM използва собствен вътрешен набор от инструкции за изпълнение на компилиран код. Изпълнява се javap с -c Опцията включва компилираните реализации на метода в изхода. Ако разгледаме нашите RegularPolygon.class файл по този начин, ще видим следния изход за нашите getPerimeter() метод:

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

Действителният байт код може да изглежда по следния начин:

xB2 00 17 x12 19 xB6 00 1D x27 ...

Всяка инструкция започва с еднобайтово opcode идентифициране на инструкцията на JVM, последвана от нула или повече операнди с инструкции, с които да се работи, в зависимост от формата на конкретната инструкция. Това обикновено са или константни стойности, или препратки към константния пул. javap услужливо превежда байт кода в разбираема за човека форма, показваща:

Операндите, които се показват със знак за паунд, като #23, са препратки към записи в константния пул. Както виждаме, javap също така дава полезни коментари в изхода, идентифицирайки какво точно се препраща от пула.

Ще обсъдим няколко от често срещаните инструкции по-долу. За подробна информация относно пълния набор от инструкции на JVM вижте документация .

Метод повиквания и стек повиквания

Всяко извикване на метод трябва да може да се изпълнява със собствен контекст, който включва неща като локално декларирани променливи или аргументи, предадени на метода. Заедно те съставляват a стекова рамка . При извикване на метод се създава нов кадър и се поставя върху стек повиквания . Когато методът се върне, текущият кадър се премахва от стека на повикванията и се изхвърля, а кадърът, който е бил в сила преди извикването на метода, се възстановява.

Рамката на стека включва няколко различни структури. Две важни са стек от операнди и локална променлива таблица , обсъдени по-нататък.

Стека на повикванията на JVM.

Изпълнение на стека на операндите

Много инструкции на JVM работят върху техните рамки стек от операнди . Вместо да посочват изрично постоянен операнд в байт кода, вместо това тези инструкции приемат за вход стойностите в горната част на стека на операндите. Обикновено тези стойности се премахват от стека в процеса. Някои инструкции също поставят нови стойности в горната част на стека. По този начин инструкциите на JVM могат да се комбинират за извършване на сложни операции. Например изразът:

sideLength * this.numSides

се компилира към следното в нашия getPerimeter() метод:

8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul

Инструкциите на JVM могат да работят върху стека на операндите, за да изпълняват сложни функции.

Когато се извика метод, се създава нов стек на операнд като част от неговата рамка на стека, където ще се извършват операции. Трябва да бъдем внимателни с терминологията тук: думата „стек“ може да се отнася до стек повиквания , стекът от рамки, осигуряващи контекст за изпълнение на метод, или към конкретен кадър стек от операнди , при което действат инструкциите на JVM.

Локални променливи

Всяка рамка на стека поддържа таблица на локални променливи . Това обикновено включва препратка към this обект, всички аргументи, които са били предадени при извикване на метода, и всички локални променливи, декларирани в тялото на метода. Изпълнява се javap с -v опцията ще включва информация за това как трябва да се настрои рамката на стека на всеки метод, включително неговата локална таблица на променливите:

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

В този пример има две локални променливи. Променливата в слот 0 се нарича this, с типа RegularPolygon. Това е препратката към собствения клас на метода. Променливата в слот 1 се нарича sideLength, с типа D (посочва двойно). Това е аргументът, който се предава на нашите getPerimeter() метод.

Инструкции като iload_1, fstore_2 или aload [n], прехвърлят различни видове локални променливи между стека на операндите и таблицата с локални променливи. Тъй като първият елемент в таблицата обикновено е препратката към this, инструкцията aload_0 се среща често при всеки метод, който оперира със собствен клас.

С това завършваме нашето разглеждане на основите на JVM. Щракнете тук, за да се върнете към основната статия.

: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

Зацапайте ръцете си с байт кода на Scala JVM



Езикът Scala продължава да набира популярност през последните няколко години, благодарение на отличната си комбинация от функционални и обектно-ориентирани принципи за разработване на софтуер и нейното внедряване върху доказаната Java Virtual Machine (JVM).

Макар че Стълба компилира се в байт код на Java, той е предназначен да подобри много от възприетите недостатъци на езика Java. Предлагайки пълна функционална поддръжка за програмиране, основният синтаксис на Scala съдържа много неявни структури, които трябва да бъдат изградени изрично от програмистите на Java, някои от които включват значителна сложност.



Създаването на език, който се компилира в байт код на Java, изисква задълбочено разбиране на вътрешната работа на Java Virtual Machine. За да оценим постигнатото от разработчиците на Scala, е необходимо да отидем под капака и да проучим как изходният код на Scala се интерпретира от компилатора, за да се получи ефективен и ефективен JVM байт код.



Нека да разгледаме как се изпълняват всички тези неща.



Предпоставки

Четенето на тази статия изисква известно разбиране на байт кода на Java Virtual Machine. Пълна спецификация на виртуалната машина може да бъде получена от Официалната документация на Oracle . Четенето на цялата спецификация не е от решаващо значение за разбирането на тази статия, така че за кратко въведение в основите съм подготвил кратко ръководство в долната част на статията.

Щракнете тук, за да прочетете сривен курс за основите на JVM.

Необходима е помощна програма за разглобяване на байт кода на Java, за да се възпроизведат примерите, предоставени по-долу, и да се продължи с допълнително проучване. Java Development Kit предоставя своя собствена помощна програма за команден ред, javap, която ще използваме тук. Бърза демонстрация на това как javap работи е включен в ръководството отдолу .



И разбира се, работеща инсталация на компилатора Scala е необходима за читателите, които искат да следват заедно с примерите. Тази статия е написана с помощта на Скала 2.11.7 . Различните версии на Scala могат да генерират малко по-различен байт код.

По подразбиране гетери и сетери

Въпреки че Java конвенцията винаги осигурява методи за получаване и задаване за публични атрибути, Java програмистите са длъжни да ги напишат сами, въпреки факта, че моделът за всеки не се е променил от десетилетия. Scala, за разлика от това, предлага по подразбиране гетери и сетери.



Нека разгледаме следния пример:

class Person(val name:String) { }

Нека да разгледаме вътре в класа Person. Ако компилираме този файл с scalac, тогава стартираме $ javap -p Person.class дава ни:



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 се генерира поле и неговият метод за получаване. Полето е частно и окончателно, докато методът е публичен.

Ако заменим val с var в Person източник и прекомпилирайте, след това полето final модификаторът отпада и се добавя и методът на задаване:



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 }

Ако има val или var се дефинира вътре в тялото на класа, след което се създават съответните частни полета и методи за достъп и се инициализират по подходящ начин при създаване на екземпляр.

Имайте предвид, че такова изпълнение на ниво клас val и var полета означава, че ако някои променливи се използват на ниво клас за съхранение на междинни стойности и никога не са достъпни директно от програмиста, инициализацията на всяко такова поле ще добави един до два метода към отпечатъка на класа. Добавяне на private модификатор за такива полета не означава, че съответните аксесоари ще бъдат отпаднали. Те просто ще станат частни.



Определения на променливи и функции

Нека приемем, че имаме метод, m(), и създаваме три различни препратки в стил Scala към тази функция:

class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }

Как са всяка от тези препратки към m конструиран? Кога m да се изпълни във всеки случай? Нека да разгледаме получения байт код. Следващият изход показва резултатите от javap -v Person.class (пропускайки много излишни резултати):

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.'':()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

В константния пул виждаме, че препратката към метода m() се съхранява в индекс #30. В кода на конструктора виждаме, че този метод се извиква два пъти по време на инициализация, с инструкцията invokevirtual #30 появява се първо при изместване на байта 11, след това при изместване 19. Първото извикване е последвано от инструкцията putfield #22 което присвоява резултата от този метод на полето m1, посочено чрез индекс #22 в постоянния басейн. Второто извикване е последвано от същия модел, този път присвоявайки стойността на полето m2, индексирано на #24 в постоянния басейн.

С други думи, присвояване на метод на променлива, дефинирана с val или var само присвоява резултат на метода към тази променлива. Виждаме, че методите m1() и m2() които са създадени са просто гетери за тези променливи. В случая на var m2, ние също виждаме, че задателят m2_$eq(int) се създава, който се държи точно като всеки друг сетер, като презаписва стойността в полето.

Използването на ключовата дума обаче def дава различен резултат. Вместо да извлича стойност на полето за връщане, методът m3() включва също инструкцията invokevirtual #30. Тоест, всеки път, когато се извика този метод, той извиква m() и връща резултата от този метод.

Така че, както виждаме, Scala предоставя три начина за работа с полета на класа и те лесно се определят чрез ключовите думи val, var и def. В Java ще трябва да внедрим необходимите сетери и гетери изрично и такъв ръчно написан код на шаблон ще бъде много по-малко изразителен и по-податлив на грешки.

Мързеливи ценности

По-сложен код се получава при деклариране на мързелива стойност. Да приемем, че сме добавили следното поле към предварително дефинирания клас:

lazy val m4 = m

Изпълнява се javap -p -v Person.class сега ще разкрие следното:

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

В този случай стойността на полето m4 не се изчислява, докато не е необходимо. Специалният, частен метод m4$lzycompute() се създава за изчисляване на мързеливата стойност, а полето bitmap$0 за проследяване на състоянието му. Метод m4() проверява дали стойността на това поле е 0, което показва, че m4 все още не е инициализиран, в този случай m4$lzycompute() се извиква, попълващ m4 и връщане на стойността му. Този частен метод също така задава стойността на bitmap$0 до 1, така че следващия път m4() се нарича, той ще пропусне извикването на метода за инициализация и вместо това просто ще върне стойността на m4

Резултатите от първото извикване на Scala мързелива стойност.

Байтовият код, който Scala произвежда тук, е проектиран да бъде едновременно безопасен и ефективен. За да бъде безопасен за нишки, методът на мързеливи изчисления използва monitorenter / monitorexit чифт инструкции. Методът остава ефективен, тъй като режийните разходи за тази синхронизация се появяват само при първото четене на мързеливата стойност.

За да се посочи състоянието на мързеливата стойност е необходим само един бит. Така че, ако няма повече от 32 мързеливи стойности, едно поле int може да ги проследи всички. Ако в изходния код е дефинирана повече от една мързелива стойност, горният байт код ще бъде модифициран от компилатора, за да приложи битова маска за тази цел.

Отново Scala ни позволява лесно да се възползваме от специфичен вид поведение, което би трябвало да бъде внедрено изрично в Java, спестявайки усилия и намалявайки риска от печатни грешки.

Функция като стойност

Сега нека да разгледаме следния код на Scala:

class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }

Printer class има едно поле, output, с типа String => Unit: функция, която приема String и връща обект от тип Unit (подобно на void в Java). В основния метод създаваме един от тези обекти и задаваме това поле като анонимна функция, която отпечатва даден низ.

Компилирането на този код генерира четири класа файлове:

Изходният код се компилира в четири класа файлове.

Hello.class е клас на обвивка, чийто основен метод просто извиква Hello$.main():

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

Скритото Hello$.class съдържа реалното изпълнение на основния метод. За да разгледате байт кода му, уверете се, че сте избягали правилно $ според правилата на вашата командна обвивка, за да избегнете нейното тълкуване като специален знак:

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.'':()V 11: invokespecial #22 // Method Printer.'':(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

Методът създава Printer. След това създава Hello$$anonfun$1, която съдържа нашата анонимна функция s => println(s). Printer се инициализира с този обект като output поле. След това това поле се зарежда в стека и се изпълнява с операнда 'Hello'.

Нека да разгледаме анонимния функционален клас, Hello$$anonfun$1.class, по-долу. Виждаме, че разширява Scala’s Function1 (като AbstractFunction1) чрез прилагане на apply() метод. Всъщност той създава две apply() методи, едната обвива другата, които заедно извършват проверка на типа (в този случай, че входът е String) и изпълняват анонимната функция (отпечатване на входа с println()).

public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 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

Поглеждайки назад към Hello$.main() метод по-горе, можем да видим, че при отместване 21, изпълнението на анонимната функция се задейства от извикване на нейния apply( Object ) метод.

И накрая, за пълнота, нека разгледаме байт кода за Printer.class:

public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return

Виждаме, че анонимната функция тук се третира точно като всяка val променлива. Той се съхранява в полето на класа output и гетерът output() е създаден. Единствената разлика е, че тази променлива вече трябва да реализира интерфейса Scala scala.Function1 (което AbstractFunction1 прави).

И така, цената на тази елегантна функция Scala са основните класове помощни програми, създадени да представят и изпълнят една анонимна функция, която може да се използва като стойност. Трябва да вземете предвид броя на тези функции, както и подробности за вашата реализация на VM, за да разберете какво означава това за вашето конкретно приложение.

Преминаване под капака със Scala: Разгледайте как този мощен език е реализиран в JVM байт код. Tweet

Скала черти

Характеристиките на Scala са подобни на интерфейсите в Java. Следващата характеристика дефинира два метода и осигурява изпълнение по подразбиране на втория. Нека да видим как се прилага:

trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

Изходният код се компилира в два класа файлове.

Произвеждат се две обекти: Similarity.class, интерфейсът, деклариращ двата метода, и синтетичният клас, Similarity$class.class, осигуряващ изпълнението по подразбиране:

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

Когато клас изпълнява тази черта и извиква метода isNotSimilar, компилаторът Scala генерира инструкция за байт код invokestatic да извика статичния метод, предоставен от придружаващия клас.

Сложните полиморфизъм и наследствени структури могат да бъдат създадени от черти. Например, множество черти, както и класът на внедряване, могат всички да заменят метод с един и същ подпис, извикващ super.methodName() за да предадете контрола на следващата черта. Когато компилаторът Scala срещне такива повиквания, той:

По този начин можем да видим, че мощната концепция за чертите е реализирана на ниво JVM по начин, който не води до значителни режийни разходи, и програмистите на Scala могат да се насладят на тази функция, без да се притесняват, че тя ще бъде твърде скъпа по време на изпълнение.

Единични

Scala предоставя изричната дефиниция на единични класове, използвайки ключовата дума object Нека разгледаме следния сингъл клас:

object Config { val home_dir = '/home/user' }

Компилаторът създава два класа файлове:

Изходният код се компилира в два класа файлове.

Config.class е доста проста:

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

Това е просто декоратор за синтетичния Config$ клас, който вгражда функционалността на синглона. Изследване на този клас с javap -p -c произвежда следния байт код:

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 '':()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.'':()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

Състои се от следното:

Сингълтънът е популярен и полезен модел на дизайн. Езикът Java не осигурява директен начин за неговото определяне на езиково ниво; по-скоро отговорността на разработчика е да го внедри в Java source. Scala, от друга страна, предоставя ясен и удобен начин за деклариране на сингълтон изрично с помощта на object ключова дума. Както виждаме, гледайки под капака, той е изпълнен по достъпен и естествен начин.

Заключение

Сега видяхме как Scala компилира няколко неявни и функционални функции за програмиране в сложни Java байтови структури. С този поглед към вътрешната работа на Scala, ние можем да разберем по-задълбочено силата на Scala, помагайки ни да се възползваме максимално от този мощен език.

Вече разполагаме и с инструментите, за да изследваме самия език. Има много полезни функции на синтаксиса на Scala, които не са обхванати в тази статия, като класове на случаи, currying и разбиране на списъци. Препоръчвам ви сами да проучите изпълнението на Scala от тези структури, за да можете да научите как да бъдете нинджа от следващо ниво Scala!


Виртуалната машина Java: Crash Course

Подобно на Java компилатора, Scala компилаторът преобразува изходния код в .class файлове, съдържащи Java байт код, който да бъде изпълнен от Java Virtual Machine. За да се разбере как двата езика се различават под капака, е необходимо да се разбере системата, към която и двамата са насочени. Тук представяме кратък преглед на някои основни елементи от архитектурата на Java Virtual Machine, файловата структура на класа и основите на асемблера.

Имайте предвид, че това ръководство ще обхваща само минимума, който да позволи последващо, заедно с горната статия. Въпреки че много основни компоненти на JVM не са обсъдени тук, пълни подробности можете да намерите в официалните документи, тук .

Декомпилиране на файлове от клас с javap
Постоянен басейн
Таблици за полета и методи
JVM байт код
Метод повиквания и стек повиквания
Изпълнение на стека на операндите
Локални променливи
Върнете се в началото

Декомпилиране на файлове от клас с javap

Java се доставя с javap помощна програма за команден ред, която декомпилира .class файлове в разбираема за човека форма. Тъй като Scala и Java класните файлове и двата са насочени към една и съща JVM, javap може да се използва за изследване на файлове с класове, съставени от Scala.

Нека компилираме следния изходен код:

// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }

Компилиране на това с scalac RegularPolygon.scala ще произведе RegularPolygon.class. Ако тогава стартираме javap RegularPolygon.class ще видим следното:

$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Това е много проста разбивка на файла на класа, която просто показва имената и типовете публични членове на класа. Добавяне на -p опцията ще включва частни членове:

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

Това все още не е много информация. За да видим как методите са внедрени в байтовия код на Java, нека добавим -c опция:

$ 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.'':()V 9: return }

Това е малко по-интересно. За да разберем цялата история обаче, трябва да използваме -v или -verbose опция, както в javap -p -v RegularPolygon.class:

Пълното съдържание на файл с клас на Java.

Тук най-накрая виждаме какво всъщност е във файла на класа. Какво означава всичко това? Нека да разгледаме някои от най-важните части.

Постоянен басейн

Цикълът на разработка на приложения C ++ включва етапи на компилация и свързване. Цикълът на разработка за Java прескача явен етап на свързване, тъй като свързването се случва по време на изпълнение. Файлът на класа трябва да поддържа това свързване по време на изпълнение. Това означава, че когато изходният код се отнася до което и да е поле или метод, полученият байт код трябва да съхранява съответните препратки в символна форма, готови за дереферентиране, след като приложението се зареди в паметта и действителните адреси могат да бъдат разрешени от свързващия механизъм за изпълнение. Тази символична форма трябва да съдържа:

Спецификацията на файловия формат на класа включва раздел от файла, наречен постоянен басейн , таблица на всички референции, необходими на линкера. Той съдържа записи от различен тип.

// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

Първият байт на всеки запис е цифров маркер, указващ вида на записа. Останалите байтове предоставят информация за стойността на записа. Броят на байтовете и правилата за тяхното тълкуване зависи от вида, посочен от първия байт.

Например Java клас, който използва константно цяло число 365 може да има постоянен запис в пула със следния байт код:

x03 00 00 01 6D

Първият байт, x03, идентифицира вида на записа, CONSTANT_Integer. Това информира линкера, че следващите четири байта съдържат стойността на цялото число. (Обърнете внимание, че 365 в шестнадесетичен е x16D). Ако това е 14-и запис в константния пул, javap -v ще го направи по този начин:

#14 = Integer 365

Много константни типове са съставени от препратки към по-„примитивни“ константни типове другаде в константния пул. Например нашият примерен код съдържа изявлението:

println( 'Calculating perimeter...' )

Използването на низова константа ще доведе до два записа в пула от константи: един запис с тип CONSTANT_String , и друг запис от тип CONSTANT_Utf8. Въвеждането на тип Constant_UTF8 съдържа действителното представяне на UTF8 на низовата стойност. Въвеждането на тип CONSTANT_String съдържа препратка към CONSTANT_Utf8 запис:

#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

Такова усложнение е необходимо, тъй като има други видове постоянни записи в пула, които се отнасят до записи от тип Utf8 и които не са записи от тип String. Например всяко позоваване на атрибут на клас ще създаде CONSTANT_Fieldref тип, който съдържа серия препратки към името на класа, името на атрибута и типа на атрибута:

#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

За повече подробности относно постоянния пул вижте документацията на JVM .

Таблици за полета и методи

Файлът на класа съдържа полева маса който съдържа информация за всяко поле (т.е. атрибут), дефинирано в класа. Това са препратки към записи в константния пул, които описват името и типа на полето, както и знамена за контрол на достъпа и други съответни данни.

Подобен таблица на методите присъства във файла на класа. Въпреки това, освен информация за име и тип, за всеки абстрактен метод той съдържа действителните инструкции за байт код, които трябва да бъдат изпълнени от JVM, както и структури от данни, използвани от стека на метода, описан по-долу.

JVM байт код

JVM използва собствен вътрешен набор от инструкции за изпълнение на компилиран код. Изпълнява се javap с -c Опцията включва компилираните реализации на метода в изхода. Ако разгледаме нашите RegularPolygon.class файл по този начин, ще видим следния изход за нашите getPerimeter() метод:

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

Действителният байт код може да изглежда по следния начин:

xB2 00 17 x12 19 xB6 00 1D x27 ...

Всяка инструкция започва с еднобайтово opcode идентифициране на инструкцията на JVM, последвана от нула или повече операнди с инструкции, с които да се работи, в зависимост от формата на конкретната инструкция. Това обикновено са или константни стойности, или препратки към константния пул. javap услужливо превежда байт кода в разбираема за човека форма, показваща:

Операндите, които се показват със знак за паунд, като #23, са препратки към записи в константния пул. Както виждаме, javap също така дава полезни коментари в изхода, идентифицирайки какво точно се препраща от пула.

Ще обсъдим няколко от често срещаните инструкции по-долу. За подробна информация относно пълния набор от инструкции на JVM вижте документация .

Метод повиквания и стек повиквания

Всяко извикване на метод трябва да може да се изпълнява със собствен контекст, който включва неща като локално декларирани променливи или аргументи, предадени на метода. Заедно те съставляват a стекова рамка . При извикване на метод се създава нов кадър и се поставя върху стек повиквания . Когато методът се върне, текущият кадър се премахва от стека на повикванията и се изхвърля, а кадърът, който е бил в сила преди извикването на метода, се възстановява.

Рамката на стека включва няколко различни структури. Две важни са стек от операнди и локална променлива таблица , обсъдени по-нататък.

Стека на повикванията на JVM.

Изпълнение на стека на операндите

Много инструкции на JVM работят върху техните рамки стек от операнди . Вместо да посочват изрично постоянен операнд в байт кода, вместо това тези инструкции приемат за вход стойностите в горната част на стека на операндите. Обикновено тези стойности се премахват от стека в процеса. Някои инструкции също поставят нови стойности в горната част на стека. По този начин инструкциите на JVM могат да се комбинират за извършване на сложни операции. Например изразът:

sideLength * this.numSides

се компилира към следното в нашия getPerimeter() метод:

8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul

Инструкциите на JVM могат да работят върху стека на операндите, за да изпълняват сложни функции.

Когато се извика метод, се създава нов стек на операнд като част от неговата рамка на стека, където ще се извършват операции. Трябва да бъдем внимателни с терминологията тук: думата „стек“ може да се отнася до стек повиквания , стекът от рамки, осигуряващи контекст за изпълнение на метод, или към конкретен кадър стек от операнди , при което действат инструкциите на JVM.

Локални променливи

Всяка рамка на стека поддържа таблица на локални променливи . Това обикновено включва препратка към this обект, всички аргументи, които са били предадени при извикване на метода, и всички локални променливи, декларирани в тялото на метода. Изпълнява се javap с -v опцията ще включва информация за това как трябва да се настрои рамката на стека на всеки метод, включително неговата локална таблица на променливите:

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

В този пример има две локални променливи. Променливата в слот 0 се нарича this, с типа RegularPolygon. Това е препратката към собствения клас на метода. Променливата в слот 1 се нарича sideLength, с типа D (посочва двойно). Това е аргументът, който се предава на нашите getPerimeter() метод.

Инструкции като iload_1, fstore_2 или aload [n], прехвърлят различни видове локални променливи между стека на операндите и таблицата с локални променливи. Тъй като първият елемент в таблицата обикновено е препратката към this, инструкцията aload_0 се среща често при всеки метод, който оперира със собствен клас.

С това завършваме нашето разглеждане на основите на JVM. Щракнете тук, за да се върнете към основната статия.

: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

Зацапайте ръцете си с байт кода на Scala JVM



Езикът Scala продължава да набира популярност през последните няколко години, благодарение на отличната си комбинация от функционални и обектно-ориентирани принципи за разработване на софтуер и нейното внедряване върху доказаната Java Virtual Machine (JVM).

Макар че Стълба компилира се в байт код на Java, той е предназначен да подобри много от възприетите недостатъци на езика Java. Предлагайки пълна функционална поддръжка за програмиране, основният синтаксис на Scala съдържа много неявни структури, които трябва да бъдат изградени изрично от програмистите на Java, някои от които включват значителна сложност.



Създаването на език, който се компилира в байт код на Java, изисква задълбочено разбиране на вътрешната работа на Java Virtual Machine. За да оценим постигнатото от разработчиците на Scala, е необходимо да отидем под капака и да проучим как изходният код на Scala се интерпретира от компилатора, за да се получи ефективен и ефективен JVM байт код.



Нека да разгледаме как се изпълняват всички тези неща.



Предпоставки

Четенето на тази статия изисква известно разбиране на байт кода на Java Virtual Machine. Пълна спецификация на виртуалната машина може да бъде получена от Официалната документация на Oracle . Четенето на цялата спецификация не е от решаващо значение за разбирането на тази статия, така че за кратко въведение в основите съм подготвил кратко ръководство в долната част на статията.

Щракнете тук, за да прочетете сривен курс за основите на JVM.

Необходима е помощна програма за разглобяване на байт кода на Java, за да се възпроизведат примерите, предоставени по-долу, и да се продължи с допълнително проучване. Java Development Kit предоставя своя собствена помощна програма за команден ред, javap, която ще използваме тук. Бърза демонстрация на това как javap работи е включен в ръководството отдолу .



И разбира се, работеща инсталация на компилатора Scala е необходима за читателите, които искат да следват заедно с примерите. Тази статия е написана с помощта на Скала 2.11.7 . Различните версии на Scala могат да генерират малко по-различен байт код.

По подразбиране гетери и сетери

Въпреки че Java конвенцията винаги осигурява методи за получаване и задаване за публични атрибути, Java програмистите са длъжни да ги напишат сами, въпреки факта, че моделът за всеки не се е променил от десетилетия. Scala, за разлика от това, предлага по подразбиране гетери и сетери.



Нека разгледаме следния пример:

class Person(val name:String) { }

Нека да разгледаме вътре в класа Person. Ако компилираме този файл с scalac, тогава стартираме $ javap -p Person.class дава ни:



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 се генерира поле и неговият метод за получаване. Полето е частно и окончателно, докато методът е публичен.

Ако заменим val с var в Person източник и прекомпилирайте, след това полето final модификаторът отпада и се добавя и методът на задаване:



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 }

Ако има val или var се дефинира вътре в тялото на класа, след което се създават съответните частни полета и методи за достъп и се инициализират по подходящ начин при създаване на екземпляр.

Имайте предвид, че такова изпълнение на ниво клас val и var полета означава, че ако някои променливи се използват на ниво клас за съхранение на междинни стойности и никога не са достъпни директно от програмиста, инициализацията на всяко такова поле ще добави един до два метода към отпечатъка на класа. Добавяне на private модификатор за такива полета не означава, че съответните аксесоари ще бъдат отпаднали. Те просто ще станат частни.



Определения на променливи и функции

Нека приемем, че имаме метод, m(), и създаваме три различни препратки в стил Scala към тази функция:

class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }

Как са всяка от тези препратки към m конструиран? Кога m да се изпълни във всеки случай? Нека да разгледаме получения байт код. Следващият изход показва резултатите от javap -v Person.class (пропускайки много излишни резултати):

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.'':()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

В константния пул виждаме, че препратката към метода m() се съхранява в индекс #30. В кода на конструктора виждаме, че този метод се извиква два пъти по време на инициализация, с инструкцията invokevirtual #30 появява се първо при изместване на байта 11, след това при изместване 19. Първото извикване е последвано от инструкцията putfield #22 което присвоява резултата от този метод на полето m1, посочено чрез индекс #22 в постоянния басейн. Второто извикване е последвано от същия модел, този път присвоявайки стойността на полето m2, индексирано на #24 в постоянния басейн.

С други думи, присвояване на метод на променлива, дефинирана с val или var само присвоява резултат на метода към тази променлива. Виждаме, че методите m1() и m2() които са създадени са просто гетери за тези променливи. В случая на var m2, ние също виждаме, че задателят m2_$eq(int) се създава, който се държи точно като всеки друг сетер, като презаписва стойността в полето.

Използването на ключовата дума обаче def дава различен резултат. Вместо да извлича стойност на полето за връщане, методът m3() включва също инструкцията invokevirtual #30. Тоест, всеки път, когато се извика този метод, той извиква m() и връща резултата от този метод.

Така че, както виждаме, Scala предоставя три начина за работа с полета на класа и те лесно се определят чрез ключовите думи val, var и def. В Java ще трябва да внедрим необходимите сетери и гетери изрично и такъв ръчно написан код на шаблон ще бъде много по-малко изразителен и по-податлив на грешки.

Мързеливи ценности

По-сложен код се получава при деклариране на мързелива стойност. Да приемем, че сме добавили следното поле към предварително дефинирания клас:

lazy val m4 = m

Изпълнява се javap -p -v Person.class сега ще разкрие следното:

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

В този случай стойността на полето m4 не се изчислява, докато не е необходимо. Специалният, частен метод m4$lzycompute() се създава за изчисляване на мързеливата стойност, а полето bitmap$0 за проследяване на състоянието му. Метод m4() проверява дали стойността на това поле е 0, което показва, че m4 все още не е инициализиран, в този случай m4$lzycompute() се извиква, попълващ m4 и връщане на стойността му. Този частен метод също така задава стойността на bitmap$0 до 1, така че следващия път m4() се нарича, той ще пропусне извикването на метода за инициализация и вместо това просто ще върне стойността на m4

Резултатите от първото извикване на Scala мързелива стойност.

Байтовият код, който Scala произвежда тук, е проектиран да бъде едновременно безопасен и ефективен. За да бъде безопасен за нишки, методът на мързеливи изчисления използва monitorenter / monitorexit чифт инструкции. Методът остава ефективен, тъй като режийните разходи за тази синхронизация се появяват само при първото четене на мързеливата стойност.

За да се посочи състоянието на мързеливата стойност е необходим само един бит. Така че, ако няма повече от 32 мързеливи стойности, едно поле int може да ги проследи всички. Ако в изходния код е дефинирана повече от една мързелива стойност, горният байт код ще бъде модифициран от компилатора, за да приложи битова маска за тази цел.

Отново Scala ни позволява лесно да се възползваме от специфичен вид поведение, което би трябвало да бъде внедрено изрично в Java, спестявайки усилия и намалявайки риска от печатни грешки.

Функция като стойност

Сега нека да разгледаме следния код на Scala:

class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }

Printer class има едно поле, output, с типа String => Unit: функция, която приема String и връща обект от тип Unit (подобно на void в Java). В основния метод създаваме един от тези обекти и задаваме това поле като анонимна функция, която отпечатва даден низ.

Компилирането на този код генерира четири класа файлове:

Изходният код се компилира в четири класа файлове.

Hello.class е клас на обвивка, чийто основен метод просто извиква Hello$.main():

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

Скритото Hello$.class съдържа реалното изпълнение на основния метод. За да разгледате байт кода му, уверете се, че сте избягали правилно $ според правилата на вашата командна обвивка, за да избегнете нейното тълкуване като специален знак:

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.'':()V 11: invokespecial #22 // Method Printer.'':(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

Методът създава Printer. След това създава Hello$$anonfun$1, която съдържа нашата анонимна функция s => println(s). Printer се инициализира с този обект като output поле. След това това поле се зарежда в стека и се изпълнява с операнда 'Hello'.

Нека да разгледаме анонимния функционален клас, Hello$$anonfun$1.class, по-долу. Виждаме, че разширява Scala’s Function1 (като AbstractFunction1) чрез прилагане на apply() метод. Всъщност той създава две apply() методи, едната обвива другата, които заедно извършват проверка на типа (в този случай, че входът е String) и изпълняват анонимната функция (отпечатване на входа с println()).

public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 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

Поглеждайки назад към Hello$.main() метод по-горе, можем да видим, че при отместване 21, изпълнението на анонимната функция се задейства от извикване на нейния apply( Object ) метод.

И накрая, за пълнота, нека разгледаме байт кода за Printer.class:

public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return

Виждаме, че анонимната функция тук се третира точно като всяка val променлива. Той се съхранява в полето на класа output и гетерът output() е създаден. Единствената разлика е, че тази променлива вече трябва да реализира интерфейса Scala scala.Function1 (което AbstractFunction1 прави).

И така, цената на тази елегантна функция Scala са основните класове помощни програми, създадени да представят и изпълнят една анонимна функция, която може да се използва като стойност. Трябва да вземете предвид броя на тези функции, както и подробности за вашата реализация на VM, за да разберете какво означава това за вашето конкретно приложение.

Преминаване под капака със Scala: Разгледайте как този мощен език е реализиран в JVM байт код. Tweet

Скала черти

Характеристиките на Scala са подобни на интерфейсите в Java. Следващата характеристика дефинира два метода и осигурява изпълнение по подразбиране на втория. Нека да видим как се прилага:

trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

Изходният код се компилира в два класа файлове.

Произвеждат се две обекти: Similarity.class, интерфейсът, деклариращ двата метода, и синтетичният клас, Similarity$class.class, осигуряващ изпълнението по подразбиране:

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

Когато клас изпълнява тази черта и извиква метода isNotSimilar, компилаторът Scala генерира инструкция за байт код invokestatic да извика статичния метод, предоставен от придружаващия клас.

Сложните полиморфизъм и наследствени структури могат да бъдат създадени от черти. Например, множество черти, както и класът на внедряване, могат всички да заменят метод с един и същ подпис, извикващ super.methodName() за да предадете контрола на следващата черта. Когато компилаторът Scala срещне такива повиквания, той:

По този начин можем да видим, че мощната концепция за чертите е реализирана на ниво JVM по начин, който не води до значителни режийни разходи, и програмистите на Scala могат да се насладят на тази функция, без да се притесняват, че тя ще бъде твърде скъпа по време на изпълнение.

Единични

Scala предоставя изричната дефиниция на единични класове, използвайки ключовата дума object Нека разгледаме следния сингъл клас:

object Config { val home_dir = '/home/user' }

Компилаторът създава два класа файлове:

Изходният код се компилира в два класа файлове.

Config.class е доста проста:

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

Това е просто декоратор за синтетичния Config$ клас, който вгражда функционалността на синглона. Изследване на този клас с javap -p -c произвежда следния байт код:

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 '':()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.'':()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

Състои се от следното:

Сингълтънът е популярен и полезен модел на дизайн. Езикът Java не осигурява директен начин за неговото определяне на езиково ниво; по-скоро отговорността на разработчика е да го внедри в Java source. Scala, от друга страна, предоставя ясен и удобен начин за деклариране на сингълтон изрично с помощта на object ключова дума. Както виждаме, гледайки под капака, той е изпълнен по достъпен и естествен начин.

Заключение

Сега видяхме как Scala компилира няколко неявни и функционални функции за програмиране в сложни Java байтови структури. С този поглед към вътрешната работа на Scala, ние можем да разберем по-задълбочено силата на Scala, помагайки ни да се възползваме максимално от този мощен език.

Вече разполагаме и с инструментите, за да изследваме самия език. Има много полезни функции на синтаксиса на Scala, които не са обхванати в тази статия, като класове на случаи, currying и разбиране на списъци. Препоръчвам ви сами да проучите изпълнението на Scala от тези структури, за да можете да научите как да бъдете нинджа от следващо ниво Scala!


Виртуалната машина Java: Crash Course

Подобно на Java компилатора, Scala компилаторът преобразува изходния код в .class файлове, съдържащи Java байт код, който да бъде изпълнен от Java Virtual Machine. За да се разбере как двата езика се различават под капака, е необходимо да се разбере системата, към която и двамата са насочени. Тук представяме кратък преглед на някои основни елементи от архитектурата на Java Virtual Machine, файловата структура на класа и основите на асемблера.

Имайте предвид, че това ръководство ще обхваща само минимума, който да позволи последващо, заедно с горната статия. Въпреки че много основни компоненти на JVM не са обсъдени тук, пълни подробности можете да намерите в официалните документи, тук .

Декомпилиране на файлове от клас с javap
Постоянен басейн
Таблици за полета и методи
JVM байт код
Метод повиквания и стек повиквания
Изпълнение на стека на операндите
Локални променливи
Върнете се в началото

Декомпилиране на файлове от клас с javap

Java се доставя с javap помощна програма за команден ред, която декомпилира .class файлове в разбираема за човека форма. Тъй като Scala и Java класните файлове и двата са насочени към една и съща JVM, javap може да се използва за изследване на файлове с класове, съставени от Scala.

Нека компилираме следния изходен код:

// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }

Компилиране на това с scalac RegularPolygon.scala ще произведе RegularPolygon.class. Ако тогава стартираме javap RegularPolygon.class ще видим следното:

$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Това е много проста разбивка на файла на класа, която просто показва имената и типовете публични членове на класа. Добавяне на -p опцията ще включва частни членове:

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

Това все още не е много информация. За да видим как методите са внедрени в байтовия код на Java, нека добавим -c опция:

$ 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.'':()V 9: return }

Това е малко по-интересно. За да разберем цялата история обаче, трябва да използваме -v или -verbose опция, както в javap -p -v RegularPolygon.class:

Пълното съдържание на файл с клас на Java.

Тук най-накрая виждаме какво всъщност е във файла на класа. Какво означава всичко това? Нека да разгледаме някои от най-важните части.

Постоянен басейн

Цикълът на разработка на приложения C ++ включва етапи на компилация и свързване. Цикълът на разработка за Java прескача явен етап на свързване, тъй като свързването се случва по време на изпълнение. Файлът на класа трябва да поддържа това свързване по време на изпълнение. Това означава, че когато изходният код се отнася до което и да е поле или метод, полученият байт код трябва да съхранява съответните препратки в символна форма, готови за дереферентиране, след като приложението се зареди в паметта и действителните адреси могат да бъдат разрешени от свързващия механизъм за изпълнение. Тази символична форма трябва да съдържа:

Спецификацията на файловия формат на класа включва раздел от файла, наречен постоянен басейн , таблица на всички референции, необходими на линкера. Той съдържа записи от различен тип.

// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

Първият байт на всеки запис е цифров маркер, указващ вида на записа. Останалите байтове предоставят информация за стойността на записа. Броят на байтовете и правилата за тяхното тълкуване зависи от вида, посочен от първия байт.

Например Java клас, който използва константно цяло число 365 може да има постоянен запис в пула със следния байт код:

x03 00 00 01 6D

Първият байт, x03, идентифицира вида на записа, CONSTANT_Integer. Това информира линкера, че следващите четири байта съдържат стойността на цялото число. (Обърнете внимание, че 365 в шестнадесетичен е x16D). Ако това е 14-и запис в константния пул, javap -v ще го направи по този начин:

#14 = Integer 365

Много константни типове са съставени от препратки към по-„примитивни“ константни типове другаде в константния пул. Например нашият примерен код съдържа изявлението:

println( 'Calculating perimeter...' )

Използването на низова константа ще доведе до два записа в пула от константи: един запис с тип CONSTANT_String , и друг запис от тип CONSTANT_Utf8. Въвеждането на тип Constant_UTF8 съдържа действителното представяне на UTF8 на низовата стойност. Въвеждането на тип CONSTANT_String съдържа препратка към CONSTANT_Utf8 запис:

#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

Такова усложнение е необходимо, тъй като има други видове постоянни записи в пула, които се отнасят до записи от тип Utf8 и които не са записи от тип String. Например всяко позоваване на атрибут на клас ще създаде CONSTANT_Fieldref тип, който съдържа серия препратки към името на класа, името на атрибута и типа на атрибута:

#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

За повече подробности относно постоянния пул вижте документацията на JVM .

Таблици за полета и методи

Файлът на класа съдържа полева маса който съдържа информация за всяко поле (т.е. атрибут), дефинирано в класа. Това са препратки към записи в константния пул, които описват името и типа на полето, както и знамена за контрол на достъпа и други съответни данни.

Подобен таблица на методите присъства във файла на класа. Въпреки това, освен информация за име и тип, за всеки абстрактен метод той съдържа действителните инструкции за байт код, които трябва да бъдат изпълнени от JVM, както и структури от данни, използвани от стека на метода, описан по-долу.

JVM байт код

JVM използва собствен вътрешен набор от инструкции за изпълнение на компилиран код. Изпълнява се javap с -c Опцията включва компилираните реализации на метода в изхода. Ако разгледаме нашите RegularPolygon.class файл по този начин, ще видим следния изход за нашите getPerimeter() метод:

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

Действителният байт код може да изглежда по следния начин:

xB2 00 17 x12 19 xB6 00 1D x27 ...

Всяка инструкция започва с еднобайтово opcode идентифициране на инструкцията на JVM, последвана от нула или повече операнди с инструкции, с които да се работи, в зависимост от формата на конкретната инструкция. Това обикновено са или константни стойности, или препратки към константния пул. javap услужливо превежда байт кода в разбираема за човека форма, показваща:

Операндите, които се показват със знак за паунд, като #23, са препратки към записи в константния пул. Както виждаме, javap също така дава полезни коментари в изхода, идентифицирайки какво точно се препраща от пула.

Ще обсъдим няколко от често срещаните инструкции по-долу. За подробна информация относно пълния набор от инструкции на JVM вижте документация .

Метод повиквания и стек повиквания

Всяко извикване на метод трябва да може да се изпълнява със собствен контекст, който включва неща като локално декларирани променливи или аргументи, предадени на метода. Заедно те съставляват a стекова рамка . При извикване на метод се създава нов кадър и се поставя върху стек повиквания . Когато методът се върне, текущият кадър се премахва от стека на повикванията и се изхвърля, а кадърът, който е бил в сила преди извикването на метода, се възстановява.

Рамката на стека включва няколко различни структури. Две важни са стек от операнди и локална променлива таблица , обсъдени по-нататък.

Стека на повикванията на JVM.

Изпълнение на стека на операндите

Много инструкции на JVM работят върху техните рамки стек от операнди . Вместо да посочват изрично постоянен операнд в байт кода, вместо това тези инструкции приемат за вход стойностите в горната част на стека на операндите. Обикновено тези стойности се премахват от стека в процеса. Някои инструкции също поставят нови стойности в горната част на стека. По този начин инструкциите на JVM могат да се комбинират за извършване на сложни операции. Например изразът:

sideLength * this.numSides

се компилира към следното в нашия getPerimeter() метод:

8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul

Инструкциите на JVM могат да работят върху стека на операндите, за да изпълняват сложни функции.

Когато се извика метод, се създава нов стек на операнд като част от неговата рамка на стека, където ще се извършват операции. Трябва да бъдем внимателни с терминологията тук: думата „стек“ може да се отнася до стек повиквания , стекът от рамки, осигуряващи контекст за изпълнение на метод, или към конкретен кадър стек от операнди , при което действат инструкциите на JVM.

Локални променливи

Всяка рамка на стека поддържа таблица на локални променливи . Това обикновено включва препратка към this обект, всички аргументи, които са били предадени при извикване на метода, и всички локални променливи, декларирани в тялото на метода. Изпълнява се javap с -v опцията ще включва информация за това как трябва да се настрои рамката на стека на всеки метод, включително неговата локална таблица на променливите:

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

В този пример има две локални променливи. Променливата в слот 0 се нарича this, с типа RegularPolygon. Това е препратката към собствения клас на метода. Променливата в слот 1 се нарича sideLength, с типа D (посочва двойно). Това е аргументът, който се предава на нашите getPerimeter() метод.

Инструкции като iload_1, fstore_2 или aload [n], прехвърлят различни видове локални променливи между стека на операндите и таблицата с локални променливи. Тъй като първият елемент в таблицата обикновено е препратката към this, инструкцията aload_0 се среща често при всеки метод, който оперира със собствен клас.

С това завършваме нашето разглеждане на основите на JVM. Щракнете тук, за да се върнете към основната статия.

: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

В този случай стойността на полето m4 не се изчислява, докато не е необходимо. Специалният, частен метод m4$lzycompute() се създава за изчисляване на мързеливата стойност, а полето bitmap

Зацапайте ръцете си с байт кода на Scala JVM



Езикът Scala продължава да набира популярност през последните няколко години, благодарение на отличната си комбинация от функционални и обектно-ориентирани принципи за разработване на софтуер и нейното внедряване върху доказаната Java Virtual Machine (JVM).

Макар че Стълба компилира се в байт код на Java, той е предназначен да подобри много от възприетите недостатъци на езика Java. Предлагайки пълна функционална поддръжка за програмиране, основният синтаксис на Scala съдържа много неявни структури, които трябва да бъдат изградени изрично от програмистите на Java, някои от които включват значителна сложност.



Създаването на език, който се компилира в байт код на Java, изисква задълбочено разбиране на вътрешната работа на Java Virtual Machine. За да оценим постигнатото от разработчиците на Scala, е необходимо да отидем под капака и да проучим как изходният код на Scala се интерпретира от компилатора, за да се получи ефективен и ефективен JVM байт код.



Нека да разгледаме как се изпълняват всички тези неща.



Предпоставки

Четенето на тази статия изисква известно разбиране на байт кода на Java Virtual Machine. Пълна спецификация на виртуалната машина може да бъде получена от Официалната документация на Oracle . Четенето на цялата спецификация не е от решаващо значение за разбирането на тази статия, така че за кратко въведение в основите съм подготвил кратко ръководство в долната част на статията.

Щракнете тук, за да прочетете сривен курс за основите на JVM.

Необходима е помощна програма за разглобяване на байт кода на Java, за да се възпроизведат примерите, предоставени по-долу, и да се продължи с допълнително проучване. Java Development Kit предоставя своя собствена помощна програма за команден ред, javap, която ще използваме тук. Бърза демонстрация на това как javap работи е включен в ръководството отдолу .



И разбира се, работеща инсталация на компилатора Scala е необходима за читателите, които искат да следват заедно с примерите. Тази статия е написана с помощта на Скала 2.11.7 . Различните версии на Scala могат да генерират малко по-различен байт код.

По подразбиране гетери и сетери

Въпреки че Java конвенцията винаги осигурява методи за получаване и задаване за публични атрибути, Java програмистите са длъжни да ги напишат сами, въпреки факта, че моделът за всеки не се е променил от десетилетия. Scala, за разлика от това, предлага по подразбиране гетери и сетери.



Нека разгледаме следния пример:

class Person(val name:String) { }

Нека да разгледаме вътре в класа Person. Ако компилираме този файл с scalac, тогава стартираме $ javap -p Person.class дава ни:



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 се генерира поле и неговият метод за получаване. Полето е частно и окончателно, докато методът е публичен.

Ако заменим val с var в Person източник и прекомпилирайте, след това полето final модификаторът отпада и се добавя и методът на задаване:



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 }

Ако има val или var се дефинира вътре в тялото на класа, след което се създават съответните частни полета и методи за достъп и се инициализират по подходящ начин при създаване на екземпляр.

Имайте предвид, че такова изпълнение на ниво клас val и var полета означава, че ако някои променливи се използват на ниво клас за съхранение на междинни стойности и никога не са достъпни директно от програмиста, инициализацията на всяко такова поле ще добави един до два метода към отпечатъка на класа. Добавяне на private модификатор за такива полета не означава, че съответните аксесоари ще бъдат отпаднали. Те просто ще станат частни.



Определения на променливи и функции

Нека приемем, че имаме метод, m(), и създаваме три различни препратки в стил Scala към тази функция:

class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }

Как са всяка от тези препратки към m конструиран? Кога m да се изпълни във всеки случай? Нека да разгледаме получения байт код. Следващият изход показва резултатите от javap -v Person.class (пропускайки много излишни резултати):

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.'':()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

В константния пул виждаме, че препратката към метода m() се съхранява в индекс #30. В кода на конструктора виждаме, че този метод се извиква два пъти по време на инициализация, с инструкцията invokevirtual #30 появява се първо при изместване на байта 11, след това при изместване 19. Първото извикване е последвано от инструкцията putfield #22 което присвоява резултата от този метод на полето m1, посочено чрез индекс #22 в постоянния басейн. Второто извикване е последвано от същия модел, този път присвоявайки стойността на полето m2, индексирано на #24 в постоянния басейн.

С други думи, присвояване на метод на променлива, дефинирана с val или var само присвоява резултат на метода към тази променлива. Виждаме, че методите m1() и m2() които са създадени са просто гетери за тези променливи. В случая на var m2, ние също виждаме, че задателят m2_$eq(int) се създава, който се държи точно като всеки друг сетер, като презаписва стойността в полето.

Използването на ключовата дума обаче def дава различен резултат. Вместо да извлича стойност на полето за връщане, методът m3() включва също инструкцията invokevirtual #30. Тоест, всеки път, когато се извика този метод, той извиква m() и връща резултата от този метод.

Така че, както виждаме, Scala предоставя три начина за работа с полета на класа и те лесно се определят чрез ключовите думи val, var и def. В Java ще трябва да внедрим необходимите сетери и гетери изрично и такъв ръчно написан код на шаблон ще бъде много по-малко изразителен и по-податлив на грешки.

Мързеливи ценности

По-сложен код се получава при деклариране на мързелива стойност. Да приемем, че сме добавили следното поле към предварително дефинирания клас:

lazy val m4 = m

Изпълнява се javap -p -v Person.class сега ще разкрие следното:

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

В този случай стойността на полето m4 не се изчислява, докато не е необходимо. Специалният, частен метод m4$lzycompute() се създава за изчисляване на мързеливата стойност, а полето bitmap$0 за проследяване на състоянието му. Метод m4() проверява дали стойността на това поле е 0, което показва, че m4 все още не е инициализиран, в този случай m4$lzycompute() се извиква, попълващ m4 и връщане на стойността му. Този частен метод също така задава стойността на bitmap$0 до 1, така че следващия път m4() се нарича, той ще пропусне извикването на метода за инициализация и вместо това просто ще върне стойността на m4

Резултатите от първото извикване на Scala мързелива стойност.

Байтовият код, който Scala произвежда тук, е проектиран да бъде едновременно безопасен и ефективен. За да бъде безопасен за нишки, методът на мързеливи изчисления използва monitorenter / monitorexit чифт инструкции. Методът остава ефективен, тъй като режийните разходи за тази синхронизация се появяват само при първото четене на мързеливата стойност.

За да се посочи състоянието на мързеливата стойност е необходим само един бит. Така че, ако няма повече от 32 мързеливи стойности, едно поле int може да ги проследи всички. Ако в изходния код е дефинирана повече от една мързелива стойност, горният байт код ще бъде модифициран от компилатора, за да приложи битова маска за тази цел.

Отново Scala ни позволява лесно да се възползваме от специфичен вид поведение, което би трябвало да бъде внедрено изрично в Java, спестявайки усилия и намалявайки риска от печатни грешки.

Функция като стойност

Сега нека да разгледаме следния код на Scala:

class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }

Printer class има едно поле, output, с типа String => Unit: функция, която приема String и връща обект от тип Unit (подобно на void в Java). В основния метод създаваме един от тези обекти и задаваме това поле като анонимна функция, която отпечатва даден низ.

Компилирането на този код генерира четири класа файлове:

Изходният код се компилира в четири класа файлове.

Hello.class е клас на обвивка, чийто основен метод просто извиква Hello$.main():

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

Скритото Hello$.class съдържа реалното изпълнение на основния метод. За да разгледате байт кода му, уверете се, че сте избягали правилно $ според правилата на вашата командна обвивка, за да избегнете нейното тълкуване като специален знак:

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.'':()V 11: invokespecial #22 // Method Printer.'':(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

Методът създава Printer. След това създава Hello$$anonfun$1, която съдържа нашата анонимна функция s => println(s). Printer се инициализира с този обект като output поле. След това това поле се зарежда в стека и се изпълнява с операнда 'Hello'.

Нека да разгледаме анонимния функционален клас, Hello$$anonfun$1.class, по-долу. Виждаме, че разширява Scala’s Function1 (като AbstractFunction1) чрез прилагане на apply() метод. Всъщност той създава две apply() методи, едната обвива другата, които заедно извършват проверка на типа (в този случай, че входът е String) и изпълняват анонимната функция (отпечатване на входа с println()).

public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 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

Поглеждайки назад към Hello$.main() метод по-горе, можем да видим, че при отместване 21, изпълнението на анонимната функция се задейства от извикване на нейния apply( Object ) метод.

И накрая, за пълнота, нека разгледаме байт кода за Printer.class:

public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return

Виждаме, че анонимната функция тук се третира точно като всяка val променлива. Той се съхранява в полето на класа output и гетерът output() е създаден. Единствената разлика е, че тази променлива вече трябва да реализира интерфейса Scala scala.Function1 (което AbstractFunction1 прави).

И така, цената на тази елегантна функция Scala са основните класове помощни програми, създадени да представят и изпълнят една анонимна функция, която може да се използва като стойност. Трябва да вземете предвид броя на тези функции, както и подробности за вашата реализация на VM, за да разберете какво означава това за вашето конкретно приложение.

Преминаване под капака със Scala: Разгледайте как този мощен език е реализиран в JVM байт код. Tweet

Скала черти

Характеристиките на Scala са подобни на интерфейсите в Java. Следващата характеристика дефинира два метода и осигурява изпълнение по подразбиране на втория. Нека да видим как се прилага:

trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

Изходният код се компилира в два класа файлове.

Произвеждат се две обекти: Similarity.class, интерфейсът, деклариращ двата метода, и синтетичният клас, Similarity$class.class, осигуряващ изпълнението по подразбиране:

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

Когато клас изпълнява тази черта и извиква метода isNotSimilar, компилаторът Scala генерира инструкция за байт код invokestatic да извика статичния метод, предоставен от придружаващия клас.

Сложните полиморфизъм и наследствени структури могат да бъдат създадени от черти. Например, множество черти, както и класът на внедряване, могат всички да заменят метод с един и същ подпис, извикващ super.methodName() за да предадете контрола на следващата черта. Когато компилаторът Scala срещне такива повиквания, той:

По този начин можем да видим, че мощната концепция за чертите е реализирана на ниво JVM по начин, който не води до значителни режийни разходи, и програмистите на Scala могат да се насладят на тази функция, без да се притесняват, че тя ще бъде твърде скъпа по време на изпълнение.

Единични

Scala предоставя изричната дефиниция на единични класове, използвайки ключовата дума object Нека разгледаме следния сингъл клас:

object Config { val home_dir = '/home/user' }

Компилаторът създава два класа файлове:

Изходният код се компилира в два класа файлове.

Config.class е доста проста:

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

Това е просто декоратор за синтетичния Config$ клас, който вгражда функционалността на синглона. Изследване на този клас с javap -p -c произвежда следния байт код:

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 '':()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.'':()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

Състои се от следното:

Сингълтънът е популярен и полезен модел на дизайн. Езикът Java не осигурява директен начин за неговото определяне на езиково ниво; по-скоро отговорността на разработчика е да го внедри в Java source. Scala, от друга страна, предоставя ясен и удобен начин за деклариране на сингълтон изрично с помощта на object ключова дума. Както виждаме, гледайки под капака, той е изпълнен по достъпен и естествен начин.

Заключение

Сега видяхме как Scala компилира няколко неявни и функционални функции за програмиране в сложни Java байтови структури. С този поглед към вътрешната работа на Scala, ние можем да разберем по-задълбочено силата на Scala, помагайки ни да се възползваме максимално от този мощен език.

Вече разполагаме и с инструментите, за да изследваме самия език. Има много полезни функции на синтаксиса на Scala, които не са обхванати в тази статия, като класове на случаи, currying и разбиране на списъци. Препоръчвам ви сами да проучите изпълнението на Scala от тези структури, за да можете да научите как да бъдете нинджа от следващо ниво Scala!


Виртуалната машина Java: Crash Course

Подобно на Java компилатора, Scala компилаторът преобразува изходния код в .class файлове, съдържащи Java байт код, който да бъде изпълнен от Java Virtual Machine. За да се разбере как двата езика се различават под капака, е необходимо да се разбере системата, към която и двамата са насочени. Тук представяме кратък преглед на някои основни елементи от архитектурата на Java Virtual Machine, файловата структура на класа и основите на асемблера.

Имайте предвид, че това ръководство ще обхваща само минимума, който да позволи последващо, заедно с горната статия. Въпреки че много основни компоненти на JVM не са обсъдени тук, пълни подробности можете да намерите в официалните документи, тук .

Декомпилиране на файлове от клас с javap
Постоянен басейн
Таблици за полета и методи
JVM байт код
Метод повиквания и стек повиквания
Изпълнение на стека на операндите
Локални променливи
Върнете се в началото

Декомпилиране на файлове от клас с javap

Java се доставя с javap помощна програма за команден ред, която декомпилира .class файлове в разбираема за човека форма. Тъй като Scala и Java класните файлове и двата са насочени към една и съща JVM, javap може да се използва за изследване на файлове с класове, съставени от Scala.

Нека компилираме следния изходен код:

// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }

Компилиране на това с scalac RegularPolygon.scala ще произведе RegularPolygon.class. Ако тогава стартираме javap RegularPolygon.class ще видим следното:

$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Това е много проста разбивка на файла на класа, която просто показва имената и типовете публични членове на класа. Добавяне на -p опцията ще включва частни членове:

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

Това все още не е много информация. За да видим как методите са внедрени в байтовия код на Java, нека добавим -c опция:

$ 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.'':()V 9: return }

Това е малко по-интересно. За да разберем цялата история обаче, трябва да използваме -v или -verbose опция, както в javap -p -v RegularPolygon.class:

Пълното съдържание на файл с клас на Java.

Тук най-накрая виждаме какво всъщност е във файла на класа. Какво означава всичко това? Нека да разгледаме някои от най-важните части.

Постоянен басейн

Цикълът на разработка на приложения C ++ включва етапи на компилация и свързване. Цикълът на разработка за Java прескача явен етап на свързване, тъй като свързването се случва по време на изпълнение. Файлът на класа трябва да поддържа това свързване по време на изпълнение. Това означава, че когато изходният код се отнася до което и да е поле или метод, полученият байт код трябва да съхранява съответните препратки в символна форма, готови за дереферентиране, след като приложението се зареди в паметта и действителните адреси могат да бъдат разрешени от свързващия механизъм за изпълнение. Тази символична форма трябва да съдържа:

Спецификацията на файловия формат на класа включва раздел от файла, наречен постоянен басейн , таблица на всички референции, необходими на линкера. Той съдържа записи от различен тип.

// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

Първият байт на всеки запис е цифров маркер, указващ вида на записа. Останалите байтове предоставят информация за стойността на записа. Броят на байтовете и правилата за тяхното тълкуване зависи от вида, посочен от първия байт.

Например Java клас, който използва константно цяло число 365 може да има постоянен запис в пула със следния байт код:

x03 00 00 01 6D

Първият байт, x03, идентифицира вида на записа, CONSTANT_Integer. Това информира линкера, че следващите четири байта съдържат стойността на цялото число. (Обърнете внимание, че 365 в шестнадесетичен е x16D). Ако това е 14-и запис в константния пул, javap -v ще го направи по този начин:

#14 = Integer 365

Много константни типове са съставени от препратки към по-„примитивни“ константни типове другаде в константния пул. Например нашият примерен код съдържа изявлението:

println( 'Calculating perimeter...' )

Използването на низова константа ще доведе до два записа в пула от константи: един запис с тип CONSTANT_String , и друг запис от тип CONSTANT_Utf8. Въвеждането на тип Constant_UTF8 съдържа действителното представяне на UTF8 на низовата стойност. Въвеждането на тип CONSTANT_String съдържа препратка към CONSTANT_Utf8 запис:

#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

Такова усложнение е необходимо, тъй като има други видове постоянни записи в пула, които се отнасят до записи от тип Utf8 и които не са записи от тип String. Например всяко позоваване на атрибут на клас ще създаде CONSTANT_Fieldref тип, който съдържа серия препратки към името на класа, името на атрибута и типа на атрибута:

#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

За повече подробности относно постоянния пул вижте документацията на JVM .

Таблици за полета и методи

Файлът на класа съдържа полева маса който съдържа информация за всяко поле (т.е. атрибут), дефинирано в класа. Това са препратки към записи в константния пул, които описват името и типа на полето, както и знамена за контрол на достъпа и други съответни данни.

Подобен таблица на методите присъства във файла на класа. Въпреки това, освен информация за име и тип, за всеки абстрактен метод той съдържа действителните инструкции за байт код, които трябва да бъдат изпълнени от JVM, както и структури от данни, използвани от стека на метода, описан по-долу.

JVM байт код

JVM използва собствен вътрешен набор от инструкции за изпълнение на компилиран код. Изпълнява се javap с -c Опцията включва компилираните реализации на метода в изхода. Ако разгледаме нашите RegularPolygon.class файл по този начин, ще видим следния изход за нашите getPerimeter() метод:

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

Действителният байт код може да изглежда по следния начин:

xB2 00 17 x12 19 xB6 00 1D x27 ...

Всяка инструкция започва с еднобайтово opcode идентифициране на инструкцията на JVM, последвана от нула или повече операнди с инструкции, с които да се работи, в зависимост от формата на конкретната инструкция. Това обикновено са или константни стойности, или препратки към константния пул. javap услужливо превежда байт кода в разбираема за човека форма, показваща:

Операндите, които се показват със знак за паунд, като #23, са препратки към записи в константния пул. Както виждаме, javap също така дава полезни коментари в изхода, идентифицирайки какво точно се препраща от пула.

Ще обсъдим няколко от често срещаните инструкции по-долу. За подробна информация относно пълния набор от инструкции на JVM вижте документация .

Метод повиквания и стек повиквания

Всяко извикване на метод трябва да може да се изпълнява със собствен контекст, който включва неща като локално декларирани променливи или аргументи, предадени на метода. Заедно те съставляват a стекова рамка . При извикване на метод се създава нов кадър и се поставя върху стек повиквания . Когато методът се върне, текущият кадър се премахва от стека на повикванията и се изхвърля, а кадърът, който е бил в сила преди извикването на метода, се възстановява.

Рамката на стека включва няколко различни структури. Две важни са стек от операнди и локална променлива таблица , обсъдени по-нататък.

Стека на повикванията на JVM.

Изпълнение на стека на операндите

Много инструкции на JVM работят върху техните рамки стек от операнди . Вместо да посочват изрично постоянен операнд в байт кода, вместо това тези инструкции приемат за вход стойностите в горната част на стека на операндите. Обикновено тези стойности се премахват от стека в процеса. Някои инструкции също поставят нови стойности в горната част на стека. По този начин инструкциите на JVM могат да се комбинират за извършване на сложни операции. Например изразът:

sideLength * this.numSides

се компилира към следното в нашия getPerimeter() метод:

8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul

Инструкциите на JVM могат да работят върху стека на операндите, за да изпълняват сложни функции.

Когато се извика метод, се създава нов стек на операнд като част от неговата рамка на стека, където ще се извършват операции. Трябва да бъдем внимателни с терминологията тук: думата „стек“ може да се отнася до стек повиквания , стекът от рамки, осигуряващи контекст за изпълнение на метод, или към конкретен кадър стек от операнди , при което действат инструкциите на JVM.

Локални променливи

Всяка рамка на стека поддържа таблица на локални променливи . Това обикновено включва препратка към this обект, всички аргументи, които са били предадени при извикване на метода, и всички локални променливи, декларирани в тялото на метода. Изпълнява се javap с -v опцията ще включва информация за това как трябва да се настрои рамката на стека на всеки метод, включително неговата локална таблица на променливите:

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

В този пример има две локални променливи. Променливата в слот 0 се нарича this, с типа RegularPolygon. Това е препратката към собствения клас на метода. Променливата в слот 1 се нарича sideLength, с типа D (посочва двойно). Това е аргументът, който се предава на нашите getPerimeter() метод.

Инструкции като iload_1, fstore_2 или aload [n], прехвърлят различни видове локални променливи между стека на операндите и таблицата с локални променливи. Тъй като първият елемент в таблицата обикновено е препратката към this, инструкцията aload_0 се среща често при всеки метод, който оперира със собствен клас.

С това завършваме нашето разглеждане на основите на JVM. Щракнете тук, за да се върнете към основната статия.

за проследяване на състоянието му. Метод m4() проверява дали стойността на това поле е 0, което показва, че m4 все още не е инициализиран, в този случай m4$lzycompute() се извиква, попълващ m4 и връщане на стойността му. Този частен метод също така задава стойността на bitmap

Зацапайте ръцете си с байт кода на Scala JVM



Езикът Scala продължава да набира популярност през последните няколко години, благодарение на отличната си комбинация от функционални и обектно-ориентирани принципи за разработване на софтуер и нейното внедряване върху доказаната Java Virtual Machine (JVM).

Макар че Стълба компилира се в байт код на Java, той е предназначен да подобри много от възприетите недостатъци на езика Java. Предлагайки пълна функционална поддръжка за програмиране, основният синтаксис на Scala съдържа много неявни структури, които трябва да бъдат изградени изрично от програмистите на Java, някои от които включват значителна сложност.



Създаването на език, който се компилира в байт код на Java, изисква задълбочено разбиране на вътрешната работа на Java Virtual Machine. За да оценим постигнатото от разработчиците на Scala, е необходимо да отидем под капака и да проучим как изходният код на Scala се интерпретира от компилатора, за да се получи ефективен и ефективен JVM байт код.



Нека да разгледаме как се изпълняват всички тези неща.



Предпоставки

Четенето на тази статия изисква известно разбиране на байт кода на Java Virtual Machine. Пълна спецификация на виртуалната машина може да бъде получена от Официалната документация на Oracle . Четенето на цялата спецификация не е от решаващо значение за разбирането на тази статия, така че за кратко въведение в основите съм подготвил кратко ръководство в долната част на статията.

Щракнете тук, за да прочетете сривен курс за основите на JVM.

Необходима е помощна програма за разглобяване на байт кода на Java, за да се възпроизведат примерите, предоставени по-долу, и да се продължи с допълнително проучване. Java Development Kit предоставя своя собствена помощна програма за команден ред, javap, която ще използваме тук. Бърза демонстрация на това как javap работи е включен в ръководството отдолу .



И разбира се, работеща инсталация на компилатора Scala е необходима за читателите, които искат да следват заедно с примерите. Тази статия е написана с помощта на Скала 2.11.7 . Различните версии на Scala могат да генерират малко по-различен байт код.

По подразбиране гетери и сетери

Въпреки че Java конвенцията винаги осигурява методи за получаване и задаване за публични атрибути, Java програмистите са длъжни да ги напишат сами, въпреки факта, че моделът за всеки не се е променил от десетилетия. Scala, за разлика от това, предлага по подразбиране гетери и сетери.



Нека разгледаме следния пример:

class Person(val name:String) { }

Нека да разгледаме вътре в класа Person. Ако компилираме този файл с scalac, тогава стартираме $ javap -p Person.class дава ни:



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 се генерира поле и неговият метод за получаване. Полето е частно и окончателно, докато методът е публичен.

Ако заменим val с var в Person източник и прекомпилирайте, след това полето final модификаторът отпада и се добавя и методът на задаване:



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 }

Ако има val или var се дефинира вътре в тялото на класа, след което се създават съответните частни полета и методи за достъп и се инициализират по подходящ начин при създаване на екземпляр.

Имайте предвид, че такова изпълнение на ниво клас val и var полета означава, че ако някои променливи се използват на ниво клас за съхранение на междинни стойности и никога не са достъпни директно от програмиста, инициализацията на всяко такова поле ще добави един до два метода към отпечатъка на класа. Добавяне на private модификатор за такива полета не означава, че съответните аксесоари ще бъдат отпаднали. Те просто ще станат частни.



Определения на променливи и функции

Нека приемем, че имаме метод, m(), и създаваме три различни препратки в стил Scala към тази функция:

class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }

Как са всяка от тези препратки към m конструиран? Кога m да се изпълни във всеки случай? Нека да разгледаме получения байт код. Следващият изход показва резултатите от javap -v Person.class (пропускайки много излишни резултати):

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.'':()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

В константния пул виждаме, че препратката към метода m() се съхранява в индекс #30. В кода на конструктора виждаме, че този метод се извиква два пъти по време на инициализация, с инструкцията invokevirtual #30 появява се първо при изместване на байта 11, след това при изместване 19. Първото извикване е последвано от инструкцията putfield #22 което присвоява резултата от този метод на полето m1, посочено чрез индекс #22 в постоянния басейн. Второто извикване е последвано от същия модел, този път присвоявайки стойността на полето m2, индексирано на #24 в постоянния басейн.

С други думи, присвояване на метод на променлива, дефинирана с val или var само присвоява резултат на метода към тази променлива. Виждаме, че методите m1() и m2() които са създадени са просто гетери за тези променливи. В случая на var m2, ние също виждаме, че задателят m2_$eq(int) се създава, който се държи точно като всеки друг сетер, като презаписва стойността в полето.

Използването на ключовата дума обаче def дава различен резултат. Вместо да извлича стойност на полето за връщане, методът m3() включва също инструкцията invokevirtual #30. Тоест, всеки път, когато се извика този метод, той извиква m() и връща резултата от този метод.

Така че, както виждаме, Scala предоставя три начина за работа с полета на класа и те лесно се определят чрез ключовите думи val, var и def. В Java ще трябва да внедрим необходимите сетери и гетери изрично и такъв ръчно написан код на шаблон ще бъде много по-малко изразителен и по-податлив на грешки.

Мързеливи ценности

По-сложен код се получава при деклариране на мързелива стойност. Да приемем, че сме добавили следното поле към предварително дефинирания клас:

lazy val m4 = m

Изпълнява се javap -p -v Person.class сега ще разкрие следното:

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

В този случай стойността на полето m4 не се изчислява, докато не е необходимо. Специалният, частен метод m4$lzycompute() се създава за изчисляване на мързеливата стойност, а полето bitmap$0 за проследяване на състоянието му. Метод m4() проверява дали стойността на това поле е 0, което показва, че m4 все още не е инициализиран, в този случай m4$lzycompute() се извиква, попълващ m4 и връщане на стойността му. Този частен метод също така задава стойността на bitmap$0 до 1, така че следващия път m4() се нарича, той ще пропусне извикването на метода за инициализация и вместо това просто ще върне стойността на m4

Резултатите от първото извикване на Scala мързелива стойност.

Байтовият код, който Scala произвежда тук, е проектиран да бъде едновременно безопасен и ефективен. За да бъде безопасен за нишки, методът на мързеливи изчисления използва monitorenter / monitorexit чифт инструкции. Методът остава ефективен, тъй като режийните разходи за тази синхронизация се появяват само при първото четене на мързеливата стойност.

За да се посочи състоянието на мързеливата стойност е необходим само един бит. Така че, ако няма повече от 32 мързеливи стойности, едно поле int може да ги проследи всички. Ако в изходния код е дефинирана повече от една мързелива стойност, горният байт код ще бъде модифициран от компилатора, за да приложи битова маска за тази цел.

Отново Scala ни позволява лесно да се възползваме от специфичен вид поведение, което би трябвало да бъде внедрено изрично в Java, спестявайки усилия и намалявайки риска от печатни грешки.

Функция като стойност

Сега нека да разгледаме следния код на Scala:

class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }

Printer class има едно поле, output, с типа String => Unit: функция, която приема String и връща обект от тип Unit (подобно на void в Java). В основния метод създаваме един от тези обекти и задаваме това поле като анонимна функция, която отпечатва даден низ.

Компилирането на този код генерира четири класа файлове:

Изходният код се компилира в четири класа файлове.

Hello.class е клас на обвивка, чийто основен метод просто извиква Hello$.main():

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

Скритото Hello$.class съдържа реалното изпълнение на основния метод. За да разгледате байт кода му, уверете се, че сте избягали правилно $ според правилата на вашата командна обвивка, за да избегнете нейното тълкуване като специален знак:

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.'':()V 11: invokespecial #22 // Method Printer.'':(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

Методът създава Printer. След това създава Hello$$anonfun$1, която съдържа нашата анонимна функция s => println(s). Printer се инициализира с този обект като output поле. След това това поле се зарежда в стека и се изпълнява с операнда 'Hello'.

Нека да разгледаме анонимния функционален клас, Hello$$anonfun$1.class, по-долу. Виждаме, че разширява Scala’s Function1 (като AbstractFunction1) чрез прилагане на apply() метод. Всъщност той създава две apply() методи, едната обвива другата, които заедно извършват проверка на типа (в този случай, че входът е String) и изпълняват анонимната функция (отпечатване на входа с println()).

public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 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

Поглеждайки назад към Hello$.main() метод по-горе, можем да видим, че при отместване 21, изпълнението на анонимната функция се задейства от извикване на нейния apply( Object ) метод.

И накрая, за пълнота, нека разгледаме байт кода за Printer.class:

public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return

Виждаме, че анонимната функция тук се третира точно като всяка val променлива. Той се съхранява в полето на класа output и гетерът output() е създаден. Единствената разлика е, че тази променлива вече трябва да реализира интерфейса Scala scala.Function1 (което AbstractFunction1 прави).

И така, цената на тази елегантна функция Scala са основните класове помощни програми, създадени да представят и изпълнят една анонимна функция, която може да се използва като стойност. Трябва да вземете предвид броя на тези функции, както и подробности за вашата реализация на VM, за да разберете какво означава това за вашето конкретно приложение.

Преминаване под капака със Scala: Разгледайте как този мощен език е реализиран в JVM байт код. Tweet

Скала черти

Характеристиките на Scala са подобни на интерфейсите в Java. Следващата характеристика дефинира два метода и осигурява изпълнение по подразбиране на втория. Нека да видим как се прилага:

trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

Изходният код се компилира в два класа файлове.

Произвеждат се две обекти: Similarity.class, интерфейсът, деклариращ двата метода, и синтетичният клас, Similarity$class.class, осигуряващ изпълнението по подразбиране:

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

Когато клас изпълнява тази черта и извиква метода isNotSimilar, компилаторът Scala генерира инструкция за байт код invokestatic да извика статичния метод, предоставен от придружаващия клас.

Сложните полиморфизъм и наследствени структури могат да бъдат създадени от черти. Например, множество черти, както и класът на внедряване, могат всички да заменят метод с един и същ подпис, извикващ super.methodName() за да предадете контрола на следващата черта. Когато компилаторът Scala срещне такива повиквания, той:

По този начин можем да видим, че мощната концепция за чертите е реализирана на ниво JVM по начин, който не води до значителни режийни разходи, и програмистите на Scala могат да се насладят на тази функция, без да се притесняват, че тя ще бъде твърде скъпа по време на изпълнение.

Единични

Scala предоставя изричната дефиниция на единични класове, използвайки ключовата дума object Нека разгледаме следния сингъл клас:

object Config { val home_dir = '/home/user' }

Компилаторът създава два класа файлове:

Изходният код се компилира в два класа файлове.

Config.class е доста проста:

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

Това е просто декоратор за синтетичния Config$ клас, който вгражда функционалността на синглона. Изследване на този клас с javap -p -c произвежда следния байт код:

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 '':()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.'':()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

Състои се от следното:

Сингълтънът е популярен и полезен модел на дизайн. Езикът Java не осигурява директен начин за неговото определяне на езиково ниво; по-скоро отговорността на разработчика е да го внедри в Java source. Scala, от друга страна, предоставя ясен и удобен начин за деклариране на сингълтон изрично с помощта на object ключова дума. Както виждаме, гледайки под капака, той е изпълнен по достъпен и естествен начин.

Заключение

Сега видяхме как Scala компилира няколко неявни и функционални функции за програмиране в сложни Java байтови структури. С този поглед към вътрешната работа на Scala, ние можем да разберем по-задълбочено силата на Scala, помагайки ни да се възползваме максимално от този мощен език.

Вече разполагаме и с инструментите, за да изследваме самия език. Има много полезни функции на синтаксиса на Scala, които не са обхванати в тази статия, като класове на случаи, currying и разбиране на списъци. Препоръчвам ви сами да проучите изпълнението на Scala от тези структури, за да можете да научите как да бъдете нинджа от следващо ниво Scala!


Виртуалната машина Java: Crash Course

Подобно на Java компилатора, Scala компилаторът преобразува изходния код в .class файлове, съдържащи Java байт код, който да бъде изпълнен от Java Virtual Machine. За да се разбере как двата езика се различават под капака, е необходимо да се разбере системата, към която и двамата са насочени. Тук представяме кратък преглед на някои основни елементи от архитектурата на Java Virtual Machine, файловата структура на класа и основите на асемблера.

Имайте предвид, че това ръководство ще обхваща само минимума, който да позволи последващо, заедно с горната статия. Въпреки че много основни компоненти на JVM не са обсъдени тук, пълни подробности можете да намерите в официалните документи, тук .

Декомпилиране на файлове от клас с javap
Постоянен басейн
Таблици за полета и методи
JVM байт код
Метод повиквания и стек повиквания
Изпълнение на стека на операндите
Локални променливи
Върнете се в началото

Декомпилиране на файлове от клас с javap

Java се доставя с javap помощна програма за команден ред, която декомпилира .class файлове в разбираема за човека форма. Тъй като Scala и Java класните файлове и двата са насочени към една и съща JVM, javap може да се използва за изследване на файлове с класове, съставени от Scala.

Нека компилираме следния изходен код:

// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }

Компилиране на това с scalac RegularPolygon.scala ще произведе RegularPolygon.class. Ако тогава стартираме javap RegularPolygon.class ще видим следното:

$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Това е много проста разбивка на файла на класа, която просто показва имената и типовете публични членове на класа. Добавяне на -p опцията ще включва частни членове:

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

Това все още не е много информация. За да видим как методите са внедрени в байтовия код на Java, нека добавим -c опция:

$ 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.'':()V 9: return }

Това е малко по-интересно. За да разберем цялата история обаче, трябва да използваме -v или -verbose опция, както в javap -p -v RegularPolygon.class:

Пълното съдържание на файл с клас на Java.

Тук най-накрая виждаме какво всъщност е във файла на класа. Какво означава всичко това? Нека да разгледаме някои от най-важните части.

Постоянен басейн

Цикълът на разработка на приложения C ++ включва етапи на компилация и свързване. Цикълът на разработка за Java прескача явен етап на свързване, тъй като свързването се случва по време на изпълнение. Файлът на класа трябва да поддържа това свързване по време на изпълнение. Това означава, че когато изходният код се отнася до което и да е поле или метод, полученият байт код трябва да съхранява съответните препратки в символна форма, готови за дереферентиране, след като приложението се зареди в паметта и действителните адреси могат да бъдат разрешени от свързващия механизъм за изпълнение. Тази символична форма трябва да съдържа:

Спецификацията на файловия формат на класа включва раздел от файла, наречен постоянен басейн , таблица на всички референции, необходими на линкера. Той съдържа записи от различен тип.

// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

Първият байт на всеки запис е цифров маркер, указващ вида на записа. Останалите байтове предоставят информация за стойността на записа. Броят на байтовете и правилата за тяхното тълкуване зависи от вида, посочен от първия байт.

Например Java клас, който използва константно цяло число 365 може да има постоянен запис в пула със следния байт код:

x03 00 00 01 6D

Първият байт, x03, идентифицира вида на записа, CONSTANT_Integer. Това информира линкера, че следващите четири байта съдържат стойността на цялото число. (Обърнете внимание, че 365 в шестнадесетичен е x16D). Ако това е 14-и запис в константния пул, javap -v ще го направи по този начин:

#14 = Integer 365

Много константни типове са съставени от препратки към по-„примитивни“ константни типове другаде в константния пул. Например нашият примерен код съдържа изявлението:

println( 'Calculating perimeter...' )

Използването на низова константа ще доведе до два записа в пула от константи: един запис с тип CONSTANT_String , и друг запис от тип CONSTANT_Utf8. Въвеждането на тип Constant_UTF8 съдържа действителното представяне на UTF8 на низовата стойност. Въвеждането на тип CONSTANT_String съдържа препратка към CONSTANT_Utf8 запис:

#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

Такова усложнение е необходимо, тъй като има други видове постоянни записи в пула, които се отнасят до записи от тип Utf8 и които не са записи от тип String. Например всяко позоваване на атрибут на клас ще създаде CONSTANT_Fieldref тип, който съдържа серия препратки към името на класа, името на атрибута и типа на атрибута:

#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

За повече подробности относно постоянния пул вижте документацията на JVM .

Таблици за полета и методи

Файлът на класа съдържа полева маса който съдържа информация за всяко поле (т.е. атрибут), дефинирано в класа. Това са препратки към записи в константния пул, които описват името и типа на полето, както и знамена за контрол на достъпа и други съответни данни.

Подобен таблица на методите присъства във файла на класа. Въпреки това, освен информация за име и тип, за всеки абстрактен метод той съдържа действителните инструкции за байт код, които трябва да бъдат изпълнени от JVM, както и структури от данни, използвани от стека на метода, описан по-долу.

JVM байт код

JVM използва собствен вътрешен набор от инструкции за изпълнение на компилиран код. Изпълнява се javap с -c Опцията включва компилираните реализации на метода в изхода. Ако разгледаме нашите RegularPolygon.class файл по този начин, ще видим следния изход за нашите getPerimeter() метод:

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

Действителният байт код може да изглежда по следния начин:

xB2 00 17 x12 19 xB6 00 1D x27 ...

Всяка инструкция започва с еднобайтово opcode идентифициране на инструкцията на JVM, последвана от нула или повече операнди с инструкции, с които да се работи, в зависимост от формата на конкретната инструкция. Това обикновено са или константни стойности, или препратки към константния пул. javap услужливо превежда байт кода в разбираема за човека форма, показваща:

Операндите, които се показват със знак за паунд, като #23, са препратки към записи в константния пул. Както виждаме, javap също така дава полезни коментари в изхода, идентифицирайки какво точно се препраща от пула.

Ще обсъдим няколко от често срещаните инструкции по-долу. За подробна информация относно пълния набор от инструкции на JVM вижте документация .

Метод повиквания и стек повиквания

Всяко извикване на метод трябва да може да се изпълнява със собствен контекст, който включва неща като локално декларирани променливи или аргументи, предадени на метода. Заедно те съставляват a стекова рамка . При извикване на метод се създава нов кадър и се поставя върху стек повиквания . Когато методът се върне, текущият кадър се премахва от стека на повикванията и се изхвърля, а кадърът, който е бил в сила преди извикването на метода, се възстановява.

Рамката на стека включва няколко различни структури. Две важни са стек от операнди и локална променлива таблица , обсъдени по-нататък.

Стека на повикванията на JVM.

Изпълнение на стека на операндите

Много инструкции на JVM работят върху техните рамки стек от операнди . Вместо да посочват изрично постоянен операнд в байт кода, вместо това тези инструкции приемат за вход стойностите в горната част на стека на операндите. Обикновено тези стойности се премахват от стека в процеса. Някои инструкции също поставят нови стойности в горната част на стека. По този начин инструкциите на JVM могат да се комбинират за извършване на сложни операции. Например изразът:

sideLength * this.numSides

се компилира към следното в нашия getPerimeter() метод:

8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul

Инструкциите на JVM могат да работят върху стека на операндите, за да изпълняват сложни функции.

Когато се извика метод, се създава нов стек на операнд като част от неговата рамка на стека, където ще се извършват операции. Трябва да бъдем внимателни с терминологията тук: думата „стек“ може да се отнася до стек повиквания , стекът от рамки, осигуряващи контекст за изпълнение на метод, или към конкретен кадър стек от операнди , при което действат инструкциите на JVM.

Локални променливи

Всяка рамка на стека поддържа таблица на локални променливи . Това обикновено включва препратка към this обект, всички аргументи, които са били предадени при извикване на метода, и всички локални променливи, декларирани в тялото на метода. Изпълнява се javap с -v опцията ще включва информация за това как трябва да се настрои рамката на стека на всеки метод, включително неговата локална таблица на променливите:

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

В този пример има две локални променливи. Променливата в слот 0 се нарича this, с типа RegularPolygon. Това е препратката към собствения клас на метода. Променливата в слот 1 се нарича sideLength, с типа D (посочва двойно). Това е аргументът, който се предава на нашите getPerimeter() метод.

Инструкции като iload_1, fstore_2 или aload [n], прехвърлят различни видове локални променливи между стека на операндите и таблицата с локални променливи. Тъй като първият елемент в таблицата обикновено е препратката към this, инструкцията aload_0 се среща често при всеки метод, който оперира със собствен клас.

С това завършваме нашето разглеждане на основите на JVM. Щракнете тук, за да се върнете към основната статия.

до 1, така че следващия път m4() се нарича, той ще пропусне извикването на метода за инициализация и вместо това просто ще върне стойността на m4

Резултатите от първото извикване на Scala мързелива стойност.

Байтовият код, който Scala произвежда тук, е проектиран да бъде едновременно безопасен и ефективен. За да бъде безопасен за нишки, методът на мързеливи изчисления използва monitorenter / monitorexit чифт инструкции. Методът остава ефективен, тъй като режийните разходи за тази синхронизация се появяват само при първото четене на мързеливата стойност.

За да се посочи състоянието на мързеливата стойност е необходим само един бит. Така че, ако няма повече от 32 мързеливи стойности, едно поле int може да ги проследи всички. Ако в изходния код е дефинирана повече от една мързелива стойност, горният байт код ще бъде модифициран от компилатора, за да приложи битова маска за тази цел.

Отново Scala ни позволява лесно да се възползваме от специфичен вид поведение, което би трябвало да бъде внедрено изрично в Java, спестявайки усилия и намалявайки риска от печатни грешки.

Функция като стойност

Сега нека да разгледаме следния код на Scala:

class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }

Printer class има едно поле, output, с типа String => Unit: функция, която приема String и връща обект от тип Unit (подобно на void в Java). В основния метод създаваме един от тези обекти и задаваме това поле като анонимна функция, която отпечатва даден низ.

Компилирането на този код генерира четири класа файлове:

Изходният код се компилира в четири класа файлове.

Hello.class е клас на обвивка, чийто основен метод просто извиква Hello$.main():

колко голяма е музикалната индустрия
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

Скритото Hello$.class съдържа реалното изпълнение на основния метод. За да разгледате байт кода му, уверете се, че сте избягали правилно $ според правилата на вашата командна обвивка, за да избегнете нейното тълкуване като специален знак:

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 7: dup 8: invokespecial #19 // Method Hello$$anonfun.'':()V 11: invokespecial #22 // Method Printer.'':(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

Методът създава Printer. След това създава Hello$$anonfun, която съдържа нашата анонимна функция s => println(s). Printer се инициализира с този обект като output поле. След това това поле се зарежда в стека и се изпълнява с операнда 'Hello'.

Нека да разгледаме анонимния функционален клас, Hello$$anonfun.class, по-долу. Виждаме, че разширява Scala’s Function1 (като AbstractFunction1) чрез прилагане на apply() метод. Всъщност той създава две apply() методи, едната обвива другата, които заедно извършват проверка на типа (в този случай, че входът е String) и изпълняват анонимната функция (отпечатване на входа с println()).

public final class Hello$$anonfun extends scala.runtime.AbstractFunction1 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

Поглеждайки назад към Hello$.main() метод по-горе, можем да видим, че при отместване 21, изпълнението на анонимната функция се задейства от извикване на нейния apply( Object ) метод.

И накрая, за пълнота, нека разгледаме байт кода за Printer.class:

public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return

Виждаме, че анонимната функция тук се третира точно като всяка val променлива. Той се съхранява в полето на класа output и гетерът output() е създаден. Единствената разлика е, че тази променлива вече трябва да реализира интерфейса Scala scala.Function1 (което AbstractFunction1 прави).

И така, цената на тази елегантна функция Scala са основните класове помощни програми, създадени да представят и изпълнят една анонимна функция, която може да се използва като стойност. Трябва да вземете предвид броя на тези функции, както и подробности за вашата реализация на VM, за да разберете какво означава това за вашето конкретно приложение.

Преминаване под капака със Scala: Разгледайте как този мощен език е реализиран в JVM байт код. Tweet

Скала черти

Характеристиките на Scala са подобни на интерфейсите в Java. Следващата характеристика дефинира два метода и осигурява изпълнение по подразбиране на втория. Нека да видим как се прилага:

trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

Изходният код се компилира в два класа файлове.

Произвеждат се две обекти: Similarity.class, интерфейсът, деклариращ двата метода, и синтетичният клас, Similarity$class.class, осигуряващ изпълнението по подразбиране:

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

Когато клас изпълнява тази черта и извиква метода isNotSimilar, компилаторът Scala генерира инструкция за байт код invokestatic да извика статичния метод, предоставен от придружаващия клас.

Сложните полиморфизъм и наследствени структури могат да бъдат създадени от черти. Например, множество черти, както и класът на внедряване, могат всички да заменят метод с един и същ подпис, извикващ super.methodName() за да предадете контрола на следващата черта. Когато компилаторът Scala срещне такива повиквания, той:

По този начин можем да видим, че мощната концепция за чертите е реализирана на ниво JVM по начин, който не води до значителни режийни разходи, и програмистите на Scala могат да се насладят на тази функция, без да се притесняват, че тя ще бъде твърде скъпа по време на изпълнение.

Единични

Scala предоставя изричната дефиниция на единични класове, използвайки ключовата дума object Нека разгледаме следния сингъл клас:

object Config { val home_dir = '/home/user' }

Компилаторът създава два класа файлове:

Изходният код се компилира в два класа файлове.

Config.class е доста проста:

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

Това е просто декоратор за синтетичния Config$ клас, който вгражда функционалността на синглона. Изследване на този клас с javap -p -c произвежда следния байт код:

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 '':()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.'':()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

Състои се от следното:

Сингълтънът е популярен и полезен модел на дизайн. Езикът Java не осигурява директен начин за неговото определяне на езиково ниво; по-скоро отговорността на разработчика е да го внедри в Java source. Scala, от друга страна, предоставя ясен и удобен начин за деклариране на сингълтон изрично с помощта на object ключова дума. Както виждаме, гледайки под капака, той е изпълнен по достъпен и естествен начин.

Заключение

Сега видяхме как Scala компилира няколко неявни и функционални функции за програмиране в сложни Java байтови структури. С този поглед към вътрешната работа на Scala, ние можем да разберем по-задълбочено силата на Scala, помагайки ни да се възползваме максимално от този мощен език.

Вече разполагаме и с инструментите, за да изследваме самия език. Има много полезни функции на синтаксиса на Scala, които не са обхванати в тази статия, като класове на случаи, currying и разбиране на списъци. Препоръчвам ви сами да проучите изпълнението на Scala от тези структури, за да можете да научите как да бъдете нинджа от следващо ниво Scala!


Виртуалната машина Java: Crash Course

Подобно на Java компилатора, Scala компилаторът преобразува изходния код в .class файлове, съдържащи Java байт код, който да бъде изпълнен от Java Virtual Machine. За да се разбере как двата езика се различават под капака, е необходимо да се разбере системата, към която и двамата са насочени. Тук представяме кратък преглед на някои основни елементи от архитектурата на Java Virtual Machine, файловата структура на класа и основите на асемблера.

Имайте предвид, че това ръководство ще обхваща само минимума, който да позволи последващо, заедно с горната статия. Въпреки че много основни компоненти на JVM не са обсъдени тук, пълни подробности можете да намерите в официалните документи, тук .

Декомпилиране на файлове от клас с javap
Постоянен басейн
Таблици за полета и методи
JVM байт код
Метод повиквания и стек повиквания
Изпълнение на стека на операндите
Локални променливи
Върнете се в началото

Декомпилиране на файлове от клас с javap

Java се доставя с javap помощна програма за команден ред, която декомпилира .class файлове в разбираема за човека форма. Тъй като Scala и Java класните файлове и двата са насочени към една и съща JVM, javap може да се използва за изследване на файлове с класове, съставени от Scala.

Нека компилираме следния изходен код:

// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }

Компилиране на това с scalac RegularPolygon.scala ще произведе RegularPolygon.class. Ако тогава стартираме javap RegularPolygon.class ще видим следното:

$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Това е много проста разбивка на файла на класа, която просто показва имената и типовете публични членове на класа. Добавяне на -p опцията ще включва частни членове:

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

Това все още не е много информация. За да видим как методите са внедрени в байтовия код на Java, нека добавим -c опция:

$ 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.'':()V 9: return }

Това е малко по-интересно. За да разберем цялата история обаче, трябва да използваме -v или -verbose опция, както в javap -p -v RegularPolygon.class:

Пълното съдържание на файл с клас на Java.

Тук най-накрая виждаме какво всъщност е във файла на класа. Какво означава всичко това? Нека да разгледаме някои от най-важните части.

Постоянен басейн

Цикълът на разработка на приложения C ++ включва етапи на компилация и свързване. Цикълът на разработка за Java прескача явен етап на свързване, тъй като свързването се случва по време на изпълнение. Файлът на класа трябва да поддържа това свързване по време на изпълнение. Това означава, че когато изходният код се отнася до което и да е поле или метод, полученият байт код трябва да съхранява съответните препратки в символна форма, готови за дереферентиране, след като приложението се зареди в паметта и действителните адреси могат да бъдат разрешени от свързващия механизъм за изпълнение. Тази символична форма трябва да съдържа:

Спецификацията на файловия формат на класа включва раздел от файла, наречен постоянен басейн , таблица на всички референции, необходими на линкера. Той съдържа записи от различен тип.

// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

Първият байт на всеки запис е цифров маркер, указващ вида на записа. Останалите байтове предоставят информация за стойността на записа. Броят на байтовете и правилата за тяхното тълкуване зависи от вида, посочен от първия байт.

Например Java клас, който използва константно цяло число 365 може да има постоянен запис в пула със следния байт код:

x03 00 00 01 6D

Първият байт, x03, идентифицира вида на записа, CONSTANT_Integer. Това информира линкера, че следващите четири байта съдържат стойността на цялото число. (Обърнете внимание, че 365 в шестнадесетичен е x16D). Ако това е 14-и запис в константния пул, javap -v ще го направи по този начин:

#14 = Integer 365

Много константни типове са съставени от препратки към по-„примитивни“ константни типове другаде в константния пул. Например нашият примерен код съдържа изявлението:

println( 'Calculating perimeter...' )

Използването на низова константа ще доведе до два записа в пула от константи: един запис с тип CONSTANT_String , и друг запис от тип CONSTANT_Utf8. Въвеждането на тип Constant_UTF8 съдържа действителното представяне на UTF8 на низовата стойност. Въвеждането на тип CONSTANT_String съдържа препратка към CONSTANT_Utf8 запис:

#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

Такова усложнение е необходимо, тъй като има други видове постоянни записи в пула, които се отнасят до записи от тип Utf8 и които не са записи от тип String. Например всяко позоваване на атрибут на клас ще създаде CONSTANT_Fieldref тип, който съдържа серия препратки към името на класа, името на атрибута и типа на атрибута:

#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

За повече подробности относно постоянния пул вижте документацията на JVM .

Таблици за полета и методи

Файлът на класа съдържа полева маса който съдържа информация за всяко поле (т.е. атрибут), дефинирано в класа. Това са препратки към записи в константния пул, които описват името и типа на полето, както и знамена за контрол на достъпа и други съответни данни.

Подобен таблица на методите присъства във файла на класа. Въпреки това, освен информация за име и тип, за всеки абстрактен метод той съдържа действителните инструкции за байт код, които трябва да бъдат изпълнени от JVM, както и структури от данни, използвани от стека на метода, описан по-долу.

JVM байт код

JVM използва собствен вътрешен набор от инструкции за изпълнение на компилиран код. Изпълнява се javap с -c Опцията включва компилираните реализации на метода в изхода. Ако разгледаме нашите RegularPolygon.class файл по този начин, ще видим следния изход за нашите getPerimeter() метод:

node.js е труден за научаване
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

Действителният байт код може да изглежда по следния начин:

xB2 00 17 x12 19 xB6 00 1D x27 ...

Всяка инструкция започва с еднобайтово opcode идентифициране на инструкцията на JVM, последвана от нула или повече операнди с инструкции, с които да се работи, в зависимост от формата на конкретната инструкция. Това обикновено са или константни стойности, или препратки към константния пул. javap услужливо превежда байт кода в разбираема за човека форма, показваща:

Операндите, които се показват със знак за паунд, като #23, са препратки към записи в константния пул. Както виждаме, javap също така дава полезни коментари в изхода, идентифицирайки какво точно се препраща от пула.

Ще обсъдим няколко от често срещаните инструкции по-долу. За подробна информация относно пълния набор от инструкции на JVM вижте документация .

Метод повиквания и стек повиквания

Всяко извикване на метод трябва да може да се изпълнява със собствен контекст, който включва неща като локално декларирани променливи или аргументи, предадени на метода. Заедно те съставляват a стекова рамка . При извикване на метод се създава нов кадър и се поставя върху стек повиквания . Когато методът се върне, текущият кадър се премахва от стека на повикванията и се изхвърля, а кадърът, който е бил в сила преди извикването на метода, се възстановява.

Рамката на стека включва няколко различни структури. Две важни са стек от операнди и локална променлива таблица , обсъдени по-нататък.

Стека на повикванията на JVM.

Изпълнение на стека на операндите

Много инструкции на JVM работят върху техните рамки стек от операнди . Вместо да посочват изрично постоянен операнд в байт кода, вместо това тези инструкции приемат за вход стойностите в горната част на стека на операндите. Обикновено тези стойности се премахват от стека в процеса. Някои инструкции също поставят нови стойности в горната част на стека. По този начин инструкциите на JVM могат да се комбинират за извършване на сложни операции. Например изразът:

sideLength * this.numSides

се компилира към следното в нашия getPerimeter() метод:

8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul

Инструкциите на JVM могат да работят върху стека на операндите, за да изпълняват сложни функции.

Когато се извика метод, се създава нов стек на операнд като част от неговата рамка на стека, където ще се извършват операции. Трябва да бъдем внимателни с терминологията тук: думата „стек“ може да се отнася до стек повиквания , стекът от рамки, осигуряващи контекст за изпълнение на метод, или към конкретен кадър стек от операнди , при което действат инструкциите на JVM.

Локални променливи

Всяка рамка на стека поддържа таблица на локални променливи . Това обикновено включва препратка към this обект, всички аргументи, които са били предадени при извикване на метода, и всички локални променливи, декларирани в тялото на метода. Изпълнява се javap с -v опцията ще включва информация за това как трябва да се настрои рамката на стека на всеки метод, включително неговата локална таблица на променливите:

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

В този пример има две локални променливи. Променливата в слот 0 се нарича this, с типа RegularPolygon. Това е препратката към собствения клас на метода. Променливата в слот 1 се нарича sideLength, с типа D (посочва двойно). Това е аргументът, който се предава на нашите getPerimeter() метод.

Инструкции като iload_1, fstore_2 или aload [n], прехвърлят различни видове локални променливи между стека на операндите и таблицата с локални променливи. Тъй като първият елемент в таблицата обикновено е препратката към this, инструкцията aload_0 се среща често при всеки метод, който оперира със собствен клас.

С това завършваме нашето разглеждане на основите на JVM. Щракнете тук, за да се върнете към основната статия.