棧中的數據都是以棧幀(Stack Frame)的格式存在,棧幀是一個記憶體區塊,是一個數據集,是一個有關方法( Method )和運行期數據的數據集,當一個方法A被調用時就產生了一個棧幀 Fl ,並被壓入到棧中, A方法又調用了B方法,於是產生棧幀 F2 也被壓入棧,B方法又調用了C方法,於是產生棧幀... ...
一、JDK的組成
JDK:JDK是Java開發工具包,是Sun Microsystems針對Java開發員的產品。JDK中包含JRE(在JDK的安裝目錄下有一個名為jre的目錄,裡面有兩個文件夾bin和lib,在這裡可以認為bin里的就是jvm,lib中則是jvm工作所需要的類庫,而jvm和 lib和起來就稱為jre)和一堆Java工具(javac/java/jdb等)和Java基礎的類庫(即Java API 包括rt.jar)。
Java Runtime Environment(JRE):是運行基於Java語言編寫的程式所不可缺少的運行環境。也是通過它,Java的開發者才得以將自己開發的程式發佈到用戶手中,讓用戶使用。JRE中包含了Java virtual machine(JVM),runtime class libraries和Java application launcher,這些是運行Java程式的必要組件。
JVM(java virtual machine):就是我們常說的java虛擬機,它是整個java實現跨平臺的最核心的部分,所有的java程式會首先被編譯為.class的類文件,這種類文件可以在虛擬機上執行。
二、JVM的位置
JVM就是運行在操作系統之上的一個軟體
三、JVM體繫結構
JVM的組成:
- 類載入子系統 Class loader
- 運行時數據區 JVM 記憶體模型
- 執行引擎
四、類載入子系統
======================類載入器=======================
類載入器(ClassLoader):負責載入class文件(classs文件在文件開頭有特定的文件標識),將class文件位元組碼內容載入到記憶體中,並將這些內容轉換成方法區中的運行時數據結構;ClassLoader只負責載入class文件的載入,至於它是否可以運行,則由Execution Engine決定。
1、BootStrapLoader(引導類載入器):類載入器也是java類,他們也需要類載入器載入進入記憶體,顯然必須要有第一個不是java類的類載入器,來完成這個工作,這個正是BootStrap。負責載入存放在D:\Program Files (x86)\Java\jdk1.7.0_79\jre\lib下,或被-Xbootclasspath參數指定的路徑中的,並且能被虛擬機識別的類庫(如rt.jar,所有的java.*開頭的類均被Bootstrap ClassLoader載入);啟動類載入器是無法被Java程式直接引用的;rt.jar 裡面的類的載入器都是BootStrapLoader。
2、Extension ClassLoader(擴展類載入器):該載入器由sun.misc.Launcher$ExtClassLoader實現,它負責載入D:\Program Files (x86)\Java\jdk1.7.0_79\jre\lib\ext目錄中,或者由java.ext.dirs系統變數指定的路徑中的所有類庫(如javax.*開頭的類),開發者可以直接使用擴展類載入器。ext 目錄下所有的類的載入器都是Extension ClassLoader
3、Application ClassLoader(應用程式類載入器):該類載入器由sun.misc.Launcher$AppClassLoader來實現,它負責載入用戶類路徑(ClassPath)所指定的類,開發者可以直接使用該類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。
====================JVM類載入機制==============
全盤負責:當前線程的類載入器負責載入某個Class時,該Class所依賴的和引用的其他Class也將由該類載入器負責載入,除非顯示使用CLassLoader.loadClass()指定類載入器來載入
父類委托:先讓父類載入器試圖載入該類,只有在父類載入器無法載入該類時才嘗試從自己的類路徑中載入該類。所以我們在開發中儘量不要使用與JDK相同的類(例如自定義一個java.lang.System類),因為父類載入器中已經有一份java.lang.System類了,它會直接將該類給程式使用,而你自定義的類壓根就不會被載入。
雙親委派模型:
雙親委派模型的工作流程是:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把請求委托給父載入器去完成,依次向上,因此,所有的類載入請求最終都應該被傳遞到頂層的啟動類載入器中,
只有當父載入器在它的搜索範圍中沒有找到所需的類時,即無法完成該載入,子載入器才會嘗試自己去載入該類。
雙親委派機制:
- 1、當AppClassLoader載入一個class時,它首先不會自己去嘗試載入這個類,而是把類載入請求委派給父類載入器ExtClassLoader去完成。
- 2、當ExtClassLoader載入一個class時,它首先也不會自己去嘗試載入這個類,而是把類載入請求委派給BootStrap ClassLoader去完成。
- 3、如果BootStrap ClassLoader載入失敗(例如在$JAVA_HOME/jre/lib里未查找到該class),會使用ExtClassLoader來嘗試載入;
- 4、若ExtClassLoader也載入失敗,則會使用AppClassLoader來載入,如果AppClassLoader也載入失敗,則會報出異常ClassNotFoundException。
雙親委派模型意義:
- -系統類防止記憶體中出現多份同樣的位元組碼
- -保證Java程式安全穩定運行
==================類的載入過程======================
類的載入過程:JVM將javac編譯好的class位元組碼文件載入到記憶體中,並對該數據進行驗證、解析和初始化、形成JVM可以直接使用的JAVA類,最終回收(卸載)的過程。
位元組碼(.class)文件來源:
- – 從本地系統中直接載入
- – 通過網路下載.class文件
- – 從zip,jar等歸檔文件中載入.class文件
- – 從專有資料庫中提取.class文件
- – 將Java源文件動態編譯為.class文件
1、載入:載入階段其實就是JVM通過一個類的全限定名來獲取其定義的二進位位元組流,並將這個位元組流所代表的靜態存儲結構轉化為方法區的運行時數據結構且在Java堆中生成一個代表這個類的java.lang.Class對象,作為對方法區中這些數據的訪問入口。在該階段我們開發人員可以干預,例如:我們可以指定類載入器來載入該位元組數組或者自定義類載入器來載入。
2、鏈接:將java類的二進位代碼合併到JVM的運行狀態中的過程
- a、驗證:驗證是為了確保Class文件的位元組流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。
- b、準備:該階段是在方法區中為類變數(static變數)分配記憶體並設置類變數初始值。例如:public static int flag=1;該階段初始化值為0。
- c、解析:虛擬機將常量池中的符號引用替換為直接引用的過程。(直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄)
3、初始化:初始化為類的靜態變數賦予正確的初始值,JVM負責對類進行初始化,主要對類變數進行初始化。
- 初始化階段就是執行類構造器<clinit>()的過程,類構造器<clinit>()是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊中的語句合併產生的。
- 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先初始化其父類。
- 虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確加鎖和同步。
- 當訪問一個java 類的靜態域時,只有正真申明這個域的類才會被初始化。
4、使用:程式使用JVM載入的類
5、卸載
- 執行了System.exit()方法
- JVM垃圾回收機制觸發回收
- 程式正常執行結束
- 程式在執行過程中遇到了異常或錯誤而異常終止
- 由於操作系統出現錯誤而導致Java虛擬機進程終止
五、運行時數據區
1、方法區(Method Area):方法區是各個線程共用的記憶體區域;方法區用於存儲已被虛擬機載入的類的模板信息、常量、靜態變數等;雖然Java虛擬機規範把方法區描述為堆的一部分,但是他還有個別名叫做Non-heap(非堆),目的應該是與Java堆區分開來;根據Java虛擬機規範的規定,當方法區無法滿足記憶體分配需求時,將拋出OutOfMemoryError 異常;相對而言,垃圾收集在這個區域是比較少出現的,但並非數據進入了方法區就如永久代的名字一樣永久存在了。這區域的記憶體回收目標重要是針對常量池的回收和類型的卸載。
方法區只是一個規範:
- 在HotSpot虛擬機上開發、部署程式我們把方法區稱為“永久代”(Permanent Generation);
- 他虛擬機(如 BEA JRockit、IBM J9 等)來說是不存在永久代的概念的。
- HotSpot虛擬機在JKD.8中已經沒有方法區的概念了,他使用元空間代替該區域
2、PC寄存器(程式計數器):每個線程都有一個程式計數器,是線程私有的;就是一個指針,指向方法區中的方法位元組碼(用來存儲指向下一條指令的地址,既將要執行的指令代碼),由執行引擎讀取下一條指令,是一個非常小的記憶體空間,幾乎可以忽略不記;它是當前線程所執行的位元組碼的行號指示器,位元組碼解釋器通過改變這個計數器的值來選取下一條需要執行的位元組碼指令。如果執行的是一個Native方法,那這個計數器是空的;用以完成分支、迴圈、跳轉、異常處理、線程恢復等基礎功能。不會發生記憶體溢出OOM錯誤
本地方法棧(Native Stack):與虛擬機棧基本類似,區別在於虛擬機棧為虛擬機執行的java方法服務,而本地方法棧則是為Native方法服務。(棧的空間大小遠遠小於堆)
3、虛擬機棧(Vm Stack)
棧也叫棧記憶體,主管 Java 程式的運行,是線上程創建時創建,它的生命期是跟隨線程的生命期,線程結束棧記憶體也就仔放,對於棧來說不存在垃圾回收問題,只要線程結束該棧就釋放,生命周期和線程一致,是線程私有的。8種基木類型的變數+對象的引用變數+實例方法都是在函數的棧記憶體中分配。
棧的運行原理:棧中的數據都是以棧幀(Stack Frame)的格式存在,棧幀是一個記憶體區塊,是一個數據集,是一個有關方法( Method )和運行期數據的數據集,當一個方法A被調用時就產生了一個棧幀 Fl ,並被壓入到棧中, A方法又調用了B方法,於是產生棧幀 F2 也被壓入棧,B方法又調用了C方法,於是產生棧幀 F3 也被壓入棧,執行完畢後,先彈出 F3 棧幀,再彈出 F2 棧幀,再彈出 Fl 棧幀 以此類推, 遵循“先進後出” / “後進先出”原則。每個方法執行的同時都會創建一個棧幀,用於存儲局部變數表、操作數、動態鏈接、方法出口等信息,每一個方法從調用直至執行完畢的過,就對應著一個棧幀在虛擬機中入棧到出棧的過程。棧的大小和具體JVM的實現有關,通常在 256K~1024K 之間, 1M 左右。
JVM棧的特點:
- 局部變數表所需的記憶體空間在編譯期間完成記憶體分配。當進入一個方法時,這個方法需要在幀中分配多大的記憶體空間是完全確定的,在方法運行期間不會改變局部變數表的大小。
- 在Java虛擬機規範中,對這個區域規定了兩種異常狀態:如果線程請求的棧的深度大於虛擬機允許的深度,將拋出StackOverFlowError異常(棧溢出);如果虛擬機棧可以動態擴展(現在大部分Java虛擬機都可以動態擴展,只不過Java虛擬機規範中也允許固定長度的java虛擬機棧),如果擴展時無法申請到足夠的記憶體空間,就會拋出OutOfMemoryError異常(沒有足夠的記憶體)。
4、本地方法棧(Native Method Stacks):與虛擬機棧所發揮的作用是非常相似的,他們之間的區別不過是虛擬機棧為虛擬機執行Java方法(也就是位元組碼)服務,而本地方法棧則為虛擬機使用到的本地Native方法服務‘;在虛擬機規範中對本地方法棧中的使用方法、語言、數據結構並沒有強制規定,因此具體的虛擬機可以自由實現它。甚至有的虛擬機(例如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二為一。本地方法棧也會拋出StackOverFlowError和OutOfmMemoryError異常。
5、Java堆(Java Heap):是Java虛擬機管理記憶體中的最大一塊;Java堆是所有線程共用的一塊記憶體管理區域。此記憶體區域唯一目的就是存放對象的實例,幾乎所有對象實例都在堆中分配記憶體。這一點在Java虛擬機規範中的描述是:所有對象實例以及數組都要在堆上分配,但是隨著JIT編譯器的發展與逃逸技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有的對象都分配在堆上也不是變的那麼“絕對”了。詳解請學習我的:JVM之堆的體繫結構