你必須瞭解的java內存管理機制(一)-運行時數據區

前言

  本打算花一篇文章來聊聊JVM內存管理機制,結果發現越扯越多,因而分了四遍文章(文章講解JVM以Hotspot虛擬機爲例,jdk版本爲1.8),本文爲其中第一篇。from 你必須瞭解的java內存管理機制-運行時數據區
  相關連接(注:文章講解JVM以Hotspot虛擬機爲例,jdk版本爲1.8,我的技術博客www.17coding.info
  一、 你必須瞭解的java內存管理機制-運行時數據區
  二、 你必須瞭解的java內存管理機制-內存分配
  三、 你必須瞭解的java內存管理機制-垃圾標記
  四、 你必須瞭解的java內存管理機制-垃圾回收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狀況的區域(程序運行過程當中計數器中改變的只是值,而不會隨着程序的運行須要更大的空間)jvm

二、本身的事情本身作!-虛擬機棧

  這個區域就是咱們常常所說的棧,是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方法,則虛擬機棧中棧幀的狀況應該是以下圖狀況,棧頂爲正在執行的方法。

spa

  咱們能看到,每一個棧幀都包含局部變量表,操做數棧、動態連接、返回地址等……

  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方法的字節碼:3d

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恢復發起調用的方法的棧幀,若是在調用者中也未對異常進行捕捉,則調用者也會當即終止,層層向上,直到最外層拋出異常。

 

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

  本地方法是什麼?本地方法就是在jdk中(也能夠自定義)那些被Native關鍵字修飾的方法(下圖)。這類方法有點相似java中的接口,沒有實現體,但其實是由jvm在加載時調用底層實現的,實現體是由非java語言(如C、C++)實現的,因此本地方法能夠理解爲鏈接java代碼和其餘語言實現的代碼的入口。而本地方法棧的功能就相似於虛擬機棧,只是一個服務於java方法執行,一個服務於執行本地方法執行。



2、來啊,快活啊!反正有大把空間!-線程共享區域

一、 喂,你的對象都在這裏!-堆

  堆區域在jvm中是很是重要的一塊區域,由於咱們日常建立的對象的實例就存在在這個區域,這個區域的幾乎是被全部線程共享。同時也是java虛擬機管理的內存中最大的一塊。因爲目前主流的垃圾收集器都採用分代收集算法,因此一般將堆細分爲新生代、老年代,新生代又分爲兩塊Eden區、From Survivor區、To Survivor區(這裏主要針對一般使用的分代收集器,G1收集器採用不一樣的劃分策略,後面有機會再講)。不過無論怎麼劃分,目的都是爲了更合理的利用內存,提升內存空間使用率,提升垃圾回收的效率和回收質量。下圖展現了堆區域的劃分

  咱們在這篇文章裏只談堆區內存的劃分,關於內存分配、內存回收等會在下篇文章細講,由於涉及的內容太多了……不過咱們能夠先思考幾個問題一、爲何須要區分新生代、老年代?二、爲何將新生代分爲Eden、Survivor區?各區大小怎麼分配?有什麼分配依據?

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

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

  因此,咱們常說的新生代、老年代、永久代中的永久代就是方法區的一種實現,且只存在於HotSpot虛擬機中有這種概念。用過jdk1.8以前的版本(HotSpot虛擬機)的同窗應該常常能碰到永久代溢出的異常「java.lang.OutOfMemoryError: PermGen space」,這裏的PermGen space指的是永久代。在jdk6中,永久代包含方法區和常量池,可是在jdk1.7的版本中規劃去除永久代,因而在1.7中將常量池移到了老年代中。在jdk1.8中完全廢除了永久代,取而代之的是元空間。

 

三、 會有天使替我去愛你!-直接內存

  永久代設置太大吧,浪費資源!永久代設置過小吧,溢出了!因而讓人惱火的永久代溢出的異常時常發生,而且永久代的GC效率低下,因而,在jdk1.8中完全廢除了永久區,放到了直接內存的元空間中!元空間的本質和永久代相似,都是對JVM規範中方法區的實現。元空間相比永久代有什特性呢?永久代在物理上是堆的一部分,與新生代老年代的地址是連續的,而元空間屬於本地內存,不受JVM控制,也不會發生永久代溢出的異常。   直接內存也能夠稱爲堆外內存,爲何要將方法區放入到直接內存呢?  一、 永久代會爲 GC 帶來沒必要要的複雜度,而且回收效率偏低。  二、 類及方法的信息等比較難肯定其大小,所以永久代調優較爲困難,容易發生內存溢出。  三、 加快了複製的速度。由於堆內在flush到遠程時,會先複製到直接內存(非堆內存),而後再發送,而堆外內存至關於省略掉了這個工做。  四、 Oracle 可能會將HotSpot 與 JRockit 合二爲一

相關文章
相關標籤/搜索