Invokedynamic 101

מהדורת Java 7 של אורקל הציגה invokedynamicהוראות מכונת bytecode חדשה למכונת ה- Java Virtual Java (JVM) java.lang.invokeוחבילת API חדשה לספריית המחלקות הרגילה. פוסט זה מציג בפניך את ההוראות וה- API הזה.

מה ואיך של היוזם הדינמי

ש: מה זה invokedynamic?

ת: invokedynamic היא הוראת קוד-קל המאפשרת יישום שפות דינמיות (עבור ה- JVM) באמצעות הפעלת שיטה דינמית. הוראה זו מתוארת במהדורת Java SE 7 של מפרט JVM.

שפות דינמיות וסטטיות

שפה דינמית (הידוע גם בתור שפה דינמית-מוקלדת ) היא שפת תכנות עילית בדיקה שסוגיו בדרך כלל מתבצע בזמן הריצה, תכונה המכונית הקלדה דינמית . בדיקת סוג מאמתת שתוכנית בטוחה : כל טיעוני הפעולה הם בעלי הסוג הנכון. גרובי, רובי ו- JavaScript הם דוגמאות לשפות דינמיות. ( @groovy.transform.TypeCheckedההערה גורמת לגרובי להקליד צ'ק בזמן הקומפילציה).

לעומת זאת, שפה סטטית (המכונה גם שפה בהקלדה סטטית ) מבצעת בדיקת סוג בזמן הידור, תכונה המכונה הקלדה סטטית . המהדר מוודא שתוכנית נכונה לסוגיה, אם כי היא עשויה לדחות בדיקת סוג כלשהי לזמן הריצה (חושב על השלכות checkcastוההוראות). ג'אווה היא דוגמה לשפה סטטית. מהדר הג'אווה משתמש במידע מסוג זה כדי לייצר קוד byted שהוזן חזק, שניתן לבצע ביעילות על ידי ה- JVM.

ש: איך invokedynamicמקל על יישום שפה דינמית?

ת: בשפה דינמית, בדיקת סוג מתרחשת בדרך כלל בזמן הריצה. על מפתחים לעבור סוגים מתאימים או להסתכן בכשלים בזמן הריצה. לרוב זה java.lang.Objectהסוג המדויק ביותר לוויכוח שיטה. מצב זה מסבך את בדיקת הסוגים, אשר משפיעה על הביצועים.

אתגר נוסף הוא ששפות דינמיות מציעות בדרך כלל את היכולת להוסיף שדות / שיטות ולהסיר אותם מהשיעורים הקיימים. כתוצאה מכך, יש צורך לדחות את רזולוציית המחלקה, השיטה והשטח לזמן הריצה. כמו כן, לעיתים קרובות יש צורך להתאים קריאת שיטה ליעד שיש לו חתימה אחרת.

אתגרים אלה דרשו באופן מסורתי לבנות תמיכה בזמן ריצה אד-הוק על גבי ה- JVM. תמיכה זו כוללת שיעורי סוג עטיפה, שימוש בטבלאות hash כדי לספק רזולוציית סמלים דינמית וכן הלאה. Bytecode נוצר עם נקודות כניסה לזמן הריצה בצורה של שיחות שיטה באמצעות אחת מארבע הוראות ההפעלה לשיטה:

  • invokestaticמשמש להפעלת staticשיטות.
  • invokevirtualמשמש להפעיל publicו protectedהלא staticשיטות באמצעות שיגור דינמי.
  • invokeinterfaceדומה invokevirtualלמעט שיגור השיטה מבוסס על סוג ממשק.
  • invokespecialמשמש להפעלת שיטות אתחול מופע (קונסטרוקטורים) כמו גם privateשיטות ושיטות של מעמד-על של המעמד הנוכחי.

תמיכה בזמן ריצה זה משפיעה על הביצועים. קוד bytecode שנוצר דורש לעיתים קרובות מספר הפעלות בפועל של שיטת JVM לצורך הפעלת שיטת שפה דינמית אחת. השתקפות נמצאת בשימוש נרחב ותורמת להידרדרות הביצועים. כמו כן, מסלולי הביצוע הרבים השונים לא מאפשרים למהדר ה- JIT של ה- JVM ליישם אופטימיזציות.

כדי לטפל בביצועים גרועים, invokedynamicההוראה מבטלת תמיכה בזמן ריצה אד-הוק. במקום זאת, רצועות האתחול הראשונות של השיחה על ידי הפעלת לוגיקה של זמן ריצה הבוחר ביעילות שיטת יעד, והשיחות הבאות בדרך כלל קוראות לשיטת היעד מבלי להצטרף לאתחול מחדש.

invokedynamicמועיל גם למיישמי שפה דינמית על ידי תמיכה ביעדי שינוי שינויים דינמיים - אתר שיחות , ליתר דיוק, אתר שיחות דינמי הוא invokedynamicהוראה. יתר על כן, מכיוון שה- JVM תומך באופן פנימי invokedynamic, ניתן לבצע אופטימיזציה טובה יותר של הוראה זו על ידי מהדר ה- JIT.

ידיות בשיטה

ש: אני מבין invokedynamicשעובד עם ידיות שיטה כדי להקל על הפעלת שיטה דינמית. מהי ידית שיטה?

ת: ידית שיטה היא "הפנית הפעלה מוקלדת, ישירות שיטה בסיסית, בנאי, שדה, או פעולה ברמה נמוכה דומה, עם תמורות אופציונאלי של ויכוחים או ערכים לחזור." במילים אחרות, זה דומה למצביע פונקציה בסגנון C המצביע על קוד הפעלה - מטרה - ואשר ניתן להפנות אותו להפעלת קוד זה. ידיות השיטה מתוארות על ידי java.lang.invoke.MethodHandleהכיתה המופשטת .

ש: האם אתה יכול לספק דוגמה פשוטה ליצירת שיטות ולהפעלה?

ת: עיין ברשימה 1.

רישום 1. MHD.java(גרסה 1)

import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; public class MHD { public static void main(String[] args) throws Throwable { MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findStatic(MHD.class, "hello", MethodType.methodType(void.class)); mh.invokeExact(); } static void hello() { System.out.println("hello"); } }

Listing 1 מתאר תכנית הפגנת ידית שיטה מורכבת main()ואת hello()שיטות כיתה. מטרת תוכנית זו היא להפעיל hello()באמצעות ידית שיטה.

main()המשימה הראשונה היא להשיג java.lang.invoke.MethodHandles.Lookupאובייקט. אובייקט זה הוא מפעל ליצירת ידיות לשיטות ומשמש לחיפוש יעדים כגון שיטות וירטואליות, שיטות סטטיות, שיטות מיוחדות, קונסטרוקציות ואביזרי שטח. יתר על כן, זה תלוי בהקשר הפנייה של אתר שיחה ואוכף הגבלות גישה לטיפול בשיטה בכל פעם שנוצרת ידית שיטה. במילים אחרות, אתר שיחות (כגון main()שיטת Listing 1 הפועל כאתר שיחה) שמשיג אובייקט חיפוש יכול לגשת רק לאותם יעדים הנגישים לאתר השיחות. אובייקט החיפוש מתקבל על ידי הפעלת שיטת java.lang.invoke.MethodHandlesהכיתה MethodHandles.Lookup lookup().

publicLookup()

MethodHandlesמכריז גם על MethodHandles.Lookup publicLookup()שיטה. שלא כמו lookup(), אשר ניתן להשתמש בו כדי להשיג ידית שיטה לכל שיטה / קונסטרוקטור או שדה נגישים, publicLookup()ניתן להשתמש בה כדי להשיג ידית שיטה לשדה נגיש לציבור או לשיטה / קונסטרוקטור נגיש לציבור בלבד.

After obtaining the lookup object, this object's MethodHandle findStatic(Class refc, String name, MethodType type) method is called to obtain a method handle to the hello() method. The first argument passed to findStatic() is a reference to the class (MHD) from which the method (hello()) is accessed, and the second argument is the method's name. The third argument is an example of a method type, which "represents the arguments and return type accepted and returned by a method handle, or the arguments and return type passed and expected by a method handle caller." It's represented by an instance of the java.lang.invoke.MethodType class, and obtained (in this example) by calling java.lang.invoke.MethodType's MethodType methodType(Class rtype) method. This method is called because hello() only provides a return type, which happens to be void. This return type is made available to methodType() by passing void.class to this method.

The returned method handle is assigned to mh. This object is then used to call MethodHandle's Object invokeExact(Object... args) method, to invoke the method handle. In other words, invokeExact() results in hello() being called, and hello being written to the standard output stream. Because invokeExact() is declared to throw Throwable, I've appended throws Throwable to the main() method header.

Q: In your previous answer, you mentioned that the lookup object can only access those targets that are accessible to the call site. Can you provide an example that demonstrates trying to obtain a method handle to an inaccessible target?

A: Check out Listing 2.

Listing 2. MHD.java (version 2)

import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; class HW { public void hello1() { System.out.println("hello from hello1"); } private void hello2() { System.out.println("hello from hello2"); } } public class MHD { public static void main(String[] args) throws Throwable { HW hw = new HW(); MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findVirtual(HW.class, "hello1", MethodType.methodType(void.class)); mh.invoke(hw); mh = lookup.findVirtual(HW.class, "hello2", MethodType.methodType(void.class)); } }

Listing 2 declares HW (Hello, World) and MHD classes. HW declares a publichello1() instance method and a privatehello2() instance method. MHD declares a main() method that will attempt to invoke these methods.

main()'s first task is to instantiate HW in preparation for invoking hello1() and hello2(). Next, it obtains a lookup object and uses this object to obtain a method handle for invoking hello1(). This time, MethodHandles.Lookup's findVirtual() method is called and the first argument passed to this method is a Class object describing the HW class.

It turns out that findVirtual() will succeed, and the subsequent mh.invoke(hw); expression will invoke hello1(), resulting in hello from hello1 being output.

Because hello1() is public, it's accessible to the main() method call site. In contrast, hello2() isn't accessible. As a result, the second findVirtual() invocation will fail with an IllegalAccessException.

When you run this application, you should observe the following output:

hello from hello1 Exception in thread "main" java.lang.IllegalAccessException: member is private: HW.hello2()void, from MHD at java.lang.invoke.MemberName.makeAccessException(MemberName.java:507) at java.lang.invoke.MethodHandles$Lookup.checkAccess(MethodHandles.java:1172) at java.lang.invoke.MethodHandles$Lookup.checkMethod(MethodHandles.java:1152) at java.lang.invoke.MethodHandles$Lookup.accessVirtual(MethodHandles.java:648) at java.lang.invoke.MethodHandles$Lookup.findVirtual(MethodHandles.java:641) at MHD.main(MHD.java:27)

Q: Listings 1 and 2 use the invokeExact() and invoke() methods to execute a method handle. What's the difference between these methods?

A: Although invokeExact() and invoke() are designed to execute a method handle (actually, the target code to which the method handle refers), they differ when it comes to performing type conversions on arguments and the return value. invokeExact() doesn't perform automatic compatible-type conversion on arguments. Its arguments (or argument expressions) must be an exact type match to the method signature, with each argument provided separately, or all arguments provided together as an array. invoke() requires its arguments (or argument expressions) to be a type-compatible match to the method signature -- automatic type conversions are performed, with each argument provided separately, or all arguments provided together as an array.

Q: Can you provide me with an example that shows how to invoke an instance field's getter and setter?

A: Check out Listing 3.

Listing 3. MHD.java (version 3)

import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; class Point { int x; int y; } public class MHD { public static void main(String[] args) throws Throwable { MethodHandles.Lookup lookup = MethodHandles.lookup(); Point point = new Point(); // Set the x and y fields. MethodHandle mh = lookup.findSetter(Point.class, "x", int.class); mh.invoke(point, 15); mh = lookup.findSetter(Point.class, "y", int.class); mh.invoke(point, 30); mh = lookup.findGetter(Point.class, "x", int.class); int x = (int) mh.invoke(point); System.out.printf("x = %d%n", x); mh = lookup.findGetter(Point.class, "y", int.class); int y = (int) mh.invoke(point); System.out.printf("y = %d%n", y); } }

Listing 3 introduces a Point class with a pair of 32-bit integer instance fields named x and y. Each field's setter and getter is accessed by calling MethodHandles.Lookup's findSetter() and findGetter() methods, and the resulting MethodHandle is returned. Each of findSetter() and findGetter() requires a Class argument that identifies the field's class, the field's name, and a Class object that identifies the field's signature.

The invoke() method is used to execute a setter or getter-- behind the scenes, the instance fields are accessed via the JVM's putfield and getfield instructions. This method requires that a reference to the object whose field is being accessed be passed as the initial argument. For setter invocations, a second argument, consisting of the value being assigned to the field, also must be passed.

When you run this application, you should observe the following output:

x = 15 y = 30

Q: Your definition of method handle includes the phrase "with optional transformations of arguments or return values". Can you provide an example of argument transformation?

ת: יצרתי דוגמא על פי שיטת הכיתה Mathשל double pow(double a, double b)הכיתה. בדוגמא זו, אני משיג ידית מתודה pow()לשיטה, ומשנה את ידית השיטה הזו כך שהארגומנט השני שעובר אליו pow()הוא תמיד 10. בדוק את הרישום 4.