JVM系列整體欄目
內(nèi)容 | 鏈接地址 |
---|---|
【一】初識虛擬機與java虛擬機 | https://blog.csdn.net/zhenghuishengq/article/details/129544460 |
【二】jvm的類加載子系統(tǒng)以及jclasslib的基本使用 | https://blog.csdn.net/zhenghuishengq/article/details/129610963 |
【三】運行時私有區(qū)域之虛擬機棧、程序計數(shù)器、本地方法棧 | https://blog.csdn.net/zhenghuishengq/article/details/129684076 |
【四】運行時數(shù)據(jù)區(qū)共享區(qū)域之堆、逃逸分析 | https://blog.csdn.net/zhenghuishengq/article/details/129796509 |
【五】運行時數(shù)據(jù)區(qū)共享區(qū)域之方法區(qū)、常量池 | https://blog.csdn.net/zhenghuishengq/article/details/129958466 |
一,運行時數(shù)據(jù)區(qū)共享區(qū)域—方法區(qū)
1,方法區(qū)的基本概述
方法區(qū)和堆一樣,也是屬于運行時數(shù)據(jù)區(qū)中的共享區(qū)域,并且也屬于重要的一個內(nèi)存空間,該空間主要是配合堆棧一起工作。
如下面這行代碼,new User就是存在Java堆中,第一個User就是存在方法區(qū)中,第二個user就是作為局部變量表存儲在棧中。
User user = new User();
#方法區(qū):User
#棧:user
#堆:new User();
《Java虛擬機規(guī)范》中明確說明:“盡管所有的方法區(qū)在邏輯上是屬于堆的一部分,但一些簡單的實現(xiàn)可能不會選擇區(qū)進行垃圾回收或者進行壓縮”。但是對于HotSpot虛擬機而言,方法區(qū)還有一個別名就叫做Non-Heap(非堆),目的就是要和堆分開。因此,方法區(qū)可以看做是一塊獨立于Java堆的內(nèi)存空間
?? 方法區(qū)和java堆一樣,屬于是各個線程共享的區(qū)域
?? 方法區(qū)在Jvm啟動的時候被創(chuàng)建,他的物理內(nèi)存和堆一樣可以是不連續(xù)的
?? 方法區(qū)的大小和堆空間一樣,可以選擇固定大小或者可擴展
?? 方法區(qū)的大小決定了系統(tǒng)可以保存多少個類,如果系統(tǒng)定義了太多的類,導(dǎo)致方法區(qū)溢出,虛擬機同樣會拋出內(nèi)存溢出錯誤,如加載大量的第三方j(luò)ar包,Tomcat部署的工程過多,大量的動態(tài)反射類等
?? 關(guān)閉JVM之后,就會釋放這個區(qū)域的內(nèi)存
2,方法區(qū)的演進過程
這里主要是針對HotSpot虛擬機,在JDK8以前,習(xí)慣將方法區(qū)稱為永久代;從JDK8開始,使用了這個元空間取代了永久代。就相當于把這個方法區(qū)當成是一個接口,而永久代和元空間就是該接口的具體實現(xiàn)。
In JDK8,classes metadata is now stored in the navite heap and this space is called MetaSpace
方法區(qū)和這個永久代并不等價,《Java虛擬機規(guī)范》對如何實現(xiàn)方法區(qū),不做統(tǒng)一的要求。
到了這個 JDK8 之后,終于完全廢棄了永久代的概念,改用JRockit、J9一樣在本地內(nèi)存中實現(xiàn)的元空間來代替。
元空間的本質(zhì)和永久代類似,都是JVM規(guī)范中方法區(qū)實現(xiàn)的,不過元空間與永久代最大的區(qū)別在于:元空間不在虛擬機設(shè)置的內(nèi)存中,而是使用的是本地內(nèi)存。
永久代和元空間二者不只是名字變了,內(nèi)部結(jié)構(gòu)也調(diào)整了,根據(jù)《java虛擬機規(guī)范》的規(guī)定,如果方法區(qū)無法滿足新的內(nèi)存分配需求時,將拋出OOM異常。
3,方法區(qū)大小設(shè)置與OOM
方法區(qū)的大小不必是固定的,jvm可以根據(jù)應(yīng)用的需要動態(tài)調(diào)整。
3.1,方法區(qū)內(nèi)存大小的分配
在jdk1.8之前
?? 可以通過 -XX:PermSize
來設(shè)置永久代初始的分配空間,默認值為20.75M
?? 通過 -XX:MaxPermSize
來設(shè)置永久代最大的分配空間,32位機器默認是64M,64位機器為82M
?? 當JVM加載的類信息容量超過了這個值,會報異常OutOfMemoryError:PermGen
在jdk1.8及以后
?? 元數(shù)據(jù)區(qū)大小可以使用參數(shù) -XX:MetaspaceSize
和 -XX:MaxMetaspaceSize
設(shè)置原始值和最大值
?? 默認值依賴平臺,windows下 -XX:MetaspaceSize
為21M,-XX:MaxMetaspaceSize
為-1,即沒有限制
?? 默認情況下,虛擬機會耗盡所有內(nèi)存,如果元數(shù)據(jù)區(qū)溢出,虛擬機會拋異常OutofMemoryError:Metaspace
?? 當內(nèi)存高于設(shè)置的21M時,就會觸發(fā)Full GC,F(xiàn)ull GC就會卸載掉沒用的類
?? 因此建議將這個初始內(nèi)存設(shè)置一個相對較高的值,以免頻繁觸發(fā)Full GC
3.2,OOM的解決方案
- 要解決這些OOM異?;蛘遠eap space異常,一段手段是通過內(nèi)存印象分析工具堆dump出來的堆轉(zhuǎn)存儲快照進行分析,重點是確認內(nèi)存中的對象是否是必要的,也就是要分清楚是出現(xiàn)了內(nèi)存泄漏還是內(nèi)存溢出。
- 如果是內(nèi)存泄漏,可進一步通過工具查看泄漏對象到GC Roots的引用鏈,于是就能找到泄漏對象是通過怎樣的路徑與GC Roots相關(guān)聯(lián)。在掌握了泄漏對象的信息,以及GC Roots引用鏈之后,就可以準確的定位到泄漏代碼的位置了。
- 如果不存在內(nèi)存泄漏,換句話就是說內(nèi)存中的對象確實是還活著,那么就應(yīng)該檢查虛擬機的參數(shù),與機器內(nèi)存相比看是否還可以調(diào)大,從代碼上檢查是否存在某些對象的生命周期過長,持有時間過長等情況,從而減少運行期間的內(nèi)存消耗。
4,方法區(qū)的內(nèi)部結(jié)構(gòu)
4.1,方法區(qū)存儲數(shù)據(jù)概述
在將 .java
文件編譯成 .class
字節(jié)碼文件之后,這個字節(jié)碼文件是需要存儲的,而類本身的一些信息,則需要存儲在這個方法區(qū)里面,除了類信息之外,這個運行時常量池也是存儲在這個方法區(qū)里面的。
在《深入理解Java虛擬機》這本書中,對方法區(qū)存儲內(nèi)容的概述如下:它用于存儲已被虛擬機加載的類型信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼緩存等。 但是隨著JDK的不斷迭代,其內(nèi)存存儲的東西也會有著稍小的變化。
類型信息:對加載的類型,包括類class、接口interface、枚舉enum、注解annotation等,JVM必須在方法區(qū)中存儲以下類型
? 這個類型的完整有效名稱,全名就是 包名.類名
? 直接父類的完整有效名,interface和Object都是沒有父類的
? 這個類型的修飾符,如public、static、final、abstract
? 直接接口的一個有序列表,如這個類可能實現(xiàn)多個接口
屬性信息:域的相關(guān)信息主要包括以下:域名稱、域類型、域修飾符等
方法信息:方法信息主要包括一些方法名稱、返回類型、參數(shù)的數(shù)量和類型、方法的修飾符、方法的字節(jié)碼、局部變量表以及其大小、異常表等
4.2,static final
在靜態(tài)變量中,一般是隨著累的加載而加載,他們成為類數(shù)據(jù)在邏輯上的一部分,并且類變量被類所有的實例共享,即使類實例不存在也可以進行訪問。
而被聲明的final的類變量的處理方法則不同,該變量在編譯階段就會被分配了;而沒有被聲明final的類變量,在準備階段進行初步的賦值,在初始化階段進行一個最終的賦值。
如寫一個Java測試類,定義一個被final修飾的類變量和不被final修飾的類變量,并且這個類型為基礎(chǔ)數(shù)據(jù)類型
public class Test {
public static final int i = 10;
public static int k = 20;
public static void main(String[] args) {
System.out.println(i+k);
}
}
然后在編譯好的文件中,輸入反編譯命令,并將最終輸出的文件加載到zhs.txt文件中
javap -v -p Test.class > zhs.txt
接下來重點分析這兩個變量,可以發(fā)現(xiàn)這個加了final修飾的i,在編譯階段就進行了分配,賦值為10;而沒有加final修飾的k,在編譯階段沒有賦予默認值,而是在準備階段賦值的默認值,在初始化階段賦予的最終值
public static final int i;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 10
public static int k;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
4.3,運行時常量池
4.3.1,什么是常量池
一個有效的字節(jié)碼文件除了包含類的版本信息、字段、方法以及接口等描述信息之外,還包含一項重要的信息,那就是常量池表(Constant Pool Table),其中包括各種字面量和對類型,字段和方法的符號引用,如下
Constant pool:
#1 = Methodref #6.#27 // java/lang/Object."<init>":()V
#2 = Fieldref #28.#29 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Class #30 // com/fc/v2/util/Test
#4 = Fieldref #3.#31 // com/fc/v2/util/Test.k:I
一個Java源文件在編譯之后會產(chǎn)生一個字節(jié)碼文件,而字節(jié)碼文件需要數(shù)據(jù)支持,通常這種數(shù)據(jù)量很大不能直接存儲到字節(jié)碼文件里面,因此就通過常量池的方式,提前將數(shù)據(jù)存儲在常量池中,然后根據(jù)引用去獲取對應(yīng)的數(shù)據(jù),如在棧幀的 動態(tài)鏈接 就是通過這種方式來獲取數(shù)據(jù)的。
在常量池中存儲的數(shù)據(jù)類型主要有:數(shù)量值,字符串值,類引用,字段引用,方法引用
常量池就可以看做成是一張表,虛擬機指令根據(jù)這張常量表找到執(zhí)行的類名、方法名、參數(shù)類型和字面量等類型。
4.3.2,什么是運行時常量池
上面提到了常量池,常量池是字節(jié)碼文件的一部分,用于存放編譯期生成的各個字面量和符號引用,這部分內(nèi)容在類加載之后存放到方法區(qū)的運行時常量池中;而運行時常量池是屬于方法區(qū)的一部分,接下來詳細的描述一下上面是運行時常量池
? 在將類和接口加載到虛擬機之后,就會創(chuàng)建對應(yīng)的運行時常量池
? JVM會為每個已加載的類型都維護一個常量池,池中的數(shù)據(jù)可以通過索引訪問
? 運行時常量池包含多種不同的常量,包括編譯期就已經(jīng)明確的數(shù)值,如棧幀的大小,以及運行期間才能獲取到的方法或者字段引用,此時不再是常量池中的符號地址#,而是具體的真實地址。
? 運行時常量池具備動態(tài)性,如實際大小可能比計算的大小大
? 當創(chuàng)建接口或者類的運行時常量池時,如果構(gòu)造的運行時常量池所需要的內(nèi)存空間超過了方法區(qū)所能提供的最大值,則JVM就會拋出OutOfMemoryError異常
5,方法區(qū)的使用
接下來查看一段簡單的代碼,如下
/**
* @author zhenghuisheng
* @date : 2023/4/4
*/
public class Test {
public static void main(String[] args) {
int x = 500;
int y = 100;
int a = x/y;
int b = 50;
System.out.println(a+b);
}
}
然后編譯之后,通過jclasslib插件查看對應(yīng)的字節(jié)碼,在前面的章節(jié)又講如何安裝使用
其對應(yīng)的字節(jié)碼文件如下
0 sipush 500
3 istore_1
4 bipush 100
6 istore_2
7 iload_1
8 iload_2
9 idiv
10 istore_3
11 bipush 50
13 istore 4
15 getstatic #2 <java/lang/System.out>
18 iload_3
19 iload 4
21 iadd
22 invokevirtual #3 <java/io/PrintStream.println>
25 return
這個具體的操作流程如下,由于沒有new對象,因此在下圖中暫時先不展示堆空間。
首先程序計數(shù)器的記錄的地址為0,并將這個500這個數(shù)值壓入到操作數(shù)棧中。
然后就是這個istore_1,將操作數(shù)棧中存儲的值存儲到本地變量表中。而這個本地變量表,如果是實例方法或者是構(gòu)造方法,第一個數(shù)據(jù)存儲的應(yīng)該是this,而這里的是static的靜態(tài)方法,因此第一個slot存儲的不是this。
后續(xù)的操作和上面的一樣,這個iload_1就是將數(shù)據(jù)從本地變量本中取出來放到操作數(shù)棧頂,iload_2原理一樣,然后結(jié)果這個idiv除法運算,將結(jié)果5存放在本地變量表3的位置
隨后就是將50入棧,也存儲到本地變量表中4的位置,然后通過這個getstatic #2,就是獲取常量池中的#2的位置,然后最終可以獲取到這個System類,out類和對應(yīng)的type屬性,然后加載到方法區(qū)中。如果這些類或者屬性在方法區(qū)中存在就不會進行加載,如果不存在就會將這些加載到方法區(qū)中,然后將這些#等符號的間接引用變成地址的直接引用。
然后通過這個iload3和iload4將本地變量表的數(shù)據(jù)加載到操作數(shù)棧中
然后再經(jīng)過iadd計算,再調(diào)用這個invokevirtual #3的符號引用,最終可以定位到一個打印操作,最終return結(jié)束
通過上圖可以發(fā)現(xiàn),無論是在哪一個操作,程序計數(shù)器都會指向?qū)?yīng)的執(zhí)行位置。
6,方法區(qū)的演進細節(jié)
在虛擬機中,只有HotSpot虛擬機才有永久代,JRockit和J9是不存在永久代的概念的。在HotSpot虛擬機的方法區(qū)變化如下(方法區(qū)是一種概念,永久代和元空間屬于具體實現(xiàn)):
- 在jdk6及以前:有永久代,靜態(tài)變量存放在永久代上面
- 在jdk7中:有永久代,但逐步去除,字符串常量池、靜態(tài)變量保存在堆中
- 在jdk8及以后:無永久代,類型信息、字段、方法、常量保存在本地內(nèi)存的元空間中,字符串常量池和靜態(tài)變量在堆中
6.1,方法區(qū)的具體實現(xiàn)以及內(nèi)部組成的演進
jdk6的方法區(qū)的組成如下,也稱為永久代,其靜態(tài)變量,運行時常量池,字符串常量池等都是保存在這個永久代的里面,并且字符串常量池是屬于運行時常量池的一部分
而在jdk7開始,就將字符串常量池和靜態(tài)變量存放在堆里面
而從jdk8開始,永久代已經(jīng)不存在了,取而代之的是本地內(nèi)存的 元空間,會將運行時常量池,類信息等全部存儲在本地內(nèi)存中,并且靜態(tài)變量和字符串常量池都是存儲在堆中
在官方文檔中https://openjdk.org/jeps/122,也提到過這個刪除這個永久代的動機,如下,主要就是說參考了這個 JRockit 內(nèi)部的實現(xiàn),這樣用戶可以不必自己去配置這個永久代。這樣這塊空間可以不用jvm本身去管理,從而交給本地內(nèi)存區(qū)實現(xiàn)。
This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.
其實這項改動還是很有必要的,接下來從兩個方面來說明為啥要替換
- 永久代的空間大小設(shè)置是很難確定的,在某些場景下,如果動態(tài)的加載類過多,就很容易出現(xiàn)OOM,比如一些Web項目。而元空間和永久代之間最大的區(qū)別在于:元空間不在虛擬機中,而是使用的是本地內(nèi)存,因此元空間的大小只受本地內(nèi)存限制
- 永久代的調(diào)優(yōu)比較困難
6.2,字符串常量池為何要存儲到堆中
在jdk7的時候,將靜態(tài)變量和字符串常量池都存儲到了堆中,其主要原因是在永久代中,其觸發(fā)的回收效率很低,在full gc的時候才會觸發(fā)。而full gc的老年代空間不足,或者永久代空間不足時才會觸發(fā),因此這就導(dǎo)致了這個字符串常量池的回收率不高。
而在如今的開發(fā)中,可能會創(chuàng)建大量的字符串,如果還是存儲在方法區(qū)內(nèi)部,那么其回收率會比較低,很容易導(dǎo)致永久代的內(nèi)存不足,因此選擇將這個字符串常量池存放到堆中,這樣就可以快速的實現(xiàn)內(nèi)存回收。
7,方法區(qū)的垃圾回收機制
在運行時數(shù)據(jù)區(qū)中,方法區(qū)又被稱為non-heap,就是非堆的意思。并且在《java虛擬機規(guī)范》中描述,對方法區(qū)中的約束是非常寬松的,提到過不要求虛擬機在方法區(qū)中實現(xiàn)垃圾回收
一般來說這個區(qū)域的回收效果比較難令人滿意,但是有時又確實是有必要的,因此在HotSpot虛擬機中,方法區(qū)的垃圾回收主要是回收兩部分內(nèi)容:常量池中廢棄的常量和不再使用的類型
常量回收的策略就是只要該常量沒有被任何地方引用,就可以被回收,回收廢棄常量和回收Java堆的對象非常類似
類型回收的條件相對比較苛刻,需要同時的滿足以下三點條件:文章來源:http://www.zghlxwxcb.cn/news/detail-406782.html
- 該類的實例被回收,該類以及對應(yīng)的子類在堆中不存在
- 該類的類加載器已被回收
- 該類的Class對象沒有任何地方被引用,無法在任何地方通過反射訪問到該類
滿足這三點條件也不是一定會進行回收,而是可能被允許回收。在大量的使用反射、動態(tài)代理、CGLib這些字節(jié)碼框架,動態(tài)生成Jsp等這些場景中,通常需要java虛擬機具備類型卸載的能力,以保證不會對方法區(qū)造成過大的壓力。文章來源地址http://www.zghlxwxcb.cn/news/detail-406782.html
到了這里,關(guān)于【jvm系列-05】精通運行時數(shù)據(jù)區(qū)共享區(qū)域---方法區(qū)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!