1. 程式人生 > 實用技巧 >Java虛擬機器(JVM)-自動記憶體管理(簡約版)

Java虛擬機器(JVM)-自動記憶體管理(簡約版)

JVM,是Java Virtual Machine(Java虛擬機器)的縮寫,要完全弄明白JVM,可能需要花很多時間去學習、研究。

胖子語錄:點成線,線成面,切勿貪心,否則一臉懵逼

我們先了解、弄清楚以下幾點,剩下的,讀者自行深造。推薦紙質書《深入理解Java虛擬機器》or深入理解Java虛擬機器,建議一樣來一發,要雨露均沾,同時加深印象,雖然內容一樣的。

1.Java記憶體區域與記憶體溢位異常

1.1 JVM執行時資料區

在這裡插入圖片描述

1.1.1 程式計數器

程式計數器(執行緒私有),佔一個非常小的記憶體空間。它可以看成當前執行緒所執行的位元組碼直譯器的行號指示器。位元組碼直譯器工作時,就是通過改變這個計數器的值來選取下一條需要執行指令的位元組碼指令,如:分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴它完成。

注意這裡:

  • 如是執行緒執行的是Java方法的話,計數器記錄的是正在執行的虛擬位元組碼指令的地址;如果執行緒執行的是native方法(至於什麼叫native方法,讀者自行查閱),則計數器記錄的為undefined。
  • 程式計數器的記憶體區域,是唯一一個JVM規範中,沒有規定任何OutOfMemoryError情況的區域。
1.1.2 JVM棧

描述的是Java方法執行的動態記憶體模型

棧幀:每個方法在執行時,都會建立一個棧幀,用來儲存:區域性變量表、運算元棧、動態連結、方法出口等。方法執行完畢,棧幀銷燬。

所以,每個方法從呼叫到執行結束,對應著該棧幀從JVM棧入棧到出棧的過程。

區域性變量表:存放了編譯期可知的各種基本資料型別、引用型別、返回地址型別(指向了下一條位元組碼的地址)。區域性變量表的大小,在編譯期已經可以確定,在執行時期不會發生改變。

棧的大小:

  • StackOverFlowError:執行緒請求的棧深度大於JVM所允許的深度,報該錯誤
  • OutOfMemoryError:如果JVM棧可以動態擴充套件,而擴充套件時無法申請到足夠的記憶體,報該錯誤
1.1.3 本地方法棧

JVM棧為虛擬機器執行Java方法(也就是位元組碼)服務的。JVM虛擬機器實現可能需要C Stacks來支援Native語言,這個C Stacks就是本地方法棧,本地方法棧是為虛擬機器使用到的Native方法服務的。

1.1.4 Java Heap(Java 堆)

對於大多數應用來說,這塊區域是JVM所管理的記憶體中最大的一塊。執行緒共享,主要存放實力物件和陣列,幾乎所有的物件例項都會在這裡分配記憶體。記憶體會劃分出多個執行緒私有的分配緩衝區(Thread Local Allocation Buffer, TLAB),可以位於物理位置上不連續的空間,但是邏輯位置要連續。

Java Heap儲存的物件,被垃圾收集器管理,這些受管理的物件,無法顯式的銷燬。

OutOfMemoryError:如果堆中沒有記憶體完成例項分配,並且堆也無法再擴充套件時,丟擲該異常。

1.1.5 方法區

所有執行緒共享的執行時記憶體區域。用來儲存已被JVM載入的類資訊、執行時常量池、欄位、方法資訊、靜態變數、即時編譯器編譯後的程式碼等資料。方法區是Java Heap的邏輯組成部分,它一樣是物理上不需要連續,而且可以選擇在方法區中不實現垃圾收集。

執行時常量池:用於存放編譯期生成的各種字面量和符號引用,編譯器和執行期(String 的 intern() )都可以將常量放入池中,記憶體有限,無法申請時丟擲OutOfMemoryError。

1.2 HotSpot虛擬機器

主要介紹資料是如何建立、如何佈局以及如何訪問的。

1.2.1 物件的建立

當遇到new指令時,首先檢查這個指令的引數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已經被載入、解析和初始化過。如果沒有,執行相應的類載入。

類載入檢查通過之後,為新物件分配記憶體(記憶體大小在類載入完成後便可確認)。在堆的空間記憶體中劃分一塊區域(通過“指標碰撞-記憶體規整”或“空閒列表-記憶體交錯”的分配方式)。

因為每個執行緒在堆中都會有私有的分配緩衝區(TLAB),這樣可以很大程式避免在併發情況下頻繁建立物件造成的執行緒不安全。

記憶體空間分配完成後會初始化為0(不包含物件頭),接下來就是填充物件頭,把物件是那個類的例項、如何才能找到類的元資料資訊、物件的雜湊碼、物件的GC分代年齡等資訊存入物件頭。

執行new指令後執行init方法後,才算一份真正可用的物件建立完成。

1.2.2 物件的記憶體佈局

在HotSpot虛擬機器中,分為3塊區域:物件頭(Header)、例項資料(Instance Data)和對齊填充(Padding)

物件頭(Header):包含兩部分

第一部分:用於儲存物件自身的執行時資料,如雜湊碼、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等,32位虛擬機器佔32bit,64位虛擬機器佔64bit,官方稱“Mark Word”。

第二部分:型別指標,即物件指向它的類的元資料指標,虛擬機器通過這個指標確定這個物件是哪個類的例項,官方稱“Klass Word”。

如果是Java陣列,物件頭中還必須有一塊用於記錄陣列長度的資料,因為普通物件可以通過Java物件元資料確定大小,而陣列物件不可以。

例項資料(Instance Data)

物件的屬性。程式程式碼中,所定義的各種型別的欄位內容(包含父類繼承下來的和子類中定義的)。

對其填充(Padding)

不是必須需要,主要是佔位,為了減少堆記憶體的碎片空間(不一定準確),保證物件大小是某個位元組的整數倍。

1.2.3 物件的訪問定位

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

通過句炳訪問

Java堆中會分配一塊記憶體作為句炳池。reference儲存的是句炳地址。

句炳地址包含指向物件例項資料、物件型別資料的指標

使用直接指標訪問

reference中直接儲存物件地址

兩者比較

句炳訪問:在物件移動(GC)只改變句炳包含的例項指標地址,reference自身不需要修改。

直接訪問:速度快,省了一次指標定位的開銷。

結論

如果物件頻繁GC,句炳訪問好;

如果物件頻繁訪問,直接指標訪問好。