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