Uvod u Invoke Dynamic u JVM-u

1. Pregled

Invoke Dynamic (poznat i kao Indy) bio je dio JSR 292 namijenjen poboljšanju JVM podrške za dinamički upisane jezike. Nakon svog prvog izdanja u Javi 7, invokedynamic opcode prilično opsežno koriste dinamični jezici temeljeni na JVM-u poput JRuby, pa čak i statički upisani jezici poput Java.

U ovom uputstvu ćemo demistificirati invokedynamic i vidi kako možepomoći dizajnerima knjižnica i jezika da implementiraju mnoge oblike dinamičnosti.

2. Upoznajte Invoke Dynamic

Počnimo s jednostavnim lancem poziva API-ja Stream:

javna klasa Main {javna statička praznina main (String [] args) {long lengthyColors = List.of ("Red", "Green", "Blue") .stream (). filter (c -> c.length ()> 3) .count (); }}

U početku bismo mogli pomisliti da Java stvara anonimnu unutarnju klasu koja potječe iz Predikat a zatim prosljeđuje tu instancu na filtar metoda. Ali, pogriješili bismo.

2.1. Bytecode

Da bismo provjerili ovu pretpostavku, možemo zaviriti u generirani bytecode:

javap -c -p Glavna // skraćeni // nazivi klasa pojednostavljeni su radi kratkoće // na primjer, Stream je zapravo java / util / stream / Stream 0: ldc # 7 // String Red 2: ldc # 9 / / String Green 4: ldc # 11 // String Blue 6: invokestatic # 13 // InterfaceMethod List.of: (LObject; LObject;) LList; 9: invokeinterface # 19, 1 // InterfaceMethod List.stream:()LStream; 14: invokedynamic # 23, 0 // InvokeDynamic # 0: test :() LPredicate; 19: invokeinterface # 27, 2 // InterfaceMethod Stream.filter: (LPredicate;) LStream; 24: invokeinterface # 33, 1 // InterfaceMethod Stream.count :() J 29: lstore_1 30: return

Unatoč onome što smo mislili, ne postoji anonimna unutarnja klasa i sigurno, nitko ne prosljeđuje primjerak takve klase filtar metoda.

Iznenađujuće, invokedynamic Uputa je nekako odgovorna za stvaranje Predikat primjer.

2.2. Lambda specifične metode

Pored toga, Java kompajler je također generirao sljedeću statičnu metodu smiješnog izgleda:

privatna statička logička lambda $ main $ 0 (java.lang.String); Šifra: 0: aload_0 1: invokevirtual # 37 // Metoda java / lang / String.length :() I 4: iconst_3 5: if_icmple 12 8: iconst_1 9: goto 13 12: iconst_0 13: ireturn

Ova metoda zahtijeva Niz kao ulaz, a zatim izvodi sljedeće korake:

  • Izračunavanje ulazne duljine (invokevirtual na duljina)
  • Uspoređujući duljinu s konstantom 3 (if_icmple i iconst_3)
  • Povratak lažno ako je duljina manja ili jednaka 3

Zanimljivo je da je ovo zapravo ekvivalent lambdi kojoj smo prešli filtar metoda:

c -> c.length ()> 3

Dakle, umjesto anonimne unutarnje klase, Java stvara posebnu statičku metodu i nekako je poziva putem invokedynamic.

Tijekom ovog članka vidjet ćemo kako ovaj poziv interno djeluje. Ali, prvo, definirajmo problem koji invokedynamic pokušava riješiti.

2.3. Problem

Prije Jave 7, JVM je imao samo četiri vrste poziva metode: prizivajuvirtualni nazvati metode normalne klase, invokestatski pozvati statičke metode, invokeinterface za pozivanje metoda sučelja i invokespecial za pozivanje konstruktora ili privatnih metoda.

Unatoč razlikama, sve ove pozive dijeli jedno jednostavno svojstvo: Oni imaju nekoliko unaprijed definiranih koraka za dovršetak svakog poziva metode, a mi te korake ne možemo obogatiti svojim prilagođenim ponašanjem.

Postoje dva glavna zaobilazna rješenja za ovo ograničenje: jedno za vrijeme sastavljanja, a drugo za vrijeme izvođenja. Prvi se obično koristi u jezicima poput Scale ili Koltina, a drugi je rješenje izbora za dinamičke jezike temeljene na JVM poput JRuby.

Pristup izvođenja obično se temelji na refleksiji i posljedično je neučinkovit.

S druge strane, rješenje vremena prevođenja obično se oslanja na generiranje koda u vrijeme prevođenja. Ovaj je pristup učinkovitiji za vrijeme izvođenja. Međutim, donekle je lomljiv i može uzrokovati sporije vrijeme pokretanja jer se obrađuje više bajtkoda.

Sad kad smo bolje razumjeli problem, pogledajmo kako rješenje interno djeluje.

3. Ispod haube

invokedynamic omogućuje nam pokretanje postupka pozivanja metode na bilo koji način koji želimo. Odnosno, kada JVM vidi invokedynamic opcode prvi put poziva posebnu metodu poznatu kao bootstrap metoda za inicijalizaciju procesa pozivanja:

Metoda bootstrap normalni je dio Java koda koji smo napisali za postavljanje postupka pozivanja. Stoga može sadržavati bilo kakvu logiku.

Jednom kada se bootstrap metoda dovrši normalno, trebala bi vratiti primjerak CallSite. Ovaj CallSite sadrži sljedeće podatke:

  • Pokazivač na stvarnu logiku koju bi JVM trebao izvršiti. Ovo bi trebalo predstaviti kao MethodHandle.
  • Uvjet koji predstavlja valjanost vraćenog CallSite.

Od sada, svaki put kad JVM ponovo vidi ovaj određeni opcode, preskočit će polagani put i izravno pozvati temeljnu izvršnu datoteku. Štoviše, JVM će i dalje preskakati spor put do stanja u CallSite promjene.

Za razliku od Reflection API-ja, JVM može u potpunosti progledati MethodHandles i pokušat će ih optimizirati, a time i bolje performanse.

3.1. Tablica Bootstrap metode

Pogledajmo još jednom generirano invokedynamic bytecode:

14: invokedynamic # 23, 0 // InvokeDynamic # 0: test :() Ljava / util / function / Predicate;

To znači da bi ova posebna uputa trebala pozvati prvu metodu bootstrap (# 0 dio) iz tablice metode bootstrap. Također, spominje neke od argumenata koje treba proslijediti bootstrap metodi:

  • The test je jedina apstraktna metoda u Predikat
  • The () Ljava / util / function / Predicate predstavlja potpis metode u JVM - metoda ne uzima ništa kao ulaz i vraća instancu datoteke Predikat sučelje

Da bismo vidjeli tablicu metode bootstrap za lambda primjer, trebali bismo proći -v mogućnost da javap:

javap -c -p -v Glavna // skraćena // dodani su novi redovi za kratkoću BootstrapMethods: 0: # 55 REF_invokeStatic java / lang / invoke / LambdaMetafactory.metafactory: (Ljava / lang / invoke / MethodHandles $ Lookup; Ljava / lang / Niz; Ljava / lang / invoke / MethodType; Ljava / lang / invoke / MethodType; Ljava / lang / invoke / MethodHandle; Ljava / lang / invoke / MethodType;) Ljava / lang / invoke / CallSite; Argumenti metode: # 62 (Ljava / lang / Object;) Z # 64 REF_invokeStatic Main.lambda $ main $ 0: (Ljava / lang / String;) Z # 67 (Ljava / lang / String;) Z

Način bootstrapa za sve lambde je metafabrika statička metoda u LambdaMetafactory razred.

Slično svim ostalim metodama bootstrapa, i ovaj uzima najmanje tri argumenta kako slijedi:

  • The Ljava / lang / invoke / MethodHandles $ Lookup argument predstavlja kontekst pretraživanja za invokedynamic
  • The Ljava / lang / String predstavlja ime metode na mjestu poziva - u ovom primjeru naziv metode je test
  • The Ljava / lang / invoke / MethodType je dinamička metoda potpisa mjesta poziva - u ovom slučaju je () Ljava / util / function / Predicate

Uz ova tri argumenta, metode bootstrapa mogu po želji prihvatiti jedan ili više dodatnih parametara. U ovom su primjeru ovo dodatni:

  • The (Ljava / lang / Object;) Z je izbrisani potpis metode koji prihvaća instancu Objekt i vraćanje a boolean.
  • The REF_invokeStatic Main.lambda $ main $ 0: (Ljava / lang / String;) Z je MethodHandle ukazujući na stvarnu lambda logiku.
  • The (Ljava / lang / String;) Z je neizbrisani potpis metode koji prihvaća jedan Niz i vraćanje a boolean.

Pojednostavljeno, JVM će proslijediti sve tražene podatke metodi bootstrapa. Bootstrap metoda će, pak, koristiti te podatke za stvaranje odgovarajuće instance Predikat. Tada će JVM proslijediti tu instancu na filtar metoda.

3.2. Različite vrste CallSites

Jednom kad JVM vidi invokedynamic u ovom primjeru prvi put poziva metodu bootstrap. Od pisanja ovog članka, metoda lambda bootstrap će koristiti InnerClassLambdaMetafactoryza generiranje unutarnje klase za lambda tijekom izvođenja.

Tada metoda bootstrap enkapsulira generiranu unutarnju klasu unutar posebne vrste CallSite poznat kao ConstantCallSite. Ova vrsta CallSite nikad se ne bi promijenio nakon postavljanja. Stoga će nakon prvog postavljanja za svaku lambda-u JVM uvijek koristiti brzi put za izravno pozivanje lambda-logike.

Iako je ovo najučinkovitiji tip invokedynamic, to sigurno nije jedina dostupna opcija. Zapravo, Java pruža MutableCallSite i VolatileCallSite prilagoditi se dinamičnijim zahtjevima.

3.3. Prednosti

Dakle, kako bi implementirala lambda izraze, umjesto stvaranja anonimnih unutarnjih klasa u vrijeme prevođenja, Java ih stvara u vrijeme izvođenja putem invokedynamic.

Moglo bi se raspravljati protiv odgađanja generacije unutarnje klase do vremena izvođenja. Međutim invokedynamic pristup ima nekoliko prednosti u odnosu na jednostavno rješenje vremena kompajliranja.

Prvo, JVM ne generira unutarnju klasu do prve upotrebe lambde. Stoga, nećemo platiti za dodatni otisak povezan s unutarnjom klasom prije prvog izvođenja lambde.

Uz to, velik dio logike povezivanja premješta se iz bajt-koda u metodu bootstrapa. Stoga, the invokedynamic bytecode je obično mnogo manji od alternativnih rješenja. Manji bajtkod može povećati brzinu pokretanja.

Pretpostavimo da novija verzija Jave dolazi s učinkovitijom implementacijom metode bootstrap. Zatim naš invokedynamic bytecode može iskoristiti ovo poboljšanje bez ponovnog prevođenja. Na taj način možemo postići neku vrstu prosljeđivanja binarne kompatibilnosti. U osnovi, možemo se prebacivati ​​između različitih strategija bez ponovne kompilacije.

Konačno, pisanje logike pokretanja i povezivanja na Javi obično je lakše nego prelazak AST-a za generiranje složenog dijela bajt-koda. Tako, invokedynamic može biti (subjektivno) manje lomljiv.

4. Još primjera

Lambda izrazi nisu jedina značajka i Java zasigurno nije jedini jezik koji se koristi invokedynamic. U ovom ćemo odjeljku upoznati nekoliko drugih primjera dinamičkog pozivanja.

4.1. Java 14: Zapisi

Zapisi su nova značajka pregleda u Javi 14 koja pruža lijepu sažetu sintaksu za deklariranje klasa koje bi trebale biti nijemi nositelji podataka.

Evo jednostavnog primjera zapisa:

boja javnog zapisa (naziv niza, int kôd) {}

S obzirom na ovu jednostavnu liniju, Java prevodilac generira odgovarajuće implementacije za pristupne metode, toString, jednako, i hashcode.

U svrhu provedbe toString, jednako, ili hashcode, Java koristi invokedynamic. Na primjer, bajtkod za jednako je kako slijedi:

javna konačna logička vrijednost jednako (java.lang.Object); Šifra: 0: aload_0 1: aload_1 2: invokedynamic # 27, 0 // InvokeDynamic # 0: jednako: (LColor; Ljava / lang / Object;) Z 7: ireturn

Alternativno rješenje je pronaći sva polja zapisa i generirati jednako logika koja se temelji na tim poljima u vrijeme prevođenja. Što više imamo polja, duži je bajt kod.

Suprotno tome, Java poziva bootstrap metodu kako bi povezala odgovarajuću implementaciju u vrijeme izvođenja. Stoga, duljina bajtkoda ostala bi konstantna bez obzira na broj polja.

Pomnije proučavanje bajtkoda pokazuje da je metoda bootstrapa ObjectMethods # bootstrap:

BootstrapMethods: 0: # 42 REF_invokeStatic java / lang / runtime / ObjectMethods.bootstrap: (Ljava / lang / invoke / MethodHandles $ Lookup; Ljava / lang / String; Ljava / lang / invoke / TypeDescriptor; Ljava / lang / Class; Ljava / lang / String; [Ljava / lang / invoke / MethodHandle;) Ljava / lang / Object; Argumenti metode: # 8 Boja # 49 ime; kod # 51 REF_getField Boja.ime: Ljava/lang/String; # 52 REF_getField Color.code: I

4.2. Java 9: ​​Spajanje nizova

Prije Java 9, provodile su se netrivijalne kokatenacije nizova StringBuilder. Kao dio JEP 280, sada se koristi spajanje nizova invokedynamic. Na primjer, spojimo konstantni niz sa slučajnom varijablom:

"random-" + ThreadLocalRandom.current (). nextInt ();

Evo kako izgleda bajtkod za ovaj primjer:

0: invokestatic # 7 // Method ThreadLocalRandom.current :() LThreadLocalRandom; 3: invokevirtual # 13 // Method ThreadLocalRandom.nextInt :() I 6: invokedynamic # 17, 0 // InvokeDynamic # 0: makeConcatWithConstants: (I) LString;

Štoviše, metode bootstrapa za spajanje nizova nalaze se u StringConcatFactory razred:

BootstrapMethods: 0: # 30 REF_invokeStatic java / lang / invoke / StringConcatFactory.makeConcatWithConstants: (Ljava / lang / invoke / MethodHandles $ Lookup; Ljava / lang / String; Ljava / lang / invoke / MethodType; [Ljava / lang / String; / lang / Object;) Ljava / lang / invoke / CallSite; Argumenti metode: # 36 random- \ u0001

5. Zaključak

U ovom smo se članku prvo upoznali s problemima koje Indy pokušava riješiti.

Zatim smo, prolazeći kroz jednostavan primjer lambda izraza, vidjeli kako invokedynamic radi interno.

Konačno, nabrojali smo još nekoliko primjera indyja u novijim verzijama Jave.


$config[zx-auto] not found$config[zx-overlay] not found