不是大佬教你學Java虛擬機:內存管理+線程+JIT,你就不打算學?

大數據架構師 發佈 2022-11-25T19:08:42.030418+00:00

400628: b9000fe0 str w0,40062c: b9000be1 str w1,400630: b9400fe1 ldr w1,400634: b9400be0 ldr w0,400638: 0b000020 add w0, w1, w0。

內存管理

內存管理也稱為垃圾回收(Garbage Collection),指的是虛擬機在應用程式運行時管理應用程式使用的內存。Java代碼中只需要分配內存而不需要考慮釋放內存,內存釋放的工作交由虛擬機處理。虛擬機在內存管理中通常要做以下4方面的事情。

1)分配(Allocate):從OS請求內存,虛擬機需要考慮何時請求內存,請求內存的粒度。

2)使用(Use):針對應用程式的請求,設計連續內存或者非連續內存的管理,為應用程式提供高速內存分配。

3)回收(Recycle):當虛擬機管理的內存都被使用時,需要識別內存中的活躍對象,對活躍對象保留或者對非活躍對象釋放,完成非活躍對象占用內存的回收,並將回收後的內存重新用於應用程式的分配。

4)釋放(Free):向OS歸還內存,虛擬機需要考慮何時釋放內存,釋放內存的粒度等。

不是所有的虛擬機都包含分配釋放這兩個步驟,主要原因是虛擬機在實現時可以藉助一些內存管理庫來代替自己提供這些功能。

另外,需要指出的是,這裡所說的使用和回收是大家常提到的分配和回收。本節使用分配和使用來區別虛擬機向OS請求內存及應用程式向虛擬機請求內存。為了保持閱讀的一致性,後文統一使用分配和回收替代此處的使用和回收。

然而設計和實現一款垃圾回收器並不容易,不同的應用場景對於垃圾回收的訴求也不相同。一款垃圾回收器主要從以下幾個方面衡量。

1)吞吐量:指的是在一段時間內回收的內存量。吞吐量越大說明垃圾回收器的效率越高。

2)停頓時間:指的是垃圾回收器在垃圾回收過程中可能會要求應用暫停以配合垃圾回收的工作。停頓時間越長,則說明垃圾回收器對應用的影響越大,停頓時間越短,說明垃圾回收器對應用的影響越小。

3)數據訪問的局部性:垃圾回收器在進行垃圾回收時可能會調整內存中活躍對象的位置,當對象的位置發生變化後會影響應用訪問內存的速度,從而影響應用程式執行的效率。

4)額外資源消耗:垃圾回收器實現時都需要額外的內存管理其內部數據結果。不同的垃圾回收器採用的算法不同,使用的數據結果也不同,占用的額外資源也不同。通常來說,額外資源消耗越少,說明垃圾回收器越優秀。

本文後面將詳細介紹JVM中實現的垃圾回收器,讀者在閱讀相關章節時可以從這個幾個方面思考垃圾回收器實現的優劣。

線程管理

通常高級語言都支持多線程,所以虛擬機需要考慮如何高效地支持多線程,例如高級語言的線程和虛擬機的線程以及作業系統的線程關係是什麼?是否可以支持協程?這些內容都非常複雜,部分內容也和垃圾回收密切相關,但限於篇幅,本文不展開介紹。

以JVM為例,JVM為了執行字節碼或者編譯代碼,需要為代碼準備執行的線程和線程棧。例如當啟動JVM後,啟動線程將變成執行Java的main線程,如果在Java代碼中產生新的線程,則由OS產生線程。

所以,從這個角度來說Java字節碼或者編譯代碼的執行和C語言的執行完全一致。但是JVM為了更好地管理和執行代碼,實現了線程對象和線程棧對象,線程對象和線程棧對象也是分配在JVM的本地堆中。線程對象和線程棧對象除了會關聯真正底層OS的線程之外,還會存儲一些額外的信息,這些信息用於描述當前線程和線程棧的信息,比如線程屬於哪個Java線程對象、關聯哪個類加載器、線程棧的調用鏈信息等。

另外,高級語言通常會支持多言語的互操作,當進行互操作時,需要考慮不同語言線程執行的約定,例如參數和返回值如何組織,內存是否可以互訪問等。在JVM中支持通過JNI(Java Native Interface)的方式調用C/C++代碼,但是這樣的互操作除了要考慮線程管理以外,還要考慮內存的影響,特別是垃圾回收的影響。例如JVM在執行一些JNI時通常會阻塞垃圾回收的執行(例如調用JNI的Critical API),當然阻塞與否還與垃圾回收器的實現有關。在第2章中介紹安全點相關的知識時會進一步展開介紹。

擴展閱讀:JIT概述

虛擬機的實現通常可以劃分為3部分:運行時(Run-Time)、編譯優化(JIT)和垃圾回收。已經有較多的書籍和文章介紹了運行時,本文不再介紹。垃圾回收是本文的重點,後面會詳細介紹。關於JIT的相關介紹並不多,同時JIT也非常複雜,特別是編譯優化的相關知識。

本節在Linux/AArch64平台的基礎上,通過一個簡單的例子演示JIT的基本概念。

首先從一個簡單的C代碼例子出發,如下所示:

#include <stdio.h>

int add(int a, int b){

return a + b;

}

int main(){

printf("%d\n",add(4,5));

return 0;

}

該代碼片段的功能非常簡單,其中函數add實現加法功能。這個add例子和1.4.1節中Java的add功能完全相關,都是完成兩個整數的加法計算並返回結果。

本節構造C的add函數就是為了讓讀者可以方便地理解在編譯優化時Java的函數(字節碼片段)可以被一個C/C++的函數替代。當然,這裡省略了JVM構造這個C語言的add函數的過程,這本質上就是編譯優化要做的工作。

使用gcc進行編譯,這裡先使用O2的編譯優化級別,命令如下:

gcc -O2 -o test test.c

編譯後使用objdump命令查看add函數的反彙編代碼:

0000000000400650 <add>:

400650: 0b010000 add w0, w0, w1

400654: d65f03c0 ret

注意:在AArch64平台中有31個通用寄存器,其中x0~x7用於傳遞參數和返回值。

w0~w7是x0~x7的低32位,用於傳遞32位的參數,當函數的參數個數超過8個時,通過棧傳遞。

在這個例子中,add的兩個參數通過w0和w1傳入,通過add指令完成加法,結果存放在寄存器w0中,通過ret返回函數的執行結果。

假設JVM識別Java的add函數為熱點,現在也知道add函數對應的彙編代碼,那麼還有一個問題,就是如何讓JVM替換原來的add函數而執行編譯後的代碼。下面通過一個例子演示C/C++代碼直接執行編譯後代碼的過程。首先將編譯後的代碼作為輸入數據,表示待執行的函數,然後通過mmap函數將數據加載到內存區,並設置內存區可以執行(PROT_EXEC),最後再通過函數調用執行相關代碼。代碼示例如下:

#include<stdio.h>

#include<memory.h>

#include<sys/mman.h>

typedef int (* add_func)(int a, int b);

int main() {

char code[] = {

0x00,0x00,0x01,0x0b, //0x0b010000, 等價於指令 add w0, w0, w1

0xc0,0x03,0x5f,0xd6 //0xd65f03c0, 等價於指令 ret

}; //參考objdump對add函數的反彙編代碼

void * code_cache = mmap(NULL, sizeof(code), PROT_WRITE | PROT_EXEC,

MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);

memcpy(code_cache, code, sizeof(code));

add_func p_add = (add_func)code_cache;

printf(「%d\n」, p_add(4,5));

return 0;

}

示例中通過一個函數調用完成彙編代碼的執行。實際上除了使用函數調用以外,還可以直接通過jmp完成相關的調用(函數調用的本質是通過call指令完成控制流的轉移)。JVM執行編譯後的代碼原理和示例介紹基本類似,通過識別熱點代碼(例如Java中的add函數),並對熱點代碼進行編譯優化,產生目標機器代碼(類似於此處C代碼中add函數的反彙編代碼),然後執行目標機器代碼。

在add函數的編譯過程中直接使用了O2的編譯優化級別,gcc默認的編譯優化級別為O0。下面是使用默認編譯優化級別產生的目標文件反彙編的結果。

0000000000400624 <add>:

400624: d10043ff sub sp, sp, #0x10

400628: b9000fe0 str w0, [sp, #12]

40062c: b9000be1 str w1, [sp, #8]

400630: b9400fe1 ldr w1, [sp, #12]

400634: b9400be0 ldr w0, [sp, #8]

400638: 0b000020 add w0, w1, w0

40063c: 910043ff add sp, sp, #0x10

400640: d65f03c0 ret

比較O2和O0的編譯優化結果可以發現,O2的代碼質量遠高於O0的代碼質量(指令明顯少了很多)。那麼O2採用的編譯優化會更加複雜,編譯耗時也更多。JVM中C1和C2編譯器的目的也是生成不同指令的編譯代碼,可以簡單理解為gcc不同編譯級別產生的代碼。當然JVM中C1和C2採用了不同的技術,使用的IR和編譯優化手段都不相同。

本文給大家講解的內容是Java虛擬機和垃圾回收基礎知識:內存+線程+JIT概述

  1. 下篇文章給大家講解的內容是JVM中垃圾回收相關的基本知識:GC算法分類
  2. 覺得文章不錯的朋友可以轉發此文關注小編;
  3. 感謝大家的支持!
關鍵字: