JAVA jvm系列--java內(nèi)存區(qū)域
JVM: Java Virtual Machine,Java虛擬機(jī),包括處理器、堆棧 、寄存器等,是用來(lái)執(zhí)行java字節(jié)碼(二進(jìn)制的形式)的虛擬計(jì)算機(jī)。
一、JVM的組成
JVM由以下四部分組成(兩個(gè)子系統(tǒng)和兩個(gè)組件):
類加載器(ClassLoader)
執(zhí)行引擎(Execution Engine)
運(yùn)行時(shí)數(shù)據(jù)區(qū)(Runtime Data Area)
本地庫(kù)接口(Native Interface)
結(jié)構(gòu)如圖:
(1)運(yùn)行時(shí)數(shù)據(jù)區(qū)域我們?cè)诒疚倪M(jìn)行詳解;
(2)類加載機(jī)制會(huì)在后續(xù)文章中依次分析,本文主要介紹運(yùn)行時(shí)數(shù)據(jù)區(qū)域;
(3)執(zhí)行引擎:
JIT編譯器:編譯執(zhí)行;將字節(jié)碼指令變成機(jī)器指令。將機(jī)器指令放在方法區(qū)緩存。
解釋器:逐行解釋字節(jié)碼。
垃圾回收器:內(nèi)存回收的具體實(shí)現(xiàn)。
(4)本地方法庫(kù):
有時(shí)java應(yīng)用需要與java外面的環(huán)境、操作系統(tǒng)交互。這是本地方法存在的主要原因,你可以想想java需要與一些底層系統(tǒng)如操作系統(tǒng)或某些硬件交換信息時(shí)的情況。
jre大部分是用java實(shí)現(xiàn)的,它也通過(guò)一些本地方法與外界交互。例如:類java.lang.Thread 的 setPriority()方法是用java實(shí)現(xiàn)的,但是它實(shí)現(xiàn)調(diào)用的是該類里的本地方法setPriority0()。這個(gè)本地方法是用C實(shí)現(xiàn)的,并被植入JVM內(nèi)部,在Windows 95的平臺(tái)上,這個(gè)本地方法最終將調(diào)用Win32 SetPriority() API。這是一個(gè)本地方法的具體實(shí)現(xiàn)由JVM直接提供,更多的情況是本地方法由外部的動(dòng)態(tài)鏈接庫(kù)(external dynamic link library)提供,然后被JVM調(diào)用。
本地方法可以通過(guò) JNI(Java Native Interface)來(lái)訪問(wèn)虛擬機(jī)運(yùn)行時(shí)的數(shù)據(jù)區(qū),甚至可以調(diào)用寄存器,具有和 JVM 相同的能力和權(quán)限。 當(dāng)大量本地方法出現(xiàn)時(shí),勢(shì)必會(huì)削弱 JVM 對(duì)系統(tǒng)的控制力,因?yàn)樗某鲥e(cuò)信息都比較黑盒。對(duì)內(nèi)存不足的情況,本地方法棧還是會(huì)拋出 nativeheapOutOfMemory。
二、JVM運(yùn)行流程
(1)程序在執(zhí)行之前先要把java代碼轉(zhuǎn)換成字節(jié)碼(class文件);
(2)jvm首先需要把字節(jié)碼通過(guò)類加載器(ClassLoader) 把文件加載到 運(yùn)行時(shí)數(shù)據(jù)區(qū)(Runtime Data Area) ;
(3)字節(jié)碼文件不能直接交個(gè)底層操作系統(tǒng)去執(zhí)行,因此需要特定的命令解析器 執(zhí)行引擎(Execution Engine) 將字節(jié)碼翻譯成底層系統(tǒng)指令再交由CPU去執(zhí)行;
(4)第三步過(guò)程中需要調(diào)用其他語(yǔ)言的接口 本地庫(kù)接口(Native Interface) 來(lái)實(shí)現(xiàn)整個(gè)程序的功能。
注:Java 虛擬機(jī)與 Java 語(yǔ)言沒(méi)有什么必然的聯(lián)系,它只與特定的二進(jìn)制文件.Class 文件有關(guān) 。 因此無(wú)論任何語(yǔ)言只要能編譯成.Class 文件,就可以被 Java 虛擬機(jī)識(shí)別并執(zhí)行,比如Groovy、Kotlin。
三、java內(nèi)存區(qū)域詳解(運(yùn)行時(shí)數(shù)據(jù)區(qū)域)
我們說(shuō)的Java內(nèi)存區(qū)域,一般都指運(yùn)行時(shí)數(shù)據(jù)區(qū)域,其組成如圖所示:
JDK1.8之后的內(nèi)存區(qū)域布局如下:
參考文章:Java內(nèi)存區(qū)域(運(yùn)行時(shí)數(shù)據(jù)區(qū)域)和內(nèi)存模型(JMM)
(一)程序計(jì)數(shù)器
程序計(jì)數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。——內(nèi)存空間小
字節(jié)碼解釋器工作是就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行指令的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴計(jì)數(shù)器完成。——計(jì)數(shù)執(zhí)行
對(duì)于一個(gè)單核cpu(或者是一個(gè)內(nèi)核)來(lái)說(shuō),只能同時(shí)執(zhí)行一條指令,而JVM通過(guò)快速切換線程執(zhí)行指令來(lái)達(dá)到多線程的,真正處理器就能同時(shí)處理一條指令,只是這種切換速度很快,我們根本不會(huì)感知到。為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ),我們稱這類內(nèi)存區(qū)域?yàn)椤熬€程私有”的內(nèi)存?!?strong>線程私有,多線程的實(shí)現(xiàn)
如果線程正在執(zhí)行的是一個(gè) Java 方法,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是 Native 方法,這個(gè)計(jì)數(shù)器值則為空(Undefined)。此內(nèi)存區(qū)域是唯一一個(gè)在 Java 虛擬機(jī)規(guī)范中沒(méi)有規(guī)定任何 OutOfMemoryError 情況的區(qū)域?!?strong>無(wú)內(nèi)存溢出
(二)java虛擬機(jī)棧
線程私有:Java 虛擬機(jī)棧(Java Virtual Machine Stacks)也是線程私有的,它的生命周期與線程相同,與線程同時(shí)創(chuàng)建。線程的生命周期請(qǐng)參考我的另一篇文章:線程的生命周期。
虛擬機(jī)棧描述的是 Java 方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame,是方法運(yùn)行時(shí)的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu))用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。每一個(gè)方法從調(diào)用直至執(zhí)行完成的過(guò)程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過(guò)程。
在活動(dòng)線程中,只有位于棧頂?shù)膸攀怯行У模Q為當(dāng)前棧幀。正在執(zhí)行的方法稱為當(dāng)前方法,棧幀是方法運(yùn)行的基本結(jié)構(gòu)。在執(zhí)行引擎運(yùn)行時(shí),所有指令都只能針對(duì)當(dāng)前棧幀進(jìn)行操作。
(1)局部變量表
局部變量表是一組變量值的存儲(chǔ)空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。局部變量要顯示初始化,沒(méi)有默認(rèn)值。
存放了編譯期間可知的基本數(shù)據(jù)類型、對(duì)象引用類型(引用指針)和returnAddress類型(程序就是存儲(chǔ)在方法區(qū)的字節(jié)碼指令,指向特定指令內(nèi)存地址的指針)。
32位的數(shù)據(jù)類型占用一個(gè)局部變量空間(Slot),64位的long和double占2個(gè)。
在Java程序被編譯為Class文件時(shí),就在方法的Code屬性(Java程序方法中的代碼經(jīng)過(guò)javac編譯之后形成字節(jié)碼存在了Code屬性內(nèi))的max_locals數(shù)據(jù)項(xiàng)中確定了方法所需的分配的局部變量表的最大容量。
(2)操作數(shù)棧
操作棧是個(gè)初始狀態(tài)為空的桶式結(jié)構(gòu)棧。在方法執(zhí)行過(guò)程中, 會(huì)有各種指令往棧中寫(xiě)入和提取信息。JVM 的執(zhí)行引擎是基于棧的執(zhí)行引擎, 其中的棧指的就是操作棧。
虛擬機(jī)把操作數(shù)棧作為它的工作區(qū)——大多數(shù)指令都要從這里彈出數(shù)據(jù),執(zhí)行運(yùn)算,然后把結(jié)果壓回操作數(shù)棧。
i++ 和 ++i 的區(qū)別: i++:從局部變量表取出 i 并壓入操作棧(load memory),然后對(duì)局部變量表中的 i 自增 1(add&store memory),將操作棧棧頂值取出使用,如此線程從操作棧讀到的是自增之前的值。 ++i:先對(duì)局部變量表的 i 自增 1(load memory&add&store memory),然后取出并壓入操作 棧(load memory),再將操作棧棧頂值取出使用,線程從操作棧讀到的是自增之后的值。
(3)動(dòng)態(tài)鏈接
每個(gè)棧幀中包含一個(gè)在運(yùn)行時(shí)常量池中對(duì)所在方法的引用, 目的是支持方法調(diào)用過(guò)程的動(dòng)態(tài)連接。
現(xiàn)有動(dòng)態(tài)鏈接,再有棧幀。
(1)每一個(gè)棧幀當(dāng)中都包含指向運(yùn)行時(shí)常量池棧幀所屬方法的引用(invokedynamic指令);
(2)在java源文件被編譯到字節(jié)碼文件中時(shí),所有的變量和方法引用都作為符號(hào)引用保存在class文件的常量池里;
比如:描述一個(gè)方法調(diào)用的另外的其它方法時(shí),就是通過(guò)常量池中指向該方法的符號(hào)引用來(lái)表示,那么動(dòng)態(tài)鏈接的作用就是為了將這些符號(hào)引用轉(zhuǎn)換為調(diào)用方法的直接引用。
參考:https://www.zhihu.com/question/347395101
知乎上參考到的理解:
比如類里有個(gè)a方法,加載到了元空間的內(nèi)存地址:0x0000 0001號(hào)單元 然后運(yùn)行時(shí)常量池里把這個(gè)方法的符號(hào)引用轉(zhuǎn)換為直接引用: a — 0x0000 0001。
然后調(diào)用a方法,創(chuàng)建棧幀,里面保存了常量池里指向a方法的這個(gè)直接引用 0x0000 0001。就可以從這個(gè)直接引用找到a方法代碼的入口執(zhí)行a方法。
線程切換恢復(fù)后也可以根據(jù)程序計(jì)數(shù)器(偏移量)結(jié)合這個(gè)引用,再次找到a方法在內(nèi)存中上次執(zhí)行到的位置,繼續(xù)執(zhí)行代碼。
什么是符號(hào)引用:
符號(hào)引用以一組符號(hào)來(lái)描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用時(shí)能夠無(wú)歧義的定位到目標(biāo)即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等類型的常量出現(xiàn)。符號(hào)引用與虛擬機(jī)的內(nèi)存布局無(wú)關(guān),引用的目標(biāo)并不一定加載到內(nèi)存中。在Java中,一個(gè)java類將會(huì)編譯成一個(gè)class文件。在編譯時(shí),java類并不知道所引用的類的實(shí)際地址,因此只能使用符號(hào)引用來(lái)代替。比如org.simple.People類引用了org.simple.Language類,在編譯時(shí)People類并不知道Language類的實(shí)際內(nèi)存地址,因此只能使用符號(hào)org.simple.Language(假設(shè)是這個(gè),當(dāng)然實(shí)際中是由類似于CONSTANT_Class_info的常量來(lái)表示的)來(lái)表示Language類的地址。
(4)方法返回地址
方法出口。
方法執(zhí)行時(shí)有兩種退出情況:
正常退出,即正常執(zhí)行到任何方法的返回字節(jié)碼指令; 異常退出。
無(wú)論何種退出情況,都將返回至方法當(dāng)前被調(diào)用的位置。方法退出的過(guò)程相當(dāng)于彈出當(dāng)前棧幀,退出可能有三種方式:
返回值壓入上層調(diào)用棧幀。 異常信息拋給能夠處理的棧幀。 PC計(jì)數(shù)器指向方法調(diào)用后的下一條指令。
(三)本地方法棧
本地方法棧(Native Method Stack)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,它們之間的區(qū)別不過(guò)是虛擬機(jī)棧為虛擬機(jī)執(zhí)行 Java 方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機(jī)使用到的 Native 方法服務(wù)。Sun HotSpot 虛擬機(jī)直接就把本地方法棧和虛擬機(jī)棧合二為一。與虛擬機(jī)棧一樣,本地方法棧區(qū)域也會(huì)拋出 StackOverflowError 和 OutOfMemoryError 異常。
(四)java堆
對(duì)于大多數(shù)應(yīng)用來(lái)說(shuō),Java 堆(Java Heap)是 Java 虛擬機(jī)所管理的內(nèi)存中最大的一塊。Java 堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存?!€程共享
jdk1.8之后,字符串常量池從方法區(qū)移到了堆中。
堆是垃圾收集器管理的主要區(qū)域,因此很多時(shí)候也被稱做“GC堆”(Garbage Collected Heap)。
(1)從內(nèi)存回收的角度來(lái)看,由于現(xiàn)在收集器基本都采用分代收集算法,所以 Java 堆中還可以細(xì)分為:新生代和老年代;再細(xì)致一點(diǎn)的有 Eden 空間、From Survivor 空間、To Survivor 空間等。
(2)從內(nèi)存分配的角度來(lái)看,線程共享的 Java 堆中可能劃分出多個(gè)線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer,TLAB)。
Java 堆可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可,當(dāng)前主流的虛擬機(jī)都是按照可擴(kuò)展來(lái)實(shí)現(xiàn)的(通過(guò) -Xmx 和 -Xms 控制)。如果在堆中沒(méi)有內(nèi)存完成實(shí)例分配,并且堆也無(wú)法再擴(kuò)展時(shí),將會(huì)拋出 OutOfMemoryError 異常?!獌?nèi)存溢出
(五)方法區(qū)
作用:用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
回收:垃圾收集行為在這個(gè)區(qū)域是比較少出現(xiàn)的,其內(nèi)存回收目標(biāo)主要是針對(duì)常量池的回收和對(duì)類型的卸載。
異常:當(dāng)方法區(qū)無(wú)法滿足內(nèi)存分配需求時(shí),將拋出 OutOfMemoryError 異常。
JDK8 之前,Hotspot 中方法區(qū)的實(shí)現(xiàn)是永久代(Perm),JDK8 開(kāi)始使用元空間(Metaspace),以前永久代所有內(nèi)容的字符串常量移至堆內(nèi)存,其他內(nèi)容移至元空間,元空間直接在本地內(nèi)存分配,元空間的大小取決于本地內(nèi)存的大小。
運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。
一般來(lái)說(shuō),除了保存 Class 文件中描述的符號(hào)引用外,還會(huì)把翻譯出來(lái)的直接引用也存儲(chǔ)在運(yùn)行時(shí)常量池中。
運(yùn)行時(shí)常量池相對(duì)于 Class 文件常量池的另外一個(gè)重要特征是具備動(dòng)態(tài)性,Java 語(yǔ)言并不要求常量一定只有編譯期才能產(chǎn)生,也就是并非預(yù)置入 Class 文件中常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時(shí)常量池,運(yùn)行期間也可能將新的常量放入池中,這種特性被開(kāi)發(fā)人員利用得比較多的便是 String 類的 intern() 方法。(當(dāng)調(diào)用 intern() 方法時(shí),編譯器會(huì)將字符串添加到常量池中(stringTable維護(hù)),并返回指向該常量的引用。)
既然運(yùn)行時(shí)常量池是方法區(qū)的一部分,自然受到方法區(qū)內(nèi)存的限制,當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí)會(huì)拋出 OutOfMemoryError 異常。
(六)直接內(nèi)存
直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是 Java 虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。
在 JDK 1.4 中新加入了 NIO,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的 I/O 方式,它可以使用 Native 函數(shù)庫(kù)直接分配堆外內(nèi)存,然后通過(guò)一個(gè)存儲(chǔ)在 Java 堆中的 DirectByteBuffer 對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場(chǎng)景中顯著提高性能,因?yàn)楸苊饬嗽?Java 堆和 Native 堆中來(lái)回復(fù)制數(shù)據(jù)。
顯然,本機(jī)直接內(nèi)存的分配不會(huì)受到 Java 堆大小的限制,但是,既然是內(nèi)存,肯定還是會(huì)受到本機(jī)總內(nèi)存(包括 RAM 以及 SWAP 區(qū)或者分頁(yè)文件)大小以及處理器尋址空間的限制。服務(wù)器管理員在配置虛擬機(jī)參數(shù)時(shí),會(huì)根據(jù)實(shí)際內(nèi)存設(shè)置 -Xmx 等參數(shù)信息,但經(jīng)常忽略直接內(nèi)存,使得各個(gè)內(nèi)存區(qū)域總和大于物理內(nèi)存限制(包括物理的和操作系統(tǒng)級(jí)的限制),從而導(dǎo)致動(dòng)態(tài)擴(kuò)展時(shí)出現(xiàn) OutOfMemoryError 異常。
總結(jié)
如圖所示:
本篇文章就到這里了,希望能夠給你帶來(lái)幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
Java使用jdbc連接實(shí)現(xiàn)對(duì)MySQL增刪改查操作的全過(guò)程
JDBC的全稱是Java?Database?Connectivity,即Java數(shù)據(jù)庫(kù)連接,它是一種可以執(zhí)行SQL語(yǔ)句的Java?API,下面這篇文章主要給大家介紹了關(guān)于Java使用jdbc連接實(shí)現(xiàn)對(duì)MySQL增刪改查操作的相關(guān)資料,需要的朋友可以參考下2023-03-03Java、JavaScript、Oracle、MySQL中實(shí)現(xiàn)的MD5加密算法分享
這篇文章主要介紹了Java、JavaScript、Oracle、MySQL中實(shí)現(xiàn)的MD5加密算法分享,需要的朋友可以參考下2014-09-09Mybatis-Plus自動(dòng)填充更新操作相關(guān)字段的實(shí)現(xiàn)
數(shù)據(jù)庫(kù)表中應(yīng)該都要有create_time、update_time字段;那么在開(kāi)發(fā)中,對(duì)于這些共有字段的處理應(yīng)該要進(jìn)行統(tǒng)一,這樣就可以簡(jiǎn)化我們的開(kāi)發(fā)過(guò)程。那么本文就對(duì)Mybatis-Plus中的字段自動(dòng)填充進(jìn)行記錄2021-11-11Windows環(huán)境下重啟jar服務(wù)bat代碼的解決方案
在Windows環(huán)境下部署java的jar包,若有多個(gè)服務(wù)同時(shí)啟動(dòng),很難找到相應(yīng)服務(wù)重啟,每次都重啟全部服務(wù)很麻煩,應(yīng)用場(chǎng)景大多用于部署測(cè)試,今天給大家分享Windows環(huán)境下重啟jar服務(wù)bat代碼,感興趣的朋友一起看看吧2023-08-08Struts2中ognl遍歷數(shù)組,list和map方法詳解
這篇文章主要介紹了Struts2中ognl遍歷數(shù)組,list和map方法詳解,需要的朋友可以參考下。2017-09-09Java中Controller引起的Ambiguous?mapping問(wèn)題及解決
這篇文章主要介紹了Java中Controller引起的Ambiguous?mapping問(wèn)題及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10關(guān)于Spring?Boot內(nèi)存泄露排查的記錄
這篇文章主要介紹了關(guān)于Spring?Boot內(nèi)存泄露排查的記錄,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06