【Java虛擬機器學習】記憶體區域
根據《Java虛擬機器規範(Java SE 7)》的規定,Java虛擬機器所管理的記憶體包括如圖所示的幾個執行時資料區域:
JVM有兩種機制:一個是裝載具有合適名稱的類(類或是介面),包含類的裝載、連線、初始化的過程叫做類裝載子系統;另外的一個負責執行包含在已裝載的類或介面中的指令,叫做執行引擎。
程式計數器
程式計數器(Program Counter Register)是一塊較小的記憶體空間,用於指示當前執行緒所執行的位元組碼執行到了第幾行,可以理解為是當前執行緒的行號指示器。位元組碼直譯器在工作時,會通過改變程式計數器的值來取下一條位元組碼指令。
每個程式計數器只用來記錄一個執行緒的行號,各執行緒之間計數器互不影響,所以它是執行緒私有
如果執行緒正在執行的是一個Java方法,則計數器記錄的是正在執行的虛擬機器位元組碼指令地址;如果正在執行的是一個本地(Native)方法,則計數器的值為空(Undefined)。由於程式計數器只是記錄當前指令地址,所以不存在記憶體溢位的情況,程式計數器也是所有JVM記憶體區域中唯一一個沒有定義OutOfMemoryError的區域。
虛擬機器棧
虛擬機器棧描述的是Java方法執行時的記憶體模型:執行緒的每個方法在執行的同時,都會建立一個棧幀(Statck Frame),棧幀中儲存的有區域性變量表、運算元棧、動態連結、方法出口等。當方法被呼叫時,棧幀在JVM棧中入棧,當方法執行完成時,棧幀出棧。
區域性變量表中儲存著編譯期可知的各種基本資料型別、物件的引用、返回地址等,一般是在函式的引數和函式內部定義的一些基本型別的變數、物件的引用變數和返回地址。在區域性變量表中,只有long和double型別會佔用2個區域性變數空間(Slot,對於32位機器,一個Slot就是32個bit),其它都是1個Slot。需要注意的是,區域性變量表所需的記憶體空間在編譯期間完成分配,方法在棧幀中需要分配多大的空間是完全確定的,在方法執行期間不會改變區域性變量表的大小。
虛擬機器棧中定義了兩種異常,如果執行緒呼叫的棧深度大於虛擬機器所允許的最大深度,則丟擲StackOverflowError(棧溢位);
多數Java虛擬機器都允許動態擴充套件虛擬機器棧的大小(有少部分是固定長度的虛擬機器棧),如果擴充套件時無法申請到足夠的記憶體,就會丟擲 OutOfMemoryError(記憶體溢位)。
建立過多執行緒會導致JVM效能降低,因為它會導致額外的上下文環境切換開銷,甚至還會導致棧空間記憶體溢位OutOfMemoryError。
本地方法棧
本地方法棧在作用、執行機制、異常型別等方面都與虛擬機器棧相同,唯一的區別是:虛擬機器棧執行Java方法,而本地方法棧執行Native方法。在很多虛擬機器中(如Sun HotSpot虛擬機器),直接就把本地方法棧和虛擬機器棧合二為一。
堆區
在JVM所管理的記憶體中,堆區是最大的一塊,堆區也是Java GC機制所管理的主要記憶體區域,堆區由所有執行緒共享,在虛擬機器啟動時建立。堆區的存在是為了儲存物件例項,原則上講,所有的物件都在堆區上分配記憶體(不過現代虛擬機器技術裡,也不是這麼絕對,也有棧上直接分配的情況),即堆區用於存放由new建立的物件和陣列。
根據Java虛擬機器規範規定,堆記憶體需要在邏輯上是連續的(在物理上不需要),在實現時,可以是固定大小的,也可以是可擴充套件的,目前主流的虛擬機器都是可擴充套件的。如果在執行垃圾回收之後,仍沒有足夠的記憶體分配,並且堆也無法再擴充套件,將會丟擲OutOfMemoryError:Java heap space異常。
逃逸分析,是一種可以有效減少Java 程式中同步負載和記憶體堆分配壓力的跨函式全域性資料流分析演算法。通過逃逸分析,Java Hotspot編譯器能夠分析出一個新的物件的引用的使用範圍從而決定是否要將這個物件分配到堆上。
JVM在Server模式下的逃逸分析可以分析出某個物件是否永遠只在某個方法、執行緒的範圍內,並沒有“逃逸”出這個範圍,逃逸分析的一個結果就是對於某些未逃逸物件可以直接在棧上分配,由於該物件一定是區域性的,所以棧上分配不會有問題。
為物件分配記憶體就是把一塊大小確定的記憶體從堆記憶體中劃分出來,通常有兩種方法實現:
1 、指標碰撞法
假設Java堆中記憶體時完整的,已分配的記憶體和空閒記憶體分別在不同的一側,通過一個指標作為分界點,需要分配記憶體時,僅僅需要把指標往空閒的一端移動與物件大小相等的距離。
2、空閒列表法
事實上,Java堆的記憶體並不是完整的,已分配的記憶體和空閒記憶體相互交錯,JVM通過維護一個列表,記錄可用的記憶體塊資訊,當分配操作發生時,從列表中找到一個足夠大的記憶體塊分配給物件例項,並更新列表上的記錄。
物件建立是一個非常頻繁的行為,進行堆記憶體分配時還需要考慮多執行緒併發問題,可能出現正在給物件A分配記憶體,指標或記錄還未更新,物件B又同時分配到原來的記憶體,解決這個問題有兩種方案:
1、採用CAS保證資料更新操作的原子性;
2、把記憶體分配的行為按照執行緒進行劃分,在不同的空間中進行,每個執行緒在Java堆中預先分配一個記憶體塊,稱為本地執行緒分配緩衝(Thread Local Allocation Buffer, TLAB)。
方法區
方法區是各個執行緒共享的區域,用於儲存已經被虛擬機器載入的類資訊(即載入類時需要載入的資訊,包括版本、變數、方法、介面等資訊)、final常量、靜態變數、即時編譯器編譯後的程式碼等資料。
方法區在物理上也不需要是連續的,可以選擇固定大小或可擴充套件,並且方法區比堆還多了一個限制:可以選擇是否執行垃圾收集。一般的,方法區上執行的垃圾收集是很少的,這也是方法區被稱為永久代的原因之一(HotSpot虛擬機器),但這也不代表著在方法區上完全沒有垃圾收集,方法區的垃圾收集主要是針對常量池的記憶體回收和對已載入類的解除安裝。
在方法區上定義了OutOfMemoryError:PermGen space異常,在記憶體不足時丟擲。
執行時常量池(Runtime Constant Pool)是方法區的一部分,用於儲存編譯期生成的字面常量、符號引用、翻譯出來的直接引用(符號引用就是編碼是用字串表示某個變數、介面的位置,直接引用就是根據符號引用翻譯出來的地址,將在類連結階段完成翻譯)。執行時常量池除了儲存編譯期常量外,也可以儲存在執行時間產生的常量(比如String類的intern()方法,作用是String維護了一個常量池,如果呼叫的字元“abc”已經在常量池中,則返回池中的字串地址,否則,新建一個常量加入池中,並返回地址)。
private final String str = "JVM"; // 在執行時常量池中
private String strBuilder = new StringBuilder(“Java”).append(“JVM”).toString();// 在堆區
private String strOfIntern = strBuilder.intern();// 在執行時常量池中
直接記憶體(Direct Memory)
直接記憶體並不是虛擬機器執行時資料區的一部分,可以這樣理解,直接記憶體就是JVM以外的機器記憶體,假設有4G的記憶體,JVM佔用了1G,則其餘的3G就是直接記憶體。
JDK 1.4中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)和緩衝區 (Buffer)的I/O方式,可以使用Native函式庫直接分配堆外記憶體,然後用儲存在JVM堆中的DirectByteBuffer物件作為這塊記憶體的引用進行操作。 由於直接記憶體受到機器記憶體的限制,所以動態擴充套件時也可能出現OutOfMemoryError的異常。