En introduksjon til Invoke Dynamic i JVM

1. Oversikt

Invoke Dynamic (også kjent som Indy) var en del av JSR 292 som skulle forbedre JVM-støtten for dynamisk skrevne språk. Etter den første utgivelsen i Java 7, ble påkalt dynamisk opcode brukes ganske mye av dynamiske JVM-baserte språk som JRuby og til og med statisk skrevne språk som Java.

I denne opplæringen skal vi avmystifisere påkalt dynamisk og se hvordan det kanhjelpe bibliotek- og språkdesignere med å implementere mange former for dynamikk.

2. Møt Invoke Dynamic

La oss starte med en enkel kjede av Stream API-anrop:

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

Først tror vi kanskje at Java skaper en anonym indre klasse som stammer fra Predikere og sender deretter den instansen til filter metode. Men vi tar feil.

2.1. Bytecode

For å sjekke denne antagelsen kan vi ta en titt på den genererte bytecode:

javap -c -p Hoved // avkortet // klassenavn er forenklet for korthets skyld // for eksempel er Stream egentlig 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: påkalle grensesnitt nr. 19, 1 // InterfaceMethod List.stream :()LStream; 14: påkalt dynamisk # 23, 0 // InvokeDynamic # 0: test :() LPredikat; 19: påkalle grensesnitt # 27, 2 // InterfaceMethod Stream.filter: (LPredicate;) LStream; 24: invokeinterface # 33, 1 // InterfaceMethod Stream.count :() J 29: lstore_1 30: return

Til tross for hva vi trodde, det er ingen anonym indre klasse og absolutt ingen overfører en forekomst av en slik klasse til filter metode.

Overraskende nok påkalt dynamisk instruksjon er på en eller annen måte ansvarlig for å skape Predikere forekomst.

2.2. Lambda-spesifikke metoder

I tillegg genererte Java-kompilatoren også den følgende morsomme statiske metoden:

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 metoden tar en String som inngang og utfører deretter følgende trinn:

  • Beregner inngangslengden (invokevirtuallengde)
  • Sammenligning av lengden med konstanten 3 (if_icmple og iconst_3)
  • Går tilbake falsk hvis lengden er mindre enn eller lik 3

Interessant, dette tilsvarer faktisk lambdaen vi passerte til filter metode:

c -> c.lengde ()> 3

Så i stedet for en anonym indre klasse, oppretter Java en spesiell statisk metode og på en eller annen måte påkaller den metoden via påkalt dynamisk.

I løpet av denne artikkelen skal vi se hvordan denne påkallingen fungerer internt. Men først, la oss definere problemet som påkalt dynamisk prøver å løse.

2.3. Problemet

Før Java 7 hadde JVM bare fire metodeanropstyper: invokevirtual å kalle normale klassemetoder, invokestatiske å ringe statiske metoder, påkalle grensesnitt å ringe grensesnittmetoder, og invokespesial å kalle konstruktører eller private metoder.

Til tross for forskjellene deres deler alle disse påkallelsene ett enkelt trekk: De har noen forhåndsdefinerte trinn for å fullføre hver metodeanrop, og vi kan ikke berike disse trinnene med vår tilpassede oppførsel.

Det er to hovedløsninger for denne begrensningen: den ene på kompileringstid og den andre ved kjøretid. Førstnevnte brukes vanligvis av språk som Scala eller Koltin, og sistnevnte er den valgte løsningen for JVM-baserte dynamiske språk som JRuby.

Runtime-tilnærmingen er vanligvis refleksjonsbasert og følgelig ineffektiv.

På den annen side er kompileringstidsløsningen vanligvis avhengig av kodegenerering på kompileringstid. Denne tilnærmingen er mer effektiv ved kjøretid. Imidlertid er det litt sprøtt og kan også føre til en langsommere oppstartstid da det er mer bytekode å behandle.

Nå som vi har fått en bedre forståelse av problemet, la oss se hvordan løsningen fungerer internt.

3. Under hetten

påkalt dynamisk lar oss bootstrap metoden påkallingsprosessen på den måten vi vil. Det vil si når JVM ser en påkalt dynamisk opcode for første gang, det kaller en spesiell metode kjent som bootstrap-metoden for å initialisere påkallingsprosessen:

Bootstrap-metoden er et vanlig stykke Java-kode som vi har skrevet for å sette opp påkallingsprosessen. Derfor kan den inneholde hvilken som helst logikk.

Når bootstrap-metoden er fullført normalt, skal den returnere en forekomst av CallSite. Dette CallSite innkapsler følgende informasjon:

  • En pekepinn til den faktiske logikken som JVM skal utføre. Dette skal representeres som en Metode Håndtak.
  • En tilstand som representerer gyldigheten av den returnerte CallSite.

Fra nå av, hver gang JVM ser denne spesifikke opoden igjen, vil den hoppe over den langsomme banen og kalle direkte den underliggende kjørbare. Videre vil JVM fortsette å hoppe over den langsomme banen til tilstanden i CallSite Endringer.

I motsetning til Reflection API, kan JVM fullstendig gjennomsiktig Metode Håndtaks og vil prøve å optimalisere dem, derav bedre ytelse.

3.1. Bootstrap Method Table

La oss ta en titt på det genererte påkalt dynamisk bytecode:

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

Dette betyr at denne spesielle instruksjonen skal kalle den første bootstrap-metoden (# 0 del) fra bootstrap-metodetabellen. Det nevner også noen av argumentene for å overføre til bootstrap-metoden:

  • De test er den eneste abstrakte metoden i Predikere
  • De () Ljava / util / funksjon / Predikat representerer en metodesignatur i JVM - metoden tar ingenting som input og returnerer en forekomst av Predikere grensesnitt

For å se bootstrap-metodetabellen for lambda-eksemplet, bør vi passere -v alternativ til javap:

javap -c -p -v Main // avkortet // lagt til nye linjer for kortfattethet BootstrapMethods: 0: # 55 REF_invokeStatic java / lang / invoke / LambdaMetafactory.metafactory: (Ljava / lang / invoke / MethodHandles $ Lookup; Ljava / lang / String; Ljava / lang / påkalle / MethodType; Ljava / lang / påkalle / MethodType; Ljava / lang / påkalle / MethodHandle; Ljava / lang / påkalle / MethodType;) Ljava / lang / påkalle / CallSite; Metode argumenter: # 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 metafabrikk statisk metode i Lambda Metafactory klasse.

I likhet med alle andre bootstrap-metoder, tar denne minst tre argumenter som følger:

  • De Ljava / lang / påkalle / MethodHandles $ Lookup argument representerer oppslagskonteksten for påkalt dynamisk
  • De Ljava / lang / String representerer metodens navn i anropssiden - i dette eksemplet er metodens navn test
  • De Ljava / lang / påkalle / MethodType er den dynamiske metodesignaturen til anropssiden - i dette tilfellet er det () Ljava / util / funksjon / Predikat

I tillegg til disse tre argumentene, kan bootstrap-metoder også valgfritt godta en eller flere ekstra parametere. I dette eksemplet er disse de ekstra:

  • De (Ljava / lang / Objekt;) Z er en slettet metodesignatur som godtar en forekomst av Gjenstand og returnerer en boolsk.
  • De REF_invokeStatic Main.lambda $ main $ 0: (Ljava / lang / String;) Z er den Metode Håndtak peker på selve lambdalogikken.
  • De (Ljava / lang / String;) Z er en ikke-slettet metodesignatur som godtar en String og returnerer en boolsk.

Enkelt sagt, JVM vil overføre all nødvendig informasjon til bootstrap-metoden. Bootstrap-metoden vil i sin tur bruke den informasjonen til å lage en passende forekomst av Predikere. Deretter vil JVM sende den forekomsten til filter metode.

3.2. Ulike typer CallSites

Når JVM ser det påkalt dynamisk i dette eksemplet for første gang kaller det bootstrap-metoden. Når du skriver denne artikkelen, lambda bootstrap-metoden vil bruke InnerClassLambdaMetafactoryå generere en indre klasse for lambda ved kjøretid.

Deretter innkapsler bootstrap-metoden den genererte indre klassen i en spesiell type CallSite kjent som ConstantCallSite. Denne typen CallSite ville aldri endres etter oppsett. Derfor, etter det første oppsettet for hver lambda, vil JVM alltid bruke den raske banen for å direkte kalle lambdalogikken.

Selv om dette er den mest effektive typen påkalt dynamisk, det er absolutt ikke det eneste tilgjengelige alternativet. Faktisk gir Java MutableCallSite og VolatileCallSite for å imøtekomme for mer dynamiske krav.

3.3. Fordeler

For å implementere lambda-uttrykk, i stedet for å lage anonyme indre klasser på kompileringstid, oppretter Java dem ved kjøretid via påkalt dynamisk.

Man kan argumentere mot å utsette generasjon av indre klasse til kjøretid. Imidlertid, den påkalt dynamisk tilnærming har noen fordeler i forhold til den enkle kompileringstidsløsningen.

For det første genererer ikke JVM den indre klassen før første gangs bruk av lambda. Derfor, vi betaler ikke for ekstra fotavtrykk knyttet til den indre klassen før den første lambda-henrettelsen.

I tillegg flyttes mye av koblingslogikken fra bytekoden til bootstrap-metoden. Derfor, de påkalt dynamisk bytecode er vanligvis mye mindre enn alternative løsninger. Den mindre bytekoden kan øke oppstartshastigheten.

Anta at en nyere versjon av Java kommer med en mer effektiv implementering av bootstrap-metoden. Så vår påkalt dynamisk bytecode kan dra nytte av denne forbedringen uten å kompilere på nytt. På denne måten kan vi oppnå en slags videresending av binær kompatibilitet. I utgangspunktet kan vi bytte mellom forskjellige strategier uten rekompilering.

Til slutt er det vanligvis enklere å skrive bootstrap og koblingslogikk i Java enn å krysse en AST for å generere et komplekst stykke bytekode. Så, påkalt dynamisk kan være (subjektivt) mindre sprø.

4. Flere eksempler

Lambda-uttrykk er ikke den eneste funksjonen, og Java er ikke sikkert det eneste språket som brukes påkalt dynamisk. I denne delen skal vi bli kjent med noen få andre eksempler på dynamisk påkallelse.

4.1. Java 14: poster

Records er en ny forhåndsvisning av Java 14 som gir en fin, kort syntaks for å erklære klasser som skal være dumme dataholdere.

Her er et enkelt rekordeksempel:

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

Gitt denne enkle enlinjeren, genererer Java-kompilator passende implementeringer for tilgangsmetoder, toString, er lik, og hashcode.

For å gjennomføre toString, er lik, eller hashcode, Java bruker påkalt dynamisk. For eksempel bytekoden for er lik er som følgende:

offentlig endelig boolsk lik (java.lang.Object); Kode: 0: aload_0 1: aload_1 2: påkalt dynamisk # 27, 0 // InvokeDynamic # 0: tilsvarer: (LColor; Ljava / lang / Object;) Z 7: ireturn

Den alternative løsningen er å finne alle postfelt og generere er lik logikk basert på disse feltene på kompileringstid. Jo flere felt vi har, desto lengre bytekode.

Tvert imot kaller Java en bootstrap-metode for å koble riktig implementering under kjøretid. Derfor, bytekodelengden vil forbli konstant uavhengig av antall felt.

Å se nærmere på bytecode viser at bootstrap-metoden 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 / påkalle / MethodHandle;) Ljava / lang / Object; Metode argumenter: # 8 Farge # 49 navn; kode # 51 REF_getField Farge.navn: Ljava/lang/String; # 52 REF_getField Color.code: Jeg

4.2. Java 9: ​​streng sammenføyning

Før Java 9 ble ikke-trivielle strengkombinasjoner implementert ved hjelp av StringBuilder. Som en del av JEP 280 bruker nå strengkonkaterasjon påkalt dynamisk. La oss for eksempel sammenkoble en konstant streng med en tilfeldig variabel:

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

Slik ser bytekoden ut for dette eksemplet:

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

Videre befinner bootstrap-metodene for strengkonkatenasjoner seg 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 tilfeldig- \ u0001

5. Konklusjon

I denne artikkelen ble vi først kjent med problemene indy prøver å løse.

Så, ved å gå gjennom et enkelt eksempel på lambdauttrykk, så vi hvordan påkalt dynamisk jobber internt.

Til slutt oppsummerte vi noen andre eksempler på indy i nyere versjoner av Java.


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