第二章(java內存區域與內存溢出異常)

程序計數器

是一塊較小的內存空間,字節碼解析器工作時通過改變程序計數器的值來選取下一條需要執行的字節碼指令。程序的分支、循環、跳轉、異常處理以及線程恢復等基礎功能都是依賴程序計數器來完成。
Java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間片來實現,因此,爲了確保線程切換之後能恢復到正確的執行位置,每條線程都需要一個獨立的程序計數器,因此程序計數器是線程私有的內存。
程序計數器是java虛擬機中唯一一個沒有規定任何內存溢出OutOfMemoryError的內存區域。

java虛擬機棧

Java虛擬機棧也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是java方法執行的內存模型:每個方法被執行時都會同時創建一個棧幀用於存放局部變量表、操作數棧、動態連接和方法出口等信息。每個方法被調用直至執行完成過程,就對應着一個棧幀在虛擬機中從入棧到出棧的過程。

Java虛擬機棧有兩種異常狀況:如果線程請求的棧深度大於虛擬機所允許的最大深度時,拋出StackOverflowError異常;如果虛擬機棧可以動態擴展,當擴展時無法申請到足夠內存時會拋出OutOfMemoryError異常。

本地方法棧

本地方法棧與java虛擬機棧作用非常類似,其區別是:java虛擬機棧是爲虛擬機執行java方法服務,而本地方法棧是爲虛擬機調用的操作系統本地方法服務。HotSpot不區分本地方法棧和虛擬機棧。

堆是java虛擬機所管理的內存區域中最大一塊,java堆是被所有線程所共享的一塊內存區域,在java虛擬機啓動時創建,堆內存的唯一目的就是存放對象實例。幾乎所有的對象實例都是在堆分配內存。
Java堆是垃圾收集器管理的主要區域,從垃圾回收的角度看,由於現在的垃圾收集器基本都採用的是分代收集算法,因此java堆還可以初步細分爲新生代和年老代。
Java虛擬機規範規定,堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可。在實現上即可以是固定大小的,也可以是可動態擴展的。如果在堆中沒有內存完成實例分配,並且堆大小也無法在擴展時,將會拋出OutOfMemoryError異常。

方法區

方法區與堆一樣,是被各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯後的代碼等數據。雖然java虛擬機規範把方法區描述爲堆的一個邏輯部分,但是方法區卻有一個別名叫Non-Heap(非堆)。

Sun HotSpot虛擬機把方法區叫永久代(Permanent Generation),其他虛擬機沒有永久代的概念。方法區中最重要的部分是運行時常量池。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各種字面變量、符號引用、直接引用等,這些內容將在類加載後存放到方法區的運行時常量池中,另外在運行期間也可以將新的常量存放到常量池中,如String的intern()方法。
方法區和運行時常量池在無法滿足內存分配時,也會拋出OutOfMemoryError異常。

直接內存

直接內存並不是java虛擬機運行時數據區的一部分,也不是java虛擬機規範中定義的內存區域,但是在java開發中還是會使用到。
JDK1.4中新引入的NIO(new I/O),引入了一種基於通道(Channel)和緩衝區(Buffer)的I/O方式,可以使用操作系統本地方法庫直接分配堆外內存,然後通過一個存儲在java堆裏面的DirectByteBuffer對象作爲堆外直接內存的引用進行操作,避免了java堆內存和本地直接內存間的數據拷貝,可以顯著提高性能。
雖然直接內存並不直接收到java虛擬機內存影響,但是如果java虛擬機各個內存區域總和大於物理內存限制,從而導致直接內存不足,動態擴展時也會拋出OutOfMemoryError異常。

對象的創建

虛擬機遇到一條new指令時,首先檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被加載,解析和初始化。如果沒有則執行類加載過程。

類加載檢查通過後,虛擬機爲對象分配內存。對象所需內存的大小在類加載完成後完全確定。假設java堆是絕對規整的,空閒內存與用過的內存中間有個指針,分配的時候就是把指針向空閒空間那邊移動一段與對象大小相等的距離。這種分配方式稱爲“指針碰撞”。如果不是規整的,虛擬機就維護一個列表,記錄哪些內存塊是可用的,在分配的時候從列表中找到一個足夠大的空間劃分給對象。並更新列表上的記錄。這種分配方式便是”空閒列表“。

還有一個併發問題,有兩種解決方案:
1. 對分配內存空間的動作進行同步處理。實際上虛擬機採用CAS配上失敗重試的方法保證更新操作的原子性。
2. 把內存分配的動作按照線程劃分在不同的空間之中進行。即每個線程在java堆中預先分配一小塊內存,稱爲本地線程分配緩衝TLAB。哪個線程要分配內存,就在哪個線程的TLAB上分配。只有TLAB用完並分配新的TLAB時,才需要同步鎖定。虛擬機是否採用TALB,通過參數-XX:+/-UseTLAB設定。

內存分配完成後虛擬機將分配到的空間都初始化爲0值(不包括對象頭)。保證對象的實例字段在java代碼中可以不賦初始值就可以直接使用。

接下來,虛擬機對對象進行必要的設置,例如這個對象是哪個類的實例,如何才能找到類的元數據信息,對象的哈希碼等信息,這些信息存放在對象的對象頭之中。

執行完new指令之後,接下來會執行方法。進行初始化。

對象的內存佈局

對象在內存中儲存的佈局可以分爲3塊區域:對象頭,實例數據和對齊填充。

HotSpot虛擬機的對象頭包括兩部分信息,第一部分用於存儲對象自身運行時數據,如哈希碼,GC分代年齡,鎖狀態標誌等。對象頭的另外一部分是類型指針。即指向它的類元數據的指針。虛擬機通過這個指針確定對象是哪個類的實例。注意,並不是所有的虛擬機實現都必須在對象數據上保留類型指針。如果是java數組,在對象頭還需要有一塊用於記錄數組長度的數據。因爲虛擬機無法從數組的元數據中確定數組的大小。而普通對象可以。

實例數據纔是對象真正儲存的有效信息。也是在程序代碼中所定義的各種類型的字段內容。HotSpot虛擬機把相同寬度的字段分配在一起。滿足這一前提下,父類中定義的變量出現在子類前面。

對齊填充並不是必然存在的。HotSpot虛擬機的自動內存管理要求對象起始地址必須是8字節的整數倍。也就是說對象的大小必須是8字節的整數倍,而對象頭正好是8字節的整數倍。因此當實例數據沒有對齊時需要填充。

對象的訪問定位

java程序需要通過棧上的reference數據來操作堆上的具體對象。reference類型只規定了一個指向對象的引用。目前主流的有使用句柄和直接指針兩種。

  1. 使用句柄,java堆中劃分出一塊內存來作爲句柄池,reference儲存的就是對象的句柄地址,而句柄包含了對象實例數據與類型數據的各自的具體地址信息。
  2. 直接內存訪問,reference存儲的是對象地址,而對象裏面應該有一個到對象類型數據的指針。

簡單虛擬機參數

  • -Xmx,-Xms:堆的最大值與最小值
  • -Xss:棧容量
  • MaxPermSize:最大方法區容量。

String.intern()

intern方法的作用:如果字符串常量池中已經包含一個等於String對象的字符串,則返回代表池中這個字符創的對象。否則,將此String對象包含的字符串添加到常量池中,並且返回此String對象的引用。

JKD1.6中,intern()方法會在首次遇到的字符串複製到永久代中,返回的也是永久代中這個字符串的引用。而由StrignBuilder創建的字符串實例在java堆上,所以不是同一個引用。JKD1.7不會再複製實例。只是在常量池中記錄首次出現的實例引用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章