JDK 方法區(qū)變遷史:版本間的差異與改進
經??吹匠鯇WJVM的讀者會因為方法區(qū)這一概念提出下面這些混淆的問題和概念:
- 什么是方法區(qū)?
- 方法區(qū)和永久代還有元空間是什么關系?
- JDK8版本的常量和靜態(tài)變量是在堆區(qū)?永久代?還是方法區(qū)?還是元空間?
所以,筆者這里就以這篇文章來幫助讀者梳理一下JVM中方法區(qū)的概念。
詳解各版本JVM方法區(qū)
方法區(qū)簡介
方法區(qū)其實是一個《Java虛擬機規(guī)范》一個邏輯上的概念,對于不同版本的JVM都有不同的實現(xiàn),就以我們常用的HotSpot JVM而言,方法區(qū)還有一個別名叫Non-Heap,即非堆內存,這么定義的目的自然是要讓Java開發(fā)者明白方法區(qū)和堆是一塊獨立于Java堆的內存空間,而這里筆者也列出方法區(qū)幾個通用的概念:
- 方法區(qū)和Java堆內存一樣也是屬于各個線程共享的內存區(qū)域。
- 方法區(qū)在JVM啟動就時創(chuàng)建,并且它實際的物理內存空間和Java堆內存一樣可以是不連續(xù)的,注意筆者所說,可以是不連續(xù)的。
- 方法區(qū)內存大小也可以選擇固定大小或者可擴展。
- 方法區(qū)的大小決定了系統(tǒng)可以保存多少個類,如果系統(tǒng)定義了太多的類,同樣會出現(xiàn)內存溢出的問題,可能是java.lang.OutOfMemoryError:PermGen space(永久代空間滿了),也可能是java.lang.OutOfMemoryError:Metaspace(元空間滿了),這一點筆者會在后文中方法區(qū)在各個版本中的實現(xiàn)進行拓展說明。
這里我們補充說明一下,后文所涉及的不同版本的JVM版本都是以HotSpot虛擬機展開探討。
JDK7之前的版本
先來在JDK7之前的版本內存結構圖,在這些版本上邏輯上方法區(qū)和堆區(qū)在邏輯上是連續(xù)的,實際上在物理內存上來說,它們卻可是一塊連續(xù)的內存。在JDK7之前的版本,它們都用的是一個名為PermGen(永久代)的虛作為方法區(qū)的實現(xiàn)。 這也是為什么很多讀者會把永久代和老年代混淆,實際上這兩個完全不是一個概念,在JDK7之前的版本,永久代僅僅是作為方法區(qū)的實現(xiàn)并和老年代捆綁在一起,當老年代或者永久代任何一個內存空間滿了的時候,都會觸發(fā)一次垃圾收集。
在這些個版本的JVM,方法區(qū)即永久代存儲的是:
- 類信息
- 字段信息
- 方法信息
- 常量
- 靜態(tài)變量
- 即時編譯器編譯后的代碼緩存等數(shù)據
JDK7版本的變化
JDK7則是基于原有的內存結構的基礎上將部分數(shù)據進行轉移:
- 將符號引用(Symbols)轉移到Native Memory(本地內存),可能很多讀者經常聽到本地內存這一概念,這里筆者進行拓展解釋一下,本地內存即JVM運行時內存,它是不受GC管理的一塊內存區(qū)域,是直接由操作系統(tǒng)分配給JVM的一塊內存,需要程序手動進行獲取和釋放。
- 因為永久代的GC是跟隨著老年代觸發(fā)的,所以考慮到垃圾回收的效率,JDK7將所有字符串常量的信息都直接移動到Java Heap中。
- 類的靜態(tài)變量轉移到Java Heap中。
JDK8版本對于方法區(qū)的實現(xiàn)
最后我們再來說說現(xiàn)主流的JDK8版本,它基于JDK7的存儲方式,將永久代(Perm Gen) 改為元空間(Metaspace) 作為方法區(qū)的實現(xiàn),同時元空間不再與堆內存連續(xù),是一個劃分在本地內存(Native memory) 的一塊內存區(qū)域,這也就意味著JDK8版本實現(xiàn)的方法區(qū)不參與Java Heap的GC,僅僅處理元數(shù)據空間那些已卸載類的垃圾回收。
所以JDK8版本的內存結構最終如下圖所示,這也就意味著JDK7版本對永久代的設置參數(shù)(-XX:MaxPermSize) 變?yōu)闊o效參數(shù),取而代之的是對元空間空間大小設置的參數(shù)(-XX:MetaspaceSize)。
實踐驗證觀點
接下來我們通過幾段代碼來印證筆者的觀點,來看看這段代碼,筆者這里直接聲明了一段最大長度的靜態(tài)數(shù)組,這個數(shù)組長度為Integer.MAX_VALUE,粗略估算這個數(shù)組大致需要占用4G左右的內存空間。
//聲明一個靜態(tài)數(shù)組
public static int[] arr=new int[Integer.MAX_VALUE];
public void test(){
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
public static void main(String[] args) {
new Main().test();
}
輸出結果如下,可以看到直接拋出了OOM異常,這也就意味著靜態(tài)變量在JDK8版本的堆內存中。
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
at com.sharkChili.webTemplate.Main.<clinit>(Main.java:16)
Exception in thread "main"
同理的再來看看這段代碼。筆者聲明了一個常量數(shù)組,如果它也存在于堆內存中的話,那么它的運行結果也是OOM:
//常量全局數(shù)組
final int[] arr = new int[Integer.MAX_VALUE];
public void test() {
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
public static void main(String[] args) {
new Main().test();
}
意料之內,在JDK8版本常量也是分配于堆內存中:
Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
at com.sharkChili.webTemplate.Main.<init>(Main.java:15)
at com.sharkChili.webTemplate.Main.main(Main.java:25)
接下來這個實驗比較特殊,我們都知道CGLIB是一個強大且高性能的字節(jié)碼生成庫,它支持運行時擴展Java類或接口實現(xiàn),本質上就是動態(tài)生成一個子類并覆蓋要代理的類。所以為了驗證JDK8版本的類信息是否是存于堆區(qū)還是方法區(qū),我們就基于一個CGLIB通過無限循環(huán)去創(chuàng)建無數(shù)的代理類,讓JVM去存儲這些類定義的信息,看看最終拋出的是OOM還是元空間不足。
為了能夠更快看到效果,筆者手動調整了一下元空間的大小:
-XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m
示例代碼如下,通過無限循環(huán)生成代理類并創(chuàng)建EmptyObject的代理對象:
public static void main(String[] args) {
while (true){
Enhancer enhancer = new Enhancer();
//設置代理目標
enhancer.setSuperclass(EmptyObject.class);
//不生成同屬性類的靜態(tài)緩存
enhancer.setUseCache(false);
//設置單一回調對象,在調用中攔截對目標方法的調用
enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> methodProxy.invokeSuper(objects, args));
//如有必要,生成一個新類,并使用指定的回調(如果有的話)創(chuàng)建一個新的對象實例。
enhancer.create();
}
}
啟動后我們使用jvisualvm查看當前程序的GC情況,可以看到Java Heap運行正常,即時創(chuàng)建的無用代理對象都會被回收掉:
再來看看元空間,可以看到隨著實踐的推移,無數(shù)個全新的代理類的信息存到元空間,因為元空間不受GC管理,所以使用內存不斷增加:
最終如預期所說出現(xiàn)java.lang.OutOfMemoryError: Metaspace:
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:345)
at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:114)
at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:291)
at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
at com.sharkChili.webTemplate.Main.main(Main.java:39)
常見面試題
1.為什么JDK8要將取消永久代的概念
大體來說取消永久代有以下兩個原因:
- 首要原因是Hotspot和JRockit代碼合并,前者并沒有所謂的永久代。
- 為了提高垃圾的回收的效率。我們都知道在JDK8版本之前老年代和永久代內存空間是連續(xù)的,任何一個滿了都可能觸發(fā)GC,這種做法對于永久代來說回收效率偏低(每次GC基本回收不了多少垃圾),且Hotspot為了做到這一點還需要專門對元數(shù)據信息進行特殊處理,所以為了簡化GC處理,JDK8版本就將方法區(qū)改為使用元空間實現(xiàn),如此后續(xù)對于元數(shù)據內存優(yōu)化可以專門處理而無需考慮對于堆空間的影響。
2.什么是方法區(qū)?是如何實現(xiàn)的?
方法區(qū)是Java虛擬機規(guī)范中定義的一塊用于存儲類信息、常量、靜態(tài)變量以及編譯器便后的代碼數(shù)據的邏輯內存區(qū)域,注意這里筆者所強調的是邏輯內存邏輯區(qū)域,而非物理形式的內存區(qū)域,而對應的內存實現(xiàn),在不同的JDK版本不同的實現(xiàn):
- 在JDK6的版本方法區(qū)都是通過永久代進行實現(xiàn),存儲類信息、常量池(包括字符串常量池)、靜態(tài)變量和JIT編譯器編譯后的代碼等數(shù)據。
- JDK7方法區(qū)還是永久代實現(xiàn),只不過將字符串常量池和靜態(tài)變量都存放到堆內存中,主要原因是永久代GC效率太低,只有在full gc的時候才會回收,所以將字符串常量池放到堆區(qū)保證高效的回收字符串。
- 從JDK8開始方法區(qū)的實現(xiàn)直接用元空間來實現(xiàn),而元空間使用的即native memory,也就是本地內存,而本地內存即動態(tài)向操作系統(tǒng)獲取的內存空間,需要程序手動進行獲取和釋放,從Java的角度來說就是不受JVM虛擬機所約束的內存空間。也正是因為這幾個特點,保證元空間可以根據應用程序的需求動態(tài)調整大小,避免永久代內存溢出問題的同時還減少的GC回收的壓力。