Bevezetés a dinamikus meghívásához a JVM-ben

1. Áttekintés

Az Invoke Dynamic (más néven Indy) a JSR 292 része volt, amelynek célja a dinamikusan gépelt nyelvek JVM támogatásának fokozása. Az első Java 7-es kiadás után a megidézett dinamikus Az opkódot meglehetősen széles körben használják az olyan dinamikus JVM-alapú nyelvek, mint a JRuby, és még a statikusan beírt nyelvek is, mint a Java.

Ebben az oktatóanyagban demisztifikálunk megidézett dinamikus és nézze meg, hogyan lehetsegítse a könyvtár- és nyelvtervezőket a dinamika számos formájának megvalósításában.

2. Ismerje meg az Invoke Dynamic alkalmazást

Kezdjük a Stream API-hívások egyszerű láncolatával:

public class Main {public static void main (String [] args) {long lengthyColors = List.of ("Piros", "Zöld", "Kék") .stream (). szűrő (c -> c.hossz ()> 3) .szám (); }}

Először azt gondolhatnánk, hogy a Java egy névtelen belső osztályt hoz létre, amelyből származik Állítmány majd továbbadja azt a példányt a szűrő módszer. De tévednénk.

2.1. A Bytecode

Ennek a feltételezésnek az ellenőrzésére bepillanthatunk a generált bycecode-ba:

javap -c -p Main // csonka // osztálynevek a rövidség kedvéért leegyszerűsödnek // például: A Stream valójában 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: teszt :() LPredicate; 19: invokeinterface # 27, 2 // InterfaceMethod Stream.filter: (LPredicate;) LStream; 24: invokeinterface # 33, 1 // InterfaceMethod Stream.count :() J 29: lstore_1 30: return

Annak ellenére, amit gondoltunk, nincs névtelen belső osztály és minden bizonnyal senki sem adja át egy ilyen osztály példányát a szűrő módszer.

Meglepő módon a megidézett dinamikus utasítás valahogy felelős a Állítmány példa.

2.2. Lambda specifikus módszerek

Ezenkívül a Java fordító a következő vicces megjelenésű statikus módszert is létrehozta:

privát statikus logikai lambda $ main $ 0 (java.lang.String); Kód: 0: aload_0 1: invokevirtual # 37 // Java / lang / String.length módszer :() I 4: iconst_3 5: if_icmple 12 8: iconst_1 9: goto 13 12: iconst_0 13: ireturn

Ez a módszer a Húr bemenetként, majd a következő lépéseket hajtja végre:

  • A bemeneti hossz kiszámítása (invokevirtuális tovább hossz)
  • Összehasonlítva a hosszat a 3 (if_icmple és iconst_3)
  • Visszatérve hamis ha a hossza kisebb vagy egyenlő 3-mal

Érdekes módon ez valójában megegyezik azzal a lambdával, amelyet átadtunk a szűrő módszer:

c -> c.hossz ()> 3

Tehát anonim belső osztály helyett a Java létrehoz egy speciális statikus módszert, és valahogyan keresztül hívja meg ezt a módszert megidézett dinamikus.

A cikk folyamán megnézzük, hogyan működik ez a meghívás belsőleg. De először is határozzuk meg azt a problémát megidézett dinamikus megpróbálja megoldani.

2.3. A probléma

A Java 7 előtt a JVM-nek csak négy metódushívási típusa volt: invokevirtuális normál osztály módszereket hívni, invokesztatikus statikus módszerek hívása, invokeinterface az interfész metódusainak meghívása, és különlegeset hív konstruktorok vagy privát módszerek hívása.

Különbségeik ellenére ezeknek az invokációknak egyetlen vonása van: néhány előre meghatározott lépéssel rendelkeznek az egyes módszerhívások befejezéséhez, és ezeket a lépéseket nem gazdagíthatjuk egyéni viselkedésmódjainkkal.

Ennek a korlátozásnak két fő megoldása van: az egyik fordítási időben, a másik futás közben. Az előbbit általában olyan nyelvek használják, mint a Scala vagy a Koltin, az utóbbit pedig a JVM-alapú dinamikus nyelvek, például a JRuby számára választják.

A futásidejű megközelítés általában reflexión alapul, következésképpen nem hatékony.

Másrészt a fordítási idejű megoldás általában a fordítás idején történő kódgenerálásra támaszkodik. Ez a megközelítés futás közben hatékonyabb. Ez azonban kissé törékeny, és lassabb indítási időt is okozhat, mivel több bytecode van feldolgozva.

Most, hogy jobban megértettük a problémát, nézzük meg, hogyan működik a megoldás belsőleg.

3. A motorháztető alatt

megidézett dinamikus lehetővé teszi, hogy a kívánt módon indítsuk el a metódus meghívási folyamatot. Vagyis amikor a JVM lát egy megidézett dinamikus Az opcode első alkalommal meghív egy speciális módszert bootstrap metódusnak az invokációs folyamat inicializálásához:

A bootstrap módszer egy normál Java-kód, amelyet a meghívási folyamat beállításához írtunk. Ezért bármilyen logikát tartalmazhat.

Miután a bootstrap módszer normálisan befejeződött, vissza kell adnia a CallSite. Ez CallSite a következő információkat foglalja magában:

  • Mutató arra a tényleges logikára, amelyet a JVM-nek végre kell hajtania. Ezt a-ként kell képviselni MethodHandle.
  • A visszatérés érvényességét képviselő feltétel CallSite.

Mostantól minden alkalommal, amikor a JVM újra meglátja ezt a bizonyos opkódot, kihagyja a lassú utat, és közvetlenül meghívja az alapul szolgáló futtatható fájlt. Sőt, a JVM továbbra is kihagyja a lassú utat, amíg a CallSite változtatások.

A Reflection API-val szemben a JVM teljesen átlátszó MethodHandles megpróbálja optimalizálni őket, ezáltal a jobb teljesítmény.

3.1. Bootstrap módszer táblázat

Vessünk egy újabb pillantást a létrehozottra megidézett dinamikus bytecode:

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

Ez azt jelenti, hogy ennek az utasításnak meg kell hívnia az első bootstrap metódust (# 0 rész) a bootstrap metódus táblából. Emellett megemlít néhány argumentumot, amelyet át kell adni a bootstrap metódusnak:

  • A teszt az egyetlen elvont módszer a Állítmány
  • A () Ljava / util / function / predikátum metódus aláírást képvisel a JVM-ben - a metódus nem vesz semmit bemenetként, és a Állítmány felület

Ahhoz, hogy megnézzük a lambda példa bootstrap metódus táblázatát, át kell mennünk -v opció javap:

javap -c -p -v Main // csonka // új sorokat adott a rövidséghez BootstrapMethods: 0: # 55 REF_invokeStatic java / lang / invoke / LambdaMetafactory.metafactory: (Ljava / lang / invoke / MethodHandles $ Lookup; Ljava / lang / Karakterlánc; Ljava / lang / invoke / MethodType; Ljava / lang / invoke / MethodType; Ljava / lang / invoke / MethodHandle; Ljava / lang / invoke / MethodType;) Ljava / lang / invoke / CallSite; A módszer érvei: # 62 (Ljava / lang / Object;) Z # 64 REF_invokeStatic Main.lambda $ main $ 0: (Ljava / lang / String;) Z # 67 (Ljava / lang / String;) Z

Az összes lambdához tartozó bootstrap módszer a metafactory statikus módszer a LambdaMetafactory osztály.

Az összes többi bootstrap módszerhez hasonlóan ez is legalább három argumentumot tartalmaz az alábbiak szerint:

  • A Ljava / lang / invoke / MethodHandles $ Lookup argumentum a. keresési kontextusát képviseli megidézett dinamikus
  • A Ljava / lang / String a hívás helyén a metódus nevét jelenti - ebben a példában a metódus neve teszt
  • A Ljava / lang / invoke / MethodType a hívási hely dinamikus metódusú aláírása - ebben az esetben az () Ljava / util / function / predikátum

A bootstrap módszerek ezen a három argumentumon felül opcionálisan elfogadhatnak egy vagy több extra paramétert is. Ebben a példában ezek a további példák:

  • A (Ljava / lang / Object;) Z egy törölt metódus aláírás, amely elfogadja a Tárgy és visszatér a logikai.
  • A REF_invokeStatic Main.lambda $ main $ 0: (Ljava / lang / String;) Z az a MethodHandle rámutatva a tényleges lambda logikára.
  • A (Ljava / lang / String;) Z egy nem törölt metódus aláírás, amely elfogadja az egyiket Húr és visszatér a logikai.

Leegyszerűsítve: a JVM átadja az összes szükséges információt a bootstrap módszernek. A Bootstrap módszer viszont felhasználja ezeket az információkat a megfelelő példány létrehozásához Állítmány. Ezután a JVM továbbítja ezt a példányt a szűrő módszer.

3.2. Különböző típusú CallSites

Miután a JVM meglátja megidézett dinamikus ebben a példában először hívja meg a bootstrap metódust. A cikk írásakor a lambda bootstrap módszer a InnerClassLambdaMetafactoryhogy generáljon egy belső osztályt a lambda számára futás közben.

Ezután a bootstrap metódus a generált belső osztályt egy speciális típusba foglalja CallSite ismert, mint ConstantCallSite. Ez a fajta CallSite soha nem változik a beállítás után. Ezért az egyes lambdák első beállítása után a JVM mindig a gyors elérési utat használja a lambda logika közvetlen meghívására.

Bár ez a leghatékonyabb típusú invutedynamic, természetesen nem ez az egyetlen elérhető lehetőség. Ami azt illeti, a Java biztosítja MutableCallSite és VolatileCallSite hogy megfeleljen a dinamikusabb követelményeknek.

3.3. Előnyök

Tehát a lambda kifejezések megvalósítása érdekében anonim belső osztályok létrehozása helyett fordítási időben a Java futás közben létrehozza őket a megidézett dinamikus.

Lehet vitatkozni a belső osztály generációjának futásig történő halasztása ellen. Azonban a megidézett dinamikus A megközelítésnek néhány előnye van az egyszerű fordítási idejű megoldással szemben.

Először is, a JVM csak a lambda első használatakor hozza létre a belső osztályt. Ennélfogva, az első lambda végrehajtás előtt nem fizetjük a belső osztályhoz kapcsolódó extra lábnyomot.

Ezenkívül a linkelési logika nagy része a bájtkódról a rendszerindító módszerre kerül. Ebből kifolyólag, a megidézett dinamikus a bájtkód általában jóval kisebb, mint az alternatív megoldások. A kisebb bájtkód növelheti az indítási sebességet.

Tegyük fel, hogy a Java újabb verziója hatékonyabb bootstrap módszer megvalósítással jár. Akkor a mi megidézett dinamikus a bytecode újrafordítás nélkül kihasználhatja ezt a fejlesztést. Így elérhetünk valamilyen továbbító bináris kompatibilitást. Alapvetően újrafordítás nélkül válthatunk a különböző stratégiák között.

Végül, a bootstrap és a linking logika Java-ba írása általában könnyebb, mint egy AST-n való áthaladás egy bonyolult bájtkód létrehozásához. Így, megidézett dinamikus lehet (szubjektíven) kevésbé törékeny.

4. További példák

A lambda kifejezések nem az egyetlen szolgáltatás, és a Java nem biztos, hogy az egyetlen nyelv megidézett dinamikus. Ebben a részben megismerkedünk a dinamikus meghívás néhány más példájával.

4.1. Java 14: Rekordok

A rekordok egy új előnézeti funkció a Java 14-ben, amely egy szűkszavú szintaxist nyújt az állítólag néma adattartalmú osztályok deklarálásához.

Íme egy egyszerű rekordpélda:

nyilvános rekord színe (karakterlánc neve, int kód) {}

Tekintettel erre az egyszerű egyvonalasra, a Java fordító megfelelő megvalósításokat generál az accessor módszerekhez, toString, egyenlő, és hash kód.

A megvalósítás érdekében toString, egyenlő, vagy hash kód, A Java használja megidézett dinamikus. Például a egyenlő az alábbiak:

nyilvános végső logikai egyenlő (java.lang.Object); Kód: 0: aload_0 1: aload_1 2: invokedynamic # 27, 0 // InvokeDynamic # 0: egyenlő: (LColor; Ljava / lang / Object;) Z 7: ireturn

Az alternatív megoldás az összes rekordmező megkeresése és a egyenlő logika ezen mezők alapján a fordítás idején. Minél több mezőnk van, annál hosszabb a bájtkód.

Épp ellenkezőleg, a Java meghív egy bootstrap metódust a megfelelő megvalósítás futásidejű összekapcsolására. Ebből kifolyólag, a bájtkód hossza a mezők számától függetlenül állandó maradna.

Ha jobban megnézzük a bájtkódot, az azt mutatja, hogy a bootstrap módszer az 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; A módszer érvei: # 8 Color # 49 name; # 51 code REF_getField Color.name:Ljava/lang/String; # 52 REF_getField Szín.kód: I

4.2. Java 9: ​​Húrok összefűzése

A Java 9 előtt nem triviális karakterlánc-összefűzéseket hajtottak végre StringBuilder. A JEP 280 részeként a húrok összefűzését használják megidézett dinamikus. Például összefűzünk egy állandó karakterláncot egy véletlen változóval:

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

Így néz ki a bájtkód ebben a példában:

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

Ezenkívül a string összefűzések bootstrap módszerei a StringConcatFactory osztály:

BootstrapMethods: 0: # 30 REF_invokeStatic java / lang / invoke / StringConcatFactory.makeConcatWithConstants: (Ljava / lang / invoke / MethodHandles $ Lookup; Ljava / lang / String; Ljava / lang / invoke / MethodTyva; Ljava / lang; / lang / Object;) Ljava / lang / invoke / CallSite; A módszer érvei: # 36 random- \ u0001

5. Következtetés

Ebben a cikkben először megismerkedtünk azokkal a problémákkal, amelyeket az indy próbál megoldani.

Ezután egy egyszerű lambda kifejezés példáján keresztül meglátva, hogyan megidézett dinamikus belsőleg működik.

Végül felsoroltunk néhány további példát az indy-ra a Java legújabb verzióiban.


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