秋招開始了,前面因爲作別的事耽誤了半個月,之前學的東西不用就很容易忘記。因此,此次從新閱讀《深刻理解 JVM 虛擬機》時,想作一個記錄。將碎片的知識整合,方便本身之後閱讀,同時也和你們一塊兒分享。內容中會添加我本身的理解,其中若是有錯誤,歡迎你們指正。html
根據《Java 虛擬機規範(Java SE 7 版)》規定,Java 虛擬機所管理的內存以下圖所示。java
內存空間小,線程私有。字節碼解釋器工做是就是經過改變這個計數器的值來選取下一條須要執行指令的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴計數器完成算法
若是線程正在執行一個 Java 方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是 Native 方法,這個計數器的值則爲 (Undefined)。此內存區域是惟一一個在 Java 虛擬機規範中沒有規定任何 OutOfMemoryError 狀況的區域。數據庫
線程私有,生命週期和線程一致。描述的是 Java 方法執行的內存模型:每一個方法在執行時都會牀建立一個棧幀(Stack Frame)用於存儲
局部變量表
、操做數棧
、動態連接
、方法出口
等信息。每個方法從調用直至執行結束,就對應着一個棧幀從虛擬機棧中入棧到出棧的過程。數組
局部變量表:存放了編譯期可知的各類基本類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference 類型)和 returnAddress 類型(指向了一條字節碼指令的地址)緩存
StackOverflowError:線程請求的棧深度大於虛擬機所容許的深度。
OutOfMemoryError:若是虛擬機棧能夠動態擴展,而擴展時沒法申請到足夠的內存。安全
區別於 Java 虛擬機棧的是,Java 虛擬機棧爲虛擬機執行 Java 方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的 Native 方法服務。也會有 StackOverflowError 和 OutOfMemoryError 異常。網絡
對於絕大多數應用來講,這塊區域是 JVM 所管理的內存中最大的一塊。線程共享,主要是存放對象實例和數組。內部會劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer, TLAB)。能夠位於物理上不連續的空間,可是邏輯上要連續。數據結構
OutOfMemoryError:若是堆中沒有內存完成實例分配,而且堆也沒法再擴展時,拋出該異常。多線程
屬於共享內存區域,存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
如今用一張圖來介紹每一個區域存儲的內容。
屬於方法區一部分,用於存放編譯期生成的各類字面量和符號引用。編譯器和運行期(String 的 intern() )均可以將常量放入池中。內存有限,沒法申請時拋出 OutOfMemoryError。
非虛擬機運行時數據區的部分
在 JDK 1.4 中新加入 NIO (New Input/Output) 類,引入了一種基於通道(Channel)和緩存(Buffer)的 I/O 方式,它可使用 Native 函數庫直接分配堆外內存,而後經過一個存儲在 Java 堆中的 DirectByteBuffer 對象做爲這塊內存的引用進行操做。能夠避免在 Java 堆和 Native 堆中來回的數據耗時操做。
OutOfMemoryError:會受到本機內存限制,若是內存區域總和大於物理內存限制從而致使動態擴展時出現該異常。
主要介紹數據是如何建立、如何佈局以及如何訪問的。
建立過程比較複雜,建議看書瞭解,這裏提供我的的總結。
遇到 new 指令時,首先檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,而且檢查這個符號引用表明的類是否已經被加載、解析和初始化過。若是沒有,執行相應的類加載。
類加載檢查經過以後,爲新對象分配內存(內存大小在類加載完成後即可確認)。在堆的空閒內存中劃分一塊區域(‘指針碰撞-內存規整’或‘空閒列表-內存交錯’的分配方式)。
前面講的每一個線程在堆中都會有私有的分配緩衝區(TLAB),這樣能夠很大程度避免在併發狀況下頻繁建立對象形成的線程不安全。
內存空間分配完成後會初始化爲 0(不包括對象頭),接下來就是填充對象頭,把對象是哪一個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的 GC 分代年齡等信息存入對象頭。
執行 new 指令後執行 init 方法後纔算一份真正可用的對象建立完成。
在 HotSpot 虛擬機中,分爲 3 塊區域:
對象頭(Header)
、實例數據(Instance Data)
和對齊填充(Padding)
對象頭(Header)
:包含兩部分,第一部分用於存儲對象自身的運行時數據,如哈希碼、GC 分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程 ID、偏向時間戳等,32 位虛擬機佔 32 bit,64 位虛擬機佔 64 bit。官方稱爲 ‘Mark Word’。第二部分是類型指針,即對象指向它的類的元數據指針,虛擬機經過這個指針肯定這個對象是哪一個類的實例。另外,若是是 Java 數組,對象頭中還必須有一塊用於記錄數組長度的數據,由於普通對象能夠經過 Java 對象元數據肯定大小,而數組對象不能夠。點擊免費「領取Java架構資料」
實例數據(Instance Data)
:程序代碼中所定義的各類類型的字段內容(包含父類繼承下來的和子類中定義的)。
對齊填充(Padding)
:不是必然須要,主要是佔位,保證對象大小是某個字節的整數倍。
使用對象時,經過棧上的 reference 數據來操做堆上的具體對象。
經過句柄訪問
Java 堆中會分配一塊內存做爲句柄池。reference 存儲的是句柄地址。詳情見圖。
使用直接指針訪問
reference 中直接存儲對象地址
比較:使用句柄的最大好處是 reference 中存儲的是穩定的句柄地址,在對象移動(GC)是隻改變實例數據指針地址,reference 自身不須要修改。直接指針訪問的最大好處是速度快,節省了一次指針定位的時間開銷。若是是對象頻繁 GC 那麼句柄方法好,若是是對象頻繁訪問則直接指針訪問好。
// 待填
程序計數器、虛擬機棧、本地方法棧 3 個區域隨線程生滅(由於是線程私有),棧中的棧幀隨着方法的進入和退出而有條不紊地執行着出棧和入棧操做。而 Java 堆和方法區則不同,一個接口中的多個實現類須要的內存可能不同,一個方法中的多個分支須要的內存也可能不同,咱們只有在程序處於運行期才知道那些對象會建立,這部份內存的分配和回收都是動態的,垃圾回收期所關注的就是這部份內存。
在進行內存回收以前要作的事情就是判斷那些對象是‘死’的,哪些是‘活’的。
給對象添加一個引用計數器。可是難以解決循環引用問題。
從圖中能夠看出,若是不下當心直接把 Obj1-reference 和 Obj2-reference 置 null。則在 Java 堆當中的兩塊內存依然保持着互相引用沒法回收。
經過一系列的 ‘GC Roots’ 的對象做爲起始點,從這些節點出發所走過的路徑稱爲引用鏈。當一個對象到 GC Roots 沒有任何引用鏈相連的時候說明對象不可用。
可做爲 GC Roots 的對象:
前面的兩種方式判斷存活時都與‘引用’有關。可是 JDK 1.2 以後,引用概念進行了擴充,下面具體介紹。
下面四種引用強度一次逐漸減弱
強引用
相似於
Object obj = new Object();
建立的,只要強引用在就不回收。
軟引用
SoftReference 類實現軟引用。在系統要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行二次回收。
弱引用
WeakReference 類實現弱引用。對象只能生存到下一次垃圾收集以前。在垃圾收集器工做時,不管內存是否足夠都會回收掉只被弱引用關聯的對象。
虛引用
PhantomReference 類實現虛引用。沒法經過虛引用獲取一個對象的實例,爲一個對象設置虛引用關聯的惟一目的就是能在這個對象被收集器回收時收到一個系統通知。
即便在可達性分析算法中不可達的對象,也並不是是「facebook」的,這時候它們暫時出於「緩刑」階段,一個對象的真正死亡至少要經歷兩次標記過程:若是對象在進行中可達性分析後發現沒有與 GC Roots 相鏈接的引用鏈,那他將會被第一次標記而且進行一次篩選,篩選條件是此對象是否有必要執行 finalize() 方法。當對象沒有覆蓋 finalize() 方法,或者 finalize() 方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲「沒有必要執行」。
若是這個對象被斷定爲有必要執行 finalize() 方法,那麼這個對象竟會放置在一個叫作 F-Queue 的隊列中,並在稍後由一個由虛擬機自動創建的、低優先級的 Finalizer 線程去執行它。這裏所謂的「執行」是指虛擬機會出發這個方法,並不承諾或等待他運行結束。finalize() 方法是對象逃脫死亡命運的最後一次機會,稍後 GC 將對 F-Queue 中的對象進行第二次小規模的標記,若是對象要在 finalize() 中成功拯救本身 —— 只要從新與引用鏈上的任何一個對象簡歷關聯便可。
finalize() 方法只會被系統自動調用一次。
在堆中,尤爲是在新生代中,一次垃圾回收通常能夠回收 70% ~ 95% 的空間,而永久代的垃圾收集效率遠低於此。
永久代垃圾回收主要兩部份內容:廢棄的常量和無用的類。
判斷廢棄常量:通常是判斷沒有該常量的引用。
判斷無用的類:要如下三個條件都知足
僅提供思路
直接標記清除就可。
兩個不足:
把空間分紅兩塊,每次只對其中一塊進行 GC。當這塊內存使用完時,就將還存活的對象複製到另外一塊上面。
解決前一種方法的不足,可是會形成空間利用率低下。由於大多數新生代對象都不會熬過第一次 GC。因此不必 1 : 1 劃分空間。能夠分一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 空間和其中一塊 Survivor。當回收時,將 Eden 和 Survivor 中還存活的對象一次性複製到另外一塊 Survivor 上,最後清理 Eden 和 Survivor 空間。大小比例通常是 8 : 1 : 1,每次浪費 10% 的 Survivor 空間。可是這裏有一個問題就是若是存活的大於 10% 怎麼辦?這裏採用一種分配擔保策略:多出來的對象直接進入老年代。
不一樣於針對新生代的複製算法,針對老年代的特色,建立該算法。主要是把存活對象移到內存的一端。
根據存活對象劃分幾塊內存區,通常是分爲新生代和老年代。而後根據各個年代的特色制定相應的回收算法。
新生代
每次垃圾回收都有大量對象死去,只有少許存活,選用複製算法比較合理。
老年代
老年代中對象存活率較高、沒有額外的空間分配對它進行擔保。因此必須使用
標記 —— 清除
或者標記 —— 整理
算法回收。
// 待填
收集算法是內存回收的理論,而垃圾回收器是內存回收的實踐。
說明:若是兩個收集器之間存在連線說明他們之間能夠搭配使用。
這是一個單線程收集器。意味着它只會使用一個 CPU 或一條收集線程去完成收集工做,而且在進行垃圾回收時必須暫停其它全部的工做線程直到收集結束。
能夠認爲是 Serial 收集器的多線程版本。
並行:Parallel
指多條垃圾收集線程並行工做,此時用戶線程處於等待狀態
併發:Concurrent
指用戶線程和垃圾回收線程同時執行(不必定是並行,有多是交叉執行),用戶進程在運行,而垃圾回收線程在另外一個 CPU 上運行。
這是一個新生代收集器,也是使用複製算法實現,同時也是並行的多線程收集器。
CMS 等收集器的關注點是儘量地縮短垃圾收集時用戶線程所停頓的時間,而 Parallel Scavenge 收集器的目的是達到一個可控制的吞吐量(Throughput = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間))。
做爲一個吞吐量優先的收集器,虛擬機會根據當前系統的運行狀況收集性能監控信息,動態調整停頓時間。這就是 GC 的自適應調整策略(GC Ergonomics)。
收集器的老年代版本,單線程,使用
標記 —— 整理
。
Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多線程,使用
標記 —— 整理
CMS (Concurrent Mark Sweep) 收集器是一種以獲取最短回收停頓時間爲目標的收集器。基於
標記 —— 清除
算法實現。
運做步驟:
缺點:對 CPU 資源敏感、沒法收集浮動垃圾、標記 —— 清除
算法帶來的空間碎片
面向服務端的垃圾回收器。
優勢:並行與併發、分代收集、空間整合、可預測停頓。
運做步驟:
對象主要分配在新生代的 Eden 區上,若是啓動了本地線程分配緩衝區,將線程優先在 (TLAB) 上分配。少數狀況會直接分配在老年代中。
通常來講 Java 堆的內存模型以下圖所示:
新生代 GC (Minor GC)
發生在新生代的垃圾回收動做,頻繁,速度快。
老年代 GC (Major GC / Full GC)
發生在老年代的垃圾回收動做,出現了 Major GC 常常會伴隨至少一次 Minor GC(非絕對)。Major GC 的速度通常會比 Minor GC 慢十倍以上。
點擊免費「領取Java架構資料」
屏蔽掉各類硬件和操做系統的內存訪問差別。
操做 | 做用對象 | 解釋 |
---|---|---|
lock | 主內存 | 把一個變量標識爲一條線程獨佔的狀態 |
unlock | 主內存 | 把一個處於鎖定狀態的變量釋放出來,釋放後纔可被其餘線程鎖定 |
read | 主內存 | 把一個變量的值從主內存傳輸到線程工做內存中,以便 load 操做使用 |
load | 工做內存 | 把 read 操做從主內存中獲得的變量值放入工做內存中 |
use | 工做內存 | 把工做內存中一個變量的值傳遞給執行引擎, 每當虛擬機遇到一個須要使用到變量值的字節碼指令時將會執行這個操做 |
assign | 工做內存 | 把一個從執行引擎接收到的值賦接收到的值賦給工做內存的變量, 每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做 |
store | 工做內存 | 把工做內存中的一個變量的值傳送到主內存中,以便 write 操做 |
write | 工做內存 | 把 store 操做從工做內存中獲得的變量的值放入主內存的變量中 |
關鍵字 volatile 是 Java 虛擬機提供的最輕量級的同步機制。
一個變量被定義爲 volatile 的特性:
若是不符合
運算結果並不依賴變量當前值,或者可以確保只有單一的線程修改變量的值
和變量不須要與其餘的狀態變量共同參與不變約束
就要經過加鎖(使用 synchronize 或 java.util.concurrent 中的原子類)來保證原子性。
經過插入內存屏障保證一致性。
Java 要求對於主內存和工做內存之間的八個操做都是原子性的,可是對於 64 位的數據類型,有一條寬鬆的規定:容許虛擬機將沒有被 volatile 修飾的 64 位數據的讀寫操做劃分爲兩次 32 位的操做來進行,即容許虛擬機實現選擇能夠不保證 64 位數據類型的 load、store、read 和 write 這 4 個操做的原子性。這就是 long 和 double 的非原子性協定。
回顧下併發下應該注意操做的那些特性是什麼,同時加深理解。
由 Java 內存模型來直接保證的原子性變量操做包括 read、load、assign、use、store 和 write。大體能夠認爲基本數據類型的操做是原子性的。同時 lock 和 unlock 能夠保證更大範圍操做的原子性。而 synchronize 同步塊操做的原子性是用更高層次的字節碼指令 monitorenter 和 monitorexit 來隱式操做的。
是指當一個線程修改了共享變量的值,其餘線程也可以當即得知這個通知。主要操做細節就是修改值後將值同步至主內存(volatile 值使用前都會從主內存刷新),除了 volatile 還有 synchronize 和 final 能夠保證可見性。同步塊的可見性是由「對一個變量執行 unlock 操做以前,必須先把此變量同步會主內存中( store、write 操做)」這條規則得到。而 final 可見性是指:被 final 修飾的字段在構造器中一旦完成,而且構造器沒有把 「this」 的引用傳遞出去( this 引用逃逸是一件很危險的事情,其餘線程有可能經過這個引用訪問到「初始化了一半」的對象),那在其餘線程中就能看見 final 字段的值。
若是在被線程內觀察,全部操做都是有序的;若是在一個線程中觀察另外一個線程,全部操做都是無序的。前半句指「線程內表現爲串行的語義」,後半句是指「指令重排」現象和「工做內存與主內存同步延遲」現象。Java 語言經過 volatile 和 synchronize 兩個關鍵字來保證線程之間操做的有序性。volatile 自身就禁止指令重排,而 synchronize 則是由「一個變量在同一時刻指容許一條線程對其進行 lock 操做」這條規則得到,這條規則決定了持有同一個鎖的兩個同步塊只能串行的進入。
也就是 happens-before 原則。這個原則是判斷數據是否存在競爭、線程是否安全的主要依據。先行發生是 Java 內存模型中定義的兩項操做之間的偏序關係。
自然的先行發生關係
規則 | 解釋 |
---|---|
程序次序規則 | 在一個線程內,代碼按照書寫的控制流順序執行 |
管程鎖定規則 | 一個 unlock 操做先行發生於後面對同一個鎖的 lock 操做 |
volatile 變量規則 | volatile 變量的寫操做先行發生於後面對這個變量的讀操做 |
線程啓動規則 | Thread 對象的 start() 方法先行發生於此線程的每個動做 |
線程終止規則 | 線程中全部的操做都先行發生於對此線程的終止檢測 (經過 Thread.join() 方法結束、 Thread.isAlive() 的返回值檢測) |
線程中斷規則 | 對線程 interrupt() 方法調用優先發生於被中斷線程的代碼檢測到中斷事件的發生 (經過 Thread.interrupted() 方法檢測) |
對象終結規則 | 一個對象的初始化完成(構造函數執行結束)先行發生於它的 finalize() 方法的開始 |
傳遞性 | 若是操做 A 先於 操做 B 發生,操做 B 先於 操做 C 發生,那麼操做 A 先於 操做 C |
使用內核線程實現
直接由操做系統內核支持的線程,這種線程由內核完成切換。程序通常不會直接去使用內核線程,而是去使用內核線程的一種高級接口 —— 輕量級進程(LWP),輕量級進程就是咱們一般意義上所講的線程,每一個輕量級進程都有一個內核級線程支持。
使用用戶線程實現
廣義上來講,只要不是內核線程就能夠認爲是用戶線程,所以能夠認爲輕量級進程也屬於用戶線程。狹義上說是徹底創建在用戶空間的線程庫上的而且內核系統不可感知的。
使用用戶線程夾加輕量級進程混合實現
直接看圖
Java 線程實現
平臺不一樣實現方式不一樣,能夠認爲是一條 Java 線程映射到一條輕量級進程。
協同式線程調度
線程執行時間由線程自身控制,實現簡單,切換線程本身可知,因此基本沒有線程同步問題。壞處是執行時間不可控,容易阻塞。
搶佔式線程調度
每一個線程由系統來分配執行時間。
五種狀態:
建立後還沒有啓動的線程。
Runable 包括了操做系統線程狀態中的 Running 和 Ready,也就是出於此狀態的線程有可能正在執行,也有可能正在等待 CPU 爲他分配時間。
出於這種狀態的線程不會被 CPU 分配時間,它們要等其餘線程顯示的喚醒。
如下方法會然線程進入無限期等待狀態:
1.沒有設置 Timeout 參數的 Object.wait() 方法。
2.沒有設置 Timeout 參數的 Thread.join() 方法。
3.LookSupport.park() 方法。
處於這種狀態的線程也不會分配時間,不過無需等待配其餘線程顯示地喚醒,在必定時間後他們會由系統自動喚醒。
如下方法會讓線程進入限期等待狀態:
1.Thread.sleep() 方法。
2.設置了 Timeout 參數的 Object.wait() 方法。
3.設置了 Timeout 參數的 Thread.join() 方法。
4.LockSupport.parkNanos() 方法。
5.LockSupport.parkUntil() 方法。
線程被阻塞了,「阻塞狀態」和「等待狀態」的區別是:「阻塞狀態」在等待着獲取一個排他鎖,這個時間將在另一個線程放棄這個鎖的時候發生;而「等待狀態」則是在等待一段時間,或者喚醒動做的發生。在程序等待進入同步區域的時候,線程將進入這種狀態。
已終止線程的線程狀態。
// 待填
// 待填
有點懶了。。。先貼幾個網址吧。
1. Official:The class File Format
2.亦山: 《Java虛擬機原理圖解》 1.一、class文件基本組織結構
點擊免費「領取Java架構資料」
虛擬機把描述類的數據從 Class 文件加載到內存,並對數據進行校驗、裝換解析和初始化,最終造成能夠被虛擬機直接使用的 Java 類型。
在 Java 語言中,類型的加載、鏈接和初始化過程都是在程序運行期間完成的。
其中加載、驗證、準備、初始化和卸載這五個階段的順序是肯定的。解析階段能夠在初始化以後再開始(運行時綁定或動態綁定或晚期綁定)。
如下五種狀況必須對類進行初始化(而加載、驗證、準備天然須要在此以前完成):
前面的五種方式是對一個類的主動引用,除此以外,全部引用類的方法都不會觸發初始化,佳做被動引用。舉幾個例子~
public class SuperClass { static { System.out.println("SuperClass init!"); } public static int value = 1127; } public class SubClass extends SuperClass { static { System.out.println("SubClass init!"); } } public class ConstClass { static { System.out.println("ConstClass init!"); } public static final String HELLOWORLD = "hello world!" } public class NotInitialization { public static void main(String[] args) { System.out.println(SubClass.value); /** * output : SuperClass init! * * 經過子類引用父類的靜態對象不會致使子類的初始化 * 只有直接定義這個字段的類纔會被初始化 */ SuperClass[] sca = new SuperClass[10]; /** * output : * * 經過數組定義來引用類不會觸發此類的初始化 * 虛擬機在運行時動態建立了一個數組類 */ System.out.println(ConstClass.HELLOWORLD); /** * output : * * 常量在編譯階段會存入調用類的常量池當中,本質上並無直接引用到定義類常量的類, * 所以不會觸發定義常量的類的初始化。 * 「hello world」 在編譯期常量傳播優化時已經存儲到 NotInitialization 常量池中了。 */ } }複製代碼
6.2.1 加載
數組類的特殊性:數組類自己不經過類加載器建立,它是由 Java 虛擬機直接建立的。但數組類與類加載器仍然有很密切的關係,由於數組類的元素類型最終是要靠類加載器去建立的,數組建立過程以下:
內存中實例的 java.lang.Class 對象存在方法區中。做爲程序訪問方法區中這些類型數據的外部接口。
加載階段與鏈接階段的部份內容是交叉進行的,可是開始時間保持前後順序。
6.2.2 驗證
是鏈接的第一步,確保 Class 文件的字節流中包含的信息符合當前虛擬機要求。
文件格式驗證
只有經過這個階段的驗證後,字節流纔會進入內存的方法區進行存儲,因此後面 3 個驗證階段所有是基於方法區的存儲結構進行的,再也不直接操做字節流。
元數據驗證
這一階段主要是對類的元數據信息進行語義校驗,保證不存在不符合 Java 語言規範的元數據信息。
字節碼驗證
這是整個驗證過程當中最複雜的一個階段,主要目的是經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。這個階段對類的方法體進行校驗分析,保證校驗類的方法在運行時不會作出危害虛擬機安全的事件。
符號引用驗證
最後一個階段的校驗發生在迅疾將符號引用轉化爲直接引用的時候,這個轉化動做將在鏈接的第三階段——解析階段中發生。符號引用驗證能夠看作是對類自身之外(常量池中的各類符號引用)的信息進行匹配性校驗,還有以上說起的內容。
符號引用的目的是確保解析動做能正常執行,若是沒法經過符號引用驗證將拋出一個 java.lang.IncompatibleClass.ChangeError 異常的子類。如 java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError 等。
6.2.3 準備
這個階段正式爲類分配內存並設置類變量初始值,內存在方法去中分配(含 static 修飾的變量不含實例變量)。
public static int value = 1127;
這句代碼在初始值設置以後爲 0,由於這時候還沒有開始執行任何 Java 方法。而把 value 賦值爲 1127 的 putstatic 指令是程序被編譯後,存放於 clinit() 方法中,因此初始化階段纔會對 value 進行賦值。
基本數據類型的零值
數據類型 | 零值 | 數據類型 | 零值 |
---|---|---|---|
int | 0 | boolean | false |
long | 0L | float | 0.0f |
short | (short) 0 | double | 0.0d |
char | '\u0000' | reference | null |
byte | (byte) 0 |
特殊狀況:若是類字段的字段屬性表中存在 ConstantValue 屬性,在準備階段虛擬機就會根據 ConstantValue 的設置將 value 賦值爲 1127。
6.2.4 解析
這個階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。
解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符 7 類符號引用進行,分別對應於常量池的 7 中常量類型。
6.2.5 初始化
前面過程都是以虛擬機主導,而初始化階段開始執行類中的 Java 代碼。
經過一個類的全限定名來獲取描述此類的二進制字節流。
6.3.1 雙親委派模型
從 Java 虛擬機角度講,只存在兩種類加載器:一種是啓動類加載器(C++ 實現,是虛擬機的一部分);另外一種是其餘全部類的加載器(Java 實現,獨立於虛擬機外部且全繼承自 java.lang.ClassLoader)
啓動類加載器
加載 lib 下或被 -Xbootclasspath 路徑下的類
擴展類加載器
加載 lib/ext 或者被 java.ext.dirs 系統變量所指定的路徑下的類
引用程序類加載器
ClassLoader負責,加載用戶路徑上所指定的類庫。
除頂層啓動類加載器以外,其餘都有本身的父類加載器。
工做過程:若是一個類加載器收到一個類加載的請求,它首先不會本身加載,而是把這個請求委派給父類加載器。只有父類沒法完成時子類纔會嘗試加載。
6.3.2 破壞雙親委派模型
keyword:線程上下文加載器(Thread Context ClassLoader)