程序員源碼網(wǎng)站個(gè)人怎么創(chuàng)建網(wǎng)站
前言
本文是JVM系列的內(nèi)存模型篇,參考資料為《深入理解Java虛擬機(jī)》,本文章將會(huì)以HotSpot 虛擬機(jī)為介紹基礎(chǔ)。
1.JVM簡(jiǎn)單介紹
Java Virtual Machine是運(yùn)行Java程序的基礎(chǔ),JVM基于C、C++實(shí)現(xiàn),JVM有很多種類,但是這些虛擬機(jī)都必須按照《Java虛擬機(jī)規(guī)范》來(lái)進(jìn)行實(shí)現(xiàn)。目前JDK使用的是HotSpot虛擬機(jī)。
2.JVM內(nèi)存模型
根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,Java虛擬機(jī)所管理的內(nèi)存將會(huì)包括以下幾個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)域
- 程序計(jì)數(shù)器
- Java虛擬機(jī)棧
- 本地方法棧
- 方法區(qū)
- 堆
分布如下圖:
3.程序計(jì)數(shù)器
程序計(jì)數(shù)器是Java中占用內(nèi)存比較少的一個(gè)區(qū)域,他的作用是記錄當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指令,通俗的理解就是代碼執(zhí)行到哪里了。
我們很容易思考到,在多線程中,是發(fā)生線程切換這種情況的,那么一個(gè)線程被切換后,它的狀態(tài)就需要被記錄到上下文中,方便線程能正確執(zhí)行到原來(lái)的位置,那么為了記錄這個(gè)位置,就需要程序計(jì)數(shù)器來(lái)進(jìn)行實(shí)現(xiàn)
為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每個(gè)線程都需要一個(gè)獨(dú)立程序計(jì)數(shù)器,各個(gè)線程之間計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ)。因此它也是“線程私有”的內(nèi)存
這片區(qū)域也是唯一一個(gè)在《Java虛擬機(jī)規(guī)范》中沒(méi)有任何OutOfMemoryError情況的區(qū)域。
4.Java虛擬機(jī)棧
Java虛擬機(jī)棧,以“?!泵?#xff0c;在內(nèi)存模型中,基本都是用來(lái)處理方法的,所以Java虛擬機(jī)棧是用來(lái)處理Java語(yǔ)言實(shí)現(xiàn)的方法的。同理的,這個(gè)棧也是線程私有的。他的生命周期與線程生命周期一樣長(zhǎng)。
一個(gè)線程在調(diào)用方法的時(shí)候,會(huì)在虛擬機(jī)棧中,創(chuàng)建一個(gè)棧幀,這個(gè)棧幀會(huì)存放局部變量表、操作數(shù)棧、動(dòng)態(tài)連接、方法出口等信息。
棧幀包含以下內(nèi)容:
-
局部變量表: 棧幀用于存儲(chǔ)方法的局部變量,表中存放了編譯期可知的基本數(shù)據(jù)類型和對(duì)象引用(對(duì)象引用指針或者句柄)。這些局部變量在方法調(diào)用時(shí)分配內(nèi)存空間,并在方法調(diào)用結(jié)束后被釋放。
-
操作數(shù)棧: 棧幀還包含一個(gè)操作數(shù)棧,用于存儲(chǔ)方法執(zhí)行時(shí)的操作數(shù)。當(dāng)方法需要進(jìn)行計(jì)算或操作時(shí),操作數(shù)會(huì)被入?;虺鰲?。
-
動(dòng)態(tài)鏈接: 棧幀包含指向運(yùn)行時(shí)常量池中當(dāng)前方法引用的指針,用于在方法中訪問(wèn)其他類或方法。
-
方法出口: 當(dāng)方法調(diào)用完成后,程序需要返回到方法調(diào)用的地方繼續(xù)執(zhí)行。棧幀包含方法返回地址,用于記錄返回的位置。
額外提一嘴的是當(dāng)Java虛擬機(jī)棧的深度被方法調(diào)用填滿的時(shí)候,就會(huì)出現(xiàn)StackOverFlowError;如果棧的大小動(dòng)態(tài)擴(kuò)展到?jīng)]辦法擴(kuò)展的時(shí)候,會(huì)報(bào)OOM(OutOfMemoryError)的錯(cuò)誤。
5.本地方法棧
這個(gè)棧和Java虛擬機(jī)棧是一樣的功能,但是作用的對(duì)象不一樣,Java虛擬機(jī)棧對(duì)應(yīng)的是Java方法,而本地方法棧對(duì)應(yīng)的是被Native標(biāo)志的方法,這類方法一般都是C、C++代碼。其他東西基本和Java虛擬機(jī)棧一致。
6.方法區(qū)
方法區(qū)與Java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域,這塊區(qū)域是用來(lái)存儲(chǔ)已經(jīng)被加載的類元信息,這些信息包含:類型信息、常量、靜態(tài)變量、即使編譯后的代碼緩存等信息。
6.1永久代與元空間
早在JDK1.8以前,方法區(qū)使用的永久代的實(shí)現(xiàn)方式,而在1.8后才正式確定使用元空間。那么二者實(shí)現(xiàn)上有什么區(qū)別呢????
最大的區(qū)別就是前者是使用的虛擬機(jī)內(nèi)存,后者使用了直接內(nèi)存,也就是說(shuō)永久代的內(nèi)存大小受JVM限制,而元空間內(nèi)存大小受真實(shí)機(jī)子內(nèi)存大小限制,明顯后者內(nèi)存大小更大,前者更容易OOM。
在方法區(qū)使用元空間后,字符串常量池也從方法區(qū)移動(dòng)到了堆內(nèi)存中。
6.2運(yùn)行時(shí)常量池
提到方法區(qū),就不得不提到一個(gè)叫運(yùn)行時(shí)常量池的東西,它也是方法區(qū)的一部分。
一個(gè)Class文件除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池表,用于存放編譯期生成的各種字面量與符號(hào)引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中。
7.堆
堆內(nèi)存是整個(gè)虛擬機(jī)中最大的一塊,這塊區(qū)域是被線程共享的,這塊對(duì)象就是用來(lái)在程序執(zhí)行時(shí),大部分對(duì)象存放的地方(還有極小一部分可能會(huì)發(fā)生逃逸分析,在棧上創(chuàng)建和銷毀)。
堆這塊區(qū)域,也是最容易發(fā)生OOM的地方,原因可想而知,公共的地方,大家都來(lái)這里放東西,時(shí)間一長(zhǎng),沒(méi)有空間也很正常,所以這塊區(qū)域也是發(fā)生GC(Garbage Collected)頻率最高的一個(gè)場(chǎng)所。(具體GC流程,下篇文章會(huì)詳細(xì)介紹)
7.1 對(duì)象創(chuàng)建
堆中的對(duì)象(普通對(duì)象)創(chuàng)建過(guò)程也是比較講究的,下面我們帶著問(wèn)題,一步一步理解這個(gè)過(guò)程
首先,如何創(chuàng)建?
很簡(jiǎn)單的,new關(guān)鍵字
那么問(wèn)題又來(lái)了,對(duì)象創(chuàng)建依賴的信息從哪里來(lái)?
當(dāng)Java遇到一條字節(jié)碼new指令的時(shí)候,首先將去檢測(cè)這個(gè)指令能否在常量池中定位到一個(gè)類的符號(hào)引用,并檢查這個(gè)符號(hào)引用代表的類是否已被加載、解析和初始化過(guò)。如果沒(méi)有,那么必須先執(zhí)行相應(yīng)的類加載(雙親委派模型)。
對(duì)象依賴信息得到后,內(nèi)存大小該如何劃分分配?
當(dāng)一個(gè)類被加載之后,相應(yīng)的對(duì)象創(chuàng)建所需的內(nèi)存大小也就能被確定了。那么要在堆中創(chuàng)建對(duì)象,就需要?jiǎng)澐挚臻g,JVM中有兩種劃分空間的方式,分別是“指針碰撞”和“空閑列表”
- 指針碰撞
假設(shè)Java堆中內(nèi)存分配絕對(duì)規(guī)整,使用過(guò)的和未使用的分成兩邊,只需要在邊界設(shè)置指針,這個(gè)指針只需要挪動(dòng)和對(duì)象大小一樣的距離即可,這種就是指針碰撞- 空閑列表
假如Java堆內(nèi)存并不規(guī)整,使用過(guò)的和未使用的都混在一起,這種情況,要分配內(nèi)存就只能維護(hù)一個(gè)列表,這個(gè)列表記錄了哪些內(nèi)存可以使用,分配內(nèi)存就需要在表中查找到足夠到的區(qū)間進(jìn)行分配即可,這就是空閑列表
并發(fā)下,對(duì)象創(chuàng)建的內(nèi)存分配安全如何得到保證?
為我們所知的,堆內(nèi)存是一個(gè)線程共享的,這就意味著,我們堆在劃分內(nèi)存大小的時(shí)候,可能會(huì)出現(xiàn)線程安全問(wèn)題??赡艹霈F(xiàn)線程1在給A分配大小的時(shí)候,還沒(méi)來(lái)得及修改指針,但是線程2在創(chuàng)建B時(shí),使用了這個(gè)指針,就導(dǎo)致了內(nèi)存數(shù)據(jù)被改寫了。解決這個(gè)問(wèn)題有兩個(gè)方式
- 加鎖同步
實(shí)際實(shí)現(xiàn)中虛擬機(jī)是采用CAS+失敗重試的方式保證更新操作的原子性- TLAB(Thread Local Allocation Buffer,本地緩沖區(qū)),也和ThreadLocal一樣,給每個(gè)線程各自劃分好區(qū)域,線程要?jiǎng)?chuàng)建對(duì)象,就在這個(gè)區(qū)域內(nèi)創(chuàng)建就行,如果TLAB使用完了才需要進(jìn)行同步鎖定分配對(duì)象。如果JVM要使用TLAB,可以通過(guò)-XX:+/-UseTLAB參數(shù)來(lái)設(shè)定
實(shí)際上,內(nèi)存分配成功之后,虛擬機(jī)還會(huì)對(duì)分配到的內(nèi)存空間(不包括對(duì)象頭)進(jìn)行初始化工作,零值處理。這步操作是為了保證對(duì)象實(shí)例字段在Java代碼中可以不賦值就能直接使用。
經(jīng)歷以上步驟,對(duì)象創(chuàng)建后,對(duì)象還需要設(shè)置什么?
需要設(shè)置“對(duì)象屬于哪個(gè)類的實(shí)例”、“類的元數(shù)據(jù)信息“”、“對(duì)象hash碼(實(shí)際調(diào)用Object::hashCode才會(huì)生成)”、“GC分代年齡”,這些信息都被描述在對(duì)象頭中
最終
在上面工作都完成后,看似一個(gè)對(duì)象已經(jīng)被創(chuàng)建了,但實(shí)際上,整個(gè)生命過(guò)程還差一步,即初始化,構(gòu)造函數(shù)中的初始化工作還沒(méi)有被真正執(zhí)行,也就是 < init > ()方法,所以值都是默認(rèn)為零值的,所以當(dāng)構(gòu)造函數(shù)執(zhí)行完成后,一個(gè)對(duì)象就被完成創(chuàng)建了。
7.2 對(duì)象的內(nèi)存布局
在了解一個(gè)對(duì)象的創(chuàng)建過(guò)程后,我們來(lái)看看,一個(gè)對(duì)象內(nèi)部布局是如何的,直接看下圖:
對(duì)象頭:這部分包含了兩部分信息
- 第一部分:HashCode、GC分代年齡、鎖狀態(tài)標(biāo)記、線程持有的鎖、偏向鎖ID、偏向鎖時(shí)間戳等信息等,這部分信息官方稱之為:Mark Word,這部分?jǐn)?shù)據(jù)在32位和64位虛擬機(jī)(未開(kāi)啟指針壓縮)中分別占用32bit和64bit。
- 第二部分:類型指針,即對(duì)象指向它的類型元數(shù)據(jù)的指針,Java虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。如果是數(shù)組對(duì)象,對(duì)象頭中還會(huì)記錄數(shù)組長(zhǎng)度,如果不是則無(wú)記錄。
實(shí)例數(shù)據(jù):這部分?jǐn)?shù)據(jù)是對(duì)象真正存儲(chǔ)的有效信息
對(duì)齊填充:這部分的內(nèi)容不是必然存在的,也沒(méi)有特殊含義,這部分的主要作用就是保證這個(gè)對(duì)象大小是8字節(jié)的整數(shù)倍,差多少,盡可能補(bǔ)多少。
JVM執(zhí)行流程
-
代碼編譯:Java源代碼通過(guò)Java編譯器(javac)編譯成字節(jié)碼文件(.class文件)。
-
類加載:JVM的類加載器將字節(jié)碼文件加載到內(nèi)存中,并進(jìn)行校驗(yàn)、準(zhǔn)備、解析等處理。
-
內(nèi)存分配:JVM為加載的類分配內(nèi)存,包括方法區(qū)、堆、棧等。
-
初始化:JVM對(duì)類進(jìn)行初始化,包括靜態(tài)變量的賦值、靜態(tài)代碼塊的執(zhí)行等。
-
執(zhí)行:JVM開(kāi)始執(zhí)行字節(jié)碼指令,逐行讀取字節(jié)碼文件并執(zhí)行。這個(gè)執(zhí)行過(guò)程交給執(zhí)行引擎將字節(jié)碼翻譯成CPU指令交給操作系統(tǒng)去執(zhí)行
-
…