1.棧、堆、方法區(qū)的交互關(guān)系
從內(nèi)存結(jié)構(gòu)看
從線程共享與否的角度看
ThreadLocal:如何保證多個線程在并發(fā)環(huán)境下的安全性?典型應用就是數(shù)據(jù)庫連接管理,以及獨立會話管理
棧、堆、方法區(qū)的交互關(guān)系
- Person 類的 .class 信息存放在方法區(qū)中
- person 變量存放在 Java 棧的局部變量表中
- 真正的 person 對象存放在 Java 堆中
- 在 person 對象中,有個指針指向方法區(qū)中的 person 類型數(shù)據(jù),表明這個 person 對象是用方法區(qū)中的 Person 類 new 出來的
2.方法區(qū)的理解
官方文檔
點此進入官方文檔
2.1 方法區(qū)的位置
- 《Java虛擬機規(guī)范》中明確說明:盡管所有的方法區(qū)在邏輯上是屬于堆的一部分,但一些簡單的實現(xiàn)可能不會選擇去進行垃圾收集或者進行壓縮。
- 但對于HotSpotJVM而言,方法區(qū)還有一個別名叫做Non-Heap(非堆),目的就是要和堆分開。
- 所以,方法區(qū)可以看作是一塊獨立于Java堆的內(nèi)存空間。
2.2 方法區(qū)的理解
方法區(qū)主要存放的是 Class,而堆中主要存放的是實例化的對象
- 方法區(qū)(Method Area)與Java堆一樣,是各個線程共享的內(nèi)存區(qū)域
- 多個線程同時加載統(tǒng)一個類時,只能有一個線程能加載該類,其他線程只能等等待該線程加載完畢,然后直接使用該類,即類只能加載一次。
- 方法區(qū)在JVM啟動的時候被創(chuàng)建,并且它的實際物理內(nèi)存空間和Java堆區(qū)一樣都可以是不連續(xù)的。
- 方法區(qū)的大小,跟堆空間一樣,可以選擇固定大小或者可擴展。
- 方法區(qū)是接口,元空間或者永久代是方法區(qū)的實現(xiàn)
- 方法區(qū)的大小決定了系統(tǒng)可以保存多少個類,如果系統(tǒng)定義了太多的類,導致方法區(qū)溢出,虛擬機同樣會拋出內(nèi)存溢出錯誤:
- java.lang.OutofMemoryError:PermGen space(JDK7之前)
- 或者
- java.lang.OutOfMemoryError:Metaspace(JDK8之后)
- 舉例說明方法區(qū) OOM
- 加載大量的第三方的jar包
- Tomcat部署的工程過多(30~50個)
- 大量動態(tài)的生成反射類
- 關(guān)閉JVM就會釋放這個區(qū)域的內(nèi)存。
代碼示例
/**
* -Xms600m -Xmx600m
*/
public class EdenSurvivorTest {
public static void main(String[] args) {
System.out.println('我只是來打個醬油~');
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 可以看到設置堆內(nèi)存為600M后,年輕代+老年代=600M,所以說方法區(qū)是不存在堆中的
2.3 Hotspot中方法區(qū)的演進過程
- 在 JDK7 及以前,習慣上把方法區(qū),稱為永久代。JDK8開始,使用元空間取代了永久代。JDK 1.8之后,元空間存放在堆外內(nèi)存中
- 我們可以將方法區(qū)類比為Java中的接口,將永久代或元空間類比為Java中具體的實現(xiàn)類
- 本質(zhì)上,方法區(qū)和永久代并不等價。僅是對Hotspot而言的可以看作等價。《Java虛擬機規(guī)范》對如何實現(xiàn)方法區(qū),不做統(tǒng)一要求。例如:BEAJRockit / IBM J9 中不存在永久代的概念。
- 現(xiàn)在來看,當年使用永久代,不是好的idea。導致Java程序更容易OOm(超過-XX:MaxPermsize上限)
- 而到了JDK8,終于完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地內(nèi)存中實現(xiàn)的元空間(Metaspace)來代替
- 元空間的本質(zhì)和永久代類似,都是對JVM規(guī)范中方法區(qū)的實現(xiàn)。不過元空間與永久代最大的區(qū)別在于:元空間不在虛擬機設置的內(nèi)存中,而是使用本地內(nèi)存
- 永久代、元空間二者并不只是名字變了,內(nèi)部結(jié)構(gòu)也調(diào)整了
- 根據(jù)《Java虛擬機規(guī)范》的規(guī)定,如果方法區(qū)無法滿足新的內(nèi)存分配需求時,將拋出OOM異常
3.設置方法區(qū)大小與OOM
方法區(qū)的大小不必是固定的,JVM可以根據(jù)應用的需要動態(tài)調(diào)整
3.1 JDK7 永久代
- 通過-XX:Permsize來設置永久代初始分配空間。默認值是20.75M
- -XX:MaxPermsize來設定永久代最大可分配空間。32位機器默認是64M,64位機器模式是82M
- 當JVM加載的類信息容量超過了這個值,會報異常OutofMemoryError:PermGen space。
3.2 JDK8 元空間
- 元數(shù)據(jù)區(qū)大小可以使用參數(shù) -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 指定
- 默認值依賴于平臺,Windows下,-XX:MetaspaceSize 約為21M,-XX:MaxMetaspaceSize的值是-1,即沒有限制。
- 與永久代不同,如果不指定大小,默認情況下,虛擬機會耗盡所有的可用系統(tǒng)內(nèi)存。如果元數(shù)據(jù)區(qū)發(fā)生溢出,虛擬機一樣會拋出異常OutOfMemoryError:Metaspace
- -XX:MetaspaceSize:設置初始的元空間大小。對于一個 64位 的服務器端 JVM 來說,其默認的 -XX:MetaspaceSize值為21MB。這就是初始的高水位線,一旦觸及這個水位線,F(xiàn)ull GC將會被觸發(fā)并卸載沒用的類(即這些類對應的類加載器不再存活),然后這個高水位線將會重置。新的高水位線的值取決于GC后釋放了多少元空間。
- 如果釋放的空間不足,那么在不超過MaxMetaspaceSize時,適當提高該值。
- 如果釋放空間過多,則適當降低該值。
- 如果初始化的高水位線設置過低,上述高水位線調(diào)整情況會發(fā)生很多次。通過垃圾回收器的日志可以觀察到Full GC多次調(diào)用。為了避免頻繁地GC,建議將-XX:MetaspaceSize設置為一個相對較高的值。
配置元空間大小示例
/**
* 測試設置方法區(qū)大小參數(shù)的默認值
*
* jdk7及以前:
* -XX:PermSize=100m -XX:MaxPermSize=100m
*
* jdk8及以后:
* -XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m
*/
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...');
}
}
-XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m
xiexu@Macintosh ~ % jps
xiexu@Macintosh ~ % jinfo -flag MetaspaceSize 47064
xiexu@Macintosh ~ % jinfo -flag MaxMetaspaceSize 47064
3.3 方法區(qū)OOM
- OOMTest 類繼承 ClassLoader 類,獲得 defineClass() 方法,可自己進行類的加載
/**
* 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對象,用于生成類的二進制字節(jié)碼
ClassWriter classWriter = new ClassWriter(0);
//指明版本號,修飾符,類名,包名,父類,接口
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);
}
}
}
不設置元空間的上限
- 使用默認的 JVM 參數(shù),元空間不設置上限
10000
設置元空間的上限
-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
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 cn.sxt.java.OOMTest.main(OOMTest.java:26)
3.4 如何解決OOM?
- 要解決OOM異?;騢eap space的異常,一般的手段是首先通過內(nèi)存映像分析工具(如Eclipse Memory Analyzer)對dump出來的堆轉(zhuǎn)儲快照進行分析,重點是確認內(nèi)存中的對象是否是必要的,也就是要先分清楚到底是出現(xiàn)了內(nèi)存泄漏(Memory Leak)還是內(nèi)存溢出(Memory Overflow)
- 內(nèi)存泄漏就是有大量的引用指向某些對象,但是這些對象以后不會使用了,但是因為它們還和GC ROOT有關(guān)聯(lián),所以導致以后這些對象也不會被回收,這就是內(nèi)存泄漏的問題
- 如果是內(nèi)存泄漏,可進一步通過工具查看泄漏對象到GC Roots的引用鏈。于是就能找到泄漏對象是通過怎樣的路徑與GC Roots相關(guān)聯(lián)并導致垃圾收集器無法自動回收它們的。掌握了泄漏對象的類型信息,以及GC Roots引用鏈的信息,就可以比較準確地定位出泄漏代碼的位置。
- 如果不存在內(nèi)存泄漏,換句話說就是內(nèi)存中的對象確實都還必須存活著,那就應當檢查虛擬機的堆參數(shù)(-Xmx與-Xms),與機器物理內(nèi)存對比看是否還可以調(diào)大,從代碼上檢查是否存在某些對象生命周期過長、持有狀態(tài)時間過長的情況,嘗試減少程序運行期的內(nèi)存消耗。
4.方法區(qū)的內(nèi)部結(jié)構(gòu)
4.1 方法區(qū)結(jié)構(gòu)
《深入理解Java虛擬機》書中對方法區(qū)(Method Area)存儲內(nèi)容描述如下:它用于存儲已被虛擬機加載的類型信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼緩存等。
4.1.1 類型信息
對每個加載的類型(類class、接口interface、枚舉enum、注解annotation),JVM必須在方法區(qū)中存儲以下類型信息:
- 這個類型的完整有效名稱(全類名=包名.類名)
- 這個類型直接父類的完整有效名(對于interface或是java.lang.Object,都沒有父類)
- 這個類型的修飾符(public,abstract,final的某個子集)
- 這個類型直接接口的一個有序列表
4.1.2 域(Field)信息
- JVM必須在方法區(qū)中保存類型的所有域的相關(guān)信息以及域的聲明順序。
域信息 通俗來講是類的成員變量- 域的相關(guān)信息包括:
- 域名稱
- 域類型
- 域修飾符(public,private,protected,static,final,volatile,transient的某個子集)
4.1.3 方法(Method)信息
JVM必須保存所有方法的以下信息,同域信息一樣包括聲明順序:
- 方法名稱
- 方法的返回類型(包括 void 返回類型),void 在 Java 中對應的類為 void.class
- 方法參數(shù)的數(shù)量和類型(按順序)
- 方法的修飾符(public,private,protected,static,final,synchronized,native,abstract的一個子集)
- 方法的字節(jié)碼(bytecodes)、操作數(shù)棧、局部變量表及大?。╝bstract和native方法除外)
- 異常表(abstract和native方法除外),異常表記錄每個異常處理的開始位置、結(jié)束位置、代碼處理在程序計數(shù)器中的偏移地址、被捕獲的異常類的常量池索引
代碼示例
/**
* 測試方法區(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;
}
}
- 反編譯字節(jié)碼文件,并輸出到文本文件中,便于查看
- 參數(shù) -p 確保能查看 private 權(quán)限類型的字段或方法
javap -v -p MethodInnerStrucTest.class > Text.txt
類型信息
- 在運行時方法區(qū)中,類信息中記錄了哪個加載器加載了該類,同時類加載器也記錄了它加載了哪些類
- 從反編譯文件可以看出,字節(jié)碼文件記錄了 MethodInnerStrucTest 繼承了哪些類,實現(xiàn)了哪些方法
//類型信息
public class cn.sxt.java.MethodInnerStrucTest extends java.lang.Object
implements java.lang.Comparable<java.lang.String>, java.io.Serializable
域信息
- descriptor: I 表示字段類型為 Integer
- 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
方法信息
- descriptor: ( )V 表示方法返回值類型為 void
- flags: ACC_PUBLIC 表示方法權(quán)限修飾符為 public
- stack=3 表示操作數(shù)棧深度為 3
- locals=2 表示局部變量個數(shù)為 2 個(實力方法包含 this)
- test1( ) 方法雖然沒有參數(shù),但是其 args_size=1 ,這是因為將 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 Lcn/sxt/java/MethodInnerStrucTest;
3 26 1 count I
4.2 域信息特殊情況
non-final 類型的類變量
- 靜態(tài)變量和類關(guān)聯(lián)在一起,隨著類的加載而加載,他們成為類數(shù)據(jù)在邏輯上的一部分
- 類變量被類的所有實例共享,即使沒有類實例時,你也可以訪問它
代碼示例
- 如下代碼所示,即使我們把order設置為null,也不會出現(xiàn)空指針異常
- 這更加表明了 static 類型的字段和方法隨著類的加載而加載,并不屬于特定的類實例
/**
* non-final的類變量
*/
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
- 全局常量就是使用 static final 進行修飾
- 被聲明為final的類變量的處理方法則不同,每個全局常量在編譯的時候就會被分配了。
代碼示例
class Order {
public static int count = 1;
public static final int number = 2;
public static void hello() {
System.out.println('hello!');
}
}
- 反編譯,查看字節(jié)碼指令,可以發(fā)現(xiàn) number 的值已經(jīng)寫死在字節(jié)碼文件中了
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
4.3 運行時常量池
點此進入官方文檔
運行時常量池 VS 常量池
- 方法區(qū),內(nèi)部包含了運行時常量池
- 字節(jié)碼文件,內(nèi)部包含了常量池
- 要弄清楚方法區(qū),需要理解清楚ClassFile,因為加載類的信息都在方法區(qū)。
- 要弄清楚方法區(qū)的運行時常量池,需要理解清楚ClassFile中的常量池。
常量池
- 一個有效的字節(jié)碼文件中除了包含類的版本信息、字段、方法以及接口等描述符信息外
- 還包含一項信息就是常量池表(Constant Pool Table),包括各種字面量和對類型、域和方法的符號引用
為什么需要常量池?
- 一個java源文件中的類、接口,編譯后產(chǎn)生一個字節(jié)碼文件。而Java中的字節(jié)碼需要數(shù)據(jù)支持,通常這種數(shù)據(jù)會很大以至于不能直接存到字節(jié)碼里,換另一種方式,可以存到常量池
- 這個字節(jié)碼包含了指向常量池的引用。在動態(tài)鏈接的時候會用到運行時常量池,之前有介紹
- 比如:如下的代碼:
public class SimpleClass {
public void sayHello() {
System.out.println('hello');
}
}
- 雖然上述代碼只有194字節(jié),但是里面卻使用了String、System、PrintStream及Object等結(jié)構(gòu)。
- 如果不使用常量池,就需要將用到的類信息、方法信息等記錄在當前的字節(jié)碼文件中,造成文件臃腫
- 所以我們將所需用到的結(jié)構(gòu)信息記錄在常量池中,并通過引用的方式,來加載、調(diào)用所需的結(jié)構(gòu)
- 這里的代碼量其實很少了,如果代碼多的話,引用的結(jié)構(gòu)將會更多,這里就需要用到常量池了。
常量池中有什么?
常量池代碼舉例
/**
* 測試方法區(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;
}
}
- 來看下最簡單的 test1( ) 方法,帶 # 的字節(jié)碼指令,就使用到了常量池的引用
- 通過字節(jié)碼指令可以看出,拼接字符串時,編譯器幫我們造了個 StringBuilder 對象,然后調(diào)用其 append( ) 方法完成了字符串的拼接
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 Lcn/sxt/java/MethodInnerStrucTest;
3 26 1 count I
Constant pool:
#1 = Methodref #18.#52 // java/lang/Object.'<init>':()V
#2 = Fieldref #17.#53 // cn/sxt/java/MethodInnerStrucTest.num:I
#3 = Fieldref #54.#55 // java/lang/System.out:Ljava/io/PrintStream;
#4 = Class #56 // java/lang/StringBuilder
#5 = Methodref #4.#52 // java/lang/StringBuilder.'<init>':()V
#6 = String #57 // count =
#7 = Methodref #4.#58 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = Methodref #4.#59 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#9 = Methodref #4.#60 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#10 = Methodref #61.#62 // java/io/PrintStream.println:(Ljava/lang/String;)V
#11 = Class #63 // java/lang/Exception
#12 = Methodref #11.#64 // java/lang/Exception.printStackTrace:()V
#13 = Class #65 // java/lang/String
#14 = Methodref #17.#66 // cn/sxt/java/MethodInnerStrucTest.compareTo:(Ljava/lang/String;)I
#15 = String #67 // 測試方法的內(nèi)部結(jié)構(gòu)
#16 = Fieldref #17.#68 // cn/sxt/java/MethodInnerStrucTest.str:Ljava/lang/String;
#17 = Class #69 // cn/sxt/java/MethodInnerStrucTest
#18 = Class #70 // java/lang/Object
#19 = Class #71 // java/lang/Comparable
#20 = Class #72 // 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 Lcn/sxt/java/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 #63 // java/lang/Exception
#43 = Utf8 compareTo
#44 = Utf8 (Ljava/lang/String;)I
#45 = Utf8 o
#46 = Utf8 (Ljava/lang/Object;)I
#47 = Utf8 <clinit>
#48 = Utf8 Signature
#49 = Utf8 Ljava/lang/Object;Ljava/lang/Comparable<Ljava/lang/String;>;Ljava/io/Serializable;
#50 = Utf8 SourceFile
#51 = Utf8 MethodInnerStrucTest.java
#52 = NameAndType #25:#26 // '<init>':()V
#53 = NameAndType #21:#22 // num:I
#54 = Class #73 // java/lang/System
#55 = NameAndType #74:#75 // out:Ljava/io/PrintStream;
#56 = Utf8 java/lang/StringBuilder
#57 = Utf8 count =
#58 = NameAndType #76:#77 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#59 = NameAndType #76:#78 // append:(I)Ljava/lang/StringBuilder;
#60 = NameAndType #79:#80 // toString:()Ljava/lang/String;
#61 = Class #81 // java/io/PrintStream
#62 = NameAndType #82:#83 // println:(Ljava/lang/String;)V
#63 = Utf8 java/lang/Exception
#64 = NameAndType #84:#26 // printStackTrace:()V
#65 = Utf8 java/lang/String
#66 = NameAndType #43:#44 // compareTo:(Ljava/lang/String;)I
#67 = Utf8 測試方法的內(nèi)部結(jié)構(gòu)
#68 = NameAndType #23:#24 // str:Ljava/lang/String;
#69 = Utf8 cn/sxt/java/MethodInnerStrucTest
#70 = Utf8 java/lang/Object
#71 = Utf8 java/lang/Comparable
#72 = Utf8 java/io/Serializable
#73 = Utf8 java/lang/System
#74 = Utf8 out
#75 = Utf8 Ljava/io/PrintStream;
#76 = Utf8 append
#77 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#78 = Utf8 (I)Ljava/lang/StringBuilder;
#79 = Utf8 toString
#80 = Utf8 ()Ljava/lang/String;
#81 = Utf8 java/io/PrintStream
#82 = Utf8 println
#83 = Utf8 (Ljava/lang/String;)V
#84 = Utf8 printStackTrace
常量池總結(jié)
常量池,可以看做是一張表,虛擬機指令根據(jù)這張常量表找到要執(zhí)行的類名、方法名、參數(shù)類型、字面量等信息
運行時常量池
- 運行時常量池(Runtime Constant Pool)是方法區(qū)的一部分。
- 常量池表(Constant Pool Table)是Class字節(jié)碼文件的一部分,用于存放編譯期生成的各種字面量與符號引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運行時常量池中。
- 運行時常量池,在加載類和接口到虛擬機后,就會創(chuàng)建對應的運行時常量池。
- JVM為每個已加載的類型(類或接口)都維護一個常量池。池中的數(shù)據(jù)項像數(shù)組項一樣,是通過索引訪問的。
- 運行時常量池中包含多種不同的常量,包括編譯期就已經(jīng)明確的數(shù)值字面量,也包括到運行期解析后才能夠獲得的方法或者字段引用。此時不再是常量池中的符號地址了,這里換為真實地址。
- 運行時常量池,相對于Class文件常量池的另一重要特征是:
具備動態(tài)性 。 - 運行時常量池類似于傳統(tǒng)編程語言中的符號表(symbol table),但是它所包含的數(shù)據(jù)卻比符號表要更加豐富一些。
- 當創(chuàng)建類或接口的運行時常量池時,如果構(gòu)造運行時常量池所需的內(nèi)存空間超過了方法區(qū)所能提供的最大值,則JVM會拋OutOfMemoryError異常。
5.方法區(qū)使用舉例
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);
}
}
圖解字節(jié)碼指令執(zhí)行流程
- 字節(jié)碼執(zhí)行過程展示:初始狀態(tài)
- 首先將操作數(shù)500壓入操作數(shù)棧中
- 然后將操作數(shù) 500 從操作數(shù)棧中取出,存儲到局部變量表中索引為 1 的位置
- 然后操作數(shù) 100 從操作數(shù)棧中取出,存儲到局部變量表中索引為 2 的位置
- 兩數(shù)相除,計算結(jié)果放在操作數(shù)棧頂,之后執(zhí)行 istore_3 指令,將計算結(jié)果從操作數(shù)棧中彈出,存入本地變量表 3 中
- 將操作數(shù) 50 從棧頂彈出,保存在局部變量表 4 中
- 將本地變量表 3 的值取出,壓入操作數(shù)棧中,準備進行加法運算
- 將本地變量表 4 的值取出,壓入操作數(shù)棧中,準備進行加法運算
- 執(zhí)行加法運算后,將計算結(jié)果放在操作數(shù)棧頂
- 調(diào)用靜態(tài)方法 println( ) ,輸出加法結(jié)果
關(guān)于【符號引用 --> 直接引用】的理解
- 上面代碼調(diào)用 System.out.println( ) 方法時,首先需要看 System 類有沒有加載,再看看 PrintStream 類有沒有加載
- 如果沒有加載,則執(zhí)行加載,執(zhí)行時,將常量池中的符號引用(字面量)轉(zhuǎn)換為直接引用(真正的地址值)
關(guān)于程序計數(shù)器的說明
程序計數(shù)器始終存儲的都是當前字節(jié)碼指令的索引地址,目的是為了方便記錄方法調(diào)用后能夠正常返回,或者是進行了CPU切換后,也能回到原來的代碼繼續(xù)執(zhí)行。
6.方法區(qū)的演進細節(jié)
6.1 永久代演進過程
- 首先明確:只有Hotspot才有
永久代 。 - BEA JRockit、IBMJ9等來說,是不存在永久代的概念的。原則上如何實現(xiàn)方法區(qū)屬于虛擬機實現(xiàn)細節(jié),不受《Java虛擬機規(guī)范》管束,并不要求統(tǒng)一
- Hotspot中方法區(qū)的變化:
JDK 版本 | 演變細節(jié) |
---|
JDK1.6及以前 | 有永久代(permanent generation),靜態(tài)變量存儲在永久代上 | JDK1.7 | 有永久代,但已經(jīng)逐步 “去永久代”,字符串常量池、靜態(tài)變量從永久代中移除,保存在堆中 | JDK1.8 | 無永久代,類型信息,字段,方法,常量保存在本地內(nèi)存的元空間,但字符串常量池、靜態(tài)變量仍然在堆中。 |
JDK6
- 方法區(qū)由永久代實現(xiàn),使用 JVM 虛擬機內(nèi)存
JDK7
- 方法區(qū)由永久代實現(xiàn),使用 JVM 虛擬機內(nèi)存
JDK8及以后
- 方法區(qū)由元空間實現(xiàn),使用物理機本地內(nèi)存
6.2 永久代為什么要被元空間替代?
點此進入官方文檔
- 官方的牽強解釋:JRockit是和HotSpot融合后的結(jié)果,因為JRockit沒有永久代,所以他們不需要配置永久代,HotSpot也就取消了永久代
- 隨著Java8的到來,HotSpot VM中再也見不到永久代了。但是這并不意味著類的元數(shù)據(jù)信息也消失了。這些數(shù)據(jù)被移到了一個與堆不相連的本地內(nèi)存區(qū)域,這個區(qū)域叫做元空間(Metaspace)。
由于類的元數(shù)據(jù)分配在本地內(nèi)存中,元空間的最大可分配空間就是系統(tǒng)可用內(nèi)存空間,這項改動是很有必要的,原因有:
- 為永久代設置空間大小是很難確定的。
- 在某些場景下,如果動態(tài)加載類過多,容易產(chǎn)生Perm區(qū)的OOM。比如某個實際Web工程中,因為功能點比較多,在運行過程中,要不斷動態(tài)地加載很多類,經(jīng)常出現(xiàn)致命錯誤。
Exception in thread 'dubbo client x.x connector' java.lang.OutOfMemoryError:PermGen space - 而元空間和永久代之間最大的區(qū)別在于:元空間并不在虛擬機中,而是使用本地內(nèi)存。因此,默認情況下,元空間的大小僅受本地內(nèi)存限制。
- 對永久代進行調(diào)優(yōu)是很困難的。
- 方法區(qū)的垃圾收集主要回收兩部分內(nèi)容:常量池中廢棄的常量和不再用的類型,方法區(qū)的調(diào)優(yōu)主要是為了降低Full GC
- 有些人認為方法區(qū)(如HotSpot虛擬機中的元空間或者永久代)是沒有垃圾收集行為的,其實不然。《Java虛擬機規(guī)范》對方法區(qū)的約束是非常寬松的,提到過可以不要求虛擬機在方法區(qū)中實現(xiàn)垃圾收集。事實上也確實有未實現(xiàn)或未能完整實現(xiàn)方法區(qū)類型卸載的收集器存在(如JDK11時期的ZGC收集器就不支持類卸載)。
- 一般來說這個區(qū)域的回收效果比較難令人滿意,尤其是類型的卸載,條件相當苛刻。但是這部分區(qū)域的回收有時又確實是必要的。以前Sun公司的Bug列表中,曾出現(xiàn)過的若干個嚴重的Bug就是由于低版本的HotSpot虛擬機對此區(qū)域未完全回收而導致內(nèi)存泄漏
6.3 字符串常量池
字符串常量池 StringTable 為什么要調(diào)整位置?
- JDK7中將StringTable放到了堆空間中。因為永久代的回收效率很低,在Full GC的時候才會執(zhí)行永久代的垃圾回收,而Full GC是老年代的空間不足、永久代不足時才會觸發(fā)。
- 這就導致StringTable回收效率不高,而我們開發(fā)中會有大量的字符串被創(chuàng)建,回收效率低,導致永久代內(nèi)存不足。放到堆里,能及時回收內(nèi)存。
6.4 靜態(tài)變量位置
靜態(tài)變量存放在哪里?
/**
* 結(jié)論:
* 靜態(tài)變量在jdk6/7存在與永久代中,在jdk8存在于堆中 //private static byte[] arr
* 靜態(tài)引用對應的對象實體始終都存在堆空間 //new byte[1024 * 1024 * 100];
*
* 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);
}
}
-Xms200m -Xmx200m -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
- 通過 GC 日志可以看出:靜態(tài)變量引用對應的對象實體始終都在堆空間中(arr 數(shù)組對象直接懟到老年區(qū)去了)
代碼示例2
/**
* 《深入理解Java虛擬機》中的案例:
* staticObj、instanceObj、localObj存放在哪里?
*/
public class StaticObjTest {
static class Test {
//靜態(tài)屬性
static ObjectHolder staticObj = new ObjectHolder();
//非靜態(tài)屬性
ObjectHolder instanceObj = new ObjectHolder();
void foo() {
//局部變量
ObjectHolder localObj = new ObjectHolder();
System.out.println('done');
}
}
private static class ObjectHolder {
}
public static void main(String[] args) {
Test test = new StaticObjTest.Test();
test.foo();
}
}
- 可以使用 JHSDB.exe,在JDK9的時候才引入的
- 分析:staticObj隨著Test的類型信息存放在方法區(qū),instanceObj隨著Test的對象實例存放在Java堆,localObject則是存放在foo( )方法棧幀的局部變量表中。
- 測試發(fā)現(xiàn):三個對象的數(shù)據(jù)在內(nèi)存中的地址都落在Eden區(qū)范圍內(nèi),所以結(jié)論:只要是對象實例必然會在Java堆中分配。
- 接著,找到了一個引用該staticObj對象的地方,是在一個java.lang.Class的實例里,并且給出了這個實例的地址,通過Inspector查看該對象實例,可以清楚看到這確實是一個java.lang.Class類型的對象實例,里面有一個名為staticobj的實例字段:
- 從《Java虛擬機規(guī)范》所定義的概念模型來看,所有Class相關(guān)的信息都應該存放在方法區(qū)之中,但方法區(qū)該如何實現(xiàn),《Java虛擬機規(guī)范》并未做出規(guī)定,這就成了一件允許不同虛擬機自己靈活把握的事情。JDK7及其以后版本的HotSpot虛擬機選擇把靜態(tài)變量與類型在Java語言一端的映射Class對象存放在一起,存儲于Java堆之中,從我們的實驗中也明確驗證了這一點
7.方法區(qū)的垃圾回收
方法區(qū)的垃圾收集
- 有些人認為方法區(qū)(如Hotspot虛擬機中的元空間或者永久代)是沒有垃圾收集行為的,其實不然。
- 《Java虛擬機規(guī)范》對方法區(qū)的約束是非常寬松的,提到過可以不要求虛擬機在方法區(qū)中實現(xiàn)垃圾收集。事實上也確實有未實現(xiàn)或未能完整實現(xiàn)方法區(qū)類型卸載的收集器存在(如JDK11時期的ZGC收集器就不支持類卸載)。
- 一般來說這個區(qū)域的回收效果比較難令人滿意,尤其是類型的卸載,條件相當苛刻。但是這部分區(qū)域的回收有時又確實是必要的。以前sun公司的Bug列表中,曾出現(xiàn)過的若干個嚴重的Bug就是由于低版本的HotSpot虛擬機對此區(qū)域未完全回收而導致內(nèi)存泄漏。
- 方法區(qū)的垃圾收集主要回收兩部分內(nèi)容:常量池中廢棄的常量和不再使用的類型。
7.1 方法區(qū)常量的回收
- 先來說說方法區(qū)內(nèi)常量池之中主要存放的兩大類常量:字面量和符號引用
- 字面量比較接近Java語言層次的常量概念,如文本字符串、被聲明為final的常量值等
- 而符號引用則屬于編譯原理方面的概念,包括下面三類常量:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
- HotSpot虛擬機對常量池的回收策略是很明確的,
只要常量池中的常量沒有被任何地方引用,就可以被回收 。 - 回收廢棄常量與回收Java堆中的對象非常類似。(關(guān)于常量的回收比較簡單,重點是類的回收)
7.2 方法區(qū)類的回收
- 判定一個常量是否“廢棄”還是相對簡單,而要判定一個類型是否屬于“不再被使用的類”的條件就比較苛刻了。需要同時滿足下面三個條件:
- 該類所有的實例都已經(jīng)被回收,也就是Java堆中不存在該類及其任何派生子類的實例。
- 加載該類的類加載器已經(jīng)被回收,這個條件除非是經(jīng)過精心設計的可替換類加載器的場景,如OSGi、JSP的重加載等,否則通常是很難達成的。
- 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
- Java虛擬機被允許對滿足上述三個條件的無用類進行回收,這里說的僅僅是“被允許”,而并不是和對象一樣,沒有引用了就必然會回收。關(guān)于是否要對類型進行回收,HotSpot虛擬機提供了
-Xnoclassgc 參數(shù)進行控制,還可以使用-verbose:class 以及 -XX:+TraceClass-Loading 、-XX:+TraceClassUnLoading 查看類加載和卸載信息 - 在大量使用反射、動態(tài)代理、CGLib等字節(jié)碼框架,動態(tài)生成JSP以及 OSGi 這類頻繁自定義類加載器的場景中,通常都需要Java虛擬機具備類型卸載的能力,以保證不會對方法區(qū)造成過大的內(nèi)存壓力。
8.運行時數(shù)據(jù)區(qū)總結(jié)
- 線程私有結(jié)構(gòu):程序計數(shù)器、虛擬機棧、本地方法棧
- 每個虛擬機棧由具體的棧幀組成,在棧幀的動態(tài)鏈接中,保存至對方法的引用
- 方法區(qū)在 JDK7 之前,使用永久代實現(xiàn),在 JDK8 之后,使用元空間實現(xiàn)
- Minor GC 針對于新生區(qū),Major GC 針對于老年區(qū),F(xiàn)ull GC 針對于整個堆空間和方法區(qū)
9.大廠面試題
百度
- 三面:說一下JVM內(nèi)存模型吧,有哪些區(qū)?分別干什么的?
字節(jié)跳動
- 二面:Java的內(nèi)存分區(qū)
- 二面:講講vm運行時數(shù)據(jù)庫區(qū)
- 什么時候?qū)ο髸M入老年代?
螞蟻金服
- Java8的內(nèi)存分代改進
- JVM內(nèi)存分哪幾個區(qū),每個區(qū)的作用是什么?
- 一面:JVM內(nèi)存分布/內(nèi)存結(jié)構(gòu)?棧和堆的區(qū)別?堆的結(jié)構(gòu)?為什么兩個survivor區(qū)?
- 二面:Eden和survior的比例分配
小米
- jvm內(nèi)存分區(qū),為什么要有新生代和老年代
京東
- JVM的內(nèi)存結(jié)構(gòu),Eden和Survivor比例。
- JVM內(nèi)存為什么要分成新生代,老年代,持久代。新生代中為什么要分為Eden和survivor。
天貓
- 一面:Jvm內(nèi)存模型以及分區(qū),需要詳細到每個區(qū)放什么。
- 一面:JVM的內(nèi)存模型,Java8做了什么改
拼多多
- JVM內(nèi)存分哪幾個區(qū),每個區(qū)的作用是什么?
美團
- java內(nèi)存分配
- jvm的永久代中會發(fā)生垃圾回收嗎?
- 一面:jvm內(nèi)存分區(qū),為什么要有新生代和老年代?
|