1. Java 內(nèi)存區(qū)域與內(nèi)存溢出異常 1.1 運(yùn)行時數(shù)據(jù)區(qū)域 根據(jù)《Java 虛擬機(jī)規(guī)范(Java SE 7 版)》規(guī)定,Java 虛擬機(jī)所管理的內(nèi)存如下圖所示。
1.1.1 程序計數(shù)器 內(nèi)存空間小,線程私有。字節(jié)碼解釋器工作是就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行指令的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴計數(shù)器完成
如果線程正在執(zhí)行一個 Java 方法,這個計數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是 Native 方法,這個計數(shù)器的值則為 (Undefined)。此內(nèi)存區(qū)域是唯一一個在 Java 虛擬機(jī)規(guī)范中沒有規(guī)定任何 OutOfMemoryError 情況的區(qū)域。 1.1.2 Java 虛擬機(jī)棧 線程私有,生命周期和線程一致。描述的是 Java 方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行時都會床創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表 、操作數(shù)棧 、動態(tài)鏈接 、方法出口 等信息。每一個方法從調(diào)用直至執(zhí)行結(jié)束,就對應(yīng)著一個棧幀從虛擬機(jī)棧中入棧到出棧的過程。
局部變量表:存放了編譯期可知的各種基本類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference 類型)和 returnAddress 類型(指向了一條字節(jié)碼指令的地址) StackOverflowError:線程請求的棧深度大于虛擬機(jī)所允許的深度。 OutOfMemoryError:如果虛擬機(jī)??梢詣討B(tài)擴(kuò)展,而擴(kuò)展時無法申請到足夠的內(nèi)存。 1.1.3 本地方法棧 區(qū)別于 Java 虛擬機(jī)棧的是,Java 虛擬機(jī)棧為虛擬機(jī)執(zhí)行 Java 方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機(jī)使用到的 Native 方法服務(wù)。也會有 StackOverflowError 和 OutOfMemoryError 異常。
1.1.4 Java 堆 對于絕大多數(shù)應(yīng)用來說,這塊區(qū)域是 JVM 所管理的內(nèi)存中最大的一塊。線程共享,主要是存放對象實例和數(shù)組。內(nèi)部會劃分出多個線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer, TLAB)。可以位于物理上不連續(xù)的空間,但是邏輯上要連續(xù)。
OutOfMemoryError:如果堆中沒有內(nèi)存完成實例分配,并且堆也無法再擴(kuò)展時,拋出該異常。 1.1.5 方法區(qū) 屬于共享內(nèi)存區(qū)域,存儲已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。
現(xiàn)在用一張圖來介紹每個區(qū)域存儲的內(nèi)容。 1.1.6 運(yùn)行時常量池 屬于方法區(qū)一部分,用于存放編譯期生成的各種字面量和符號引用。編譯器和運(yùn)行期(String 的 intern() )都可以將常量放入池中。內(nèi)存有限,無法申請時拋出 OutOfMemoryError。
1.1.7 直接內(nèi)存 非虛擬機(jī)運(yùn)行時數(shù)據(jù)區(qū)的部分
在 JDK 1.4 中新加入 NIO (New Input/Output) 類,引入了一種基于通道(Channel)和緩存(Buffer)的 I/O 方式,它可以使用 Native 函數(shù)庫直接分配堆外內(nèi)存,然后通過一個存儲在 Java 堆中的 DirectByteBuffer 對象作為這塊內(nèi)存的引用進(jìn)行操作??梢员苊庠?Java 堆和 Native 堆中來回的數(shù)據(jù)耗時操作。 OutOfMemoryError:會受到本機(jī)內(nèi)存限制,如果內(nèi)存區(qū)域總和大于物理內(nèi)存限制從而導(dǎo)致動態(tài)擴(kuò)展時出現(xiàn)該異常。 1.2 HotSpot 虛擬機(jī)對象探秘 主要介紹數(shù)據(jù)是如何創(chuàng)建、如何布局以及如何訪問的。
1.2.1 對象的創(chuàng)建 創(chuàng)建過程比較復(fù)雜,建議看書了解,這里提供個人的總結(jié)。
遇到 new 指令時,首先檢查這個指令的參數(shù)是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已經(jīng)被加載、解析和初始化過。如果沒有,執(zhí)行相應(yīng)的類加載。 類加載檢查通過之后,為新對象分配內(nèi)存(內(nèi)存大小在類加載完成后便可確認(rèn))。在堆的空閑內(nèi)存中劃分一塊區(qū)域(‘指針碰撞-內(nèi)存規(guī)整’或‘空閑列表-內(nèi)存交錯’的分配方式)。 前面講的每個線程在堆中都會有私有的分配緩沖區(qū)(TLAB),這樣可以很大程度避免在并發(fā)情況下頻繁創(chuàng)建對象造成的線程不安全。 內(nèi)存空間分配完成后會初始化為 0(不包括對象頭),接下來就是填充對象頭,把對象是哪個類的實例、如何才能找到類的元數(shù)據(jù)信息、對象的哈希碼、對象的 GC 分代年齡等信息存入對象頭。 執(zhí)行 new 指令后執(zhí)行 init 方法后才算一份真正可用的對象創(chuàng)建完成。 1.2.2 對象的內(nèi)存布局 在 HotSpot 虛擬機(jī)中,分為 3 塊區(qū)域:對象頭(Header) 、實例數(shù)據(jù)(Instance Data) 和對齊填充(Padding)
對象頭(Header) :包含兩部分,第一部分用于存儲對象自身的運(yùn)行時數(shù)據(jù),如哈希碼、GC 分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程 ID、偏向時間戳等,32 位虛擬機(jī)占 32 bit,64 位虛擬機(jī)占 64 bit。官方稱為 ‘Mark Word’。第二部分是類型指針,即對象指向它的類的元數(shù)據(jù)指針,虛擬機(jī)通過這個指針確定這個對象是哪個類的實例。另外,如果是 Java 數(shù)組,對象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù),因為普通對象可以通過 Java 對象元數(shù)據(jù)確定大小,而數(shù)組對象不可以。
實例數(shù)據(jù)(Instance Data) :程序代碼中所定義的各種類型的字段內(nèi)容(包含父類繼承下來的和子類中定義的)。
對齊填充(Padding) :不是必然需要,主要是占位,保證對象大小是某個字節(jié)的整數(shù)倍。
1.2.3 對象的訪問定位 使用對象時,通過棧上的 reference 數(shù)據(jù)來操作堆上的具體對象。
通過句柄訪問 Java 堆中會分配一塊內(nèi)存作為句柄池。reference 存儲的是句柄地址。詳情見圖。
使用直接指針訪問 reference 中直接存儲對象地址
比較:使用句柄的最大好處是 reference 中存儲的是穩(wěn)定的句柄地址,在對象移動(GC)是只改變實例數(shù)據(jù)指針地址,reference 自身不需要修改。直接指針訪問的最大好處是速度快,節(jié)省了一次指針定位的時間開銷。如果是對象頻繁 GC 那么句柄方法好,如果是對象頻繁訪問則直接指針訪問好。 1.3 實戰(zhàn) // 待填
2. 垃圾回收器與內(nèi)存分配策略 2.1 概述 程序計數(shù)器、虛擬機(jī)棧、本地方法棧 3 個區(qū)域隨線程生滅(因為是線程私有),棧中的棧幀隨著方法的進(jìn)入和退出而有條不紊地執(zhí)行著出棧和入棧操作。而 Java 堆和方法區(qū)則不一樣,一個接口中的多個實現(xiàn)類需要的內(nèi)存可能不一樣,一個方法中的多個分支需要的內(nèi)存也可能不一樣,我們只有在程序處于運(yùn)行期才知道那些對象會創(chuàng)建,這部分內(nèi)存的分配和回收都是動態(tài)的,垃圾回收期所關(guān)注的就是這部分內(nèi)存。
2.2 對象已死嗎? 在進(jìn)行內(nèi)存回收之前要做的事情就是判斷那些對象是‘死’的,哪些是‘活’的。
2.2.1 引用計數(shù)法 給對象添加一個引用計數(shù)器。但是難以解決循環(huán)引用問題。
從圖中可以看出,如果不下小心直接把 Obj1-reference 和 Obj2-reference 置 null。則在 Java 堆當(dāng)中的兩塊內(nèi)存依然保持著互相引用無法回收。 2.2.2 可達(dá)性分析法 通過一系列的 ‘GC Roots’ 的對象作為起始點,從這些節(jié)點出發(fā)所走過的路徑稱為引用鏈。當(dāng)一個對象到 GC Roots 沒有任何引用鏈相連的時候說明對象不可用。
可作為 GC Roots 的對象: 2.2.3 再談引用 前面的兩種方式判斷存活時都與‘引用’有關(guān)。但是 JDK 1.2 之后,引用概念進(jìn)行了擴(kuò)充,下面具體介紹。
下面四種引用強(qiáng)度一次逐漸減弱 強(qiáng)引用 類似于 Object obj = new Object(); 創(chuàng)建的,只要強(qiáng)引用在就不回收。
軟引用 SoftReference 類實現(xiàn)軟引用。在系統(tǒng)要發(fā)生內(nèi)存溢出異常之前,將會把這些對象列進(jìn)回收范圍之中進(jìn)行二次回收。
弱引用 WeakReference 類實現(xiàn)弱引用。對象只能生存到下一次垃圾收集之前。在垃圾收集器工作時,無論內(nèi)存是否足夠都會回收掉只被弱引用關(guān)聯(lián)的對象。
虛引用 PhantomReference 類實現(xiàn)虛引用。無法通過虛引用獲取一個對象的實例,為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個對象被收集器回收時收到一個系統(tǒng)通知。
2.2.4 生存還是死亡 即使在可達(dá)性分析算法中不可達(dá)的對象,也并非是“facebook”的,這時候它們暫時出于“緩刑”階段,一個對象的真正死亡至少要經(jīng)歷兩次標(biāo)記過程:如果對象在進(jìn)行中可達(dá)性分析后發(fā)現(xiàn)沒有與 GC Roots 相連接的引用鏈,那他將會被第一次標(biāo)記并且進(jìn)行一次篩選,篩選條件是此對象是否有必要執(zhí)行 finalize() 方法。當(dāng)對象沒有覆蓋 finalize() 方法,或者 finalize() 方法已經(jīng)被虛擬機(jī)調(diào)用過,虛擬機(jī)將這兩種情況都視為“沒有必要執(zhí)行”。 如果這個對象被判定為有必要執(zhí)行 finalize() 方法,那么這個對象竟會放置在一個叫做 F-Queue 的隊列中,并在稍后由一個由虛擬機(jī)自動建立的、低優(yōu)先級的 Finalizer 線程去執(zhí)行它。這里所謂的“執(zhí)行”是指虛擬機(jī)會出發(fā)這個方法,并不承諾或等待他運(yùn)行結(jié)束。finalize() 方法是對象逃脫死亡命運(yùn)的最后一次機(jī)會,稍后 GC 將對 F-Queue 中的對象進(jìn)行第二次小規(guī)模的標(biāo)記,如果對象要在 finalize() 中成功拯救自己 —— 只要重新與引用鏈上的任何一個對象簡歷關(guān)聯(lián)即可。 finalize() 方法只會被系統(tǒng)自動調(diào)用一次。
2.2.5 回收方法區(qū) 在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空間,而永久代的垃圾收集效率遠(yuǎn)低于此。 永久代垃圾回收主要兩部分內(nèi)容:廢棄的常量和無用的類。
判斷廢棄常量:一般是判斷沒有該常量的引用。 判斷無用的類:要以下三個條件都滿足 該類所有的實例都已經(jīng)回收,也就是 Java 堆中不存在該類的任何實例 加載該類的 ClassLoader 已經(jīng)被回收 該類對應(yīng)的 java.lang.Class 對象沒有任何地方唄引用,無法在任何地方通過反射訪問該類的方法
2.3 垃圾回收算法 僅提供思路
2.3.1 標(biāo)記 —— 清除算法 直接標(biāo)記清除就可。
兩個不足: 2.3.2 復(fù)制算法 把空間分成兩塊,每次只對其中一塊進(jìn)行 GC。當(dāng)這塊內(nèi)存使用完時,就將還存活的對象復(fù)制到另一塊上面。
解決前一種方法的不足,但是會造成空間利用率低下。因為大多數(shù)新生代對象都不會熬過第一次 GC。所以沒必要 1 : 1 劃分空間??梢苑忠粔K較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 空間和其中一塊 Survivor。當(dāng)回收時,將 Eden 和 Survivor 中還存活的對象一次性復(fù)制到另一塊 Survivor 上,最后清理 Eden 和 Survivor 空間。大小比例一般是 8 : 1 : 1,每次浪費(fèi) 10% 的 Survivor 空間。但是這里有一個問題就是如果存活的大于 10% 怎么辦?這里采用一種分配擔(dān)保策略:多出來的對象直接進(jìn)入老年代。 2.3.3 標(biāo)記-整理算法 不同于針對新生代的復(fù)制算法,針對老年代的特點,創(chuàng)建該算法。主要是把存活對象移到內(nèi)存的一端。
2.3.4 分代回收 根據(jù)存活對象劃分幾塊內(nèi)存區(qū),一般是分為新生代和老年代。然后根據(jù)各個年代的特點制定相應(yīng)的回收算法。
新生代 每次垃圾回收都有大量對象死去,只有少量存活,選用復(fù)制算法比較合理。
老年代 老年代中對象存活率較高、沒有額外的空間分配對它進(jìn)行擔(dān)保。所以必須使用 標(biāo)記 —— 清除 或者 標(biāo)記 —— 整理 算法回收。
2.4 HotSpot 的算法實現(xiàn) // 待填
2.5 垃圾回收器 收集算法是內(nèi)存回收的理論,而垃圾回收器是內(nèi)存回收的實踐。
說明:如果兩個收集器之間存在連線說明他們之間可以搭配使用。 2.5.1 Serial 收集器 這是一個單線程收集器。意味著它只會使用一個 CPU 或一條收集線程去完成收集工作,并且在進(jìn)行垃圾回收時必須暫停其它所有的工作線程直到收集結(jié)束。
2.5.2 ParNew 收集器 可以認(rèn)為是 Serial 收集器的多線程版本。
并行:Parallel 指多條垃圾收集線程并行工作,此時用戶線程處于等待狀態(tài)
并發(fā):Concurrent 指用戶線程和垃圾回收線程同時執(zhí)行(不一定是并行,有可能是交叉執(zhí)行),用戶進(jìn)程在運(yùn)行,而垃圾回收線程在另一個 CPU 上運(yùn)行。
2.5.3 Parallel Scavenge 收集器 這是一個新生代收集器,也是使用復(fù)制算法實現(xiàn),同時也是并行的多線程收集器。
CMS 等收集器的關(guān)注點是盡可能地縮短垃圾收集時用戶線程所停頓的時間,而 Parallel Scavenge 收集器的目的是達(dá)到一個可控制的吞吐量(Throughput = 運(yùn)行用戶代碼時間 / (運(yùn)行用戶代碼時間 + 垃圾收集時間))。 作為一個吞吐量優(yōu)先的收集器,虛擬機(jī)會根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況收集性能監(jiān)控信息,動態(tài)調(diào)整停頓時間。這就是 GC 的自適應(yīng)調(diào)整策略(GC Ergonomics)。 2.5.4 Serial Old 收集器 收集器的老年代版本,單線程,使用 標(biāo)記 —— 整理 。
2.5.5 Parallel Old 收集器 Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多線程,使用 標(biāo)記 —— 整理
2.5.6 CMS 收集器 CMS (Concurrent Mark Sweep) 收集器是一種以獲取最短回收停頓時間為目標(biāo)的收集器?;?標(biāo)記 —— 清除 算法實現(xiàn)。
運(yùn)作步驟: 初始標(biāo)記(CMS initial mark):標(biāo)記 GC Roots 能直接關(guān)聯(lián)到的對象 并發(fā)標(biāo)記(CMS concurrent mark):進(jìn)行 GC Roots Tracing 重新標(biāo)記(CMS remark):修正并發(fā)標(biāo)記期間的變動部分 并發(fā)清除(CMS concurrent sweep)
缺點:對 CPU 資源敏感、無法收集浮動垃圾、標(biāo)記 —— 清除 算法帶來的空間碎片 2.5.7 G1 收集器 面向服務(wù)端的垃圾回收器。
優(yōu)點:并行與并發(fā)、分代收集、空間整合、可預(yù)測停頓。 運(yùn)作步驟: 初始標(biāo)記(Initial Marking) 并發(fā)標(biāo)記(Concurrent Marking) 最終標(biāo)記(Final Marking) 篩選回收(Live Data Counting and Evacuation)
2.6 內(nèi)存分配與回收策略 2.6.1 對象優(yōu)先在 Eden 分配 對象主要分配在新生代的 Eden 區(qū)上,如果啟動了本地線程分配緩沖區(qū),將線程優(yōu)先在 (TLAB) 上分配。少數(shù)情況會直接分配在老年代中。
一般來說 Java 堆的內(nèi)存模型如下圖所示: 新生代 GC (Minor GC) 發(fā)生在新生代的垃圾回收動作,頻繁,速度快。
老年代 GC (Major GC / Full GC) 發(fā)生在老年代的垃圾回收動作,出現(xiàn)了 Major GC 經(jīng)常會伴隨至少一次 Minor GC(非絕對)。Major GC 的速度一般會比 Minor GC 慢十倍以上。
2.6.2 大對象直接進(jìn)入老年代 2.6.3 長期存活的對象將進(jìn)入老年代 2.6.4 動態(tài)對象年齡判定 2.6.5 空間分配擔(dān)保 3. Java 內(nèi)存模型與線程 3.1 Java 內(nèi)存模型 屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異。
3.1.1 主內(nèi)存和工作內(nèi)存之間的交互 操作 | 作用對象 | 解釋 |
---|
lock | 主內(nèi)存 | 把一個變量標(biāo)識為一條線程獨占的狀態(tài) | unlock | 主內(nèi)存 | 把一個處于鎖定狀態(tài)的變量釋放出來,釋放后才可被其他線程鎖定 | read | 主內(nèi)存 | 把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程工作內(nèi)存中,以便 load 操作使用 | load | 工作內(nèi)存 | 把 read 操作從主內(nèi)存中得到的變量值放入工作內(nèi)存中 | use | 工作內(nèi)存 | 把工作內(nèi)存中一個變量的值傳遞給執(zhí)行引擎, 每當(dāng)虛擬機(jī)遇到一個需要使用到變量值的字節(jié)碼指令時將會執(zhí)行這個操作 | assign | 工作內(nèi)存 | 把一個從執(zhí)行引擎接收到的值賦接收到的值賦給工作內(nèi)存的變量, 每當(dāng)虛擬機(jī)遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作 | store | 工作內(nèi)存 | 把工作內(nèi)存中的一個變量的值傳送到主內(nèi)存中,以便 write 操作 | write | 工作內(nèi)存 | 把 store 操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中 |
3.1.2 對于 volatile 型變量的特殊規(guī)則 關(guān)鍵字 volatile 是 Java 虛擬機(jī)提供的最輕量級的同步機(jī)制。
一個變量被定義為 volatile 的特性: 保證此變量對所有線程的可見性。但是操作并非原子操作,并發(fā)情況下不安全。
如果不符合 運(yùn)算結(jié)果并不依賴變量當(dāng)前值,或者能夠確保只有單一的線程修改變量的值 和 變量不需要與其他的狀態(tài)變量共同參與不變約束 就要通過加鎖(使用 synchronize 或 java.util.concurrent 中的原子類)來保證原子性。
禁止指令重排序優(yōu)化。
通過插入內(nèi)存屏障保證一致性。
3.1.3 對于 long 和 double 型變量的特殊規(guī)則 Java 要求對于主內(nèi)存和工作內(nèi)存之間的八個操作都是原子性的,但是對于 64 位的數(shù)據(jù)類型,有一條寬松的規(guī)定:允許虛擬機(jī)將沒有被 volatile 修飾的 64 位數(shù)據(jù)的讀寫操作劃分為兩次 32 位的操作來進(jìn)行,即允許虛擬機(jī)實現(xiàn)選擇可以不保證 64 位數(shù)據(jù)類型的 load、store、read 和 write 這 4 個操作的原子性。這就是 long 和 double 的非原子性協(xié)定。
3.1.4 原子性、可見性與有序性 回顧下并發(fā)下應(yīng)該注意操作的那些特性是什么,同時加深理解。
由 Java 內(nèi)存模型來直接保證的原子性變量操作包括 read、load、assign、use、store 和 write。大致可以認(rèn)為基本數(shù)據(jù)類型的操作是原子性的。同時 lock 和 unlock 可以保證更大范圍操作的原子性。而 synchronize 同步塊操作的原子性是用更高層次的字節(jié)碼指令 monitorenter 和 monitorexit 來隱式操作的。
是指當(dāng)一個線程修改了共享變量的值,其他線程也能夠立即得知這個通知。主要操作細(xì)節(jié)就是修改值后將值同步至主內(nèi)存(volatile 值使用前都會從主內(nèi)存刷新),除了 volatile 還有 synchronize 和 final 可以保證可見性。同步塊的可見性是由“對一個變量執(zhí)行 unlock 操作之前,必須先把此變量同步會主內(nèi)存中( store、write 操作)”這條規(guī)則獲得。而 final 可見性是指:被 final 修飾的字段在構(gòu)造器中一旦完成,并且構(gòu)造器沒有把 “this” 的引用傳遞出去( this 引用逃逸是一件很危險的事情,其他線程有可能通過這個引用訪問到“初始化了一半”的對象),那在其他線程中就能看見 final 字段的值。
如果在被線程內(nèi)觀察,所有操作都是有序的;如果在一個線程中觀察另一個線程,所有操作都是無序的。前半句指“線程內(nèi)表現(xiàn)為串行的語義”,后半句是指“指令重排”現(xiàn)象和“工作內(nèi)存與主內(nèi)存同步延遲”現(xiàn)象。Java 語言通過 volatile 和 synchronize 兩個關(guān)鍵字來保證線程之間操作的有序性。volatile 自身就禁止指令重排,而 synchronize 則是由“一個變量在同一時刻指允許一條線程對其進(jìn)行 lock 操作”這條規(guī)則獲得,這條規(guī)則決定了持有同一個鎖的兩個同步塊只能串行的進(jìn)入。
3.1.5 先行發(fā)生原則 也就是 happens-before 原則。這個原則是判斷數(shù)據(jù)是否存在競爭、線程是否安全的主要依據(jù)。先行發(fā)生是 Java 內(nèi)存模型中定義的兩項操作之間的偏序關(guān)系。
天然的先行發(fā)生關(guān)系 規(guī)則 | 解釋 |
---|
程序次序規(guī)則 | 在一個線程內(nèi),代碼按照書寫的控制流順序執(zhí)行 | 管程鎖定規(guī)則 | 一個 unlock 操作先行發(fā)生于后面對同一個鎖的 lock 操作 | volatile 變量規(guī)則 | volatile 變量的寫操作先行發(fā)生于后面對這個變量的讀操作 | 線程啟動規(guī)則 | Thread 對象的 start() 方法先行發(fā)生于此線程的每一個動作 | 線程終止規(guī)則 | 線程中所有的操作都先行發(fā)生于對此線程的終止檢測 (通過 Thread.join() 方法結(jié)束、 Thread.isAlive() 的返回值檢測) | 線程中斷規(guī)則 | 對線程 interrupt() 方法調(diào)用優(yōu)先發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生 (通過 Thread.interrupted() 方法檢測) | 對象終結(jié)規(guī)則 | 一個對象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的 finalize() 方法的開始 | 傳遞性 | 如果操作 A 先于 操作 B 發(fā)生,操作 B 先于 操作 C 發(fā)生,那么操作 A 先于 操作 C |
3.2 Java 與線程 3.2.1 線程的實現(xiàn) 使用內(nèi)核線程實現(xiàn) 直接由操作系統(tǒng)內(nèi)核支持的線程,這種線程由內(nèi)核完成切換。程序一般不會直接去使用內(nèi)核線程,而是去使用內(nèi)核線程的一種高級接口 —— 輕量級進(jìn)程(LWP),輕量級進(jìn)程就是我們通常意義上所講的線程,每個輕量級進(jìn)程都有一個內(nèi)核級線程支持。
使用用戶線程實現(xiàn) 廣義上來說,只要不是內(nèi)核線程就可以認(rèn)為是用戶線程,因此可以認(rèn)為輕量級進(jìn)程也屬于用戶線程。狹義上說是完全建立在用戶空間的線程庫上的并且內(nèi)核系統(tǒng)不可感知的。
使用用戶線程夾加輕量級進(jìn)程混合實現(xiàn) 直接看圖
Java 線程實現(xiàn) 平臺不同實現(xiàn)方式不同,可以認(rèn)為是一條 Java 線程映射到一條輕量級進(jìn)程。
3.2.2 Java 線程調(diào)度 協(xié)同式線程調(diào)度 線程執(zhí)行時間由線程自身控制,實現(xiàn)簡單,切換線程自己可知,所以基本沒有線程同步問題。壞處是執(zhí)行時間不可控,容易阻塞。
搶占式線程調(diào)度 每個線程由系統(tǒng)來分配執(zhí)行時間。
3.2.3 狀態(tài)轉(zhuǎn)換 五種狀態(tài): 創(chuàng)建后尚未啟動的線程。
Runable 包括了操作系統(tǒng)線程狀態(tài)中的 Running 和 Ready,也就是出于此狀態(tài)的線程有可能正在執(zhí)行,也有可能正在等待 CPU 為他分配時間。
出于這種狀態(tài)的線程不會被 CPU 分配時間,它們要等其他線程顯示的喚醒。
以下方法會然線程進(jìn)入無限期等待狀態(tài): 1.沒有設(shè)置 Timeout 參數(shù)的 Object.wait() 方法。 2.沒有設(shè)置 Timeout 參數(shù)的 Thread.join() 方法。 3.LookSupport.park() 方法。 處于這種狀態(tài)的線程也不會分配時間,不過無需等待配其他線程顯示地喚醒,在一定時間后他們會由系統(tǒng)自動喚醒。
以下方法會讓線程進(jìn)入限期等待狀態(tài): 1.Thread.sleep() 方法。 2.設(shè)置了 Timeout 參數(shù)的 Object.wait() 方法。 3.設(shè)置了 Timeout 參數(shù)的 Thread.join() 方法。 4.LockSupport.parkNanos() 方法。 5.LockSupport.parkUntil() 方法。 線程被阻塞了,“阻塞狀態(tài)”和“等待狀態(tài)”的區(qū)別是:“阻塞狀態(tài)”在等待著獲取一個排他鎖,這個時間將在另外一個線程放棄這個鎖的時候發(fā)生;而“等待狀態(tài)”則是在等待一段時間,或者喚醒動作的發(fā)生。在程序等待進(jìn)入同步區(qū)域的時候,線程將進(jìn)入這種狀態(tài)。
已終止線程的線程狀態(tài)。
4. 線程安全與鎖優(yōu)化 // 待填
5. 類文件結(jié)構(gòu) // 待填
有點懶了。。。先貼幾個網(wǎng)址吧。 1. Official:The class File Format 2.亦山: 《Java虛擬機(jī)原理圖解》 1.1、class文件基本組織結(jié)構(gòu) 6. 虛擬機(jī)類加載機(jī)制 虛擬機(jī)把描述類的數(shù)據(jù)從 Class 文件加載到內(nèi)存,并對數(shù)據(jù)進(jìn)行校驗、裝換解析和初始化,最終形成可以被虛擬機(jī)直接使用的 Java 類型。
在 Java 語言中,類型的加載、連接和初始化過程都是在程序運(yùn)行期間完成的。 6.1 類加載時機(jī) 類的生命周期( 7 個階段) 其中加載、驗證、準(zhǔn)備、初始化和卸載這五個階段的順序是確定的。解析階段可以在初始化之后再開始(運(yùn)行時綁定或動態(tài)綁定或晚期綁定)。 以下五種情況必須對類進(jìn)行初始化(而加載、驗證、準(zhǔn)備自然需要在此之前完成): 遇到 new、getstatic、putstatic 或 invokestatic 這 4 條字節(jié)碼指令時沒初始化觸發(fā)初始化。使用場景:使用 new 關(guān)鍵字實例化對象、讀取一個類的靜態(tài)字段(被 final 修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)、調(diào)用一個類的靜態(tài)方法。 使用 java.lang.reflect 包的方法對類進(jìn)行反射調(diào)用的時候。 當(dāng)初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行初始化,則需先觸發(fā)其父類的初始化。 當(dāng)虛擬機(jī)啟動時,用戶需指定一個要加載的主類(包含 main() 方法的那個類),虛擬機(jī)會先初始化這個主類。 當(dāng)使用 JDK 1.7 的動態(tài)語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最后的解析結(jié)果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且這個方法句柄所對應(yīng)的類沒有進(jìn)行過初始化,則需先觸發(fā)其初始化。
前面的五種方式是對一個類的主動引用,除此之外,所有引用類的方法都不會觸發(fā)初始化,佳作被動引用。舉幾個例子~ 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! * * 通過子類引用父類的靜態(tài)對象不會導(dǎo)致子類的初始化 * 只有直接定義這個字段的類才會被初始化 */ SuperClass[] sca = new SuperClass[10]; /** * output : * * 通過數(shù)組定義來引用類不會觸發(fā)此類的初始化 * 虛擬機(jī)在運(yùn)行時動態(tài)創(chuàng)建了一個數(shù)組類 */ System.out.println(ConstClass.HELLOWORLD); /** * output : * * 常量在編譯階段會存入調(diào)用類的常量池當(dāng)中,本質(zhì)上并沒有直接引用到定義類常量的類, * 因此不會觸發(fā)定義常量的類的初始化。 * “hello world” 在編譯期常量傳播優(yōu)化時已經(jīng)存儲到 NotInitialization 常量池中了。 */ }}
6.2 類的加載過程 6.2.1 加載 通過一個類的全限定名來獲取定義次類的二進(jìn)制流(ZIP 包、網(wǎng)絡(luò)、運(yùn)算生成、JSP 生成、數(shù)據(jù)庫讀取)。 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時數(shù)據(jù)結(jié)構(gòu)。 在內(nèi)存中生成一個代表這個類的 java.lang.Class 對象,作為方法去這個類的各種數(shù)據(jù)的訪問入口。
數(shù)組類的特殊性:數(shù)組類本身不通過類加載器創(chuàng)建,它是由 Java 虛擬機(jī)直接創(chuàng)建的。但數(shù)組類與類加載器仍然有很密切的關(guān)系,因為數(shù)組類的元素類型最終是要靠類加載器去創(chuàng)建的,數(shù)組創(chuàng)建過程如下: 如果數(shù)組的組件類型是引用類型,那就遞歸采用類加載加載。 如果數(shù)組的組件類型不是引用類型,Java 虛擬機(jī)會把數(shù)組標(biāo)記為引導(dǎo)類加載器關(guān)聯(lián)。 數(shù)組類的可見性與他的組件類型的可見性一致,如果組件類型不是引用類型,那數(shù)組類的可見性將默認(rèn)為 public。
內(nèi)存中實例的 java.lang.Class 對象存在方法區(qū)中。作為程序訪問方法區(qū)中這些類型數(shù)據(jù)的外部接口。 加載階段與連接階段的部分內(nèi)容是交叉進(jìn)行的,但是開始時間保持先后順序。 6.2.2 驗證 是連接的第一步,確保 Class 文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)要求。
文件格式驗證 是否以魔數(shù) 0xCAFEBABE 開頭 主、次版本號是否在當(dāng)前虛擬機(jī)處理范圍之內(nèi) 常量池的常量是否有不被支持常量的類型(檢查常量 tag 標(biāo)志) 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量 CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 編碼的數(shù)據(jù) Class 文件中各個部分集文件本身是否有被刪除的附加的其他信息 ……
只有通過這個階段的驗證后,字節(jié)流才會進(jìn)入內(nèi)存的方法區(qū)進(jìn)行存儲,所以后面 3 個驗證階段全部是基于方法區(qū)的存儲結(jié)構(gòu)進(jìn)行的,不再直接操作字節(jié)流。 元數(shù)據(jù)驗證 這個類是否有父類(除 java.lang.Object 之外) 這個類的父類是否繼承了不允許被繼承的類(final 修飾的類) 如果這個類不是抽象類,是否實現(xiàn)了其父類或接口之中要求實現(xiàn)的所有方法 類中的字段、方法是否與父類產(chǎn)生矛盾(覆蓋父類 final 字段、出現(xiàn)不符合規(guī)范的重載)
這一階段主要是對類的元數(shù)據(jù)信息進(jìn)行語義校驗,保證不存在不符合 Java 語言規(guī)范的元數(shù)據(jù)信息。 字節(jié)碼驗證 保證任意時刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都鞥配合工作(不會出現(xiàn)按照 long 類型讀一個 int 型數(shù)據(jù)) 保證跳轉(zhuǎn)指令不會跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上 保證方法體中的類型轉(zhuǎn)換是有效的(子類對象賦值給父類數(shù)據(jù)類型是安全的,反過來不合法的) ……
這是整個驗證過程中最復(fù)雜的一個階段,主要目的是通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的、符合邏輯的。這個階段對類的方法體進(jìn)行校驗分析,保證校驗類的方法在運(yùn)行時不會做出危害虛擬機(jī)安全的事件。 符號引用驗證 符號引用中通過字符創(chuàng)描述的全限定名是否能找到對應(yīng)的類 在指定類中是否存在符方法的字段描述符以及簡單名稱所描述的方法和字段 符號引用中的類、字段、方法的訪問性(private、protected、public、default)是否可被當(dāng)前類訪問 ……
最后一個階段的校驗發(fā)生在迅疾將符號引用轉(zhuǎn)化為直接引用的時候,這個轉(zhuǎn)化動作將在連接的第三階段——解析階段中發(fā)生。符號引用驗證可以看做是對類自身以外(常量池中的各種符號引用)的信息進(jìn)行匹配性校驗,還有以上提及的內(nèi)容。 符號引用的目的是確保解析動作能正常執(zhí)行,如果無法通過符號引用驗證將拋出一個 java.lang.IncompatibleClass.ChangeError 異常的子類。如 java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError 等。 6.2.3 準(zhǔn)備 這個階段正式為類分配內(nèi)存并設(shè)置類變量初始值,內(nèi)存在方法去中分配(含 static 修飾的變量不含實例變量)。
public static int value = 1127;
這句代碼在初始值設(shè)置之后為 0,因為這時候尚未開始執(zhí)行任何 Java 方法。而把 value 賦值為 1127 的 putstatic 指令是程序被編譯后,存放于 clinit() 方法中,所以初始化階段才會對 value 進(jìn)行賦值。 基本數(shù)據(jù)類型的零值 數(shù)據(jù)類型 | 零值 | 數(shù)據(jù)類型 | 零值 |
---|
int | 0 | boolean | false | long | 0L | float | 0.0f | short | (short) 0 | double | 0.0d | char | '' | reference | null | byte | (byte) 0 | |
|
特殊情況:如果類字段的字段屬性表中存在 ConstantValue 屬性,在準(zhǔn)備階段虛擬機(jī)就會根據(jù) ConstantValue 的設(shè)置將 value 賦值為 1127。 6.2.4 解析 這個階段是虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程。
符號引用 符號引用以一組符號來描述所引用的目標(biāo),符號可以使任何形式的字面量。 直接引用 直接引用可以使直接指向目標(biāo)的指針、相對偏移量或是一個能間接定位到目標(biāo)的句柄。直接引用和迅疾的內(nèi)存布局實現(xiàn)有關(guān)
解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點限定符 7 類符號引用進(jìn)行,分別對應(yīng)于常量池的 7 中常量類型。 6.2.5 初始化 前面過程都是以虛擬機(jī)主導(dǎo),而初始化階段開始執(zhí)行類中的 Java 代碼。
6.3 類加載器 通過一個類的全限定名來獲取描述此類的二進(jìn)制字節(jié)流。
6.3.1 雙親委派模型 從 Java 虛擬機(jī)角度講,只存在兩種類加載器:一種是啟動類加載器(C++ 實現(xiàn),是虛擬機(jī)的一部分);另一種是其他所有類的加載器(Java 實現(xiàn),獨立于虛擬機(jī)外部且全繼承自 java.lang.ClassLoader)
啟動類加載器 加載 lib 下或被 -Xbootclasspath 路徑下的類 擴(kuò)展類加載器 加載 lib/ext 或者被 java.ext.dirs 系統(tǒng)變量所指定的路徑下的類 引用程序類加載器 ClassLoader負(fù)責(zé),加載用戶路徑上所指定的類庫。
除頂層啟動類加載器之外,其他都有自己的父類加載器。 工作過程:如果一個類加載器收到一個類加載的請求,它首先不會自己加載,而是把這個請求委派給父類加載器。只有父類無法完成時子類才會嘗試加載。 6.3.2 破壞雙親委派模型 keyword:線程上下文加載器(Thread Context ClassLoader)
最后 前面兩次粗略的閱讀,能理解內(nèi)容,但是很難記住細(xì)節(jié)。每每碰到不會的知識點就上網(wǎng)查,所以知識點太碎片腦子里沒有體系不僅更不容易記住,而且更加容易混亂。但是通過這種方式記錄發(fā)現(xiàn)自己清晰了很多,就算以后忘記,知識再次撿起的成本也低了很多。 這次還有一些章節(jié)雖然閱讀了,但是還未完成記錄。等自己理解深刻有空閑了就再次記錄下來,這里的內(nèi)容均出自周志明老師的《深入理解 Java 虛擬機(jī)》,有興趣的可以入手紙質(zhì)版。 多謝閱讀
Java工程化、高性能及分布式、高性能、深入淺出。高架構(gòu)。性能調(diào)優(yōu)、Spring,MyBatis,Netty源碼分析和大數(shù)據(jù)等多個知識點。如果你想拿高薪的,想學(xué)習(xí)的,想就業(yè)前景好的,想跟別人競爭能取得優(yōu)勢的,想進(jìn)阿里面試但擔(dān)心面試不過的,你都可以來,群號為:647631030 注:加群要求 1、具有1-5工作經(jīng)驗的,面對目前流行的技術(shù)不知從何下手,需要突破技術(shù)瓶頸的可以加。 2、在公司待久了,過得很安逸,但跳槽時面試碰壁。需要在短時間內(nèi)進(jìn)修、跳槽拿高薪的可以加。 3、如果沒有工作經(jīng)驗,但基礎(chǔ)非常扎實,對java工作機(jī)制,常用設(shè)計思想,常用java開發(fā)框架掌握熟練的,可以加。 4、覺得自己很牛B,一般需求都能搞定。但是所學(xué)的知識點沒有系統(tǒng)化,很難在技術(shù)領(lǐng)域繼續(xù)突破的可以加。 5.阿里Java高級大牛直播講解知識點,分享知識,多年工作經(jīng)驗的梳理和總結(jié),帶著大家全面、科學(xué)地建立自己的技術(shù)體系和技術(shù)認(rèn)知! 6.小號或者小白之類加群一律不給過,謝謝。
|