En introduktion til Invoke Dynamic i JVM

1. Oversigt

Invoke Dynamic (også kendt som Indy) var en del af JSR 292, der havde til formål at forbedre JVM-understøttelsen af ​​dynamisk typede sprog. Efter sin første udgivelse i Java 7, blev påkaldt dynamisk opcode bruges ret udstrakt af dynamiske JVM-baserede sprog som JRuby og endda statisk typede sprog som Java.

I denne vejledning vil vi afmystificere påkaldt dynamisk og se hvordan det kanhjælpe biblioteks- og sprogdesignere med at implementere mange former for dynamik.

2. Mød Invoke Dynamic

Lad os starte med en simpel kæde af Stream API-opkald:

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

Først tror vi måske, at Java opretter en anonym indre klasse, der stammer fra Prædikat og derefter videregiver denne instans til filter metode. Men vi ville tage fejl.

2.1. Bytecode

For at kontrollere denne antagelse kan vi kigge på den genererede bytecode:

javap -c -p Hoved // afkortet // klassenavne er forenklet af hensyn til kortheden // for eksempel er Stream faktisk 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: påkaldt dynamisk # 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

På trods af hvad vi troede, der er ingen anonym indre klasse og bestemt giver ingen en instans af en sådan klasse til filter metode.

Overraskende nok påkaldt dynamisk instruktion er på en eller anden måde ansvarlig for at skabe Prædikat eksempel.

2.2. Lambda-specifikke metoder

Derudover genererede Java-kompilatoren også den følgende sjove udseende statiske metode:

privat statisk boolsk lambda $ main $ 0 (java.lang.String); Kode: 0: aload_0 1: invokevirtual # 37 // Method java / lang / String.length :() I 4: iconst_3 5: if_icmple 12 8: iconst_1 9: goto 13 12: iconst_0 13: ireturn

Denne metode tager en Snor som input og udfører derefter følgende trin:

  • Beregning af inputlængden (invokevirtuallængde)
  • Sammenligning af længden med konstanten 3 (if_icmple og ikonst_3)
  • Vender tilbage falsk hvis længden er mindre end eller lig med 3

Interessant nok svarer det faktisk til den lambda, vi sendte til filter metode:

c -> c.længde ()> 3

Så i stedet for en anonym indre klasse opretter Java en særlig statisk metode og på en eller anden måde påberåber den sig via påkaldt dynamisk.

I løbet af denne artikel vil vi se, hvordan denne påkaldelse fungerer internt. Men lad os først definere det problem, der påkaldt dynamisk forsøger at løse.

2.3. Problemet

Før Java 7 havde JVM kun fire metodeopkaldstyper: invokevirtual at kalde normale klassemetoder, invokestatiske at kalde statiske metoder, påkald grænseflade at kalde grænseflademetoder og invokespecial at kalde konstruktører eller private metoder.

På trods af deres forskelle deler alle disse påkaldelser et simpelt træk: De har et par foruddefinerede trin til at gennemføre hvert metodeopkald, og vi kan ikke berige disse trin med vores brugerdefinerede opførsel.

Der er to vigtigste løsninger på denne begrænsning: den ene ved kompileringstid og den anden ved kørselstid. Førstnævnte bruges normalt af sprog som Scala eller Koltin, og sidstnævnte er den valgte løsning til JVM-baserede dynamiske sprog som JRuby.

Runtime-tilgangen er normalt refleksionsbaseret og følgelig ineffektiv.

På den anden side er kompileringstidsløsningen normalt afhængig af kodegenerering på kompileringstidspunktet. Denne tilgang er mere effektiv ved kørsel. Det er dog noget skørt og kan også medføre en langsommere opstartstid, da der er mere bytecode at behandle.

Nu hvor vi har fået en bedre forståelse af problemet, lad os se, hvordan løsningen fungerer internt.

3. Under hætten

påkaldt dynamisk lader os bootstrap metodeopkaldsprocessen på den måde, vi ønsker. Det vil sige, når JVM ser en påkaldt dynamisk opcode for første gang kalder det en speciel metode kendt som bootstrap-metoden til at initialisere indkaldelsesprocessen:

Bootstrap-metoden er et normalt stykke Java-kode, som vi har skrevet for at konfigurere indkaldelsesprocessen. Derfor kan den indeholde enhver logik.

Når bootstrap-metoden er afsluttet normalt, skal den returnere en forekomst af CallSite. Det her CallSite indkapsler følgende oplysninger:

  • En markør til den faktiske logik, som JVM skal udføre. Dette skal repræsenteres som en Metode Håndtag.
  • En betingelse, der repræsenterer gyldigheden af ​​det returnerede CallSite.

Fra nu af, hver gang JVM ser denne særlige opkode igen, vil den springe over den langsomme sti og kalde direkte den underliggende eksekverbare. Desuden vil JVM fortsætte med at springe over den langsomme vej, indtil tilstanden i CallSite ændringer.

I modsætning til Reflection API kan JVM fuldstændig se igennem Metode Håndtags og vil forsøge at optimere dem, dermed bedre ydelse.

3.1. Bootstrap-metodetabel

Lad os se igen på det genererede påkaldt dynamisk bytecode:

14: påkaldt dynamisk # 23, 0 // InvokeDynamic # 0: test :() Ljava / util / function / Predicate;

Dette betyder, at denne særlige instruktion skal kalde den første bootstrap-metode (# 0-del) fra bootstrap-metodetabellen. Det nævner også nogle af argumenterne for at overføre til bootstrap-metoden:

  • Det prøve er den eneste abstrakte metode i Prædikat
  • Det () Ljava / util / funktion / Predikat repræsenterer en metodesignatur i JVM - metoden tager intet som input og returnerer en forekomst af Prædikat interface

For at se tabellen bootstrap-metoden for lambda-eksemplet, skal vi passere -v mulighed for at javap:

javap -c -p -v Main // trunkeret // tilføjede nye linjer for kortfattethed BootstrapMethods: 0: # 55 REF_invokeStatic java / lang / invoke / LambdaMetafactory.metafactory: (Ljava / lang / invoke / MethodHandles $ Lookup; Ljava / lang / String; Ljava / lang / påberåbe sig / MethodType; Ljava / lang / påberåbe sig / MethodType; Ljava / lang / påberåbe sig / MethodHandle; Ljava / lang / påberåbe sig / MethodType;) Ljava / lang / påberåbe sig / CallSite; Metodeargumenter: # 62 (Ljava / lang / Object;) Z # 64 REF_invokeStatic Main.lambda $ main $ 0: (Ljava / lang / String;) Z # 67 (Ljava / lang / String;) Z

Bootstrap-metoden for alle lambdas er metafaktorisk statisk metode i Lambda Metafactory klasse.

I lighed med alle andre bootstrap-metoder tager denne mindst tre argumenter som følger:

  • Det Ljava / lang / påkalde / MethodHandles $ Lookup argument repræsenterer opslagskonteksten for påkaldt dynamisk
  • Det Ljava / lang / String repræsenterer metodenavnet på opkaldsstedet - i dette eksempel er metodens navn prøve
  • Det Ljava / lang / påkalde / MethodType er den dynamiske metodesignatur for opkaldswebstedet - i dette tilfælde er det () Ljava / util / funktion / Predikat

Ud over disse tre argumenter kan bootstrap-metoder også valgfrit acceptere en eller flere ekstra parametre. I dette eksempel er disse de ekstra:

  • Det (Ljava / lang / Objekt;) Z er en slettet metodesignatur, der accepterer en forekomst af Objekt og returnerer en boolsk.
  • Det REF_invokeStatic Main.lambda $ main $ 0: (Ljava / lang / String;) Z er Metode Håndtag peger på den aktuelle lambda-logik.
  • Det (Ljava / lang / String;) Z er en ikke-slettet metodesignatur, der accepterer en Snor og returnerer en boolsk.

Kort sagt, JVM videregiver alle de krævede oplysninger til bootstrap-metoden. Bootstrap-metoden vil igen bruge disse oplysninger til at oprette en passende forekomst af Prædikat. Derefter vil JVM videregive denne instans til filter metode.

3.2. Forskellige typer CallSites

Når JVM ser det påkaldt dynamisk i dette eksempel kalder det første gang bootstrap-metoden. Fra og med at skrive denne artikel, lambda bootstrap-metoden bruger InnerClassLambdaMetafactoryat generere en indre klasse til lambda ved kørsel.

Derefter indkapsler bootstrap-metoden den genererede indre klasse i en speciel type CallSite kendt som ConstantCallSite. Denne type CallSite ville aldrig ændre sig efter installationen. Derfor, efter den første opsætning for hver lambda, bruger JVM altid den hurtige sti til direkte at kalde lambdalogikken.

Selv om dette er den mest effektive type påkaldt dynamisk, det er bestemt ikke den eneste tilgængelige mulighed. Faktisk leverer Java MutableCallSite og VolatileCallSite for at imødekomme mere dynamiske krav.

3.3. Fordele

Så for at implementere lambda-udtryk i stedet for at oprette anonyme indre klasser på kompileringstid opretter Java dem ved kørsel via påkaldt dynamisk.

Man kan argumentere mod udsættelse af den indre klassegeneration indtil runtime. Men den påkaldt dynamisk tilgang har et par fordele i forhold til den enkle kompileringstidsløsning.

For det første genererer JVM ikke den indre klasse før den første brug af lambda. Derfor, vi betaler ikke for det ekstra fodaftryk, der er knyttet til den indre klasse, før den første lambda-udførelse.

Derudover flyttes meget af koblingslogikken ud fra bytekoden til bootstrap-metoden. Derfor, det påkaldt dynamisk bytecode er normalt meget mindre end alternative løsninger. Den mindre bytecode kan øge starthastigheden.

Antag, at en nyere version af Java leveres med en mere effektiv implementering af bootstrap-metoden. Så vores påkaldt dynamisk bytecode kan drage fordel af denne forbedring uden at kompilere igen. På denne måde kan vi opnå en slags videresendelse af binær kompatibilitet. Dybest set kan vi skifte mellem forskellige strategier uden rekompilering.

Endelig er det normalt lettere at skrive bootstrap- og linklogikken i Java end at krydse en AST for at generere et komplekst stykke bytecode. Så, påkaldt dynamisk kan være (subjektivt) mindre skør.

4. Flere eksempler

Lambda-udtryk er ikke den eneste funktion, og Java er bestemt ikke det eneste sprog, der bruger påkaldt dynamisk. I dette afsnit vil vi blive fortrolige med et par andre eksempler på dynamisk påkaldelse.

4.1. Java 14: Records

Records er en ny preview-funktion i Java 14, der giver en flot kortfattet syntaks til at erklære klasser, der formodes at være dumme dataholdere.

Her er et simpelt rekordeksempel:

offentlig post Farve (strengnavn, int-kode) {}

I betragtning af denne enkle one-liner genererer Java compiler passende implementeringer for accessor-metoder, toString, er lig med og hashcode.

For at gennemføre toString, er lig med eller hashcode, Java bruger påkaldt dynamisk. For eksempel bytekoden til lige med er som følgende:

offentlige endelige boolske lig (java.lang.Object); Kode: 0: aload_0 1: aload_1 2: påkaldt dynamisk # 27, 0 // InvokeDynamic # 0: er lig med: (LColor; Ljava / lang / Object;) Z 7: ireturn

Den alternative løsning er at finde alle postfelter og generere lige med logik baseret på disse felter på kompileringstidspunktet. Jo mere vi har felter, jo længerevarende bytekode.

Tværtimod kalder Java en bootstrap-metode til at linke den relevante implementering ved kørsel. Derfor, bytekodelængden vil forblive konstant uanset antallet af felter.

At se nærmere på bytecode viser, at metoden bootstrap er 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; Metode argumenter: # 8 Farve # 49 navn; kode # 51 REF_getField Color.name:Ljava/lang/String; # 52 REF_getField Color.code: I

4.2. Java 9: ​​String-sammenkædning

Forud for Java 9 blev ikke-trivielle strengkombinationer implementeret ved hjælp af StringBuilder. Som en del af JEP 280 bruger strengstrengskædning nu påkaldt dynamisk. Lad os for eksempel sammenkæde en konstant streng med en tilfældig variabel:

"tilfældig-" + ThreadLocalRandom.current (). nextInt ();

Sådan ser bytecode ud for dette eksempel:

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

Desuden findes bootstrap-metoderne til strengkombinationer i StringConcatFactory klasse:

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 / Object;) Ljava / lang / invoke / CallSite; Metode argumenter: # 36 tilfældig- \ u0001

5. Konklusion

I denne artikel blev vi først fortrolig med de problemer, som indy forsøger at løse.

Derefter, ved at gå gennem et simpelt lambdaekspressionseksempel, så vi hvordan påkaldt dynamisk arbejder internt.

Endelig opregnede vi et par andre eksempler på indy i nyere versioner af Java.