1. 程式人生 > >Java常見面試題—JVM執行時資料區域

Java常見面試題—JVM執行時資料區域

JVM—執行時資料區域

這裡寫圖片描述

JVM在執行JAVA程式時會把它管理的記憶體區域劃分為若干個不同的資料區域,統稱為執行時資料區,由圖可見JVM程式所佔的內可劃分成5個部分:程式計數器、虛擬機器棧(執行緒棧)、本地方法棧、堆(heap)和方法區(內含常量池),其中方法區和堆被所有執行緒共享。下面分別介紹各部分的功能:

程式計數器

JVM是多執行緒的,每一個執行緒都有一個獨立的程式計數器(JVM是多執行緒的,為了執行緒切換後能恢復到正確的執行位置),是一塊較小的記憶體空間,它與執行緒共存亡。JVM中的程式計數器指向的是正在執行的位元組碼地址,可以看作是當前執行緒所執行的位元組碼的行號指示器。

如果執行緒正在執行的是一個java方法,程式計數器記錄的是正在執行的虛擬機器位元組碼指令地址;
若執行緒執行的是Native方法,程式計數器則為Undefined。
程式計數器是JVM中唯一一個沒有規定任何OutOfMemoryError情況的區域。

Java虛擬機器棧

一個執行緒一個棧,並且生命週期與執行緒相同。它的內部由一個個棧幀構成,一個棧幀代表一個呼叫的方法,執行緒在每次方法呼叫執行時建立一個棧幀然後壓棧,棧幀用於存放區域性變數、運算元、動態連結、方法出口等資訊。方法執行完成後對應的棧幀出棧。我們平時說的棧記憶體就是指這個棧。

一個執行緒中的方法可能還會呼叫其他方法,這樣就會構成方法呼叫鏈,而且這個鏈可能會很長,而且每個執行緒都有方法處於執行狀態。對於執行引擎來說,只有活動執行緒棧頂的棧幀才是有效的,稱為當前棧幀(Current Stack Frame),這個棧幀關聯的方法稱為當前方法(Current Method)。

棧幀的大致結構如下圖所示:

這裡寫圖片描述
每一個棧幀的結構都包括了局部變量表、運算元棧、方法返回地址和一些額外的附加資訊。某個方法的棧幀需要多大的區域性變量表、多深的運算元棧都在編譯程式時完全確定了,並且寫入到類方法表的相應屬性中了,因此某個方法的棧幀需要分配多少記憶體,不會受到程式執行期變數資料變化的影響,而僅僅取決於具體虛擬機器的實現。

區域性變數區域:儲存方法的區域性變數和引數,儲存單位以slot(4 byte)為最小單位。區域性變數存放的資料型別有:基本資料型別、物件引用和return address(指向一條位元組碼指令的地址)。其中64位長度的long和double型別的變數會佔用2個slot,其它資料型別只佔用1個slot。

類的靜態方法和物件的例項方法被呼叫時,各自棧幀對應的區域性變數結構基本類似。但有以下如圖示區別:例項方法中第一個位置存放的是它所屬物件的引用,而靜態方法則沒有物件的引用。另外靜態方法裡所操作的靜態變數存放在方法區。

void test(Object object)

{int i=0;

Boolean b=false;

}
static void test1(int i ,Object object,boolean b)

{

...

}

這裡寫圖片描述
關於區域性變量表,還有一點需要強調,就是區域性變數不像類的例項變數那樣會有預設初始化值。所以區域性變數需要手工初始化,如果一個區域性變數定義了但沒有賦初始值是不能使用的。

運算元棧: 所謂運算元是指那些被指令操作的資料。當需要對引數操作時如c=a+b,就將即將被操作的引數資料壓棧,如將a 和b 壓棧,然後由操作指令將它們彈出,並執行操作。虛擬機器將運算元棧作為工作區。Java虛擬機器沒有暫存器,所有引數傳遞、值返回都是使用運算元棧來完成的。

Java虛擬機器的解釋執行引擎稱為“基於棧的執行引擎”,其中所指的“棧”就是運算元棧。

例如:

public static int add(int a,int b){

int c=0;

c=a+b;

return c;

}

add(25,23);

主要操作步驟:
這裡寫圖片描述
壓棧步驟:

0:  ....

1:   iload_0  // 把區域性變數0壓棧,int a;

2:   iload_1 // 區域性變數1壓棧,int b;

3:   iadd      //彈出2個變數,求和,結果壓棧48

4:   istore_2 //彈出結果,放於區域性變數2;int c;

5... 

動態連線:它是個指向執行時常量池中該棧幀所屬方法的引用,這個引用是為了支援方法呼叫過程中能進行動態連線。我們知道Class檔案的常量池存有方法的符號引用,位元組碼中的方法呼叫指令就以指向常量池中方法的符號引用為引數。這些符號引用一部分會在類載入階段或第一次使用的時候轉化為直接引用,這種轉化稱為靜態解析。餘下部分將在每一次執行期間轉化為直接引用,這部分稱為動態連線。
方法返回地址:
正常退出,執行引擎遇到方法返回的位元組碼,將返回值傳遞給呼叫者;

異常退出,遇到Exception,並且方法未捕捉異常,返回地址由異常處理器來確定,並且不會有任何返回值。

方法退出的過程實際上等同於把當前棧幀出棧,因此退出時可能執行的操作有:恢復上層方法的區域性變量表和運算元棧,把返回值(如果有的話)壓入呼叫者棧幀的運算元棧中,調整PC計數器的值以指向方法呼叫指令後面的一條指令等。

額外附加資訊:虛擬機器規範沒有明確規定,由具體虛擬機器實現。

Java虛擬機器規範規定該區域有兩種異常:
StackOverFlowError:當執行緒請求棧深度超出虛擬機器棧所允許的深度時丟擲
OutOfMemoryError:當Java虛擬機器動態擴充套件到無法申請足夠記憶體時丟擲

另外需要提醒一下,在規範模型中,棧幀相互之間是完全獨立的。但在大多數虛擬機器的實現裡都會做一些優化處理,這樣兩個棧幀可能會出現一部分重疊。這樣在下面的棧幀會有部分運算元棧與上面棧幀的部分區域性變量表重疊在一起,這樣在進行方法呼叫時就可以有部分資料共享,而無須進行額外的引數複製傳遞了。具體情形如下圖所示:

這裡寫圖片描述

本地方法棧

Java可以通過java本地介面JNI(Java Native Interface)來呼叫其它語言編寫(如C)的程式,在Java裡面用native修飾符來描述一個方法是本地方法。本地方法棧就是虛擬機器執行緒呼叫Native方法執行時的棧,它與虛擬機器棧發揮類似的作用。但是要注意,虛擬機器規範中沒有對本地方法棧作強制規定,虛擬機器可以自由實現,所以可以不是位元組碼。如果是以位元組碼實現的話,虛擬機器棧本地方法棧就可以合二為一,事實上,OpenJDK和SunJDK所自帶的HotSpot虛擬機器就是直接將虛擬機器棧和本地方法棧合二為一的。

Java虛擬機器規範規定該區域也可丟擲StackOverFlowError和OutOfMemoryError。

這個區域用來放置所有物件例項以及陣列,不過在JIT(Just-in-time)情況下有些時候也有可能在棧上分配物件例項。堆也是java垃圾收集器管理的主要區域(所以很多時候會稱它為GC堆),被所有執行緒共享。

從GC回收的角度看,由於現在GC基本都是採用的分代收集演算法,所以堆記憶體結構還可以分塊成:新生代和老年代;再細一點的有Eden空間、From Survivor空間、To Survivor空間等。如下圖:
這裡寫圖片描述

物件在堆內分配記憶體的兩種方法:
為物件分配空間的任務等同於把一塊確定大小的記憶體從Java堆中劃分出來。

指標碰撞(Serial、ParNew等帶Compact過程的收集器)
假設Java堆中記憶體是絕對規整的,所有用過的記憶體都放在一邊,空閒的記憶體放在另一邊,中間放著一個指標作為分界點的指示器,那所分配記憶體就僅僅是把那個指標向空閒空間那邊挪動一段與物件大小相等的距離,這種分配方式稱為“指標碰撞”(Bump the Pointer)。
空閒列表(CMS這種基於Mark-Sweep演算法的收集器)
如果Java堆中的記憶體並不是規整的,已使用的記憶體和空閒的記憶體相互交錯,那就沒有辦法簡單地進行指標碰撞了,虛擬機器就必須維護一個列表,記錄上哪些記憶體塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給物件例項,並更新列表上的記錄,這種分配方式稱為“空閒列表”(Free List)。
選擇哪種分配方式由Java堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。因此,在使用Serial、ParNew等帶Compact過程的收集器時,系統採用的分配演算法是指標碰撞,而使用CMS這種基於Mark-Sweep演算法的收集器時,通常採用空閒列表。

方法區

它是虛擬機器在載入類檔案時,用於存放已載入的類的類資訊,常量,靜態變數,及jit編譯後的程式碼(類方法)等資料的記憶體區域,是執行緒共享的。

方法區存放的資訊包括:

1.類的基本資訊:

每個類的全限定名

每個類的直接超類的全限定名(可約束型別轉換)

該類是類還是介面

該型別的訪問修飾符

直接超介面的全限定名的有序列表

2.已裝載類的詳細資訊:

3.執行時常量池:

類資訊除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量、符號引用,文字字串、final變數值、類名和方法名常量,這部分內容將在類載入後存放到方法區的執行時常量池中。它們以陣列形式訪問,是呼叫方法、與類聯絡及類的物件化的橋樑。

這裡再講一下,JDK1.7之前執行時常量池是方法區的一部分,JDK1.7及之後版本已經將執行時常量池從方法區中移了出來,在堆(Heap)中開闢了一塊區域存放執行時常量池。

執行時常量池除了存放編譯期產生的Class檔案的常量外,還可存放在程式執行期間生成的新常量,比較常見增加新常量方法有String類的intern()方法。String.intern()是一個Native方法,它的作用是:如果執行時常量池中已經包含一個等於此String物件內容的字串,則返回常量池中該字串的引用;如果沒有,則在常量池中建立與此String內容相同的字串,並返回常量池中建立的字串的引用。不過JDK7的intern()方法的實現有所不同,當常量池中沒有該字串時,不再是在常量池中建立與此String內容相同的字串,而改為在常量池中記錄堆中首次出現的該字串的引用,並返回該引用。

4.欄位資訊:

欄位資訊存放類中宣告的每一個欄位(例項變數)的資訊,包括欄位的名、型別、修飾符。

如private String a=“”;則a為欄位名,String為描述符,private為修飾符。

5.方法資訊:

類中宣告的每一個方法的資訊,包括方法名、返回值型別、引數型別、修飾符、異常、方法的位元組碼。(在編譯的時候,就已經將方法的區域性變量表、運算元棧大小等完全確定並存放在位元組碼中,在載入載的時候,隨著類一起裝入方法區。)

在執行時,虛擬機器執行緒呼叫方法時從常量池中獲得符號引用,然後在執行時解析成方法的實際地址,最後通過常量池中的全限定名、方法和欄位描述符,把當前類或介面中的程式碼與其它類或介面中的程式碼聯絡起來。

5.靜態變數:

就是類變數,被類的所有例項物件共享,我們只需知道,在方法區有個靜態區,靜態區專門存放靜態變數和靜態塊。

6.到類ClassLoader的引用:到該類的類裝載器的引用。

7.到類Class的引用:虛擬機器為每一個被裝載的型別建立一個Class例項,用來代表這個被裝載的類。

Java虛擬機器規範規定該區域可丟擲OutOfMemoryError。

直接記憶體

直接記憶體(Direct Memory)雖然不是程式執行時資料區的一部分,也不是Java虛擬機器規範中定義的記憶體區域,但這部分記憶體也被頻繁使用,而且它也可能導致OutOfMemoryError異常出現。

在JDK1.4中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可以使用Native方法庫直接分配堆外記憶體,然後通過一個儲存在Java堆裡面的DirecByteBuffer物件作為這塊記憶體的引用進行操作。這樣能在某些應用場景中顯著提高效能,因為它避免了在Java堆和Native堆中來回複製資料。

顯然,本機直接記憶體的分配不會受到Java堆大小的限制,但是,還是會受到本機總記憶體(包括RAM及SWAP區或者分頁檔案)的大小及處理器定址空間的限制,從而導致動態擴充套件時出現OutOfMemoryError異常。

總結

在程式執行時類是在方法區,例項物件本身在堆裡面。

方法位元組碼在方法區。執行緒呼叫方法執行時建立棧幀並壓棧,方法的引數和區域性變數在棧幀的區域性變量表。

物件的例項變數和物件一起在堆裡,所以各個執行緒都可以共享訪問物件的例項變數。

靜態變數在方法區,所有物件共享。字串常量等常量在執行時常量池。

各執行緒呼叫的方法,通過堆內的物件,方法區的靜態資料,可以共享互動資訊。

各執行緒呼叫的方法所有引數傳遞、方法返回值的返回,都是使用棧幀裡的運算元棧來完成的。