方法區(qū)
棧,堆,方法區(qū)的交互關(guān)系
從線程共享與否的角度來看
ThreadLocal:如何保證多個(gè)線程在并發(fā)環(huán)境下的安全性?典型場景就是數(shù)據(jù)庫連接管理,以及會(huì)話管理。

堆,棧,方法區(qū)的交互關(guān)系
下面涉及了對象的訪問定位
(1)Person類的.class信息存放在方法區(qū)中;
(2)person變量存放在Java棧的局部變量表中
(3)真正的person對象存放在java堆中
(4)在person對象中,有個(gè)指針指向方法區(qū)中person類型數(shù)據(jù),表明這個(gè)person對象使用方法區(qū)中的Person類new出來的。

方法區(qū)的理解
官方文檔:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4
方法區(qū)在哪里?

方法區(qū)的理解
方法區(qū)主要存放的是Class,而堆中主要存放的是實(shí)例化的對象
(1)方法區(qū)(Method Area)與Java堆一樣,線程共享的內(nèi)存區(qū)域。多個(gè)線程同時(shí)加載統(tǒng)一類時(shí),只能有一個(gè)線程能加載該類,其他線程只能等待該線程加載完畢,然后直接使用該類,即類只能加載一次。
(2)方法區(qū)在JVM啟動(dòng)的時(shí)候被創(chuàng)建,并且它的實(shí)際物理內(nèi)存空間中和Java堆區(qū)一樣都是可以不連續(xù)的
(3)方法區(qū)的大小,跟堆空間一樣,可以選擇固定大小或者可擴(kuò)展。
(4)方法區(qū)的大小決定了系統(tǒng)可以保存多少個(gè)類,如果系統(tǒng)定義了太多了類,導(dǎo)致方法區(qū)溢出,虛擬機(jī)同樣會(huì)拋出內(nèi)存溢出錯(cuò)誤:java.lang.OutofMemoryError:PermGen space或者java.lang.OutOfMemoryError:Metaspace
加載大量的第三方j(luò)ar包
tomcat部署的工程過多(30-50個(gè))
大量動(dòng)態(tài)的生成反射類
(5)關(guān)閉JVM就會(huì)釋放這個(gè)區(qū)域的內(nèi)存
代碼舉例
public class MethodAreaDemo { public static void main(String[] args) { System.out.println("start..."); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("end..."); } }
簡單的程序,加載了1600多個(gè)類

HotSpot方法區(qū)演進(jìn)
(1)在JDK7及以前,習(xí)慣上把方法區(qū),稱為永久代。在JDK8開始,使用元空間取代了永久代,我們可以將方法區(qū)類比為Java中的接口,將永久代或元空間類比為Java中具體的實(shí)現(xiàn)類
(2)本質(zhì)上,方法區(qū)和永久代并不等價(jià)。僅是對Hotspot而言的可以看作等價(jià)。《Java虛擬機(jī)規(guī)范》對如何實(shí)現(xiàn)方法區(qū),不做統(tǒng)一要求。例如:BEAJRockit / IBM J9 中不存在永久代的概念。
現(xiàn)在來看,當(dāng)年使用永久代,不是好的idea。導(dǎo)致Java程序更容易OOm(超過-XX:MaxPermsize上限)
(3)而到了JDK8,終于完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地內(nèi)存中實(shí)現(xiàn)的元空間(Metaspace)來代替
(4)元空間的本質(zhì)和永久代類似,都是JVM規(guī)范中方法區(qū)的實(shí)現(xiàn),不過元空間與永久代最大的區(qū)別在于:元空間不在虛擬機(jī)設(shè)置的內(nèi)存中,而是使用本地內(nèi)存;
(5)永久代,元空間二者并不是名字改變了,內(nèi)存結(jié)構(gòu)也調(diào)整了
(6)根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,如果方法區(qū)無法滿足新的內(nèi)存分配需求時(shí),將拋出OOM異常

設(shè)置方法區(qū)的大小與OOM
方法區(qū)的大小不必是固定的,JVM可以根據(jù)應(yīng)用的需要?jiǎng)討B(tài)調(diào)整
JDK7及以前(永久代)
(1)通過-XX:Permsize來設(shè)置永久代初始分配空間,默認(rèn)值是20.75M
(2)-XX:MaxPermsize來設(shè)置永久代最大可分配空間。32位機(jī)器默認(rèn)是64M,64位機(jī)器模式是82M.
(3)當(dāng)JVM加載的類信息超過了這個(gè)值,會(huì)報(bào)異常OutofMemoryError:PermGen space。

JDK8及以后(元空間)
JDK 8版本設(shè)置元空間大小
(1)元數(shù)據(jù)區(qū)大小可以使用參數(shù) -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 指定
(2)默認(rèn)值依賴于平臺(tái),Windows下,-XX:MetaspaceSize 約為21M,-XX:MaxMetaspaceSize的值是-1,即沒有限制。
(3)與永久代不同,如果不指定大小,默認(rèn)情況下,虛擬機(jī)會(huì)耗盡所有的可用系統(tǒng)內(nèi)存,如果元數(shù)據(jù)區(qū)發(fā)生溢出,虛擬機(jī)一樣會(huì)拋出異常OutOfMemoryError:Metaspace
(4)-XX:MetaspaceSize:設(shè)置初始的元空間大小。對于一個(gè) 64位 的服務(wù)器端 JVM 來說,其默認(rèn)的 -XX:MetaspaceSize值為21MB。這就是初始的高水位線,一旦觸及這個(gè)水位線,F(xiàn)ull GC將會(huì)被觸發(fā)并卸載沒用的類(即這些類對應(yīng)的類加載器不再存活),然后這個(gè)高水位線將會(huì)重置。新的高水位線的值取決于GC后釋放了多少元空間。如果釋放的空間不足,那么在不超過MaxMetaspaceSize時(shí),適當(dāng)提高該值。如果釋放空間過多,則適當(dāng)降低該值。
(5)如果初始化的高水位線設(shè)置過低,上述高水位線調(diào)整情況會(huì)發(fā)生很多次。通過垃圾回收器的日志可以觀察到Full GC多次調(diào)用。為了避免頻繁地GC,建議將-XX:MetaspaceSize設(shè)置為一個(gè)相對較高的值
方法區(qū)OOM
舉例:
代碼:OOMTest類繼承ClassLoader類,獲得defineClass()方法,可自己進(jìn)行類的加載
/** * jdk6/7中: * -XX:PermSize=10m -XX:MaxPermSize=10m * * jdk8中: * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m * */ public class OOMTest extends ClassLoader { public static void main(String[] args) { int j = 0; try { OOMTest test = new OOMTest(); for (int i = 0; i < 10000; i++) { //創(chuàng)建ClassWriter對象,用于生成類的二進(jìn)制字節(jié)碼 ClassWriter classWriter = new ClassWriter(0); //指明版本號(hào),修飾符,類名,包名,父類,接口 classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); //返回byte[] byte[] code = classWriter.toByteArray(); //類的加載 test.defineClass("Class" + i, code, 0, code.length);//Class對象 j++; } } finally { System.out.println(j); } } }
不設(shè)置元空間的上限
使用默認(rèn)的JVM參數(shù),元空間不設(shè)置上限
輸出結(jié)果
10000
設(shè)置元空間的上限
JVM參數(shù)
-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
輸出結(jié)果
3331 Exception in thread "main" java.lang.OutOfMemoryError: Compressed class space at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:756) at java.lang.ClassLoader.defineClass(ClassLoader.java:635) at com.example.demo.methodarea.OOMTest.main(OOMTest.java:27)
如何解決OOM
這個(gè)屬于調(diào)優(yōu)的問題,這里先簡單說下
(1)要解決OOM異常或heap space的異常,一般的手段是首先通過內(nèi)存映像分析工具(如Ec1ipse Memory Analyzer)對dump出來的堆轉(zhuǎn)儲(chǔ)快照進(jìn)行分析,重點(diǎn)是確認(rèn)內(nèi)存中的對象是否是必要的,也就是要先分清楚到底是出現(xiàn)了內(nèi)存泄漏(Memory Leak)還是內(nèi)存溢出(Memory Overflow)
(2)內(nèi)存泄漏就是有大量的引用指向某些對象,但是這些對象以后不會(huì)使用了,但是因?yàn)樗鼈冞€和GC ROOT有關(guān)聯(lián),所以導(dǎo)致以后這些對象也不會(huì)被回收,這就是內(nèi)存泄漏的問題
(3)如果是內(nèi)存泄漏,可進(jìn)一步通過工具查看泄漏對象到GC Roots的引用鏈。于是就能找到泄漏對象是通過怎樣的路徑與GC Roots相關(guān)聯(lián)并導(dǎo)致垃圾收集器無法自動(dòng)回收它們的。掌握了泄漏對象的類型信息,以及GC Roots引用鏈的信息,就可以比較準(zhǔn)確地定位出泄漏代碼的位置。
(4)如果不存在內(nèi)存泄漏,換句話說就是內(nèi)存中的對象確實(shí)都還必須存活著,那就應(yīng)當(dāng)檢查虛擬機(jī)的堆參數(shù)(-Xmx與-Xms),與機(jī)器物理內(nèi)存對比看是否還可以調(diào)大,從代碼上檢查是否存在某些對象生命周期過長、持有狀態(tài)時(shí)間過長的情況,嘗試減少程序運(yùn)行期的內(nèi)存消耗
方法區(qū)的內(nèi)部結(jié)構(gòu)
方法區(qū)存儲(chǔ)什么?
概念

《深入理解Java虛擬機(jī)》書中對方法區(qū)(Method Area)存儲(chǔ)內(nèi)容描述如下:它用于存儲(chǔ)已被虛擬機(jī)加載的類型信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼緩存等。

類型信息
對每個(gè)加載的類型(類class,接口interface,枚舉enum,注解annotation),JVM必須在方法區(qū)中存儲(chǔ)以下類型信息:
(1)這個(gè)類型的完整的有效名稱(全名=包名.類名)
(2)這個(gè)類型直接父類的完整有效名(對于interface或者是java.lang.Object,都沒有父類)
(3)這個(gè)類型的修飾符(public,abstract,final的某個(gè)子集)
(4)這個(gè)類型直接接口的一個(gè)有序列表
域(Field)信息
也就是我們常說的成員變量,域信息是比較官方的說法
(1)JVM必須在方法區(qū)中保存類型的所有域的相關(guān)信息以及域的聲明順序
(2)域的相關(guān)信息包括:域名稱,域類型,域修飾符(public,private,protected,static,final,volatile,transient的某個(gè)子集)
方法(Method)信息
JVM必須保存所有方法的以下信息,同域信息一樣包括聲明順序:
** * 測試方法區(qū)的內(nèi)部構(gòu)成 */ public class MethodInnerStrucTest extends Object implements Comparable<String>,Serializable { //屬性 public int num = 10; private static String str = "測試方法的內(nèi)部結(jié)構(gòu)"; //構(gòu)造器 //方法 public void test1(){ int count = 20; System.out.println("count = " + count); } public static int test2(int cal){ int result = 0; try { int value = 30; result = value / cal; } catch (Exception e) { e.printStackTrace(); } return result; } @Override public int compareTo(String o) { return 0; } }
javap -v -p MethodInnerStrucTest.class > test.txt
反編譯字節(jié)碼文件,并輸出值文本文件中,便于查看。參數(shù)-p確保能查看private權(quán)限類型的字段或方法
字節(jié)碼:
Classfile /D:/practice/test01/demo/target/classes/com/example/demo/methodarea/MethodInnerStrucTest.class Last modified 2024-7-1; size 1700 bytes MD5 checksum 2089e2caed2dedbaa294a7446722b38b Compiled from "MethodInnerStrucTest.java" public class com.example.demo.methodarea.MethodInnerStrucTest extends java.lang.Object implements java.lang.Comparable<java.lang.String>, java.io.Serializable minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #18.#53 // java/lang/Object."<init>":()V #2 = Fieldref #17.#54 // com/example/demo/methodarea/MethodInnerStrucTest.num:I #3 = Fieldref #55.#56 // java/lang/System.out:Ljava/io/PrintStream; #4 = Class #57 // java/lang/StringBuilder #5 = Methodref #4.#53 // java/lang/StringBuilder."<init>":()V #6 = String #58 // count = #7 = Methodref #4.#59 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #8 = Methodref #4.#60 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; #9 = Methodref #4.#61 // java/lang/StringBuilder.toString:()Ljava/lang/String; #10 = Methodref #62.#63 // java/io/PrintStream.println:(Ljava/lang/String;)V #11 = Class #64 // java/lang/Exception #12 = Methodref #11.#65 // java/lang/Exception.printStackTrace:()V #13 = Class #66 // java/lang/String #14 = Methodref #17.#67 // com/example/demo/methodarea/MethodInnerStrucTest.compareTo:(Ljava/lang/String;)I #15 = String #68 // 測試方法的內(nèi)部結(jié)構(gòu) #16 = Fieldref #17.#69 // com/example/demo/methodarea/MethodInnerStrucTest.str:Ljava/lang/String; #17 = Class #70 // com/example/demo/methodarea/MethodInnerStrucTest #18 = Class #71 // java/lang/Object #19 = Class #72 // java/lang/Comparable #20 = Class #73 // java/io/Serializable #21 = Utf8 num #22 = Utf8 I #23 = Utf8 str #24 = Utf8 Ljava/lang/String; #25 = Utf8 <init> #26 = Utf8 ()V #27 = Utf8 Code #28 = Utf8 LineNumberTable #29 = Utf8 LocalVariableTable #30 = Utf8 this #31 = Utf8 Lcom/example/demo/methodarea/MethodInnerStrucTest; #32 = Utf8 test1 #33 = Utf8 count #34 = Utf8 test2 #35 = Utf8 (I)I #36 = Utf8 value #37 = Utf8 e #38 = Utf8 Ljava/lang/Exception; #39 = Utf8 cal #40 = Utf8 result #41 = Utf8 StackMapTable #42 = Class #64 // java/lang/Exception #43 = Utf8 MethodParameters #44 = Utf8 compareTo #45 = Utf8 (Ljava/lang/String;)I #46 = Utf8 o #47 = Utf8 (Ljava/lang/Object;)I #48 = Utf8 <clinit> #49 = Utf8 Signature #50 = Utf8 Ljava/lang/Object;Ljava/lang/Comparable<Ljava/lang/String;>;Ljava/io/Serializable; #51 = Utf8 SourceFile #52 = Utf8 MethodInnerStrucTest.java #53 = NameAndType #25:#26 // "<init>":()V #54 = NameAndType #21:#22 // num:I #55 = Class #74 // java/lang/System #56 = NameAndType #75:#76 // out:Ljava/io/PrintStream; #57 = Utf8 java/lang/StringBuilder #58 = Utf8 count = #59 = NameAndType #77:#78 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #60 = NameAndType #77:#79 // append:(I)Ljava/lang/StringBuilder; #61 = NameAndType #80:#81 // toString:()Ljava/lang/String; #62 = Class #82 // java/io/PrintStream #63 = NameAndType #83:#84 // println:(Ljava/lang/String;)V #64 = Utf8 java/lang/Exception #65 = NameAndType #85:#26 // printStackTrace:()V #66 = Utf8 java/lang/String #67 = NameAndType #44:#45 // compareTo:(Ljava/lang/String;)I #68 = Utf8 測試方法的內(nèi)部結(jié)構(gòu) #69 = NameAndType #23:#24 // str:Ljava/lang/String; #70 = Utf8 com/example/demo/methodarea/MethodInnerStrucTest #71 = Utf8 java/lang/Object #72 = Utf8 java/lang/Comparable #73 = Utf8 java/io/Serializable #74 = Utf8 java/lang/System #75 = Utf8 out #76 = Utf8 Ljava/io/PrintStream; #77 = Utf8 append #78 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #79 = Utf8 (I)Ljava/lang/StringBuilder; #80 = Utf8 toString #81 = Utf8 ()Ljava/lang/String; #82 = Utf8 java/io/PrintStream #83 = Utf8 println #84 = Utf8 (Ljava/lang/String;)V #85 = Utf8 printStackTrace { public int num; descriptor: I flags: ACC_PUBLIC private static java.lang.String str; descriptor: Ljava/lang/String; flags: ACC_PRIVATE, ACC_STATIC public com.example.demo.methodarea.MethodInnerStrucTest(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: bipush 10 7: putfield #2 // Field num:I 10: return LineNumberTable: line 10: 0 line 12: 4 LocalVariableTable: Start Length Slot Name Signature 0 11 0 this Lcom/example/demo/methodarea/MethodInnerStrucTest; public void test1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=2, args_size=1 0: bipush 20 2: istore_1 3: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 6: new #4 // class java/lang/StringBuilder 9: dup 10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V 13: ldc #6 // String count = 15: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 18: iload_1 19: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 22: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 25: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 28: return LineNumberTable: line 17: 0 line 18: 3 line 19: 28 LocalVariableTable: Start Length Slot Name Signature 0 29 0 this Lcom/example/demo/methodarea/MethodInnerStrucTest; 3 26 1 count I public static int test2(int); descriptor: (I)I flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: iconst_0 1: istore_1 2: bipush 30 4: istore_2 5: iload_2 6: iload_0 7: idiv 8: istore_1 9: goto 17 12: astore_2 13: aload_2 14: invokevirtual #12 // Method java/lang/Exception.printStackTrace:()V 17: iload_1 18: ireturn Exception table: from to target type 2 9 12 Class java/lang/Exception LineNumberTable: line 21: 0 line 23: 2 line 24: 5 line 27: 9 line 25: 12 line 26: 13 line 28: 17 LocalVariableTable: Start Length Slot Name Signature 5 4 2 value I 13 4 2 e Ljava/lang/Exception; 0 19 0 cal I 2 17 1 result I StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 12 locals = [ int, int ] stack = [ class java/lang/Exception ] frame_type = 4 /* same */ MethodParameters: Name Flags cal public int compareTo(java.lang.String); descriptor: (Ljava/lang/String;)I flags: ACC_PUBLIC Code: stack=1, locals=2, args_size=2 0: iconst_0 1: ireturn LineNumberTable: line 33: 0 LocalVariableTable: Start Length Slot Name Signature 0 2 0 this Lcom/example/demo/methodarea/MethodInnerStrucTest; 0 2 1 o Ljava/lang/String; MethodParameters: Name Flags o public int compareTo(java.lang.Object); descriptor: (Ljava/lang/Object;)I flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: aload_1 2: checkcast #13 // class java/lang/String 5: invokevirtual #14 // Method compareTo:(Ljava/lang/String;)I 8: ireturn LineNumberTable: line 10: 0 LocalVariableTable: Start Length Slot Name Signature 0 9 0 this Lcom/example/demo/methodarea/MethodInnerStrucTest; MethodParameters: Name Flags o synthetic static {}; descriptor: ()V flags: ACC_STATIC Code: stack=1, locals=0, args_size=0 0: ldc #15 // String 測試方法的內(nèi)部結(jié)構(gòu) 2: putstatic #16 // Field str:Ljava/lang/String; 5: return LineNumberTable: line 13: 0 } Signature: #50 // Ljava/lang/Object;Ljava/lang/Comparable<Ljava/lang/String;>;Ljava/io/Serializable; SourceFile: "MethodInnerStrucTest.java"
類型信息
在運(yùn)行時(shí)方法區(qū)中,類信息中記錄了哪個(gè)加載器加載了該類,同時(shí)類加載器也記錄了它加載了哪些類
//類型信息 public class com.example.demo.methodarea.MethodInnerStrucTest extends java.lang.Object implements java.lang.Comparable<java.lang.String>, java.io.Serializable
域信息
(1)descriptor: I 表示字段類型為Integer
(2)flags: ACC_PUBLIC 表示字段權(quán)限修飾符為public
public int num; descriptor: I flags: ACC_PUBLIC private static java.lang.String str; descriptor: Ljava/lang/String; flags: ACC_PRIVATE, ACC_STATIC
方法信息:
(1)descriptor: ()V 表示方法返回值類型為 void
(2)flags: ACC_PUBLIC 表示方法權(quán)限修飾符為 public
(3)stack=3 表示操作數(shù)棧深度為 3
(4)locals=2 表示局部變量個(gè)數(shù)為 2 個(gè)(實(shí)力方法包含 this)
(5)test1() 方法雖然沒有參數(shù),但是其 args_size=1 ,這時(shí)因?yàn)閷?this 作為了參數(shù)
public void test1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=2, args_size=1 0: bipush 20 2: istore_1 3: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 6: new #4 // class java/lang/StringBuilder 9: dup 10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V 13: ldc #6 // String count = 15: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 18: iload_1 19: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 22: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 25: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 28: return LineNumberTable: line 17: 0 line 18: 3 line 19: 28 LocalVariableTable: Start Length Slot Name Signature 0 29 0 this Lcom/atguigu/java/MethodInnerStrucTest; 3 26 1 count I
non-final類型的類變量
(1)靜態(tài)變量和類關(guān)聯(lián)在一起,隨著類的加載而加載,他們成為類數(shù)據(jù)在邏輯的一部分
(2)類變量被類的所有實(shí)例共享,即使沒有類實(shí)例時(shí),也可以訪問
舉例
(1)如下,即使我們把order設(shè)置為null,也不會(huì)出現(xiàn)空指針異常
(2)這更加表明了static類型的字段和方法隨著類的加載而加載,并不屬于特定的類實(shí)例
public class MethodAreaTest { public static void main(String[] args) { Order order = null; order.hello(); 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!"); } }
輸出結(jié)果:
hello! 1
全局常量:static final
(1)全局常量就是使用 static final 進(jìn)行修飾
(2)被聲明為final的類變量的處理方法則不同,每個(gè)全局常量在編譯的時(shí)候就會(huì)被分配了。
查看上面的代碼,這部分的字節(jié)碼指令:
class Order { public static int count = 1; public static final int number = 2; ... }
public static int count; descriptor: I flags: ACC_PUBLIC, ACC_STATIC public static final int number; descriptor: I flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL ConstantValue: int 2
可以發(fā)現(xiàn)static和final同時(shí)修飾的numer的值在編譯上時(shí)候已經(jīng)寫死在字節(jié)碼文件中了
運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池 VS 常量池
官方文檔:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

(1)方法區(qū),內(nèi)部包含了運(yùn)行時(shí)常量池
(2)字節(jié)碼文件,內(nèi)部包含了常量池。(之前的字節(jié)碼文件中已經(jīng)看到了很多Constant pool的東西,這個(gè)就是常量池)
(3)要弄清楚方法區(qū),需要理解清楚ClassFile,因?yàn)榧虞d類的信息都在方法區(qū)。
(4)要弄清楚方法區(qū)的運(yùn)行時(shí)常量池,需要理解清楚ClassFile中的常量池。
常量池
(1)一個(gè)有效的字節(jié)碼文件中除了包含類的版本信息、字段、方法以及接口等描述符信息外。還包含一項(xiàng)信息就是常量池表(Constant Pool Table),包括各種字面量和對類型、域和方法的符號(hào)引用。
(2)字面量: 10 , “我是某某”這種數(shù)字和字符串都是字面量

為什么需要常量池
(1)一個(gè)java源文件中的類、接口,編譯后產(chǎn)生一個(gè)字節(jié)碼文件。而Java中的字節(jié)碼需要數(shù)據(jù)支持,通常這種數(shù)據(jù)會(huì)很大以至于不能直接存到字節(jié)碼里,換另一種方式,可以存到常量池。這個(gè)字節(jié)碼包含了指向常量池的引用。在動(dòng)態(tài)鏈接的時(shí)候會(huì)用到運(yùn)行時(shí)常量池,之前有介紹
比如:如下的代碼:
public class SimpleClass { public void sayHello() { System.out.println("hello"); } }
(1)雖然上述代碼只有194字節(jié),但是里面卻使用了String、System、PrintStream及Object等結(jié)構(gòu)。
(2)比如說我們這個(gè)文件中有6個(gè)地方用到了”hello”這個(gè)字符串,如果不用常量池,就需要在6個(gè)地方全寫一遍,造成臃腫。我們可以將”hello”等所需用到的結(jié)構(gòu)信息記錄在常量池中,并通過引用的方式,來加載、調(diào)用所需的結(jié)構(gòu)
(3)這里的代碼量其實(shí)很少了,如果代碼多的話,引用的結(jié)構(gòu)將會(huì)更多,這里就需要用到常量池了。
常量池中有啥
數(shù)量值,字符串值,類引用,字段引用,方法引用
MethodInnerStrucTest 的 test1方法的字節(jié)碼
0 bipush 20 2 istore_1 3 getstatic #3 <java/lang/System.out> 6 new #4 <java/lang/StringBuilder> 9 dup 10 invokespecial #5 <java/lang/StringBuilder.<init>> 13 ldc #6 <count = > 15 invokevirtual #7 <java/lang/StringBuilder.append> 18 iload_1 19 invokevirtual #8 <java/lang/StringBuilder.append> 22 invokevirtual #9 <java/lang/StringBuilder.toString> 25 invokevirtual #10 <java/io/PrintStream.println> 28 return
#3,#5等等這些帶# 的,都是引用了常量池。
運(yùn)行時(shí)常量池
(1)運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分
(2)常量池表(Constant Pool Table)是Class字節(jié)碼文件的一部分,用于存放編譯期生成的各種字面量與符號(hào)引用,這部分內(nèi)容在類加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中。(運(yùn)行時(shí)常量池就是常量池在程序運(yùn)行時(shí)的稱呼)
(3)運(yùn)行時(shí)常量池,在加載類和接口到虛擬機(jī)后,就會(huì)創(chuàng)建對應(yīng)的運(yùn)行時(shí)常量池
(4)JVM為每個(gè)已加載的類型(類或接口)都維護(hù)一個(gè)常量池,池中的數(shù)據(jù)項(xiàng)像數(shù)組項(xiàng)一樣,都是通過索引訪問的。
(5)運(yùn)行時(shí)常量池中包含多種不同的常量,包括編譯期就已經(jīng)明確的數(shù)值字面量,也包括到運(yùn)行期解析后才能夠獲得的方法或者字段引用。此時(shí)不再是常量池中的符號(hào)地址了,這里換為真實(shí)地址
運(yùn)行時(shí)常量池,相對于Class文件常量池的另一重要特征是:具備動(dòng)態(tài)性
(6)運(yùn)行時(shí)常量池類似于傳統(tǒng)編程語言中的符號(hào)表(symbol table),但是它所包含的數(shù)據(jù)卻比符號(hào)表要更加豐富一些
(7)當(dāng)創(chuàng)建類或接口的運(yùn)行時(shí)常量池時(shí),如果構(gòu)造運(yùn)行時(shí)常量池所需的內(nèi)存空間超過了方法區(qū)所能提供的最大值,則JVM會(huì)拋OutofMemoryError異常
方法區(qū)的使用舉例
public class MethodAreaDemo1 { public static void main(String[] args) { int x = 500; int y = 100; int a = x / y; int b = 50; System.out.println(a + b); } }
字節(jié)碼
Classfile /D:/practice/test01/demo/target/classes/com/example/demo/methodarea/MethodAreaDemo1.class Last modified 2024-7-2; size 693 bytes MD5 checksum ab63b3f6ca578dd4b6b79c3b51b75d31 Compiled from "MethodAreaDemo1.java" public class com.example.demo.methodarea.MethodAreaDemo1 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #5.#25 // java/lang/Object."<init>":()V #2 = Fieldref #26.#27 // java/lang/System.out:Ljava/io/PrintStream; #3 = Methodref #28.#29 // java/io/PrintStream.println:(I)V #4 = Class #30 // com/example/demo/methodarea/MethodAreaDemo1 #5 = Class #31 // java/lang/Object #6 = Utf8 <init> #7 = Utf8 ()V #8 = Utf8 Code #9 = Utf8 LineNumberTable #10 = Utf8 LocalVariableTable #11 = Utf8 this #12 = Utf8 Lcom/example/demo/methodarea/MethodAreaDemo1; #13 = Utf8 main #14 = Utf8 ([Ljava/lang/String;)V #15 = Utf8 args #16 = Utf8 [Ljava/lang/String; #17 = Utf8 x #18 = Utf8 I #19 = Utf8 y #20 = Utf8 a #21 = Utf8 b #22 = Utf8 MethodParameters #23 = Utf8 SourceFile #24 = Utf8 MethodAreaDemo1.java #25 = NameAndType #6:#7 // "<init>":()V #26 = Class #32 // java/lang/System #27 = NameAndType #33:#34 // out:Ljava/io/PrintStream; #28 = Class #35 // java/io/PrintStream #29 = NameAndType #36:#37 // println:(I)V #30 = Utf8 com/example/demo/methodarea/MethodAreaDemo1 #31 = Utf8 java/lang/Object #32 = Utf8 java/lang/System #33 = Utf8 out #34 = Utf8 Ljava/io/PrintStream; #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (I)V { public com.example.demo.methodarea.MethodAreaDemo1(); descriptor: ()V flags: 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 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/example/demo/methodarea/MethodAreaDemo1; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=5, args_size=1 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 // Field java/lang/System.out:Ljava/io/PrintStream; 18: iload_3 19: iload 4 21: iadd 22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 25: return LineNumberTable: line 5: 0 line 6: 4 line 7: 7 line 8: 11 line 9: 15 line 10: 25 LocalVariableTable: Start Length Slot Name Signature 0 26 0 args [Ljava/lang/String; 4 22 1 x I 7 19 2 y I 11 15 3 a I 15 11 4 b I MethodParameters: Name Flags args } SourceFile: "MethodAreaDemo1.java"
圖解字節(jié)碼指令執(zhí)行過程
(1)初始狀態(tài)

(2)首先將操作數(shù)500壓入操作數(shù)棧中

(3)然后操作數(shù)500從操作數(shù)棧中取出,存儲(chǔ)到局部變量表中索引為1的位置

(4)

(5)

(6)

(7)

(8)

(9)

(10)

(11)getstatic (#26.#27)獲取system類

(12)

(13)

(14)執(zhí)行加法運(yùn)算后,將計(jì)算結(jié)果放在操作數(shù)棧頂

(15)打印

(16)

符號(hào)引用->直接引用
(1)上面代碼調(diào)用 System.out.println() 方法時(shí),首先需要看看 System 類有沒有加載,再看看 PrintStream 類有沒有加載
(2)如果沒有加載,則執(zhí)行加載,執(zhí)行時(shí),將常量池中的符號(hào)引用(字面量)轉(zhuǎn)換為運(yùn)行時(shí)常量池的直接引用(真正的地址值)
方法區(qū)的演進(jìn)
永久代的演進(jìn)過程
(1)首先明確:只有Hotspot才有永久代。BEA JRockit、IBMJ9等來說,是不存在永久代的概念的。原則上如何實(shí)現(xiàn)方法區(qū)屬于虛擬機(jī)實(shí)現(xiàn)細(xì)節(jié),不受《Java虛擬機(jī)規(guī)范》管束,并不要求統(tǒng)一
(2)Hotspot中方法區(qū)的變化:
JDK1.6及以前 有永久代(permanent generation),靜態(tài)變量存儲(chǔ)在永久代上
JDK1.7 有永久代,但已經(jīng)逐步 “去永久代”,字符串常量池,靜態(tài)變量移除,保存在堆中
JDK1.8 無永久代,類型信息,字段,方法,常量保存在本地內(nèi)存的元空間,但字符串常量池、靜態(tài)變量仍然在堆中
JDK6
方法區(qū)由永久代實(shí)現(xiàn),使用 JVM 虛擬機(jī)內(nèi)存(虛擬的內(nèi)存)

JDK7
方法區(qū)由永久代實(shí)現(xiàn),使用 JVM 虛擬機(jī)內(nèi)存

JDK8
方法區(qū)由元空間實(shí)現(xiàn),使用物理機(jī)本地內(nèi)存

永久代為什么要被元空間替代?
官方文檔:http://openjdk.java.net/jeps/122
(1)隨著Java8的到來,HotSpot VM中再也見不到永久代了。但是這并不意味著類的元數(shù)據(jù)信息也消失了。這些數(shù)據(jù)被移到了一個(gè)與堆不相連的本地內(nèi)存區(qū)域,這個(gè)區(qū)域叫做元空間(Metaspace)。
(2)由于類的元數(shù)據(jù)分配在本地內(nèi)存中,元空間的最大可分配空間就是系統(tǒng)可用內(nèi)存空間
(3)這項(xiàng)改動(dòng)是很有必要的,原因有:
為永久代設(shè)置空間大小是很難確定的。在某些場景下,如果動(dòng)態(tài)加載類過多,容易產(chǎn)生Perm區(qū)的OOM。比如某個(gè)實(shí)際Web工程中,因?yàn)楣δ茳c(diǎn)比較多,在運(yùn)行過程中,要不斷動(dòng)態(tài)加載很多類,經(jīng)常出現(xiàn)致命錯(cuò)誤。Exception in thread 'dubbo client x.x connector' java.lang.OutOfMemoryError:PermGen space而元空間和永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。 因此,默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制
對永久代進(jìn)行調(diào)優(yōu)是很困難的。方法區(qū)的垃圾收集主要回收兩部分內(nèi)容:常量池中廢棄的常量和不再用的類型,方法區(qū)的調(diào)優(yōu)主要是為了降低Full GC
有些人認(rèn)為方法區(qū)(如HotSpot虛擬機(jī)中的元空間或者永久代)是沒有垃圾收集行為的,其實(shí)不然。《Java虛擬機(jī)規(guī)范》對方法區(qū)的約束是非常寬松的,提到過可以不要求虛擬機(jī)在方法區(qū)中實(shí)現(xiàn)垃圾收集。事實(shí)上也確實(shí)有未實(shí)現(xiàn)或未能完整實(shí)現(xiàn)方法區(qū)類型卸載的收集器存在(如JDK11時(shí)期的ZGC收集器就不支持類卸載)
一般來說這個(gè)區(qū)域的回收效果比較難令人滿意,尤其是類型的卸載,條件相當(dāng)苛刻**。但是這部分區(qū)域的回收有時(shí)又確實(shí)是必要的。以前Sun公司的Bug列表中,曾出現(xiàn)過的若干個(gè)嚴(yán)重的Bug就是由于低版本的HotSpot虛擬機(jī)對此區(qū)域未完全回收而導(dǎo)致內(nèi)存泄漏
字符串常量池
字符串常量池StringTable為什么要調(diào)整位置?
(1)JDK7中將StringTable放到了堆空間中。因?yàn)橛谰么幕厥招屎艿停贔ull GC的時(shí)候才會(huì)執(zhí)行永久代的垃圾回收,而Full GC是老年代的空間不足、永久代不足時(shí)才會(huì)觸發(fā)。
(2)這就導(dǎo)致StringTable回收效率不高,而我們開發(fā)中會(huì)有大量的字符串被創(chuàng)建,回收效率低,導(dǎo)致永久代內(nèi)存不足。放到堆里,能及時(shí)回收內(nèi)存。
靜態(tài)變量放在哪里
對象實(shí)體在哪里放著?
/** * 結(jié)論: * 1、靜態(tài)引用對應(yīng)的對象實(shí)體(也就是這個(gè)new byte[1024 * 1024 * 100])始終都存在堆空間, * 2、只是那個(gè)變量(相當(dāng)于下面的arr變量名)在JDK6,JDK7,JDK8存放位置中有所變化 * * jdk7: * -Xms200m -Xmx200m -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails * jdk 8: * -Xms200m -Xmx200m -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails */ public class StaticFieldTest { private static byte[] arr = new byte[1024 * 1024 * 100];//100MB public static void main(String[] args) { System.out.println(StaticFieldTest.arr); } }
JDK6環(huán)境下:

JDK7環(huán)境下

JDK8環(huán)境下

運(yùn)行時(shí)數(shù)據(jù)區(qū)總結(jié)

方法區(qū)的垃圾回收
(1)有些人認(rèn)為方法區(qū)(如Hotspot虛擬機(jī)中的元空間或者永久代)是沒有垃圾收集行為的,其實(shí)不然。《Java虛擬機(jī)規(guī)范》對方法區(qū)的約束是非常寬松的,提到過可以不要求虛擬機(jī)在方法區(qū)中實(shí)現(xiàn)垃圾收集。事實(shí)上也確實(shí)有未實(shí)現(xiàn)或未能完整實(shí)現(xiàn)方法區(qū)類型卸載的收集器存在(如JDK11時(shí)期的ZGC收集器就不支持類卸載)。
(2)一般來說這個(gè)區(qū)域的回收效果比較難令人滿意,尤其是類型的卸載,條件相當(dāng)苛刻。但是這部分區(qū)域的回收有時(shí)又確實(shí)是必要的。以前sun公司的Bug列表中,曾出現(xiàn)過的若干個(gè)嚴(yán)重的Bug就是由于低版本的HotSpot虛擬機(jī)對此區(qū)域未完全回收而導(dǎo)致內(nèi)存泄漏
(3)方法區(qū)的垃圾收集主要回收兩部分內(nèi)容:常量池中廢棄的常量和不再使用的類型。
(4)先來說說方法區(qū)內(nèi)常量池之中主要存放的兩大類常量:字面量和符號(hào)引用。字面量比較接近Java語言層次的常量概念,如文本字符串、被聲明為final的常量值等。而符號(hào)引用則屬于編譯原理方面的概念,包括下面三類常量
類和接口的全限定名
字段的名稱和描述符
方法的名稱和描述符
(5)HotSpot虛擬機(jī)對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方引用,就可以被回收
(6)回收廢棄常量與回收J(rèn)ava堆中的對象非常類似。(關(guān)于常量的回收比較簡單,重點(diǎn)是類的回收)
下面也稱作類卸載
(1)判定一個(gè)常量是否“廢棄”還是相對簡單,而要判定一個(gè)類型是否屬于“不再被使用的類”的條件就比較苛刻了。需要同時(shí)滿足下面三個(gè)條件:
該類所有的實(shí)例都已經(jīng)被回收,也就是Java堆中不存在該類及其任何派生子類的實(shí)例
加載該類的類加載器已經(jīng)被回收,這個(gè)條件除非是經(jīng)過精心設(shè)計(jì)的可替換類加載器的場景,如OSGi、JSP的重加載等,否則通常是很難達(dá)成的。
該類對應(yīng)的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法
(2)Java虛擬機(jī)被允許對滿足上述三個(gè)條件的無用類進(jìn)行回收,這里說的僅僅是“被允許”,而并不是和對象一樣,沒有引用了就必然會(huì)回收。關(guān)于是否要對類型進(jìn)行回收,HotSpot虛擬機(jī)提供了-Xnoclassgc參數(shù)進(jìn)行控制,還可以使用-verbose:class 以及 -XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看類加載和卸載信息
(3)在大量使用反射、動(dòng)態(tài)代理、CGLib等字節(jié)碼框架,動(dòng)態(tài)生成JSP以及OSGi這類頻繁自定義類加載器的場景中,通常都需要Java虛擬機(jī)具備類型卸載的能力,以保證不會(huì)對方法區(qū)造成過大的內(nèi)存壓力。
浙公網(wǎng)安備 33010602011771號(hào)