文章目錄
1. 棧、堆、方法區的交互關係
- 線程共享的角度
1. 線程共享區域:堆和方法區,兩者都會有OutOfMemoryError和GC
2. 線程獨佔區域:虛擬機棧,本地方法棧,程序計數器,兩個棧會出現StackOverFlowErr,都沒有GC
- 對象聲明涉及到的棧堆方法區
2. 方法區的理解
- 簡述
1. 方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域
2. 方法區在JVM啓動的時候被創建,實際的物理內存和Java堆區一樣都可以是不連續的
3. 方法區的大小跟堆空間一樣,可以選擇固定大小或者可擴展
4. 方法區的大小決定了系統可以保存多少個類,類加載太多會導致內存溢出
(java.lang.OutOfMemoryError:PermGen space jdk7及以前或者java.lang.OutOfMemoryError:Metaspace jdk8及以後)
4.1 加載大量第三方的jar包
4.2 Tomcat部署工程過多(30-50個)
4.3 大量動態代理生成反射類
5. 關閉jvm會釋放這個區域的內存
3. 方法區的演進
1. 方法區是java虛擬機規範中的概念,相當於接口,永久代和元空間是hotspot虛擬機的實現
2. jdk7及以前方法區被稱爲永久代,jdk8及以後使用元空間取代了永久代
3. 永久代的方法區使用的是虛擬機內存,容易出現oom(超過XX:MaxPermSize上限),元空間使用的是本地內存(默認只受本地內存限制)
4. 設置方法區大小
- jdk7及以前
1. 通過-XX:PermSize來設置永久代初始分配空間,默認值是20.75
2. -XX:MaxPermSize來設定永久代最大可分配空間
32位機器默認是64M,64機器默認是82M
3. jvm加載的類信息超過了MaxPermSize會包OutOfMemoryError:PermGen space
查看java進程的參數配置
- jdk1.8及以後
1. 元數據區大小可以使用參數-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定
2. windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,表示沒有限制
3. 默認情況下,虛擬機會耗盡所有的可用內存,如果元數據發生溢出,還是會有OutOfMemory:Metaspace的異常
4. --XX:MetaspaceSize 設置初始的元空間大小,對於64位的服務器端jvm來說,其默認值爲21M,這就是高水位線,
一但觸及這個水位線,FullGC將會被觸發,卸載沒用的類,然後高水位線將被重置。
新的高水位線的值取決於GC後釋放了多少元空間,如果釋放的空間不足,那麼在不超過MaxMetaspaceSize時,適當提高這個值,相反則適當降低這個值
5. 方法區的內部結構
- 簡述
方法區用於存儲已被虛擬機加載的類型信息、常量、靜態變量、即時編譯後的代碼緩存等——《深入理解Java虛擬機》
5.1 類型信息
對每個加載的類型包括類class、接口interface、枚舉enum、註解annotation,JVM必須在方法區中存儲以下類型的消息
1. 這個類型的完整有效名稱(全名=包名.類名)
2. 這個類型直接父類的完整有效名(對於interface或是java.lang.Object,都沒有父類)
3. 這個類型的修飾符(public,abstract,final的某個子集)
4. 這個類型直接接口的一個有序列表
5.2 域(Field)信息
1. JVM必須在方法區中保存類型的所有域的相關信息以及域的聲明順序
2. 域的相關信息包括:域名稱、域類型、域修飾符
(public,private,protected,static,final,volatile,transient的某個子集)
5.3 方法(Method)信息
1. 方法名稱
2. 方法的返回類型(或void)
3. 方法的參數的數量和類型(按順序)
4. 方法的修飾符(public,private,protected,static,final,synchronized,native,abstract的一個子集)
5. 方法的字節碼(bytecodes)、操作數棧、局部變量表及大小(abstract和native方法除外)
6. 異常表(abstract和native方法除外)
每個異常處理的開始位置、結束位置、代碼處理在程序計數器中的偏移地址
被捕獲的異常類的常量池索引
5.4 類變量
- non-final的類變量
1. 靜態變量和類關聯在一起,隨着類的加載而加載(準備階段零值初始化,初始化階段真實賦值)
2. 類變量被類的所有實例共享,即使沒有實例也可以訪問
- static-final變量
被聲明爲final的類變量的處理方法則不同,每個全局變量在編譯的時候就會被分配了。
1. 八種基礎數據類型和字符串常量,在編譯期間直接確定
2. final對象創建會和static變量一起賦值,在static代碼塊中賦值
// 八種基礎數據類型和字符串常量在編譯期就被確定
public final static byte a1 = 1;
public final static short a2 = 2;
public final static char a3 = 3;
public final static int a4 = 4;
public final static long a5 = 5;
public final static boolean a6 = false;
public final static float a7 = 1.0f;
public final static double a8 = 2.0;
private static final String str = "測試方法的內部結構";
// 字節碼
public static final byte a1;
descriptor: B
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 1
public static final short a2;
descriptor: S
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 2
public static final char a3;
descriptor: C
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 3
public static final int a4;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 4
public static final long a5;
descriptor: J
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: long 5l
public static final boolean a6;
descriptor: Z
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 0
public static final float a7;
descriptor: F
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: float 1.0f
public static final double a8;
descriptor: D
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: double 2.0d
private static final java.lang.String str;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
ConstantValue: String 測試方法的內部結構
// 引用類型 在static代碼塊中賦值
private static final String s2 = new String("測試");
private static final ATest aTest = new ATest();
// 字節碼
private static final java.lang.String s2;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
private static final ATest aTest;
descriptor: LATest;
flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=3, locals=0, args_size=0
0: new #14 // class java/lang/String
3: dup
4: ldc #16 // String 測試
6: invokespecial #17 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: putstatic #18 // Field s2:Ljava/lang/String;
12: new #19 // class ATest
15: dup
16: invokespecial #20 // Method ATest."<init>":()V
19: putstatic #12 // Field aTest:LATest;
22: return
LineNumberTable:
line 24: 0
line 25: 12
5.5 運行時常量池 & 常量池
-
常量池
常量池是字節碼文件的一部分,包括各種字面量和對類型、域和方法的符號引用
-
爲什麼需要常量池
一個java源文件中的類、接口、編譯後產生一個字節碼文件。字節碼中有可能會有多個方法引用同一個類,如果在每個方法中都保存這個引用,會顯得很冗餘,java虛擬機把這個引用放到了常量池,其它方法都保存符號引用,指向常量池
-
常量池中有什麼
1. 數量值
2. 字符串值
3. 類引用
4. 字段引用
5. 方法引用
常量池可以看做一張表,虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等類型
- 運行時常量池
1. 運行時常量池是方法區的一部分
2. 常量池是Class文件的一部分,用於存放編譯期的各種字面量與符號引用,
這部分內容將在類加載後存放到方法區的運行時常量池中
3. 運行時常量池,在加載類和接口到虛擬機後,就會創建對應的運行時常量池
4. JVM爲每個已加載的類型(類或接口)都維護一個常量池。池中的數據項像數組項一樣,是通過索引訪問的
5. 運行時常量池中包含多種不同的常量,包括編譯期就已經明確的數值字面量,
也包括到運行期解析後才能夠獲得的方法或者字段引用,此時已經不再是常量池中的符號地址了,這裏換爲真實地址
5.1 運行時常量池,相對於Class文件中的常量池的另一個重要特徵就是具備動態性,
可以動態添加常量(String.intern())
6. 運行時常量池的數據要比常量池的數據更加豐富一些
7. 當創建類或接口的運行時常量池時,如果構建運行時常量池所需的內存空間
超過了最大能提供的空間,則JVM會拋OutOfMemoryError異常
6. 方法區的演進細節
永久代是HotSpot虛擬機中的概念,是java虛擬機規範方法區的落地實現
jdk版本 | 演進內容 |
---|---|
jdk6及之前 | 有永久代(permanent generation),靜態變量存放在永久代上 |
jdk7 | 有永久代,但已經逐步“去永久代”,字符串常量池、靜態變量移除,保存在堆中 |
jdk8及以後 | 無永久代,類型信息、字段、方法、常量保存在本地內存的元空間,但字符串常量池、靜態變量仍在堆 |
- 永久代爲什麼要被元空間替換?
1. 永久代設置空間大小是很難確定的
隨着空能的擴展,加載的類變多,容易出現oom
而元空間和本地空間的區別在於:元空間使用的是本地內存,內存空間不受虛擬機內存大小控制,不容易出現oom
2. 對永久代的調優是很困難的
- StringTable爲什麼要調整?
jdk7將StringTable放到了堆空間中。因爲永久代的回收效率很低,
在full gc的時候纔會觸發,而full gc是老年代、永久代不足時纔會觸發。
這就導致StringTable回收效率不高。而我們開發中會有大量的字符串被創建,回收效率低,導致永久代內存不足,放在堆裏,能及時回收內存。
- 靜態變量放在哪裏?
/**
* 結論:
* 靜態引用對應的對象實體始終都存在堆空間
*
* jdk7:
* -Xms200m -Xmx200m -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails
* jdk 8:
* -Xms200m -Xmx200m -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
* @author shkstart [email protected]
* @create 2020 21:20
*/
public class StaticFieldTest {
private static byte[] arr = new byte[1024 * 1024 * 100];//100MB
public static void main(String[] args) {
System.out.println(StaticFieldTest.arr);
// try {
// Thread.sleep(1000000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
結論1
靜態引用對應的對象實體始終都存在堆空間,jdk678版本new byte[1024*1024*1024]都在堆上分配
/**
* 《深入理解Java虛擬機》中的案例:
* staticObj、instanceObj、localObj存放在哪裏?
* @author shkstart [email protected]
* @create 2020 11:39
*/
public class StaticObjTest {
static class Test {
static ObjectHolder staticObj = new ObjectHolder();
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();
}
}
結論2
三個new出來的對象,都存放在堆區,instanceObj跟隨Test對象實例放在堆區,localObj跟隨foo棧幀放在棧區,staticObj跟隨Class對象放在堆區
關於類的元數據和Class對象
參考
Class對象是存放在堆區的,不是方法區,這點很多人容易犯錯。類的元數據(元數據並不是類的Class對象!Class對象是加載的最終產品,類的方法代碼,變量名,方法名,訪問權限,返回值等等都是在方法區的)纔是存在方法區的。
7. 方法區的垃圾回收
- 主要回收內容
1. 常量池中廢棄的常量
a) 字面量
文本字符串
被聲明爲final的常量
b) 符號引用
類和接口的全限定名
字段的名稱和描述符
方法的名稱和描述符
2. 不再使用的類
- 什麼時候回收
1. 常量池回收
常量池中的常量沒有被任何地方引用,就可以回收
2. 不再使用的類回收
2.1 所有實例被回收
2.2 加載該類的類加載器被回收
2.3 Class對象沒有再任何地方被引用