jvm初探(二):JAVA內存詳解


我們知道,對於java開發者來說,在jvm自動內存管理機制的幫助下,不再需要爲每一個對象都特定專門的釋放方法,不容易出現內存泄露和溢出的問題,由jvm管理內存。不過,也正是因爲java程序員將內存管理的權利交給了jvm,一旦出現了內存泄露和溢出方面的問題,如果不瞭解jvm是如何使用內存的,那麼排查錯誤將會變成一項很艱難的工作。
本篇文章將介紹jvm的內存區域組成及其作用以及對象創建及使用的全過程

一.運行時數據區域:

在我們上一篇文章:jvm初探(一):java類加載機制與過程 中,我們說到,類加載的第一步——加載,它的任務就有將字節流中的靜態存儲結構轉化成虛擬機中方法區的運行時數據結構,然後才能夠被java虛擬機使用。所以,弄清楚運行時數據結構裏面的組成對於java內存管理是非常重要的。總覽如下圖,下面我們將一一詳解:
![java內存圖]Alt

程序計數器:——線程私有

它是一塊較小的內存空間,它可以看做是當前線程所執行的字節碼的行號指示器。在jvm的概率模型中(僅僅是概念模型),字節碼解釋器就是通過改變這個計數器的值來選擇下一條要執行的字節碼指令,分支、循環、跳轉、異常處理等基礎功能都需要依賴這個計數器來完成。

  • 如果該線程執行的是java方法,那麼該計數器記錄的就是正在執行的虛擬機字節碼指令的地址
  • 如果該線程執行的是Native方法,那這個計數器就爲空

注意:此內存區域是唯一沒有規定任何OutOfMemoryError情況的區域

Java虛擬機棧:——線程私有

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

通俗來說,java虛擬機棧又可稱作局部變量表。局部變量表中存放了編譯器可知的各種基本數據類型、對象引用和returnAddress類型,其中:

  • 基本數據類型:boolean、int、byte、double等
  • 對象引用:reference類型,它不等同與對象本身,可能是一個指向對象起始地址的指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置
  • returnAddress類型:指向了一條字節碼指令的地址

局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在棧中分配多大空間是完全確定的,在方法運行期間是不會改變局部變量表的大小

內存異常情況

  • 如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常
  • 如果虛擬機可以動態擴展,但擴展時無法申請到足夠的內存,將拋出OutOfMemoryError異常

本地方法棧

它與java虛擬機棧的作用差不多,區別僅僅是java虛擬機棧爲java方法服務
而本地方法棧爲虛擬機使用到的Native方法服務。

Java堆:——線程共享

在虛擬機啓動時創建,此內存區域的唯一目的就是存儲對象實例,在java虛擬機規範中的描述是:所有的對象實例及以及數組都要在堆上分配。

java堆是垃圾收集器管理的主要區域,因此許多時候也被稱爲GC堆

  • 從內存回收的角度來看:由於現在很多收集器基本都採用分代收集算法,所以Java堆中還可細分爲—新生代與老生代;新生代又可分爲—Eden區、From Survivor區、To Survivor區,比例爲8:1:1
  • 從內存分配的角度來看:線程共享的Java堆中可能會劃分出多個線程私有的分配緩衝區(TLAB)。

方法區:——線程共享

它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然Java虛擬機規範將方法區描述爲堆的一個邏輯部分,但是他卻有一個別名叫Non-heap(非堆),目的應該是與Java堆區分開來。

運行時常量區

運行時常量區是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息就是常量池,用於存儲編譯器生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。其中:

  • 字面量:文本字符串及final常量
  • 符號引用:類、字段、方法的名稱等

二、探祕"對象"

對象的創建:

通常情況下,我們新建對象往往是通過new關鍵字來創建的,當虛擬機遇到一條new指令時:

  1. 檢查運行時常量區中是否有該類的符號引用
  2. 檢查該符號引用代表的類是否被加載、連接、初始化過
  3. 如果沒有,那麼就必須進行類加載過程

類加載檢查通過後,虛擬機將爲對象分配內存。對象所需內存大小在類加載後便可完全確定,即將一塊大小確定的內存從堆中劃分出來,而劃分實現的方式隨着虛擬機的不同也不同,這裏主要有2種:

  1. 指針碰撞:假設java堆中的內存是絕對規整的,所有用過的內存放在一邊,沒用的內存放在另一邊,中間放着一個指針作爲分界點的指示器。那麼,所謂分配內存就是講指針移動代表同等內存大小的距離。
  2. 空閒列表:如果java堆中的內存不是絕對規整的,虛擬機就會維護一個記錄了哪些是已被分配的內存,哪些是沒有被分配的內存的列表,分配內存時就直接劃分給對象,並更新列表數據即可

選擇哪種分配方式由java堆是否規整而定,而是否規整又由所採用的垃圾回收器是否帶有壓縮整理功能決定

內存分配完成之後,虛擬機將要使用到的內存都初始化爲零,然後對對象進行必要的設置,如這個對象是哪個類的實例,如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭中。這樣,一個對象就在虛擬機中創建好了。

對象創建過程中的問題:

對於java開發來說,新建銷燬對象是非常頻繁的事情,如果僅僅修改一個指針來管理內存,很可能會出現A線程要new一個對象時,指針還未移動,B線程又new一個對象,來移動之前的指針。這樣就會出現線程安全問題,而解決這種問題,java虛擬機開發者提供了2個方案:

  1. 對分配內存空間的動作進行同步處理——虛擬機採用CAS配上失敗重試的方式保證更新操作的原子性
  2. 將內存分配的動作按照線程劃分在不同的空間中進行,即每個線程在Java堆中預先分配一塊內存,稱爲本地線程分配緩衝(TLAB),哪個線程需要分配內存,就在哪個線程的TLAB上分配,只有TLAB用完並分配新的TLAB時,才需要同步鎖定。虛擬機是否使用TLAB,可以通過-XX:+/UseTLAB參數來設置

對象的內存佈局:

在HotSpot虛擬機中,對象在內存中存儲的佈局可以分爲3個區域:對象頭、實例數據和對齊填充。其中:

  • 對象頭:對象頭一般包括兩部分信息:第一部分存儲對象自身的運行時數據:哈希碼,GC分代年齡,鎖狀態標誌等。第二部分是類型指針,虛擬機能通過這個指針來確定對象是哪個類的實例,但並不是所有的虛擬機都必須保留該指針,還可通過其他方式來查找該對象的信息。
  • 實例數據:是對象真正存儲的有效信息
  • 對齊填充:不是必然存在的,也沒有特別的含義,僅僅起着佔位符的作用,由於HotSpot虛擬機要求對象的大小必須是8字節的整數倍,當對象大小不是8的倍數時,就用對齊填充來補充

對象的訪問定位:

當通過方法來調用對象時,虛擬機是通過在棧上的reference類型數據(即對象的引用)來操作的,但如何通過引用來定位、訪問堆中對象的具體位置呢,這也取決於虛擬機的具體種類,目前主流訪問方式有兩種:

  1. 句柄訪問:使用句柄訪問的虛擬機會在堆中專門劃分出一塊內存作爲句柄池,對象的引用就是存儲的對象的句柄地址,而句柄就包含了對象實例數據與類型數據的具體地址信息,如下圖:
    在這裏插入圖片描述
  2. 直接指針:reference存儲的直接就是對象地址,如下圖:

在這裏插入圖片描述
句柄訪問的優勢就是reference類型存儲的始終是穩定的句柄地址,在對象被移動時(相當頻繁),只會改變句柄中的實例數據指針,而reference本身不需要修改

直接指針訪問方式的優勢就是速度快,節省了一次指針定位的時間開銷,我們通常使用的HotSpot虛擬機就是使用該方式來訪問對象的

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