1. 程式人生 > >Java虛擬機器(一)——記憶體區域理解

Java虛擬機器(一)——記憶體區域理解

說明:本文內容主要參考了《深入理解Java虛擬機器》第2版。

 

一,概述

最近因為辭職了,玩了一段時間了,有時間去學習。加上之前買了一二本書,有不少卻沒有仔細去看,今天狀態還不錯,剛好看到JVM相關的內容,覺得還是在部落格裡好好總結一下。

本文主要還是對JVM的記憶體區域進行較為詳細的說明,暫時不結合實現開發過程中遇到的記憶體溢位等問題進行說明。

 

二,Java執行時資料區域

Java虛擬機器所管理的記憶體包含以下幾個執行時資料區域。如下圖:

下面對每個區域進行說明。其中,方法區和堆是純種共享的,程式計數器、本地方法區、虛擬機器棧是執行緒私有的。

2.1,程式計數器(Program Counter Register)

是一塊較小的記憶體區域,可看作是當執行緒所執行的位元組碼的行號指示器。位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。

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

2.2,虛擬機器棧(VM Stack)

虛擬機器棧是執行緒私有的,它的生命週期與執行緒相同。虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一從此棧幀(Stack Frame,是方法執行時的基礎資料結構)用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。

經常有人把Java記憶體分為堆記憶體(Heap)和棧記憶體(Stack),這種方法比較粗糙,Java記憶體區域的劃分實際上遠比這個複雜。這種劃分方式的流行只能說明大多數程式設計師最關注的、與物件記憶體分配關係最密切的記憶體區域是這兩塊。其中所說的“棧”就是虛擬機器棧,或者說是虛擬機器棧中區域性變數部分。

區域性變量表儲存了編譯期可知的各種基本資料型別(char、boolean、byte、short、int、long、float、double)、物件引用(reference型別,它不等同於物件本身,可能是一個指向物件起始地址的引用指標,也可能是指向一個代表物件的控制代碼或其他與此物件相關的位置)和returnAddress型別(指向了一條位元組碼指令的地址)。

2.3,本地方法棧(Native Method Stack)

本地方法棧與虛擬機器棧所發揮的作用是非常相似的,區別是虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的Native方法服務。

2.4,堆(Heap)

堆是虛擬機器所管理的記憶體中最大的一塊。是被執行緒所共享的,在虛擬機器啟動時建立。堆記憶體區域的唯一目的就是存放物件例項,幾乎所有物件例項都在這裡分配記憶體。但是隨著JIT編譯器的發展和逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有的物件都分配在堆上也漸漸變得不那麼“絕對”了。

堆是垃圾收集器管理的主要區域,因此很多時候也被稱”GC堆“。

2.5,方法區(Method Area)

方法區是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。雖然JVM規範把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫Non-Heap(非堆),目的應該是與Java堆區分開來。

執行時常量池(Runtime Constant Pool)是方法區的一部分。Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類載入後進入方法區的執行時常量池中存放。執行時常量池相對於Class檔案常量池有一個重要特性是具有動態性,Java語言並不要求常量一定只有編譯期間才能產生,也就是並非預置入Class檔案中常量池內容才能進入方法區執行時常量池,執行期間也可以將新的常量放入池中,這種特性被開發人員利用得比較多的就是String類的intern()方法。

 

三,總結

通常我們定義一個基本資料型別的變數,一個物件的引用,還有就是函式呼叫的現場儲存都使用JVM中的棧空間;而通過new關鍵字和構造器建立的物件則放在堆空間,堆是垃圾收集器管理的主要區域,由於現在的垃圾收集器都採用分代收集演算法,所以堆空間還可以細分為新生代和老生代,再具體一點可以分為Eden、Survivor(又可分為From Survivor和To Survivor)、Tenured;方法區和堆都是各個執行緒共享的記憶體區域,用於儲存已經被JVM載入的類資訊、常量、靜態變數、JIT編譯器編譯後的程式碼等資料;程式中的字面量(literal)如直接書寫的100、"hello"和常量都是放在常量池中,常量池是方法區的一部分,。棧空間操作起來最快但是棧很小,通常大量的物件都是放在堆空間,棧和堆的大小都可以通過JVM的啟動引數來進行調整,棧空間用光了會引發StackOverflowError,而堆和常量池空間不足則會引發OutOfMemoryError。

String str = new String("hello");

上面的語句中變數str放在棧上,用new創建出來的字串物件放在堆上,而"hello"這個字面量是放在方法區的。