一、概述
對(duì)于 C C++ 來(lái)說(shuō),在內(nèi)存管理領(lǐng)域,JVM既擁有最高的權(quán)利,但是同時(shí)他們又是從事最基礎(chǔ)工作的勞動(dòng)人員,因?yàn)樗麄儞?dān)負(fù)著每一個(gè)對(duì)象從開(kāi)始到結(jié)束的維護(hù)責(zé)任。而對(duì)于Java來(lái)說(shuō),再虛擬機(jī)自動(dòng)內(nèi)存管理的幫助下,不再需要為每一個(gè)new操作去分配內(nèi)存,不容易出現(xiàn)內(nèi)存泄漏和內(nèi)存溢出的情況,但是因?yàn)槲覀僇ava程序員 不用管理內(nèi)存,所以一旦出現(xiàn)內(nèi)存問(wèn)題,很容易讓我們手忙腳亂,所以呢我們必須要了解Java虛擬器的內(nèi)存管理機(jī)制,以便我們能更好的處理各種各樣的問(wèn)題。
二、運(yùn)行時(shí)數(shù)據(jù)區(qū)
Java虛擬機(jī)在執(zhí)行程序的過(guò)程中會(huì)把所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域,這些區(qū)域都有各自的用途,以及創(chuàng)建和銷(xiāo)毀時(shí)間。再Java 1.8中 從宏觀上來(lái)說(shuō)分為線程共享,和線程私有 主要是分為以下幾個(gè)區(qū)域
三、程序計(jì)數(shù)器
特點(diǎn):線程內(nèi)存獨(dú)享,占用內(nèi)存小,生命周期與線程相同(隨線程誕生而誕生,隨線程消亡而消亡)
功能:當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。在虛擬機(jī)的概念模型里字節(jié)碼解釋器工作時(shí)就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)(cpu在不斷輪詢執(zhí)行任務(wù))等基礎(chǔ)功能都需要依賴(lài)這個(gè)計(jì)數(shù)器來(lái)完成
異常:該區(qū)域沒(méi)有定義異常
四、Java虛擬機(jī)棧
特點(diǎn):先進(jìn)后出,線程內(nèi)存獨(dú)享,生命周期與線程相同
單位:棧幀
功能:已先進(jìn)后出執(zhí)行方法體的方法,執(zhí)行完成的棧幀出棧
4.1 運(yùn)行原理
a. JVM直接對(duì)Java棧的操作只有兩個(gè),就是對(duì)棧幀的壓棧和出棧,遵循“先進(jìn)后出” / "后進(jìn)先出”原則。
b. 在一條活動(dòng)線程中,一個(gè)時(shí)間點(diǎn)上,只會(huì)有一個(gè)活動(dòng)的棧幀。即只有當(dāng)前正在執(zhí)行的方法的棧幀(棧頂棧幀)是有效的,這個(gè)棧幀被稱(chēng)為當(dāng)前棧幀(current Frame) ,與當(dāng)前棧幀相對(duì)應(yīng)的方法就是當(dāng)前方法(currentMethod) ,定義這個(gè)方法的類(lèi)就是當(dāng)前類(lèi)(current Class)。不同線程中所包含的棧幀是不允許存在相互引用的,即不可能在一個(gè)棧幀之中引用另外一個(gè)線程的棧。
c. 執(zhí)行引擎運(yùn)行的所有字節(jié)碼指令只針對(duì)當(dāng)前棧幀進(jìn)行操作。
d. 如果在該方法中調(diào)用了其他方法,對(duì)應(yīng)的新的棧幀會(huì)被創(chuàng)建出來(lái),放在棧的頂端,成為新的當(dāng)前幀.方法返回之際,當(dāng)前棧幀會(huì)傳回此方法的執(zhí)行結(jié)果給前一個(gè)棧幀,接著,虛擬機(jī)會(huì)丟棄當(dāng)前棧幀,使得前一個(gè)棧幀重新成為當(dāng)前棧幀。
e. Java方法有兩種返回函數(shù)的方式,一種是正常的函數(shù)返回,使用return指令;另外一種是方法執(zhí)行中出現(xiàn)未捕獲處理的異常,以拋出異常的方式結(jié)束。不管使用哪種方式,都會(huì)導(dǎo)致棧幀被彈出。
結(jié)論
● 一個(gè)線程 表示的是一個(gè)Java虛擬機(jī)棧
● 一個(gè)方法的執(zhí)行,可以通過(guò)壓棧的方式,也就是 方法對(duì)應(yīng)的是棧幀
4.2 棧的基本單位棧幀
棧幀(每一個(gè)方法對(duì)應(yīng)一個(gè)棧幀)
只有虛擬機(jī)棧頂?shù)臈攀怯行У?,稱(chēng)為當(dāng)前棧幀 (Current Stack Frame),這個(gè)棧幀所關(guān)聯(lián)的方法稱(chēng)為當(dāng)前方法(Current Method) 組成:
- 局部變量表
- 操作數(shù)棧
- 動(dòng)態(tài)鏈接
- 方法出口信息
局部變量表:由基本數(shù)據(jù)類(lèi)型和對(duì)象引用組成的
作用:用來(lái)存儲(chǔ)方法中的局部變量
基本單位:slot
- 局部變量表的大小在編譯器就可以確定其大小了,因此在程序執(zhí)行期間局部變量表的大小是不會(huì)改變的。
- 如果存儲(chǔ)的是基本數(shù)據(jù)類(lèi)型那么直接存儲(chǔ)值
- 如果存儲(chǔ)的是對(duì)象引用那么存儲(chǔ)對(duì)象的引用地址( reference)(堆中)
reference的兩種實(shí)現(xiàn)方式
直接引用
reference直接指向?qū)ο?,?duì)象中指向?qū)ο箢?lèi)型數(shù)據(jù)
優(yōu)點(diǎn):速度快,節(jié)約指針開(kāi)銷(xiāo)。HotSpot采用的主要方式
使用句柄池
java堆中會(huì)維護(hù)一個(gè)句柄池,句柄池分別指向?qū)ο髮?shí)例(堆)的和對(duì)象類(lèi)型數(shù)據(jù)(方法區(qū))
優(yōu)點(diǎn):對(duì)象移動(dòng)后只需改變句柄池的指向地址,而不需要改變引用的指向地址。穩(wěn)定
其實(shí)用白話來(lái)說(shuō) 就是2個(gè)人是直接自己?jiǎn)尉€聯(lián)系,還是通過(guò)一個(gè)第三方聯(lián)系,自己并不知道自己要聯(lián)系的是誰(shuí)。
4.3 操作數(shù)棧
操作數(shù)棧的深度在編譯器就可以確定其大小了,因此在程序執(zhí)行期間局部變量表的大小是不會(huì)改變的。
功能:實(shí)現(xiàn)程序功能
4.4 動(dòng)態(tài)連接
補(bǔ)充下直接引用與符號(hào)引用
● 直接引用:當(dāng)類(lèi)已經(jīng)加載到虛擬機(jī)時(shí),通過(guò)地址直接調(diào)用該類(lèi)
● 符號(hào)引用(常量池中):在編譯的時(shí)候還不知道類(lèi)是否被加載,先用符號(hào)代替該類(lèi),等實(shí)際運(yùn)行時(shí)再用直接引用替換間接引用。
靜態(tài)解析:符號(hào)引用一部分會(huì)在類(lèi)加載階段或第一次使用的時(shí)候轉(zhuǎn)化為直接引用
動(dòng)態(tài)連接: 將在每一次的運(yùn)行期期間轉(zhuǎn)化為直接引用
4.5 方法出口信息
當(dāng)一個(gè)方法執(zhí)行完畢之后,要返回之前調(diào)用它的地方,因此在棧幀中必須保存一個(gè)方法返回地址。
本地方法棧
● 大體上都類(lèi)似于虛擬機(jī)棧
● 不同點(diǎn):棧執(zhí)行的java方法服務(wù)
● 本地方法棧執(zhí)行的是Native方法(不一定是用java開(kāi)發(fā)的)服務(wù)
五、Java堆
特點(diǎn):存儲(chǔ)對(duì)象,線程間內(nèi)存共享,占用大量?jī)?nèi)存,垃圾回收關(guān)注的重點(diǎn)區(qū)域
異常:OutOfMemoryError
每次都向堆中存放對(duì)象,方法結(jié)束后,銷(xiāo)毀棧幀的局部變量表時(shí)同時(shí)銷(xiāo)毀引用,該對(duì)象就成了可回收的垃圾。咋看起來(lái)沒(méi)什么不對(duì)呀,可是仔細(xì)思考下還是存在兩個(gè)問(wèn)題 1.不斷的來(lái)回增加刪除對(duì)象,對(duì)于GC的工作量太大。 2.java使指針碰撞(堆中存入新對(duì)象的時(shí)候,指針根據(jù)對(duì)象大小移動(dòng)到相應(yīng)位置)來(lái)為對(duì)象分配內(nèi)存。如果在多線程的環(huán)境下,就會(huì)出現(xiàn)兩個(gè)對(duì)象同時(shí)移動(dòng)當(dāng)前前指針的情況,造成線程不安全的情況。
這里就要引入TLAB的概念了
TLAB的全稱(chēng)是Thread Local Allocation Buffer,這是一個(gè)線程專(zhuān)用的內(nèi)存分配區(qū)域。每個(gè)線程都會(huì)從Eden分配一塊空間,當(dāng)線程銷(xiāo)毀時(shí),我們自然可以回收掉TLAB的內(nèi)存。
使用TLAB指令 -XX:UseTLAB
優(yōu)點(diǎn):線程安全,減少垃圾回收的壓力。
缺點(diǎn):TLAB空間大小是固定的,面對(duì)大對(duì)象的時(shí)候不夠靈活
六、方法區(qū)
特點(diǎn):存儲(chǔ)類(lèi),線程間內(nèi)存共享
存放已被虛擬機(jī)加載的類(lèi)信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)
異常:OutOfMemoryError
提到方法區(qū)不得不說(shuō)的就是運(yùn)行時(shí)常量池
補(bǔ)充:方法區(qū)不是永久代,只是Hotspot的實(shí)現(xiàn)方式而已。
遠(yuǎn)行時(shí)常量池
運(yùn)行時(shí)常量池是方法區(qū)的一部分,Class文件除了有類(lèi)的版本,字段,方法,還有常量池
Java虛擬機(jī)對(duì)class文件每一部分的格式都有嚴(yán)格規(guī)定,每一個(gè)字節(jié)用于存儲(chǔ)哪種數(shù)據(jù)都必須符合規(guī)范才會(huì)被jvm認(rèn)可。但對(duì)于運(yùn)行時(shí)常量池,Java虛擬機(jī)規(guī)范沒(méi)做任何細(xì)節(jié)要求。
運(yùn)行時(shí)常量池有個(gè)重要特性是動(dòng)態(tài)性,Java語(yǔ)言不要求常量一定只在編譯期才能產(chǎn)生,也就是并非預(yù)置入class文件中常量池的內(nèi)容才能進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池,運(yùn)行期間也有可能將新的常量放入池中,這種特性使用最多的是String類(lèi)的intern()方法。
既然運(yùn)行時(shí)常量池是方法區(qū)的一部分,自然受到方法區(qū)內(nèi)存的限制。當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí)會(huì)拋出outOfMemeryError異常。
七、對(duì)象的創(chuàng)建
當(dāng)虛擬機(jī)遇到一條New指令時(shí):會(huì)進(jìn)行如下步驟
● 檢查指令的參數(shù)(即工作中我們New的對(duì)象),能否在常量池中找到它的符號(hào)引用。
● 如果存在,檢查符號(hào)引用代表的類(lèi)是否被加載、解析、初始化過(guò)。(如果沒(méi)有則執(zhí)行類(lèi)的加載-----相關(guān)加載過(guò)程參考我前面的文章類(lèi)加載機(jī)制)。
● 加載通過(guò)后,虛擬機(jī)將為新生對(duì)象分配內(nèi)存。(所需內(nèi)存大小在類(lèi)加載完成后便可確定)
7.1 兩種內(nèi)存分配的方式
● 指針碰撞:假設(shè)Java堆中的內(nèi)存是絕對(duì)規(guī)整的,所有用過(guò)的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊。中間放著一個(gè)指針作為分界點(diǎn)的指示器,分配內(nèi)存就僅僅是把指針往空閑空間那邊挪動(dòng)一段與對(duì)象大小相等的距離。這種方式則屬于指針碰撞。
● 空閑列表:如果堆中的內(nèi)存并不是規(guī)整的,已使用的內(nèi)存和空閑內(nèi)存相互交錯(cuò),顯然無(wú)法使用指針碰撞。虛擬機(jī)就必須維護(hù)一個(gè)列表,記錄哪些內(nèi)存是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新記錄表上的數(shù)據(jù)。這種方式屬于空閑列表。
具體選擇哪種分配方式由Java堆決定,而Java堆是否規(guī)整,則有GC收集器決定。因此使用Serial、ParNew等帶Compact過(guò)程的收集器時(shí),系統(tǒng)采用的分配算法是指針碰撞。而使用CMS這種基于Mark-Sweep算法的收集器時(shí),通常采用的空閑列表。
7.2 如何保證分配內(nèi)存時(shí)線程的安全性
● 對(duì)分配內(nèi)存的動(dòng)作進(jìn)行同步處理(實(shí)際上虛擬機(jī)采用CAS配上失敗重試的機(jī)制保證了更新操作的原子性)
● 把分配內(nèi)存的動(dòng)作按照線程劃分在不同的空間之中進(jìn)行(即每個(gè)線程在Java堆中預(yù)先分配一小塊內(nèi)存(稱(chēng)為本地線程分配緩沖))。
7.3 對(duì)象的內(nèi)存布局
在HotSpot虛擬機(jī)中,對(duì)象在內(nèi)存中的布局可以分為3塊區(qū)域:對(duì)象頭,實(shí)例數(shù)據(jù)和對(duì)齊填充
對(duì)象頭包括兩部分信息:
● 存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù)(如:哈希碼、GC分代年齡、鎖 等)
● 類(lèi)型指針(即對(duì)象指向他的類(lèi)元數(shù)據(jù)的指針,虛擬機(jī)根據(jù)此指針來(lái)確認(rèn)對(duì)象屬于哪個(gè)類(lèi)的實(shí)例)
● 如果是數(shù)據(jù) 記錄數(shù)組的大小 實(shí)例數(shù)據(jù):
● 實(shí)例數(shù)據(jù)才是對(duì)象真正存貯的有效信息(即程序中所定義的各種類(lèi)型的字段內(nèi)容)。
對(duì)齊填充:
● 不是必然存在的,僅僅起到占位符的作用,因?yàn)镠otSpot虛擬機(jī)要求對(duì)象的起始地址必須是8個(gè)字節(jié)的整數(shù)倍。
來(lái)個(gè)例子把JVM的運(yùn)行時(shí)的區(qū)域全部串起來(lái)文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-724227.html
public class Kafka {
public static void main() {
ReplicaManager replicaManager = new ReplicaManager() ;
replicaManager.loadReplicasFromDisk();
}
}
public class ReplicaManager{
private long replicacount;
public void loadReplicasFromDisk(){
Boolean hasFinishedLoad = false;
if (isLocalDatacorrupt()) {}
}
public Boolean isLocalDatacorrupt(){
Boolean isCorrupt = false;
return iscorrupt;
}
}
其實(shí)我們把上面的那個(gè)圖和下面的這個(gè)總的大圖一起串起來(lái)看看,還有配合整體的代碼,我們來(lái)捋一下整體的流程,就會(huì)覺(jué)得很清晰。
首先,你的JVM進(jìn)程會(huì)啟動(dòng),就會(huì)先加載你的Kafka類(lèi)到內(nèi)存里。然后有一個(gè)main線程,開(kāi)始執(zhí)行你的Kafka中的main0方法。
main線程是關(guān)聯(lián)了一個(gè)程序計(jì)數(shù)器的,那么他執(zhí)行到哪一行指令,就會(huì)記錄在這里,如上圖
其次,就是main線程在執(zhí)行main方法的時(shí)候,會(huì)在main線程關(guān)聯(lián)的Java虛擬機(jī)棧里,壓入一個(gè)main方法的棧幀。接著會(huì)發(fā)現(xiàn)需要?jiǎng)?chuàng)建一個(gè)ReplicaManager類(lèi)的實(shí)例對(duì)象,此時(shí)會(huì)加載ReplicaManager類(lèi)到內(nèi)存里來(lái)。然后會(huì)創(chuàng)建一個(gè)ReplicaManager的對(duì)象實(shí)例分配在Java堆內(nèi)存里,并且在main方法的棧幀里的局部變量表引入-"replicaManager”變量,讓他引用ReplicaManager對(duì)象在Java堆內(nèi)存中的地址。接著,main線程開(kāi)始執(zhí)行ReplicaManager對(duì)象中的方法,會(huì)依次把自己執(zhí)行到的方法對(duì)應(yīng)的棧幀壓入自己的Java虛擬機(jī)棧執(zhí)行完方法之后再把方法對(duì)應(yīng)的棧幀從Java虛擬機(jī)棧里出棧。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-724227.html
到了這里,關(guān)于JVM調(diào)優(yōu)(10)JVM的運(yùn)行時(shí)數(shù)據(jù)區(qū)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!