最近,一直有小夥伴讓我整理下關於JVM的知識,通過十幾天的收集與整理,第一版算是整理出來了。但願對你們有所幫助。也能夠加做者冰河的微信:sun_shine_lyz進行交流。
JDK 是用於支持 Java 程序開發的最小環境。java
JRE 是支持 Java 程序運行的標準環境。程序員
程序計數器(Program Counter Register)是一塊較小的內存空間,能夠看做是當前線程所執行字節碼的行號指示器。分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器完成。算法
因爲 Java 虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式實現的。爲了線程切換後能恢復到正確的執行位置,每條線程都須要一個獨立的程序計數器,各線程之間的計數器互不影響,獨立存儲。數組
程序計數器是惟一一個沒有規定任何 OutOfMemoryError 的區域。緩存
Java 虛擬機棧(Java Virtual Machine Stacks)是線程私有的,生命週期與線程相同。
虛擬機棧描述的是 Java 方法執行的內存模型:每一個方法被執行的時候都會建立一個棧幀(Stack Frame),存儲安全
每個方法被調用到執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。微信
這個區域有兩種異常狀況:數據結構
虛擬機棧爲虛擬機執行 Java 方法(字節碼)服務。多線程
本地方法棧(Native Method Stacks)爲虛擬機使用到的 Native 方法服務。併發
Java 堆(Java Heap)是 Java 虛擬機中內存最大的一塊。Java 堆在虛擬機啓動時建立,被全部線程共享。
做用:存放對象實例。垃圾收集器主要管理的就是 Java 堆。Java 堆在物理上能夠不連續,只要邏輯上連續便可。
方法區(Method Area)被全部線程共享,用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
和 Java 堆同樣,不須要連續的內存,能夠選擇固定的大小,更能夠選擇不實現垃圾收集。
運行時常量池(Runtime Constant Pool)是方法區的一部分。保存 Class 文件中的符號引用、翻譯出來的直接引用。運行時常量池能夠在運行期間將新的常量放入池中。
Object obj = new Object();
對於上述最簡單的訪問,也會涉及到 Java 棧、Java 堆、方法區這三個最重要內存區域。
Object obj
若是出如今方法體中,則上述代碼會反映到 Java 棧的本地變量表中,做爲 reference 類型數據出現。
new Object()
反映到 Java 堆中,造成一塊存儲了 Object 類型全部對象實例數據值的內存。Java堆中還包含對象類型數據的地址信息,這些類型數據存儲在方法區中。
給對象添加一個引用計數器,每當有一個地方引用它,計數器就+1,;當引用失效時,計數器就-1;任什麼時候刻計數器都爲0的對象就是不能再被使用的。
很難解決對象之間的循環引用問題。
經過一系列的名爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到 GC Roots 沒有任何引用鏈相連(用圖論的話來講就是從 GC Roots 到這個對象不可達)時,則證實此對象是不可用的。
在 JDK 1.2 以後,Java 對引用的概念進行了擴充,將引用分爲
Object obj = new Object();
代碼中廣泛存在的,像上述的引用。只要強引用還在,垃圾收集器永遠不會回收掉被引用的對象。
用來描述一些還有用,但並不是必須的對象。軟引用所關聯的對象,有在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍,並進行第二次回收。若是此次回收仍是沒有足夠的內存,纔會拋出內存異常。提供了 SoftReference 類實現軟引用。
描述非必須的對象,強度比軟引用更弱一些,被弱引用關聯的對象,只能生存到下一次垃圾收集發生前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。提供了 WeakReference 類來實現弱引用。
一個對象是否有虛引用,徹底不會對其生存時間夠成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象關聯虛引用的惟一目的,就是但願在這個對象被收集器回收時,收到一個系統通知。提供了 PhantomReference 類來實現虛引用。
分爲標記和清除兩個階段。首先標記出全部須要回收的對象,在標記完成後統一回收被標記的對象。
效率問題:標記和清除過程的效率都不高。
空間問題:標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能致使,程序分配較大對象時沒法找到足夠的連續內存,不得不提早出發另外一次垃圾收集動做。
將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中一塊。當這一塊的內存用完了,就將存活着的對象複製到另外一塊上面,而後再把已經使用過的內存空間一次清理掉。
複製算法使得每次都是針對其中的一塊進行內存回收,內存分配時也不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。
將內存縮小爲原來的一半。在對象存活率較高時,須要執行較多的複製操做,效率會變低。
商業的虛擬機都採用複製算法來回收新生代。由於新生代中的對象容易死亡,因此並不須要按照1:1的比例劃份內存空間,而是將內存分爲一塊較大的 Eden 空間和兩塊較小的 Survivor 空間。每次使用 Eden 和其中的一塊 Survivor。
當回收時,將 Eden 和 Survivor 中還存活的對象一次性拷貝到另一塊 Survivor 空間上,最後清理掉 Eden 和剛纔用過的 Survivor 空間。Hotspot 虛擬機默認 Eden 和 Survivor 的大小比例是8:1,也就是每次新生代中可用內存空間爲整個新生代容量的90%(80% + 10%),只有10%的內存是會被「浪費」的。
標記過程仍然與「標記-清除」算法同樣,但不是直接對可回收對象進行清理,而是讓全部存活的對象向一端移動,而後直接清理掉邊界之外的內存。
根據對象的存活週期,將內存劃分爲幾塊。通常是把 Java 堆分爲新生代和老年代,這樣就能夠根據各個年代的特色,採用最適當的收集算法。
Minor GC:新生代 GC,指發生在新生代的垃圾收集動做,由於 Java 對象大多死亡頻繁,因此 Minor GC 很是頻繁,通常回收速度較快。
Full GC:老年代 GC,也叫 Major GC,速度通常比 Minor GC 慢 10 倍以上。
對於一個大型的系統,當建立的對象及方法變量比較多時,即堆內存中的對象比較多,若是逐一分析對象是否該回收,效率很低。分區是爲了進行模塊化管理,管理不一樣的對象及變量,以提升 JVM 的執行效率。
主要用來存儲新建立的對象,內存較小,垃圾回收頻繁。這個區又分爲三個區域:一個 Eden Space 和兩個 Survivor Space。
Tenure Generation Space(採用標記-整理算法)
主要用來存儲長時間被引用的對象。它裏面存放的是通過幾回在 Young Generation Space 進行掃描判斷過仍存活的對象,內存較大,垃圾回收頻率較小。
存儲不變的類定義、字節碼和常量等。
Class文件是一組以8位字節爲基礎單位的二進制流,各個數據項目間沒有任何分隔符。當遇到8位字節以上空間的數據項時,則會按照高位在前的方式分隔成若干個8位字節進行存儲。
每一個Class文件的頭4個字節稱爲魔數(Magic Number),它的惟一做用是用於肯定這個文件是否爲一個能被虛擬機接受的Class文件。OxCAFEBABE。
接下來是Class文件的版本號:第5,6字節是次版本號(Minor Version),第7,8字節是主版本號(Major Version)。
使用JDK 1.7編譯輸出Class文件,格式代碼爲:
前四個字節爲魔數,次版本號是0x0000,主版本號是0x0033,說明本文件是能夠被1.7及以上版本的虛擬機執行的文件。
類加載器實現類的加載動做,同時用於肯定一個類。對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立其在Java虛擬機中的惟一性。即便兩個類來源於同一個Class文件,只要加載它們的類加載器不一樣,這兩個類就不相等。
雙親委派模型(Parents Delegation Model)要求除了頂層的啓動類加載器外,其他加載器都應當有本身的父類加載器。類加載器之間的父子關係,經過組合關係複用。
工做過程:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器完成。每一個層次的類加載器都是如此,所以全部的加載請求最終都應該傳送到頂層的啓動類加載器中,只有到父加載器反饋本身沒法完成這個加載請求(它的搜索範圍沒有找到所需的類)時,子加載器纔會嘗試本身去加載。
Java類隨着它的類加載器一塊兒具有了一種帶優先級的層次關係。好比java.lang.Object,它存放在rt.jar中,不管哪一個類加載器要加載這個類,最終都是委派給啓動類加載器進行加載,所以Object類在程序的各個類加載器環境中,都是同一個類。
若是沒有使用雙親委派模型,讓各個類加載器本身去加載,那麼Java類型體系中最基礎的行爲也得不到保障,應用程序會變得一片混亂。
Class文件描述的各類信息,都須要加載到虛擬機後才能運行。虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。
這兩種機器都有代碼執行的能力,可是:
棧幀是用於支持虛擬機進行方法調用和方法執行的數據結構, 存儲了方法的
每個方法從調用開始到執行完成的過程,就對應着一個棧幀在虛擬機棧裏面從入棧到出棧的過程。
方法調用惟一的任務是肯定被調用方法的版本(調用哪一個方法),暫時還不涉及方法內部的具體運行過程。
Class文件的編譯過程不包含傳統編譯的鏈接步驟,一切方法調用在Class文件裏面存儲的都只是符號引用,而不是方法在實際運行時內存佈局中的入口地址。這使得Java有強大的動態擴展能力,但使Java方法的調用過程變得相對複雜,須要在類加載期間甚至到運行時才能肯定目標方法的直接引用。
解釋執行(經過解釋器執行)
編譯執行(經過即時編譯器產生本地代碼)
當主流的虛擬機中都包含了即時編譯器後,Class文件中的代碼到底會被解釋執行仍是編譯執行,只有虛擬機本身才能準確判斷。
Javac編譯器完成了程序代碼通過詞法分析、語法分析到抽象語法樹,再遍歷語法樹生成線性的字節碼指令流的過程。由於這一動做是在Java虛擬機以外進行的,而解釋器在虛擬機的內部,因此Java程序的編譯是半獨立的實現。
Java編譯器輸出的指令流,裏面的指令大部分都是零地址指令,它們依賴操做數棧進行工做。
計算「1+1=2」,基於棧的指令集是這樣的:
iconst_1 iconst_1 iadd istore_0
兩條iconst_1指令連續地把兩個常量1壓入棧中,iadd指令把棧頂的兩個值出棧相加,把結果放回棧頂,最後istore_0把棧頂的值放到局部變量表的第0個Slot中。
最典型的是x86的地址指令集,依賴寄存器工做。
計算「1+1=2」,基於寄存器的指令集是這樣的:
mov eax, 1 add eax, 1
mov指令把EAX寄存器的值設爲1,而後add指令再把這個值加1,結果就保存在EAX寄存器裏。
優勢:
缺點:
頻繁的訪問棧,意味着頻繁的訪問內存,相對於處理器,內存纔是執行速度的瓶頸。
Java程序最初是經過解釋器進行解釋執行的,當虛擬機發現某個方法或代碼塊的運行特別頻繁,就會把這些代碼認定爲「熱點代碼」(Hot Spot Code)。
爲了提升熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關的機器碼,並進行各類層次的優化,完成這個任務的編譯器成爲即時編譯器(Just In Time Compiler,JIT編譯器)。
許多主流的商用虛擬機,都同時包含解釋器和編譯器。
若是內存資源限制較大(部分嵌入式系統),可使用解釋執行節約內存,反之可使用編譯執行來提高效率。同時編譯器的代碼還能退回成解釋器的代碼。
由於即時編譯器編譯本地代碼須要佔用程序運行時間,要編譯出優化程度更高的代碼,所花費的時間越長。
分層編譯根據編譯器編譯、優化的規模和耗時,劃分不一樣的編譯層次,包括:
用Client Compiler和Server Compiler將會同時工做。用Client Compiler獲取更高的編譯速度,用Server Compiler獲取更好的編譯質量。
要知道一段代碼是否是熱點代碼,是否是須要觸發即時編譯,這個行爲稱爲熱點探測。主要有兩種方法:
統計的是一個相對的執行頻率,即一段時間內方法被調用的次數。當超過必定的時間限度,若是方法的調用次數仍然不足以讓它提交給即時編譯器編譯,那這個方法的調用計數器就會被減小一半,這個過程稱爲方法調用計數器的熱度衰減,這個時間就被稱爲半衰週期。
廣泛應用於各類編譯器的經典優化技術,它的含義是:
若是一個表達式E已經被計算過了,而且從先前的計算到如今E中全部變量的值都沒有發生變化,那麼E的此次出現就成了公共子表達式。沒有必要從新計算,直接用結果代替E就能夠了。
由於Java會自動檢查數組越界,每次數組元素的讀寫都帶有一次隱含的條件斷定操做,對於擁有大量數組訪問的程序代碼,這無疑是一種性能負擔。
若是數組訪問發生在循環之中,而且使用循環變量來進行數組訪問,若是編譯器只要經過數據流分析就能夠斷定循環變量的取值範圍永遠在數組區間內,那麼整個循環中就能夠把數組的上下界檢查消除掉,能夠節省不少次的條件判斷操做。
內聯消除了方法調用的成本,還爲其餘優化手段創建良好的基礎。
編譯器在進行內聯時,若是是非虛方法,那麼直接內聯。若是遇到虛方法,則會查詢當前程序下是否有多個目標版本可供選擇,若是查詢結果只有一個版本,那麼也能夠內聯,不過這種內聯屬於激進優化,須要預留一個逃生門(Guard條件不成立時的Slow Path),稱爲守護內聯。
若是程序的後續執行過程當中,虛擬機一直沒有加載到會令這個方法的接受者的繼承關係發現變化的類,那麼內聯優化的代碼能夠一直使用。不然須要拋棄掉已經編譯的代碼,退回到解釋狀態執行,或者從新進行編譯。
逃逸分析的基本行爲就是分析對象動態做用域:當一個對象在方法裏面被定義後,它可能被外部方法所引用,這種行爲被稱爲方法逃逸。被外部線程訪問到,被稱爲線程逃逸。
運算任務,除了須要處理器計算以外,還須要與內存交互,如讀取運算數據、存儲運算結果等(不能僅靠寄存器來解決)。
計算機的存儲設備和處理器的運算速度差了幾個數量級,因此不得不加入一層讀寫速度儘量接近處理器運算速度的高速緩存(Cache),做爲內存與處理器之間的緩衝:將運算須要的數據複製到緩存中,讓運算快速運行。當運算結束後再從緩存同步回內存,這樣處理器就無需等待緩慢的內存讀寫了。
基於高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,可是引入了一個新的問題:緩存一致性。在多處理器系統中,每一個處理器都有本身的高速緩存,它們又共享同一主內存。當多個處理器的運算任務都涉及同一塊主內存時,可能致使各自的緩存數據不一致。
爲了解決一致性的問題,須要各個處理器訪問緩存時遵循緩存一致性協議。同時爲了使得處理器充分被利用,處理器可能會對輸出代碼進行亂序執行優化。Java虛擬機的即時編譯器也有相似的指令重排序優化。
Java虛擬機的規範,用來屏蔽掉各類硬件和操做系統的內存訪問差別,以實現讓Java程序在各個平臺下都能達到一致的併發效果。
定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出這樣的底層細節。此處的變量包括實例字段、靜態字段和構成數組對象的元素,可是不包括局部變量和方法參數,由於這些是線程私有的,不會被共享,因此不存在競爭問題。
因此的變量都存儲在主內存,每條線程還有本身的工做內存,保存了被該線程使用到的變量的主內存副本拷貝。線程對變量的全部操做(讀取、賦值)都必須在工做內存中進行,不能直接讀寫主內存的變量。不一樣的線程之間也沒法直接訪問對方工做內存的變量,線程間變量值的傳遞須要經過主內存。
一個變量如何從主內存拷貝到工做內存、如何從工做內存同步回主內存,Java內存模型定義了8種操做:
關鍵字volatile是Java虛擬機提供的最輕量級的同步機制。當一個變量被定義成volatile以後,具有兩種特性:
volatile變量在各個線程的工做內存,不存在一致性問題(各個線程的工做內存中volatile變量,每次使用前都要刷新到主內存)。可是Java裏面的運算並不是原子操做,致使volatile變量的運算在併發下同樣是不安全的。
在某些狀況下,volatile同步機制的性能要優於鎖(synchronized關鍵字),可是因爲虛擬機對鎖實行的許多消除和優化,因此並非很快。
volatile變量讀操做的性能消耗與普通變量幾乎沒有差異,可是寫操做則可能慢一些,由於它須要在本地代碼中插入許多內存屏障指令來保證處理器不發生亂序執行。
併發不必定要依賴多線程,PHP中有多進程併發。可是Java裏面的併發是多線程的。
線程是比進程更輕量級的調度執行單位。線程能夠把一個進程的資源分配和執行調度分開,各個線程既能夠共享進程資源(內存地址、文件I/O),又能夠獨立調度(線程是CPU調度的最基本單位)。
操做系統支持怎樣的線程模型,在很大程度上就決定了Java虛擬機的線程是怎樣映射的。
線程調度是系統爲線程分配處理器使用權的過程。
雖然Java線程調度是系統自動完成的,可是咱們能夠建議系統給某些線程多分配點時間——設置線程優先級。Java語言有10個級別的線程優先級,優先級越高的線程,越容易被系統選擇執行。
可是並不能徹底依靠線程優先級。由於Java的線程是被映射到系統的原生線程上,因此線程調度最終仍是由操做系統說了算。如Windows中只有7種優先級,因此Java不得不出現幾個優先級相同的狀況。同時優先級可能會被系統自行改變。Windows系統中存在一個「優先級推動器」,當系統發現一個線程執行特別勤奮,可能會越過線程優先級爲它分配執行時間。
當多個線程訪問一個對象時,若是不用考慮這些線程在運行時環境下的調度和交替執行,也不須要進行額外的同步,或者在調用方法進行任何其餘的協調操做,調用這個對象的行爲均可以得到正確的結果,那這個對象就是線程安全的。
在Java語言裏,不可變的對象必定是線程安全的,只要一個不可變的對象被正確構建出來,那其外部的可見狀態永遠也不會改變,永遠也不會在多個線程中處於不一致的狀態。
虛擬機提供了同步和鎖機制。
互斥是實現同步的一種手段,臨界區、互斥量和信號量都是主要的互斥實現方式。Java中最基本的同步手段就是synchronized關鍵字,其編譯後會在同步塊的先後分別造成monitorenter和monitorexit兩個字節碼指令。這兩個字節碼都須要一個Reference類型的參數指明要鎖定和解鎖的對象。若是Java程序中的synchronized明確指定了對象參數,那麼這個對象就是Reference;若是沒有明確指定,那就根據synchronized修飾的是實例方法仍是類方法,去獲取對應的對象實例或Class對象做爲鎖對象。
在執行monitorenter指令時,首先要嘗試獲取對象的鎖。
除了synchronized以外,還可使用java.util.concurrent包中的重入鎖(ReentrantLock)來實現同步。ReentrantLock比synchronized增長了高級功能:等待可中斷、可實現公平鎖、鎖能夠綁定多個條件。
等待可中斷:當持有鎖的線程長期不釋放鎖的時候,正在等待的線程能夠選擇放棄等待,對處理執行時間很是長的同步塊頗有用。
公平鎖:多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次得到鎖。synchronized中的鎖是非公平的。
互斥同步最大的問題,就是進行線程阻塞和喚醒所帶來的性能問題,是一種悲觀的併發策略。老是認爲只要不去作正確的同步措施(加鎖),那就確定會出問題,不管共享數據是否真的會出現競爭,它都要進行加鎖、用戶態核心態轉換、維護鎖計數器和檢查是否有被阻塞的線程須要被喚醒等操做。
隨着硬件指令集的發展,咱們可使用基於衝突檢測的樂觀併發策略。先進行操做,若是沒有其餘線程徵用數據,那操做就成功了;若是共享數據有徵用,產生了衝突,那就再進行其餘的補償措施。這種樂觀的併發策略的許多實現不須要線程掛起,因此被稱爲非阻塞同步。
JDK1.6的一個重要主題,就是高效併發。HotSpot虛擬機開發團隊在這個版本上,實現了各類鎖優化:
互斥同步對性能最大的影響是阻塞的實現,掛起線程和恢復線程的操做都須要轉入內核態中完成,這些操做給系統的併發性帶來很大壓力。同時不少應用共享數據的鎖定狀態,只會持續很短的一段時間,爲了這段時間去掛起和恢復線程並不值得。先不掛起線程,等一下子。
若是物理機器有一個以上的處理器,能讓兩個或以上的線程同時並行執行,讓後面請求鎖的線程稍等一會,但不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放。爲了讓線程等待,咱們只需讓線程執行一個忙循環(自旋)。
自旋等待自己雖然避免了線程切換的開銷,但它要佔用處理器時間。因此若是鎖被佔用的時間很短,自旋等待的效果就很是好;若是時間很長,那麼自旋的線程只會白白消耗處理器的資源。因此自旋等待的時間要有必定的限度,若是自旋超過了限定的次數仍然沒有成功得到鎖,那就應該使用傳統的方式掛起線程了。
自旋的時間不固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。
有了自適應自旋,隨着程序運行和性能監控信息的不斷完善,虛擬機對程序鎖的情況預測就會愈來愈準確,虛擬機也會愈來愈聰明。
鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但被檢測到不可能存在共享數據競爭的鎖進行消除。主要根據逃逸分析。
程序員怎麼會在明知道不存在數據競爭的狀況下使用同步呢?不少不是程序員本身加入的。
原則上,同步塊的做用範圍要儘可能小。可是若是一系列的連續操做都對同一個對象反覆加鎖和解鎖,甚至加鎖操做在循環體內,頻繁地進行互斥同步操做也會致使沒必要要的性能損耗。
鎖粗化就是增大鎖的做用域。
在沒有多線程競爭的前提下,減小傳統的重量級鎖使用操做系統互斥量產生的性能消耗。
消除數據在無競爭狀況下的同步原語,進一步提升程序的運行性能。即在無競爭的狀況下,把整個同步都消除掉。這個鎖會偏向於第一個得到它的線程,若是在接下來的執行過程當中,該鎖沒有被其餘的線程獲取,則持有偏向鎖的線程將永遠不須要同步。
參考:《深刻理解Java虛擬機:JVM高級特性與最佳實踐(第2版)》