深入理解Java虛擬機器--個人總結(持續更新)
每天按照書本學一點,會把自己的總結思考寫下來,形成輸出,持續更新,立帖為證
-- 2020年7月7日 開始第一次學習
-- 2020年7月8日 今天在百忙Rush B中抽出時間,學了點習,計劃明天把本地方法棧和Java堆看完總結完
-- 2020年7月10日 第一次週五學習,也算是有進步,翻了一下書感覺好多啊,不知道什麼時候能看完
-- 2020年7月15日 衝鴨!!!!
第二部分、自動記憶體管理
一、Java記憶體區域與記憶體溢位異常
Java與C++在記憶體控制方面截然不同,因為Java虛擬機器有自動記憶體管理機制,所以Java程式設計師就犧牲部分記憶體控制權,來換取編寫程式時的便利。雖然不容易出現記憶體洩漏和記憶體溢位問題,但還是有必要學習點Java虛擬機器相關知識,除了在遇到虛擬機器問題時可以快速解決之外,還可以和別人裝逼(最大的快樂!)
Java虛擬機器在執行Java程式的時候,會將記憶體自動劃分為不同區域,不同區域對應的功能、建立銷燬時間也不同,有些區域會隨著虛擬機器啟動而一直存在,有些區域以來使用者的執行緒啟動結束而建立銷燬。
記憶體區域分為以下幾個區域:
- 程式計數器
- Java虛擬機器棧
- 本地方法棧
- Java堆
- 方法區
- 執行時常量池
- 直接記憶體
程式計數器
- 程式計數器是:一小塊記憶體空間,記錄當前執行緒執行位元組碼指令的地址,位元組碼直譯器就是通過改變計數器裡面的值來確定下一條需要執行的位元組碼指令
- “執行緒私有”的記憶體空間:在任何確定時刻,處理器都只會執行一個執行緒中的指令(注:Java的多執行緒是通過切換執行緒,分配處理器執行時間來實現的),每個執行緒都需要記錄執行到那條指令了,接下來該執行哪條,所以每個執行緒都會有一個執行緒計數器,而且獨立儲存互不干擾
- 儲存內容:
- 如果執行緒正在執行Java方法,則計數器記錄的是正在執行虛擬機器位元組碼的指令地址
- 如果正在執行本地方法(Native),則計數器為空
- 此記憶體區域是唯一一個在《Java虛擬機器規範》中沒有規定任何OutOfMemoryError情況的區域
Java虛擬機器棧
- Java虛擬機器棧:描述的就是在執行Java方法時執行緒記憶體模型。每個方法在被呼叫的時候都會同步建立一個"棧幀",儲存到Java虛擬機器棧中,這個棧幀裡面包含:區域性變數表、運算元幀、動態連線、方法出口等資訊,一個方法從被呼叫開始執行到執行完畢,就對應這棧幀從入棧到出棧過程。
- 執行緒私有記憶體空間:和程式計數器一樣時執行緒私有的,生命週期和執行緒同步。
思考:了下為什麼也是執行緒私有的?應該時每個執行緒執行方法不同,裡面的一些臨時變數等也不會相同,為了在切換執行緒時不會發生混亂互相干擾,所以需要和程式計數器一樣,也是執行緒私有的記憶體空間
- 會有人把Java虛擬機器記憶體空間籠統的劃分為"棧空間","堆空間",這裡說的"棧空間"通常就是指Java虛擬機器棧,在籠統一點通常指的是Java虛擬機器棧裡面的區域性變數表這部分
- 區域性變數表:
- 存放內容:存放了編譯器基本資料型別、物件引用(並不是物件本身,可能是隻想物件起始地址的指標,也可能是指向一個代表物件的控制程式碼???,或者其他於此物件相關的位置),returnAddress型別(返回地址型別,指向一條位元組碼指令的地址)
- 區域性變數表中的儲存空間:都是以區域性變數槽(Slot)來表示,其中64位長度的long和double型別資料佔兩個變數槽,其餘資料型別佔一個。
- 在程式編譯期間就已經確定好了區域性變數表的大小並完成分配。當進入一個方法時,該方法需要在區域性變數表中分配多大的空間都是確定好的,在執行期間不會改變區域性變數表的大小。
- 上面所說的”大小“是指區域性變數槽的數量,不同虛擬機器的一個變數槽可能會佔不同大小記憶體空間(一個變數槽佔32位元或64位元)
- 異常:《Java虛擬機器規範》對該記憶體區域規定了兩個異常:
- 如果執行緒請求棧的深度大於虛擬機器所允許的深度,則會丟擲StackOverflowError異常;
- 如果Java虛擬機器棧的容量可以動態擴充套件,當棧擴充套件時無法申請到足夠的記憶體就會丟擲OutOfMemoryError異常
本地方法棧
本地方法棧與Java虛擬機器棧作用相似,但也稍有區別。Java虛擬機器棧是為虛擬機器執行Java方法(位元組碼)服務的,而本地方法棧是為虛擬機器執行本地方法(Native)服務的。
因為在《Java虛擬機器規範》中,並沒有對本地方法棧做強制規定,所以不同虛擬機器實現的方式可能不同,有些虛擬機器(HotSpot虛擬機器)直接將本地方法棧和Java虛擬機器棧合二為一
與Java虛擬機器棧一樣,當本地方法棧深度超出規定(溢位)和棧擴充套件失敗的時候,也會報StackOverflowError和OutOfMemoryError異常
Java堆(Java Heap)
Java堆是虛擬機器管理記憶體中最大的一塊,被所有執行緒共享,在虛擬機器啟動時建立。
主要是負責存放物件例項,按照《Java虛擬機器規範》描述是:所有物件例項以及陣列都應當在堆上分配。考慮到Java語言的發展,和即時編譯技術的出現,未來可能會出現物件例項不在堆上分配的情況。
Java堆是垃圾收集器管理的記憶體區域,因此也被稱為"GC堆"。垃圾收集器大部分是基於分代收集理論設計的,所以會出現新生代、老年代、永久代,Eden空間、Form Survivor空間、To Survivor空間等名詞,這些劃分的區域僅僅是垃圾收集器共同特性或設計風格,並不能說是Java堆是由這些區域組成的。
從分配記憶體角度說,執行緒共享的Java堆可以劃分多個執行緒私有的分配緩衝區(TLAB),劃分出來的唯一作用還是存放物件例項,目的是為了更快更好的分配和回收記憶體。
Java堆在邏輯上是連續的,但在物理上並不要求連續。如果存放的是大物件,例如:陣列物件,大多數虛擬機器為了實現簡單、儲存高效,可能會要求連續的儲存空間。
Java堆既可以是固定大小,也可以是可擴充套件的。目前主流虛擬機器都是可擴充套件的,通過引數-Xmx和-Xms設定。如果在Java堆中沒有記憶體給物件例項分配,並且無法再擴充套件時,Java虛擬機器將會丟擲OutOfMemoryError異常。
方法區
在《Java虛擬機器規範》中對方法區的約束是十分寬鬆的,許多部分和Java堆相同,例如:
- 都是執行緒共享
- 物理上不需要連續的儲存空間
- 可以選擇固定大小或可擴充套件
並把方法區描述為堆的一個邏輯部分,但是方法區和堆還是有區別的,方法區的另一個別名叫"非堆(Non-Heap)",方法區用來存放已經被虛擬機器載入的型別資訊、常量、靜態變數、即時編譯器編譯後的程式碼快取等資料。
方法區與永久代關係
本質上兩者並不是等價的,但很多人將兩者混為一談,這是因為當初HotSpot虛擬機器在設計的時候,為了簡單方便可以像管理Java堆一樣管理這部分記憶體,將垃圾收集器的分代設計擴充套件至方法區,即使用永久代來實現方法區。但Java虛擬機器規範中並沒有對方法區的實現做具體要求,所以其他虛擬機器(如:BEA的JRockit、IBM的J9)都沒有永久代這個概念。
使用永久代實現方法區好處:
可以像管理Java堆一樣管理一部分記憶體,省去了專門為方法區編寫管理程式碼的工作
使用永久代實現方法區壞處:
會導致Java應用更容易遇到記憶體溢位的問題,永久代有-XX:MaxPermSize的上限,即使沒有設定也有預設值,而J9和JRockit只要沒有觸碰到程式可用記憶體的上限,例如32位系統中4GB限制,就不會出現問題。
有極少數方法(String::intern())會因永久代的原因而導致不同虛擬機器下有不同表現
永久代介紹
垃圾收集行為在永久代很少出現,但並不是資料進入永久代之後就永久存在了,這一區域記憶體回收目的主要是針對常量池回收和對型別的解除安裝,但是因為回收條件嚴格,所以回收效果總不能令人滿意。
- JDK6的時候,HotSpot開發團隊計劃放棄使用永久代,逐步改為採用本地記憶體(Native Memory)來實現方法區
- JDK7的時候,把原本放在永久代的字串常量池、靜態變數移出
- JDK8的時候,放棄使用永久代,在本地記憶體中實現元空間(MetaSpace)來代替,並把JDK7中還保留在永久代中的內容全部移出。
當方法區無法滿足新內容記憶體分配的時候,就會丟擲OutOfMemoryError異常。
執行時常量池
執行時常量池是方法區的一部分,用來存放編輯時生成的各種字面值和符號引用,因為《Java虛擬機器規範》並沒有對這部分做詳細要求,所以虛擬機器開發者可以按照自己需求去實現這部分記憶體。除了上面戳的符號引用外,一般還會將符號引用翻譯出來的直接引用也存到執行時常量池中。
具備動態性。並不一定是預置入Class檔案中常量池才能進入方法區的執行時常量池,執行期間可以將新的常量放入。
當無法申請到足夠記憶體時,會丟擲OutOfMemoryError異常。
直接記憶體
直接記憶體並不是虛擬機器執行時資料區(上面寫的都是)的一部分,也不是《Java虛擬機器規範》中定義的記憶體。
用力提高效能,避免在Java堆和Native堆中來回複製資料。
直接記憶體並不受Java堆記憶體大小的限制,但是受本機總記憶體的限制。根據實際記憶體設定-Xmx等引數時,如果忽略直接記憶體,可能會導致丟擲OutOfMemoryError異常。
二、HotSpot虛擬機器物件探祕
1、物件建立
-
類載入:Java虛擬機器遇到new指令的時候,首先會去常量池定位一個類的符號引用,並檢查這個類是否已經被載入,解析和初始化過。如果沒有則進行類載入過程
-
分配記憶體:在類載入之後,就知道物件所需要的記憶體大小,接下來開始為物件分配記憶體。物件分配記憶體是在堆上完成的,劃出一塊未使用的記憶體給物件,分配的方式有兩種:"指標碰撞","空閒列表"。到底採用哪種分配方式取決於Java堆是否規整,Java堆是否規整又取決於垃圾收集器是否帶有空間壓縮整理(Compact)能力。
分配記憶體中為了解決執行緒安全問題有兩種方案:一、對分配記憶體空間的動作進行同步處理,即虛擬機器採用CAS配上失敗重試的方式保證更新操作的原子性。二、使用本地執行緒分配緩衝區(TLAB),哪個執行緒要分配記憶體,就在哪個執行緒的本地緩衝區進行分配,只有緩衝區用完了,在分配新的緩衝區的時候才需要同步鎖定。
-
賦初始值:保證物件的例項欄位不賦初始值就可以直接使用,可以直接訪問這些欄位的初始值。
-
虛擬機器對物件設定:虛擬機器會將一些必要資訊儲存在物件頭中,如:這個物件是哪個類的例項,如何才能找到類的後設資料資訊,物件的雜湊碼等等。
-
執行建構函式:此時站在虛擬機器角度看物件已經建立好了,但是此時物件中欄位還是預設零值,需要執行建構函式,按照設計意圖構造好。