jQuery選擇器
847
2025-03-31
大家好,我是程序員學長。
JVM 系列文章我們已經更新完了JVM 的類加載子系統和虛擬機棧,今天我們來聊一下 JVM 之方法區。
首先,我們來看一下方法區和堆、棧之間的交互關系。
User 存放在元空間,也可以說是在方法區中
變量 user 存放在 java 棧的局部變量表中
new User() 存放在 java 堆中
方法區的理解
在《Java虛擬機規范》中明確說明,“盡管所有的方法區在邏輯上屬于堆的一部分,但一些簡單的實現可能不會選擇去進行垃圾收集或者進行壓縮”。但對于 HotSpot 虛擬機而言,方法區還有一個別名叫做 Non-Heap(非堆),目的就是要和堆分開。
所以,方法區可以看做是一塊獨立于 Java 堆的內存空間。
方法區主要存放的是 Class,而堆中主要存放的是實例化對象。
方法區和 JAVA 堆一樣,是各個線程共享的區域。
方法區在 JVM 啟動的時候被創建,并且它實際的物理內存空間和 JAVA 堆 一樣都可以是不連續的。
方法區的大小,和堆空間一樣,可以選擇固定大小或者可擴展的。
方法區的大小決定了系統可以保存多少個類,如果系統定義了太多的類,導致方法區溢出,虛擬機同樣會拋出內存溢出的錯誤。
加載大量的第三方的 jar 包
Tomcat 部署的工程過多
大量動態生成的反射類
關閉JVM會釋放這個內存區域
在 JDK1.7 及以前,習慣上把方法區,稱為永久代。從 JDK1.8開始,使用元空間代替了永久代。JDK1.8之后,元空間存放在堆外內存中。
《Java虛擬機規范》中,對如何實現方法區,沒有做統一的要求。例如,IBM J9 中就不存在永久代的概念。從JDK1.8之后,HotSpot虛擬機完全廢棄了永久代的概念,改用與 J9 一樣在本地內存中實現的元空間來代替。
元空間的本質與永久代類似,都是對虛擬機規范中的方法區的實現。不過元空間與永久代最大的區別在于:元空間不在虛擬機設置的內存中,而是使用本地內存。
方法區的大小不必是固定的,JVM 可以根據應用的需要動態調整。
jdk7及之前:
通過 -XX:Permsize 來設置永久代初始分配空間。默認值是 20.75 M。
通過 -XX:MaxPermsize 來設置永久代最大可分配空間。32 位的機器默認是 64M,64位的機器默認是 82M。
當 JVM 加載的類信息容量超過了這個值,會報異常 OutOfMemoryError:PermGen Space。
jdk8以后:
元數據大小可以使用參數 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定。與永久代不同,如果不指定大小,默認情況下,虛擬機會耗盡所有的可用系統內存。如果元數據區發生溢出,虛擬機一樣會拋出異常 OutOfMemoryError: Metaspace。
-XX:MetaspceSize:設置初始的元空間大小。對于一個 64 位的服務器端的 JVM,其默認值是 21M。這是初始的高水位線,一旦觸及這個水位線,FullGC 將會被觸發并卸載沒用的類(即這些類對應的類加載器不再存活),然后這個高水位線將會重置。新的高水位線的值取決于 GC 后釋放了多少元空間。如果釋放的空間不足,那么在不超過 MaxMetaspaceSize 時,適當的提高該值。如果釋放的空間過多,則適當的降低該值。
如果初始化的高水位線設置過低,上述高水位線調整情況就會發生很多次。通過垃圾回收器的日志可以觀察到 FullGC 多次調用。為了避免頻繁的 GC ,建議將 -XX:MetaspaceSize 設置為一個相對較高的值。
要解決 OOM 異常或 Heap Space 的異常,一般的手段是首先通過內存分析工具(Eclipse Memory Analyzer)對 dump 出來的堆轉存快照進行分析,重點是確認內存中的對象是否是必要的,也就是要先分清楚到底是出現了內存泄露(Memory Leak)還是內存溢出(Memory Overflow)。
內存泄露就是指有大量的引用指向了一些對象,但是這些對象以后不會再被使用了,由于此時它們還和 GC Roots 有關聯,所以導致以后這些對象也不會被回收,這就是內存泄露的問題。
如果是內存泄漏,可進一步通過工具查看泄漏對象到 GC Roots 的引用鏈。于是就能找到泄漏對象是通過怎樣的路徑與GC Roots 相關聯并導致垃圾收集器無法自動回收他們。掌握了泄漏對象的類型信息,以及 GCRoots 引用鏈的信息,就可以比較準確的定位出泄漏代碼的位置。
如果不存在內存泄漏,換句話說就是內存中的對象確實都還必須存活著,那就應當檢查虛擬機的堆參數(-Xms 和 -Xmx),與機器物理內存對比看是否還可以調大,從代碼上檢查是否存在某些對象生命周期過長、持有狀態時間過長的情況,嘗試減少程序運行期的內存消耗。
方法區中主要存儲已被虛擬機加載的類型信息、常量、靜態變量、即時編譯器編譯后的代碼緩存等。
對每個加載的類型(類 class、接口 interface、枚舉 enum、注解 annotation),JVM 必須在方法區中存儲以下類型信息。
這個類型的完整有效名稱(全名:包名.類名)
這個類型直接父類的完整有效名稱(對于 interface 或是 java.lang.Object,都沒有父類)
這個類型的修飾符(public、abstract、final 的某個子集)
這個類型直接接口的一個有序列表
JVM 必須在方法區中保存類型的所有域的相關信息以及域的聲明順序。
域的相關信息包括:域名稱、域類型、域修飾符(public,private,protected,static,final,volatile,transiend的某個子集)
JVM 必須保存所有方法的以下信息,同域信息一樣包括聲明順序:
方法名稱
方法的返回類型(或 void)
方法參數的數量和類型(按順序)
方法的修飾符(public,private,protected,static,final,synchronized,native,abstrat 的一個子集)
方法的字節碼(bytecodes)、操作數棧、局部變量表及大小(abstract 和 native 方法除外)
異常表(abstract 和 native 方法除外)
靜態變量和類關聯在一起,隨著類的加載而加載,它們成為類數據在邏輯上的一部分。
類變量被類的所有實例共享,即使沒有類實例時,你也可以訪問它。
// non-final 的類變量 public class MethodAreaTest { public static void main(String[] args) { Student student=new Student(); student=null; student.hello(); System.out.println(student.gender); } } class Student{ public static String name="張三"; public static final String gender="男"; public static void hello(){ System.out.println("hello"); } }
如上述代碼所示,即使我們把 student 設置為 null,也不會出現空指針異常。
全局常量就是使用 static final 進行修飾,每個全局常量在編譯時就會被分配了。
顧名思義,運行時常量池就是指運行時的常量池。
方法區,內部包含了運行時常量池
字節碼文件,內部包含了常量池
要弄清楚方法區,需要理解清楚 ClassFile,因為加載類的信息都在方法區
要弄清楚方法區的運行時常量池,就需要理解清楚 ClassFile 中的常量池
一個有效的字節碼文件中除了包含類的版本信息、字段、方法以及接口等描述符信息外,還包含一項信息就是常量池表,包括各種字面量和對類型、域、方法的符號引用。
一個 java 源文件中的類、接口,編譯后產生一個字節碼文件。而 java 中的字節碼需要數據支持,通常這種數據會很大以至于不能直接存到字節碼里,換另一種方式,可以存到常量池,這個字節碼包含了指向常量池的引用。
常量池中主要包括:
數量值
字符串值
類引用
字段引用
方法引用
總的來說,常量池可以看做是一張表,虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等類型。
運行時常量池是方法區的一部分。常量池表是 Class 文件的一部分,用于存放編譯期生成的各種字面量和符合引用,這部分內容將在類加載后存放到方法區的運行時常量池中。
JVM 為每個已加載的類型(類或接口)都維護一個常量池。池中的數據項像數組項一樣,是通過索引訪問的。
運行時常量池中包含多種不同的常量,包括編譯期就已經明確的數值字面量,也包括到運行期解析后才能夠獲得的方法和字段引用。此時不再是常量池中的符號地址,這里換為真實地址。
運行時常量池,相對于 Class 文件常量池的另一重要特征是:具備動態性。
運行時常量池類似于傳統編程語言中的符號表,但是它所包含的數據卻比符號表要更豐富一些。
當創建類或者接口的運行時常量池時,如果構造運行時常量池所需的內存空間超過了方法區所能提供的最大值,則 JVM 會拋出 OutofMemoryError 異常。
jdk1.6及以前:有永久代,靜態變量存儲在永久代上。
jdk1.7 : 有永久代,但已經逐步在去“永久代”,字符串常量池、靜態變量保存在堆中。
jdk1.8 : 無永久代,類型信息、字段、方法、常量保存在本地內存的元空間中,但字符串常量池、靜態變量保存在堆中。
jdk 1.7 中,將 字符串常量池 放到了堆空間中。因為永久代的回收效率很低,在 full gc 的時候才會觸發。而 full gc 是老年代空間不足、永久代空間不足時才會觸發,導致 StringTable 回收效率不高。而我們開發中會有大量的字符串被創建,回收效率低,導致永久代內存不足。而放到堆中,就能更及時的進行回收。
靜態引用對應的對象實體始終都存在堆空間中。
有些人認為方法區是沒有垃圾收集行為的,其實不然。《java 虛擬機規范》對方法區的約束是非常寬松的,提到過可以不要求虛擬機在方法區中實現垃圾收集。事實上也確實有未實現或未能完整實現方法區類型卸載的收集器存在。
一般來說這個區域的回收效果比較難令人滿意,尤其是類型的卸載,條件相當苛刻。但是這部分區域的回收有時又確實是必要的。以前sun 公司的 bug 列表中,曾出現過若干個嚴重的 bug 就是由于低版本的 HotSpot 的虛擬機對此區域未完全回收而導致內存泄露。
方法區的垃圾收集主要回收兩部分內容:常量池中廢棄的常量和不再使用的類型。
先來說說方法區內的常量池之中主要存放的兩大類常量:字面量和符號引用。字面量比較接近 java 語言層次的 常量概念,如文本字符串、被申明為 final 的常量值等。而符號引用則屬于編譯原理方面的概念,包括下面三類常量:
類和接口的全限定名
字段的名稱和描述符
方法的名稱和描述符
HotSpot 虛擬機對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方引用,就可以被回收。回收廢棄常量和回收Java堆中的對象非常類似。(關于常量的回收比較簡單,重點是類的回收)。
判定一個常量是否“廢棄”還是相對簡單,而要判定一個類型是否屬于“不在被使用的類”的條件就比較苛刻了。需要同時滿足下面三個條件:
該類所有的實例都已經被回收,也就是 java 堆中不存在該類及其任意派生子類的實例。
加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如OSGI、JSP的重加載等,否則是很難達成的。
該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
Java 虛擬機被允許對滿足上述三個條件的無用類進行回收,這里說的僅僅是“被允許”,并不是和對象一樣,沒有引用了必然會回收。關于是否要對類型進行回收,HotSpot 虛擬機提供了 -Xnoclassgc 參數進行控制,還可以使用 -verbose:class 以及 -XX:+TraceClassLoading、-XX:+TraceClassUnLoading 查看類加載和卸載信息。
在大量使用反射、動態代理、CGLib 等字節碼框架,動態生成 JSP 以及 OSGI 這類頻繁自定義類加載器的場景,通常都需要 java 虛擬機具備類型卸載的能力,以保證不會對方法區造成過大的內存壓力。
到此為止,我們就把 JVM 的方法區聊完了,如果覺得不錯,轉發、在看、安排起來吧。
你知道的越多,你的思維越開闊。我們下期再見。
Java JVM
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。