天天看點

jvm之java記憶體區域(一)

一、概述

用c或者c++寫過算法的人都該知道,對于記憶體管理區域,需要手動設定和管理,即擁有每個對象的所有權,但也背負着每個對象生命的開始和結束。但是在java中,就不需要這麼複雜的操作了,在虛拟機自動記憶體的管理機制下,不需要特意的去管理對象的 ‘生’ 和 ‘死’ 了,也因虛拟機的存在,不容易出現記憶體洩露和溢出問題。但是正是因為虛拟機幫java程式員解決了記憶體管理的問題,是以需要去學習 jvm 是如何操作的,這樣一旦在出現記憶體洩露的問題的時候,将可以快速的排查錯誤和解決問題

二、運作時資料區
jvm之java記憶體區域(一)

如上圖所示,java虛拟機在執行java程式的過程中會将它管理的記憶體劃分為若幹個不同的資料區域,不同的區域有着不同的用途,根據《java虛拟機規範》的規定,java虛拟機所管理的記憶體包括上圖上面的幾個運作時資料區域

1,程式計數器

程式計數器:Program Counter Register,指的是一塊較小的記憶體空間,可以當做是目前線程所執行的位元組碼的行号暗示器,主要是通過改變這個計數器的值選取下一條需要執行的位元組碼指令,它是程式控制流的訓示器,分支,循環,跳轉,異常處理,線程恢複等基礎功能都是需要這個計數器來完成。一個線程隻會執行一條程式中的指令,每條線程都會有一個獨立的程式計數器。

2,java虛拟機棧

屬于線程私有,即它的生命周期與線程的相同。虛拟機棧描述的是java方法執行的記憶體模型:每個方法在被執行的時候,都會建立一個棧幀,用于存儲局部變量表,操作數棧,動态連接配接,方法出口等。主要用于存放局部變量,如java虛拟機的基本資料類型和對象應用類型。局部變量表所需的空間在編譯期間完成配置設定,是以在進入一個方法時,這個方法所需要的棧幀是确定的,在方法運作期間不會改變局部變量表的大小

如果線程請求的棧深度大于虛拟機所允許的深度,将抛出StackOverflowError異常;如果java虛拟機可以動态擴充,當棧擴充時無法申請到足夠的記憶體會抛出OutOfMemoryError

3,本地方法棧

本地方法棧與虛拟機棧所發揮的作用是非常相似的,其差別是虛拟機棧為虛拟機執行的java方法服務,而本地方法則是為虛拟機使用到本地方法服務,與虛拟機棧一樣,當棧擴充失敗或者棧深度溢出時分别會抛出OutOfMemoryError異常和StackOverflowError異常

4,java堆

java堆是被所有線程共享的一塊區域,在虛拟機啟動時建立,此區域的唯一目的是存放對象的執行個體,在java中,“幾乎” 所有的執行個體都是這配置設定記憶體。如 A a = new A(),new A()存儲的地點就是在堆裡面的,而a是存儲在棧裡面的。

根據《java虛拟機規範》的規定,java堆可以處于實體上不連續的記憶體空間,但在邏輯上他應該是被視為連續的,這點就像磁盤空間去存儲檔案一樣,并不要求每個檔案都連續存放。

java堆也可以根據參數(-Xmx和-Xms)來實作擴充,如果在java堆中沒有記憶體完成執行個體配置設定,并且對也無法在進行擴充時,java虛拟機将會抛出OutOfMemoryError異常

5,方法區

各個線程共享的記憶體區域,用于存儲已被虛拟機加載的類型資訊、常量、靜态變量、即時編譯器編譯後的代碼緩存等資料。

在《java虛拟機規範》中對方法區的限制是非常寬松的,除了和java堆一樣不需要連續的記憶體和可以選擇固定大小或者可擴充外,甚至還可以選擇不實作垃圾收集;并根據規定,如果方法區中無法滿足記憶體配置設定的需求時,将抛出OutOfMemoryError異常

6,運作時常量池

該常量池也屬于方法區的一部分,用于存放編譯器生成的各種字面量與符号應用,這部分内容将在類加載後存放到方法區的運作常量池中,如一些靜态的變量或者屬性。是以在static 的變量或者屬性中,這些都是随着類的加載而加載的,并且這些屬性或者變量即存在常量池中,是以有一些很坑的面試題,問你 String str = new String(“abc”) 該對象可能建立了幾次,是以就是可能在靜态常量池中建立了一次,又在堆中建立了一次,或者隻在堆中建立了一次,是以答案是一次或者兩次,這取決于在常量池中是否建立過。

運作時常量池相對于Class檔案常量池的另外一個重要的特征就是具備動态性,java語言并不要求一定隻有在編譯期才能産生,也就是說,運作期間也能将新的常量放入到常量池中。常量池屬于方法區的一部分,是以如果常量池中無法滿足記憶體配置設定的需求時,将抛出OutOfMemoryError異常

三,對象的建立

java是一門面向對象的語言,java程式中無時無刻都有程式被建立出來。當java虛拟機在遇到一條位元組碼為new的指令的時候,首先會去檢查這個指令的參數是否能在常量池中去定位到一個類的符号引用,并檢查這個符号引用代表的類是否被加載、解析和初始化,如果沒有,則必須執行相應的類加載過程。

在類加載通過之後,接下來虛拟機将會為新生對象配置設定記憶體,對象所需的記憶體大小在類加載之後便可完全确定,為對象配置設定空間的任務實際上就是将一塊确定大小的記憶體從java堆中劃分出來。主要方式有兩種,分别是 指針碰撞 和 空閑清單

指針碰撞:假設堆中記憶體是絕對規整的,所有使用過的記憶體都被放在一邊,空閑的放在另外一邊,之間放着一個指針作為分界點的訓示器,配置設定記憶體的方式就是把那個指針向空閑的一方挪動一段與對象大小相等的距離

空閑清單:假設堆中記憶體并不規整,那麼虛拟機需要維護一張表,記錄哪些記憶體是可用的,在配置設定的時候從清單中找到一塊足夠大的空間劃分給對象執行個體,并更新清單的記錄

差別:選擇哪種方式由java堆是否規整決定,而java堆是否規整又由所采用的垃圾收集器是否帶有 空間壓縮整理 的能力決定

當然以上是在單線程的情況下操作,如果在并發情況下,線程也是不安全的,解決這個問題有兩種方案,一種是CAS來保證更新操作的原子性,另一種就是将記憶體配置設定的動作按照線程劃分在不同的空間進行,即每個線程在java堆中預先配置設定記憶體,稱為本地線程配置設定緩沖

在以上工作都完成之後,從虛拟機的視角來看,一個新的對象以及産生了,但是從java程式的視角來看,對象建立才剛剛開始——構造函數,即Class檔案的()方法還沒有開始執行。是以在java編譯器遇到new關鍵字的地方會同時生成這兩條位元組碼指令,new指令後會接着執行()方法,這樣一個真正的可用的對象才被建構出來

四,對象的通路定位

建立對象自然就是為了後續的使用該對象,主流的方式主要有 句柄 和 直接指針 等兩種方式

jvm之java記憶體區域(一)
jvm之java記憶體區域(一)

圖一使用的為句柄通路,即在java堆中将可能會劃分出一塊記憶體來作為句柄池,reference 中存儲的就是對象的句柄位址,而句柄包含了對象執行個體資料域類型資料各自具體的位址資訊

圖二為使用直接指針通路,Java堆中對象的記憶體布局就必須考慮放置通路資料類型的相關資訊,reference中存儲的對象位址就是直接位址,如果就是通路對象本身的話,就不需要一次間接通路的開銷

優缺點:

使用句柄通路時帶來的最大的好處就是reference中存儲的是穩定的句柄位址,在對象被移動的時候(垃圾回收而移動)隻會改變句柄中的執行個體資料指針,而reference本身不需要改變。但是會很廢記憶體,增加記憶體的開銷和通路的時間開銷

使用直接指針來通路最大的好處就是速度更快,他節省了一次指針機關的時間開銷和記憶體的開銷,由于對象在java中通路非常的頻繁,是以這類開銷積少成多也是一項極為客觀的執行成本。但是就是不穩定,随着對象的移動而需要重新定位

是以從整個軟體的開發範圍來看,各種語言和架構還是更偏向使用句柄通路

參考書籍:《深入了解Java虛拟機》–周志明版