יסודות Bytecode

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

פורמט קוד הביצה

Bytecodes הם שפת המכונה של המכונה הווירטואלית של Java. כאשר JVM טוען קובץ מחלקה, הוא מקבל זרם אחד של קודי byt עבור כל שיטה בכיתה. זרמי ה- bytecodes מאוחסנים באזור השיטה של ​​ה- JVM. קודי ה- BYT לשיטה מבוצעים כאשר מופעלת שיטה זו במהלך הפעלת התוכנית. ניתן לבצע אותם על ידי פרשנות, קומפילציה בדיוק בזמן או כל טכניקה אחרת שנבחרה על ידי המעצב של JVM מסוים.

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

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

// זרם Bytecode: 03 3b 84 00 01 1a 05 68 3b a7 ff f9 // פירוק: iconst_0 // 03 istore_0 // 3b iinc 0, 1 // 84 00 01 iload_0 // 1a iconst_2 // 05 imul // 68 istore_0 // 3b goto -7 // a7 ff f9 

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

כל החישובים ב- JVM מתמקדים בערימה. מכיוון של- JVM אין רושמים לאחסון ערכים אביטריים, יש לדחוף את הכל לערימה לפני שניתן יהיה להשתמש בו בחישוב. הוראות Bytecode פועלות אפוא בעיקר על הערימה. לדוגמא, ברצף הקוד המקודד לעיל משתנה מקומי מוכפל בשניים על ידי דחיפה ראשונה של המשתנה המקומי אל הערימה iload_0, ואז דחיפת שניים על הערימה עם iconst_2. לאחר ששני המספרים השלמים נדחקו למחסנית, imulההוראה מוציאה למעשה את שני המספרים השלמים מהמחסנית, מכפילה אותם ודוחפת את התוצאה חזרה למחסנית. התוצאה מוצגת מראש הערימה ומאוחסנת בחזרה למשתנה המקומי על ידיistore_0הוראה. ה- JVM תוכנן כמכונה מבוססת מחסנית ולא כמכונה מבוססת רישום כדי להקל על יישום יעיל בארכיטקטורות דלות ברשומות כמו אינטל 486.

טיפוסים פרימיטיביים

ה- JVM תומך בשבעה סוגי נתונים פרימיטיביים. מתכנתים של ג'אווה יכולים להכריז ולהשתמש במשתנים מסוגי נתונים אלה, וקודי בייטה של ​​Java פועלים על פי סוגי נתונים אלה. שבעת הסוגים הפרימיטיביים מפורטים בטבלה הבאה:

סוּג הַגדָרָה
byte בית אחד חתם על מספר השלמים המלא של שניים
short שני בתים חתמו על מספר שלם משלים של שני
int 4 בתים חתמו על מספר שלם משלים של שניים
long 8 בתים חתמו על מספר שלם משלים של שני
float 4-בתים IEEE 754 דיוק יחיד לצוף
double 8 בתים IEEE 754 דיוק כפול לצוף
char דמות Unicode ללא חתימה בת 2 בתים

הסוגים הפרימיטיביים מופיעים כאופרנדים בזרמי קוד קוד. כל הסוגים הפרימיטיביים שתופסים יותר מבת אחד מאוחסנים בסדר גדול האנדיאנים בזרם ה- bytecode, כלומר בתים מסדר גבוה קודמים לבתים בסדר נמוך יותר. לדוגמה, כדי לדחוף את הערך הקבוע 256 (hex 0100) אל הערימה, תשתמש sipushבקוד ואחריו אופראנד קצר. הקצר מופיע בזרם ה- bytecode, המוצג למטה, כ- "01 00" מכיוון שה- JVM הוא אנדיאני גדול. אם ה- JVM היה מעט אנדיאני, הקצר היה מופיע כ" 00 01 ".

// זרם Bytecode: 17 01 00 // פירוק: סיפוש 256; // 17 01 00

קוד קידוד של Java מציין בדרך כלל את סוג הפעולות שלהם. זה מאפשר לאופרטים להיות עצמם, ללא צורך לזהות את סוגם בפני ה- JVM. לדוגמה, במקום שיהיה אופקוד אחד שדוחף משתנה מקומי למחסנית, ל- JVM יש כמה. Opcodes iload, lload, fload, ו dloadלדחוף משתנים מקומיים של int סוג, ארוך, לצוף, כפול, בהתאמה, על המחסנית.

דוחף קבועים על הערימה

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

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

אופקוד אופרנדים תיאור
iconst_m1 (אף אחד) דוחף int -1 לערימה
iconst_0 (אף אחד) דוחף את int 0 אל הערימה
iconst_1 (אף אחד) דוחף את int 1 אל הערימה
iconst_2 (אף אחד) דוחף את int 2 אל הערימה
iconst_3 (אף אחד) דוחף את int 3 אל הערימה
iconst_4 (אף אחד) דוחף את int 4 אל הערימה
iconst_5 (אף אחד) דוחף את int 5 אל הערימה
fconst_0 (אף אחד) דוחף לצוף 0 אל הערימה
fconst_1 (אף אחד) דוחף לצוף 1 על הערימה
fconst_2 (אף אחד) דוחף לצוף 2 על הערימה

קידודי ה- opc המוצגים בטבלה הקודמת דוחפים כניסות וצפות, שהם ערכים של 32 סיביות. רוחב כל חריץ בערימת Java הוא 32 ביט. לכן בכל פעם שדחיפה של int או float אל הערימה היא תופסת חריץ אחד.

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

אופקוד אופרנדים תיאור
lconst_0 (אף אחד) דוחף ארוך 0 אל הערימה
lconst_1 (אף אחד) דוחף ארוך 1 אל הערימה
dconst_0 (אף אחד) דוחף כפול 0 אל הערימה
dconst_1 (אף אחד) דוחף כפול 1 אל הערימה

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

אופקוד אופרנדים תיאור
aconst_null (אף אחד) דוחף הפניה לאובייקט null על הערימה

שני קודנים מצביעים על הקבוע לדחיפה באמצעות אופרנד שעוקב מיד אחרי האופקוד. קידודים אלה, המוצגים בטבלה הבאה, משמשים לדחיפת קבועים שלמים שנמצאים בטווח התקף עבור סוגי בתים או קצרים. הבית או הקצר שעוקב אחרי ה- opcode מורחב ל- int לפני שהוא נדחף אל הערימה, מכיוון שכל חריץ בערימת Java הוא 32 סיביות. פעולות על בתים ומכנסיים קצרים שנדחקו אל הערימה נעשות למעשה במקבילים שלהם.

אופקוד אופרנדים תיאור
bipush בת 1 מרחיב את byte1 (סוג בתים) ל- int ודוחף אותו אל הערימה
sipush בת 1, בת 2 מרחיב את byte1, byte2 (סוג קצר) ל- int ודוחף אותו אל הערימה

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

אינדקס הבריכה הקבוע הוא ערך לא חתום שעוקב מיד אחר ה- opcode בזרם ה- bytecode. Opcodes lcd1ו lcd2לדחוף פריט 32 סיבי על המחסנית, כגון int או float. ההבדל בין lcd1לבין lcd2הוא lcd1יכול להתייחס רק למקומות ברכה קבוע אחת דרך 255 בגלל האינדקס שלו הוא רק 1 בייט. (למיקום בריכה קבוע אפס אין שימוש.) lcd2יש אינדקס של 2 בתים, כך שהוא יכול להתייחס לכל מיקום בריכה קבוע. lcd2wיש לו גם אינדקס של 2 בתים, והוא משמש להתייחס לכל מיקום בריכה קבוע המכיל ארוך או כפול, שתופס 64 סיביות. הקודים שצפויים לדחוף קבועים מהבריכה הקבועה מוצגים בטבלה הבאה:

אופקוד אופרנדים תיאור
ldc1 אינדקסבייט 1 דוחף כניסה של 32 סיביות ל- constant_pool שצוינה על ידי indexbyte1 אל הערימה
ldc2 indexbyte1, indexbyte2 דוחף ערך 32-bit constant_pool שצוין על ידי indexbyte1, indexbyte2 על הערימה
ldc2w indexbyte1, indexbyte2 דוחף את הערך של 64 סיביות constant_pool שצוין על ידי indexbyte1, indexbyte2 אל הערימה

דחיפת משתנים מקומיים אל הערימה

Local variables are stored in a special section of the stack frame. The stack frame is the portion of the stack being used by the currently executing method. Each stack frame consists of three sections -- the local variables, the execution environment, and the operand stack. Pushing a local variable onto the stack actually involves moving a value from the local variables section of the stack frame to the operand section. The operand section of the currently executing method is always the top of the stack, so pushing a value onto the operand section of the current stack frame is the same as pushing a value onto the top of the stack.

The Java stack is a last-in, first-out stack of 32-bit slots. Because each slot in the stack occupies 32 bits, all local variables occupy at least 32 bits. Local variables of type long and double, which are 64-bit quantities, occupy two slots on the stack. Local variables of type byte or short are stored as local variables of type int, but with a value that is valid for the smaller type. For example, an int local variable which represents a byte type will always contain a value valid for a byte (-128 <= value <= 127).

Each local variable of a method has a unique index. The local variable section of a method's stack frame can be thought of as an array of 32-bit slots, each one addressable by the array index. Local variables of type long or double, which occupy two slots, are referred to by the lower of the two slot indexes. For example, a double that occupies slots two and three would be referred to by an index of two.

Several opcodes exist that push int and float local variables onto the operand stack. Some opcodes are defined that implicitly refer to a commonly used local variable position. For example, iload_0 loads the int local variable at position zero. Other local variables are pushed onto the stack by an opcode that takes the local variable index from the first byte following the opcode. The iload instruction is an example of this type of opcode. The first byte following iload is interpreted as an unsigned 8-bit index that refers to a local variable.

Unsigned 8-bit local variable indexes, such as the one that follows the iload instruction, limit the number of local variables in a method to 256. A separate instruction, called wide, can extend an 8-bit index by another 8 bits. This raises the local variable limit to 64 kilobytes. The wide opcode is followed by an 8-bit operand. The wide opcode and its operand can precede an instruction, such as iload, that takes an 8-bit unsigned local variable index. The JVM combines the 8-bit operand of the wide instruction with the 8-bit operand of the iload instruction to yield a 16-bit unsigned local variable index.

The opcodes that push int and float local variables onto the stack are shown in the following table:

Opcode Operand(s) Description
iload vindex pushes int from local variable position vindex
iload_0 (none) דוחף int ממיקום משתנה מקומי אפס
iload_1 (אף אחד) דוחף int ממיקום משתנה מקומי אחד
iload_2 (אף אחד) דוחף int ממיקום משתנה מקומי שני
iload_3 (אף אחד) דוחף int ממיקום משתנה מקומי שלוש
fload vindex דוחף לצוף ממיקום משתנה מקומי vindex
fload_0 (אף אחד) דוחף צף ממיקום משתנה מקומי אפס
fload_1 (אף אחד) דוחף לצוף ממצב משתנה מקומי אחד
fload_2 (אף אחד) דוחף צף ממיקום משתנה מקומי שני
fload_3 (אף אחד) דוחף צף ממצב שלוש מקומי משתנה

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