JVM的基礎世界(一)

GOGOGO

最近抽空又拜讀了下JVM 虛擬機第三版,也算鞏固鞏固知識順便做下總結吧,先拋磚引玉梳理下基礎知識。第三版擴充了不少知識點還是很不錯的,建議有時間可以完整讀一下,這玩意真是每次讀感覺都不一樣啊。
總結:
1、內存結構的基本概念
2、使用new 關鍵字後發生了什麼
3、對象已經創建了,對象在內存中的結構
4、怎麼定位到內存中的對象

0x01 運行時數據區

程序計數器:

可以看成當前線程執行的字節碼行號的指示器,是流程控制指示器分支、循環 跳轉 異常處理 線程切換都依賴它,屬於線程私有。
如果線程執行的是一個java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址。
如果正在執行的是本地(Native)方法,這個計數器值則應爲空(Undefined)
是內存中唯一一個沒有OutOfMemiryError的地方

虛擬機棧:

是線程私有的,生命週期和線程相同。
方法調用時都會創建棧針虛擬機棧包含,局部變量表、操作數棧、動態鏈接、方法出口,方法調用直到完成就是一個入棧出棧的過程。
局部變量表存儲了基礎數據 long int byte char double float boolean short、對象引用、和返回地址。
局部變量表空間以局部變量槽表示,其中long double佔兩個64位,其餘佔一個。
局部變量表所在的內存空間在編譯期就完成分配。
異常問題,有兩類,stackOverFlowError ,OutOfMemoryError,只要棧申請內存分配完成就不會發生第二類異常。

本地方法棧:

和虛擬機棧非常相似、是爲虛擬機使用的本地方法服務也有stackOverFlowError ,OutOfMemoryError 兩類異常。
其區別只是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的本地(Native)方法服務。

java 堆

所有線程共享,幾乎所有的實例對象和數組都在此上分配內存。
可以劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer,TLAB),以提升對象分配時的效率
會有OutOfMemoryError異常

方法區

和堆一樣都是線程共享的,用於存儲虛擬機加載的類型信息、常量、靜態變量、即使編譯器編譯後的代碼
jdk7 永久帶中字符串常量池、 靜態變量被移到堆 ,配置參數 -XX:MaxPermSize -XX:PermSize
jdk8 完全廢棄了永久代概念,變成元空間,把JDK 7中永久代還剩餘的內容(主要是類型信息)全部移到元空間中,配置參數 -XX:MetaspaceSize -XX:MaxMetaspaceSize。

注意:jdk8一定要配置元空間參數,其中XX:MetaspaceSize 不管配置多大出始默認都是20.8m,這點和-XX:PermSize 的配置多大是多大有點不一樣。所以元空間區間範圍 [20.8m, MaxMetaspaceSize)

運行時常量池

方法區的一部分,class文件中除了有類的版本、字段、方法、接口描述信息外,還有一個常量表用於存儲字面量和符號引用、這部分在類 加載後放到方法區的運行時常量池。
常量池是方法區的一部分,所以受到方法區內存的限制,當常量池無法再申請到內存時會拋出OutOfMemoryError異常。

直接內存

不算虛擬機運行時數據區的一部分

jdk1.4加入了NIO(New Input/Output)類,引入基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可以使用Native函數庫直接分配堆外內存,然後通過一個存儲在Java堆裏面的DirectByteBuffer對象作爲這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因爲避免了在Java堆和Native堆中來回複製數據

既然是內存,則肯定還是會受到本機總內存(包括物理內存、SWAP分區或者分頁文件)大小以及處理器尋址空間的限制,也可能導致OutOfMemoryError異常

0x02 對象創建

都知道一般創建對象直接 new就可以了,那具體內部到底發生了什麼!

1、遇到new 關鍵字後會先去常量池尋找是否有此類的符號引用,此引用的類是否被加載、解析初始化過,如果沒有,那必須先執行相應的類加載過程。

2、內存分配,檢查完畢後,爲新對象分配內存,對象所需要的內存在類加載後就完全確定了,根據內存是否規整有兩種分配方式,而內存是否規整又根據垃圾收集器決定的。

空閒列表:假如內存不規整,虛擬機內部必須維護一個列表,記錄哪塊內存可用,分配時從列表中找出一塊足夠大的空間劃分給對象實例。

指針碰撞:假如內存規整,分配內存就僅僅把指針向空閒空間方向挪動一段與對象大小相等的距離。

所以當使用Serial、ParNew 帶壓縮的收集器時,系統採用指針碰撞分配內存,使用CMS 時會使用空閒列表。

對象創建是非常頻繁的,所以僅修改指針分配在併發情況下是不安全的。爲了解決併發分配的問題有兩種可選方案:

本地線程緩衝區 TLAB(Thread Local Allocation Buffer) : 每個線程創建時預分配一塊內存,稱爲本地線程分配緩衝,線程內創建對象時先使用TLAB

分配動作同步處理:虛擬機採用CAS配上失敗重試的方式保證更新原子性

3、初始化,內存分配完畢後就需要將分配的內存空間(不包括對象頭)進行初始化,如果使用了TLAB ,初始化工作將提前到TLAB分配時順便進行。此操作保證了實例字段會附有初始值,對應着類加載的初始化過程。()方法也在此執行,這方法由編譯器自動收集類中所有類變量的賦值動作和靜態語句塊static{}中的語句塊合併產生。

4、設置對象頭, 需要設置下這個對象是哪個類的實例、如何找到類的元數據信息、對象哈希碼、對象GC分代年齡等信息。

5、 到目前爲止,從虛擬機來看一個對象已經產生了。從java角度看,對象創建纔剛開始,當前還沒有執行類的構造函數,即CLass 文件中的()方法還沒執行,所有的字段都是默認值0/null。其實從開發角度看來在new 指令後就會執行方法,對對象進行初始化操作。

0x03 對象內存佈局

對象已經創建了,但對象內部到底是什麼情況,到底有哪些屬性?

其實對象在內存中也沒那麼複雜,分爲三個部分
對象頭
對象頭就和你人的名片一樣,表明了你這個對象在整個jvm世界中的一種狀態屬性信息。
對象頭包含了兩種類型的信息:

1、存儲對象自身的運行時數據
哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳,這部分數據在虛擬機32位 | 64位 分別佔用32bit | 64bit ,稱爲Mark Word。都說Mark Word被設計成動態的數據結構,其實指的就是上面不同屬性所佔用的存儲空間是變動的。

2、對象指向它的類型元數據的指針
該指針用於確定該對象屬於哪個類,如果對象是數組,對象頭中還必須有一塊用於記錄數組長度。

實例數據: 對象真正存儲的有效信息,就是程序代碼裏面定義的各種類型字段內容。
對齊填充 :純佔位使用,因爲虛擬機自動內存管理系統要求對象起止地址必須是8字節整數倍。

0x04 對象的訪問定位

創建完了的對象後程序可以通過棧上的reference來引用和操作對象,那到底怎麼定位到內存中的對象呢?
其實對象的訪問方式是由虛擬機實現而定。主流有如下兩種方式:
句柄:
使用句柄其實就是堆中搞了一個句柄池,reference中存儲的其實就是句柄地址,而句柄中包含了對象實例數據和類型數據的具體地址信息。
好處就是reference引用的句柄穩定,在對象移動時(GC的時候)只會改變句柄實例數據指針而已

直接指針:
reference中存儲的直接就是對象地址,如果只訪問對象,就不需要間接開銷

0x05 OutOfMemoryError

上面說了在JVM中除了程序計數器沒有OutOfMemoryError,其他基本都有可能發生,下面總結下各個區域
堆溢出

 錯誤:
java.lang.OutOfMemoeyError: Java heap space
Dumping heap to java_pid1120.hprof ...
Heap dump file created [22045981 bytes in 0.66 secs]

我們可以配置jvm 啓動參數 -XX:+HeapDumpOnOutOfMemoryError 在堆溢出時Dump當前內存快照,後續可以使用Mat進行數據分析

虛擬機棧和本地方法棧溢出

錯誤:
java.lang.StackOverflowError
java.lang.OutOfMemoeyError

一般我們使用-Xss來設置棧容量,棧有兩種異常表現
1、線程執行時請求棧深度超過了你設置的容量,就會拋出 StackOverflowError,比如你搞了個死循環
2、創建線程申請內存時,如果無法活的足夠內存就會出現OutOfMemoeyError(所以程序中如果線程開得太多而不回收也容易造成OOM )

方法區和運行時常量池溢出
經典案例,while(true) String::intern() 本地方法,它的意思就是字符串常量池中有就返回引用,沒有就把這個添加進去返回此對象引用。

錯誤:
Exception in thread "main"   java.lang.OutOfMemoeyError: PermGen space
     at java.lang.String.intern(Native Method)

經測試:
JDK1.6 會發生 java.lang.OutOfMemoeyError: PermGen space 從信息“PermGen space” 看出運行時常量池屬於方法區
JDK1.7 JDK1.8 不會發生異常,是因爲從JDK1.7開始 字符串常量池被移到了堆中
JDK1.7 要注意使用動態代理的情況下新類型的創建,否則容易溢出 ,而一個類如果要被垃圾回收,條件是比較苛刻的。
JDK1.8 永久代被元空間代替了,默認設置下,再也不需要擔心方法區溢出了,-XX:MaxMetaspaceSize:設置元空間最大值,默認是-1,即不限制,或者說只受限於本地內存大小。
但在實際使用中還需要進行設置,防止資源耗盡

直接內存溢出

    錯誤:
    Exception in thread "main"  java.lang.OutOfMemoeyError:
            at sum.misc.Unsafe.allocateMemory(Native Method)

上面提過NIO 可以使用直接內存,容量大小可通過-XX:MaxDirectMemorySize參數來指定,如果不指定默認與java 堆最大值(-Xmx)一樣。
DirectByteBuffer類直接通過反射獲取Unsafe實例進行內存分配。

查看更多

角兒旮旯

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