1. 程式人生 > >深入理解Java虛擬機—內存管理機制

深入理解Java虛擬機—內存管理機制

heap 通信 行程 單元 和數 define 正在 調用方法 滿足

前面說過了類的加載機制,裏面講到了類的初始化中時用到了一部分內存管理的知識,這裏讓我們來看下Java虛擬機是如何管理內存的。

先讓我們來看張圖

技術分享圖片

有些文章中對線程隔離區還稱之為線程獨占區,其實是一個意思了。下面讓我們來詳細介紹下這五部分;

運行時數據區

Java虛擬機在執行Java程序的過程中會把它所管理的內存劃分為若幹個不同的數據區域,這些區域都擁有自己的用途,並隨著JVM進程的啟動或者用戶線程的啟動和結束建立和銷毀。

先讓我們了解下進程和線程的區別:

進程是資源分配的最小單位,線程是程序執行的最小單位。

進程有自己的獨立地址空間,每啟動一個進程,系統就會為它分配一個地址空間、建立數據表來維護代碼段、堆棧段和數據段,這種操作非常昂貴。而線程是共享進程中的數據的,使用相同的地址空間,因此CPU切換一個線程的花費要比進程小很多,同時創建一個線程的開銷也要比進程小很多。

同一個進程中可以包括多個線程,並且線程共享整個進程的資源(寄存器、堆棧、上下文),一個進程至少包含一個線程。線程之間的通信更加方便,同一進程下的線程共享全局變量、靜態變量等數據,而進程之間的通信則需要以通信的方式(IPC)進行。

這裏引用在知乎中某位同學的解釋,

進程的顆粒度太大,每次都要有上下的調入,保存,調出。如果我們把進程比喻為一個運行在電腦上的軟件,那麽一個軟件的執行不可能是一條邏輯執行的,必定有多個分支和多個程序段,就好比要實現程序A,實際分成 a,b,c等多個塊組合而成。那麽這裏具體的執行就可能變成:

程序A得到CPU =》CPU加載上下文,開始執行程序A的a小段,然後執行A的b小段,然後再執行A的c小段,最後CPU保存A的上下文。

這裏a,b,c的執行是共享了A的上下文,CPU在執行的時候沒有進行上下文切換的。

看到這裏是不是對線程共享和線程隔離區有了一個更深次的理解。可以理解為方法區和堆是分配給進程的,也就是線程共享區,而棧和程序計數器則是分配給每個獨立線程的。

在SUN公司的HotSpot虛擬機中將java虛擬機棧和本地方法棧合二為一了

程序計數器(Program Counter Register)

程序計數器(Program Counter Register)是一塊較小的內存空間,它可以看成是當前線程所執行字節碼的行號指示器。在計算機中,其實程序計數器就是一個寄存器,依據不同計算機細節的差異,它可以存放當前正在被執行的指令,也可以放下一個被執行的指令。

在虛擬機的概念模型中,字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令的

由於Java虛擬機的多線程是通過線程輪詢切換並分配處理器執行時間的方式來實現的,在任何一個確定的時候,一個處理器都只會執行一條線程中的指令,因此為了線程切換之後能過恢復到正確的執行位置,每條線程都需要擁有一個獨立的程序計數器,各條線程之間的計數器互不影響,獨立存儲,所以程序計數器是線程私有的內存,也就是它屬於線程隔離區的。

如果線程執行的是一個Java方法,這個計數器記錄的就是正在執行的虛擬機字節碼指令地址;如果正在執行的是Native方法,那麽這個計數器的值就是(Undefined)。

此內存區域是唯一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError情況的區域。

Java虛擬機棧

Java虛擬機棧(Java Virtual Machine Stack)也是線程私有的,即他的生命周期和線程相同。

在Java中,JVM中的棧記錄了線程的方法調用,每個線程擁有一個棧,在某個線程的運行過程中,如果有新的方法調用,那麽該線程對應的棧就會增加一個存儲單元,即棧針(Stack Frame)。

虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行時都會創建一個棧針(Stack Frame)用於存儲局部變量表、操作數棧、動態連接、方法出口等信息。每一個方法從調用至完成的過程,就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。

當被調用方法運行結束時,該方法對應的幀將被刪除,參數和局部變量所占據的空間也隨之釋放。線程回到原方法,繼續執行。當所有的棧都清空時,程序也隨之運行結束。

我們經常說的棧內存其實就是現在講的虛擬機棧,或者說是虛擬機棧中局部變量表部分。

局部變量表存放了編譯器可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它不等同於對象本身,可能是指向對象起始位置的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置,引用所指向的對象保存在堆中(引用可能為Null,即不指向任何對象))和returnAddress類型(指向了一條字節碼指令的地址)。

其中64位長度的long和double類型的數據會占用2個局部變量空間(Slot),其余數據類型只占用1個。局部變量表所需要的內存空間在編譯時期完成分配。當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。

異常有兩種

1,線程請求的棧深度大於虛擬機所允許的深度將拋出StackOverflowError異常 (遞歸調用)

2,如果虛擬機可以動態擴展,如果擴展時已經無法申請到足夠的內存就會拋出OutOfMemeoryError異常。

List list=new ArrayList();
        for(;;){
            int[] tmp=new int[1000000];
            list.add(tmp);
        }

本地方法棧

本地方法棧(Native Method Stack)與虛擬機棧所發揮的作用是非常相似的。他們之間的區別就是Java虛擬機棧是位虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧為位虛擬機使用到的Native方法服務。

其實虛擬機規範中對本地方發棧中方法所使用的語言、使用方式以及數據結構都沒有強制規定,因此具體的虛擬機可以自由地實現它。甚至在有的虛擬機(如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二為一。與虛擬機棧一樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemory異常。

Java堆

對於大多數應用來說,Java堆(Java Heap)是Java虛擬機管理的內存中最大的一塊。Java堆是被所有線程共享的一塊數據區域,在虛擬機啟動時創建,這一內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這裏分配內存。但是隨著JIT編譯器的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有的對象都分配在堆上也逐漸變得不是那麽“絕對”。 堆中可細分為新生代和老年代,在細分可以分為Eden空間、Form Survivor空間、to Survivor空間。 Java堆是垃圾收集器管理的主要區域,因此很多時候也被稱為“GC堆”。 根據Java虛擬機規範規定,Java堆可以處於物理上不連續的內存中,即只要邏輯上是連續的即可,就像我們的磁盤空間一樣。在實現時,可以固定大小也是可擴展的。主流的虛擬機都是按照可擴展來實現的(通過-Xmx和-Xms來控制)。如果在堆中沒有內存可分配,並且堆也無法繼續擴展時,將會拋出OutOfMemortError異常。

Java的普通對象存活在堆中,與棧不同,堆的空間不會隨著方法調用結束而清空。因此,在某個方法中創建的對象,可以在方法調用結束之後,繼續存在堆中。這帶來的一個問題是,如果我們不斷的創建新的對象,內存控件將會最終消耗殆盡。

方法區

方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域,它用於存儲已經被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯之後的代碼等數據。雖然Java虛擬機將其描述為堆的一個邏輯部分,但它卻有一個別名叫做Non-Heap(非堆)。目的是與Java堆區分開來。(以前很多人把方法區稱為永久代,現在JDK1.8中已經用元數據區域取代了永久代)。

運行時常量池

運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息就是常量池。用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入到方法區的運行時常量池中存放。並非預置入Class文件中常量池的內容才進入方法運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的便是String類的intern()方法。

技術分享圖片
public class Test
{
    public static void main(String[] args)
    {
        String s1="hello china";//字節碼常量
        String s2="hello china";
        String s3=new String("hello china");

        System.out.println(s1 == s2);
        System.out.println(s1 == s3);
        System.out.println(s1 == s3.intern());//運行時常量  intern 是個native方法
    }
}
技術分享圖片

技術分享圖片

當方法區無法滿足內存分配需求時,拋出OutOfMemoryError

註:JDK8之前,方法區由永久代實現,主要存放類的信息、常量池、方法數據、方法代碼等;JDK8之後,取消了永久代,提出了元空間,並且常量池、靜態成員變量等遷移到了堆中;元空間不在虛擬機內存中,而是放在本地內存中。

直接內存

由於直接內存(Direct Memory)並不是java虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域,但是這部分也被頻繁使用,而且也可能導致內存溢出異常出現,所以也放到這一部分進行簡介。

先讓,本機直接內存的分配不會受到Java堆大小的限制,但是肯定還是會受到本機總內存大小以及處理器尋址空間的限制。管理員在配置虛擬機參數時,會根據實際內存設置-Xmx等參數信息,但經常會忽略直接內存,使得各個內存區域總和大於物理內存限制(包含物理的和操作系統級的限制),從而導致動態擴展時出現OutOfMemoryError異常。

JDK1.4加入了NIO,引入一種基於通道與緩沖區的I/O方式,它可以使用Native函數庫直接分配堆外內存,然後通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內存的引用進行操作。因此避免了在Java堆和Native堆中來回復制數據,提高了性能。

Object obj = new Object();

Object obj將會反映到虛擬機棧中(reference類型)
new Object()將會反映到Java堆中
此類的對象類型、父類、實現的接口、方法等信息數據,將反映到方法區中

深入理解Java虛擬機—內存管理機制