標籤:ddr 跳轉 安全 資料 回收 功能 werror 一點 基本
我們都知道虛擬機器的記憶體劃分了多個地區,並不是一張大餅。那麼為什麼要劃分為多塊地區呢,直接搞一塊地區,所有用到記憶體的地方都往這塊地區裡扔不就行了,豈不痛快。是的,如果不進列區域劃分,扔的時候確實痛快,可用的時候再去找怎麼辦呢,這就引入了第一個問題,分類管理,類似於衣櫃,系統磁碟等等,為了方便尋找,我們會進行分區分類。另外如果不進行分區,記憶體用盡了怎麼辦呢?這裡就引入了記憶體劃分的第二個原因,就是為了方便記憶體的回收。如果不分,回收記憶體需要全部記憶體掃描,那就慢死了,記憶體根據不同的使用功能分成不同的地區,那麼記憶體回收也就可以根據每個地區的特定進行回收,比如像棧記憶體中的棧幀,隨著方法的執行棧幀進棧,方法執行完畢就出棧了,而對於像堆記憶體的回收就需要使用經典的回收演算法來進行回收了,所以看起來分類這麼麻煩,其實是大有好處的。
提到虛擬機器的記憶體結構,可能首先想起來的就是堆棧。對象分配到堆上,棧上用來指派至的引用以及一些基礎資料型別 (Elementary Data Type)相關的值。但是·虛擬機器的記憶體結構遠比此要複雜的多。除了我們所認識的(還沒有認識完全)的堆棧以外,還有程式計數器,本地方法棧和方法區。我們平時所說的棧記憶體,一般是指的棧記憶體中的局部變數表。下面是官方所給的虛擬機器的記憶體結構圖
可以看到有5大記憶體地區,按照是否被線程所共用可分為兩部分,一部分是線程獨佔地區,包括Java棧,本地方法棧和程式計數器。還有一部分是被線程所共用的,包括方法區和堆。什麼是線程共用和線程獨佔呢,非常好理解,我們知道每一個Java進行都會有多個線程同時運行,那麼線程共用區的這片地區就是被所有線程一起使用的,不管有多少個線程,這片空間始終就這一個。而線程的獨佔區,是每個線程都有這麼一份記憶體空間,每個線程的這片空間都是專屬的,有多少個線程就有多少個這麼個空間。的地區的大小並不代表實際記憶體地區的大小,實際運行過程中,記憶體地區的大小也是可以動態調整的。下面來具體說說每一個地區的主要功能。
程式計數器,我們在寫代碼的過程中,開發工具一般都會給我們標註行號方便查看和閱讀代碼。那麼在程式在運行過程中也有一個類似的行號方便虛擬機器的執行,就是程式計數器,在c語言中,我們知道會有一個goto語句,其實就是跳轉到了指定的行,這個行號就是程式計數器。儲存的就是程式下一條所執行的指令。這部分地區是線程所獨享的地區,我們知道線程是一個順序執行流,每個線程都有自己的執行順序,如果所有線程共用一個程式計數器,那麼程式執行肯定就會出亂子。為了保證每個線程的執行順序,所以程式計數器是被單個線程所獨顯的。程式計數器這塊記憶體地區是唯一一個在jvm規範中沒有規定記憶體溢出的。
java虛擬機器棧,java虛擬機器棧是程式啟動並執行動態地區,每個方法的執行都伴隨著棧幀的入棧和出棧。 棧幀也叫過程活動記錄,是編譯器用來實現過程/函數調用的一種資料結構。棧幀中包括了局部變數表,運算元棧,方法返回地址以及額外的一些附加資訊,在編譯過程中,局部變數表的大小已經確定,運算元棧深度也已經確定,因此棧幀在啟動並執行過程中需要分配多大的記憶體是固定的,不受運行時影響。對於沒有逃逸的對象也會在棧上分配記憶體,對象的大小其實在運行時也是確定的,因此即使出現了棧上記憶體配置,也不會導致棧幀改變大小。
一個線程中,可能調用鏈會很長,很多方法都同時處於執行狀態。對於執行引擎來講,活動線程中,只有棧頂的棧幀是最有效,稱為當前棧幀,這個棧幀所關聯的方法稱為當前方法。執行引擎所啟動並執行位元組碼指令僅對當前棧幀進行操作。
局部變數表:我們平時所說的棧記憶體一般就是指棧記憶體中的局部變數表。這裡主要是儲存變數所用。對於基礎資料型別 (Elementary Data Type)直接儲存其值,對於引用資料類型則儲存其地址。局部變數表的最小儲存單位是Slot,每個Slot都能存放一個boolean、byte、char、short、int、float、reference或returnAddress類型的資料。
既然前面提到了資料類型,在此順便說一下,一個Slot可以存放一個32位以內的資料類型,Java中佔用32位以內的資料類型有boolean、byte、char、short、int、float、reference和returnAddress八種類型。前面六種不需要多解釋,大家都認識,而後面的reference是對象的引用。虛擬機器規範既沒有說明它的長度,也沒有明確指出這個引用應有怎樣的結構,但是一般來說,虛擬機器實現至少都應當能從此引用中直接或間接地尋找到對象在Java堆中的起始地址索引和方法區中的物件類型資料。而returnAddress是為位元組碼指令jsr、jsr_w和ret服務的,它指向了一條位元組碼指令的地址。
對於64位的資料類型,虛擬機器會以高位在前的方式為其分配兩個連續的Slot空間。Java語言中明確規定的64位的資料類型只有long和double兩種(reference類型則可能是32位也可能是64位)。值得一提的是,這裡把long和double資料類型讀寫分割為兩次32讀寫的做法類似。不過,由於局部變數表建立線上程的堆棧上,是線程私人的資料,無論讀寫兩個連續的Slot是否是原子操作,都不會引起資料安全問題。
運算元棧是一個後入先出(Last In First Out, LIFO)棧。同局部變數表一樣,運算元棧的最大深度也在編譯的時候被寫入到位元組碼檔案中,關於位元組碼檔案,後面我會具體的來描述。運算元棧的每一個元素可以是任意的Java資料類型,包括long和double。32位元據類型所佔的棧容量為1,64位元據類型所佔的棧容量為2。在方法執行的任何時候,運算元棧的深度都不會超過在max_stacks資料項目中設定的最大值。
當一個方法剛剛開始執行的時候,這個方法的運算元棧是空的,在方法的執行過程中,會有各種位元組碼指令向運算元棧中寫入和提取內容,也就是入棧出棧操作。例如,在做算術運算的時候是通過運算元棧來進行的,又或者在調用其他方法的時候是通過運算元棧來進行參數傳遞的。
舉個例子,整數加法的位元組碼指令iadd在啟動並執行時候要求運算元棧中最接近棧頂的兩個元素已經存入了兩個int型的數值,當執行這個指令時,會將這兩個int值和並相加,然後將相加的結果入棧。
運算元棧中元素的資料類型必須與位元組碼指令的序列嚴格匹配,在編譯器代碼的時候,編譯器要嚴格保證這一點,在類校正階段的資料流分析中還要再次驗證這一點。再以上面的iadd指令為例,這個指令用於整型數加法,它在執行時,最接近棧頂的兩個元素的資料類型必須為int型,不能出現一個long和一個float使用iadd命令相加的情況。
本地方法棧 與虛擬機器棧所發揮的作用是非常相似的,其區別不過是虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧則是為虛擬機器使用到的Native方法服務。虛擬機器規範中對本地方法棧中的方法使用的語言、使用方式與資料結構並沒有強制規定,因此具體的虛擬機器可以自由實現它。甚至有的虛擬機器(譬如Sun HotSpot虛擬機器)直接就把本地方法棧和虛擬機器棧合二為一。與虛擬機器棧一樣,本地方法棧地區也會拋出StackOverflowError和OutOfMemoryError異常。
方法區經常會被人稱之為永久代,但這倆並不是一個概念。首先永久代的概念僅僅在HotSpot虛擬機器中存在,不幸的是,在jdk8中,Hotspot去掉了永久代這一說法,使用了Native Memory,也就是Metaspace空間。那麼方法區是幹嘛的呢?我們可以這麼理解,我們要運行Java代碼,首先需要編譯,然後才能運行。在啟動並執行過程中,我們知道首先需要載入位元組碼檔案。也就是說要把位元組碼檔案載入到記憶體中。好了,問題就來了,位元組碼檔案放到記憶體中的什麼地方呢,就是方法區中。當然除了編譯後的位元組碼之外,方法區中還會存放常量,靜態變數以及及時編譯器編譯後的代碼等資料。
堆,一般來講堆記憶體是Java虛擬機器中最大的一塊記憶體地區,同方法區一樣,是被所有線程所共用的地區。此地區所存在的唯一目的就存放對象的執行個體(對象執行個體並不一定全部在堆中建立)。堆記憶體是垃圾收集器主要光顧的地區,一般來講根據使用的垃圾收集器的不同,堆中還會劃分為一些地區,比如新生代和老年代。新生代還可以再劃分為Eden,Survivor等地區。另外為了效能和安全性的角度,在堆中還會為線程劃分單獨的地區,稱之為線程分配緩衝區。更細緻的劃分是為了讓垃圾收集器能夠更高效的工作,提高垃圾收集的效率。
如果想要瞭解更多的關於虛擬機器的內容,歡迎觀看錄製的<深入理解Java虛擬機器>這套視頻教程。
Java虛擬機器的記憶體結構