一、簡介
方法區在JVM中也是一個非常重要的區域,它與堆一樣,是被線程共享的區域。在方法區中,存儲了每個類的信息(包括類的名稱、方法信息、字段信息)、靜態變量、常量以及編譯器編譯后的代碼等。
二、方法區結構
2.1、先看classLoader是如何加載class文件和存儲文件信息的
當一個classLoder啟動的時候,classLoader的生存地點在jvm中的堆,然后它會去主機硬盤上將A.class裝載到jvm的方法區,方法區中的這個字節文件會被虛擬機拿來new A字節碼(),然后在堆內存生成了一個A字節碼的對象,然后A字節碼這個內存文件有兩個引用一個指向A的class對象,一個指向加載自己的classLoader。那么方法區中的字節碼內存塊,除了記錄一個class自己的class對象引用和一個加載自己的ClassLoader引用之外,還記錄了什么信息呢??見下圖:
2.2、方法區關鍵信息介紹
1.類信息:修飾符(public final)
是類還是接口(class,interface)
類的全限定名(Test/ClassStruct.class)
直接父類的全限定名(java/lang/Object.class)
直接父接口的權限定名數組(java/io/Serializable)
也就是 public final class ClassStruct extends Object implements Serializable這段描述的信息提取
2.字段信息:修飾符(pirvate)
字段類型(java/lang/String.class)
字段名(name)
也就是類似private String name;這段描述信息的提取
3.方法信息:修飾符(public static final)
方法返回值(java/lang/String.class)
方法名(getStatic_str)
參數需要用到的局部變量的大小還有操作數棧大小(操作數棧我們后面會講)
方法體的字節碼(就是花括號里的內容)
異常表(throws Exception)
也就是對方法public static final String getStatic_str ()throws Exception的字節碼的提取
4.常量池:
4.1.直接常量:
1.1CONSTANT_INGETER_INFO整型直接常量池public final int CONST_INT=0;
1.2CONSTANT_String_info字符串直接常量池 public final String CONST_STR="CONST_STR";
1.3CONSTANT_DOUBLE_INFO浮點型直接常量池
等等各種基本數據類型基礎常量池(待會我們會反編譯一個類,來查看它的常量池等。)
4.2.方法名、方法描述符、類名、字段名,字段描述符的符號引用
也就是所有編譯器能夠被確定,能夠被快速查找的內容都存放在這里,它像數組一樣通過索引訪問,就是專門用來做查找的。
編譯時就能確定數值的常量類型都會復制它的所有常量到自己的常量池中,或者嵌入到它的字節碼流中。作為常量池或者字節碼流的一部分,編譯時常量保存在方法區中,就和一般的類變量一樣。但是當一般的類變量作為他們的類型的一部分數據而保存的時候,編譯時常量作為使用它們的類型的一部分而保存
5.類變量(靜態變量):
就是靜態字段( public static String static_str="static_str";)
虛擬機在使用某個類之前,必須在方法區為這些類變量分配空間。
6.一個到classLoader的引用,通過this.getClass().getClassLoader()來取得為什么要先經過class呢?思考一下,看一下上面的圖,再回來思考。(class A 對象擁有A字節碼和加載它的加載器地址引用)
7.一個到class對象的引用,這個對象存儲了所有這個字節碼內存塊的相關信息。所有你能夠看到的區域,比如:類信息,你可以通過this.getClass().getName()取得
所有的方法信息,可以通過this.getClass().getDeclaredMethods(),字段信息可以通過this.getClass().getDeclaredFields(),等等,所有在字節碼中你想得到的,調用的,通過class這個引用基本都能夠幫你完成。因為他就是字節碼在內存塊在堆中的一個對象
8.方法表,如果學習c++的人應該都知道c++的對象內存模型有一個叫虛表的東西,java本來的名字就叫c++- -,它的方法表其實說白了就是c++的虛表,它的內容就是這個類的所有實例可能被調用的所有實例方法的直接引用。也是為了動態綁定的快速定位而做的一個類似緩存的查找表,它以數組的形式存在於內存中。不過這個表不是必須存在的,取決於虛擬機的設計者,以及運行虛擬機的機器是否有足夠的內存。
三、方法區與永久代的關系
很多文章里喜歡把方法區等同與永久代,永久代既然沒了,方法區也就沒了。但我認為方法區只是一種邏輯上的概念,永久代指物理上的堆內存的一塊空間,這塊實際的空間完成了方法區存儲字節碼、靜態變量、常量的功能等等。既然如此,現在元空間也可以認為是新的方法區的實現了。
方法區也叫永久代。在過去(自定義類加載器還不是很常見的時候),類大多是”static”的,很少被卸載或收集,因此被稱為“永久的(Permanent)”。雖然Java 虛擬機規范把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是與Java 堆區分開來。同時,由於類class是JVM實現的一部分,並不是由應用創建的,所以又被認為是“非堆(non-heap)”內存。HotSpot 虛擬機的設計團隊選擇把GC 分代收集擴展至方法區,或者說使用永久代來實現方法區而已。對於其他虛擬機(如BEA JRockit、IBM J9 等)來說是不存在永久代的概念的。
永久代也是各個線程共享的區域,它用於存儲已經被虛擬機加載過的類信息,常量,靜態變量(JDK7中被移到Java堆),即時編譯期編譯后的代碼(類方法)等數據。這里要講一下運行時常量池,它是方法區的一部分,用於存放編譯期生成的各種字面量和符號引用(其實就是八大基本類型的包裝類型和String類型數據(JDK7中被移到Java堆))(官方文檔說明: In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application)。
在JDK1.7中的HotASpot中,已經把原本放在方法區的字符串常量池移到java heap中。
- 將interned String移到Java堆中(示例見《JVM體系結構之七:持久代、元空間(Metaspace) 常量池==了解String類的intern()方法、常量池介紹、常量池從Perm-->Heap》中的示例)
- 將符號Symbols移到native memory(不受GC管理的內存)
從JDK7開始永久代的移除工作,貯存在永久代的一部分數據已經轉移到了Java Heap或者是Native Heap。但永久代仍然存在於JDK7,並沒有完全的移除:符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了java heap;類的靜態變量(class statics)轉移到了java heap。隨着JDK8的到來,JVM不再有PermGen。但類的元數據信息(metadata)還在,只不過不再是存儲在連續的堆空間上,而是移動到叫做“Metaspace”的本地內存(Native memory)中。
四、jdk1.6,jdk1.7,jdk1.8變化
常量池主要可以分為以下幾種:
(1)靜態常量池:即*.class文件中的常量池,class文件中的常量池不僅僅包含字符串/數字這些字面量,還包含類、方法的信息,占用class文件絕大部分空間。這種常量池主要用於存放兩大類常量:字面量和符號引用量,字面量相當於Java語言層面常量的概念,如文本字符串,聲明為final的常量值等;符號引用則屬於編譯原理 方面的概念,包括了如下三種類型的常量:類和接口的全限定名、字段名稱描述符、方法名稱描述符。
類的加載過程中的鏈接部分的解析步驟就是把符號引用替換為直接引用,即把那些描述符(名字)替換為能直接定位到字段、方法的引用或句柄(地址)。
(2)運行時常量池:虛擬機會將各個class文件中的常量池載入到運行時常量池中,即編譯期間生成的字面量、符號引用,總之就是裝載class文件。
(3)字符串常量池 :字符串常量池可以理解為運行時常量池分出來的部分。加載時,對於class的靜態常量池,如果字符串會被裝到字符串常量池中。
(4)整型常量池:Integer,類似字符串常量池,可以理解為運行時常量池分出來的。加載時,對於class的靜態常量池裝的東西,如果是整型會被裝到整型常量池中。
類似的還有Character、Long等等常量池(基本數據類型沒有哦)。。。
在永久代移除后,字符串常量池也不再放在永久代了,但是也沒有放到新的方法區---元空間里,而是留在了堆里(為了方便回收?)。運行時常量池當然是隨着搬家到了元空間里,畢竟它是裝靜態變量、字節碼等信息的,有它的地方才稱得上方法區。
常量池,在jdk1.7,jdk1.8遷移從永久代遷移到heap堆中。見《方法區之三:運行時常量池==了解String類的intern()方法、常量池介紹、常量池從Perm-->Heap》
五、垃圾回收
jdk1.6
常量池如何觸發的垃圾回收?
Class文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量表(constant_pool table),用於存放編譯期已可知的常量,這部分內容將在類加載后進入方法區(永久代)存放。但是Java語言並不要求常量一定只有編譯期預置入Class的常量表的內容才能進入方法區常量池,運行期間也可將新內容放入常量池(最典型的String.intern()方法)。
GC的作用主要是用來卸載類和回收常量池,當然有部分方法區,即使永久代(Perm Space)也會一定的回收
jdk1.7,1.8
Metaspace 垃圾回收:
對於僵死的類及類加載器的垃圾回收將在元數據使用到“MaxMetaspaceSize”參數的設定值時進行。
適時地監控和調整元空間對於減小垃圾回收頻率和減少延時是很有必要的。持續的元空間垃圾回收說明,可能存在類、類加載器導致的內存泄漏或是大小設置不合適。