一篇文章帶你了解JVM內(nèi)存模型
1. JVM介紹
1.1 什么是JVM?
JVM是Java Virtual Machine(Java虛擬機(jī))的簡(jiǎn)稱,是一種用于計(jì)算設(shè)備的規(guī)范,是一個(gè)虛構(gòu)出來(lái)的計(jì)算機(jī),通過(guò)在實(shí)際的計(jì)算機(jī)上仿真模擬各種計(jì)算機(jī)功能來(lái)實(shí)現(xiàn)的。
1.2 JVM的優(yōu)點(diǎn)
1.2.1 一次編寫(xiě),到處運(yùn)行。
JVM可以讓java程序,一次編寫(xiě),導(dǎo)出運(yùn)行。讓底層代碼和運(yùn)行環(huán)境分離開(kāi),編寫(xiě)好一份代碼后,不用再次修改內(nèi)容,只用通過(guò)安裝不同的JVM環(huán)境自動(dòng)進(jìn)行轉(zhuǎn)換即可運(yùn)行,在各種系統(tǒng)中無(wú)縫連接。
1.2.2 自動(dòng)內(nèi)存管理,垃圾回收機(jī)制。
在Java誕生之時(shí),C和C++稱霸天下,但是這兩種語(yǔ)言中沒(méi)有內(nèi)存管理機(jī)制,是通過(guò)手動(dòng)操作來(lái)進(jìn)行的管理,非常麻煩和繁瑣。
此時(shí)Java應(yīng)運(yùn)而生,為了處理內(nèi)存管理這個(gè)方面,專門設(shè)計(jì)了垃圾回收機(jī)制,來(lái)自動(dòng)進(jìn)行內(nèi)存的管理。極大的優(yōu)化了操作,讓程序員們不用正在噼里啪啦在碼代的海洋中遨游時(shí),還要操心內(nèi)存會(huì)不會(huì)溢出這些“影響我方輸出”的問(wèn)題,頓時(shí)獲得了成噸的好評(píng)。
1.2.3 數(shù)組下標(biāo)越界檢查
在Java誕生之時(shí),還有個(gè)讓當(dāng)時(shí)C和C++大佬頭疼的問(wèn)題是,數(shù)組下標(biāo)越界是沒(méi)有檢查機(jī)制的,這還了得,又是一個(gè)影響“我方暴力輸出”的罪魁禍?zhǔn)?,因此JVM繼續(xù)抱著暖男的思想,又來(lái)了個(gè)愛(ài)的抱抱。
JVM又一次看見(jiàn)了大佬們的煩惱,果斷提供了數(shù)組下標(biāo)越界的自動(dòng)檢查機(jī)制,在檢測(cè)到數(shù)組下標(biāo)出現(xiàn)越界后,會(huì)在運(yùn)行時(shí)自動(dòng)拋出“java.lang.ArrayIndexOutOfBoundsException”這個(gè)異常,在當(dāng)時(shí)可是感動(dòng)了很多業(yè)界大佬(我猜的)。
1.2.4 多態(tài)
JVM還有一個(gè)多態(tài)功能,是通過(guò)相同接口,不同的實(shí)例進(jìn)行實(shí)現(xiàn),完成不同的業(yè)務(wù)操作,比如:定義了一個(gè)動(dòng)物接口(里面有一個(gè)吃的方法),我們就可以通過(guò)這個(gè)動(dòng)物創(chuàng)造小貓(吃魚(yú)),再創(chuàng)造一個(gè)狗狗(吃肉),再創(chuàng)造一個(gè)小助手(吃零食,O(∩_∩)O哈哈~)。
仔細(xì)想想,對(duì)我們有啥影響呢,那好處老多了,比如:
(1)消除類型之間的耦合關(guān)系;
(2)可替換性;
(3)可擴(kuò)充性;
(4)接口性;
(5)靈活性;
(6)簡(jiǎn)化性;
1.3 JVM、JRE、JDK之間的關(guān)系
1.3.1 JVM的簡(jiǎn)介
JVM是Java Virtual Machine的簡(jiǎn)稱,是Java虛擬機(jī),是一種模擬出來(lái)的虛擬計(jì)算機(jī),它通過(guò)在不同的計(jì)算機(jī)環(huán)境當(dāng)中模擬實(shí)現(xiàn)計(jì)算功能來(lái)實(shí)現(xiàn)的。
引入Java虛擬機(jī)后,Java語(yǔ)言在不同平臺(tái)上運(yùn)行時(shí)就不需要重新編譯。在其中,Java虛擬機(jī)屏蔽了與具體平臺(tái)的相關(guān)信息,使得Java源程序在編譯完成之后即可在不同的平臺(tái)運(yùn)行,達(dá)到“一次編譯,到處運(yùn)行”的目的,Java語(yǔ)言重要的特點(diǎn)之一跨平臺(tái),也即與平臺(tái)的無(wú)關(guān)性,其關(guān)鍵點(diǎn)就是JVM。
1.3.2 JRE的簡(jiǎn)介
JRE是Java Runtime Environment的簡(jiǎn)稱,是Java運(yùn)行環(huán)境,是讓操作系統(tǒng)運(yùn)行Java應(yīng)用程序的環(huán)境,其內(nèi)部包含JVM,也就是說(shuō)JRE只負(fù)責(zé)對(duì)已經(jīng)存在的Java源程序進(jìn)行運(yùn)行的操作,它不包含開(kāi)發(fā)工具JDK,對(duì)JDK內(nèi)部的編譯器、調(diào)試器和其它工具均不包含。
1.3.3 JDK的簡(jiǎn)介
JDK是Java Development Kit的簡(jiǎn)稱,是Java開(kāi)發(fā)工具包,是整個(gè)Java程序開(kāi)發(fā)的核心。其主要包含了JRE、Java的系統(tǒng)類庫(kù)以及對(duì)Java程序進(jìn)行編譯以及運(yùn)行的工具,例如:javac.exe和java.exe命令工具等。
1.4 JVM的常見(jiàn)實(shí)現(xiàn)
Oracle(Hotspot、Jrockit)、BEA(LiquidVM)、IBM(J9)、taobaoVM(淘寶專用,對(duì)Hotspot進(jìn)行了深度定制)、zing(垃圾回收機(jī)制非??欤竭_(dá)1毫秒左右)。
1.5 JVM的內(nèi)存結(jié)構(gòu)圖
當(dāng)Java程序編譯完成為.class文件==》類加載器(ClassLoader)==》將字節(jié)碼文件加載進(jìn)JVM中;
1.5.1方法區(qū)、堆
方法區(qū)中保存的主要是類的信息(類的屬性、成員變量、構(gòu)造函數(shù)等)、堆(創(chuàng)建的對(duì)象)。
1.5.2虛擬機(jī)棧、程序計(jì)數(shù)器、本地方法棧
堆中的對(duì)象調(diào)用方法時(shí),方法會(huì)運(yùn)行在虛擬機(jī)棧、程序計(jì)數(shù)器、本地方法棧中。
1.5.3執(zhí)行引擎
執(zhí)行方法中代碼時(shí),代碼通過(guò)執(zhí)行引擎執(zhí)行中的“解釋器”執(zhí)行;方法中經(jīng)常調(diào)用的代碼,即熱點(diǎn)代碼,通過(guò)“即時(shí)編譯器”執(zhí)行,其執(zhí)行速度非常快。
1.5.4 GC(垃圾回收機(jī)制)
GC是針對(duì)堆內(nèi)存中沒(méi)有引用的對(duì)象進(jìn)行回收,可以手動(dòng)也可以自動(dòng)。
1.5.5本地方法接口
因?yàn)镴VM不能直接調(diào)用操作系統(tǒng)的功能,只能通過(guò)本地方法接口來(lái)調(diào)用操作系統(tǒng)的功能。
2. JVM內(nèi)存結(jié)構(gòu)-程序計(jì)數(shù)器
2.1 程序計(jì)數(shù)器的定義
Program Counter Register即程序計(jì)數(shù)器(寄存器),用于記錄下一條Jvm指令的執(zhí)行地址。
2.2 操作步驟
javap主要用于操作JVM,javap -c 是對(duì)java代碼進(jìn)行反匯編操作。下圖為通過(guò)先編譯demo.java后,再執(zhí)行javap -c demo的輸出結(jié)果:
其中第一列為二進(jìn)制字節(jié)碼,即JVM指令,第二列為java源代碼。第一列中的序號(hào)為JVM指令的執(zhí)行地址。
JVM會(huì)通過(guò)程序計(jì)數(shù)器記錄下一條需要執(zhí)行的JVM指令的地址(比如第一行的0),然后交給解釋器解析為機(jī)器碼,最后交給cpu(只能識(shí)別機(jī)器碼),完成一行的執(zhí)行。
想要執(zhí)行下一行,繼續(xù)讓JVM的程序計(jì)數(shù)器記錄下一條地址(比如第二行的3),再交給解釋器解析后給cpu,以此類推執(zhí)行結(jié)束。
2.3 特點(diǎn)
2.3.1 線程私有的
2.3.2 不會(huì)存在內(nèi)存溢出
3. JVM內(nèi)存結(jié)構(gòu)-虛擬機(jī)棧
3.1 定義
虛擬機(jī)棧是每個(gè)線程運(yùn)行所需要的內(nèi)存空間,每個(gè)棧中由多個(gè)棧幀組成,每個(gè)線程中只能有一個(gè)活動(dòng)棧幀(對(duì)應(yīng)當(dāng)前正在執(zhí)行的方法),所有棧幀都遵循后進(jìn)先出,先進(jìn)后出的原則。
棧幀是每次調(diào)用方法時(shí)所占用的內(nèi)存,在棧幀中保存的內(nèi)容參數(shù)、局部變量、返回地址。
注1:垃圾回收不涉及棧內(nèi)存,因?yàn)闂?nèi)存是由方法調(diào)用產(chǎn)生的,當(dāng)方法調(diào)用結(jié)束后會(huì)彈出棧。
注2:棧內(nèi)存不是分配的越大越好,因?yàn)槲锢韮?nèi)存是一定的,棧內(nèi)存越大,可以支持更多的遞歸調(diào)用,但是可執(zhí)行的線程數(shù)會(huì)越來(lái)越少。
注3:方法的局部變量,當(dāng)其沒(méi)有逃離方法的作用范圍時(shí),是線程安全的;如果其引用了對(duì)象(比如靜態(tài)變量,即共享變量,用對(duì)象作為參數(shù)的方法,返回值為對(duì)象的方法),并且逃離出了方法的作用范圍,就需要考慮線程安全的問(wèn)題了。
3.2 棧內(nèi)存溢出
3.2.1 發(fā)生原因
(1)虛擬機(jī)棧中,棧幀過(guò)多(無(wú)限遞歸),如圖1棧幀過(guò)多;
(2)每個(gè)棧幀所占用過(guò)大,如圖2 棧幀過(guò)大。
3.2.2 棧內(nèi)存溢出小實(shí)驗(yàn)
3.2.2.1 棧幀過(guò)多的小實(shí)驗(yàn)
無(wú)限遞歸調(diào)用(棧幀過(guò)多)的小實(shí)驗(yàn),method1()方法在主方法中無(wú)限調(diào)用自己,那么會(huì)發(fā)生什么情況呢?
答案很明顯,程序崩潰了,產(chǎn)生了棧內(nèi)存溢出錯(cuò)誤,如下圖所示:
-Xss:該參數(shù)規(guī)定了每個(gè)線程虛擬機(jī)棧的大??;
接著我們通過(guò)設(shè)置一個(gè)虛擬機(jī)棧的大小是256k試試會(huì)發(fā)生什么?
我們發(fā)現(xiàn)當(dāng)我們調(diào)整了虛擬機(jī)棧的大小后執(zhí)行了4315次方法后內(nèi)存就溢出了,而調(diào)整虛擬機(jī)棧之前,我們是23268次,很明顯我們可以通過(guò)-Xss參數(shù)調(diào)整虛擬機(jī)棧的大小來(lái)控制內(nèi)存的溢出情況。
3.2.2.2 線程運(yùn)行診斷小實(shí)驗(yàn)
想象中的場(chǎng)景,大佬在瘋狂輸出,突然CPU爆表了,顯示CPU占用過(guò)多,如何去定位哪行代碼的問(wèn)題,是的是哪行(大佬都很忙的好嗎,當(dāng)然要精確了,一分鐘幾千萬(wàn)上下的,O(∩_∩)O哈哈~)?
Linux環(huán)境下:
在后臺(tái)運(yùn)行Stack_6這個(gè)java字節(jié)碼(.class)文件:
注:無(wú)論是否將nohup命令的輸出重定向到終端,輸出都將附加到當(dāng)前目錄的 nohup.out 文件中。
(1)通過(guò)top命令,查看進(jìn)程(相當(dāng)于任務(wù)管理器),發(fā)現(xiàn)了一個(gè)占用CPU達(dá)到100%的可疑家伙,這還了得,趕緊瞅瞅具體發(fā)生了什么,還有沒(méi)有王法,這讓其他小伙伴還怎么愉快的玩耍,秒速糾錯(cuò)ING。。。
注:top命令,查看哪個(gè)進(jìn)程占用CPU過(guò)高,返回進(jìn)程號(hào)。
(2) 通過(guò)ps H -eo pid,tid ,%cpu | grep 命令過(guò)濾任務(wù)管理器中的內(nèi)容。
注:ps H -eo pid,tid,%cpu |grep,是通過(guò)ps命令查看哪個(gè)線程占用CPU過(guò)高,返回進(jìn)程id,其中pid為進(jìn)程id,tid為線程id,%cpu為CPU占用情況;
發(fā)現(xiàn)了罪魁禍?zhǔn)?,這一串串心驚肉跳的紅色。。。
(3) 通過(guò)jstack 進(jìn)程id查看,20389這個(gè)有問(wèn)題的進(jìn)程中具體的情況。
注:jstack 進(jìn)程id,是通過(guò)jstack 命令定位具體哪段代碼出現(xiàn)占用CPU過(guò)高,注意jstack命令查找的線程id是16進(jìn)制的,需要轉(zhuǎn)換;
發(fā)現(xiàn)里面有一堆執(zhí)行的代碼,那么我們?cè)趺凑业骄唧w是哪個(gè)家伙搞事情的呢?上圖我們可以發(fā)現(xiàn)搞事情的線程是20441,那么我們通過(guò)計(jì)算器將20441轉(zhuǎn)換為16進(jìn)制的4FD9再去試試,真相只有一個(gè)。
通過(guò)對(duì)比nid(線程id)4fd9,我們發(fā)現(xiàn)這個(gè)叫thread1的線程一直在運(yùn)行(RUNNABLE狀態(tài)),并且查看到位置是位于Stack_6.java文件的第11行出現(xiàn)的問(wèn)題。。。
現(xiàn)在我們回到源碼中,在Stack_6文件的第11行,我們發(fā)現(xiàn)原來(lái)這里一直在執(zhí)行死循環(huán),終于找到你,還好我沒(méi)放棄,奈斯~
4. JVM內(nèi)存結(jié)構(gòu)-本地方法棧
4.1 定義
由于Java本身有時(shí)候是無(wú)法直接和操作系統(tǒng)底層交互的,但有時(shí)候需要Java調(diào)用本地的C或C++方法,所以此時(shí)本地方法棧應(yīng)運(yùn)而生,它們是一類帶有native關(guān)鍵字的方法。
5. JVM內(nèi)存結(jié)構(gòu)-堆
5.1 定義
Heap堆:是通過(guò)new關(guān)鍵字創(chuàng)建的對(duì)象存放在堆中
5.2 特點(diǎn)
5.2.1線程共享
堆中存放的對(duì)象都是線程共享的,因此都是需要考慮線程安全問(wèn)題的。
5.2.2有垃圾回收機(jī)制
因?yàn)槎阎写娣诺膶?duì)象存放了大量的對(duì)象,因此給他配了個(gè)小助手——垃圾回收機(jī)制(可以調(diào)自動(dòng)擋和手動(dòng)擋哦~)。
5.3 堆內(nèi)存溢出小實(shí)驗(yàn)
5.3.1 修改堆內(nèi)存大小參數(shù)的小實(shí)驗(yàn)
繼續(xù)幻想一個(gè)場(chǎng)景,當(dāng)一個(gè)大佬開(kāi)發(fā)完一個(gè)段代碼的時(shí)候(當(dāng)然一般大佬都是很自信的,我寫(xiě)的代碼怎么可能有問(wèn)題,不存在的。。。),但是測(cè)試可跑不了,穩(wěn)妥起見(jiàn)咱們還是默默得搞測(cè)試試試嘛,安全第一。但是機(jī)器的內(nèi)存就這么大,大佬肯定跑了很多次了,都沒(méi)出現(xiàn)問(wèn)題的,這不是找茬嘛。。。還是默默改下機(jī)子參數(shù)再試試吧(想去懟大佬,一定要拿出證據(jù)嘛~)。
-Xmx:JVM調(diào)優(yōu)參數(shù)之一,該參數(shù)表示java堆可以擴(kuò)展到的最大值。下面上案例:
在執(zhí)行了26次之后,果斷的后臺(tái)報(bào)了堆內(nèi)存溢出錯(cuò)誤。
下面通過(guò)-Xmx JVM調(diào)優(yōu)參數(shù)將堆內(nèi)存調(diào)小至8m,再試試會(huì)發(fā)生什么呢?
操作基本和棧內(nèi)存溢出的時(shí)候的案例一樣,次數(shù)明顯變小了,只調(diào)用了17次就出現(xiàn)了堆內(nèi)存溢出錯(cuò)誤了。
5.3.2 堆內(nèi)存診斷的小實(shí)驗(yàn)
jps工具:查看當(dāng)前系統(tǒng)中有哪些java進(jìn)程
jmap工具:查看堆內(nèi)存的占用情況jmap -heap 進(jìn)程id
jconsole工具:圖形化的工具,擁有多功能的監(jiān)測(cè)功能,可以連續(xù)監(jiān)測(cè)。
下面我們通過(guò)運(yùn)行代碼后通過(guò)jconsole可視化圖形工具,來(lái)查看堆內(nèi)存的使用情況。
上圖我們可以看到,在我們創(chuàng)建10mb的數(shù)組對(duì)象時(shí),內(nèi)存使用有一定上升;然后在我們手動(dòng)調(diào)用垃圾回收機(jī)制后,內(nèi)存又得到了很大的釋放。
6. JVM內(nèi)存結(jié)構(gòu)-方法區(qū)
6.1 定義
Java虛擬機(jī)中有一個(gè)被所有jvm線程共享的方法區(qū)。方法區(qū)有點(diǎn)類似于傳統(tǒng)編程語(yǔ)言中的編譯代碼塊或者操作系統(tǒng)層面的代碼段。它存儲(chǔ)著每個(gè)類的構(gòu)造信息,譬如運(yùn)行時(shí)的常量池,字段,方法數(shù)據(jù),以及方法和構(gòu)造方法的代碼,包括一些在類和實(shí)例初始化和接口初始化時(shí)候使用的特殊方法。
方法區(qū)有個(gè)別稱non-heap(非堆),可以看作是一塊獨(dú)立于堆的內(nèi)存空間,是JVM規(guī)范中定義的一個(gè)概念,用于存儲(chǔ)類信息、常量池、靜態(tài)變量,JIT編譯后的代碼等數(shù)據(jù),具體放在哪里,不同的實(shí)現(xiàn)可以放在不同的地方。
6.2 特點(diǎn)
(1)方法區(qū)與java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域;
(2)方法區(qū)在JVM啟動(dòng)的時(shí)候被創(chuàng)建;
(3)方法區(qū)的大小,跟堆空間一樣,可以選擇固定大小或擴(kuò)展;
(4)方法區(qū)的大小決定了系統(tǒng)可以保存多少個(gè)類,如果系統(tǒng)定義了太多的類,導(dǎo)致方法區(qū)溢出,虛擬機(jī)同樣會(huì)拋出溢出錯(cuò)誤OutOfMemoryError;
(5)關(guān)閉JVM就會(huì)釋放這個(gè)區(qū)域的內(nèi)存。
6.3 JVM內(nèi)存結(jié)構(gòu)示意圖
在JVM內(nèi)存結(jié)構(gòu)1.6的時(shí)候,方法區(qū)保存在內(nèi)存結(jié)構(gòu)中,叫做永久代,里面存儲(chǔ)了運(yùn)行時(shí)的常量池(包含串池StringTable)、類的信息、類加載器;
在JVM內(nèi)存結(jié)構(gòu)1.8的時(shí)候,方法區(qū)做為一個(gè)概念,保存在本地內(nèi)存中,叫做元空間,里面存儲(chǔ)了運(yùn)行時(shí)的常量池、類的信息、類加載器,此時(shí)串池(StringTable)儲(chǔ)存在堆之中。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來(lái)幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
一次因Java應(yīng)用造成CPU過(guò)高的排查實(shí)踐過(guò)程
一個(gè)應(yīng)用占用CPU很高,除了確實(shí)是計(jì)算密集型應(yīng)用之外,通常原因都是出現(xiàn)了死循環(huán)。下面這篇文章主要給大家介紹了一次因Java應(yīng)用造成CPU過(guò)高的排查實(shí)踐過(guò)程,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2018-11-11Spring Boot 實(shí)現(xiàn)配置文件加解密原理
這篇文章主要介紹了Spring Boot 實(shí)現(xiàn)配置文件加解密原理,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06Spring-Validation 后端數(shù)據(jù)校驗(yàn)的實(shí)現(xiàn)
這篇文章主要介紹了Spring-Validation 后端數(shù)據(jù)校驗(yàn)的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07Java數(shù)組轉(zhuǎn)換為L(zhǎng)ist的四種方式
這篇文章主要介紹了Java開(kāi)發(fā)技巧數(shù)組轉(zhuǎn)List的四種方式總結(jié),每種方式結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-09-09spring-boot整合Micrometer+Prometheus的詳細(xì)過(guò)程
這篇文章主要介紹了springboot整合Micrometer+Prometheus的詳細(xì)過(guò)程,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-05-05Spring使用Configuration注解管理bean的方式詳解
在Spring的世界里,Configuration注解就像是一位細(xì)心的園丁,它的主要職責(zé)是在這個(gè)繁花似錦的園子里,幫助我們聲明和管理各種各樣的bean,本文給大家介紹了在Spring中如何優(yōu)雅地管理你的bean,需要的朋友可以參考下2024-05-05Java數(shù)據(jù)庫(kù)存儲(chǔ)數(shù)組的方法小結(jié)
在現(xiàn)代軟件開(kāi)發(fā)中,數(shù)組是常用的數(shù)據(jù)結(jié)構(gòu)之一,然而,在關(guān)系數(shù)據(jù)庫(kù)中直接存儲(chǔ)數(shù)組并不是一個(gè)簡(jiǎn)單的任務(wù),本文將詳細(xì)介紹幾種在Java中將數(shù)組存儲(chǔ)到數(shù)據(jù)庫(kù)的方法,包括使用JPA、JSON、XML、以及關(guān)系型數(shù)據(jù)庫(kù)的數(shù)組類型等,需要的朋友可以參考下2024-09-09