JVM虛擬機

1. Java 內存區域與內存溢出異常

1.1.1 程序計數器java

內存空間小,線程私有。字節碼解釋器工做是就是經過改變這個計數器的值來選取下一條須要執行指令的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴計數器完成算法

若是線程正在執行一個 Java 方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是 Native 方法,這個計數器的值則爲 (Undefined)。此內存區域是惟一一個在 Java 虛擬機規範中沒有規定任何 OutOfMemoryError 狀況的區域。數據庫

1.1.2 Java 虛擬機棧數組

線程私有,生命週期和線程一致。描述的是 Java 方法執行的內存模型:每一個方法在執行時都會牀建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每個方法從調用直至執行結束,就對應着一個棧幀從虛擬機棧中入棧到出棧的過程。安全

局部變量表:存放了編譯期可知的各類基本類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference 類型)和 returnAddress 類型(指向了一條字節碼指令的地址)網絡

StackOverflowError:線程請求的棧深度大於虛擬機所容許的深度。
OutOfMemoryError:若是虛擬機棧能夠動態擴展,而擴展時沒法申請到足夠的內存。數據結構

1.1.3 本地方法棧併發

區別於 Java 虛擬機棧的是,Java 虛擬機棧爲虛擬機執行 Java 方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的 Native 方法服務。也會有 StackOverflowError 和 OutOfMemoryError 異常。佈局

1.1.4 Java 堆優化

對於絕大多數應用來講,這塊區域是 JVM 所管理的內存中最大的一塊。線程共享,主要是存放對象實例和數組。內部會劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer, TLAB)。能夠位於物理上不連續的空間,可是邏輯上要連續。

OutOfMemoryError:若是堆中沒有內存完成實例分配,而且堆也沒法再擴展時,拋出該異常。

1.1.5 方法區

屬於共享內存區域,存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。

 

1.2.1 對象的建立

遇到 new 指令時,首先檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,而且檢查這個符號引用表明的類是否已經被加載、解析和初始化過。若是沒有,執行相應的類加載。

類加載檢查經過以後,爲新對象分配內存(內存大小在類加載完成後即可確認)。在堆的空閒內存中劃分一塊區域(‘指針碰撞-內存規整’或‘空閒列表-內存交錯’的分配方式)。

前面講的每一個線程在堆中都會有私有的分配緩衝區(TLAB),這樣能夠很大程度避免在併發狀況下頻繁建立對象形成的線程不安全。

內存空間分配完成後會初始化爲 0(不包括對象頭),接下來就是填充對象頭,把對象是哪一個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的 GC 分代年齡等信息存入對象頭。

執行 new 指令後執行 init 方法後纔算一份真正可用的對象建立完成。

1.2.2 對象的內存佈局

在 HotSpot 虛擬機中,分爲 3 塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)

對象頭(Header):包含兩部分,第一部分用於存儲對象自身的運行時數據,如哈希碼、GC 分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程 ID、偏向時間戳等,32 位虛擬機佔 32 bit,64 位虛擬機佔 64 bit。官方稱爲 ‘Mark Word’。第二部分是類型指針,即對象指向它的類的元數據指針,虛擬機經過這個指針肯定這個對象是哪一個類的實例。另外,若是是 Java 數組,對象頭中還必須有一塊用於記錄數組長度的數據,由於普通對象能夠經過 Java 對象元數據肯定大小,而數組對象不能夠。

實例數據(Instance Data):程序代碼中所定義的各類類型的字段內容(包含父類繼承下來的和子類中定義的)。

對齊填充(Padding):不是必然須要,主要是佔位,保證對象大小是某個字節的整數倍。

1.2.3 對象的訪問定位

使用對象時,經過棧上的 reference 數據來操做堆上的具體對象。

經過句柄訪問

Java 堆中會分配一塊內存做爲句柄池。reference 存儲的是句柄地址。詳情見圖。

使用直接指針訪問

reference 中直接存儲對象地址

 

 

比較:使用句柄的最大好處是 reference 中存儲的是穩定的句柄地址,在對象移動(GC)是隻改變實例數據指針地址,reference 自身不須要修改。直接指針訪問的最大好處是速度快,節省了一次指針定位的時間開銷。若是是對象頻繁 GC 那麼句柄方法好,若是是對象頻繁訪問則直接指針訪問好。

2. 垃圾回收器與內存分配策略

程序計數器、虛擬機棧、本地方法棧 3 個區域隨線程生滅(由於是線程私有),棧中的棧幀隨着方法的進入和退出而有條不紊地執行着出棧和入棧操做。而 Java 堆和方法區則不同,一個接口中的多個實現類須要的內存可能不同,一個方法中的多個分支須要的內存也可能不同,咱們只有在程序處於運行期才知道那些對象會建立,這部份內存的分配和回收都是動態的,垃圾回收期所關注的就是這部份內存。

2.2.1 引用計數法

給對象添加一個引用計數器。可是難以解決循環引用問題。

2.2.2 可達性分析法

經過一系列的 ‘GC Roots’ 的對象做爲起始點,從這些節點出發所走過的路徑稱爲引用鏈。當一個對象到 GC Roots 沒有任何引用鏈相連的時候說明對象不可用。

可做爲 GC Roots 的對象:

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象
  • 方法區中類靜態屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方法棧中 JNI(即通常說的 Native 方法) 引用的對象

2.2.3 再談引用

前面的兩種方式判斷存活時都與‘引用’有關。可是 JDK 1.2 以後,引用概念進行了擴充,下面具體介紹。

下面四種引用強度一次逐漸減弱

強引用

相似於 Object obj = new Object(); 建立的,只要強引用在就不回收。

軟引用

SoftReference 類實現軟引用。在系統要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行二次回收。

弱引用

WeakReference 類實現弱引用。對象只能生存到下一次垃圾收集以前。在垃圾收集器工做時,不管內存是否足夠都會回收掉只被弱引用關聯的對象。

虛引用

PhantomReference 類實現虛引用。沒法經過虛引用獲取一個對象的實例,爲一個對象設置虛引用關聯的惟一目的就是能在這個對象被收集器回收時收到一個系統通知。

2.2.4 生存仍是死亡

即便在可達性分析算法中不可達的對象,也並不是是「facebook」的,這時候它們暫時出於「緩刑」階段,一個對象的真正死亡至少要經歷兩次標記過程:若是對象在進行中可達性分析後發現沒有與 GC Roots 相鏈接的引用鏈,那他將會被第一次標記而且進行一次篩選,篩選條件是此對象是否有必要執行 finalize() 方法。當對象沒有覆蓋 finalize() 方法,或者 finalize() 方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲「沒有必要執行」。

若是這個對象被斷定爲有必要執行 finalize() 方法,那麼這個對象竟會放置在一個叫作 F-Queue 的隊列中,並在稍後由一個由虛擬機自動創建的、低優先級的 Finalizer 線程去執行它。這裏所謂的「執行」是指虛擬機會出發這個方法,並不承諾或等待他運行結束。finalize() 方法是對象逃脫死亡命運的最後一次機會,稍後 GC 將對 F-Queue 中的對象進行第二次小規模的標記,若是對象要在 finalize() 中成功拯救本身 —— 只要從新與引用鏈上的任何一個對象簡歷關聯便可。

finalize() 方法只會被系統自動調用一次。


2.2.5 回收方法區

在堆中,尤爲是在新生代中,一次垃圾回收通常能夠回收 70% ~ 95% 的空間,而永久代的垃圾收集效率遠低於此。

永久代垃圾回收主要兩部份內容:廢棄的常量和無用的類。

判斷廢棄常量:通常是判斷沒有該常量的引用。

判斷無用的類:要如下三個條件都知足

該類全部的實例都已經回收,也就是 Java 堆中不存在該類的任何實例
加載該類的 ClassLoader 已經被回收
該類對應的 java.lang.Class 對象沒有任何地方唄引用,沒法在任何地方經過反射訪問該類的方法

 

2.3.1 標記 —— 清除算法

效率低,產生空間碎片

2.3.2 複製算法  新生代選用

把空間分紅兩塊,每次只對其中一塊進行 GC。當這塊內存使用完時,就將還存活的對象複製到另外一塊上面。Eden 和 Survivor 和 Survivor  8:1:1。

2.3.3 標記-整理算法  老年代選用

不一樣於針對新生代的複製算法,針對老年代的特色,建立該算法。主要是把存活對象移到內存的一端。

2.3.4 分代回收

根據存活對象劃分幾塊內存區,通常是分爲新生代和老年代。而後根據各個年代的特色制定相應的回收算法。

新生代

每次垃圾回收都有大量對象死去,只有少許存活,選用複製算法比較合理。

老年代

老年代中對象存活率較高、沒有額外的空間分配對它進行擔保。因此必須使用 標記 —— 清除 或者 標記 —— 整理 算法回收。

2.6 內存分配與回收策略

2.6.1 對象優先在 Eden 分配

對象主要分配在新生代的 Eden 區上,若是啓動了本地線程分配緩衝區,將線程優先在 (TLAB) 上分配。少數狀況會直接分配在老年代中。

通常來講 Java 堆的內存模型以下圖所示:

新生代 GC (Minor GC)

發生在新生代的垃圾回收動做,頻繁,速度快。

老年代 GC (Major GC / Full GC)

發生在老年代的垃圾回收動做,出現了 Major GC 常常會伴隨至少一次 Minor GC(非絕對)。Major GC 的速度通常會比 Minor GC 慢十倍以上。

2.6.2 大對象直接進入老年代

2.6.3 長期存活的對象將進入老年代

2.6.4 動態對象年齡斷定

2.6.5 空間分配擔保

3. Java 內存模型與線程

3.1 Java 內存模型

3.1.1 主內存和工做內存之間的交互

操做 做用對象 解釋
lock 主內存 把一個變量標識爲一條線程獨佔的狀態
unlock 主內存 把一個處於鎖定狀態的變量釋放出來,釋放後纔可被其餘線程鎖定
read 主內存 把一個變量的值從主內存傳輸到線程工做內存中,以便 load 操做使用
load 工做內存 把 read 操做從主內存中獲得的變量值放入工做內存中
use 工做內存 把工做內存中一個變量的值傳遞給執行引擎,
每當虛擬機遇到一個須要使用到變量值的字節碼指令時將會執行這個操做
assign 工做內存 把一個從執行引擎接收到的值賦接收到的值賦給工做內存的變量,
每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做
store 工做內存 把工做內存中的一個變量的值傳送到主內存中,以便 write 操做
write 工做內存 把 store 操做從工做內存中獲得的變量的值放入主內存的變量中
3.1.2 對於 volatile 型變量的特殊規則

關鍵字 volatile 是 Java 虛擬機提供的最輕量級的同步機制。

一個變量被定義爲 volatile 的特性:

保證此變量對全部線程的可見性。可是操做並不是原子操做,併發狀況下不安全。
若是不符合 運算結果並不依賴變量當前值,或者可以確保只有單一的線程修改變量的值 和 變量不須要與其餘的狀態變量共同參與不變約束 就要經過加鎖(使用 synchronize 或 java.util.concurrent 中的原子類)來保證原子性。

禁止指令重排序優化。
經過插入內存屏障保證一致性。

3.1.3 對於 long 和 double 型變量的特殊規則

Java 要求對於主內存和工做內存之間的八個操做都是原子性的,可是對於 64 位的數據類型,有一條寬鬆的規定:容許虛擬機將沒有被 volatile 修飾的 64 位數據的讀寫操做劃分爲兩次 32 位的操做來進行,即容許虛擬機實現選擇能夠不保證 64 位數據類型的 load、store、read 和 write 這 4 個操做的原子性。這就是 long 和 double 的非原子性協定。

3.1.4 原子性、可見性與有序性

回顧下併發下應該注意操做的那些特性是什麼,同時加深理解。

原子性(Atomicity)
由 Java 內存模型來直接保證的原子性變量操做包括 read、load、assign、use、store 和 write。大體能夠認爲基本數據類型的操做是原子性的。同時 lock 和 unlock 能夠保證更大範圍操做的原子性。而 synchronize 同步塊操做的原子性是用更高層次的字節碼指令 monitorenter 和 monitorexit 來隱式操做的。

可見性(Visibility)
是指當一個線程修改了共享變量的值,其餘線程也可以當即得知這個通知。主要操做細節就是修改值後將值同步至主內存(volatile 值使用前都會從主內存刷新),除了 volatile 還有 synchronize 和 final 能夠保證可見性。同步塊的可見性是由「對一個變量執行 unlock 操做以前,必須先把此變量同步會主內存中( store、write 操做)」這條規則得到。而 final 可見性是指:被 final 修飾的字段在構造器中一旦完成,而且構造器沒有把 「this」 的引用傳遞出去( this 引用逃逸是一件很危險的事情,其餘線程有可能經過這個引用訪問到「初始化了一半」的對象),那在其餘線程中就能看見 final 字段的值。

有序性(Ordering)
若是在被線程內觀察,全部操做都是有序的;若是在一個線程中觀察另外一個線程,全部操做都是無序的。前半句指「線程內表現爲串行的語義」,後半句是指「指令重排」現象和「工做內存與主內存同步延遲」現象。Java 語言經過 volatile 和 synchronize 兩個關鍵字來保證線程之間操做的有序性。volatile 自身就禁止指令重排,而 synchronize 則是由「一個變量在同一時刻指容許一條線程對其進行 lock 操做」這條規則得到,這條規則決定了持有同一個鎖的兩個同步塊只能串行的進入。

6. 虛擬機類加載機制


虛擬機把描述類的數據從 Class 文件加載到內存,並對數據進行校驗、裝換解析和初始化,最終造成能夠被虛擬機直接使用的 Java 類型。

在 Java 語言中,類型的加載、鏈接和初始化過程都是在程序運行期間完成的。

6.1 類加載時機
類的生命週期( 7 個階段)

 

6.2 類的加載過程

6.2.1 加載

經過一個類的全限定名來獲取定義次類的二進制流(ZIP 包、網絡、運算生成、JSP 生成、數據庫讀取)。
將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
在內存中生成一個表明這個類的 java.lang.Class 對象,做爲方法去這個類的各類數據的訪問入口。
數組類的特殊性:數組類自己不經過類加載器建立,它是由 Java 虛擬機直接建立的。但數組類與類加載器仍然有很密切的關係,由於數組類的元素類型最終是要靠類加載器去建立的,數組建立過程以下:

若是數組的組件類型是引用類型,那就遞歸採用類加載加載。
若是數組的組件類型不是引用類型,Java 虛擬機會把數組標記爲引導類加載器關聯。
數組類的可見性與他的組件類型的可見性一致,若是組件類型不是引用類型,那數組類的可見性將默認爲 public。
內存中實例的 java.lang.Class 對象存在方法區中。做爲程序訪問方法區中這些類型數據的外部接口。
加載階段與鏈接階段的部份內容是交叉進行的,可是開始時間保持前後順序。

6.2.2 驗證

是鏈接的第一步,確保 Class 文件的字節流中包含的信息符合當前虛擬機要求。

文件格式驗證

是否以魔數 0xCAFEBABE 開頭
主、次版本號是否在當前虛擬機處理範圍以內
常量池的常量是否有不被支持常量的類型(檢查常量 tag 標誌)
指向常量的各類索引值中是否有指向不存在的常量或不符合類型的常量
CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 編碼的數據
Class 文件中各個部分集文件自己是否有被刪除的附加的其餘信息
……
只有經過這個階段的驗證後,字節流纔會進入內存的方法區進行存儲,因此後面 3 個驗證階段所有是基於方法區的存儲結構進行的,再也不直接操做字節流。

元數據驗證

這個類是否有父類(除 java.lang.Object 以外)
這個類的父類是否繼承了不容許被繼承的類(final 修飾的類)
若是這個類不是抽象類,是否實現了其父類或接口之中要求實現的全部方法
類中的字段、方法是否與父類產生矛盾(覆蓋父類 final 字段、出現不符合規範的重載)
這一階段主要是對類的元數據信息進行語義校驗,保證不存在不符合 Java 語言規範的元數據信息。

字節碼驗證

保證任意時刻操做數棧的數據類型與指令代碼序列都鞥配合工做(不會出現按照 long 類型讀一個 int 型數據)
保證跳轉指令不會跳轉到方法體之外的字節碼指令上
保證方法體中的類型轉換是有效的(子類對象賦值給父類數據類型是安全的,反過來不合法的)
……
這是整個驗證過程當中最複雜的一個階段,主要目的是經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。這個階段對類的方法體進行校驗分析,保證校驗類的方法在運行時不會作出危害虛擬機安全的事件。

符號引用驗證

符號引用中經過字符創描述的全限定名是否能找到對應的類
在指定類中是否存在符方法的字段描述符以及簡單名稱所描述的方法和字段
符號引用中的類、字段、方法的訪問性(private、protected、public、default)是否可被當前類訪問
……

6.2.3 準備

這個階段正式爲類分配內存並設置類變量初始值,內存在方法去中分配(含 static 修飾的變量不含實例變量)。

public static int value = 1127;
這句代碼在初始值設置以後爲 0,由於這時候還沒有開始執行任何 Java 方法。而把 value 賦值爲 1127 的 putstatic 指令是程序被編譯後,存放於 clinit() 方法中,因此初始化階段纔會對 value 進行賦值。

6.2.4 解析

這個階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。

6.2.5 初始化

前面過程都是以虛擬機主導,而初始化階段開始執行類中的 Java 代碼。

6.3.1 雙親委派模型

從 Java 虛擬機角度講,只存在兩種類加載器:一種是啓動類加載器(C++ 實現,是虛擬機的一部分);另外一種是其餘全部類的加載器(Java 實現,獨立於虛擬機外部且全繼承自 java.lang.ClassLoader)

    1. 啓動類加載器
      加載 lib 下或被 -Xbootclasspath 路徑下的類

    2. 擴展類加載器
      加載 lib/ext 或者被 java.ext.dirs 系統變量所指定的路徑下的類

    3. 引用程序類加載器
      ClassLoader負責,加載用戶路徑上所指定的類庫。

 

除頂層啓動類加載器以外,其餘都有本身的父類加載器。
工做過程:若是一個類加載器收到一個類加載的請求,它首先不會本身加載,而是把這個請求委派給父類加載器。只有父類沒法完成時子類纔會嘗試加載。

 

6.3.2 破壞雙親委派模型

keyword:線程上下文加載器(Thread Context ClassLoader)

相關文章
相關標籤/搜索