1. 程式人生 > >深入理解JVM—第二章:Java記憶體區域與記憶體溢位異常

深入理解JVM—第二章:Java記憶體區域與記憶體溢位異常

1,概述

Java較C、C++,Java可以利用虛擬機器的自動記憶體管理機制,避免繁瑣的記憶體分配與回收。不容易出現記憶體洩漏和記憶體溢位問題。

記憶體洩漏:指程式申請到的記憶體空間不再歸還(無法歸還),可使用完該記憶體空間的程式也不能再訪問該空間(可能是丟失了該記憶體空間的地址)。

記憶體溢位:指程式想申請的記憶體空間,系統不能滿足,超出系統空閒記憶體空間。

2,執行時資料區域

這裡寫圖片描述

2.1 程式計數器

它是一塊較小的記憶體空間,可看做是位元組碼的行號指示器。位元組碼的直譯器工作時就是通過改變這個程式計數器的值來選取下一條需要執行的位元組碼指令。

每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間計數器互不影響,獨立儲存,因此,該記憶體區域成為“執行緒私有”的記憶體。

若執行緒在執行java方法,則PC計數器記錄的是虛擬機器位元組碼指令的地址。若執行的方法是native方法,則這個計數器值為空。該記憶體區域是java虛擬機器規範中唯一一個沒有規定任何OutOfMemoryError情況的區域。

ps:native方法作用:
①直接訪問作業系統底層(如系統硬體)
②訪問一個老的系統或已有的庫,而該系統或庫不是有Java編寫的
③提高程式效能

2.2 Java虛擬機器棧

Java虛擬機器棧是執行緒私有的,每個執行緒對應一個虛擬機器棧。

虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時會建立一個棧幀,方法執行,棧幀入虛擬機器棧,方法執行完成,棧幀出虛擬機器棧。

棧幀主要儲存的資訊如下四樣:
①區域性變量表
儲存了方法引數和方法內定義的區域性變數,其最大容量在.java檔案編譯成.class檔案的時候已經確定好。寫在Class檔案的Code屬性的max_locals中。
②運算元棧
在方法執行過程中,位元組碼指令會往運算元棧中寫入和讀取內容。運算元棧的最大深度也是在.java檔案程式設計為.class檔案的時候就已經確定好的。寫在Class檔案的Code屬性的max_stacks資料項中。
③動態連結
指向執行時常量池中該棧幀所屬方法的符號引用。並且該符號引用在執行期間才解析成直接引用。
④方法返回地址
正常完成出口:執行引擎遇到任意一個方法返回的位元組碼指令。此時返回的地址一般是呼叫者的PC計數器的值,其地址在棧幀中存。
異常完成出口:在方法體內出現異常沒得到解決。其返回地址要通過異常處理表來確定,棧幀中不儲存其資訊。

Java虛擬機器規範中,規定兩種異常狀況:
①執行緒器請求的棧深度大於虛擬機器所允許的深度,丟擲StackOverflowError異常
②可自動拓展的虛擬機器棧(大部分都可以),如果拓展時無法申請到足夠的記憶體,丟擲OutOfMemoryError異常

2.3 本地方法棧

本地方法棧類似於虛擬機器棧,不過本地方法棧是為虛擬機器使用到Native方法而服務。

對於Sun HotSpot虛擬機器直接把本地方法棧跟虛擬機器棧合二為一。

本地方法棧類似虛擬機器棧也會丟擲StackOverflowError和OutOfMemoryError異常。

2.4 Java堆

Java堆是被所有執行緒所共享的一塊記憶體區域,在虛擬機器啟動時建立。此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。

Java堆是垃圾收集器管理的主要區域。Java堆可細分為:新生代和老年代。

ps: 新生代:指很快被回收或不是特別大的物件
老年代:指經歷好幾次回收仍或者或特別大的物件

Java虛擬機器規範中,Java堆可以處於物理上不連續的記憶體空間,只要邏輯上連續即可。當前主流的虛擬機器都是按照拓展來實現的,在堆中沒有記憶體完成例項分配,並且堆也無法再拓展時,將會丟擲OutOfMemoryError異常。

2.5 方法區

儲存已被虛擬機器載入的類資訊、靜態變數、常量池和即時編譯器編譯後的程式碼等資料。

方法區是被各個執行緒所共享的記憶體區域。方法區也被稱為“永久代”

對於方法區的回收成績往往不如人意,但是對方法區的回收是必要的。該區域的記憶體回收目標是針對常量池的回收和對型別的解除安裝。

Java虛擬機器規範中,當方法區無法滿足記憶體分配需求的時候,就會丟擲OutOfMemoryError異常。

2.6 執行時常量池

儲存在編譯器生成的各種字面量和符號引用,以及符號引用解析後得到的直接引用。

執行時常量池具有動態性,不僅編譯期可以產生常量,而且在執行期間也可以將新的常量放入池中,如String類的intern()方法。

ps:
JDK1.7前:String類的intern()方法用於判斷常量池中是否有該字串,有,則返回其引用,無,則先在常量池中新增該字串,再返回其引用。
JDK1.7及之後:將常量池從永久代移到Java堆中,String類的intern()方法用於判斷常量池中是否有該字串,有,則返回其引用,無,則記錄該例項的引用。

執行時常量池是方法區的一部分,當常量池無法申請到方法區的記憶體時,丟擲OutOfMemoryError異常。

2.7 直接記憶體(堆外記憶體)

直接記憶體就是堆外記憶體,它並不是java虛擬機器執行時資料區的一部分,也不是java虛擬機器規範中定義的記憶體區域。但這部分記憶體也被頻繁地使用。

在JDK1.4中加入一個NIO類,引入一種基於通道與緩衝區的I/O方式,它可以使用Native函式庫直接分配堆外記憶體。然後通過儲存在Java堆中的DirectByteBuffer物件作為這塊記憶體的引用進行操作。這樣子可以避免資料在Java堆和Native堆中來回複製。

直接記憶體是受OS管理的,屬於核心態,Java堆是受JVM管理的,屬於使用者態。如果從堆內向磁碟寫資料時,資料會被先複製到堆外記憶體,即核心緩衝區,然後再由OS寫入磁碟,使用堆外記憶體避免了資料從使用者態向核心態的拷貝。

PS:關於直接記憶體的詳細介紹,引用庫昊天的一遍部落格文章,連結為:堆外記憶體

3,HotSpot虛擬機器物件探祕

3.1 物件的建立

①遇到new指令,先判斷這個類在方法區中有沒有被載入了。具體操作:去常量池中找該類的符號引用,並檢查該符號引用所代表的類有沒有被載入、準備、解析、初始化。若有則下一步,沒有則執行類載入過程。

②在類載入檢查通過後,為新生物件在Java堆中分配記憶體。先看該Java堆中使用的垃圾收集器是否帶有壓縮整理功能。
p1:有壓縮整理功能,說明Java記憶體是絕對規整的,利用一個指標作為已用記憶體和空閒記憶體的分界點指示器,分配記憶體時向空閒記憶體那邊挪動一段與物件大小相等的距離,這種分配方式叫做“指標碰撞”。
p2:如果沒有壓縮整理功能,說明Java記憶體是不規整的,虛擬機器需要維護一個列表,記錄哪些記憶體塊是可用的。在分配記憶體時找到一塊足夠大的記憶體空間劃分給物件使用,同時更新列表資訊。

解決多執行緒中,分配記憶體遇到的問題:
第一種:對分配記憶體空間的動作進行同步處理—-實際上虛擬機器採用CAS配上失敗重試的方式保證更新操作的原子性(原子性就是中途出現問題則回滾,只有同時成功或同時失敗)。
第二種:把記憶體分配的動作按照執行緒劃分在不同的空間上進行,每個執行緒先預先分配一塊記憶體,成為本地執行緒分配緩衝TLAB。哪個執行緒需要分配記憶體,就在哪個執行緒的TLAB上分配,只有TLAB用完並分配新的TLAB時,才需要同步鎖定。

③記憶體分配完成後,為例項欄位初始化為零值。保證物件的例項欄位在Java程式碼中可以不賦初始值就直接使用。

④虛擬機器再對物件進行設定。如這個物件是哪個類的例項、該類的元資料資訊在哪裡、物件的雜湊碼、物件的GC分代年齡等資訊。這些資訊存放在物件的物件頭中。

⑤執行 < init > 方法,把物件按照程式設計師的意願進行初始化。

3.2 物件的記憶體佈局

在HotSpot虛擬機器中,物件在記憶體中的儲存佈局可以分為3塊區域:物件頭、例項資料和對齊填充

①物件頭

物件頭包括兩部分資訊。
第一部分用於儲存物件自身的執行時資料,簡稱Mark Word。包含雜湊碼、GC分代年齡、鎖狀態標記、執行緒持有鎖、偏向執行緒ID等。
第二部分是型別指標,它指向其類元資料的指標,虛擬機器根據這個指標確定這個物件是哪個類的例項。
如果物件是一個數組,在物件頭還需要有一塊記錄陣列長度的資料。因虛擬機器可以根據陣列中java物件的元資料資訊確定物件的大小,但是無法知道陣列的大小。

PS:元資料就是描述資料的資料,而註解就是原始碼的元資料。如方法上加上註解@override,其程式碼的元資料描述這是父類方法的重寫。

②例項資料

例項資料就是物件真正儲存的有效資訊,也是在程式程式碼中所定義的各種型別的欄位記憶體。從分配策略上來看,相同寬度的欄位總是被分配到一起。

③對齊填充

對齊填充並不是必須的。它僅僅起到佔位符的作用。從而保證在自動記憶體管理系統中要求的物件起始地址為8位元組的整數倍。就是說物件的大小必須是8位元組的整數倍,而物件頭部分剛好就是8位元組的倍數,所以當例項資料部分沒有對齊的時候,就需要用到對齊填充來補全。

3.3 物件的訪問定位

在使用物件的時候,需要通過棧上的reference資料來操作堆上的具體物件。

訪問物件的兩種主流方式:
①使用控制代碼:在Java堆中建立控制代碼池,reference指向物件的控制代碼地址,而控制代碼中包含了物件例項資料指標和物件型別資料指標。

②使用直接指標:

reference直接指向Java堆中物件的起始地址,其物件中儲存了物件型別資料的指標。

兩種訪問方式的優勢:
控制代碼:好處是在reference中儲存的是控制代碼池中該物件的控制代碼地址,在GC後,物件被移動,僅僅需要改變控制代碼中的例項資料指標,而reference不用修改。
直接指標:好處就是節省一次指標定位的時間開銷,在物件被頻繁訪問的情況下,積小成多,很有效的節約成本。Sun HotSpot使用該方式來訪問物件。

—–參考《深入理解Java虛擬機器》 周志明 著