1、jvm內存模型html
JVM 內存模型主要分爲堆、程序計數器、方法區、虛擬機棧和本地方法棧java
一、堆數組
1.一、堆是 JVM 內存中最大的一塊內存空間。安全
1.二、該內存被全部線程共享,幾乎全部對象和數組都被分配到了堆內存中。多線程
1.三、堆被劃分爲新生代和老年代,新生代又被進一步劃分爲 Eden 和 Survivor 區,最後 Survivor 由 From Survivor 和 To Survivor 組成。jvm
二、程序計數器(Program Counter Register)函數
程序計數器是一塊很小的內存空間,用來記錄下一條運行的指令(實際是記錄各個線程執行的字節碼的地址,因爲 Java 是多線程語言,當執行的線程數量超過 CPU 核數時,線程之間會根據時間片輪詢爭奪 CPU 資源。若是一個線程的時間片用完了,或者是其它緣由致使這個線程的 CPU 資源被提早搶奪,那麼這個退出的線程就須要單獨的一個程序計數器,來記錄下一條運行的指令。),例如,分支、循環、跳轉、異常、線程恢復等都依賴於計數器。工具
三、方法區(Method Area)spa
3.一、方法區!=永久代,根據虛擬機類型而定;HotSpot 虛擬機使用永久代來實現方法區,但在其它虛擬機中,例如,Oracle 的 JRockit、IBM 的 J9 就不存在永久代一說。操作系統
3.二、方法區主要是用來存放已被虛擬機加載的類相關信息,包括類信息(類的版本、字段、方法、接口和父類等信息)、運行時常量池、字符串常量。
ps1:JVM 在執行某個類的時候,必須通過加載、鏈接、初始化,而鏈接又包括驗證、準備、解析三個階段。在加載類的時候,JVM 會先加載 class 文件,而在 class 文件中除了有類的版本、字段、方法和接口等描述信息外,還有一項信息是常量池 (Constant Pool Table),用於存放編譯期間生成的各類字面量和符號引用()。
ps2:字面量包括字符串(String a=「b」)、基本類型的常量(final 修飾的變量)。
ps3:符號引用則包括類和方法的全限定名(例如 String 這個類,它的全限定名就是 Java/lang/String)、變量的名稱和描述符以及方法的名稱和描述符。
ps4:當類加載到內存中後,JVM 就會將 class 文件常量池中的內容存放到運行時的常量池中。
ps5:在解析階段,JVM 會把符號引用替換爲直接引用(對象的索引值)。
3.三、方法區與堆空間相似,也是一個共享內存區,因此方法區是線程共享的。
3.二、在 Java6 版本中,永久代在非堆內存區;到了 Java7 版本,永久代的靜態變量和運行時常量池被合併到了堆中;而到了 Java8,永久代被元空間取代了,而且元空間的存儲位置是本地內存,永久代的靜態變量(class static variables)以及運行時常量池(runtime constant pool)則跟 Java7 同樣,轉移到了堆中。以下圖:
ps1:java8用直接內存元空間替代永久代的好處
1-一、移除永久代是爲了融合 HotSpot JVM 與 JRockit VM 而作出的努力,由於 JRockit 沒有永久代,因此不須要配置永久代。
1-二、永久代內存常常不夠用或發生內存溢出,爆出異常 java.lang.OutOfMemoryError: PermGen。這是由於在 JDK1.7 版本中,指定的 PermGen 區大小爲 8M,因爲 PermGen 中類的元數據信息在每次 FullGC 的時候均可能被收集,回收率都偏低,成績很難使人滿意;還有,爲 PermGen 分配多大的空間很難肯定,PermSize 的大小依賴於不少因素,好比,JVM 加載的 class 總數、常量池的大小和方法的大小等。
四、虛擬機棧(VM stack)
4.一、Java 虛擬機棧是線程私有的內存空間,它和 Java 線程一塊兒建立。
4.二、用來保存方法的局部變量、操做數棧、動態連接方法和返回地址等信息,並參與方法的調用和返回。每個方法的調用都伴隨着棧幀的入棧操做,方法的返回則是棧幀的出棧操做。
五、本地方法棧(Native Method Stack)
5.一、本地方法棧跟 Java 虛擬機棧的功能相似,Java 虛擬機棧用於管理 Java 函數的調用,而本地方法棧則用於管理本地方法的調用。
5.二、本地方法並非用 Java 實現的,而是由 C 語言實現的。
2、java類從編譯到加載到執行的過程
一、編譯
經過jdk自帶的javac工具完成
二、類加載
2.一、當一個類被建立實例或者被其它對象引用時,虛擬機在沒有加載過該類的狀況下,會經過類加載器將字節碼文件加載到內存中。
2.二、不一樣的實現類由不一樣的類加載器加載,JDK 中的本地方法類通常由根加載器(Bootstrp loader)加載進來,JDK 中內部實現的擴展類通常由擴展加載器(ExtClassLoader )實現加載,而程序中的類文件則由系統加載器(AppClassLoader )實現加載。
2.三、在類加載後,class 類文件中的常量池信息以及其它數據會被保存到 JVM 內存的方法區中。
三、類鏈接
類在加載進來以後,會進行鏈接、初始化,最後纔會被使用。在鏈接過程當中,又包括驗證、準備和解析三個部分。
3.一、驗證
驗證類符合 Java 規範和 JVM 規範,在保證符合規範的前提下,避免危害虛擬機安全。
3.二、準備
爲類的靜態變量分配內存,初始化爲系統的初始值。對於 final static 修飾的變量,直接賦值爲用戶的定義值。例如,private final static int value=123,會在準備階段分配內存,並初始化值爲 123,而若是是 private static int value=123,這個階段 value 的值仍然爲 0。
3.三、解析
將符號引用轉爲直接引用的過程。咱們知道,在編譯時,Java 類並不知道所引用的類的實際地址,所以只能使用符號引用來代替。類結構文件的常量池中存儲了符號引用,包括類和接口的全限定名、類引用、方法引用以及成員變量引用等。若是要使用這些類和方法,就須要把它們轉化爲 JVM 能夠直接獲取的內存地址或指針,即直接引用。
3.四、類初始化
編譯器會在將 .java 文件編譯成 .class 文件時,收集全部類初始化代碼,包括靜態變量賦值語句、靜態代碼塊、靜態方法,收集在一塊兒成爲 <clinit>() 方法。在這個階段中,JVM 首先將執行構造器 <clinit> 方法
初始化類的靜態變量和靜態代碼塊爲用戶自定義的值,初始化的順序和 Java 源碼從上到下的順序一致。例如:
private static int i=1; static{ i=0; } public static void main(String [] args){ System.out.println(i); }
運行結果爲:0
子類初始化時會首先調用父類的 <clinit>() 方法,再執行子類的 <clinit>() 方法,運行如下代碼:public class Parent{
public static String parentStr= "parent static string"; static{ System.out.println("parent static fields"); System.out.println(parentStr); } public Parent(){ System.out.println("parent instance initialization"); } } public class Sub extends Parent{ public static String subStr= "sub static string"; static{ System.out.println("sub static fields"); System.out.println(subStr); } public Sub(){ System.out.println("sub instance initialization"); } public static void main(String[] args){ System.out.println("sub main"); new Sub(); } }
運行結果爲:
parent static fields
parent static string
sub static fields
sub static string
sub main
parent instance initialization
sub instance initialization
JVM 會保證 <clinit>() 方法的線程安全,保證同一時間只有一個線程執行。
JVM 在初始化執行代碼時,若是實例化一個新對象,會調用 <init> 方法對實例變量進行初始化,並執行對應的構造方法內的代碼。
類初始化完後就能夠正式使用了,好比建立對象等。
四、運行時編譯
見:http://www.javashuo.com/article/p-rhnsmsau-cu.html
3、jvm內存分配過程
一、JVM 根據配置配置或默認參數向操做系統申請內存空間
二、JVM 得到內存空間後,會根據配置參數分配堆、棧以及方法區的內存大小
三、class 文件加載、驗證、準備以及解析,其中準備階段會爲類的靜態變量分配內存,初始化爲系統的初始值
四、使用過程當中內存分配