1. 程式人生 > 實用技巧 >終於有人把Java記憶體區域說清楚了

終於有人把Java記憶體區域說清楚了

很多人會誤以為Java記憶體區域和記憶體模型是同一個東西,其實並不是。

Java記憶體區域是指 JVM執行時將資料分割槽域儲存 ,簡單的說就是不同的資料放在不同的地方。通常又叫 執行時資料區域

Java記憶體模型(JMM)定義了程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體中取出變數這樣的底層細節。

1、Java記憶體區域

1.8 之前:
JDK1.8(含)之後:

區別就是 1.8有一個元資料區替代方法區了。

JDK 1.7 其實是並沒完全移除方法區,但是不會像1.6以前報 “java.lang.OutOfMemoryError: PermGen space

”,而是報 java.lang.OutOfMemoryError: Java heap space

1.7部分內容(比如 常量池、靜態變數有方法區轉移到了堆)

演變

那麼,Java 8 中 PermGen 為什麼被移出 HotSpot JVM 了?我總結了兩個主要原因(詳見:JEP 122: Remove the Permanent Generation):

  1. 由於 PermGen 記憶體經常會溢位,引發惱人的 java.lang.OutOfMemoryError: PermGen,因此 JVM 的開發者希望這一塊記憶體可以更靈活地被管理,不要再經常出現這樣的 OOM
  2. 移除 PermGen 可以促進 HotSpot JVM 與 JRockit VM 的融合,因為 JRockit 沒有永久代。

根據上面的各種原因,PermGen 最終被移除,方法區移至 Metaspace,字串常量移至 Java Heap

下面逐一介紹一下jvm管轄的這幾種記憶體區域。

2、程式計數器

程式計數器(Program Counter Register)是一塊較小的記憶體空間,由於JVM可以併發執行執行緒,因此會存線上程之間的切換,而這個時候就程式計數器會記錄下當前程式執行到的位置,以便在其他執行緒執行完畢後,恢復現場繼續執行。

JVM會為每個執行緒分配一個程式計數器,與執行緒的生命週期相同。

如果執行緒正在執行的是應該Java方法,這個計數器記錄的是正在執行虛擬機器位元組碼指令的地址。

如果正在執行的是Native方法,計數器的值則為空(undefined)

程式計數器是唯一一個在 Java 虛擬機器規範中沒有規定任何 OutOfMemoryError 情況的區域。

3、Java虛擬機器棧

虛擬機器棧 描述的是 Java 方法執行的記憶體模型:

每個方法在執行的同時都會建立一個棧幀(Stack Frame,是方法執行時的基礎資料結構)用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。

虛擬機器棧是每個執行緒獨有的,隨著執行緒的建立而存在,執行緒結束而死亡。

在虛擬機器棧記憶體不夠的時候會OutOfMemoryError,線上程執行中需要更大的虛擬機器棧時會出現StackOverFlowError

虛擬機器棧包含很多棧幀,每個方法執行的同時會建立一個棧幀,棧幀又儲存了方法的區域性變量表、運算元棧、動態連線和方法返回地址等資訊。

在活動執行緒中,只有位於棧頂的棧幀才是有效的,稱為當前棧幀,與這個棧幀相關聯的方法稱為當前方法。

1)區域性變量表

區域性變量表是存放方法引數區域性變數的區域。

全域性變數是放在堆的,有兩次賦值的階段,一次在類載入的準備階段,賦予系統初始值;另外一次在類載入的初始化階段,賦予程式碼定義的初始值。

而區域性變數沒有賦初始值是不能使用的。

2)運算元棧

一個先入後出的棧。

當一個方法剛剛開始執行的時候,這個方法的運算元棧是空的,在方法的執行過程中,會有各種位元組碼指令往運算元棧中寫入和提取內容,也就是出棧/入棧操作。

3) 動態連線

每個棧幀都包含一個指向執行時常量池中該棧幀所屬方法的引用。持有這個引用是為了支援方法呼叫過程中的動態連線(Dynamic Linking)。

常量池可以便於指令的識別

    public void methodA(){
        
    }
    public void methodB(){
        methodA();//methodB()呼叫methodA(),先找到呼叫methodA()的版本符號,再變為直接引用
    }

方法呼叫並不等同於方法執行,方法呼叫階段唯一的任務就是確定被呼叫方法的版本(即呼叫哪一個方法),這也是Java強大的擴充套件能力,在執行期間才能確定目標方法的直接引用

所有方法呼叫中的目標方法在Class檔案裡面都是一個常量池中的符號引用,在類載入的解析階段,會將其中的一部分符號引用轉化為直接引用。

4)方法返回地址(方法出口)

返回分為 正常返回 和 異常退出。

無論何種退出情況,都將返回至方法當前被呼叫的位置,這也程式才能繼續執行。

一般來說,方法正常退出時,呼叫者的PC計數器的值可以作為返回地址,棧幀中會儲存這個計數器值。

方法退出的過程相當於彈出當前棧幀。

4、本地方法棧

Java虛擬機器棧是呼叫Java方法;本地方法棧是呼叫本地native方法,可以認為是通過 JNI (Java Native Interface) 直接呼叫本地 C/C++ 庫,不受JVM控制。

Native方法 Java虛擬機器棧與本地方法棧的呼叫過程

本地方法棧也會丟擲 StackOverflowErrorOutOfMemoryError 異常

5、Java堆

Java 堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。

堆是垃圾收集器管理的主要區域,又稱為“GC堆”,可以說是Java虛擬機器管理的記憶體中最大的一塊。

現在的虛擬機器(包括HotSpot VM)都是採用分代回收演算法。在分代回收的思想中, 把堆分為:新生代+老年代+永久代(1.8沒有了); 新生代 又分為 Eden + From Survivor + To Survivor區。

6、方法區

方法區(Method Area)與 Java 堆一樣,是所有執行緒共享的記憶體區域。

方法區用於儲存已經被虛擬機器載入的類資訊(即載入類時需要載入的資訊,包括版本、field、方法、介面等資訊)、final常量、靜態變數、編譯器即時編譯的程式碼等。

方法區邏輯上屬於堆的一部分,但是為了與堆進行區分,通常又叫“非堆”。

方法區比較重要的一部分是執行時常量池(Runtime Constant Pool),為什麼叫執行時常量池呢?是因為執行期間可能會把新的常量放入池中,比如說常見的String的intern()方法。

String a = "I am HaC";
Integer b = 100;

在編譯階段就把所有的字串文字放到一個常量池中,複用同一個(比如說上述的“I am HaC”),節省空間。

關於方法區和元空間的關係:

方法區是JVM規範概念,而永久代則是Hotspot虛擬機器特有的概念,簡單點理解:方法區和堆記憶體的永久代其實一個東西,但是方法區是包含了永久代。

只有 HotSpot 才有 “PermGen space”,而對於其他型別的虛擬機器,如 JRockit(Oracle)、J9(IBM) 並沒有“PermGen space”

7、元空間

1.8就把方法區改用元空間了。類的元資訊被儲存在元空間中。元空間沒有使用堆記憶體,而是與堆不相連的本地記憶體區域。所以,理論上系統可以使用的記憶體有多大,元空間就有多大,所以不會出現永久代存在時的記憶體溢位問題。

可以通過 -XX:MetaspaceSize-XX:MaxMetaspaceSize 來指定元空間的大小。

8、總結:

Java記憶體區域

有完整的Java初級,高階對應的學習路線和資料!專注於java開發。分享java基礎、原理性知識、JavaWeb實戰、spring全家桶、設計模式、分散式及面試資料、開源專案,助力開發者成長!


歡迎關注微信公眾號:碼邦主

參考:


作者:HelloCoder的HaC
連結:https://www.jianshu.com/p/2ff1a9cebce9
來源:簡書