深刻理解Java虛擬機--我的總結(持續更新)

深刻理解Java虛擬機--我的總結(持續更新)

天天按照書本學一點,會把本身的總結思考寫下來,造成輸出,持續更新,立帖爲證java

-- 2020年7月7日 開始第一次學習
-- 2020年7月8日 今天在百忙Rush B中抽出時間,學了點習,計劃明天把本地方法棧和Java堆看完總結完
-- 2020年7月10日 第一次週五學習,也算是有進步,翻了一下書感受好多啊,不知道何時能看完
-- 2020年7月15日 衝鴨!!!!

第二部分、自動內存管理

1、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異常。

2、HotSpot虛擬機對象探祕

一、對象建立

  1. 類加載:Java虛擬機遇到new指令的時候,首先會去常量池定位一個類的符號引用,並檢查這個類是否已經被加載,解析和初始化過。若是沒有則進行類加載過程

  2. 分配內存:在類加載以後,就知道對象所須要的內存大小,接下來開始爲對象分配內存。對象分配內存是在堆上完成的,劃出一塊未使用的內存給對象,分配的方式有兩種:"指針碰撞","空閒列表"。到底採用哪一種分配方式取決於Java堆是否規整,Java堆是否規整又取決於垃圾收集器是否帶有空間壓縮整理(Compact)能力。

    分配內存中爲了解決線程安全問題有兩種方案:1、對分配內存空間的動做進行同步處理,即虛擬機採用CAS配上失敗重試的方式保證更新操做的原子性。2、使用本地線程分配緩衝區(TLAB),哪一個線程要分配內存,就在哪一個線程的本地緩衝區進行分配,只有緩衝區用完了,在分配新的緩衝區的時候才須要同步鎖定。

  3. 賦初始值:保證對象的實例字段不賦初始值就能夠直接使用,能夠直接訪問這些字段的初始值。

  4. 虛擬機對對象設置:虛擬機會將一些必要信息保存在對象頭中,如:這個對象是哪一個類的實例,如何才能找到類的元數據信息,對象的哈希碼等等。

  5. 執行構造函數:此時站在虛擬機角度看對象已經建立好了,可是此時對象中字段仍是默認零值,須要執行構造函數,按照設計意圖構造好。

二、對象的內存佈局

三、對象的訪問定位

相關文章
相關標籤/搜索