第一章 JVM 內存模型
Java 虛擬機(Java Virtual Machine=JVM)的內存空間分為五個部分,分別是:程序計數器
Java 虛擬機棧
本地方法棧
堆
方法區。
下面對這五個區域展開深入的介紹。
1.1 程序計數器
1.1.1 什么是程序計數器?
程序計數器是一塊較小的內存空間,可以把它看作當前線程正在執行的字節碼的行號指示器。也就是說,程序計數器里面記錄的是當前線程正在執行的那一條字節碼指令的地址。
注:但是,如果當前線程正在執行的是一個本地方法,那么此時程序計數器為空。
1.1.2 程序計數器的作用
程序計數器有兩個作用:字節碼解釋器通過改變程序計數器來依次讀取指令,從而實現代碼的流程控制,如:順序執行、選擇、循環、異常處理。
在多線程的情況下,程序計數器用于記錄當前線程執行的位置,從而當線程被切換回來的時候能夠知道該線程上次運行到哪兒了。
1.1.3 程序計數器的特點是一塊較小的存儲空間
線程私有。每條線程都有一個程序計數器。
是唯一一個不會出現OutOfMemoryError的內存區域。
生命周期隨著線程的創建而創建,隨著線程的結束而死亡。
1.2 Java虛擬機棧(JVM Stack)
1.2.1 什么是Java虛擬機棧?
Java虛擬機棧是描述Java方法運行過程的內存模型。
Java虛擬機棧會為每一個即將運行的Java方法創建一塊叫做“棧幀”的區域,這塊區域用于存儲該方法在運行過程中所需要的一些信息,這些信息包括:局部變量表 存放基本數據類型變量、引用類型的變量、returnAddress類型的變量。
操作數棧
動態鏈接
方法出口信息
等
當一個方法即將被運行時,Java虛擬機棧首先會在Java虛擬機棧中為該方法創建一塊“棧幀”,棧幀中包含局部變量表、操作數棧、動態鏈接、方法出口信息等。當方法在運行過程中需要創建局部變量時,就將局部變量的值存入棧幀的局部變量表中。
當這個方法執行完畢后,這個方法所對應的棧幀將會出棧,并釋放內存空間。
注意:人們常說,Java的內存空間分為“棧”和“堆”,棧中存放局部變量,堆中存放對象。
這句話不完全正確!這里的“堆”可以這么理解,但這里的“棧”只代表了Java虛擬機棧中的局部變量表部分。真正的Java虛擬機棧是由一個個棧幀組成,而每個棧幀中都擁有:局部變量表、操作數棧、動態鏈接、方法出口信息。
1.2.2 Java 虛擬機棧的特點
(1)局部變量表的創建是在方法被執行的時候,隨著棧幀的創建而創建。而且,局部變量表的大小在編譯時期就確定下來了,在創建的時候只需分配事先規定好的大小即可。此外,在方法運行的過程中局部變量表的大小是不會發生改變的。
(2)Java 虛擬機棧會出現兩種異常:StackOverFlowError 和 OutOfMemoryError。a) StackOverFlowError: 若Java虛擬機棧的內存大小不允許動態擴展,那么當線程請求棧的深度超過當前Java虛擬機棧的最大深度的時候,就拋出StackOverFlowError異常。
b) OutOfMemoryError: 若Java虛擬機棧的內存大小允許動態擴展,且當線程請求棧時內存用完了,無法再動態擴展了,此時拋出OutOfMemoryError異常。
(3)Java虛擬機棧也是線程私有的,每個線程都有各自的Java虛擬機棧,而且隨著線程的創建而創建,隨著線程的死亡而死亡。注:StackOverFlowError和OutOfMemoryError的異同?
StackOverFlowError表示當前線程申請的棧超過了事先定好的棧的最大深度,但內存空間可能還有很多。而OutOfMemoryError是指當線程申請棧時發現棧已經滿了,而且內存也全都用光了。
1.3 本地方法棧
1.3.1 什么是本地方法棧?
本地方法棧和Java虛擬機棧實現的功能類似,只不過本地方法區是本地方法運行的內存模型。
本地方法被執行的時候,在本地方法棧也會創建一個棧幀,用于存放該本地方法的局部變量表、操作數棧、動態鏈接、出口信息。
方法執行完畢后相應的棧幀也會出棧并釋放內存空間。
也會拋出StackOverFlowError和OutOfMemoryError異常。
1.4 堆
1.4.1 什么是堆?
堆是用來存放對象的內存空間。 幾乎所有的對象都存儲在堆中。
1.4.2 堆的特點
(1)線程共享
整個 Java 虛擬機只有一個堆,所有的線程都訪問同一個堆。而程序計數器、Java 虛擬機棧、本地方法棧都是一個線程對應一個的。
(2)在虛擬機啟動時創建。
(3)垃圾回收的主要場所。
(4)可以進一步細分為:新生代、老年代。
新生代又可被分為:Eden、From Survior、To Survior。不同的區域存放具有不同生命周期的對象。這樣可以根據不同的區域使用不同的垃圾回收算法,從而更具有針對性,從而更高效。
(5)堆的大小既可以固定也可以擴展,但主流的虛擬機堆的大小是可擴展的,因此當線程請求分配內存,但堆已滿,且內存已滿無法再擴展時,就拋出 OutOfMemoryError。
1.5 方法區
1.5.1 什么是方法區?
Java 虛擬機規范中定義方法區是堆的一個邏輯部分。方法區中存放已經被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等。
1.5.2 方法區的特點線程共享 方法區是堆的一個邏輯部分,因此和堆一樣,都是線程共享的。整個虛擬機中只有一個方法區。
永久代 方法區中的信息一般需要長期存在,而且它又是堆的邏輯分區,因此用堆的劃分方法,我們把方法區稱為老年代。
內存回收效率低 方法區中的信息一般需要長期存在,回收一遍內存之后可能只有少量信息無效。 對方法區的內存回收的主要目標是:對常量池的回收 和 對類型的卸載。
Java虛擬機規范對方法區的要求比較寬松。 和堆一樣,允許固定大小,也允許可擴展的大小,還允許不實現垃圾回收。
1.5.3 什么是運行時常量池?
方法區中存放三種數據:類信息、常量、靜態變量、即時編譯器編譯后的代碼。其中常量存儲在運行時常量池中。
我們一般在一個類中通過public static final來聲明一個常量。這個類被編譯后便生成Class文件,這個類的所有信息都存儲在這個class文件中。
當這個類被Java虛擬機加載后,class文件中的常量就存放在方法區的運行時常量池中。而且在運行期間,可以向常量池中添加新的常量。如:String類的intern()方法就能在運行期間向常量池中添加字符串常量。
當運行時常量池中的某些常量沒有被對象引用,同時也沒有被變量引用,那么就需要垃圾收集器回收。
1.6 直接內存
直接內存是除Java虛擬機之外的內存,但也有可能被Java使用。
在NIO中引入了一種基于通道和緩沖的IO方式。它可以通過調用本地方法直接分配Java虛擬機之外的內存,然后通過一個存儲在Java堆中的DirectByteBuffer對象直接操作該內存,而無需先將外面內存中的數據復制到堆中再操作,從而提升了數據操作的效率。
直接內存的大小不受Java虛擬機控制,但既然是內存,當內存不足時就會拋出OOM異常。
1.7 綜上所述Java虛擬機的內存模型中一共有兩個“棧”,分別是:Java虛擬機棧和本地方法棧。 兩個“棧”的功能類似,都是方法運行過程的內存模型。并且兩個“棧”內部構造相同,都是線程私有。 只不過Java虛擬機棧描述的是Java方法運行過程的內存模型,而本地方法棧是描述Java本地方法運行過程的內存模型。
Java虛擬機的內存模型中一共有兩個“堆”,一個是原本的堆,一個是方法區。方法區本質上是屬于堆的一個邏輯部分。堆中存放對象,方法區中存放類信息、常量、靜態變量、即時編譯器編譯的代碼。
堆是Java虛擬機中最大的一塊內存區域,也是垃圾收集器主要的工作區域。
程序計數器、Java虛擬機棧、本地方法棧是線程私有的,即每個線程都擁有各自的程序計數器、Java虛擬機棧、本地方法棧。并且他們的生命周期和所屬的線程一樣。 而堆、方法區是線程共享的,在Java虛擬機中只有一個堆、一個方法棧。并在JVM啟動的時候就創建,JVM停止才銷毀。
第二章 揭開Java對象創建的奧秘
2.1 對象的創建過程
當虛擬機遇到一條含有new的指令時,會進行一系列對象創建的操作:
(1)檢查常量池中是否有即將要創建的這個對象所屬的類的符號引用;若常量池中沒有這個類的符號引用,說明這個類還沒有被定義!拋出ClassNotFoundException;
若常量池中有這個類的符號引用,則進行下一步工作;
(2)進而檢查這個符號引用所代表的類是否已經被JVM加載;若該類還沒有被加載,就找該類的class文件,并加載進方法區;
若該類已經被JVM加載,則準備為對象分配內存;
(3)根據方法區中該類的信息確定該類所需的內存大小;
一個對象所需的內存大小是在這個對象所屬類被定義完就能確定的!且一個類所生產的所有對象的內存大小是一樣的!JVM在一個類被加載進方法區的時候就知道該類生產的每一個對象所需要的內存大小。
(4)從堆中劃分一塊對應大小的內存空間給新的對象;分配堆中內存有兩種方式:指針碰撞 如果JVM的垃圾收集器采用復制算法或標記-整理算法,那么堆中空閑內存是完整的區域,并且空閑內存和已使用內存之間由一個指針標記。那么當為一個對象分配內存時,只需移動指針即可。因此,這種在完整空閑區域上通過移動指針來分配內存的方式就叫做“指針碰撞”。
空閑列表 如果JVM的垃圾收集器采用標記-清除算法,那么堆中空閑區域和已使用區域交錯,因此需要用一張“空閑列表”來記錄堆中哪些區域是空閑區域,從而在創建對象的時候根據這張“空閑列表”找到空閑區域,并分配內存。 綜上所述:JVM究竟采用哪種內存分配方法,取決于它使用了何種垃圾收集器。
(5)為對象中的成員變量賦上初始值(默認初始化);
(6)設置對象頭中的信息;
(7)調用對象的構造函數進行初始化;
此時,整個對象的創建過程就完成了。