方法區
JAVA技術交流群:737698533
方法區是運行時數據區的最后一個內容,Method Area
棧,堆,方法區中的交互關系
方法區簡述
方法區(Method Area),與java堆一樣,是各個線程共享的內存區域,它用於存儲已經被虛擬機加載的類型信息,常量,靜態變量,及時編譯后的代碼緩存等數據
java虛擬機規范中明確說明:"盡管所有的方法區在邏輯上是屬於堆的一部分,但是一些簡單的實現可能不會選擇去進行垃圾回收或進行壓縮",但是對於HotSpot JVM 而言,方法區還有一個別名叫做Non-Heap(非堆),目的就是要和堆區分開來
所以,方法區看作是一塊獨立於堆的內存空間
方法區在JVM啟動時候就被創建,並且它的實際物理內存空間和java堆區一樣都是可以不連續的,方法區的大小可以選擇固定大小,也可以選擇可擴展,方法區的大小決定了可以保持多少個類,如果系統定義了太多的類,導致方法區溢出,虛擬機同樣會拋出內存溢出異常:java.lang.OutOfMemoryError:PermGen Space或java.lang.OutOfMemoryError,為什么是兩種異常呢?因為在JDK8之前,方法區又稱為"永久代",而JDK8以及8之后改為"元空間"
方法區,元空間,永久代三者關系是什么呢?方法區是java虛擬機規范的一部分,而元空間和永久代是一個具體的實現,元空間的本質和永久代類似,都是對JVM規范方法區的實現,不過兩者最大的區別就是:元空間不在虛擬機中設置內存,而是直接使用本地內存
設置方法區大小
JDK1.7及以前
- -XX:PermSize來設置永久代初始分配空間,默認為20.75m
- -XX:MaxPermSize來設定永久代最大可分配空間,32位機器默認64m,64位機器默認82m
- 當jvm加載類信息超過最大容量,會報OutOfMemoryError:PermGen Space
JDK1.8以及后
- 元數據區大小使用參數-XX:MetaSpaceSize和-XX:MaxMetaspaceSize指定,來替代jdk7原有的兩個參數
- 默認值依賴於平台,windows下默認初始化大小為21M,最大值為-1,及沒有限制
- 於永久代不同,如果不指定大小,默認情況下,虛擬機會耗盡所有的可用系統內存,如果元數據區發生移除,虛擬機一樣也會拋出異常OutOfMemoryError:Metaspace
- -XX:MetaspaceSize:設置初始的元空間大小,對於一個64位的服務器端JVM來說,其默認的內存大小為21MB,這就是初始的高水位線,一旦觸及這個水位線,Full GC將會觸發並卸載沒有用的類(即這些類對應的類加載器不在存活),然后這個高水位線會重置,新的高水位線取決於GC后釋放了多少元空間,如果釋放的空間不足,那么在不超過MaxMetaspaceSize的情況下適當提高該值,如果釋放空間過多,則適當降低該值
- 如果初始化高水位線設置過低,上述高水位線調整情況會發生很多次,通過垃圾回收日志可以觀察到Full GC多次調用,為了避免頻繁GC,建議將-XX:MetaspaceSize設置為一個相對較高的值
方法區內部結構
類型信息
對每個加載的類型(類Class,接口interface,枚舉enum,注解annotation),jvm必須在方法區存儲以下類型信息
- 類型的完整有效名稱,全限定名
- 類型直接父類的全限定名
- 類型的修飾符(public,abstract,final的某個子集)
- 類型直接接口的一個有序列表
域(Field)信息 (屬性,字段)
- JVM必須在方法區中保存類型的所有域的相關信息以及域的聲明順序
- 域的相關信息包括:域名稱,域類型,域修飾符(public,private,protected,static,final,volatile,transient的某個子集)
方法(Method)信息
JVM必須保存所有方法的一下信息,同域信息一樣包括聲明順序
- 方法名稱
- 方法的返回類型(或void)
- 方法參數的屬性和類型,按照順序
- 方法的修飾符(public,private,protected,static,final,synchronized,native,abstract的子集)
non-final的類變量
靜態類變量和類關聯在一起,隨着類的加載而加載,它們成為類數據在邏輯上的一部分,類變量被類所有實例共享,即使沒有類實例時也可以訪問
全局常量(static final)
被聲明為final的類變量處理方式則不同,每個全局變量在編譯的時候就會被分配了
運行時常量池
-
運行時常量池(Runtime Constant Pool) 是方法區的一部分
-
常量池表(Constant Pool Table)是Class文件的一部分,用於存放編譯期間生成的各種字面量與符號引用,這部分內容將在類加載后存放到方法區的運行時常量池中
-
當類和接口加載到虛擬機后,就會創建對應的運行時常量池
-
JVM為每個已加載的類型(類或接口)都維護一個常量池,池中的數據項像數組項一樣,是通過索引訪問的
-
運行時常量池中包含多種不同的常量,包括編譯期間就已經明確的數值字面量,也包括到運行期解析后才能獲得的方法或字段引用,此時不再是常量池中的符號地址,這里轉換為真實地址
- 運行時常量池對比Class文件的常量池的另一重要特性是具備動態性
-
運行時常量池類似於傳統編程語言的符號表,但是它所包含的數據卻比符號表要更加豐富一些
-
當創建接口或類的運行時常量池時,如果構造運行時常量池所需的內存空間超過了方法區能提供的最大空間,則JVM會拋出OutOfMemoryError異常
方法區的演進細節
只有HotSpot才有永久代,HotSpot中方法區的變化:
jdk1.6即之前:有永久代(permanent generation),靜態變量存放在永久代上
jdk1.7: 有永久代,但是已經逐步"去永久代",字符創常量池,靜態變量移除,存放在堆空間中
jdk1.8及以后:無永久代,類型信息,字段,方法,常量保存在本地內存的元空間,但字符創常量池,靜態變量仍在堆空間
永久代為什么要替換為元空間
- 永久代設置空間大小難以確定,如果設置比較小容易發生FullGC影響程序性能,而且容易出現OOM,如果過大又占用內存
- 對永久代的調優是很困難的
判斷一個常量是否"廢棄"是相對簡單的,而要判斷一個類型是否屬於"不再被使用的類"的條件就比較苛刻了,需要同時滿足3個條件
- 該類所有實例都已經被回收,也就是java堆中不存在該類以及任何派生子類的實例
- 加載該類的類加載器已經被回收,這個條件除非精心設計的可替換類加載器的場景,如OSGi,JSP的重加載等,否則通常很難達成
- 對應該類的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法
java虛擬機被允許對滿足上述3個條件的無用類進行回收,這里說僅僅是"被允許",而不是和對象一樣,沒有引用了就必然進行回收,關於是否要對類型進行回收,HotSpot虛擬機提供了-Xonclassgc參數進行控制,還可以使用-verbose:class以及-XX:+TraceClass-Loading,-XX:+TraceClassUnLoading查看類加載和卸載信息
在大量使用反射動態代理CGLIb等字節碼框架,動態生成JSP以及OSGI這類頻繁自定義類加載器場景中,通常都需要java虛擬機具備類型卸載的能了,以保證不會對方法區造成過大的內存壓力
StringTable為什么調整
jdk7中將StringTbale放到了堆空間,因為永久代中回收頻率很低,在Full GC 時候才會回收,而Full GC是老年代的空間不足,永久代空間不足時才會觸發,這就導致StringTable回收頻率不高,而我們開發中會有大量字符串創建,回收效率低導致永久代內存不足,放到堆里能及時回收內存
對象實例化內存布局和訪問定位
對象的實例化步驟
- 判斷對象對應的類是否加載,鏈接,初始化
- 虛擬機遇到一條new指令,首先去檢查這個指令的參數能否在MetaSpace的常量池中定位到一個類的符號引用,並檢查這個符號引用代表的類是否已經加載,解析,初始化(即判斷這個類元信息是否存在),如果沒有,那么在雙親委派機制模式下,使用當前類加載器ClassLoader+包名+類名為key進行查找對應的class文件,如果沒有找到文件,則拋出ClassNotFontException異常.如果找到,則進行類加載,並生成對應的Class類對象
- 為對象分配內存,首先計算對象占用空間的大小,接着在堆空間划分一塊內存給新對象,如果實例成員變量是引用變量,僅分配引用變量即可,即4字節大小
- 如果內存規整-指針碰撞:如果內存是規整的,那么虛擬機將采用指針碰撞來為對象分配內存,意思是所有用過的內存放在一邊,空閑內存在另一邊,中間放着一個指針作為分界點的指示器,分配內存就是將指針向空閑內存方向移動與對象大小相同的距離,如果垃圾收集器選擇的是Serial,ParNew這種基於壓縮算法的,虛擬機采用這種分配方式,一般帶有compact(整理)過程的收集器,使用指針碰撞
- 如果內存不規則-空閑列表分配:如果內存不是規整的,已經使用和未使用的內存相互交錯,那么虛擬機采用的是空閑列表法來為對象分配內存,虛擬機維護一個列表,記錄上那些內存塊是可用的,在分配時從表中找到一塊足夠大的空間來分配對象實例,並更新表上的內容,這種分配方式稱為"空閑列表(Free List)"
- 至於選擇哪種分配方式由java堆是否規整決定,而java堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定
- 處理並發安全問題
- 采用CAS失敗重試,區域加鎖保證更新的原子性
- 每個線程設預先分配一塊TLAB
- 初始化分配到的空間
- 所有屬性設置默認值,保證對象實例字段在不賦值時可以直接使用
- 設置對象頭
- 將對象的所屬類(即類的元數據信息),對象的HashCode和對象的GC信息,鎖信息等數據存儲在對象頭中,這個過程具體設置方式取決於JVM實現
- 執行init方法進行初始化
- 在java程序視角來看,初始化才正式開始,初始化成員變量,執行實例化代碼塊,調用類的構造方法,並把堆內對象的首地址賦值給引用變量,因此一般來說(由字節碼是否跟隨有invokespecial指令所決定),new指令之后會緊接着就是執行方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象才算完全創建出來
對象內存布局
對象頭
- 運行時元數據(Mark Word)
- 哈希值(HashCode)
- GC分代年齡
- 鎖狀態標志
- 線程持有的鎖
- 偏向線程ID
- 偏向時間戳
- 類型指針
- 指向類元數據InstanceKlass,確定該對象所屬類型
- 如果是數組,還需要記錄數組長度
實例數據(InstanceData)
它是對象真正存儲的有效信息,包括程序代碼中定義的各種類型的字段(包括從父類繼承下來的和本身擁有的字段)
- 相同寬度的字段被分配在一起
- 父類中定義的變量會出現在子類之前
- 如果CompactFields參數為true(默認為true),子類的窄變量可以插入到父類變量的空隙
對齊填充(Padding)
不是必須,也沒有特別含義,僅僅起到占位符的作用
public class Customer {
int id = 100;
String name;
Account account;
{
name = "小明";
}
public Customer() {
account = new Account();
}
}
class Account {
}
//=========
public class Demo {
public static void main(String[] args) {
Customer c = new Customer();
}
}
對象訪問方式
由於reference類型在java虛擬機規范中並沒有定義這個引用應該通過什么方式去定位,所以對象訪問方式也是由虛擬機自己決定的,主流的訪問方式主要有兩種:直接指針和句柄訪問
句柄訪問
直接指針
這兩種方式各有優勢,使用句柄訪問的最大好處就是reference中存儲的是穩定的句柄地址,在對象被移動(垃圾回收時移動對象是非常普遍的行為)時只需要修改句柄的實例指針,而reference本身不需要修改,而使用直接指針訪問的最大好處就是快,因為少一次指針定位的時間開銷,在HotSpot虛擬機中采用的是直接指針
直接內存
直接內存並不是虛擬機運行時數據區的一部分,也不是java虛擬機規范中的內存區域,在JDK1.4中新加入了NIO類,引入了一種基於通道(Channel)與緩沖區(Buffer)的I/O方式,它可以使用Native函數庫直接分配堆外內存,然后通過一個存儲在java堆里面的DirectByteBuffer對象作為這塊內存的引用進行操作,這樣能在一些場景中顯著提升性能,因為避免了在java堆和native堆中來回復制數據,直接內存分配不會受到java堆大小的印象,但是既然是內存,則肯定會受到本機內存大小的限制,如果內存區域大於物理內存限制,則會拋出OOM異常
直接內存大小可以通過MaxDirectMemorySize設置,如果不指定,默認大小與堆的最大值-Xmx參數值一致