研究了一波JVM,自己把手頭的資料做一些整理。
一,JVM演變史
圖出處:https://www.cnblogs.com/xiaofuge/p/14244755.html
圖中大概可以看出一個梗概,那就是方法區(永久代)的逐漸消亡,從主內存中逐漸變到本地內存中。
Hotspot中 方法區的變化:
- jdk1.6及之前:有永久代(permanent generation),靜態變量存放在 永久代上。
- jdk1.7:有永久代,但已經逐步“去永久代”,字符串常量池、靜態變量移除,保存在堆中。
- jdk1.8及之后:無永久代,類型信息、字段、方法、常量保存在本地內存的元空間,但字符串常量池、靜態變量仍留在堆空間。
那為什么要這么做呢?
原來方法區存儲了類的元數據信息和各種常量,而且他也受GC的管理,而GC的目標理應當是對這些類型的卸載和常量的回收。但由於這些數據被類的實例引用,卸載條件變得復雜且嚴格,回收不當會導致堆中的類實例失去元數據信息和常量信息。因此,回收方法區內存不是一件簡單高效的事情,往往GC在做無用功。另外隨着應用規模的變大,各種框架的引入,尤其是使用了反射,動態代理等字節碼生成技術的框架,對於方法區的大小設置無法把控,會導致方法區內存占用越來越大,最終OOM,那把方法區剔除出來是當務之急。
二,JVM模型拆檢
JVM內存模型可以分為兩個部分,堆和方法區(元空間)是所有線程共有的,而虛擬機棧,本地方法棧和程序計數器則是線程私有的。為了形象說明,參見下圖:
圖出處:https://www.cnblogs.com/yanl55555/p/13323128.html
1. 程序計數器
- 較小的內存空間、線程私有,記錄當前線程所執行的字節碼行號。
- 在虛擬機的模型里,字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、異常處理、線程恢復等基礎功能都需要依賴計數器完成。
- 這一塊區域沒有任何 OOM 定義
2. 本地方法棧(Native Stack)
與虛擬機棧基本類似,區別在於虛擬機棧為虛擬機執行的java方法服務,而本地方法棧則是為Native方法服務,普通開發可以忽略。JDK1.8 HotSpot虛擬機直接就把本地方法棧和虛擬機棧合二為一。
3. 虛擬機棧(JVM Stack)
-
java方法執行的內存模型——棧幀:用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每個方法被調用到執行完的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。生命周期與線程相同,無線程安全的顧慮,因為都是線程單獨私有的。
-
-Xss設置每個線程堆棧的大小。一般情況下256K是足夠了。
局部變量表:
-
存放方法參數和方法內部定義的局部變量(包括對象引用)。注意如果是成員變量,或者定義在方法外對象的引用,它們存儲在堆中,涉及到線程共有的都在堆中,如果都是私有,也就沒線程安全這一說了。
-
局部變量表的容量以變量槽slot為最小單位,每個變量槽都可以存儲32位長度的內存空間,例如boolean、byte、char、short、int、float、reference。其中64位長度的long和double類型的數據會占用2個空間,同時slot支持復用。
-
局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在棧幀中分配多大的局部變量是完全確定的,在運行期間棧幀不會改變局部變量表的大小空間。
操作數棧:
- 一個后進先出(Last-In-First-Out)的操作數棧,也可以稱之為表達式棧。方法開始執行時,這個方法的操作數棧是空的。
- 操作數棧的每個位置上可以保存一個java虛擬機中定義的任意數據類型的值,包括long和double。32位數據類型所占的棧容量為1,64位數據類型所占的棧容量為2。
- 所需的最大深度在編譯期就定義好了,保存在方法的Code屬性中,為max_stack的值。
- 在方法執行過程中,根據字節碼指令,往棧中寫入數據或提取數據,即入棧(push) /出棧(pop)
- 在概念模型中,兩個棧幀是相互獨立的。但是大多數虛擬機的實現都會進行優化,令兩個棧幀出現一部分重疊。令下面的部分操作數棧與上面的局部變量表重疊在一塊,這樣在方法調用的時候可以共用一部分數據,無需進行額外的參數復制傳遞
圖出處:https://blog.csdn.net/dyangel2013/article/details/106588217
動態鏈接:
-
運行時常量池中該棧幀所屬方法的引用。包含這個引用的目的就是為了支持當前方法的代碼能夠實現動態鏈接(Dynamic Linking) 。比如: invokedynamic指令
-
Java源文件被編譯到字節碼文件中時,所有的變量和方法引用都作為符號引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一個方法調用了另外的其他方法時,就是通過常量池中指向方法的符號引用來表示的,那么動態鏈接的作用就是為了將這些符號引用轉換為調用方法的直接引用。
圖出處:https://blog.csdn.net/dyangel2013/article/details/106588217
方法返回地址:
當一個方法開始執行以后,只有兩種方法可以退出當前方法:
-
當執行遇到返回指令,會將返回值傳遞給上層的方法調用者,這種退出的方式稱為正常完成出口,一般來說,調用者的PC計數器可以作為返回地址。
-
當執行遇到異常,並且當前方法體內沒有得到處理,就會導致方法退出,此時是沒有返回值的,稱為異常完成出口,返回地址要通過異常處理器表來確定。
-
當方法返回時,有三個操作:
- 恢復上層方法的局部變量表和操作數棧。
- 把返回值壓入調用者調用者棧幀的操作數棧,異常返回時沒有返回值。
- 調整PC計數器的值以指向方法調用指令后面的一條指令。