מבוא ל- Invoke Dynamic ב- JVM

1. סקירה כללית

Invoke Dynamic (המכונה גם אינדי) היה חלק מ- JSR 292 שנועד לשפר את תמיכת JVM בשפות שהוקלדו באופן דינמי. לאחר שחרורו הראשון ב- Java 7, ייעוד דינמי opcode משמש די בהרחבה על ידי שפות דינמיות מבוססות JVM כמו JRuby ואפילו שפות שהוקלדו באופן סטטי כמו Java.

במדריך זה אנו הולכים להתפלל ייעוד דינמי ולראות איך זה יכוללעזור למעצבי ספרייה ושפה ליישם צורות רבות של דינמיות.

2. פגוש את Invoke Dynamic

נתחיל בשרשרת פשוטה של ​​שיחות Stream API:

class class ראשי {public static void main (String [] args) {long lengthyColors = List.of ("Red", "Green", "Blue") .stream (). filter (c -> c.length ()> 3) .ספירה (); }}

בהתחלה, אנו עשויים לחשוב שג'אווה יוצרת מעמד פנימי אנונימי הנובע מ לְבַסֵס ואז מעביר את המופע הזה אל לְסַנֵן שיטה. אבל, היינו טועים.

2.1. ה- Bytecode

כדי לבדוק הנחה זו, אנו יכולים להציץ בקוד ה ByTecode שנוצר:

javap -c -p ראשי // קטומים // שמות מחלקות פשוטים לשם קיצור // למשל, זרם הוא למעשה Java / util / stream / Stream 0: ldc # 7 // String Red 2: ldc # 9 / / מחרוזת ירוק 4: ldc # 11 // מחרוזת כחול 6: invokestatic # 13 // InterfaceMethod List.of: (LObject; LObject;) LList; 9: ממשק # 19, 1 // InterfaceMethod List.stream:()LStream; 14: invokedynamic # 23, 0 // InvokeDynamic # 0: test :() LPredicate; 19: invokeinterface # 27, 2 // InterfaceMethod Stream.filter: (LPredicate;) LStream; 24: ממשק מספר 33, 1 // InterfaceMethod Stream.count :() J 29: lstore_1 30: return

למרות מה שחשבנו, אין מעמד פנימי אנונימי ובוודאי, אף אחד לא מעביר מופע של מעמד כזה ל לְסַנֵן שיטה.

באופן מפתיע, ה ייעוד דינמי ההוראה היא איכשהו אחראית ליצירת ה- לְבַסֵס למשל.

2.2. שיטות ספציפיות למבדה

בנוסף, מהדר Java יצר גם את השיטה הסטטית הבאה למראה מצחיק:

למבה בוליאנית סטטית פרטית $ עיקרית $ 0 (java.lang.String); קוד: 0: aload_0 1: invokevirtual # 37 // שיטה java / lang / String.length :() I 4: iconst_3 5: if_icmple 12 8: iconst_1 9: goto 13 12: iconst_0 13: ireturn

שיטה זו אורכת א חוּט כקלט ואז מבצע את השלבים הבאים:

  • חישוב אורך הקלט (invokevirtual עַל אורך)
  • השוואת האורך עם הקבוע 3 (if_icmple ו iconst_3)
  • חוזר שֶׁקֶר אם האורך קטן או שווה ל- 3

מעניין שזה למעשה שווה ערך למלבדה שהעברנו אליה לְסַנֵן שיטה:

c -> c.length ()> 3

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

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

2.3. הבעיה

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

למרות ההבדלים ביניהם, כל הקריאות הללו חולקות תכונה אחת פשוטה: יש להם כמה צעדים מוגדרים מראש להשלמת כל קריאת שיטה, ואיננו יכולים להעשיר את השלבים הללו בהתנהגויות המותאמות אישית שלנו.

ישנם שני דרכים לעקיפת הבעיה העיקריות למגבלה זו: האחד בזמן הידור והשני בזמן הריצה. הראשונה משמשת בדרך כלל שפות כמו Scala או Koltin והשנייה היא הפתרון הנבחר עבור שפות דינמיות מבוססות JVM כמו JRuby.

גישת זמן הריצה היא בדרך כלל מבוססת השתקפות וכתוצאה מכך אינה יעילה.

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

כעת, לאחר שהבנו טוב יותר את הבעיה, בואו נראה כיצד הפתרון עובד באופן פנימי.

3. מתחת למכסה המנוע

ייעוד דינמי מאפשר לנו לאתחל את תהליך קריאת השיטה בכל דרך שתרצה. כלומר כאשר ה- JVM רואה ייעוד דינמי לראשונה, קוד זה מכנה שיטה מיוחדת המכונה שיטת bootstrap לאתחול תהליך הקריאה:

שיטת bootstrap היא פיסת קוד Java רגילה שכתבנו כדי להגדיר את תהליך הקריאה. לכן, הוא יכול להכיל כל היגיון.

ברגע ששיטת האתחול תושלם כרגיל, עליה להחזיר מופע של CallSite. זֶה CallSite מכיל את חלקי המידע הבאים:

  • מצביע ללוגיקה בפועל שעל JVM לבצע. זה צריך להיות מיוצג כ- שיטת טיפול.
  • תנאי המייצג את תוקף החזר CallSite.

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

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

3.1. טבלת שיטות Bootstrap

בואו נסתכל שוב על הנוצר ייעוד דינמי קוד byt:

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

המשמעות היא כי הוראה מסוימת זו צריכה לקרוא לשיטת האתחול הראשונה (חלק מספר 0) מטבלת שיטות האתחול. כמו כן, הוא מזכיר כמה מהטיעונים להעביר לשיטת bootstrap:

  • ה מִבְחָן היא השיטה המופשטת היחידה ב לְבַסֵס
  • ה () Ljava / util / function / Predicate מייצג חתימת שיטה ב- JVM - השיטה לא לוקחת דבר כקלט ומחזירה מופע של ה- לְבַסֵס מִמְשָׁק

על מנת לראות את טבלת שיטת bootstrap לדוגמא למבדה, עלינו לעבור -v אפשרות ל javap:

javap -c -p -v ראשי // קטום // הוסיף שורות חדשות לקצרה BootstrapMethods: 0: # 55 REF_invokeStatic java / lang / invoke / LambdaMetafactory.metafactory: (Ljava / lang / invoke / MethodHandles $ Lookup; Ljava / lang / מחרוזת; Ljava / lang / invoke / MethodType; Ljava / lang / invoke / MethodType; Ljava / lang / invoke / MethodHandle; Ljava / lang / invoke / MethodType;) Ljava / lang / invoke / CallSite; טיעוני שיטה: # 62 (Ljava / lang / Object;) Z # 64 REF_invokeStatic Main.lambda $ main $ 0: (Ljava / lang / String;) Z # 67 (Ljava / lang / String;) Z

שיטת bootstrap עבור כל lambdas היא מטא-פקטורי שיטה סטטית ב LambdaMetafactory מעמד.

בדומה לכל שיטות ה- bootstrap האחרות, זו כוללת לפחות שלושה טיעונים כדלקמן:

  • ה Ljava / lang / invoke / MethodHandles $ Lookup טיעון מייצג את ההקשר לחיפוש עבור ה- ייעוד דינמי
  • ה ליבה / לאנג / מחרוזת מייצג את שם השיטה באתר השיחה - בדוגמה זו, שם השיטה הוא מִבְחָן
  • ה Ljava / lang / invoke / MethodType היא חתימת השיטה הדינמית של אתר השיחות - במקרה זה, זוהי () Ljava / util / function / Predicate

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

  • ה (Ljava / lang / Object;) Z היא חתימת שיטה שנמחקה המקבלת מופע של לְהִתְנַגֵד והחזרת א בוליאני.
  • ה REF_invokeStatic Main.lambda $ main $ 0: (Ljava / lang / String;) Z האם ה שיטת טיפול מצביע על ההיגיון הלמבדה בפועל.
  • ה (Ljava / lang / String;) Z היא חתימת שיטה שלא נמחקה המקבלת כזו חוּט והחזרת א בוליאני.

במילים פשוטות, JVM יעביר את כל המידע הנדרש לשיטת bootstrap. שיטת Bootstrap תשתמש בתורו במידע זה כדי ליצור מופע מתאים לְבַסֵס. לאחר מכן, ה- JVM יעביר את המופע הזה אל לְסַנֵן שיטה.

3.2. סוגים שונים של CallSiteס

ברגע שה- JVM רואה ייעוד דינמי בדוגמה זו לראשונה, היא מכנה את שיטת bootstrap. נכון לכתיבת מאמר זה, שיטת המגף של למבדה תשתמש ב InnerClassLambdaMetafactoryלייצר מעמד פנימי לממדה בזמן הריצה.

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

למרות שזה הסוג היעיל ביותר של ייעוד דינמי, זה בהחלט לא האפשרות היחידה הזמינה. לאמיתו של דבר, ג'אווה מספקת MutableCallSite ו VolatileCallSite כדי להתאים לדרישות דינמיות יותר.

3.3. יתרונות

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

אפשר לטעון נגד דחיית דור המעמדות הפנימיים עד לזמן הריצה. אולם, ה ייעוד דינמי לגישה יש כמה יתרונות על פני פתרון זמן ההידור הפשוט.

ראשית, ה- JVM אינו מייצר את המעמד הפנימי עד לשימוש הראשון בממבה. לָכֵן, לא נשלם עבור טביעת הרגל הנוספת הקשורה למעמד הפנימי לפני ביצוע ההמבה הראשון.

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

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

לבסוף, כתיבת לוגיקת האתחול וההצמדה ב- Java היא בדרך כלל קלה יותר מאשר מעבר של AST כדי ליצור חתיכת מורכבת של קוד-byt. כך, ייעוד דינמי יכול להיות (שבחינה סובייקטיבית) פחות שביר.

4. דוגמאות נוספות

ביטויים למבדה אינם המאפיין היחיד, וג'אווה היא בהחלט לא השפה היחידה בה משתמשים ייעוד דינמי. בחלק זה, נתוודע לעוד כמה דוגמאות להפעלה דינמית.

4.1. Java 14: רשומות

רשומות הן תכונת תצוגה מקדימה חדשה ב- Java 14 המספקת תחביר תמציתי ונחמד להכריז על כיתות שאמורות להיות בעלי נתונים מטומטמים.

להלן דוגמה רשומה פשוטה:

רשומה ציבורית צבע (שם מחרוזת, קוד int) {}

בהתחשב בשורה אחת פשוטה זו, מהדר Java מייצר יישומים מתאימים לשיטות גישה, toString, שווה, ו hashcode.

על מנת ליישם toString, שווה, אוֹ hashcode, ג'אווה משתמשת ייעוד דינמי. למשל, קוד התיקיה של שווים הוא כדלקמן:

גבול בוליאני ציבורי שווה (java.lang.Object); קוד: 0: aload_0 1: aload_1 2: invokedynamic # 27, 0 // InvokeDynamic # 0: שווה: (LColor; Ljava / lang / Object;) Z 7: ireturn

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

להפך, ג'אווה קוראת לשיטת bootstrap כדי לקשר בין היישום המתאים בזמן הריצה. לָכֵן, אורך קוד הביצה יישאר קבוע ללא קשר למספר השדות.

התבוננות מעמיקה יותר ב- bytecode מראה ששיטת bootstrap היא 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; טיעוני שיטה: # 8 שם צבע # 49; קוד # 51 REF_getField Color.name:Ljava/lang/String; # 52 REF_getField Color.code: אני

4.2. Java 9: ​​שרשור מחרוזות

לפני Java 9 יושמו שרשור מחרוזות לא טריוויאלי באמצעות StringBuilder. כחלק מ- JEP 280, כעת נעשה שימוש בשרשור המיתרים ייעוד דינמי. למשל, בואו נשרשר מחרוזת קבועה עם משתנה אקראי:

"אקראי-" + ThreadLocalRandom.current (). nextInt ();

כך נראה קוד ה- Byte לדוגמא זו:

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

יתר על כן, שיטות האתחול לשרשור מחרוזות נמצאות ב- StringConcatFactory מעמד:

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 / String; / lang / Object;) Ljava / lang / invoke / CallSite; טיעוני שיטה: # 36 אקראי- \ u0001

5. מסקנה

במאמר זה, ראשית, הכרנו את הבעיות שהאינדי מנסה לפתור.

ואז, על ידי מעבר בדוגמה פשוטה של ​​ביטוי למבדה, ראינו איך ייעוד דינמי עובד באופן פנימי.

לבסוף ספרנו כמה דוגמאות אחרות לאינדי בגרסאות האחרונות של Java.


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