前言
對JVM第二章的知識點進行總結(jié),同時方便自己以后的回顧。
Java虛擬機的多線程
通過線程輪流切換并分配處理器執(zhí)行時間的方式實現(xiàn)
程序計數(shù)器
如果執(zhí)行的是Java方法,計器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址,字節(jié)碼解釋器通過改變計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基本功能都需要依賴這個計數(shù)器來完成。
線程私有
各個線程之間計數(shù)器互不影響,獨立存儲。
OutOfMemoryError:無,唯一
Java虛擬機棧
生命周期和線程相同,描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。
線程私有
局部變量表
存放編譯期復(fù)制的各種基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long<2>、double<2>)、對象引用(可能是一個指向?qū)ο笃鹗嫉刂返囊弥羔?,也可能是指向一個代表對象的句柄或其他與此對象相關(guān)的位置)、returnAdress類型(指向一條字節(jié)碼指令的地址)
StackOverflowError
當(dāng)線程請求的棧深度大于虛擬機所允許的深度。
OutOfMemoryError
如果虛擬機可以動態(tài)擴展。當(dāng)擴展時無法申請到足夠的內(nèi)存。
本地方法棧
與虛擬機棧的作用非常相似,不同在于虛擬機棧為Java服務(wù),本地方法棧為Native方法服務(wù)。
Java堆
在虛擬機啟動時創(chuàng)建,所有對象實例以及數(shù)組都要在堆上分配(但隨著JIT編譯器和逃逸分析技術(shù)的成熟,所有對象都要在分配在堆上變得不那么絕對),是垃圾收集器管理的主要區(qū)域
線程共享
OutOfMemoryError
當(dāng)堆中沒有內(nèi)存完成實例分配,并且堆也無法再擴展時。
方法區(qū)
用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。
注意點:HotSpt虛擬機把GC分代收集擴展至方法區(qū),即是用永久代來實現(xiàn)方法區(qū),對于其他的虛擬機來說不存在永久代的概念。
特點:除了和Java堆一樣不需要連續(xù)的內(nèi)存空間和可以選擇固定大小或可擴展外,還可以選擇不實現(xiàn)垃圾收集。相對而言,垃圾收集行為在這個區(qū)域比較少出現(xiàn),這區(qū)域的內(nèi)存回收目標主要是針對常量池的回收和對類型的卸載,但是垃圾收集的效果不好,尤其是類型卸載。
OutOfMemoryError
當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時。
線程共享
運行時常量池
是方法區(qū)的一部分,用于存放Class文件中的常量池在編譯期生成的各種字面量和符號引用。
特點:運行時常量池具有動態(tài)性,并不是預(yù)置入Class文件中常量池的內(nèi)容才能進入方法區(qū)的運行時常量池,運行期間也可能將新的常量放入池中,比如String類的intern()方法。
注意點:JDK1.7之前,String的intern()方法會把首次遇到的字符串實例復(fù)制到永久代中(方法區(qū)稱為永久代,方法區(qū)的運行時常量池),返回的也是永久代中這個字符串實例的引用,如果不是該字符串不是首次出現(xiàn),則直接返回已存在字符串實例的引用。JDK1.7之后,String的intern()方法不會在復(fù)制首次遇到的實例,而是在常量池中記錄首次出現(xiàn)的實例引用,如果不是該字符串不是首次出現(xiàn),則直接返回已存在字符串實例的引用。
OutOfMemoryError
當(dāng)常量池?zé)o法再申請到內(nèi)存時。
直接內(nèi)存
使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過存儲在Java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進行操作。
注意點:直接內(nèi)存更適合在內(nèi)存申請次數(shù)較少,但讀寫操作較頻繁的場景。
OutOfMemoryError
對象的創(chuàng)建
(1) 首先檢查new指令的參數(shù)能否在常量池中定位到一個類的符號引用。
(2) 檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執(zhí)行相應(yīng)的類加載過程。
(3) 虛擬機為新生的對象分配內(nèi)存,對象所需的內(nèi)存大小在類加載完成后便可完全確定。
(4) 內(nèi)存分配完成后,虛擬機會把分配到的內(nèi)存空間都初始化為零值(不包括對象頭),保證了對象實例字段在Java代碼中可以不賦初值就可以直接使用。
(5) 虛擬機要對對象進行必要的設(shè)置,例如這個對象是哪個類的實例、如何才能找到類的元數(shù)據(jù)信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭中。
(6) 執(zhí)行<init>方法,把對象按照程序員的意愿進行初始化。
堆內(nèi)存分配規(guī)則
指針碰撞--假設(shè)java堆中的內(nèi)存是絕對規(guī)整的,所有用過的內(nèi)存放在一邊,空閑的內(nèi)存放在另一邊,使用指針作為分界點的指示器。那么在分配內(nèi)存時,只需要指針向空閑空間的方向移動對象大小相等的距離即可。
空閑列表--如果java堆的內(nèi)存并不是規(guī)整的,虛擬機需要維護一個列表,記錄上哪些內(nèi)存塊是可用的,在分配內(nèi)存時從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的記錄。
并發(fā)
(1) 對分配內(nèi)存空間的動作進行同步處理——實際上虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性。
(2) 把內(nèi)存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)。那個線程要分配內(nèi)存,就在哪個線程的TLAB上分配,只有TLAB用完并分配新的TLAB是,才需要同步鎖定。
對象的內(nèi)存布局
對象頭
第一部分:用于存儲對象自身的運行時數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標記、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分數(shù)據(jù)的長度在32位和64位的虛擬機上中分別為32bit和64bit,官方稱它為“Mark Word”。Mark Word是非固定的數(shù)據(jù)結(jié)構(gòu),它會根據(jù)自己的狀態(tài)復(fù)用自己的存儲空間。

第二部分:類型指針,即對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。另外,如果對象是一個Java數(shù)組,那在對象頭中還必須包括一塊用于記錄數(shù)組長度的數(shù)據(jù),因為虛擬機可以通過普通Java對象的元數(shù)據(jù)信息確定Java對象的大小,但是無法通過數(shù)組的元數(shù)據(jù)確定數(shù)組的大小。
注意:并不是所有的虛擬機實現(xiàn)都必須在對象數(shù)據(jù)上保留類型指針,換句話說,查找對象的元數(shù)據(jù)信息并不一定要經(jīng)過對象本身。<2.3.3>
實例數(shù)據(jù)
存儲程序代碼中所定義的各種類型的字段內(nèi)容。無論是從父類繼承下來的,還是在子類中定義的,都會被記錄起來。
對齊填充
對齊填充并不是必然存在的,也沒有特變的含義,僅僅起到占位符的作用。由于HotSpot VM的自動內(nèi)存管理系統(tǒng)要求對象的起始地址必須是8個字節(jié)的倍數(shù),所以對象所占的內(nèi)存如果不是8個字節(jié)的倍數(shù),就需要通過對齊填充來補全。
對象的訪問定位
使用句柄
在Java堆中劃分出一塊內(nèi)存作為句柄池,reference中的存儲的就是對象的句柄地址,而句柄中包含了對象實例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信息。

優(yōu)勢:reference中存儲的是穩(wěn)定的句柄地址,在對象被移動時只會改變句柄池的實例數(shù)據(jù)指針。
直接指針訪問
reference中存儲對象的地址

優(yōu)勢:速度更快,節(jié)省了一次指針定位的時間開銷。·