# 2020q3: Java 虛擬機器 ## stack frame - [github commit](https://github.com/hankluo6/pitifulvm/commit/50d78942c95eeb6744465670cb36457747e41879) 原本 pitifulVM 中實現 stack frame 的 `op_stack` 為 `int32_t` type 的陣列,但在 Dhrystone 中需要可以存放 `long` 大小的 stack,且為了之後可以支援浮點數,將 stack 重新改寫,用 `stack_frame_t` 結構表示 JVM 中的一個 stack frame: ```c typedef struct { int max_size; int size; stack_entry_t *store; } stack_frame_t; ``` 其中 `stack_entry_t` 為主要存放資料的地方,結構如下: ```c typedef struct { unsigned char entry[8]; stack_entry_type_t type; } stack_entry_t; ``` `type` 用來標示這個 data 的 type,目前支援存放 8 個 bytes 的資料,但這樣實作較浪費空間,不管型態為何都會佔用 8 個 bytes。之後或許可以使用 [Arrays of Length Zero](https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html) 的結構來減少空間浪費。 在 JVM 中操作 stack 的指令存有 `bipush`, `sipush`,但相對算術運算的指令只有 `int` 型態,`bipush` 會將資料放在 `entry[0]` 上,`short` 則將資料放在 `entry[1]` 上。n所以在取出資料時,為了方便處理,我使用 `stack_to_int` 函式將取出來的資料轉型成 64 bits 的整數: ```c int64_t stack_to_int(unsigned char *entry, size_t size) { switch (size) { /* int8_t */ case 1: { int8_t value; memcpy(&value, entry, size); return value; } /* int16_t */ case 2: { int16_t value; memcpy(&value, entry, size); return value; } /* int32_t */ case 4: { int32_t value; memcpy(&value, entry, size); return value; } /* int64_t */ case 8: { int64_t value; memcpy(&value, entry, size); return value; } default: assert(0 && "stack entry error"); return -1; } } ``` 這樣可以保證在 stack 中的操作都為 64 bits 的整數,才不會有類型不同的問題。 ## JVM Heap * [github commit](https://github.com/hankluo6/pitifulvm/commit/a16ea0ea4fcaa9e97cbe620c1d3d8a4d122fa704) > 根據 [Java Virtual Machine Specification](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.5.3) > > The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated. 我們需要一個實際空間來存放類別資訊以及物件資訊,這邊我將 heap 分成兩個區塊: * `class heap` 用來儲存所有類別資訊: ```c typedef struct { u2 length; meta_class_t **class_info; } class_heap_t; ``` 其內部有一個 type 為 `meta_class_t *` 的陣列,而 `meta_class_t` 實作如下: ```c typedef struct { class_file_t *clazz; char *path; char *name; } meta_class_t; ``` 用來存放 class 的 meta data,而 `class_file_t` 則當作 Runtime Area,存放程式執行時所需資料。 * `object heap` 用來儲存所有 `new` 產生的物件資訊: ```c typedef struct { u2 length; object_t** objects; } object_heap_t; ``` 與 `class heap` 一樣為陣列,其 `object_t` 用來當作 Java 物件: ```c typedef struct { variable_t* ptr; class_file_t *type; size_t field_count; } object_t; ``` `type` 指向此物件的類別資訊,而 `ptr` 則指向一個陣列,此陣列之長度是由此物件的類別內 field 的數量決定。 ```c typedef union { u1 char_value; u2 short_value; u4 int_value; u8 long_value; void *ptr_value; } value_t; typedef struct { value_t value; int type; } variable_t; typedef enum { NONE = 0, BYTE = 1, SHORT = 2, INT = 3, LONG = 4, PTR = 5, ARRAY_PTR = 6, MULTARRAY_PTR = 7, STR_PTR = 8 } variable_type_t; ``` 每個 `variable` 代表物件內的一個 field,利用 `union` 來選擇該 field 的值,`ptr_value` 為參考 (reference) 時使用。`variable_type_t` 用來標示 field 的 type。 在每次要使用到 class 的時候,都會先去 `class heap` 中尋找有無紀錄此類別,如果找不到則開檔並呼叫 `get_class` 取得該類別的 constant pool 等資訊,並加入至 `class_heap` 當中。 ```c char *class_name = find_class_name_from_index(index, clazz); class_file_t *new_class = find_class_from_heap(class_name); if (new_class == NULL) { char *tmp = malloc((strlen(class_name) + 7 + strlen(prefix)) * sizeof(char)); strcpy(tmp, prefix); strcat(tmp, class_name); /* attempt to read given class file */ FILE *class_file = fopen(strcat(tmp, ".class"), "r"); assert(class_file && "Failed to open file"); /* parse the class file */ new_class = malloc(sizeof(class_file_t)); *new_class = get_class(class_file); int error = fclose(class_file); assert(!error && "Failed to close file"); add_class(new_class, NULL, tmp); method_t *method = find_method("<clinit>", "()V", new_class); if (method) { local_variable_t own_locals[method->code.max_locals]; variable_t *exec_res = execute(method, own_locals, new_class); assert(exec_res->type == STACK_ENTRY_NONE && "<clinit> must be no return"); free(exec_res); } free(tmp); } ``` 此過程在每個對 class 操作的指令都會檢查一次 (e.g. `new`, `invoke`, `getstatic`)。 ## Instruction * `new` `new` 指令會先檢查 class 是否存在 (如上方程式碼),找到 class 後在 object heap 中新增一個物件,其大小為 `field count * sizeof(vaariable_t)`。接著尋找該 class 是否有 [`<clinit>` method](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.9),有的話也在此時呼叫。 > 根據 [Java: What is the difference between <init> and <clinit>? ](https://stackoverflow.com/a/8517155)`<clinit>` 是用來初始化 static field,此過程需比 `<init> (初始化 non static field)` 還要早。 * `invokespecial` 為呼叫 constructor (`<init>`) 時的指令,先找到 class 及 method,與 `invokestatic` 一樣將所需參數從 stack 中取出放入 local variable 中,且 local variable 的第 0 個位置存放該物件的指標。 ```c own_locals[0].entry.ptr = obj; own_locals[0].type = STACK_ENTRY_REF; ``` 再執行 `execute` 即可。如有繼承關係時,在 `<init>` method 中會透過 `invokespecial` 執行 parent class 的 `<init>`,此過程會一直持續直到呼叫 java.lang.Object 的 `<init>` 停止。 :::info ``` public java.lang.Object(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=0, locals=1, args_size=1 0: return LineNumberTable: line 3: 0 ``` java.lang.Object 的 `<init>` 只有一行 `return` 指令,不會繼續呼叫 `invokespecial`。 ::: * `invokevirtual` `invokevirtual` 會根據 method 的 access flag,選擇執行 native method 或 java method,java method 的實作與 `invokestatic` 類似,而 native method 會在下方說明。 * `invokedynamic` `invokedynamic` 是 Java 7 中新增的指令 ![](https://www.baeldung.com/wp-content/uploads/2020/05/Untitled-Diagram.svg) 當第一次執行 `invokedynamic` 指令時,會呼叫對應的 bootstrap method,並回傳一個 [CallSite](hhttps://docs.oracle.com/javase/8/docs/api/java/lang/invoke/CallSite.html) 物件,當下次再執行相同的 `invokedynamic` 時,利用這個 CallSite 就能更快速的執行 method。這個指令優點在於能夠在執行時期建立需要執行的程式,常見於出現 anonymous class 的應用 (e.g. Lambda, String Concatenation)。 在 Dhrystone 中出現 `invokedynamic` 的地方為 String Concatenation,且為了實作方便,移除 CallSite 的功能,在每次執行 `invokedynamic` 時都從 bootstrap method table 尋找對應的方法來呼叫。 首先在 parse class 時透過 `read_bootstrap_attribute` 把 bootstrap method table 建立起來,在 `invokedynamic` 時會先從 constant pool 找到 tag 為 `InvokeDynamic` 的 entry,這個 entry 存有兩個值,分別是對應 bootstrap method 的 index 及所需要的參數的在 constant pool 中的 index。 在 bootstrap method table 找到的 method 會發現為: `java/lang/invoke/StringConcatFactory.makeConcatWithConstants`,其只需要一個 String 類型的參數,這個參數會以 unicode 的字元及 constants 組成,unicode 的部份為程式呼叫時給予的參數,而 constants 則是在編譯時期就定義好的。 考慮以下程式: ```java public class Strings { public static void main(String[] args) { String str1 = "Hello"; String str2 = " Jvm "; System.out.println(str1 + " constant " + str2); } } ``` 預期 `javap -v` 以下輸出: ``` Constant pool: ... #4 = Fieldref #20.#21 // java/lang/System.out:Ljava/io/PrintStream; #5 = InvokeDynamic #0:#25 // #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; ... #23 = MethodHandle 6:#33 // 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; #24 = String #34 // \u0001 constant \u0001 ... public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 ... 11: invokedynamic #5, 0 // InvokeDynamic#0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; ... BootstrapMethods: 0: #23 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; Method arguments: #24 \u0001 constant \u0001 ``` `str1` 與 `str2` 被 unicode \u0001 取代,用來標示在執行時要被取代的位置,而 `constant` 字串則是在編譯時,就已經存入 constant pool #24 當中。 * `putfield`, `getfield` class 檔中關於 field 的資訊只有提供 `field name`, `field descriptor` 以及 `class name`,並沒有給予 field 在 class 中的相對位置,所以這邊我先用迴圈找尋每個 field 直到找到匹配的名字後回傳。 ```c for (i = 0; i < obj->type->fields_count; i++, field++) { if (strcmp(field->name, name) == 0) { return &obj->ptr[i]; } } ``` TODO: 改用 hash 實作 $O(1)$ 的查找 * `ldc`, `ldc2_w` 這兩個指令用來將 constant pool 的資料放到 stack 中 `ldc` 目前只支援 int 與 string。 >The run-time constant pool entry at index either must be a run-time constant of type int or float, or a reference to a string literal, or a symbolic reference to a class, method type, or method handle `ldc2_w` 則只支援 long。 >The run-time constant pool entry at the index must be a run-time constant of type long or double * `tableswitch` ```c= int32_t base = pc; pc += 3; pc -= (pc % 4); uint8_t defaultbyte1 = code_buf[pc], defaultbyte2 = code_buf[pc + 1], defaultbyte3 = code_buf[pc + 2], defaultbyte4 = code_buf[pc + 3]; int32_t _default = ((defaultbyte1 << 24) | (defaultbyte2 << 16) | (defaultbyte3 << 8) | defaultbyte4); pc += 4; uint8_t lowbyte1 = code_buf[pc], lowbyte2 = code_buf[pc + 1], lowbyte3 = code_buf[pc + 2], lowbyte4 = code_buf[pc + 3]; int32_t low = ((lowbyte1 << 24) | (lowbyte2 << 16) | (lowbyte3 << 8) | lowbyte4); pc += 4; uint8_t highbyte1 = code_buf[pc + 1], highbyte2 = code_buf[pc + 1], highbyte3 = code_buf[pc + 2], highbyte4 = code_buf[pc + 3]; int32_t high = ((highbyte1 << 24) | (highbyte2 << 16) | (highbyte3 << 8) | highbyte4); pc += 4; int32_t key = pop_int(op_stack); if (key >= low && key <= high) { unsigned index = pc + ((key - low) * 4); uint8_t byte1 = code_buf[index], byte2 = code_buf[index + 1], byte3 = code_buf[index + 2], byte4 = code_buf[index + 3]; uint32_t addr = ((byte1 << 24) | (byte2 << 16) | (byte3 << 8) | byte4); pc = base + addr; } else { pc = base + _default; } ``` 根據規格書: >Immediately after the tableswitch opcode, between zero and three bytes must act as padding 第 2 ~ 3 行是為了做 Alignment,5 ~ 13 行決定 default, low, high 的位置,16 ~ 24 行根據 switch 的值決定選擇的位置。 * `getstatic`, `putstatic` 先找到 class 的資訊,接著從 class 中每個 field 比對名字與類型,找到後回傳: ```c field_t *find_field(const char *name, const char *desc, class_file_t *clazz) { field_t *field = clazz->fields; for (u2 i = 0; i < clazz->fields_count; ++i, field++) { if (!(strcmp(name, field->name) || strcmp(desc, field->descriptor))) return field; } return NULL; } ``` 如果找不到,則以 class 的 parent 繼續尋找。 * `getfield`, `putfield` 與 `getstatic`, `putstatic` 很像,差別在於找 field 時是用 object 去尋找,透過 object 中的 field 比對,將對應的 field 位置回傳。 ```c variable_t *find_field_addr(object_t *obj, char *name) { field_t *field = obj->type->fields; u2 i; for (i = 0; i < obj->type->fields_count; i++, field++) { if (strcmp(field->name, name) == 0) { return &obj->ptr[i]; } } return NULL; } ``` * `newarray`, `multianewarray` 分別透過 `create_array` 及 `create_two_dimension_array` 來實現。為了要正確釋放記憶體,我將分配出來的 array 當成物件存在 object heap 當中。 ```c void *create_array(class_file_t *clazz, int count) { void *arr = malloc(count * sizeof(int)); object_t *arr_obj = malloc(sizeof(object_t)); arr_obj->ptr = malloc(sizeof(variable_t)); arr_obj->ptr->type = ARRAY_PTR; arr_obj->ptr->value.ptr_value = arr; arr_obj->type = clazz; object_heap.objects[object_heap.length++] = arr_obj; arr_obj->field_count = 1; return arr; } ``` `create_array` 會建立一個 object,其中 object 內的 ptr 欄位為一個 `variable_t` 空間的位置, `variable_t` 內部的 `ptr_value` 存放 array 的位置。 ```c void **create_two_dimension_array(class_file_t *clazz, int count1, int count2) { int **arr = malloc(count1 * sizeof(int *)); for (int i = 0; i < count1; ++i) { arr[i] = malloc(count2 * sizeof(int)); } object_t *arr_obj = malloc(sizeof(object_t)); arr_obj->ptr = malloc(sizeof(variable_t) * 3); arr_obj->ptr[0].type = MULTARRAY_PTR; arr_obj->ptr[0].value.ptr_value = arr; arr_obj->ptr[1].type = INT; arr_obj->ptr[1].value.int_value = count1; arr_obj->ptr[2].type = INT; arr_obj->ptr[2].value.int_value = count2; arr_obj->type = clazz; object_heap.objects[object_heap.length++] = arr_obj; arr_obj->field_count = 3; return (void **)arr; } ``` `create_two_dimension_array` 與 `create_array` 類似,但這邊需要紀錄二維陣列的欄位數,所以我用一個 object 表示二維陣列,其中有三個 field,第一個 field 為二維陣列的位置,第二個 field 存二維陣列的 row 數量,第三個 field 為 column 的數量。 ## Native Method 我將 java.lang 與 java.io 中會使用到的類別實作,並建立需要的 method 與 field,如此便能依照喜好決定哪些 method 設定為 native,也不需要把所有需要的 Java 函式庫都載入,造成 pitifulVM 沒有實作的部份被讀取到產生錯誤。 :::danger 直接編譯 java.lang 與 java.io package 會產生 module conflict,需使用 `javac --patch-module java.base=java` 來編譯。 ::: 當 `invokestatic` 與 `invokevirtual` 中 method 的 access flag 為 `ACC_NATIVE` 時,便會呼叫 `native.c` 中對應的實做。 到這邊已經能順利執行 Dhrystone 程式,要注意程式沒有垃圾回收功能,當 heap 數量滿時會產生錯誤: ``` hank@hank-VivoBook-ASUSLaptop-X580GD-N580GD:~/桌面/SystemSoftwareClass/pitifulvm$ ./jvm Dhrystone/dhry.class Dhrystone Benchmark, Version 2.1 (Language: Java) Please give the number of runs through the benchmark: 1000 Execution starts, 1000 runs through Dhrystone total time: 42ms Result: 23809 dhrystone/sec. ``` ### TODO - Garbage collection ###### tags: `linux2020`