當咱們第一次學習Java時這些原理上的東西就會被提到,可是不多有真正去學習。今天開始從頭過一遍Java,打算從JVM開始。
html
JVM是Java Virtual Machine的縮寫。它是一種基於計算設備的規範,是一臺虛擬機,即虛構的計算機。vue
JVM屏蔽了具體操做系統平臺的信息(顯然,就像是咱們在電腦上開了個虛擬機同樣),固然,JVM執行字節碼時實際上仍是要解釋成具體操做平臺的機器指令的。java
經過JVM,Java實現了平臺無關性,Java語言在不一樣平臺運行時不須要從新編譯,只須要在該平臺上部署JVM就能夠了。於是能實現一次編譯多處運行。(就像是你的虛擬機也能夠在任何安了VMWare的系統上運行)數組
JRE:Java Runtime Environment,也就是JVM的運行平臺,聯繫平時用的虛擬機,大概能夠理解成JRE=虛擬機平臺+虛擬機本體(JVM)。相似於你電腦上的VMWare+適用於VMWare的Ubuntu虛擬機。這樣咱們也就明白了JVM究竟是個什麼。緩存
JDK:Java Develop Kit,Java的開發工具包,JDK本體也是Java程序,所以運行依賴於JRE,因爲須要保持JDK的獨立性與完整性,JDK的安裝目錄下一般也附有JRE。目前Oracle提供的Windows下的JDK安裝工具會同時安裝一個正常的JRE和隸屬於JDK目錄下的JRE。markdown
JVM主要包括:程序計數器(Program Counter),Java堆(Heap),Java虛擬機棧(Stack),本地方法棧(Native Stack),方法區(Method Area)多線程
詳細的結構以下:併發
如今我來分別介紹一下每一部分的功能。app
是一個寄存器,能夠看做是代碼行號指示器,相似於實際計算機裏的PC,用於指示,跳轉下一條須要執行的命令。Java的基礎操做以及異常處理等都十分依賴PC。jvm
JVM多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的。在一個肯定的時刻,一個處理器(或者說多核處理器的一個內核)只會執行一條線程中的命令。所以,爲了正常的切換線程,每一個線程都會有一個獨立的PC,各線程的PC不會互相影響。這個私有的PC所佔的這塊內存便是線程的「私有內存」。
若是線程在執行的是Java方法,那麼PC記錄的是正在執行的虛擬機字節碼指令的地址。若是正在執行的不是Java方法即Native方法,那麼PC的值爲undefined。
PC的內存區域是惟一的沒有規定任何OutOfMemoryError的Java虛擬機規範中的區域。
同PC同樣(從工做流程圖裏咱們能夠看到,實際上,PC也是存在於JVM Stack上的),也是線程私有的,生命週期與線程相同。虛擬機棧描述Java方法執行的內存模型,每一個方法被執行時都會建立一個棧幀(Stack Frame),棧幀會利用局部變量數組存儲局部變量(Local Variables),操做棧(Operand Stack),方法出口(Return Value),動態鏈接(Current Class Constant Pool Reference)等信息。
局部變量數組存儲了編譯可知的八個基本類型(int, boolean, char, short, byte, long, float, double),對象引用(根據不一樣的虛擬機實現多是引用地址的指針或者一個handle),returnAddress類型。64位的long和double會佔用兩個Slot,其他類型會佔用一個Slot。在編譯期間,局部變量所需的空間就會完成分配,動態運行期間不會改變所需的空間。
操做棧在執行字節碼指令時會被用到,這種方式相似於原生的CPU寄存器,大部分JVM把時間花費在操做棧的花費上,操做棧和局部變量數組會頻繁的交換數據。
動態鏈接控制着運行時常量池和棧幀的鏈接。全部方法和類的引用都會被看成符號的引用存在常量池中。符號引用是實際上並不指向物理內存地址的邏輯引用。JVM 能夠選擇符號引用解析的時機,一種是當類文件加載並校驗經過後,這種解析方式被稱爲飢餓方式。另一種是符號引用在第一次使用的時候被解析,這種解析方式稱爲惰性方式。不管如何 ,JVM 必需要在第一次使用符號引用時完成解析並拋出可能發生的解析錯誤。綁定是將對象域、方法、類的符號引用替換爲直接引用的過程。綁定只會發生一次。一旦綁定,符號引用會被徹底替換。若是一個類的符號引用尚未被解析,那麼就會載入這個類。每一個直接引用都被存儲爲相對於存儲結構(與運行時變量或方法的位置相關聯的)偏移量。
對Java虛擬機棧這個區域,Java虛擬機規範規定了兩種異常:
本地方法棧如其名字,和Java Virtual Machine Stack其實極爲相似,只是執行的是Native方法,爲Native方法服務。在JVM規範中,沒有對它的實現作具體規定。
Java堆是被全部線程共享的一塊區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存(隨着技術的發展,已不絕對)。
Java堆是垃圾收集器管理的主要區域,於是也被稱爲GC堆。收集器採用分代回收法,GC堆能夠分爲新生代(Yong Generation)和老生代(Old Generation)。新生代包括Eden Space和Survivor Space。但不管哪一個區域,如何劃分,存儲的都是Java對象實例,進一步的劃分是爲了更好的回收內存或快速的分配內存。
根據Java虛擬機規範,堆所在的物理內存區間能夠是不連續的,只要邏輯連續就能夠。實現時既能夠是固定大小,也能夠是可擴展的。若是堆沒法擴展時,就會拋出OutOfMemoryError。
方法區和Java堆相似,也屬於各線程共享的內存區域。用於存儲已被虛擬機加載的類信息,常量,靜態變量,即時編譯器編譯後的代碼數據等。它屬於非堆區(Non Heap),和Java堆區分開。對於存在永久代(Permanent)概念的虛擬機(HotSpot)而言,方法區存在於永久代。Java虛擬機規範對方法區的規定很寬鬆,甚至能夠不實現GC。不過並不是進入方法區的數據就會永久存在了,這塊區域的內存回收主要爲常量池的回收和類型的卸載。這個區域的回收處理不善也會致使嚴重的內存泄漏。
當方法區沒法知足內存分配需求時也會拋出OutOfMemoryError。
用於編譯和存儲那些被 JIT 編譯器編譯成原生代碼的方法。
類信息存儲在方法區,其主要構成爲運行時常量池(Run-Time Constant Pool)和方法(Method Code)。
一個編譯後的類文件包括如下結構:
結構 | 解釋 |
---|---|
magic, minor_version, major_version | 類文件的版本信息和用於編譯這個類的 JDK 版本。 |
constant_pool | 相似於符號表,儘管它包含更多數據。下面有更多的詳細描述。 |
access_flags | 提供這個類的描述符列表。 |
this_class | 提供這個類全名的常量池(constant_pool)索引,好比org/jamesdbloom/foo/Bar。 |
super_class | 提供這個類的父類符號引用的常量池索引。 |
interfaces | 指向常量池的索引數組,提供那些被實現的接口的符號引用。 |
fields | 提供每一個字段完整描述的常量池索引數組。 |
methods | 指向constant_pool的索引數組,用於表示每一個方法簽名的完整描述。若是這個方法不是抽象方法也不是 native 方法,那麼就會顯示這個函數的字節碼。 |
attributes | 不一樣值的數組,表示這個類的附加信息,包括 RetentionPolicy.CLASS 和 RetentionPolicy.RUNTIME 註解。 |
運行時常量池是方法區的一部分。Class文件中有類的版本,字段,方法,接口等描述信息和用於存放編譯期生成的各類字面量和符號引用。這部份內容將在類加載後存放到方法區的運行時常量池中。Java虛擬機規範對Class的細節有着嚴苛的要求而對運行時常量池的實現不作要求。通常來講除了翻譯的Class,翻譯出來的直接引用也會存在運行時常量池中。
運行時常量池具有動態性,即運行時也可將新的常量放入池中。好比String類的intern()方法。
常量池沒法申請到足夠的內存分配時也會拋出OutOfMemoryError。
直接內存並不在Java虛擬機規範中,不是Java的一部分,可是也被頻繁使用並可能致使OutOfMemoryError。Native函數庫能夠直接分配堆外內存,經過存儲在Java堆裏的DirectDataBuffer對象做爲這塊內存的引用進行操做。這樣作在一些場景中能夠顯著提升性能。
直接內存是堆外內存,天然不受Java堆大小的限制,可是可能受實體機內存大小的限制。若是內存各部分總和大於實體機的內存時,也會報出OutOfMemoryError。
將內存中再也不被使用的對象進行回收,GC中用於回收的方法稱爲收集器,因爲GC須要消耗一些資源和時間,Java在對對象的生命週期特徵進行分析後,按照新生代、舊生代的方式來對對象進行收集,以儘量的縮短GC對應用形成的暫停。
不一樣的對象引用類型, GC會採用不一樣的方法進行回收,JVM對象的引用分爲了四種類型:
JVM容許一個程序使用多個併發線程,Hotspot JVM中Java的線程與原生操做系統的線程是直接映射關係。即當線程本地存儲、緩衝區分配、同步對象、棧、程序計數器等準備好之後,就會建立一個操做系統原生線程。Java 線程結束,原生線程隨之被回收。操做系統負責調度全部線程,並把它們分配到任何可用的 CPU 上。當原生線程初始化完畢,就會調用 Java 線程的 run() 方法。run() 返回時,被處理未捕獲異常,原生線程將確認因爲它的結束是否要終止 JVM 進程(好比這個線程是最後一個非守護線程)。當線程結束時,會釋放原生線程和 Java 線程的全部資源。