運行時數(shù)據(jù)區(qū)結(jié)構(gòu)


堆、棧、方法去的交互關(guān)系

1、介紹:
《Java虛擬機規(guī)范》中明確說明:“盡管所有的方法區(qū)在邏輯上是屬于堆的一部分,但一些簡單的實現(xiàn)可能不會選擇去進(jìn)行垃圾收集或者進(jìn)行壓縮?!钡珜τ贖otSpotJVM而言,方法區(qū)還有一個別名叫做Non-Heap(非堆),目的就是要和堆分開。所以,方法區(qū)看作是一塊獨立于Java堆的內(nèi)存空間。
-
方法區(qū)(Method Area)與Java堆一樣,是各個線程共享的內(nèi)存區(qū)域。
-
方法區(qū)在JVM啟動的時候被創(chuàng)建,并且它的實際的物理內(nèi)存空間中和Java堆區(qū)一樣都可以是不連續(xù)的。
-
方法區(qū)的大小,跟堆空間一樣,可以選擇固定大小或者可擴展。
-
方法區(qū)的大小決定了系統(tǒng)可以保存多少個類,如果系統(tǒng)定義了太多的類,導(dǎo)致方法區(qū)溢出,虛擬機同樣會拋出內(nèi)存溢出錯誤:java.lang.OutofMemoryError:PermGen space (8前)或者 java.lang.OutofMemoryError:Metaspace(8以及以后)
- 加載過多第三方j(luò)ar包;Tomcat部署項目過多;大量動態(tài)的生成反射類
-
關(guān)閉JVM就會釋放這個區(qū)域的內(nèi)存。
別稱:jdk7及以前(永久代),jdk8及以后(元空間)
演變
元空間的本質(zhì)和永久代類似,都是對JVM規(guī)范中方法區(qū)的實現(xiàn)。不過元空間與永久代最大的區(qū)別在于:元空間不在虛擬機設(shè)置的內(nèi)存中,而是使用本地內(nèi)存。
永久代、元空間二者并不只是名字變了,內(nèi)部結(jié)構(gòu)也調(diào)整了。
2、設(shè)置方法區(qū)內(nèi)存大小
- 元數(shù)據(jù)區(qū)大小可以使用參數(shù)
-XX:MetaspaceSize 和-XX:MaxMetaspaceSize 指定,替代上述原有的兩個參數(shù)。
- 默認(rèn)值依賴于平臺。windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,即沒有限制。
- 與永久代不同,如果不指定大小,默認(rèn)情況下,虛擬機會耗盡所有的可用系統(tǒng)內(nèi)存。如果元數(shù)據(jù)區(qū)發(fā)生溢出,虛擬機一樣會拋出異常OutOfMemoryError:Metaspace
- -XX:MetaspaceSize:設(shè)置初始的元空間大小。對于一個64位的服務(wù)器端JVM來說,其默認(rèn)的XX:MetaspaceSize值為21MB。這就是初始的高水位線,一旦觸及這個水位線,F(xiàn)ull GC將會被觸發(fā)并卸載沒用的類(即這些類對應(yīng)的類加載器不再存活)然后這個高水位線將會重置。新的高水位線的值取決于GC后釋放了多少元空間。如果釋放的空間不足,那么在不超過MaxMetaspaceSize時,適當(dāng)提高該值。如果釋放空間過多,則適當(dāng)降低該值。
- 如果初始化的高水位線設(shè)置過低,上述高水位線調(diào)整情況會發(fā)生很多次。通過垃圾回收器的日志可以觀察到Fu11 GC多次調(diào)用。為了避免頻繁地GC,建議將-XX:MetaspaceSize設(shè)置為一個相對較高的值。
如何解決這些OOM?
1、要解決OOM異常或heap space的異常,一般的手段是首先通過內(nèi)存映像分析工具(如Eclipse Memory Analyzer)對dump出來的堆轉(zhuǎn)儲快照進(jìn)行分析,重點是確認(rèn)內(nèi)存中的對象是否是必要的,也就是要先分清楚到底是出現(xiàn)了內(nèi)存泄漏(MemoryLeak)還是內(nèi)存溢出(Memory Overflow)。
2、如果是內(nèi)存泄漏,可進(jìn)一步通過工具查看泄漏對象到GC Roots的引用鏈。于是就能找到泄漏對象是通過怎樣的路徑與GC Roots相關(guān)聯(lián)并導(dǎo)致垃圾收集器無法自動回收它們的。掌握了泄漏對象的類型信息,以及GC Roots引用鏈的信息,就可以比較準(zhǔn)確地定位出泄漏代碼的位置。
3、如果不存在內(nèi)存泄漏,換句話說就是內(nèi)存中的對象確實都還必須存活著,那就應(yīng)當(dāng)檢查虛擬機的堆參數(shù)(-Xmx與-Xms),與機器物理內(nèi)存對比看是否還可以調(diào)大,從代碼上檢查是否存在某些對象生命周期過長、持有狀態(tài)時間過長的情況,嘗試減少程序運行期的內(nèi)存消耗
3、方法區(qū)內(nèi)存結(jié)構(gòu)

3.1、方法區(qū)所存儲的內(nèi)容:

1、類型信息
對每個加載的類型(類class、接口interface、枚舉enum、注解annotation),JVM在方法區(qū)中存儲以下類型信息:
①這個類型的完整有效名稱(全名=包名.類名)
②這個類型直接父類的完整有效名(對于interface或是java.lang.object,都沒有父類)
③這個類型的修飾符(public,abstract,final的某個子集)
④這個類型直接接口的一個有序列表
2、域信息
JVM必須在方法區(qū)中保存類型的所有域的相關(guān)信息以及域的聲明順序。
域的相關(guān)信息包括:域名稱、域類型、域修飾符(public,private,protected, static, final, volatile, transient的某個子集)
3、方法信息
JVM必須保存所有方法的以下信息,同域信息一樣包括聲明順序:
- 方法名稱
- 方法的返回類型(或void)
- 方法參數(shù)的數(shù)量和類型(按順序)
- 方法的修飾符(public,private,protected,static,final,synchronized,native,abstract的一個子集)
- 方法的字節(jié)碼(bytecodes)、操作數(shù)棧、局部變量表及大?。╝bstract和native方法除外)
- 異常表(abstract和native方法除外):每個異常處理的開始位置、結(jié)束位置、代碼處理在程序計數(shù)器中的偏移地址、被捕獲的異常類的常量池索引
查看 命令行輸入 javap -v -p(包含private權(quán)限) xxx.class > xxx.txt
示例:
public class Test extends HashMap implements Serializable {
private String name = "";
private int x = 1;
public Test(String name) {
this.name = name;
}
public static void main(String[] args) {
Test haha = new Test(null);
int nameLength = haha.getNameLength();
System.out.println(nameLength);
}
public int getNameLength() {
int y = 0;
try {
y = name.length();
} catch (NullPointerException e) {
System.out.println("空指針異常");
e.printStackTrace();
}
return y;
}
}
Classfile /D:/ideaFiles/Algorithm/out/production/Algorithm/com/lx/mySort/Test.class
Last modified 2020-7-29; size 1145 bytes
MD5 checksum 8f9825153f3fa6f2042785c0df59703b
Compiled from "Test.java"
//類信息
public class com.lx.mySort.Test extends java.util.HashMap implements java.io.Serializable
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #15.#44 // java/util/HashMap."<init>":()V
#2 = String #45 //
...
{
//域信息
private java.lang.String name;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE
private int x;
descriptor: I
flags: ACC_PRIVATE
//方法信息
...
public int getNameLength();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: iconst_0
1: istore_1
2: aload_0
3: getfield #3 // Field name:Ljava/lang/String;
6: invokevirtual #10 // Method java/lang/String.length:()I
9: istore_1
10: goto 26
13: astore_2
14: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
17: ldc #12 // String 空指針異常
19: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
22: aload_2
23: invokevirtual #14 // Method java/lang/NullPointerException.printStackTrace:()V
26: iload_1
27: ireturn
//異常表
Exception table:
from to target type
2 10 13 Class java/lang/NullPointerException
LineNumberTable:
line 26: 0
line 28: 2
line 32: 10
line 29: 13
line 30: 14
line 31: 22
line 33: 26
LocalVariableTable:
Start Length Slot Name Signature
14 12 2 e Ljava/lang/NullPointerException;
0 28 0 this Lcom/lx/mySort/Test;
2 26 1 y I
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 13
locals = [ class com/lx/mySort/Test, int ]
stack = [ class java/lang/NullPointerException ]
frame_type = 12 /* same */
}
SourceFile: "Test.java"
4、靜態(tài)變量
static final:編譯時(準(zhǔn)備階段)賦給定值
5、運行時常量池
常量池
常量池所在位置

一個有效的字節(jié)碼文件中除了包含類的版本信息、字段、方法以及接口等描述信息外,還包含一項信息那就是常量池表(Constant Pool Table),包括各種字面量和對類型、域和方法的符號引用。
常量池的作用:
一個java源文件中的類、接口,編譯后產(chǎn)生一個字節(jié)碼文件。而Java中的字節(jié)碼需要數(shù)據(jù)支持,通常這種數(shù)據(jù)會很大以至于不能直接存到字節(jié)碼里,換另一種方式,可以存到常量池,這個字節(jié)碼包含了指向常量池的引用。在動態(tài)鏈接的時候會用到運行時常量池,之前有介紹。
常量池存儲的數(shù)據(jù)
- 數(shù)量值
- 字符串值
- 類引用
- 字段引用
- 方法引用
運行時常量池
-
運行時常量池(Runtime Constant Pool)是方法區(qū)的一部分。
-
常量池表(Constant Pool Table)是class文件的一部分。
-
運行時常量池,在加載類和接口到虛擬機后,就會創(chuàng)建對應(yīng)的運行時常量池。
-
JVM為每個已加載的類型(類或接口)都維護(hù)一個常量池。
-
運行時常量池中包含多種不同的常量,包括編譯期就已經(jīng)明確的數(shù)值字面量,也包括到運行期解析后才能夠獲得的方法或者字段引用。此時不再是常量池中的符號地址了,這里換為真實地址。
- 運行時常量池,相對于class文件常量池的另一重要特征是:具備動態(tài)性。
-
當(dāng)創(chuàng)建類或接口的運行時常量池時,如果構(gòu)造運行時常量池所需的內(nèi)存空間超過了方法區(qū)所能提供的最大值,則JVM會拋OutOfMemoryError異常。
4、演進(jìn)過程




永久代為什么會被元空間替換
- 永久代空間大小很難確定,太小容易GC/OOM異常,太大占用內(nèi)存(元空間并不在虛擬機中、而是使用本地內(nèi)存,大小僅受本地內(nèi)存限制)
- 永久代調(diào)優(yōu)困難
- 垃圾回收頻率低
靜態(tài)變量放到哪里?
堆空間里的永久代(7及后是堆)
5、方法區(qū)的垃圾回收
主要回收:
1、常量池中廢棄的常量:字面量和符號引用...(沒用被引用,則可以進(jìn)行回收)
2、不再使用的類型(同時滿足以下三個條件的類可被允許回收):
1)該類的所有實例都被回收了,即Java堆中不存在該類及其任何派生的子類的實例
2)該類的類加載器已經(jīng)被回收了(除非精心設(shè)計,否則很難實現(xiàn),如OSGI,JSP的重加載等)
3)該類對象對應(yīng)的java.lang.Class對象沒有在任何地方引用、無法在任何地方通過反射訪問到該類的方法
關(guān)于是否要對類型進(jìn)行回收,HotSpot虛擬機提供了-Xnoclassgc參數(shù)進(jìn)行控制,還可以使用-verbose:class以及
-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看類加載和卸載信息
5、小結(jié)

6、對象實例化





初始化:
- 默認(rèn)初始化
- 顯示初始化 / 代碼塊初始化 / 構(gòu)造器初始化
7、對象的內(nèi)存布局:

示例:


8、對象的訪問定位

8.1、訪問對象的方式:
-
句柄訪問
reference中存儲穩(wěn)定句柄地址,對象被移動(垃圾回收時常見)時只會改變句柄池中到對象示例數(shù)據(jù)的指針即可,reference不用修改

-
直接指針(HotSpot采用)
內(nèi)存相對于較小

9、直接內(nèi)存
-
不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是《Java虛擬機規(guī)范》中定義的內(nèi)存區(qū)域。
-
直接內(nèi)存是在Java堆外的、直接向系統(tǒng)申請的內(nèi)存區(qū)間。
-
來源于NIO,通過存在堆中的DirectByteBuffer操作Native內(nèi)存
-
通常,訪問直接內(nèi)存的速度會優(yōu)于Java堆。即讀寫性能高。
- 因此出于性能考慮,讀寫頻繁的場合可能會考慮使用直接內(nèi)存。
- Java的NIO庫允許Java程序使用直接內(nèi)存,用于數(shù)據(jù)緩沖區(qū)
關(guān)注公眾號:java寶典
|