標籤:i/o 控制代碼 而在 runtime 一個棧 本地 擴充 區別 情況下
1.幾個電腦的概念
為以後寫文章考慮,也為鞏固自己的知識和一些基本概念,這裡要理清楚幾個電腦中的概念。
1、電腦儲存單位
從小到大依次為位Bit、位元組Byte、KBKB、兆M、千兆GB、TB,相鄰單位之間都是1024倍,1024為2的10次方,即:
- 1Byte = 8bit
- 1K = 1024Byte
- 1M = 1024K
- 1G = 1024M
- 1T = 1024G
2、電腦儲存元件
寄存器:中央處理器CPU的一部分,是電腦中讀寫速度最快的儲存元件,但是容量很少
記憶體:屬於獨立的一個組件,是和CPU溝通的橋樑,用於存放CPU中的運算資料以及與外部儲存空間交換的資料。儘管在今天,對記憶體的讀寫速度已經很快了,但是由於寄存器是在CPU上的,所以對於記憶體的讀寫速度和對於寄存器的讀寫速度上還是有幾個數量級的差距。但是沒辦法,對於記憶體的讀寫I/O操作是很難消除的,寄存器數量有限,不可能通過寄存器來完成所有的運算任務
3、核心空間和使用者空間
串連記憶體和寄存器的是地址匯流排,地址匯流排的寬度影響了物理地址的索引範圍,因為匯流排寬度決定了處理器一次可以從寄存器或記憶體中擷取多少個Bit,同時也決定了處理器最大可以定址的地址空間。比如32位CPU的系統,可定址範圍為0x00000000~0xFFFFFFFF,即232=4294967296個記憶體位置,每個記憶體位置1個位元組,即32位CPU系統可以有4GB的記憶體空間。不過應用程式是不可以完全使用這些地址空間的,因為這些地址空間被劃分為了核心空間和使用者空間,程式只能使用使用者空間的記憶體。核心空間主要是指作業系統運行時所使用的用於程式調度、虛擬記憶體的使用或者連結硬體資源的程式邏輯。區分核心空間和使用者空間的目的主要是從系統的穩定性的角度考慮的。Windows 32作業系統預設核心空間和使用者空間的比例是1:1,即2G核心空間、2G記憶體空間,32位Linux系統中預設比例則是1:3,即1G核心空間,3G記憶體空間。
4、字長
CPU的主要技術指標之一,指的是CPU一次能平行處理二進位的位元(Bit)。通常稱處理字長為8位元據的CPU為8位CPU,32位CPU就是在同一時間內處理字長為32位的位元據。不過目前雖然CPU大多是64位的,但還是以32位字長運行
2.前言
說到Java記憶體地區,可能很多人第一反應是“堆棧”。首先堆棧不是一個概念,而是兩個概念,堆和棧是兩塊不同的記憶體地區,簡單理解的話,堆是用來存放對象而棧是用來執行程式的。其次,堆記憶體和棧記憶體的這種劃分方式比較粗糙,這種劃分方式只能說明大多數程式員最關注的、與對象記憶體配置關係最密切的記憶體地區是這兩塊,Java記憶體地區的劃分實際上遠比這複雜。對於Java程式員來說,在虛擬機器自動記憶體管理機制的協助下,不再需要為每一個new操作去配對delete/free代碼,不容易出現記憶體泄露和記憶體溢出問題。但是,也正是因為Java把記憶體控制權交給了虛擬機器,一旦出現記憶體泄露和記憶體溢出的問題,就難以排查,因此一個好的Java程式員應該去瞭解虛擬機器的記憶體地區以及會引起記憶體泄露和記憶體溢出的情境。
3.運行時資料區域
Java虛擬機器(JVM)內部定義了程式在運行時需要使用到的記憶體地區:
之所以要劃分這麼多地區出來是因為這些地區都有自己的用途,以及建立和銷毀的時間。有些地區隨著虛擬機器進程的啟動而存在,有的地區則依賴使用者線程的啟動和結束而銷毀和建立。
線程共用記憶體區: 方法區和堆,
線程私人記憶體區: 虛擬機器棧、本地方法棧、程式技術器,基本上隨著線程產生和消亡,也就是說生命週期和線程相同,因此基本不需要考慮記憶體回收的問題,編譯時間確定所需記憶體大小。
從這個分類角度來看一下這幾個資料區。
3.1、線程專屬的記憶體地區
(1)PROGRAM COUNTER REGISTER,程式計數器
這塊記憶體地區很小,它是當前線程所執行的位元組碼的行號指標,位元組碼解譯器通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支,跳轉,迴圈等基礎功能都要依賴它來實現。
這裡需要注意三點內容:
- 每條線程都有一個獨立的程式計數器,各線程間的計數器互不影響,因此該地區是線程私人的。
- 當線程在執行一個Java方法時,該計數器記錄的是正在執行的虛擬機器位元組碼指令的地址,也就是說有值,當線程在執行的是Native方法(調用本地作業系統方法)時,該計數器的值為空白。
- 另外,該記憶體地區是唯一一個在Java虛擬機器規範中沒有規定任何OOM(記憶體溢出:OutOfMemoryError)情況的地區,也就是說此塊地區不會拋出記憶體溢出的異常。
(2)JAVA STACK,虛擬機器棧
- 該地區也是線程私人的,它的生命週期也與線程相同。
- 虛擬機器棧也就是我們平常所稱的棧記憶體,它是為java方法服務的,描述了java方法執行的記憶體模型。
- 每一個方法從調用直至執行完畢的過程,就對應著一個棧幀在虛擬機器中入棧到出棧的過程。
- Java方法執行的記憶體模型:每個方法被執行的時候都會同時建立一個棧幀,棧幀用於儲存局部變數表、運算元棧、動態連結、方法返回地址和一些額外的附加資訊。棧它是用於支援續虛擬機器進行方法調用和方法執行的資料結構。對於執行引擎來講,活動線程中,只有棧頂的棧幀是有效,稱為當前棧幀,這個棧幀所關聯的方法稱為當前方法,執行引擎所啟動並執行所有位元組碼指令都只針對當前棧幀進行操作。在編譯器代碼時,棧幀中需要多大的局部變數表、多深的運算元棧都已經完全確定了,並且寫入了方法表的Code屬性之中。因此,一個棧幀需要分配多少記憶體,不會受到程式運行期變數資料的影響,而僅僅取決於具體的虛擬機器實現。
- 棧的大小通常在256K~756K之間,具體和JVM的實現有關。
- 在Java虛擬機器規範中,對這個地區規定了兩種異常情況:
1、如果線程請求的棧深度大於虛擬機器所允許的深度,將拋出StackOverflowError異常。
2、如果虛擬機器在動態擴充棧時無法申請到足夠的記憶體空間,則拋出OutOfMemoryError異常。
這兩種情況存在著一些互相重疊的地方:當棧空間無法繼續分配時,到底是記憶體太小,還是已使用的棧空間太大,其本質上只是對同一件事情的兩種描述而已。在單線程的操作中,無論是由於棧幀太大,還是虛擬機器棧空間太小,當棧空間無法分配時,虛擬機器拋出的都是StackOverflowError異常,而不會得到OutOfMemoryError異常。而在多線程環境下,則會拋出OutOfMemoryError異常。
下面詳細說明棧幀中所存放的各部分資訊的作用和資料結構。
1、局部變數表
局部變數表是一組變數值儲存空間,用於存放方法參數和方法內部定義的局部變數,其中存放的資料的類型是編譯期可知的各種基礎資料型別 (Elementary Data Type)、對象引用(reference(不等同於對象本身,可能是一個指向對象起始地址的引用指標,也可能是指向一個代表對象的控制代碼或其他與此對象相關的位置))和returnAddress類型(它指向了一條位元組碼指令的地址)。
局部變數表所需的記憶體空間在編譯期間完成分配,即在Java程式被編譯成Class檔案時,就確定了所需分配的最大局部變數表的容量。當進入一個方法時,這個方法需要在棧中分配多大的局部變數空間是完全確定的,在方法運行期間不會改變局部變數表的大小。
局部變數表的容量以變數槽(Slot)為最小單位。在虛擬機器規範中並沒有明確指明一個Slot應佔用的記憶體空間大小(允許其隨著處理器、作業系統或虛擬機器的不同而發生變化),一個Slot可以存放一個32位以內的資料類型:boolean、byte、char、short、int、float、reference和returnAddresss。reference是對象的參考型別,returnAddress是為位元組指令服務的,它執行了一條位元組碼指令的地址。對於64位的資料類型(long和double),虛擬機器會以高位在前的方式為其分配兩個連續的Slot空間。
虛擬機器通過索引定位的方式使用局部變數表,索引值的範圍是從0開始到局部變數表最大的Slot數量,對於32位元據類型的變數,索引n代表第n個Slot,對於64位的,索引n代表第n和第n+1兩個Slot。
在方法執行時,虛擬機器是使用局部變數表來完成參數值到參數變數列表的傳遞過程的,如果是執行個體方法(非static),則局部變數表中的第0位索引的Slot預設是用於傳遞方法所屬對象執行個體的引用,在方法中可以通過關鍵字“this”來訪問這個隱含的參數。其餘參數則按照參數表的順序來排列,佔用從1開始的局部變數Slot,參數表分配完畢後,再根據方法體內部定義的變數順序和範圍分配其餘的Slot。
局部變數表中的Slot是可重用的,方法體中定義的變數,範圍並不一定會覆蓋整個方法體,如果當前位元組碼PC計數器的值已經超過了某個變數的範圍,那麼這個變數對應的Slot就可以交給其他變數使用。這樣的設計不僅僅是為了節省空間的,在某些情況下Slot的複用會直接影響到系統的而垃圾收集行為。
2、運算元棧
- 運算元棧又常被稱為操作棧,主要用來儲存運算結果以及運算的運算元。
- 運算元棧的最大深度也是在編譯的時候就確定了。32位元據類型所佔的棧容量為1,64為資料類型所佔的棧容量為2。
- 它不同於局部變數表通過索引來訪問,而是通過壓棧和出棧的方式:當一個方法開始執行時,它的操作棧是空的,在方法的執行過程中,會有各種位元組碼指令(比如:加操作、賦值等)向操作棧中寫入和提取內容。
- Java虛擬機器的解釋執行引擎稱為“基於棧的執行引擎”,其中所指的“棧”就是運算元棧。因此我們也稱Java虛擬機器是基於棧的,這點不同於Android虛擬機器,Android虛擬機器是基於寄存器的。基於棧的指令集最主要的優點是可移植性強,主要的缺點是執行速度相對會慢些;而由於寄存器由硬體直接提供,所以基於寄存器指令集最主要的優點是執行速度快,主要的缺點是可移植性差。
3、動態串連
每個棧幀都包含一個指向運行時常量池(在方法區中,後面介紹)中該棧幀所屬方法的引用,持有這個引用是為了支援方法調用過程中的動態串連。Class檔案的常量池中存在有大量的符號引用,位元組碼中的方法調用指令就以常量池中指向方法的符號引用為參數。這些符號引用,一部分會在類載入階段或第一次使用的時候轉化為直接引用(如final、static域等),稱為靜態解析,另一部分將在每一次的運行期間轉化為直接引用,這部分稱為動態串連。
4、方法返回地址
當一個方法被執行後,有兩種方式退出該方法:執行引擎遇到了任意一個方法返回的位元組碼指令或遇到了異常,並且該異常沒有在方法體內得到處理。無論採用何種退出方式,在方法退出之後,都需要返回到方法被調用的位置,程式才能繼續執行。方法返回時可能需要在棧幀中儲存一些資訊,用來協助恢複它的上層方法的執行狀態。一般來說,方法正常退出時,調用者的PC計數器的值就可以作為返回地址,棧幀中很可能儲存了這個計數器值,而方法異常退出時,返回地址是要通過異常處理器來確定的,棧幀中一般不會儲存這部分資訊。
方法退出的過程實際上等同於把當前棧幀出站,因此退出時可能執行的操作有:恢複上層方法的局部變數表和運算元棧,如果有傳回值,則把它壓入調用者棧幀的運算元棧中,調整PC計數器的值以指向方法調用指令後面的一條指令。
(3)NATIVE METHOD STACK,本地方法棧
和虛擬機器棧起的作用一樣,只不過方法棧為虛擬機器使用到的Native方法服務。虛擬機器規範並沒有對這個地區有什麼強制規定,因此我們使用的HotSpot虛擬機器,就乾脆沒有這塊地區了,它和虛擬機器棧是一起的。
3.2、線程間共用的記憶體地區
(1)HEAP,堆
- Java Heap是虛擬機器所管理的記憶體中最大的一塊,它是所有線程共用的一塊記憶體地區。
- 此記憶體地區的唯一目的就是存放對象執行個體,幾乎所有的對象執行個體都要在這裡分配記憶體。
- Java Heap 是垃圾收集器管理的主要區域,因此 很多 時候也被稱為"GC堆"。由於現在垃圾收集器採用的基本都是分代收集演算法,所以堆還可以細分為新生代和老年代,再細緻一點還有Eden區、From Survivior區、To Survivor區等。(這個後面講)
- 根據Java虛擬機器規範的規定,堆可以處在物理上不連續的記憶體空間中,只要邏輯上是連續的即可。如果在堆中沒有記憶體可分配時,並且堆也無法擴充時,將會拋出OutOfMemoryError異常。
(2)METHOD AREA,方法區
這塊地區用於儲存虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的代碼等資料,虛擬機器規範是把這塊地區描述為堆的一個邏輯部分的,但實際它應該是要和堆區分開的。從上面提到的分代收集演算法的角度看,HotSpot中,方法區≈永久代。不過JDK 7之後,我們使用的HotSpot應該就沒有永久代這個概念了,會採用Native Memory來實現方法區的規划了。
- 方法區也是各個線程共用的記憶體地區,它用於儲存已經被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的代碼等資料。
- 方法區又被稱為“永久代”,不過JDK7, JRockit和IBM J9等JVM已經沒有永久代的概念了。
- Java虛擬機器規範把方法區描述為Java堆的一個邏輯部分,而且它和Java Heap一樣不需要連續的記憶體,對於方法區的分配會採用Native Memory來實現。
- 垃圾收集行為在這個地區比較少出現,該地區記憶體的回收目標是對廢棄常量池和無用類的回收。
- 運行時常量池是方法區的一部分,用於存放編譯時間期產生的各種字面量和符號引用,該常量池具有動態性。
- 根據Java虛擬機器規範的規定,當方法區無法滿足記憶體配置需求時,將拋出OutOfMemoryError異常(OOM)。
(3)RUNTIME CONSTANT POOL,運行時常量池
Class檔案中除了有類的版本資訊、欄位、方法、介面等描述資訊外,還有一項資訊就是常量池,用於存放編譯期間產生的各種字面量和符號引用,這部分內容將在類載入後進入方法區的運行時常量池中,另外翻譯出來的直接引用也會儲存在這個地區中。這個地區另外一個特點就是動態性,Java並不要求常量就一定要在編譯期間才能產生,運行期間產生的常量也會存在這個常量池中,String.intern()方法就是這個特性的應用。
關於字面量、符號引用和直接引用:
字面量相當於Java語言層面常量的概念,如文本字串,聲明為final的常量值等。
符號引用則屬於編譯原理方面的概念,包括了如下三種類型的常量:1、類和介面的全限定名 2、欄位名稱和描述符 3、方法名稱和描述符。
直接引用可以是直接指向引用目標的指標、相對位移量或者是一個能夠間接定位到目標的控制代碼。直接引用是和虛擬機器的記憶體布局有關的,同一個符號引用在不同的虛擬機器上翻譯的直接引用一般是不同的。如果有了直接引用,那麼引用的目標必定是存在記憶體中的。
4、直接記憶體
直接記憶體並不是虛擬機器運行時資料區的一部分,也不是Java虛擬機器規範中定義的記憶體地區。它直接從作業系統中分配,因此不受Java堆大小的限制,但是還是會受到本機總記憶體(包括RAM、SWAP區)大小以及處理器定址空間的限制,因此它也可能導致OutOfMemoryError異常出現。
JDK1.4中新增加了NIO,引入了一種基於通道與緩衝區的I/O方式,它可以使用Native函數庫直接分配堆外記憶體,然後通過一個儲存在Java堆中的DirectByteBuffer對象作為這塊記憶體的引用進行操作。這樣能在一些情境中顯著提高效能,因為避免了在Java堆和Native堆中來回複製資料。
5、總結
簡單的總結一下:
- 程式計數器(PC):Java線程私人,類似於作業系統裡的PC計數器,用於指定下一條需要執行的位元組碼的地址;
- Java虛擬機器棧:Java線程私人,虛擬機器展描述的是Java方法執行的記憶體模型:每個方法在執行的時候,都會建立一個棧幀用於儲存局部變數、運算元、動態連結、方法出口等資訊;每個方法調用都意味著一個棧幀在虛擬機器棧中入棧到出棧的過程;
- 本地方法棧:和Java虛擬機器棧的作用類似,區別是該該地區為JVM調用到的本地方法服務;
- 堆(Heap):所有線程共用的一塊地區,垃圾收集器管理的主要區域。目前主要的記憶體回收演算法都是分代收集,因此該地區還可以細分為如下地區: – 年輕代 – Eden空間 – From Survivor空間1,From Survivor空間2,用於儲存在Young gc過程中倖存的對象; – 老年代
- 方法區:各個線程共用的一個地區,用於儲存虛擬機器載入的類資訊、常量、靜態變數等資訊;
- 運行時常量池:方法區的一部分,用於存放編譯器產生的各種字面量和符號引用;
轉載地址:http://www.cnblogs.com/xrq730/p/4827590.html
http://blog.csdn.net/qq_31957747/article/details/73662504
http://blog.csdn.net/ns_code/article/details/17565503
Java虛擬機器2:Java記憶體地區