1. 程式人生 > >java記憶體管理機制(一)-執行時資料區

java記憶體管理機制(一)-執行時資料區

前言

  本打算花一篇文章來聊聊JVM記憶體管理機制,結果發現越扯越多,於是分了三遍文章(文章講解JVM以Hotspot虛擬機器為例,jdk版本為1.8),本文為其中第一篇。from java記憶體管理機制(一)-執行時資料區 
  1、 java記憶體管理機制-執行時資料區
  2、 java記憶體管理機制-記憶體分配
  3、 java記憶體管理機制-垃圾回收

正文

  C++與java之間有一堵由記憶體動態分配和垃圾收集技術所圍成的“高牆”,牆外的人想進去,牆裡的人卻想出來……

  與C、C++程式設計師時刻要關注著記憶體的分配與釋放,會不會又有哪裡出現了記憶體洩露不同是,java程式設計師可以“高枕無憂”。因為這一切都已經有jvm來幫我們管理了,java程式設計師只需要關注具體的業務邏輯就可以了,至於記憶體分配與回收,交給jvm去幹吧。但這樣也帶來一個問題,我們不再去關注記憶體分配了,不再去關注記憶體回收了。一旦出現記憶體洩露就束手無策了,在不同的應用場景,怎麼樣去做效能調優就成了一個問題。所以,對於java程式設計師來說,這些是必須瞭解的一部分。

  沒有物件怎麼辦?new一個啊。單身狗程式設計師每次提到new物件都激動不已,可是你的物件是怎麼new出來的?new出來又放在哪裡?怎麼引用的?你的物件被別人動了怎麼辦?使用完成之後又是如何釋放的?何時釋放的?等等等等這些問題,如果你不能很輕鬆的回答出來,那麼在本系列文章中你可能會找到一些答案。當然,本人才疏學淺,文筆拙劣,只是拋磚引玉,理解不周到或者有誤的地方,歡迎拍磚。

  JVM記憶體區域可以大致劃分為“執行緒隔離區域”和“執行緒共享區域”。所謂“執行緒隔離區域”即執行緒非共享區域,每個執行緒獨享的,執行指令操作機存放私有資料。不管做什麼操作,不會影響到其他執行緒。可以想象成,你個人電腦硬碟中的蒼老師,只能你一個人在夜深人靜的時候拉上窗簾獨自享受,別人無法同你分享,你刪除或者新下載也不會對別人造成影響。而“執行緒共享區域”則是所有的執行緒共同擁有的,主要存放物件例項資料。如果A執行緒對這塊區域的某個資料進行了修改,而剛好B執行緒正在使用或者需要使用該資料,則A執行緒對資料的修改在B執行緒中也會得到體現。可以想象成你把蒼老師傳到了某社群,這時候網上其他人都能共享你的蒼老師了。當大家看得正興奮的時候,你突然刪掉了你上傳的老師,這時候大家都只能去尋找新的素材了………,不知道你是否對“執行緒隔離區域”和“執行緒共享區域”的概念有了個大致瞭解。在jvm中,執行緒隔離區域包含程式計數器、本地方法棧、虛擬機器棧。執行緒共享區域包含堆區、永久代(jdk1.8中廢除永久代)、直接記憶體(jdk1.8中新增)(看下圖)

一、這是我的私人住所,我不同意,你們別來!-執行緒隔離區域

  執行緒隔離區域存放什麼資料呢?區域性變數、方法呼叫的壓棧操作等。執行緒隔離區域包含巴拉巴拉……(看下圖)

1、睡了一覺,剛剛我做到哪了?-程式計數器

  我們都知道在多執行緒的場景下,會發生執行緒切換,如果當前執行的執行緒讓出執行權,則執行緒會被掛起,當執行緒再次被喚醒的時候,如果沒有程式計數器執行緒可能就懵逼了,我是誰?我在哪?我要做什麼?。但是如果有了程式計數器,執行緒就能找到上次執行到的位元組碼的位置繼續往下執行。程式計數器可以理解為當前執行緒正在執行的位元組碼指令的行號指示器。分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。

  查閱了一些資料,列出了程式計數器的三個特點,這裡也列舉一下

  1)、如果執行緒正在執行的是Java 方法,則這個計數器記錄的是正在執行的虛擬機器位元組碼指令地址
  2)、如果正在執行的是Native 方法,則這個計數器值為空(Undefined)。因為Native方法大多是通過C實現並未編譯成需要執行的位元組碼指令。那native 方法的多執行緒是如何實現的呢? native 方法是通過呼叫系統指令來實現的,那系統是如何實現多執行緒的則 native 就是如何實現的。Java執行緒總是需要以某種形式對映到OS執行緒上,對映模型可以是1:1(原生執行緒模型)、n:1(綠色執行緒 / 使用者態執行緒模型)、m:n(混合模型)。以HotSpot VM的實現為例,它目前在大多數平臺上都使用1:1模型,也就是每個Java執行緒都直接對映到一個OS執行緒上執行。此時,native方法就由原生平臺直接執行,並不需要理會抽象的JVM層面上的“pc暫存器”概念——原生的CPU上真正的PC暫存器是怎樣就是怎樣。就像一個用C或C++寫的多執行緒程式,它線上程切換的時候是怎樣的,Java的native方法也就是怎樣的。
  3)、此記憶體區域是唯一一個在Java虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域(程式執行過程中計數器中改變的只是值,而不會隨著程式的執行需要更大的空間)

2、自己的事情自己做!-虛擬機器棧

  這個區域就是我們經常所說的棧,是java方法執行的記憶體模型,也是我們在開發中接觸得很多的一塊區域。虛擬機器棧存放當前正在執行方法的時候所需要的資料、地址、指令。每個執行緒都會獨享一塊棧空間,每次方法呼叫都會建立一個棧幀,棧幀儲存了方法的區域性區域性變數、運算元棧、動態連結、出口等資訊。棧幀的深度也是有限制的,超過限制會丟擲StackOverflowError異常。

  我們結合一個例子來了解一下虛擬機器棧和棧幀,我們有如下程式碼:

複製程式碼

public class myProgram {
public static void main(String[] args) {
String str = "my String";
methodOne(1);
}

public static void methodOne(int i) {
int j = 2;
int sum = i + j;

// ......
methodTwo();
// .....
}

public static void methodTwo() {

if (true) {
int j = 0;
}

if (true) {
int k = 1;
}

return;
}
}

複製程式碼

 

  程式碼很簡單,main呼叫methodOne,methodOne呼叫methodTwo,如果當前正在執行methodTwo方法,則虛擬機器棧中棧幀的情況應該是如下圖情況,棧頂為正在執行的方法。

  我們能看到,每個棧幀都包含區域性變量表,運算元棧、動態連結、返回地址等……

  1)、區域性變量表
  顧名思義,區域性變量表就是存放區域性變數的表,區域性變數包括方法形參、方法內部定義的區域性變數。區域性變量表由多個變數槽(slot)組成,每個槽位都有個索引號,索引的範圍是從0開始至區域性變數最大的slot空間,虛擬機器就是通過索引定位的方式使用區域性變量表。比如在methodOne方法中,形參i就是在0號索引的slot中,區域性變數j就放在1號索引的slot中,我們看看結合methodOne方法的位元組碼進行分析(通過javap -verbose myProgram檢視位元組碼檔案)。
 

複製程式碼

public static void methodOne(int);
descriptor: (I)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: iconst_2
1: istore_1
2: iload_0
3: iload_1
4: iadd
5: istore_2
6: invokestatic #4 // Method methodTwo:()V
9: return
LineNumberTable:
line 8: 0
line 9: 2
line 12: 6
line 14: 9

複製程式碼

  0:載入int型別常量2
  1:儲存到索引為1的變數中(這裡指源程式中的j)
  2:載入索引為0的變數(這裡指源程式中的i)
  3:載入索引為1的變數(這裡指源程式中的j)
  4:執行add指令
  5:將執行結果儲存到索引為2的變數中(這裡指源程式中的sum)
  6:靜態呼叫

  需要注意的一點是,為了儘可能節省棧幀的空間,區域性變量表中的slot是可以重用的,方法體重定義的變數,其作用域不一定會覆蓋整個方法體,我們看看methodTwo的原始碼,第一個if和第二個if的作用域不一樣,所以內部變數可能是用的同一個slot,我們可以通過methodTwo方法的位元組碼來驗證一下
 

複製程式碼

public static void methodTwo();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: iconst_0
1: istore_0
2: iconst_1
3: istore_0
4: return
LineNumberTable:
line 19: 0
line 23: 2
line 26: 4

複製程式碼

  你看,我沒騙你吧,methodTwo方法兩個if中的變數j和k,使用的都是索引為0的slot。這樣的設計可以節省棧幀的空間,同時也會影響jvm的垃圾回收,因為區域性變量表是GC Root的一部分,區域性變量表slot中當前存放的變數關聯的物件為可達物件(後面講到垃圾回收時候再詳細講)。

  2)、運算元棧
  運算元棧也是一個棧,也看可以成為表示式棧。運算元棧和區域性變量表在訪問方式上有著較大的差異,它不是通過索引來訪問,而是通過標準的棧操作—壓棧和出棧—來訪問的。我們對變數的操作都是在運算元棧中完成的,我們依然拿methodOne方法來舉例。再看一下methodOne方法的位元組碼:

複製程式碼

public static void methodOne(int);
descriptor: (I)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: iconst_2
1: istore_1
2: iload_0
3: iload_1
4: iadd
5: istore_2
6: invokestatic #4 // Method methodTwo:()V
9: return
LineNumberTable:
line 8: 0
line 9: 2
line 12: 6
line 14: 9

複製程式碼


  下圖為每一行位元組碼對應運算元棧和本地變量表之間的關係,具體看圖,不用多做描述了。

  3)、動態連結
  每個棧幀都包含一個指向執行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支援方法呼叫過程中的動態連線。剛開始看這一段的時候總是覺得很生澀,比較拗口。我們還是繼續看那段程式碼的位元組碼檔案,其中有一段叫做“Constant pool”,裡面儲存了該Class檔案裡的大部分常量的內容(包括類和介面的全限定名、欄位的名稱和描述符以及方法的名稱和描述符)。

  不知道你有沒有注意我們位元組碼中是怎麼處理menthodOne方法的呼叫的?在main方法中呼叫methodone方法的位元組碼為invokestatic #3,這裡的#3就是一個” 符號引用”,我們發現#3還引用著另外的常量池專案,順著這條線把能傳遞到的常量池項都找出來(標記為Utf8的常量池項)。由此我們可以看出,invokestatic 指令就是以常量池中指向方法的符號引用作為引數,完成方法的呼叫。這些符號引用一部分在類的載入階段(解析)或第一次使用的時候就轉化為了直接引用(指向資料所存地址的指標或控制代碼等),這種轉化稱為靜態連結。而相反的,另一部分在執行期間轉化為直接引用,就稱為動態連結。我們看一下位元組碼中的常量池和符號引用,注意main方法中的#2 #3:

複製程式碼

Constant pool:
#1 = Methodref #6.#18 // java/lang/Object."<init>":()V
#2 = String #19 // my String
#3 = Methodref #5.#20 // myProgram.methodOne:(I)V
#4 = Methodref #5.#21 // myProgram.methodTwo:()V
#5 = Class #22 // myProgram
#6 = Class #23 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 methodOne
#14 = Utf8 (I)V
#15 = Utf8 methodTwo
#16 = Utf8 SourceFile
#17 = Utf8 myProgram.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = Utf8 my String
#20 = NameAndType #13:#14 // methodOne:(I)V
#21 = NameAndType #15:#8 // methodTwo:()V
#22 = Utf8 myProgram
#23 = Utf8 java/lang/Object

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: ldc #2 // String my String
2: astore_1
3: iconst_1
4: invokestatic #3 // Method methodOne:(I)V
7: return
LineNumberTable:
line 3: 0
line 4: 3
line 5: 7

複製程式碼

  4)、返回地址
  我們的經常使用return x;來使方法返回一個值給方法呼叫者,如果沒有返回值的方法也可以在方法的方法需要返回的地方加上return;當然,這不是必須的,因為原始碼在轉化為位元組碼的時候,總是會在方法的最後加上return指令,不信你看上面methodTwo方法的位元組碼那張圖片。

  正常情況下,方法遇到返回指令退出,這種退出方法的方式稱為正常完成出口。如果方法正常返回,則當前棧幀從java棧中彈出,恢復發起呼叫者的方法的棧幀,如果方法有返回值,jvm會把返回值壓入到發起呼叫方法的運算元棧。但是在異常情況下,方法執行遇到了異常,且這個異常在方法體內未得到處理,方法則會異常退出,這種退出方式稱為異常完成出口。當異常丟擲且沒有被捕捉時,則方法立即終止,然後JVM恢復發起呼叫的方法的棧幀,如果在呼叫者中也未對異常進行捕捉,則呼叫者也會立即終止,層層向上,直到最外層丟擲異常。

 

3、樓上做不了的事情,來我這做!-本地方法棧

  本地方法是什麼?本地方法就是在jdk中(也可以自定義)那些被Native關鍵字修飾的方法(下圖)。這類方法有點類似java中的介面,沒有實現體,但實際上是由jvm在載入時呼叫底層實現的,實現體是由非java語言(如C、C++)實現的,所以本地方法可以理解為連線java程式碼和其他語言實現的程式碼的入口。而本地方法棧的功能就類似於虛擬機器棧,只是一個服務於java方法執行,一個服務於執行本地方法執行。

 

二、來啊,快活啊!反正有大把空間!-執行緒共享區域

1、 喂,你的物件都在這裡!-堆

  堆區域在jvm中是非常重要的一塊區域,因為我們平常建立的物件的例項就存在在這個區域,這個區域的幾乎是被所有執行緒共享。同時也是java虛擬機器管理的記憶體中最大的一塊。由於目前主流的垃圾收集器都採用分代收集演算法,所以通常將堆細分為新生代、老年代,新生代又分為兩塊Eden區、From Survivor區、To Survivor區(這裡主要針對通常使用的分代收集器,G1收集器採用不同的劃分策略,後面有機會再講)。不過不管怎麼劃分,目的都是為了更合理的利用記憶體,提高記憶體空間使用率,提高垃圾回收的效率和回收質量。下圖展示了堆區域的劃分

  我們在這篇文章裡只談堆區記憶體的劃分,關於記憶體分配、記憶體回收等會在下篇文章細講,因為涉及的內容太多了……不過我們可以先思考幾個問題1、為什麼需要區分新生代、老年代?2、為什麼將新生代分為Eden、Survivor區?各區大小怎麼分配?有什麼分配依據?
 

2、 治不了你?那我就廢了你!-方法區

  看標題可能會有些誤解,其實這裡廢除的是永久代的概念,而不是方法區。剛開始總是搞不清這兩者的關係,後來就去查閱了一些資料總算是搞清楚了一些,書上是這麼說的:“JVM的虛擬機器規範只是規定了有方法區這麼個概念和它的作用,並沒有規定如何去實現它。不同JVM的方法區的實現會不一樣,比如在HotSpot中使用永久代實現方法區,其他JVM並沒有永久代的概念。方法區是一種規範,永久代是一種實現。”

  所以,我們常說的新生代、老年代、永久代中的永久代就是方法區的一種實現,且只存在於HotSpot虛擬機器中有這種概念。用過jdk1.8之前的版本(HotSpot虛擬機器)的同學應該經常能碰到永久代溢位的異常“java.lang.OutOfMemoryError: PermGen space”,這裡的PermGen space指的是永久代。在jdk6中,永久代包含方法區和常量池,但是在jdk1.7的版本中規劃去除永久代,於是在1.7中將常量池移到了老年代中。在jdk1.8中徹底廢除了永久代,取而代之的是元空間。

 

3、 會有天使替我去愛你!-直接記憶體

  永久代設定太大吧,浪費資源!永久代設定太小吧,溢位了!於是讓人惱火的永久代溢位的異常時常發生,並且永久代的GC效率低下,於是,在jdk1.8中徹底廢除了永久區,放到了直接記憶體的元空間中!元空間的本質和永久代類似,都是對JVM規範中方法區的實現。元空間相比永久代有什特性呢?永久代在物理上是堆的一部分,與新生代老年代的地址是連續的,而元空間屬於本地記憶體,不受JVM控制,也不會發生永久代溢位的異常。

  直接記憶體也可以稱為堆外記憶體,為什麼要將方法區放入到直接記憶體呢?
  1、 永久代會為 GC 帶來不必要的複雜度,並且回收效率偏低。
  2、 類及方法的資訊等比較難確定其大小,因此永久代調優較為困難,容易發生記憶體溢位。
  3、 加快了複製的速度。因為堆內在flush到遠端時,會先複製到直接記憶體(非堆記憶體),然後再發送,而堆外記憶體相當於省略掉了這個工作。
  4、 Oracle 可能會將HotSpot 與 JRockit 合二為一

鄭州看男科哪家醫院好

鄭州看婦科哪家好

鄭州婦科醫院