Een inleiding om Dynamic aan te roepen in de JVM

1. Overzicht

Invoke Dynamic (ook bekend als Indy) was onderdeel van JSR 292, bedoeld om de JVM-ondersteuning voor dynamisch getypte talen te verbeteren. Na de eerste release in Java 7, het invokedynamic opcode wordt vrij uitgebreid gebruikt door dynamische JVM-gebaseerde talen zoals JRuby en zelfs statisch getypeerde talen zoals Java.

In deze tutorial gaan we het demystificeren invokedynamic en kijk hoe het kanhelpen bibliotheek- en taalontwerpers om vele vormen van dynamiek te implementeren.

2. Maak kennis met Invoke Dynamic

Laten we beginnen met een eenvoudige reeks Stream API-aanroepen:

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

In eerste instantie zouden we kunnen denken dat Java een anonieme innerlijke klasse creëert die voortkomt uit Predikaat en geeft die instantie vervolgens door aan de filter methode. Maar we zouden het mis hebben.

2.1. De bytecode

Om deze aanname te controleren, kunnen we een kijkje nemen in de gegenereerde bytecode:

javap -c -p Main // afgekapt // klassennamen zijn vereenvoudigd omwille van de beknoptheid // Stream is bijvoorbeeld eigenlijk 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

Ondanks wat we dachten, er is geen anonieme innerlijke klasse en zeker, niemand geeft een instantie van een dergelijke klasse door aan de filter methode.

Verrassend genoeg is de invokedynamic instructie is op de een of andere manier verantwoordelijk voor het maken van de Predikaat voorbeeld.

2.2. Lambda-specifieke methoden

Bovendien genereerde de Java-compiler ook de volgende grappig ogende statische methode:

privé statische booleaanse lambda $ main $ 0 (java.lang.String); Code: 0: aload_0 1: invokevirtual # 37 // Methode java / lang / String.length :() I 4: iconst_3 5: if_icmple 12 8: iconst_1 9: goto 13 12: iconst_0 13: ireturn

Deze methode duurt een Draad als invoer en voert vervolgens de volgende stappen uit:

  • Berekenen van de invoerlengte (invokevirtual Aan lengte)
  • De lengte vergelijken met de constante 3 (if_icmple en iconst_3)
  • Terugkeren false als de lengte kleiner is dan of gelijk is aan 3

Interessant genoeg is dit eigenlijk het equivalent van de lambda die we hebben doorgegeven aan de filter methode:

c -> c.length ()> 3

Dus in plaats van een anonieme innerlijke klasse, creëert Java een speciale statische methode en roept op de een of andere manier die methode op via invokedynamic.

In de loop van dit artikel gaan we zien hoe deze aanroep intern werkt. Maar laten we eerst het probleem dat definiëren invokedynamic probeert op te lossen.

2.3. Het probleem

Vóór Java 7 had de JVM slechts vier typen methode-aanroep: invokevirtual om normale klassemethoden aan te roepen, invokestatisch om statische methoden aan te roepen, oproepinterface om interfacemethoden aan te roepen, en invokespecial om constructeurs of privémethoden aan te roepen.

Ondanks hun verschillen hebben al deze aanroepen één eenvoudige eigenschap gemeen: ze hebben een paar vooraf gedefinieerde stappen om elke methodeaanroep te voltooien, en we kunnen deze stappen niet verrijken met ons aangepaste gedrag.

Er zijn twee belangrijke oplossingen voor deze beperking: één tijdens het compileren en de andere tijdens runtime. De eerste wordt meestal gebruikt door talen zoals Scala of Koltin en de laatste is de voorkeursoplossing voor JVM-gebaseerde dynamische talen zoals JRuby.

De runtime-benadering is meestal gebaseerd op reflectie en bijgevolg inefficiënt.

Aan de andere kant vertrouwt de compilatie-oplossing meestal op het genereren van code tijdens het compileren. Deze aanpak is efficiënter tijdens runtime. Het is echter enigszins broos en kan ook een langzamere opstarttijd veroorzaken omdat er meer bytecode moet worden verwerkt.

Nu we het probleem beter begrijpen, gaan we eens kijken hoe de oplossing intern werkt.

3. Onder de motorkap

invokedynamic laat ons het aanroepproces van de methode op elke gewenste manier opstarten. Dat wil zeggen, wanneer de JVM een invokedynamic opcode, roept het een speciale methode aan die bekend staat als de bootstrap-methode om het aanroepproces te initialiseren:

De bootstrap-methode is een normaal stukje Java-code dat we hebben geschreven om het aanroepproces op te zetten. Daarom kan het elke logica bevatten.

Zodra de bootstrap-methode normaal is voltooid, moet deze een instantie van CallSite. Dit CallSite bevat de volgende stukjes informatie:

  • Een verwijzing naar de feitelijke logica die JVM zou moeten uitvoeren. Dit moet worden weergegeven als een MethodHandle.
  • Een voorwaarde die de geldigheid van het geretourneerde document aangeeft CallSite.

Vanaf nu zal elke keer dat JVM deze specifieke opcode opnieuw ziet, het langzame pad overslaan en direct het onderliggende uitvoerbare bestand aanroepen. Bovendien zal de JVM het langzame pad blijven overslaan totdat de toestand in het CallSite veranderingen.

In tegenstelling tot de Reflection API kan de JVM volledig doorkijken MethodHandles en zal proberen ze te optimaliseren, vandaar de betere prestaties.

3.1. Bootstrap-methodetabel

Laten we nog eens kijken naar het gegenereerde invokedynamic bytecode:

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

Dit betekent dat deze specifieke instructie de eerste bootstrap-methode (# 0-deel) uit de bootstrap-methodetabel moet aanroepen. Het vermeldt ook enkele van de argumenten die aan de bootstrap-methode moeten worden doorgegeven:

  • De test is de enige abstracte methode in de Predikaat
  • De () Ljava / util / function / Predicaat vertegenwoordigt een methodehandtekening in de JVM - de methode neemt niets als invoer en retourneert een instantie van de Predikaat koppel

Om de tabel met de bootstrap-methode voor het lambda-voorbeeld te zien, moeten we doorgeven -v optie om Javap:

javap -c -p -v Main // truncated // nieuwe regels toegevoegd voor beknoptheid BootstrapMethods: 0: # 55 REF_invokeStatic java / lang / invoke / LambdaMetafactory.metafactory: (Ljava / lang / invoke / MethodHandles $ Lookup; Ljava / lang / String; Ljava / lang / invoke / MethodType; Ljava / lang / invoke / MethodType; Ljava / lang / invoke / MethodHandle; Ljava / lang / invoke / MethodType;) Ljava / lang / invoke / CallSite; Methode argumenten: # 62 (Ljava / lang / Object;) Z # 64 REF_invokeStatic Main.lambda $ main $ 0: (Ljava / lang / String;) Z # 67 (Ljava / lang / String;) Z

De bootstrap-methode voor alle lambda's is de metafactory statische methode in het LambdaMetafactory klasse.

Net als bij alle andere bootstrap-methoden, heeft deze ten minste drie argumenten als volgt:

  • De Ljava / lang / invoke / MethodHandles $ Lookup argument vertegenwoordigt de opzoekcontext voor het invokedynamic
  • De Ljava / lang / String vertegenwoordigt de naam van de methode in de aanroepsite - in dit voorbeeld is de naam van de methode test
  • De Ljava / lang / invoke / MethodType is de dynamische methodehandtekening van de oproepsite - in dit geval is het () Ljava / util / function / Predicaat

Naast deze drie argumenten kunnen bootstrap-methoden optioneel ook een of meer extra parameters accepteren. In dit voorbeeld zijn dit de extra:

  • De (Ljava / lang / Object;) Z is een gewiste methodehandtekening die een instantie accepteert van Voorwerp en het retourneren van een boolean.
  • De REF_invokeStatic Main.lambda $ main $ 0: (Ljava / lang / String;) Z is de MethodHandle wijzend naar de feitelijke lambda-logica.
  • De (Ljava / lang / String;) Z is een niet-gewiste methodehandtekening die er een accepteert Draad en het retourneren van een boolean.

Simpel gezegd, de JVM geeft alle vereiste informatie door aan de bootstrap-methode. De Bootstrap-methode zal op zijn beurt die informatie gebruiken om een ​​geschikt exemplaar van Predikaat. Vervolgens geeft de JVM die instantie door aan het filter methode.

3.2. Verschillende types van CallSites

Zodra de JVM ziet invokedynamic in dit voorbeeld wordt voor de eerste keer de bootstrap-methode aangeroepen. Op het moment van schrijven van dit artikel, de lambda-bootstrap-methode gebruikt de InnerClassLambdaMetafactoryom tijdens runtime een innerlijke klasse voor de lambda te genereren.

Vervolgens kapselt de bootstrap-methode de gegenereerde inner class in een speciaal type CallSite bekend als ConstantCallSite. Dit soort CallSite zou nooit veranderen na de installatie. Daarom zal de JVM na de eerste setup voor elke lambda altijd het snelle pad gebruiken om de lambda-logica direct aan te roepen.

Hoewel dit het meest efficiënte type is aangeroepen dynamisch, het is zeker niet de enige beschikbare optie. In feite biedt Java MutableCallSite en VolatileCallSite om tegemoet te komen aan meer dynamische vereisten.

3.3. Voordelen

Dus om lambda-expressies te implementeren, in plaats van anonieme innerlijke klassen tijdens het compileren te maken, maakt Java ze tijdens runtime via invokedynamic.

Je zou kunnen pleiten tegen het uitstellen van het genereren van innerlijke klassen tot aan de looptijd. echter, de invokedynamic benadering heeft een aantal voordelen ten opzichte van de eenvoudige compilatietijdoplossing.

Ten eerste genereert de JVM de innerlijke klasse pas bij het eerste gebruik van lambda. Vandaar, we zullen niet betalen voor de extra footprint die hoort bij de inner class vóór de eerste lambda-uitvoering.

Bovendien wordt veel van de koppelingslogica verplaatst van de bytecode naar de bootstrap-methode. Daarom de invokedynamic bytecode is meestal veel kleiner dan alternatieve oplossingen. De kleinere bytecode kan de opstartsnelheid verhogen.

Stel dat een nieuwere versie van Java wordt geleverd met een efficiëntere implementatie van de bootstrap-methode. Dan onze invokedynamic bytecode kan profiteren van deze verbetering zonder opnieuw te compileren. Op deze manier kunnen we een soort van binaire compatibiliteit voor doorsturen bereiken. In principe kunnen we schakelen tussen verschillende strategieën zonder opnieuw te compileren.

Ten slotte is het schrijven van de bootstrap- en koppelingslogica in Java meestal gemakkelijker dan het doorlopen van een AST om een ​​complex stuk bytecode te genereren. Zo, invokedynamic kan (subjectief) minder broos zijn.

4. Meer voorbeelden

Lambda-expressies zijn niet de enige functie en Java is zeker niet de enige taal die wordt gebruikt invokedynamic. In deze sectie gaan we vertrouwd raken met een paar andere voorbeelden van dynamische aanroep.

4.1. Java 14: records

Records zijn een nieuwe preview-functie in Java 14 die een mooie beknopte syntaxis biedt om klassen te declareren die verondersteld worden domme gegevenshouders te zijn.

Hier is een eenvoudig recordvoorbeeld:

openbaar record Kleur (tekenreeksnaam, int-code) {}

Gezien deze eenvoudige one-liner genereert de Java-compiler geschikte implementaties voor accessormethoden, toString, is gelijk aan, en hashcode.

Om te implementeren toString, is gelijk aan, of hashcode, Java gebruikt invokedynamic. Bijvoorbeeld de bytecode voor is gelijk aan is als volgt:

openbare definitieve booleaanse waarde is gelijk aan (java.lang.Object); Code: 0: aload_0 1: aload_1 2: invokedynamic # 27, 0 // InvokeDynamic # 0: is gelijk aan: (LColor; Ljava / lang / Object;) Z 7: ireturn

De alternatieve oplossing is om alle recordvelden te zoeken en het is gelijk aan logica op basis van die velden tijdens het compileren. Hoe meer velden we hebben, hoe langer de bytecode.

Integendeel, Java roept een bootstrap-methode aan om de juiste implementatie tijdens runtime te koppelen. Daarom de lengte van de bytecode zou constant blijven, ongeacht het aantal velden.

Als we de bytecode nader bekijken, blijkt dat de bootstrap-methode 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; Methode argumenten: # 8 Color # 49 name; code # 51 REF_getField Color.name:Ljava/lang/String; # 52 REF_getField Kleur.code: I

4.2. Java 9: ​​String-aaneenschakeling

Vóór Java 9 werden niet-triviale aaneenschakelingen van tekenreeksen geïmplementeerd met StringBuilder. Als onderdeel van JEP 280 wordt nu stringconcatenatie gebruikt invokedynamic. Laten we bijvoorbeeld een constante reeks samenvoegen met een willekeurige variabele:

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

Hier ziet u hoe de bytecode eruitziet voor dit voorbeeld:

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

Bovendien bevinden de bootstrap-methoden voor aaneenschakelingen van tekenreeksen zich in het 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; Methode-argumenten: # 36 willekeurig- \ u0001

5. Conclusie

In dit artikel maakten we eerst kennis met de problemen die de indy probeert op te lossen.

Toen we door een eenvoudig voorbeeld van een lambda-uitdrukking liepen, zagen we hoe invokedynamic werkt intern.

Ten slotte hebben we een paar andere voorbeelden van indy opgesomd in recente versies van Java.