JVM學習(一)——內存結構

其餘更多java基礎文章:
java基礎學習(目錄)html


學習資料
Java虛擬機規範
方法區的Class信息,又稱爲永久代,是否屬於Java堆?
JVM 內部原理(一)— 概述java

運行時數據區域(Run-Time Data Areas)

Java虛擬機定義了在程序執行期間使用的各類運行時數據區域。其中一些數據區域是在Java虛擬機啓動時建立的,僅在Java虛擬機退出時銷燬。其餘數據區域是每一個線程。線程數據區域是在線程退出時建立和銷燬線程時建立的。
JVM所管理的幾個運行時數據區域:方法區、虛擬機棧、本地方法棧、堆、程序計數器,其中方法區和堆是由線程共享的數據區,其餘幾個是線程隔離的數據區。程序計數器,虛擬機棧,本地方法棧,隨線程而生,線程亡而亡程序員

線程獨享面試

  • 程序計數器
  • 虛擬機棧
  • 本地方法棧

線程共享編程

  • 方法區

程序計數器/PC計數器(The pc Register)

程序計數器是一塊較小的內存,他能夠看作是當前線程所執行的行號指示器。每一個Java虛擬機線程都有本身的程序計數器,一般程序計數器會在執行指令結束後增長,所以它須要保持下一將要執行指令的地址。 字節碼解釋器工做的時候就是經過改變這個計數器的值來選取下一條須要執行的字節碼的指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。若是線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是Native方法,這個計數器則爲空。此內存區域是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemotyError狀況的區域數組

虛擬機棧(Java Virtual Machine Stacks)

每一個線程都有本身的棧(stack),棧內以幀(frame)的形式保持着線程內執行的每一個方法。棧是一個後進先出(LIFO)的數據結構,因此當前執行的方法在棧頂部。每次方法調用時,都會建立新的幀而且壓入棧的頂部。當方法正常返回或拋出未捕獲的異常時,幀或從棧頂移除。除了壓入和移除幀對象的操做,棧沒有其餘直接的操做,所以幀對象分配在堆中,內存並不要求連續。對於執行引擎來講,活動線程中,只有棧頂的棧幀是有效的,稱爲當前棧幀,這個棧幀所關聯的方法稱爲當前方法。執行引擎所運行的全部字節碼指令都只針對當前棧幀進行操做。
在Java 虛擬機規範中,對虛擬機棧規定了兩種異常情況:若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError 異常;若是虛擬機棧能夠動態擴展(當前大部分的Java虛擬機均可動態擴展,只不過Java虛擬機規範中也容許固定長度的虛擬機棧),當擴展時沒法申請到足夠的內存時會拋出OutOfMemoryError 異常。bash

幀(Frame)

每次方法調用時,新的幀都會建立並被壓入棧頂。當方法正常返回或拋出未捕獲異常時,幀會從作退棧操做。詳細的異常處理參加後面 異常表(Exception Table)部分。數據結構

每一個幀都包括oracle

  • 局部變量(Local variable)
  • 返回地址(Return value)
  • 操做數棧(Operand stack)
  • 動態連接(Dynamic Linking)
局部變量表(Local variable)

局部變量表是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。在Java程序被編譯成Class文件時,就在方法的Code屬性的max_locals數據項中肯定了該方法所須要分配的最大局部變量表的容量。
Java虛擬機使用局部變量在方法調用上傳遞參數。在類方法調用中,任何參數都在從局部變量0開始的連續局部變量中傳遞。在實例方法調用中,局部變量0始終用於傳遞對調用實例方法的對象的引用(Java編程語言裏的this)。隨後,任何參數都在從局部變量1開始的連續局部變量中傳遞。app

局部變量能夠是:

  • boolean操做數棧
  • byte
  • char
  • long
  • short
  • int
  • float
  • double
  • reference
  • returnAddress

全部的類型都在本地變量數組中佔一個槽,而 longdouble 會佔兩個連續的槽,由於它們有雙倍寬度(64-bit 而不是 32-bit)。reference類型虛擬機規範沒有明確說明它的長度,但通常來講,虛擬機實現至少都應當能今後引用中直接或者間接地查找到對象在Java堆中的起始地址索引和方法區中的對象類型數據。returnAddress類型是爲字節碼指令jsr、jsr_w和ret服務的,它指向了一條字節碼指令的地址。

操做數棧(Operand stack)

每一個幀包含一個後進先出(LIFO)堆棧,稱爲其操做數堆棧。幀的操做數堆棧的最大深度在編譯時肯定,並與幀相關的方法的代碼一塊兒提供。
虛擬機把操做數棧做爲它的工做區——大多數指令都要從這裏彈出數據,執行運算,而後把結果壓回操做數棧。好比,iadd指令就要從操做數棧中彈出兩個整數,執行加法運算,其結果又壓回到操做數棧中,看看下面的示例,它演示了虛擬機是如何把兩個int類型的局部變量相加,再把結果保存到第三個局部變量的:

begin  
iload_0    // push the int in local variable 0 ontothe stack  
iload_1    //push the int in local variable 1 onto the stack  
iadd       // pop two ints, add them, push result  
istore_2   // pop int, store into local variable 2  
end  
複製代碼

在這個字節碼序列裏,前兩個指令iload_0iload_1將存儲在局部變量中索引爲0和1的整數壓入操做數棧中,其後iadd指令從操做數棧中彈出那兩個整數相加,再將結果壓入操做數棧。第四條指令istore_2則從操做數棧中彈出結果,並把它存儲到局部變量區索引爲2的位置。下圖詳細表述了這個過程當中局部變量和操做數棧的狀態變化,圖中沒有使用的局部變量區和操做數棧區域以空白表示。

返回地址

方法的返回分爲兩種狀況,一種是正常退出,退出後會根據方法的定義來決定是否要傳返回值給上層的調用者,一種是異常致使的方法結束,這種狀況是不會傳返回值給上層的調用方法。

不過不管是那種方式的方法結束,在退出當前方法時都會跳轉到當前方法被調用的位置,若是方法是正常退出的,則調用者的PC計數器的值就能夠做爲返回地址,若是是由於異常退出的,則是須要經過異常處理表來肯定。

方法的的一次調用就對應着棧幀在虛擬機棧中的一次入棧出棧操做,所以方法退出時可能作的事情包括:恢復上層方法的局部變量表以及操做數棧,若是有返回值的話,就把返回值壓入到調用者棧幀的操做數棧中,還會把PC計數器的值調整爲方法調用入口的下一條指令。

動態連接(Dynamic Linking)

虛擬機運行的時候,運行時常量池會保存大量的符號引用,這些符號引用能夠當作是每一個方法的間接引用。若是表明棧幀A的方法想調用表明棧幀B的方法,那麼這個虛擬機的方法調用指令就會以B方法的符號引用做爲參數,可是由於符號引用並非直接指向表明B方法的內存位置,因此在調用以前還必需要將符號引用轉換爲直接引用,而後經過直接引用才能夠訪問到真正的方法。

若是符號引用是在類加載階段或者第一次使用的時候轉化爲直接應用,那麼這種轉換成爲靜態解析,若是是在運行期間轉換爲直接引用,那麼這種轉換就成爲動態鏈接。

本地方法棧(Native Method Stack)

本地方法棧與虛擬機棧所發揮的做用是很是類似的,其區別不過是虛擬機棧爲虛擬機執行Java 方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的Native 方法服務。虛擬機規範中對本地方法棧中的方法使用的語言、使用方式與數據結構並無強制規定,所以具體的虛擬機能夠自由實現它。甚至有的虛擬機(譬如Sun HotSpot 虛擬機)直接就把本地方法棧和虛擬機棧合二爲一。

與虛擬機棧同樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。

堆(Heap)

堆是運行時分配類實例和數組內存的地方。數組和對象是不能存在棧裏的,由於棧幀(frame)不是被設計用做此目的,一旦棧幀建立了,它的大小不可更改。幀只用來存儲指向對中對象或數組的引用。與幀內本地變量數組裏基本變量和引用不一樣,對象老是存儲在堆內的,因此在方法結束前,它們不會被移除。並且,對象只能被垃圾回收器移除。

爲了支持垃圾回收的機制,堆一般被分爲三部分:

  • 新生代(Young Generation)
    • 一般分爲 新生者(Eden)和 倖存者(Survivor)
  • 老年代(Old Generation/Tenured Generation)
  • 永久代(Permanent Generation)(JDK8中已移除

內存管理(Memory Management)

對象和數組不會被顯式的移除,而是會被 GC 自動回收。一般的順序是這樣:

  1. 新的對象和數組被建立在新生代區
  2. 小的 GC 會發生在新生代,存活的對象會從 新生區(Eden)移到 倖存區(Survivor)
  3. 大的 GC ,一般會致使應用程序線程暫停,對象移動會發生在不一樣代之間。仍然存活的對象會重新生代被移動到老年代。
  4. 永久代的收集時刻都會在對老年代收集時發生。任何一代內存使用滿了,會在兩代同時發生收集。

關於新生代、老年代、GC的詳細講解請關注JVM學習後續文章

方法區(Method Area)

本文重點介紹方法區。由於jdk6,7,8中分別對方法區的實現永久代作了修改。

方法區介紹

JVM虛擬機規範中:

The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization. The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous. A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the method area, as well as, in the case of a varying-size method area, control over the maximum and minimum method area size.

在 Java 虛擬機中,方法區( Method Area) 是可供各條線程共享的運行時內存區域。方法區與傳統語言中的編譯代碼儲存區( Storage Area Of Compiled Code)或者操做系統進程的正文段( Text egment)的做用很是相似,它存儲了每個類的結構信息,例如運行時常量池( Runtime Constant Pool)、字段和方法數據、構造函數和普通方法的字節碼內容、還包括一些在類、實例、接口初始化時用到的特殊方法

方法區在虛擬機啓動的時候被建立,雖然方法區是堆的邏輯組成部分,可是簡單的虛擬機實現能夠選擇在這個區域不實現垃圾收集。這個版本的 Java 虛擬機規範也不限定實現方法區的內存位置和編譯代碼的管理策略。方法區的容量能夠是固定大小的,也能夠隨着程序執行的需求動態擴展,並在不須要過多空間時自動收縮。方法區在實際內存空間中能夠是不連續的。

Java 虛擬機實現應當提供給程序員或者最終用戶調節方法區初始容量的手段,對於能夠動態擴展和收縮方法區來講,則應當提供調節其最大、最小容量的手段

若是方法區的內存空間不能知足內存分配請求,那 Java 虛擬機將拋出一個OutOfMemoryError異常。

總之,就是用來存儲類的結構信息。它有一個別名叫作Non-Heap(非堆)。

永久代

永久代是HotSpot中方法區的實現。
平時,說到永久代(PermGen space)的時候每每將其和方法區不加區別。這麼理解在必定角度也說的過去。 由於,JVM虛擬機規範只是規定了有方法區這麼個概念和它的做用,並無規定如何去實現它。那麼,在不一樣的 JVM 上方法區的實現確定是不一樣的了。 同時,大多數用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集擴展至方法區,或者說使用永久代來實現方法區。

雖然能夠牽強的解釋這種將方法區和永久帶等同對待觀點。但最終方法區和永久帶仍是不一樣的。一個是標準一個是實現。

JDK1.7以前的永久代

java7以前,方法區位於永久代(PermGen),永久代和堆相互隔離,永久代的大小在啓動JVM時能夠設置一個固定值,不可變。這裏有個在面試中常常問的問題,就是String.intern()方法,詳情能夠閱讀以前我寫的一篇文章java基礎:String — 字符串常量池與intern(二)

JDK1.7的永久代

Highlights of Technology Changes in Java SE 7中,咱們能夠看到

In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application. This change will result in more data residing in the main Java heap, and less data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the String.intern() method will see more significant differences.

在JDK 7中,interned strings再也不分配在Java堆的永久代中,而是和應用程序建立的其餘對象同樣一塊兒分配在Java堆的主要部分(稱爲年輕代和年老代)。此更改將致使主Java堆中的數據更多,而永久代中的數據更少,所以可能須要調整堆大小。因爲這種變化,大多數應用程序在堆使用方面只會看到相對較小的差別,可是加載許多類或大量使用String.intern()方法的大型應用程序會看到更顯著的差別。

以及方法區的Class信息,又稱爲永久代,是否屬於Java堆? 中,R大的講解:

Oracle JDK7 / OpenJDK 7的HotSpot VM是把Symbol的存儲從PermGen移動到了native memory,而且把靜態變量從instanceKlass末尾(位於PermGen內)移動到了java.lang.Class對象的末尾(位於普通Java heap內)。
「常量池」若是說的是runtime constant pool,這個仍是在PermGen裏;
「常量池」若是說的是SymbolTable / StringTable,這倆table自身本來就一直在native memory裏,是它們所引用的東西在哪裏更有意思。
上面說了,7是把SymbolTable引用的Symbol移動到了native memory,而StringTable引用的java.lang.String實例則從PermGen移動到了普通Java heap。

R大說的runtime constant pool即爲運行常量池,SymbolTable 即爲標識符表,StringTable即字符串常量池。從而咱們能夠得知,即 java7中,存儲在永久代的部分數據就已經轉移到Java Heap或者Native memory。但永久代仍存在於JDK 1.7中,並無徹底移除。

jdk1.8的元空間及永久代

JEP 122: Remove the Permanent Generation

Move part of the contents of the permanent generation in Hotspot to the Java heap and the remainder to native memory.
Hotspot's representation of Java classes (referred to here as class meta-data) is currently stored in a portion of the Java heap referred to as the permanent generation. In addition, interned Strings and class static variables are stored in the permanent generation. The permanent generation is managed by Hotspot and must have enough room for all the class meta-data, interned Strings and class statics used by the Java application.
The proposed implementation will allocate class meta-data in native memory and move interned Strings and class statics to the Java heap. Hotspot will explicitly allocate and free the native memory for the class meta-data. Allocation of new class meta-data would be limited by the amount of available native memory rather than fixed by the value of -XX:MaxPermSize, whether the default or specified on the command line.

  1. 移除了永久代(PermGen),替換爲元空間(Metaspace);
  2. 永久代中的 class metadata 轉移到了 native memory(本地內存,而不是虛擬機);
  3. 永久代中的 interned Strings 和 class static variables 轉移到了 Java heap;
  4. 永久代參數 (PermSize MaxPermSize) -> 元空間參數(MetaspaceSize MaxMetaspaceSize)

java8中,取消永久代,方法區存放於元空間(Metaspace),元空間仍然與堆不相連,但與堆共享物理內存,邏輯上可認爲在堆中。

爲何移除永久代?
  1. 字符串存在永久代中,容易出現性能問題和內存溢出。
  2. 永久代大小不容易肯定,PermSize指定過小容易形成永久代OOM
  3. 永久代會爲 GC 帶來沒必要要的複雜度,而且回收效率偏低。
  4. Oracle 可能會將HotSpot 與 JRockit 合二爲一。

直接內存

直接內存並非虛擬機運行時數據區的一部分,也不是java虛擬機規範中定義的內存區域。 NIO類是一種基於通道和緩衝區的I/O方式,它可使用Native函數庫直接分配堆外內存,而後經過一個儲存在Java堆中的DirectByteBuffer對象做爲這塊直接內存的引用進行操做,這樣避免了java堆和navie堆中來回複製數據

相關文章
相關標籤/搜索