JVM自動內存管理機制——Java內存區域
一、JVM運行時數據區域概述
Java相比較於C/C++的一個特點就是,在虛擬機自動內存管理機制的幫助下,我們不需要為每一個操作都寫像C/C++一樣的delete/free代碼,所以也不容易出現內存泄漏和內存溢出的問題。顯然,這裏的不容易只是相對而言的,如果我們想要降低這種代碼隱患的發生,就需要對Java虛擬機怎樣使用內存有了解,這樣的話就算產生錯誤,排查起來也會相對容易。下面我們來說一說JVM運行時數據區域
1、程序計數器(PC寄存器): 被看作是當前線程所執行的字節碼的行號指示器,字節碼解析的時候就通過改變這個計數器的值來選取下一條需要執行的指令(包括分支、循環、跳轉、異常處理等等都依賴PC)。在多線程程序中,每條線程都需要擁有一個獨立的程序計數器,所以程序計數器是線程私有的。(如果程序正在執行一個Java方法,那麽這個計數器記錄的就是當前執行的字節碼指令的地址;如果執行的是一個Native方法,那麽計數器為Undefined),該內存區域是JVM規範中唯一一個沒有規定OutOfMemoryError的區域。
2、Java虛擬機棧:虛擬機棧描述的是Java方法執行的內存模型,這塊區域也是線程私有的,生命周期和線程相同。每個方法在執行的時候會創建棧幀,用來存放局部變量表、操作數棧、動態鏈接、返回值等信息。(我們常說的在Java中的棧內存就是指這塊區域)。在JVM規範中:如果線程請求棧深度大於虛擬機提供的深度,那麽拋出StackOutflowError異常;如果無法申請得到足夠的內存,會拋出OutofMemoryError異常
3、本地方法棧:與Java虛擬機棧不同的是,本地方法棧為虛擬機執行Native方法服務,而Java虛擬機棧為虛擬機執行Java方法服務。由於JVM規範並沒有對本地方法棧做強制規定,所以如同HotSpot一樣,直接將本地方法棧和虛擬機棧合二為一。異常情況和JAVA虛擬機棧相同
4、Java堆:Java堆是被所有線程所共享的一塊內存區域,在虛擬機啟動時候創建,這塊區域的目的就是存放對象實力,基本上我們所創建的所有的對象實例都要在這裏分配內存。我們下面會詳細的介紹Java堆
5、方法區:方法區和Java堆一樣,是各個線程共享的內存區域,
6、運行時常量池:運行時常量池是方法區的一部分,在Class文件中(存在類的版本、字段、方法、接口等)存在常量池的信息,作用就是用於存放編譯期間生成的各種字面量和符號引用。受到方法區內存的限制,當無法申請到內存時候會拋出OutOfMemoryError異常
還有一部分就是直接內存,但是直接內存並不是運行時數據區域的一部分,在Java的NIO庫中允許Java程序員頻繁的使用直接內存,從而提高性能(避免了Java堆和Native堆之間來回復制數據)。
二、再探Java堆、棧、方法區
1、Java堆:Java堆的存在是為了解決數據存儲的問題,堆中存放了對象實例。堆也是垃圾收集器管理的主要區域(所以也可以被稱為GC堆)。從內存回收的角度來講,采用分代回收算法的JVM,在堆中可被分為新生代(新生的對象或者年齡不大的對象)和老年代(老年對象),這裏面的劃分是按照垃圾收集器的次數,來判斷對象的年齡。新生代中又被分為Eden區、s0區(from區域)、s1區(to區域),From和To是兩塊大小相等,可以互換角色的區域。一般來說,新生的對象會被首先分配在Eden區,然經過一次GC之後(如果對象還存活)會到from或者to區。之後類似的每一次回收,都會加1,當對象達到一定年齡後,會進入老年代。
2、棧:Java棧是一個線程私有的空間,一般情況下一個棧由3部分組成:局部變量表、操作數棧、幀數據區。
局部變量表:裏面存放的是報錯函數的參數以及局部變量;
操作數棧:其中保存計算過程中的中間結果,同時作為計算過程中變量的臨時存儲空間;
幀數據區:除了局部變量表和操作數棧之外,還需要一些數據來支持常量池的解析,幀數據區中保存著訪問常量池的指針,方便程序訪問常量池。除此之外,當函數返回或者出現異常時,JVM必須有一個異常處理表,方便發送異常的時候找到異常的代碼(從而異常處理也是幀數據區的一部分)
3、方法區:方法區是一塊所有線程共享的內存區域,他保存了類的信息(類的字段、方法、常量池等等),方法區大小決定了系統可以保存多少個類。
4、一張簡略的圖描述一下堆、棧、方法區之間的關系
三、探秘JVM堆中對象分配
1、對象的創建
a)我們從new開始,當虛擬機遇到一條new指令的時候,會首先檢查這個指令的參數能否在常量池中找到這個類的符號引用,並且檢查這個類的加載是否被加載、解析、初始化過,如果沒有就會按照類加載過程進行相應類的加載。
b)在類加載完畢後,然後JVM對新生對象分配內存,對象的分配簡單而言就是在將一塊確定大小的內存從Java堆中分配出來。
①堆是完整的(這時候所有使用的內存在一邊,沒有使用的內存在一邊,中間放著一個指針作為分界點的指示器):分配內存就只是把指針向空閑內存那邊移動與對象大小一樣大的距離(這種方式成為“指針碰撞”)
②堆不是完整的(使用和未用的相互交錯):虛擬機需要維護一個列表,其中記錄的是空間內存的狀況,在分配的時候從空閑內存中找到一塊足夠的空間劃分給對象實例,然後更新表中的記錄信息(這種方式成為“空閑列表”)
c)考慮多線程情況下的對象內存分配
在並發的情況下是線程不安全的,可能出現正在給對象A進行分配,這時候指針位置還沒有來得及改變,然後這時候對象B的內存分配又使用了原來的指針記性分配。在《深入理解Java虛擬機》中講到兩種解決方案
①對對象的分配進行同步處理,采用CAS配置失敗重試的方式保證更新操作的原子性
②將內存分配的動作按照線程劃分在不同的空間中進行。即保證每個線程預先在Java堆中分配一小塊空間(本地線程分配緩沖TLAB)。線程需要分配的時候,就先在TLAB上面分配,然後當TLAB使用完畢再進行同步鎖定。
d)內存分配完成之後,虛擬機需要將分配的內存初始化為零值,這一步保證了對象實例字段在Java代碼中可以不被賦初值就使用,使得程序能夠訪問到這些字段的數據類型對應的零值。
e)然後虛擬機需要對對象進行必要的設置,比如對象是那個類的實例,類的元數據、對象哈希碼、GC年齡等等存放在對象頭中。
f) 上面執行完畢之後,從虛擬機角度而言已經產生了心得對象。但是程序中對象創建還沒有執行<init>方法,所有字段均為零值。所以,執行完new指令,還需要執行<init>方法,按照程序的角度進行初始化,才能使用這個對象。
2、對象的訪問定位
我們在介紹堆棧的時候就介紹過堆棧和方法區之間的關系,Java程序中對象的引用存放在棧(reference)中,引用的實例存放在Java堆中(使用棧上面的對象引用來操作堆上面的具體對象),但是我們並沒有定義怎樣通過引用去定位、訪問堆中的具體對象位置,下面介紹句柄和直接指針的方式
a)句柄方式
首先在Java堆中分配一塊區域作為句柄池,棧中的reference中存放的就是對象的句柄地址信息(句柄中包含的是對象實例數據和類型數據各自的具體地址信息)。使用句柄方式的好處就是reference中存儲的是穩定的句柄地址信息,而reference本身不需要修改。下面是句柄方式的簡略圖
b)直接指針方式:
Java堆中對象的布局中必須考慮如何放置訪問類型數據的相關信息,而reference中存儲的直接就是對象的地址。使用直接指針的方式就是存取速度快,節省了一次指針定位的時間。
JVM自動內存管理機制——Java內存區域