Úvod do vyvolania dynamiky v JVM

1. Prehľad

Invoke Dynamic (tiež známy ako Indy) bol súčasťou JSR 292 zameraného na zlepšenie podpory JVM pre dynamicky písané jazyky. Po prvom vydaní v prostredí Java 7 začal invokedynamický operačný kód je pomerne často používaný dynamickými jazykmi založenými na JVM, ako sú JRuby, a dokonca aj staticky napísanými jazykmi, ako je Java.

V tomto návode sa chystáme demystifikovať invokedynamický a uvidíme, ako to bude možnépomoc návrhárom knižníc a jazykov pri implementácii mnohých foriem dynamiky.

2. Zoznámte sa s Invoke Dynamic

Začnime s jednoduchým reťazcom volaní Stream API:

public class Main {public static void main (String [] args) {long lengthyColors = List.of ("Red", "Green", "Blue") .stream (). filter (c -> c.length ()> 3) .počet (); }}

Spočiatku by sme si mohli myslieť, že Java vytvára anonymnú vnútornú triedu odvodenú z Predikát a potom túto inštanciu odovzdá filter metóda. Ale mýlili by sme sa.

2.1. Bytecode

Aby sme skontrolovali tento predpoklad, môžeme nahliadnuť do vygenerovaného bytecode:

javap -c -p Main // skrátené // názvy tried sú zjednodušené kvôli stručnosti //, napríklad Stream je vlastne 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: návrat

Napriek tomu, čo sme si mysleli, neexistuje anonymná vnútorná trieda a určite nikto neprenáša inštanciu takejto triedy na filter metóda.

Prekvapivo invokedynamický inštrukcia je nejako zodpovedná za vytvorenie Predikát inštancia.

2.2. Metódy špecifické pre lambdu

Ďalej kompilátor Java vygeneroval aj nasledujúcu vtipne vyzerajúcu statickú metódu:

private static boolean lambda $ main $ 0 (java.lang.String); Kód: 0: aload_0 1: invokevirtual # 37 // Metóda java / lang / String.length :() I 4: iconst_3 5: if_icmple 12 8: iconst_1 9: goto 13 12: iconst_0 13: ireturn

Táto metóda vyžaduje a String ako vstup a potom vykoná nasledujúce kroky:

  • Výpočet vstupnej dĺžky (invokevirual na dĺžka)
  • Porovnaním dĺžky s konštantou 3 (if_icmple a iconst_3)
  • Vracia sa nepravdivé ak je dĺžka menšia alebo rovná 3

Je zaujímavé, že toto je vlastne ekvivalent lambdy, ku ktorej sme prešli filter metóda:

c -> c.length ()> 3

Takže namiesto anonymnej vnútornej triedy vytvorí Java špeciálnu statickú metódu a nejako ju vyvolá pomocou invokedynamický.

V priebehu tohto článku uvidíme, ako toto vyvolanie interne funguje. Najprv si však definujme problém invokedynamický sa snaží vyriešiť.

2.3. Problém

Pred jazykom Java 7 mal JVM iba štyri typy vyvolávania metód: invokevirtual volať metódy bežnej triedy, invokestatický volať statické metódy, vyvolaťrozhranie - volať metódy rozhrania a - invokálne špeciálne volať konštruktérov alebo súkromné ​​metódy.

Napriek svojim rozdielom všetky tieto vyvolania zdieľajú jednu jednoduchú vlastnosť: Majú niekoľko preddefinovaných krokov na dokončenie každého volania metódy a tieto kroky nemôžeme obohatiť o naše vlastné správanie.

Existujú dve hlavné riešenia tohto obmedzenia: Jedno v čase kompilácie a druhé v čase behu. Prvý z nich zvyčajne používajú jazyky ako Scala alebo Koltin a druhý je riešením voľby pre dynamické jazyky založené na JVM, ako sú JRuby.

Runtime prístup je zvyčajne založený na reflexii a následne neefektívny.

Na druhej strane sa riešenie kompilácie zvyčajne spolieha na generovanie kódu v čase kompilácie. Tento prístup je za behu efektívnejší. Je to však trochu krehké a môže to tiež spôsobiť pomalšie spustenie, pretože je potrebné spracovať viac bajtových kódov.

Teraz, keď sme problému lepšie porozumeli, pozrime sa, ako riešenie funguje interne.

3. Pod kapotou

invokedynamický umožňuje nám zaviesť proces vyvolania metódy ľubovoľným spôsobom. To znamená, keď JVM uvidí invokedynamický operačný kód prvýkrát volá špeciálnu metódu známu ako metóda bootstrap na inicializáciu procesu vyvolania:

Metóda bootstrap je normálny kúsok kódu Java, ktorý sme napísali na nastavenie procesu vyvolania. Preto môže obsahovať akúkoľvek logiku.

Keď sa metóda bootstrapu dokončí normálne, mala by vrátiť inštanciu CallSite. Toto CallSite obsahuje nasledujúce informácie:

  • Ukazovateľ na skutočnú logiku, ktorú by malo JVM vykonať. Toto by malo byť reprezentované ako a MethodHandle.
  • Podmienka predstavujúca platnosť vráteného CallSite.

Odteraz vždy, keď JVM znova uvidí tento konkrétny operačný kód, preskočí pomalú cestu a priamo zavolá spustiteľný súbor. Okrem toho bude JVM naďalej preskakovať pomalú cestu, kým nenastane stav v CallSite zmeny.

Na rozdiel od rozhrania Reflection API môže JVM úplne vidieť MethodHandles a pokúsi sa ich optimalizovať, tým pádom lepší výkon.

3.1. Tabuľka metód Bootstrap

Poďme sa ešte raz pozrieť na vygenerované invokedynamický bytecode:

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

To znamená, že táto konkrétna inštrukcia by mala zavolať prvú metódu bootstrap (časť # 0) z tabuľky metód bootstrap. Uvádza tiež niektoré argumenty, ktoré sa majú odovzdať metóde bootstrap:

  • The test je jedinou abstraktnou metódou v systéme Windows Predikát
  • The () Ljava / util / function / Predicate predstavuje podpis metódy v JVM - metóda neberie nič ako vstup a vracia inštanciu súboru Predikát rozhranie

Aby sme videli tabuľku metód bootstrap pre príklad lambda, mali by sme prejsť -v možnosť javap:

javap -c -p -v hlavný // skrátený // pridané nové riadky pre krátkosť BootstrapMethods: 0: # 55 REF_invokeStatic java / lang / invoke / LambdaMetafactory.metafactory: (Ljava / lang / invoke / MethodHandles $ Lookup; Ljava / lang / Reťazec; Ljava / lang / invoke / MethodType; Ljava / lang / invoke / MethodType; Ljava / lang / invoke / MethodHandle; Ljava / lang / invoke / MethodType;) Ljava / lang / invoke / CallSite; Argumenty metódy: # 62 (Ljava / lang / Object;) Z # 64 REF_invokeStatic Main.lambda $ main $ 0: (Ljava / lang / String;) Z # 67 (Ljava / lang / String;) Z

Metóda bootstrap pre všetky lambdy je metafactory statická metóda v LambdaMetafactory trieda.

Podobne ako všetky ostatné metódy bootstrap, aj tu je potrebných minimálne tri argumenty:

  • The Ljava / lang / invoke / MethodHandles $ Lookup argument predstavuje vyhľadávací kontext pre invokedynamický
  • The Ljava / lang / Reťazec predstavuje názov metódy na stránke volania - v tomto príklade je názov metódy test
  • The Ljava / lang / invoke / MethodType je podpis dynamickej metódy stránky hovoru - v tomto prípade je to () Ljava / util / function / Predicate

Okrem týchto troch argumentov môžu metódy bootstrap voliteľne akceptovať jeden alebo viac ďalších parametrov. V tomto príklade sú to ďalšie:

  • The (Ljava / lang / Objekt;) Z je vymazaný podpis metódy akceptujúci inštanciu Objekt a vrátenie a boolean.
  • The REF_invokeStatic Main.lambda $ main $ 0: (Ljava / lang / String;) Z je MethodHandle ukazuje na skutočnú logiku lambda.
  • The (Ljava / lang / String;) Z je nevymazaný podpis metódy, ktorý akceptuje jeden String a vrátenie a boolean.

Zjednodušene povedané, JVM odovzdá všetky požadované informácie metóde bootstrap. Metóda Bootstrap zase použije tieto informácie na vytvorenie vhodnej inštancie Predikát. Potom JVM túto inštanciu odovzdá filter metóda.

3.2. Rôzne typy CallSites

Raz to JVM uvidí invokedynamický v tomto príklade prvýkrát volá metódu bootstrap. V čase písania tohto článku metóda lambda bootstrap použije InnerClassLambdaMetafactoryvygenerovať vnútornú triedu pre lambdu za behu.

Potom metóda bootstrap zapuzdrí vygenerovanú vnútornú triedu vo vnútri špeciálneho typu CallSite známy ako ConstantCallSite. Tento typ CallSite sa po nastavení nikdy nezmení. Preto po prvom nastavení pre každú lambdu bude JVM vždy používať rýchlu cestu na priame volanie logiky lambda.

Aj keď je to najefektívnejší typ invokedynamic, určite to nie je jediná dostupná možnosť. V skutočnosti to poskytuje Java MutableCallSite a VolatileCallSite vyhovieť dynamickejším požiadavkám.

3.3. Výhody

Aby bolo možné implementovať výrazy lambda, namiesto vytvárania anonymných vnútorných tried v čase kompilácie ich Java vytvára za behu pomocou invokedynamický.

Niekto by mohol namietať proti odloženiu generovania vnútornej triedy na dobu behu. Avšak invokedynamický prístup má oproti jednoduchému kompilačnému riešeniu niekoľko výhod.

Po prvé, JVM negeneruje vnútornú triedu až do prvého použitia lambda. Teda nebudeme platiť za extra stopu spojenú s vnútornou triedou pred prvým vykonaním lambda.

Okrem toho je veľká časť logiky prepojenia presunutá z bytecode do metódy bootstrap. Preto the invokedynamický bytecode je zvyčajne oveľa menší ako alternatívne riešenia. Menší bytecode môže zvýšiť rýchlosť spustenia.

Predpokladajme, že novšia verzia Java prichádza s efektívnejšou implementáciou metódy bootstrap. Potom náš invokedynamický bytecode môže využiť toto vylepšenie bez opätovnej kompilácie. Týmto spôsobom môžeme dosiahnuť určitý druh preposielania binárnej kompatibility. V zásade môžeme prepínať medzi rôznymi stratégiami bez rekompilácie.

Napokon, zápis bootstrapu a logiky prepojenia v Jave je zvyčajne jednoduchšie ako prechod AST, aby sa vygeneroval zložitý bajtkód. Takže invokedynamický môže byť (subjektívne) menej krehký.

4. Ďalšie príklady

Výrazy lambda nie sú jedinou funkciou a Java nie je určite jediným používaným jazykom invokedynamický. V tejto časti sa oboznámime s niekoľkými ďalšími príkladmi dynamického vyvolania.

4.1. Java 14: Záznamy

Záznamy sú novou funkciou ukážky v prostredí Java 14, ktorá poskytuje príjemnú výstižnú syntax na deklaráciu tried, ktoré majú byť hlúpymi držiteľmi údajov.

Tu je jednoduchý príklad záznamu:

verejný záznam Farba (názov reťazca, int kód) {}

Vzhľadom na tento jednoduchý riadok generuje kompilátor Java príslušné implementácie pre prístupové metódy, toString, rovná sa, a hashcode.

Za účelom realizácie toString, rovná sa, alebo hashcode, Java používa invokedynamický. Napríklad bytecode pre rovná sa je nasledujúci:

verejné konečné booleovské rovné (java.lang.Object); Kód: 0: aload_0 1: aload_1 2: invokedynamic # 27, 0 // InvokeDynamic # 0: equals: (LColor; Ljava / lang / Object;) Z 7: ireturn

Alternatívnym riešením je nájsť všetky záznamové polia a vygenerovať rovná sa logika založená na týchto poliach v čase kompilácie. Čím viac máme polí, tým je bajtový kód dlhší.

Naopak, Java volá metódu bootstrap na prepojenie príslušnej implementácie za behu programu. Preto dĺžka bytecode by zostala konštantná bez ohľadu na počet polí.

Podrobnejší pohľad na bytecode ukazuje, že metóda bootstrap je 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; Argumenty metódy: # 8 Farba # 49 názov; kód # 51 REF_getField Názov farby: Ljava/lang/String; # 52 REF_getField Color. Kód: I

4.2. Java 9: ​​Zreťazenie reťazcov

Pred jazykom Java 9 sa netriviálne zreťazenia reťazcov implementovali pomocou StringBuilder. Ako súčasť JEP 280 sa teraz používa zreťazenie reťazcov invokedynamický. Napríklad spojme konštantný reťazec s náhodnou premennou:

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

Takto vyzerá bytecode v tomto príklade:

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

Okrem toho metódy bootstrap pre reťazenie reťazcov spočívajú v serveri StringConcatFactory trieda:

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; Ljava / lang / String; / lang / Object;) Ljava / lang / invoke / CallSite; Argumenty metódy: # 36 náhodne - \ u0001

5. Záver

V tomto článku sme sa najskôr oboznámili s problémami, ktoré sa indy snaží vyriešiť.

Potom sme prešli jednoduchým príkladom výrazu lambda a videli sme, ako na to invokedynamický pracuje interne.

Na záver sme vymenovali niekoľko ďalších príkladov indy v posledných verziách Java.