重讀 Java虛擬機(jī)(jvm)

作者

1. Java 內(nèi)存區(qū)域與內(nèi)存溢出異常

1.1 運(yùn)行時(shí)數(shù)據(jù)區(qū)域

根據(jù)《Java 虛擬機(jī)規(guī)范(Java SE 7 版)》規(guī)定,Java 虛擬機(jī)所管理的內(nèi)存如下圖所示。

1.1.1 程序計(jì)數(shù)器

內(nèi)存空間小,線程私有。字節(jié)碼解釋器工作是就是通過改變這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行指令的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴計(jì)數(shù)器完成

如果線程正在執(zhí)行一個(gè) Java 方法,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是 Native 方法,這個(gè)計(jì)數(shù)器的值則為 (Undefined)。此內(nèi)存區(qū)域是唯一一個(gè)在 Java 虛擬機(jī)規(guī)范中沒有規(guī)定任何 OutOfMemoryError 情況的區(qū)域。

1.1.2 Java 虛擬機(jī)棧

線程私有,生命周期和線程一致。描述的是 Java 方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行時(shí)都會(huì)床創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表操作數(shù)棧動(dòng)態(tài)鏈接方法出口等信息。每一個(gè)方法從調(diào)用直至執(zhí)行結(jié)束,就對(duì)應(yīng)著一個(gè)棧幀從虛擬機(jī)棧中入棧到出棧的過程。

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

StackOverflowError:線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度。

OutOfMemoryError:如果虛擬機(jī)棧可以動(dòng)態(tài)擴(kuò)展,而擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存。

1.1.3 本地方法棧

區(qū)別于 Java 虛擬機(jī)棧的是,Java 虛擬機(jī)棧為虛擬機(jī)執(zhí)行 Java 方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機(jī)使用到的 Native 方法服務(wù)。也會(huì)有 StackOverflowError 和 OutOfMemoryError 異常。

1.1.4 Java 堆

對(duì)于絕大多數(shù)應(yīng)用來說,這塊區(qū)域是 JVM 所管理的內(nèi)存中最大的一塊。線程共享,主要是存放對(duì)象實(shí)例和數(shù)組。內(nèi)部會(huì)劃分出多個(gè)線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer, TLAB)。可以位于物理上不連續(xù)的空間,但是邏輯上要連續(xù)。

OutOfMemoryError:如果堆中沒有內(nèi)存完成實(shí)例分配,并且堆也無法再擴(kuò)展時(shí),拋出該異常。

1.1.5 方法區(qū)

屬于共享內(nèi)存區(qū)域,存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。

現(xiàn)在用一張圖來介紹每個(gè)區(qū)域存儲(chǔ)的內(nèi)容。

1.1.6 運(yùn)行時(shí)常量池

屬于方法區(qū)一部分,用于存放編譯期生成的各種字面量和符號(hào)引用。編譯器和運(yùn)行期(String 的 intern() )都可以將常量放入池中。內(nèi)存有限,無法申請(qǐng)時(shí)拋出 OutOfMemoryError。

1.1.7 直接內(nèi)存

非虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的部分

在 JDK 1.4 中新加入 NIO (New Input/Output) 類,引入了一種基于通道(Channel)和緩存(Buffer)的 I/O 方式,它可以使用 Native 函數(shù)庫直接分配堆外內(nèi)存,然后通過一個(gè)存儲(chǔ)在 Java 堆中的 DirectByteBuffer 對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。可以避免在 Java 堆和 Native 堆中來回的數(shù)據(jù)耗時(shí)操作。

OutOfMemoryError:會(huì)受到本機(jī)內(nèi)存限制,如果內(nèi)存區(qū)域總和大于物理內(nèi)存限制從而導(dǎo)致動(dòng)態(tài)擴(kuò)展時(shí)出現(xiàn)該異常。

1.2 HotSpot 虛擬機(jī)對(duì)象探秘

主要介紹數(shù)據(jù)是如何創(chuàng)建、如何布局以及如何訪問的。

1.2.1 對(duì)象的創(chuàng)建

創(chuàng)建過程比較復(fù)雜,建議看書了解,這里提供個(gè)人的總結(jié)。

遇到 new 指令時(shí),首先檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類是否已經(jīng)被加載、解析和初始化過。如果沒有,執(zhí)行相應(yīng)的類加載。

類加載檢查通過之后,為新對(duì)象分配內(nèi)存(內(nèi)存大小在類加載完成后便可確認(rèn))。在堆的空閑內(nèi)存中劃分一塊區(qū)域(‘指針碰撞-內(nèi)存規(guī)整’或‘空閑列表-內(nèi)存交錯(cuò)’的分配方式)。

前面講的每個(gè)線程在堆中都會(huì)有私有的分配緩沖區(qū)(TLAB),這樣可以很大程度避免在并發(fā)情況下頻繁創(chuàng)建對(duì)象造成的線程不安全。

內(nèi)存空間分配完成后會(huì)初始化為 0(不包括對(duì)象頭),接下來就是填充對(duì)象頭,把對(duì)象是哪個(gè)類的實(shí)例、如何才能找到類的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的 GC 分代年齡等信息存入對(duì)象頭。

執(zhí)行 new 指令后執(zhí)行 init 方法后才算一份真正可用的對(duì)象創(chuàng)建完成。

1.2.2 對(duì)象的內(nèi)存布局

在 HotSpot 虛擬機(jī)中,分為 3 塊區(qū)域:對(duì)象頭(Header)實(shí)例數(shù)據(jù)(Instance Data)對(duì)齊填充(Padding)

對(duì)象頭(Header):包含兩部分,第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼、GC 分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程 ID、偏向時(shí)間戳等,32 位虛擬機(jī)占 32 bit,64 位虛擬機(jī)占 64 bit。官方稱為 ‘Mark Word’。第二部分是類型指針,即對(duì)象指向它的類的元數(shù)據(jù)指針,虛擬機(jī)通過這個(gè)指針確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。另外,如果是 Java 數(shù)組,對(duì)象頭中還必須有一塊用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù),因?yàn)槠胀▽?duì)象可以通過 Java 對(duì)象元數(shù)據(jù)確定大小,而數(shù)組對(duì)象不可以。

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

對(duì)齊填充(Padding):不是必然需要,主要是占位,保證對(duì)象大小是某個(gè)字節(jié)的整數(shù)倍。

1.2.3 對(duì)象的訪問定位

使用對(duì)象時(shí),通過棧上的 reference 數(shù)據(jù)來操作堆上的具體對(duì)象。

通過句柄訪問

Java 堆中會(huì)分配一塊內(nèi)存作為句柄池。reference 存儲(chǔ)的是句柄地址。詳情見圖。

使用直接指針訪問

reference 中直接存儲(chǔ)對(duì)象地址

比較:使用句柄的最大好處是 reference 中存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象移動(dòng)(GC)是只改變實(shí)例數(shù)據(jù)指針地址,reference 自身不需要修改。直接指針訪問的最大好處是速度快,節(jié)省了一次指針定位的時(shí)間開銷。如果是對(duì)象頻繁 GC 那么句柄方法好,如果是對(duì)象頻繁訪問則直接指針訪問好。

1.3 實(shí)戰(zhàn)

// 待填

2. 垃圾回收器與內(nèi)存分配策略

2.1 概述

程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧 3 個(gè)區(qū)域隨線程生滅(因?yàn)槭蔷€程私有),棧中的棧幀隨著方法的進(jìn)入和退出而有條不紊地執(zhí)行著出棧和入棧操作。而 Java 堆和方法區(qū)則不一樣,一個(gè)接口中的多個(gè)實(shí)現(xiàn)類需要的內(nèi)存可能不一樣,一個(gè)方法中的多個(gè)分支需要的內(nèi)存也可能不一樣,我們只有在程序處于運(yùn)行期才知道那些對(duì)象會(huì)創(chuàng)建,這部分內(nèi)存的分配和回收都是動(dòng)態(tài)的,垃圾回收期所關(guān)注的就是這部分內(nèi)存。

2.2 對(duì)象已死嗎?

在進(jìn)行內(nèi)存回收之前要做的事情就是判斷那些對(duì)象是‘死’的,哪些是‘活’的。

2.2.1 引用計(jì)數(shù)法

給對(duì)象添加一個(gè)引用計(jì)數(shù)器。但是難以解決循環(huán)引用問題。

從圖中可以看出,如果不下小心直接把 Obj1-reference 和 Obj2-reference 置 null。則在 Java 堆當(dāng)中的兩塊內(nèi)存依然保持著互相引用無法回收。

2.2.2 可達(dá)性分析法

通過一系列的 ‘GC Roots’ 的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)出發(fā)所走過的路徑稱為引用鏈。當(dāng)一個(gè)對(duì)象到 GC Roots 沒有任何引用鏈相連的時(shí)候說明對(duì)象不可用。

可作為 GC Roots 的對(duì)象:

虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象

方法區(qū)中類靜態(tài)屬性引用的對(duì)象

方法區(qū)中常量引用的對(duì)象

本地方法棧中 JNI(即一般說的 Native 方法) 引用的對(duì)象

2.2.3 再談引用

前面的兩種方式判斷存活時(shí)都與‘引用’有關(guān)。但是 JDK 1.2 之后,引用概念進(jìn)行了擴(kuò)充,下面具體介紹。

下面四種引用強(qiáng)度一次逐漸減弱

強(qiáng)引用

類似于?Object obj = new Object();?創(chuàng)建的,只要強(qiáng)引用在就不回收。

軟引用

SoftReference 類實(shí)現(xiàn)軟引用。在系統(tǒng)要發(fā)生內(nèi)存溢出異常之前,將會(huì)把這些對(duì)象列進(jìn)回收范圍之中進(jìn)行二次回收。

弱引用

WeakReference 類實(shí)現(xiàn)弱引用。對(duì)象只能生存到下一次垃圾收集之前。在垃圾收集器工作時(shí),無論內(nèi)存是否足夠都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象。

虛引用

PhantomReference 類實(shí)現(xiàn)虛引用。無法通過虛引用獲取一個(gè)對(duì)象的實(shí)例,為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知。

2.2.4 生存還是死亡

即使在可達(dá)性分析算法中不可達(dá)的對(duì)象,也并非是“facebook”的,這時(shí)候它們暫時(shí)出于“緩刑”階段,一個(gè)對(duì)象的真正死亡至少要經(jīng)歷兩次標(biāo)記過程:如果對(duì)象在進(jìn)行中可達(dá)性分析后發(fā)現(xiàn)沒有與 GC Roots 相連接的引用鏈,那他將會(huì)被第一次標(biāo)記并且進(jìn)行一次篩選,篩選條件是此對(duì)象是否有必要執(zhí)行 finalize() 方法。當(dāng)對(duì)象沒有覆蓋 finalize() 方法,或者 finalize() 方法已經(jīng)被虛擬機(jī)調(diào)用過,虛擬機(jī)將這兩種情況都視為“沒有必要執(zhí)行”。

如果這個(gè)對(duì)象被判定為有必要執(zhí)行 finalize() 方法,那么這個(gè)對(duì)象竟會(huì)放置在一個(gè)叫做 F-Queue 的隊(duì)列中,并在稍后由一個(gè)由虛擬機(jī)自動(dòng)建立的、低優(yōu)先級(jí)的 Finalizer 線程去執(zhí)行它。這里所謂的“執(zhí)行”是指虛擬機(jī)會(huì)出發(fā)這個(gè)方法,并不承諾或等待他運(yùn)行結(jié)束。finalize() 方法是對(duì)象逃脫死亡命運(yùn)的最后一次機(jī)會(huì),稍后 GC 將對(duì) F-Queue 中的對(duì)象進(jìn)行第二次小規(guī)模的標(biāo)記,如果對(duì)象要在 finalize() 中成功拯救自己 —— 只要重新與引用鏈上的任何一個(gè)對(duì)象簡(jiǎn)歷關(guān)聯(lián)即可。

finalize() 方法只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次。

2.2.5 回收方法區(qū)

在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空間,而永久代的垃圾收集效率遠(yuǎn)低于此。

永久代垃圾回收主要兩部分內(nèi)容:廢棄的常量和無用的類。

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

判斷無用的類:要以下三個(gè)條件都滿足

該類所有的實(shí)例都已經(jīng)回收,也就是 Java 堆中不存在該類的任何實(shí)例

加載該類的 ClassLoader 已經(jīng)被回收

該類對(duì)應(yīng)的 java.lang.Class 對(duì)象沒有任何地方唄引用,無法在任何地方通過反射訪問該類的方法

2.3 垃圾回收算法

僅提供思路

2.3.1 標(biāo)記 —— 清除算法

直接標(biāo)記清除就可。

兩個(gè)不足:

效率不高

空間會(huì)產(chǎn)生大量碎片

2.3.2 復(fù)制算法

把空間分成兩塊,每次只對(duì)其中一塊進(jìn)行 GC。當(dāng)這塊內(nèi)存使用完時(shí),就將還存活的對(duì)象復(fù)制到另一塊上面。

解決前一種方法的不足,但是會(huì)造成空間利用率低下。因?yàn)榇蠖鄶?shù)新生代對(duì)象都不會(huì)熬過第一次 GC。所以沒必要 1 : 1 劃分空間。可以分一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 空間和其中一塊 Survivor。當(dāng)回收時(shí),將 Eden 和 Survivor 中還存活的對(duì)象一次性復(fù)制到另一塊 Survivor 上,最后清理 Eden 和 Survivor 空間。大小比例一般是 8 : 1 : 1,每次浪費(fèi) 10% 的 Survivor 空間。但是這里有一個(gè)問題就是如果存活的大于 10% 怎么辦?這里采用一種分配擔(dān)保策略:多出來的對(duì)象直接進(jìn)入老年代。

2.3.3 標(biāo)記-整理算法

不同于針對(duì)新生代的復(fù)制算法,針對(duì)老年代的特點(diǎn),創(chuàng)建該算法。主要是把存活對(duì)象移到內(nèi)存的一端。

2.3.4 分代回收

根據(jù)存活對(duì)象劃分幾塊內(nèi)存區(qū),一般是分為新生代和老年代。然后根據(jù)各個(gè)年代的特點(diǎn)制定相應(yīng)的回收算法。

新生代

每次垃圾回收都有大量對(duì)象死去,只有少量存活,選用復(fù)制算法比較合理。

老年代

老年代中對(duì)象存活率較高、沒有額外的空間分配對(duì)它進(jìn)行擔(dān)保。所以必須使用?標(biāo)記 —— 清除或者?標(biāo)記 —— 整理?算法回收。

2.4 HotSpot 的算法實(shí)現(xiàn)

// 待填

2.5 垃圾回收器

收集算法是內(nèi)存回收的理論,而垃圾回收器是內(nèi)存回收的實(shí)踐。

說明:如果兩個(gè)收集器之間存在連線說明他們之間可以搭配使用。

2.5.1 Serial 收集器

這是一個(gè)單線程收集器。意味著它只會(huì)使用一個(gè) CPU 或一條收集線程去完成收集工作,并且在進(jìn)行垃圾回收時(shí)必須暫停其它所有的工作線程直到收集結(jié)束。

2.5.2 ParNew 收集器

可以認(rèn)為是 Serial 收集器的多線程版本。

并行:Parallel

指多條垃圾收集線程并行工作,此時(shí)用戶線程處于等待狀態(tài)

并發(fā):Concurrent

指用戶線程和垃圾回收線程同時(shí)執(zhí)行(不一定是并行,有可能是交叉執(zhí)行),用戶進(jìn)程在運(yùn)行,而垃圾回收線程在另一個(gè) CPU 上運(yùn)行。

2.5.3 Parallel Scavenge 收集器

這是一個(gè)新生代收集器,也是使用復(fù)制算法實(shí)現(xiàn),同時(shí)也是并行的多線程收集器。

CMS 等收集器的關(guān)注點(diǎn)是盡可能地縮短垃圾收集時(shí)用戶線程所停頓的時(shí)間,而 Parallel Scavenge 收集器的目的是達(dá)到一個(gè)可控制的吞吐量(Throughput = 運(yùn)行用戶代碼時(shí)間 / (運(yùn)行用戶代碼時(shí)間 + 垃圾收集時(shí)間))。

作為一個(gè)吞吐量?jī)?yōu)先的收集器,虛擬機(jī)會(huì)根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況收集性能監(jiān)控信息,動(dòng)態(tài)調(diào)整停頓時(shí)間。這就是 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)記 —— 整理

運(yùn)作步驟:

  1. 初始標(biāo)記(CMS initial mark):標(biāo)記 GC Roots 能直接關(guān)聯(lián)到的對(duì)象

  2. 并發(fā)標(biāo)記(CMS concurrent mark):進(jìn)行 GC Roots Tracing

  3. 重新標(biāo)記(CMS remark):修正并發(fā)標(biāo)記期間的變動(dòng)部分

  4. 并發(fā)清除(CMS concurrent sweep)

缺點(diǎn):對(duì) CPU 資源敏感、無法收集浮動(dòng)垃圾、標(biāo)記 —— 清除?算法帶來的空間碎片

2.5.7 G1 收集器

面向服務(wù)端的垃圾回收器。

優(yōu)點(diǎn):并行與并發(fā)、分代收集、空間整合、可預(yù)測(cè)停頓。

運(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 對(duì)象優(yōu)先在 Eden 分配

    對(duì)象主要分配在新生代的 Eden 區(qū)上,如果啟動(dòng)了本地線程分配緩沖區(qū),將線程優(yōu)先在 (TLAB) 上分配。少數(shù)情況會(huì)直接分配在老年代中。

    一般來說 Java 堆的內(nèi)存模型如下圖所示:

    新生代 GC (Minor GC)

    發(fā)生在新生代的垃圾回收動(dòng)作,頻繁,速度快。

    老年代 GC (Major GC / Full GC)

    發(fā)生在老年代的垃圾回收動(dòng)作,出現(xiàn)了 Major GC 經(jīng)常會(huì)伴隨至少一次 Minor GC(非絕對(duì))。Major GC 的速度一般會(huì)比 Minor GC 慢十倍以上。

    2.6.2 大對(duì)象直接進(jìn)入老年代

    2.6.3 長(zhǎng)期存活的對(duì)象將進(jìn)入老年代

    2.6.4 動(dòng)態(tài)對(duì)象年齡判定

    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)存之間的交互

    3.1.2 對(duì)于 volatile 型變量的特殊規(guī)則

    關(guān)鍵字 volatile 是 Java 虛擬機(jī)提供的最輕量級(jí)的同步機(jī)制。

    一個(gè)變量被定義為 volatile 的特性:

  • 保證此變量對(duì)所有線程的可見性。但是操作并非原子操作,并發(fā)情況下不安全。

  • 如果不符合?運(yùn)算結(jié)果并不依賴變量當(dāng)前值,或者能夠確保只有單一的線程修改變量的值?和?變量不需要與其他的狀態(tài)變量共同參與不變約束?就要通過加鎖(使用 synchronize 或 java.util.concurrent 中的原子類)來保證原子性。

  • 禁止指令重排序優(yōu)化。

  • 通過插入內(nèi)存屏障保證一致性。

    3.1.3 對(duì)于 long 和 double 型變量的特殊規(guī)則

    Java 要求對(duì)于主內(nèi)存和工作內(nèi)存之間的八個(gè)操作都是原子性的,但是對(duì)于 64 位的數(shù)據(jù)類型,有一條寬松的規(guī)定:允許虛擬機(jī)將沒有被 volatile 修飾的 64 位數(shù)據(jù)的讀寫操作劃分為兩次 32 位的操作來進(jìn)行,即允許虛擬機(jī)實(shí)現(xiàn)選擇可以不保證 64 位數(shù)據(jù)類型的 load、store、read 和 write 這 4 個(gè)操作的原子性。這就是 long 和 double 的非原子性協(xié)定。

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

    回顧下并發(fā)下應(yīng)該注意操作的那些特性是什么,同時(shí)加深理解。

  • 原子性(Atomicity)

  • 由 Java 內(nèi)存模型來直接保證的原子性變量操作包括 read、load、assign、use、store 和 write。大致可以認(rèn)為基本數(shù)據(jù)類型的操作是原子性的。同時(shí) lock 和 unlock 可以保證更大范圍操作的原子性。而 synchronize 同步塊操作的原子性是用更高層次的字節(jié)碼指令 monitorenter 和 monitorexit 來隱式操作的。

  • 可見性(Visibility)

  • 是指當(dāng)一個(gè)線程修改了共享變量的值,其他線程也能夠立即得知這個(gè)通知。主要操作細(xì)節(jié)就是修改值后將值同步至主內(nèi)存(volatile 值使用前都會(huì)從主內(nèi)存刷新),除了 volatile 還有 synchronize 和 final 可以保證可見性。同步塊的可見性是由“對(duì)一個(gè)變量執(zhí)行 unlock 操作之前,必須先把此變量同步會(huì)主內(nèi)存中( store、write 操作)”這條規(guī)則獲得。而 final 可見性是指:被 final 修飾的字段在構(gòu)造器中一旦完成,并且構(gòu)造器沒有把 “this” 的引用傳遞出去( this 引用逃逸是一件很危險(xiǎn)的事情,其他線程有可能通過這個(gè)引用訪問到“初始化了一半”的對(duì)象),那在其他線程中就能看見 final 字段的值。

  • 有序性(Ordering)

  • 如果在被線程內(nèi)觀察,所有操作都是有序的;如果在一個(gè)線程中觀察另一個(gè)線程,所有操作都是無序的。前半句指“線程內(nèi)表現(xiàn)為串行的語義”,后半句是指“指令重排”現(xiàn)象和“工作內(nèi)存與主內(nèi)存同步延遲”現(xiàn)象。Java 語言通過 volatile 和 synchronize 兩個(gè)關(guān)鍵字來保證線程之間操作的有序性。volatile 自身就禁止指令重排,而 synchronize 則是由“一個(gè)變量在同一時(shí)刻指允許一條線程對(duì)其進(jìn)行 lock 操作”這條規(guī)則獲得,這條規(guī)則決定了持有同一個(gè)鎖的兩個(gè)同步塊只能串行的進(jìn)入。

    3.1.5 先行發(fā)生原則

    也就是 happens-before 原則。這個(gè)原則是判斷數(shù)據(jù)是否存在競(jìng)爭(zhēng)、線程是否安全的主要依據(jù)。先行發(fā)生是 Java 內(nèi)存模型中定義的兩項(xiàng)操作之間的偏序關(guān)系。

    天然的先行發(fā)生關(guān)系

    3.2 Java 與線程

    3.2.1 線程的實(shí)現(xiàn)

    使用內(nèi)核線程實(shí)現(xiàn)

    直接由操作系統(tǒng)內(nèi)核支持的線程,這種線程由內(nèi)核完成切換。程序一般不會(huì)直接去使用內(nèi)核線程,而是去使用內(nèi)核線程的一種高級(jí)接口 —— 輕量級(jí)進(jìn)程(LWP),輕量級(jí)進(jìn)程就是我們通常意義上所講的線程,每個(gè)輕量級(jí)進(jìn)程都有一個(gè)內(nèi)核級(jí)線程支持。

    使用用戶線程實(shí)現(xiàn)

    廣義上來說,只要不是內(nèi)核線程就可以認(rèn)為是用戶線程,因此可以認(rèn)為輕量級(jí)進(jìn)程也屬于用戶線程。狹義上說是完全建立在用戶空間的線程庫上的并且內(nèi)核系統(tǒng)不可感知的。

    使用用戶線程夾加輕量級(jí)進(jìn)程混合實(shí)現(xiàn)

    Java 線程實(shí)現(xiàn)

    平臺(tái)不同實(shí)現(xiàn)方式不同,可以認(rèn)為是一條 Java 線程映射到一條輕量級(jí)進(jìn)程。

    3.2.2 Java 線程調(diào)度

    協(xié)同式線程調(diào)度

    線程執(zhí)行時(shí)間由線程自身控制,實(shí)現(xiàn)簡(jiǎn)單,切換線程自己可知,所以基本沒有線程同步問題。壞處是執(zhí)行時(shí)間不可控,容易阻塞。

    搶占式線程調(diào)度

    每個(gè)線程由系統(tǒng)來分配執(zhí)行時(shí)間。

    3.2.3 狀態(tài)轉(zhuǎn)換

    五種狀態(tài):

  • 新建(new)

  • 創(chuàng)建后尚未啟動(dòng)的線程。

  • 運(yùn)行(Runable)

  • Runable 包括了操作系統(tǒng)線程狀態(tài)中的 Running 和 Ready,也就是出于此狀態(tài)的線程有可能正在執(zhí)行,也有可能正在等待 CPU 為他分配時(shí)間。

  • 無限期等待(Waiting)

  • 出于這種狀態(tài)的線程不會(huì)被 CPU 分配時(shí)間,它們要等其他線程顯示的喚醒。

    以下方法會(huì)然線程進(jìn)入無限期等待狀態(tài):

    1.沒有設(shè)置 Timeout 參數(shù)的 Object.wait() 方法。

    2.沒有設(shè)置 Timeout 參數(shù)的 Thread.join() 方法。

    3.LookSupport.park() 方法。

  • 限期等待(Timed Waiting)

  • 處于這種狀態(tài)的線程也不會(huì)分配時(shí)間,不過無需等待配其他線程顯示地喚醒,在一定時(shí)間后他們會(huì)由系統(tǒng)自動(dòng)喚醒。

    以下方法會(huì)讓線程進(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() 方法。

  • 阻塞(Blocked)

  • 線程被阻塞了,“阻塞狀態(tài)”和“等待狀態(tài)”的區(qū)別是:“阻塞狀態(tài)”在等待著獲取一個(gè)排他鎖,這個(gè)時(shí)間將在另外一個(gè)線程放棄這個(gè)鎖的時(shí)候發(fā)生;而“等待狀態(tài)”則是在等待一段時(shí)間,或者喚醒動(dòng)作的發(fā)生。在程序等待進(jìn)入同步區(qū)域的時(shí)候,線程將進(jìn)入這種狀態(tài)。

  • 結(jié)束(Terminated)

  • 已終止線程的線程狀態(tài)。

    4. 線程安全與鎖優(yōu)化

    // 待填

    5. 類文件結(jié)構(gòu)

    // 待填

    有點(diǎn)懶了。。。先貼幾個(gè)網(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)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、裝換解析和初始化,最終形成可以被虛擬機(jī)直接使用的 Java 類型。

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

    6.1 類加載時(shí)機(jī)

    類的生命周期( 7 個(gè)階段)

    其中加載、驗(yàn)證、準(zhǔn)備、初始化和卸載這五個(gè)階段的順序是確定的。解析階段可以在初始化之后再開始(運(yùn)行時(shí)綁定或動(dòng)態(tài)綁定或晚期綁定)。

    以下五種情況必須對(duì)類進(jìn)行初始化(而加載、驗(yàn)證、準(zhǔn)備自然需要在此之前完成):

  • 遇到 new、getstatic、putstatic 或 invokestatic 這 4 條字節(jié)碼指令時(shí)沒初始化觸發(fā)初始化。使用場(chǎng)景:使用 new 關(guān)鍵字實(shí)例化對(duì)象、讀取一個(gè)類的靜態(tài)字段(被 final 修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)、調(diào)用一個(gè)類的靜態(tài)方法。

  • 使用 java.lang.reflect 包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候。

  • 當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行初始化,則需先觸發(fā)其父類的初始化。

  • 當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需指定一個(gè)要加載的主類(包含 main() 方法的那個(gè)類),虛擬機(jī)會(huì)先初始化這個(gè)主類。

  • 當(dāng)使用 JDK 1.7 的動(dòng)態(tài)語言支持時(shí),如果一個(gè) java.lang.invoke.MethodHandle 實(shí)例最后的解析結(jié)果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且這個(gè)方法句柄所對(duì)應(yīng)的類沒有進(jìn)行過初始化,則需先觸發(fā)其初始化。

  • 前面的五種方式是對(duì)一個(gè)類的主動(dòng)引用,除此之外,所有引用類的方法都不會(huì)觸發(fā)初始化,佳作被動(dòng)引用。舉幾個(gè)例子~

    6.2 類的加載過程

    6.2.1 加載

  • 通過一個(gè)類的全限定名來獲取定義次類的二進(jìn)制流(ZIP 包、網(wǎng)絡(luò)、運(yùn)算生成、JSP 生成、數(shù)據(jù)庫讀取)。

  • 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。

  • 在內(nèi)存中生成一個(gè)代表這個(gè)類的 java.lang.Class 對(duì)象,作為方法去這個(gè)類的各種數(shù)據(jù)的訪問入口。

  • 數(shù)組類的特殊性:數(shù)組類本身不通過類加載器創(chuàng)建,它是由 Java 虛擬機(jī)直接創(chuàng)建的。但數(shù)組類與類加載器仍然有很密切的關(guān)系,因?yàn)閿?shù)組類的元素類型最終是要靠類加載器去創(chuàng)建的,數(shù)組創(chuàng)建過程如下:

  • 如果數(shù)組的組件類型是引用類型,那就遞歸采用類加載加載。

  • 如果數(shù)組的組件類型不是引用類型,Java 虛擬機(jī)會(huì)把數(shù)組標(biāo)記為引導(dǎo)類加載器關(guān)聯(lián)。

  • 數(shù)組類的可見性與他的組件類型的可見性一致,如果組件類型不是引用類型,那數(shù)組類的可見性將默認(rèn)為 public。

  • 內(nèi)存中實(shí)例的 java.lang.Class 對(duì)象存在方法區(qū)中。作為程序訪問方法區(qū)中這些類型數(shù)據(jù)的外部接口。

    加載階段與連接階段的部分內(nèi)容是交叉進(jìn)行的,但是開始時(shí)間保持先后順序。

    6.2.2 驗(yàn)證

    是連接的第一步,確保 Class 文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)要求。

    文件格式驗(yàn)證

  • 是否以魔數(shù) 0xCAFEBABE 開頭

  • 主、次版本號(hào)是否在當(dāng)前虛擬機(jī)處理范圍之內(nèi)

  • 常量池的常量是否有不被支持常量的類型(檢查常量 tag 標(biāo)志)

  • 指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量

  • CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 編碼的數(shù)據(jù)

  • Class 文件中各個(gè)部分集文件本身是否有被刪除的附加的其他信息

  • ……

  • 只有通過這個(gè)階段的驗(yàn)證后,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)進(jìn)行存儲(chǔ),所以后面 3 個(gè)驗(yàn)證階段全部是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)進(jìn)行的,不再直接操作字節(jié)流。

    元數(shù)據(jù)驗(yàn)證

  • 這個(gè)類是否有父類(除 java.lang.Object 之外)

  • 這個(gè)類的父類是否繼承了不允許被繼承的類(final 修飾的類)

  • 如果這個(gè)類不是抽象類,是否實(shí)現(xiàn)了其父類或接口之中要求實(shí)現(xiàn)的所有方法

  • 類中的字段、方法是否與父類產(chǎn)生矛盾(覆蓋父類 final 字段、出現(xiàn)不符合規(guī)范的重載)

  • 這一階段主要是對(duì)類的元數(shù)據(jù)信息進(jìn)行語義校驗(yàn),保證不存在不符合 Java 語言規(guī)范的元數(shù)據(jù)信息。

    字節(jié)碼驗(yàn)證

  • 保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都鞥配合工作(不會(huì)出現(xiàn)按照 long 類型讀一個(gè) int 型數(shù)據(jù))

  • 保證跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上

  • 保證方法體中的類型轉(zhuǎn)換是有效的(子類對(duì)象賦值給父類數(shù)據(jù)類型是安全的,反過來不合法的)

  • ……

  • 這是整個(gè)驗(yàn)證過程中最復(fù)雜的一個(gè)階段,主要目的是通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的、符合邏輯的。這個(gè)階段對(duì)類的方法體進(jìn)行校驗(yàn)分析,保證校驗(yàn)類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的事件。

    符號(hào)引用驗(yàn)證

  • 符號(hào)引用中通過字符創(chuàng)描述的全限定名是否能找到對(duì)應(yīng)的類

  • 在指定類中是否存在符方法的字段描述符以及簡(jiǎn)單名稱所描述的方法和字段

  • 符號(hào)引用中的類、字段、方法的訪問性(private、protected、public、default)是否可被當(dāng)前類訪問

  • ……

  • 最后一個(gè)階段的校驗(yàn)發(fā)生在迅疾將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候,這個(gè)轉(zhuǎn)化動(dòng)作將在連接的第三階段——解析階段中發(fā)生。符號(hào)引用驗(yàn)證可以看做是對(duì)類自身以外(常量池中的各種符號(hào)引用)的信息進(jìn)行匹配性校驗(yàn),還有以上提及的內(nèi)容。

    符號(hào)引用的目的是確保解析動(dòng)作能正常執(zhí)行,如果無法通過符號(hào)引用驗(yàn)證將拋出一個(gè) java.lang.IncompatibleClass.ChangeError 異常的子類。如 java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError 等。

    6.2.3 準(zhǔn)備

    這個(gè)階段正式為類分配內(nèi)存并設(shè)置類變量初始值,內(nèi)存在方法去中分配(含 static 修飾的變量不含實(shí)例變量)。

    public static int value = 1127;

    這句代碼在初始值設(shè)置之后為 0,因?yàn)檫@時(shí)候尚未開始執(zhí)行任何 Java 方法。而把 value 賦值為 1127 的 putstatic 指令是程序被編譯后,存放于 clinit() 方法中,所以初始化階段才會(huì)對(duì) value 進(jìn)行賦值。

    基本數(shù)據(jù)類型的零值

    特殊情況:如果類字段的字段屬性表中存在 ConstantValue 屬性,在準(zhǔn)備階段虛擬機(jī)就會(huì)根據(jù) ConstantValue 的設(shè)置將 value 賦值為 1127。

    6.2.4 解析

    這個(gè)階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過程。

  • 符號(hào)引用

    符號(hào)引用以一組符號(hào)來描述所引用的目標(biāo),符號(hào)可以使任何形式的字面量。

  • 直接引用

    直接引用可以使直接指向目標(biāo)的指針、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。直接引用和迅疾的內(nèi)存布局實(shí)現(xiàn)有關(guān)

  • 解析動(dòng)作主要針對(duì)類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點(diǎn)限定符 7 類符號(hào)引用進(jìn)行,分別對(duì)應(yīng)于常量池的 7 中常量類型。

    6.2.5 初始化

    前面過程都是以虛擬機(jī)主導(dǎo),而初始化階段開始執(zhí)行類中的 Java 代碼。

    6.3 類加載器

    通過一個(gè)類的全限定名來獲取描述此類的二進(jìn)制字節(jié)流。

    6.3.1 雙親委派模型

    從 Java 虛擬機(jī)角度講,只存在兩種類加載器:一種是啟動(dòng)類加載器(C++ 實(shí)現(xiàn),是虛擬機(jī)的一部分);另一種是其他所有類的加載器(Java 實(shí)現(xiàn),獨(dú)立于虛擬機(jī)外部且全繼承自 java.lang.ClassLoader)

  • 啟動(dòng)類加載器

    加載 lib 下或被 -Xbootclasspath 路徑下的類

  • 擴(kuò)展類加載器

    加載 lib/ext 或者被 java.ext.dirs 系統(tǒng)變量所指定的路徑下的類

  • 引用程序類加載器

    ClassLoader負(fù)責(zé),加載用戶路徑上所指定的類庫。

  • 除頂層啟動(dòng)類加載器之外,其他都有自己的父類加載器。

    工作過程:如果一個(gè)類加載器收到一個(gè)類加載的請(qǐng)求,它首先不會(huì)自己加載,而是把這個(gè)請(qǐng)求委派給父類加載器。只有父類無法完成時(shí)子類才會(huì)嘗試加載。

    6.3.2 破壞雙親委派模型

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

    學(xué)習(xí)Java的同學(xué)注意了!!!
    學(xué)習(xí)過程中遇到什么問題或者想獲取學(xué)習(xí)資源的話,歡迎加入Java學(xué)習(xí)交流群346942462,我們一起學(xué)Java!

    最后編輯于
    ?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
    平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

    推薦閱讀更多精彩內(nèi)容

    • 《深入理解Java虛擬機(jī)》筆記_第一遍 先取看完這本書(JVM)后必須掌握的部分。 第一部分 走近 Java 從傳...
      xiaogmail閱讀 5,152評(píng)論 1 34
    • 一、運(yùn)行時(shí)數(shù)據(jù)區(qū)域 Java虛擬機(jī)管理的內(nèi)存包括幾個(gè)運(yùn)行時(shí)數(shù)據(jù)內(nèi)存:方法區(qū)、虛擬機(jī)棧、本地方法棧、堆、程序計(jì)數(shù)器,...
      加油小杜閱讀 1,536評(píng)論 1 15
    • 從三月份找實(shí)習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
      時(shí)芥藍(lán)閱讀 42,356評(píng)論 11 349
    • 這幾天一直在建設(shè)家里的辦公環(huán)境,先是把家里來個(gè)徹徹底底的,360度無死角的大掃除,包括客廳,臥室,廚房,還有廁所;...
      我是三百萬閱讀 670評(píng)論 0 0
    • 一場(chǎng)夢(mèng),醒來如墜冰窟。終于明白我們?yōu)槭裁床豢赡堋S行┤松鷣肀拔ⅲ缥摇<词乖趬?mèng)里,那卑微也深入骨髓,難開出花來。時(shí)...
      今我來昔閱讀 301評(píng)論 0 0