文章目錄
方法區
1. 概述
JVM中的方法區可看作時獨立於Java堆的一塊內存空間,它存在的目的就是希望和堆分開。方法區具有如下的特點:
- 線程共享
- 在JVM啓動時就被創建,在JVM關閉時內存被釋放
- 實際的物理內存空間可以是不連續的
- 空間大小可固定,也可動態擴展
- 方法區的大小決定了系統可以保存的類的個數,如果系統定義了太多的類,導致方法區溢出,JVM會拋OOM異常
如何體會方法區在程序運行中所起到的作用呢?下面我們通過一個例子先簡單的感受一下。假設定義的Student類爲:
public class Student {
private int age;
private String name;
public Student() {
}
public Student(int age, String name) {
this.age = age;
this.name = name;
}
// getter and setter
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
通常使用Student stu = new Student(18, "Forlogen")
進行對象的實例化,如果將其和棧、堆、方法區聯繫起來可以簡單的畫爲:
如上所示,使用new實例化的對象保存在堆中,Student的對象變量stu保存在棧中,它對應的是對象在堆中的地址;而棧中保存的是Student類的類型信息等其他相關的內容。
在JDK7及以前,方法區也稱爲永久代,而JDK8及之後,運行時數據區中只有元空間(Meta Space),不再有永久代。元空間的本質和永久代類似,它們都是JVM規範中方法區的實現方式。不過元空間和永久代的最大區別在於:元空間使用的不再是JVM的內存空間,而是直接使用本地內存。
2. 參數設置
前面講到方法區的特點中提到,方法區的大小不必是固定的,它也可以根據應用的需求動態擴展。有關方法區大小設置的參數爲:
-
JDK7及之前
-XX:PermSize
用來設置永久代初始分配空間,默認爲20.75M-XX:MaxPermSize
用來設置永久代最大可分配空間,默認是64M(32bits)或是82M(63bits)
當JVM加載的類信息容量超過了MaxPermSize,JVM就會拋出OutOfMemoryError:PermGen space錯誤。
-
JDK8及之後
-
-XX:MetaSpaceSize
用來設置元空間初始分配空間,默認爲21M -
-XX:MaxMetaSpaceSize
用來設置元空間最大可分配空間,默認爲-1,表示沒有限制因爲元空間使用的是直接內存,因此只有當耗盡所有的系統內存時,JVM纔會拋出OutOfMemoryError:Metaspace錯誤。
對於MetaSpaceSize默認的值的設定來說,當所用空間觸及這個值時,Full GC就會被觸發並卸載沒用的類,然後重置該值。新的值取決於GC後釋放的空間大小,如果釋放的空間不足,則在不超過MaxMetaSpaceSize的前提下,設當提高該值;如果釋放的空間過多,則適當降低該值。
如果MetaSpaceSize默認的值太小,那麼Full GC將會多次被觸發。因此,通常選擇將它設置爲一個較高的值。
-
3. 內部結構
當一個ClassLoader啓動時,生存地點在堆中,然後它將A.class裝載到JVM的方法區。方法區的這個字節碼文件被用來創建對象。這個字節碼文件中有兩個引用:
- 一個用於指向A的class類對象:它存儲了這個字節碼內存塊所有的相關信息,例如可以使用
this.getClass().getDeclaredMethods()
獲取方法信息;使用this.getClass().getDeclaredFields()
獲取字段信息等 - 一個指向加載自己的ClassLoader:例如可以使用
this.getClass().getClassLoader()
獲取類對應的類加載器
此外,方法區還用於存儲已被虛擬機加載的類型信息、常量、靜態常量、即時編譯器編譯後的代碼緩存等。
3.1 類型信息
對於每個加載的類型(類class、接口interface、枚舉enum、註解annotation),JVM必須在方法去種存儲以下的類型信息:
- 這個類型的完整有效名稱(全類名)
- 這個類型直接父類的完整有效名(對於interface或是java.lang.Object,都沒有父類)
- 這個類型的修飾符(public、abstract、final的某個子集)
- 這個類型直接接口的一個有序列表
3.2 域信息
JVM 必須在方法去中保存類型的所有域的相關信息以及域的聲明順序。域的相關信息包括:域名稱、域類型、域修飾符(public、private、protected、static、final、volatile、transient的某個子集)。
3.3 方法信息
JVM必須保存所有方法的以下信息,同樣需要保存聲明順序:
- 方法名稱
- 方法的返回類型
- 方法參數的數量和類型
- 方法的修飾符
- 方法的字節碼、操作數棧、局部變量表及大小(abstract和native方法除外)
- 異常表(abstract和native方法除外):每個異常處理的開始位置、結束位置、代碼處理在程序計數器中的偏移地址、被捕獲的異常類的常量池索引
3.4 non-final的類變量
靜態變量和類關聯在一起,隨着類的加載而加載,它們成爲類數據在邏輯上的一部分。類變量被類的所有實例共享,即使沒有類實例時也可以訪問。
3.5 例子
代碼如下所示:
public class MethodAreaTest {
public static void main(String[] args) {
Order order = new Order();
System.out.println(order.count);
}
}
class Order {
public static int count = 1;
public static final int number = 2;
public static void hello() {
System.out.println("hello!");
}
}
我們編譯上面的代碼,然後在命令行使用javap -v -p MethodAreaTest.class > test.txt
命令獲取類對應字節碼文件反編譯後的結果,主要內容如下所示:
Compiled from "MethodAreaTest.java"
public class MethodArea.MethodAreaTest
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #7 // MethodArea/MethodAreaTest
super_class: #8 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #8.#24 // java/lang/Object."<init>":()V
#2 = Class #25 // MethodArea/Order
#3 = Methodref #2.#24 // MethodArea/Order."<init>":()V
#4 = Fieldref #26.#27 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Fieldref #2.#28 // MethodArea/Order.count:I
#6 = Methodref #29.#30 // java/io/PrintStream.println:(I)V
#7 = Class #31 // MethodArea/MethodAreaTest
#8 = Class #32 // java/lang/Object
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 LMethodArea/MethodAreaTest;
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 args
#19 = Utf8 [Ljava/lang/String;
#20 = Utf8 order
#21 = Utf8 LMethodArea/Order;
#22 = Utf8 SourceFile
#23 = Utf8 MethodAreaTest.java
#24 = NameAndType #9:#10 // "<init>":()V
#25 = Utf8 MethodArea/Order
#26 = Class #33 // java/lang/System
#27 = NameAndType #34:#35 // out:Ljava/io/PrintStream;
#28 = NameAndType #36:#37 // count:I
#29 = Class #38 // java/io/PrintStream
#30 = NameAndType #39:#40 // println:(I)V
#31 = Utf8 MethodArea/MethodAreaTest
#32 = Utf8 java/lang/Object
#33 = Utf8 java/lang/System
#34 = Utf8 out
#35 = Utf8 Ljava/io/PrintStream;
#36 = Utf8 count
#37 = Utf8 I
#38 = Utf8 java/io/PrintStream
#39 = Utf8 println
#40 = Utf8 (I)V
{
public MethodArea.MethodAreaTest();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LMethodArea/MethodAreaTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class MethodArea/Order
3: dup
4: invokespecial #3 // Method MethodArea/Order."<init>":()V
7: astore_1
8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_1
12: pop
13: getstatic #5 // Field MethodArea/Order.count:I
16: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
19: return
LineNumberTable:
line 8: 0
line 9: 8
line 10: 19
LocalVariableTable:
Start Length Slot Name Signature
0 20 0 args [Ljava/lang/String;
8 12 1 order LMethodArea/Order;
}
SourceFile: "MethodAreaTest.java"
4. 運行時常量池
運行時常量池位於方法區,常量池位於字節碼文件中。一個有效的字節碼文件中除了包含類的版本信息、字段、方法以及接口等描述信息外,還包含一項信息那就是常量池表(constant pool table),包括各種字面量和對類型、域和方法的符號引用。
4.1 常量池
一個Java源文件中的類、接口在編譯後會產生一個字節碼文件,而Java中的字節碼需要數據支持。通常這種數據會很大,以至於不能直接存到字節碼文件中。而是選擇換一種方式,將其存到常量池中。這個字節碼包含了指向常量池的引用,在動態鏈接時會使用到運行時常量池。
常量池中包含了以下的幾類信息:
- 數量值
- 字符串值
- 類引用
- 字段引用
- 方法引用
總之,常量池可以看做是一張表,虛擬機指令根據這張表來找到要執行的類名、方法名、參數類型和字面量等信息。
4.2 運行時常量池
運行時常量池(Runtime Constant Pool)是方法區的一部分。前面說到的常量池是字節碼文件的一部分,它用於存放編譯期生成的各種字面量和符號引用,而這部分內容將在類加載後存放到方法區的運行時常量池中。
JVM會爲每一個已加載的類型(類或接口)維護一個常量池,池中的數據項像數組項一樣,可以通過索引進行訪問。運行時常量池中包含多種不同的常量,包括編譯期就已經明確的數值字面量,也包括到運行期解析後才能夠獲得的方法或是字段引用,此時不再是常量池中的符號地址,而是轉換後的真實地址。
當創建類或接口的運行時常量池時,如果構造運行時常量池所需的內存空間超過了方法區所能提供的最大值,則JVM會拋出OOM異常。
5. 演進過程
永久代只存在於HotSpot虛擬機的JDK7極其之前的版本,JDK8和之後的版本以及其他類型的虛擬機中並沒有永久代的概念。
針對於HotSpot虛擬機來說,方法區經過了持續的演變,主要過程爲:
JDK1.6及之前 | 有永久代,靜態變量存放在永久代上 |
---|---|
JDK1.7 | 有永久代,但已經逐步“去永久代”,字符串常量池、靜態變量保存在堆中 |
JDK1.8及之後 | 無永久代,類型信息、字段、方法、常量保存在本地內存的元空間,但字符串常量池和靜態變量仍在堆中 |
那麼,爲什麼要去永久代,或是爲什麼要適用元空間來代替永久代呢?主要的原因有:
- 爲永久代設置空間大小是很難確定:某些場景下,如果動態加載的類過多,容易產生永久代區的OOM。元空間並不在虛擬機中,而是使用本地內存。因此,默認情況下元空間的大小僅受本地內存限制
- 對永久代進行調優很困難
JDK1.7及之後將字符串常量池轉移到了堆中,這是爲什麼呢?前面提到,對於永久代的垃圾回收效率很低,在Full GC的時候纔會觸發。而只有在老年代的空間不足時,Full GC纔會被觸發,永久代不足並不是觸發的條件。這就導致了字符串常量的回收效率不高,而在實際的使用中又會大量的用到字符串。因此如果回收效率低,將導致永久代內存不足,而將其放到堆中能做到及時回收。
6. 垃圾收集
對於方法區的垃圾收集,Java虛擬機規範並沒有做強制性的要求。如果要進行方法區的垃圾收集,它主要回收兩部分內容:
- 常量池中廢棄的常量
- 不再使用的類型
對於HotSpot虛擬機來說,只要常量池中的常量沒有被任何其他的地方引用,就可以被回收。而判斷一個類型是否屬於不再被引用的類需要滿足三個條件:
- 該類的所有實例都已經被回收,即Java堆中不存在該類及其任何派生子類的實例
- 加載該類的類加載器已經被回收
- 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法
7. 使用案例
下面通過例子來看一下方法區在程序運行過程中是如何被使用的。假設代碼如下所示:
public class MethodAreaDemo {
public static void main(String[] args) {
int x = 500;
int y = 100;
int a = x / y;
int b = 50;
System.out.println(a + b);
}
}
將其編譯後再反編譯得到對應的字節碼指令爲:
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
然後我們通過圖解的方法看一下每條指令的執行過程,如下所示:
-
程序中包含args、x、y、a和b四個變量,其中args存放在局部變量表的0號位置。執行
int x = 500;
,對應0號指令,將500壓入操作數棧
-
執行3號指令,將棧頂的500存到局部變量表
-
執行4號指令,將100壓棧
-
執行6號指令,將棧頂的100 存入局部變量表
-
執行7號和8號指令,分別從局部變量表中讀取序號爲1和2的元素,並將它們壓入操作數棧
-
執行9號和10號指令,兩數相除,並將結果壓入操作數棧,最後將棧頂元素存入局部變量表
-
執行11和12號指令,將50壓棧,然後再存放到局部變量表
-
執行15號指令,獲取類或接口字段的值,並將其壓棧
-
執行18、19、21號指令,取數並執行相加操作,將結果壓棧,最後取棧頂元素存放到局部變量表
-
執行22號指令
- 最後執行25號指令,void函數返回,main方法執行結束