深入理解java虛擬機 精華總結(jié)(面試)

一楞陷、運行時數(shù)據(jù)區(qū)域

Java虛擬機管理的內(nèi)存包括幾個運行時數(shù)據(jù)內(nèi)存:方法區(qū)怔鳖、虛擬機棧、本地方法棧固蛾、堆结执、程序計數(shù)器,其中方法區(qū)和堆是由線程共享的數(shù)據(jù)區(qū)艾凯,其他幾個是線程隔離的數(shù)據(jù)區(qū)

1.1?程序計數(shù)器

程序計數(shù)器是一塊較小的內(nèi)存献幔,他可以看做是當前線程所執(zhí)行的行號指示器。字節(jié)碼解釋器工作的時候就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼的指令趾诗,分支蜡感、循環(huán)、跳轉(zhuǎn)、異常處理铸敏、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計數(shù)器來完成缚忧。如果線程正在執(zhí)行的是一個Java方法,這個計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址杈笔;如果正在執(zhí)行的是Native方法闪水,這個計數(shù)器則為空。此內(nèi)存區(qū)域是唯一一個在Java虛擬機規(guī)范中沒有規(guī)定任何OutOfMemotyError情況的區(qū)域

1.2?Java虛擬機棧

虛擬機棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀用于儲存局部變量表蒙具、操作數(shù)棧球榆、動態(tài)鏈接、方法出口等信息禁筏。每個方法從調(diào)用直至完成的過程持钉,就對應(yīng)著一個棧幀在虛擬機棧中入棧到出棧的過程。

棧內(nèi)存就是虛擬機棧篱昔,或者說是虛擬機棧中局部變量表的部分

局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean每强、byte、char州刽、short空执、int、float穗椅、long辨绊、double)、對象引用(refrence)類型和returnAddress類型(指向了一條字節(jié)碼指令的地址)

其中64位長度的long和double類型的數(shù)據(jù)會占用兩個局部變量空間匹表,其余的數(shù)據(jù)類型只占用1個门坷。

Java虛擬機規(guī)范對這個區(qū)域規(guī)定了兩種異常狀況:如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常袍镀。如果虛擬機擴展時無法申請到足夠的內(nèi)存默蚌,就會拋出OutOfMemoryError異常

1.3?本地方法棧

本地方法棧和虛擬機棧發(fā)揮的作用是非常類似的,他們的區(qū)別是虛擬機棧為虛擬機執(zhí)行Java方法(也就是字節(jié)碼)服務(wù)苇羡,而本地方法棧則為虛擬機使用到的Native方法服務(wù)

本地方法棧區(qū)域也會拋出StackOverflowError和OutOfMemoryErroy異常

1.4?Java堆

堆是Java虛擬機所管理的內(nèi)存中最大的一塊敏簿。Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機啟動的時候創(chuàng)建宣虾,此內(nèi)存區(qū)域的唯一目的是存放對象實例惯裕,幾乎所有的對象實例都在這里分配內(nèi)存。所有的對象實例和數(shù)組都在堆上分配

Java堆是垃圾收集器管理的主要區(qū)域绣硝。Java堆細分為新生代和老年代

不管怎樣蜻势,劃分的目的都是為了更好的回收內(nèi)存,或者更快地分配內(nèi)存

Java堆可以處于物理上不連續(xù)的內(nèi)存空間中鹉胖,只要邏輯上是連續(xù)的即可握玛。如果在堆中沒有完成實例分配够傍,并且堆也無法再擴展時將會拋出OutOfMemoryError異常

1.5?方法區(qū)

方法區(qū)它用于儲存已被虛擬機加載的類信息、常量挠铲、靜態(tài)變量冕屯、即時編譯器編譯后的代碼等數(shù)據(jù)

除了Java堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴展外,還可以選擇不實現(xiàn)垃圾收集拂苹。這個區(qū)域的內(nèi)存回收目標主要是針對常量池的回收和對類型的卸載

當方法區(qū)無法滿足內(nèi)存分配需求時安聘,將拋出OutOfMemoryErroy異常

1.6?運行時常量池

它是方法區(qū)的一部分。Class文件中除了有關(guān)的版本瓢棒、字段浴韭、方法、接口等描述信息外脯宿、還有一項信息是常量池念颈,用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中存放

Java語言并不要求常量一定只有編譯期才能產(chǎn)生连霉,也就是可能將新的常量放入池中榴芳,這種特性被開發(fā)人員利用得比較多的便是String類的intern()方法

當常量池無法再申請到內(nèi)存時會拋出OutOfMemoryError異常

二、hotspot虛擬機對象

2.1對象的創(chuàng)建

1.檢查?

虛擬機遇到一條new指令時跺撼,首先將去檢查這個指令的參數(shù)是否能在常量池中定位到一個類的符號引用窟感,并且檢查這個符號引用代表的類是否已經(jīng)被加載、解析和初始化過财边。如果沒有,那必須先執(zhí)行相應(yīng)的類加載過程

2.分配內(nèi)存?

接下來將為新生對象分配內(nèi)存点骑,為對象分配內(nèi)存空間的任務(wù)等同于把一塊確定的大小的內(nèi)存從Java堆中劃分出來酣难。

假設(shè)Java堆中內(nèi)存是絕對規(guī)整的,所有用過的內(nèi)存放在一遍黑滴,空閑的內(nèi)存放在另一邊憨募,中間放著一個指針作為分界點的指示器,那所分配內(nèi)存就僅僅是把那個指針指向空閑空間那邊挪動一段與對象大小相等的距離袁辈,這個分配方式叫做“指針碰撞”

如果Java堆中的內(nèi)存并不是規(guī)整的菜谣,已使用的內(nèi)存和空閑的內(nèi)存相互交錯,那就沒辦法簡單地進行指針碰撞了晚缩,虛擬機就必須維護一個列表尾膊,記錄上哪些內(nèi)存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例荞彼,并更新列表上的記錄冈敛,這種分配方式成為“空閑列表

選擇那種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定鸣皂。

使用Serial抓谴、ParNew等帶有Compact過程的收集器時暮蹂,系統(tǒng)采用的分配算法是指針碰撞,而使用CMS這種基于Mark-Sweep算法的收集器癌压,通常采用空閑列表仰泻。

3.賦零值

內(nèi)存分配完成后,虛擬機需要將分配到的內(nèi)存空間都初始化為零值滩届。

4.對象類信息關(guān)聯(lián)

接下來集侯,虛擬機要對對象進行必要的設(shè)置,例如這個對象是哪個類的實例丐吓、如何才能找到類的元數(shù)據(jù)信息浅悉、對象的哈希嗎、對象的GC分代年齡等信息券犁。這些信息存放在對象的對象頭中术健。

5. <init>

執(zhí)行new指令之后會接著執(zhí)行<init>方法,把對象按照程序員的意愿進行初始化粘衬,這樣一個對象才算產(chǎn)生出來

2.2對象的內(nèi)存布局

在HotSpot虛擬機中荞估,對象在內(nèi)存中儲存的布局可以分為3塊區(qū)域:對象頭、實例數(shù)據(jù)和對齊填充

對象頭包括兩部分:

a)?儲存對象自身的運行時數(shù)據(jù)稚新,如哈希碼勘伺、GC分帶年齡、鎖狀態(tài)標志褂删、線程持有的鎖飞醉、偏向線程ID、偏向時間戳

b)?另一部分是指類型指針屯阀,即對象指向它的類元數(shù)據(jù)的指針缅帘,虛擬機通過這個指針來確定這個對象是那個類的實例

2.3?對象的訪問定位

1.使用句柄訪問

Java堆中將會劃分出一塊內(nèi)存來作為句柄池,reference中存儲的就是對象的句柄地址难衰,而句柄中包含了對象實例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址

優(yōu)勢:reference中存儲的是穩(wěn)點的句柄地址钦无,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數(shù)據(jù)指針,而reference本身不需要修改

2.使用直接指針訪問

Java堆對象的布局就必須考慮如何訪問類型數(shù)據(jù)的相關(guān)信息盖袭,而refreence中存儲的直接就是對象的地址

優(yōu)勢:速度更快失暂,節(jié)省了一次指針定位的時間開銷,由于對象的訪問在Java中非常頻繁鳄虱,因此這類開銷積少成多后也是一項非车苋可觀的執(zhí)行成本

HotSpot使用第2種方法

三、OutOfMemoryError異常

3.1?Java堆溢出

Java堆用于存儲對象實例拙已,只要不斷的創(chuàng)建對象宣肚,并且保證GCRoots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那么在數(shù)量到達最大堆的容量限制后就會產(chǎn)生內(nèi)存溢出異常

如果是內(nèi)存泄漏悠栓,可進一步通過工具查看泄漏對象到GC?Roots的引用鏈霉涨。于是就能找到泄露對象是通過怎樣的路徑與GC?Roots相關(guān)聯(lián)并導致垃圾收集器無法自動回收它們的按价。掌握了泄漏對象的類型信息及GC?Roots引用鏈的信息,就可以比較準確地定位出泄漏代碼的位置

如果不存在泄露笙瑟,換句話說楼镐,就是內(nèi)存中的對象確實都還必須存活著,那就應(yīng)當檢查虛擬機的堆參數(shù)(-Xmx與-Xms)往枷,與機器物理內(nèi)存對比看是否還可以調(diào)大框产,從代碼上檢查是否存在某些對象生命周期過長、持有狀態(tài)時間過長的情況错洁,嘗試減少程序運行期的內(nèi)存消耗

3.2?虛擬機棧和本地方法棧溢出

對于HotSpot來說秉宿,雖然-Xoss參數(shù)(設(shè)置本地方法棧大小)存在屯碴,但實際上是無效的描睦,棧容量只由-Xss參數(shù)設(shè)定。關(guān)于虛擬機棧和本地方法棧导而,在Java虛擬機規(guī)范中描述了兩種異常:

如果線程請求的棧深度大于虛擬機所允許的最大深度忱叭,將拋出StackOverflowError

如果虛擬機在擴展棧時無法申請到足夠的內(nèi)存空間,則拋出OutOfMemoryError異常

在單線程下今艺,無論由于棧幀太大還是虛擬機棧容量太小韵丑,當內(nèi)存無法分配的時候,虛擬機拋出的都是StackOverflowError異常

如果是多線程導致的內(nèi)存溢出虚缎,與椖斐梗空間是否足夠大并不存在任何聯(lián)系,這個時候每個線程的棧分配的內(nèi)存越大实牡,反而越容易產(chǎn)生內(nèi)存溢出異常陌僵。解決的時候是在不能減少線程數(shù)或更換64為的虛擬機的情況下,就只能通過減少最大堆和減少棧容量來換取更多的線程

3.3?方法區(qū)和運行時常量池溢出

String.intern()是一個Native方法铲掐,它的作用是:如果字符串常量池中已經(jīng)包含一個等于此String對象的字符串拾弃,則返回代表池中這個字符串的String對象值桩;否則摆霉,將此String對象包含的字符串添加到常量池中,并且返回此String對象的引用

由于常量池分配在永久代中奔坟,可以通過-XX:PermSize和-XX:MaxPermSize限制方法區(qū)大小携栋,從而間接限制其中常量池的容量。

Intern():

JDK1.6?intern方法會把首次遇到的字符串實例復(fù)制到永久代咳秉,返回的也是永久代中這個字符串實例的引用婉支,而由StringBuilder創(chuàng)建的字符串實例在Java堆上,所以必然不是一個引用

JDK1.7?intern()方法的實現(xiàn)不會再復(fù)制實例澜建,只是在常量池中記錄首次出現(xiàn)的實例引用向挖,因此intern()返回的引用和由StringBuilder創(chuàng)建的那個字符串實例是同一個

四蝌以、垃圾收集

程序計數(shù)器、虛擬機棧何之、本地方法棧3個區(qū)域隨線程而生跟畅,隨線程而滅,在這幾個區(qū)域內(nèi)就不需要過多考慮回收的問題溶推,因為方法結(jié)束或者線程結(jié)束時徊件,內(nèi)存自然就跟隨著回收了

4.1.判斷對象存活

4.1.1?引用計數(shù)器法

給對象添加一個引用計數(shù)器,每當由一個地方引用它時蒜危,計數(shù)器值就加1虱痕;當引用失效時,計數(shù)器值就減1辐赞;任何時刻計數(shù)器為0的對象就是不可能再被使用的部翘。無法解決循環(huán)引用的問題。

4.1.2?可達性分析算法

通過一系列的成為“GC?Roots”的對象作為起始點占拍,從這些節(jié)點開始向下搜索略就,搜索所走過的路徑成為引用鏈,當一個對象到GC?ROOTS沒有任何引用鏈相連時晃酒,則證明此對象時不可用的

Java語言中GC?Roots的對象包括下面幾種:

1.虛擬機棧(棧幀中的本地變量表)中引用的對象

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

3.方法區(qū)中常量引用的對象

4.本地方法棧JNI(Native方法)引用的對象

4.2.引用

強引用就是在程序代碼之中普遍存在的表牢,類似Object?obj?=?new?Object()?這類的引用,只要強引用還存在贝次,垃圾收集器永遠不會回收掉被引用的對象

軟引用用來描述一些還有用但并非必須的元素崔兴。對于它在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會把這些對象列進回收范圍之中進行第二次回收蛔翅,如果這次回收還沒有足夠的內(nèi)存才會拋出內(nèi)存溢出異常

弱引用用來描述非必須對象的敲茄,但是它的強度比軟引用更弱一些,被引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前山析,當垃圾收集器工作時堰燎,無論當前內(nèi)存是否足夠都會回收掉只被弱引用關(guān)聯(lián)的對象

虛引用的唯一目的就是能在這個對象被收集器回收時收到一個系統(tǒng)通知

4.3.Finalize方法

任何一個對象的finalize()方法都只會被系統(tǒng)自動調(diào)用一次,如果對象面臨下一次回收笋轨,它的finalize()方法不會被再次執(zhí)行秆剪,因此第二段代碼的自救行動失敗了

4.3.1回收方法區(qū)

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

廢棄常量:假如一個字符串a(chǎn)bc已經(jīng)進入了常量池中,如果當前系統(tǒng)沒有任何一個String對象abc爵政,也就是沒有任何Stirng對象引用常量池的abc常量仅讽,也沒有其他地方引用的這個字面量,這個時候發(fā)生內(nèi)存回收這個常量就會被清理出常量池

無用的類:

1.該類所有的實例都已經(jīng)被回收钾挟,就是Java堆中不存在該類的任何實例

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

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

4.垃圾收集算法

4.4.1?標記—清除算法

算法分為標記和清除兩個階段:首先標記出所有需要回收的對象,在標記完成后統(tǒng)一回收所有被標記的對象掺出、

不足:一個是效率問題徽千,標記和清除兩個過程的效率都不高苫费;另一個是空間問題,標記清楚之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片双抽,空間碎片太多可能會導致以后再程序運行過程中需要分配較大的對象時黍衙,無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作

4.4.2?復(fù)制算法

他將可用內(nèi)存按照容量劃分為大小相等的兩塊,每次只使用其中的一塊荠诬。當這塊的內(nèi)存用完了琅翻,就將還存活著的對象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉柑贞。這樣使得每次都是對整個半?yún)^(qū)進行內(nèi)存回收方椎,內(nèi)存分配時也就不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動堆頂指針钧嘶,按順序分配內(nèi)存即可

不足:將內(nèi)存縮小為了原來的一半

實際中我們并不需要按照1:1比例來劃分內(nèi)存空間棠众,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor

當另一個Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時有决,這些對象將直接通過分配擔保機制進入老年代

4.4.3標記整理算法

讓所有存活的對象都向一端移動闸拿,然后直接清理掉端邊界以外的內(nèi)存

4.4.4分代收集算法

只是根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊。一般是把java堆分為新生代和老年代书幕,這樣就可以根據(jù)各個年代的特點采用最適當?shù)氖占惴ㄐ禄纭T谛律幸任椋看卫占瘯r都發(fā)現(xiàn)有大批對象死去挣跋,只有少量存活,那就選用復(fù)制算法顾稀,只需要付出少量存活對象的復(fù)制成本就可以完成收集苟呐。而老年代中因為對象存活率高痒芝、沒有額外空間對它進行分配擔保,就必須使用標記清理或者標記整理算法來進行回收

5.垃圾收集器


a)Serial收集器:

這個收集器是一個單線程的收集器牵素,但它的單線程的意義不僅僅說明它會只使用一個CPU或一條收集線程去完成垃圾收集工作严衬,更重要的是它在進行垃圾收集時,必須暫停其他所有的工作線程笆呆,直到它收集結(jié)束

b)ParNew收集器:

Serial收集器的多線程版本请琳,除了使用了多線程進行收集之外,其余行為和Serial收集器一樣

并行:指多條垃圾收集線程并行工作腰奋,但此時用戶線程仍然處于等待狀態(tài)

并發(fā):指用戶線程與垃圾收集線程同時執(zhí)行(不一定是并行的单起,可能會交替執(zhí)行)抱怔,用戶程序在繼續(xù)執(zhí)行劣坊,而垃圾收集程序運行于另一個CPU上

c)Parallel?Scavenge?

收集器是一個新生代收集器,它是使用復(fù)制算法的收集器屈留,又是并行的多線程收集器局冰。追求高吞吐量测蘑。

吞吐量:就是CPU用于運行用戶代碼的時間與CPU總消耗時間的比值。即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)

d)Serial?Old收集器:

是Serial收集器的老年代版本,是一個單線程收集器康二,使用標記整理算法

e)Parallel?Old收集器:

Parallel?Old是Paraller?Scavenge收集器的老年代版本碳胳,使用多線程和標記整理算法

f)CMS收集器:

CMS收集器是基于標記清除算法實現(xiàn)的,整個過程分為4個步驟:

1.初始標記2.并發(fā)標記3.重新標記4.并發(fā)清除

其中沫勿,初始標記挨约、重新標記這兩個步驟仍然需要"stop the world"。初始標記僅僅只是標記一下GC Roots能直接關(guān)聯(lián)到的對象产雹,速度很快诫惭,并發(fā)標記階段就是進行GC Roots Tracing的過程,而重新標記階段則是為了修正并發(fā)標記期間因用戶程序繼續(xù)運行而導致標記產(chǎn)生變動的那一部分對象的標記記錄蔓挖,這個階段的停頓時間一般會比初始標記階段稍長一些夕土,但遠比并發(fā)標記時間短。

由于整個過程中耗時最長的并發(fā)標記和并發(fā)清除過程收集器線程都可以與用戶線程一起工作瘟判,所以怨绣,從總體上來說,CMS收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)執(zhí)行的拷获。

優(yōu)點:并發(fā)收集篮撑、低停頓

缺點:

1.CMS收集器對CPU資源非常敏感,CMS默認啟動的回收線程數(shù)是(CPU數(shù)量+3)/4匆瓜,

2.CMS收集器無法處理浮動垃圾咽扇,可能出現(xiàn)Failure失敗而導致一次Full?GC的產(chǎn)生

3.CMS是基于標記清除算法實現(xiàn)的,容易產(chǎn)生大量空間碎片陕壹。

g)G1收集器:

它是一款面向服務(wù)器應(yīng)用的垃圾收集器

1.并行與并發(fā):利用多CPU縮短STOP-The-World停頓的時間

2.分代收集

3.空間整合:不會產(chǎn)生內(nèi)存碎片

4.可預(yù)測的停頓

運作方式:初始標記质欲,并發(fā)標記,最終標記糠馆,篩選回收

6.內(nèi)存分配與回收策略

4.6.1對象優(yōu)先在Eden分配:

大多數(shù)情況對象在新生代Eden區(qū)分配嘶伟,當Eden區(qū)沒有足夠空間進行分配時,虛擬機將發(fā)起一次Minor?GC

4.6.2大對象直接進入老年代:

所謂大對象就是指需要大量連續(xù)內(nèi)存空間的Java對象又碌,最典型的大對象就是那種很長的字符串以及數(shù)組九昧。這樣做的目的是避免Eden區(qū)及兩個Servivor之間發(fā)生大量的內(nèi)存復(fù)制

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

如果對象在Eden區(qū)出生并且盡力過一次Minor?GC后仍然存活,并且能夠被Servivor容納毕匀,將被移動到Servivor空間中铸鹰,并且把對象年齡設(shè)置成為1.對象在Servivor區(qū)中每熬過一次Minor?GC,年齡就增加1歲皂岔,當它的年齡增加到一定程度(默認15歲)蹋笼,就將會被晉級到老年代中

4.6.4動態(tài)對象年齡判定

為了更好地適應(yīng)不同程序的內(nèi)存狀況,虛擬機并不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉級到老年代,如果在Servivor空間中相同年齡所有對象的大小總和大于Survivor空間的一半剖毯,年齡大于或等于該年齡的對象就可以直接進入到老年代圾笨,無須等到MaxTenuringThreshold中要求的年齡

4.6.4空間分配擔保:

在發(fā)生Minor?GC之前,虛擬機會檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間逊谋,如果這個條件成立擂达,那么Minor GC可以確保是安全的。如果不成立胶滋,則虛擬機會查看HandlePromotionFailure設(shè)置值是否允許擔保失敗板鬓。如果允許那么會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于晉級到老年代對象的平均大小,如果大于究恤,將嘗試進行一次Minor?GC穗熬,盡管這次MinorGC?是有風險的:如果小于,或者HandlePromotionFailure設(shè)置不允許冒險丁溅,那這時也要改為進行一次Full?GC

五唤蔗、虛擬機類加載機制

虛擬機把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進行校驗窟赏、轉(zhuǎn)換解析和初始化妓柜,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制

在Java語言里面涯穷,類型的加載棍掐、連接和初始化過程都是在程序運行期間完成的

5.1類加載的時機

類被加載到虛擬機內(nèi)存中開始,到卸載為止拷况,整個生命周期包括:加載作煌、驗證、準備赚瘦、解析粟誓、初始化、使用和卸載7個階段

加載起意、驗證鹰服、準備、初始化和卸載這5個階段的順序是確定的揽咕,類的加載過程必須按照這種順序按部就班地開始悲酷,而解析階段則不一定:它在某些情況下可以再初始化階段之后再開始,這個是為了支持Java語言運行時綁定(也成為動態(tài)綁定或晚期綁定)

虛擬機規(guī)范規(guī)定有且只有5種情況必須立即對類進行初始化:

1.遇到new亲善、getstatic设易、putstatic或invokestatic這4條字節(jié)碼指令時,如果類沒有進行過初始化蛹头,則需要觸發(fā)其初始化顿肺。生成這4條指令的最常見的Java代碼場景是:使用new關(guān)鍵字實例化對象的時候戏溺、讀取或設(shè)置一個類的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時候挟冠,以及調(diào)用一個類的靜態(tài)方法的時候

2.使用java.lang.reflect包的方法對類進行反射調(diào)用的時候,如果類沒有進行過初始化袍睡,則需要先觸發(fā)其初始化

3.當初始化一個類的時候知染,如果發(fā)現(xiàn)其父類還沒有進行過初始化,則需要先觸發(fā)其父類的初始化

4.當虛擬機啟動時候斑胜,用戶需要指定一個要執(zhí)行的主類(包含main()方法的那個類)控淡,虛擬機會先初始化這個主類

5.當使用JDK1.7的動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結(jié)果REF_getStatic止潘、REF_putStatic掺炭、REF_invokeStatic的方法句柄,并且這個方法句柄所對應(yīng)的類沒有進行過初始化凭戴,則需要先觸發(fā)其初始化

被動引用:

1.通過子類引用父類的靜態(tài)字段涧狮,不會導致子類初始化

2.通過數(shù)組定義來引用類,不會觸發(fā)此類的初始化

3.常量在編譯階段會存入調(diào)用類的常量池中么夫,本質(zhì)上并沒有直接引用到定義常量的類者冤,因此不會觸發(fā)定義常量的類的初始化

接口的初始化:接口在初始化時,并不要求其父接口全部完成類初始化档痪,只有在真正使用到父接口的時候(如引用接口中定義的常量)才會初始化

5.2類加載的過程

5.2.1加載

1)通過一個類的全限定名類獲取定義此類的二進制字節(jié)流

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

3)在內(nèi)存中生成一個代表這個類的java.lang.Class對象涉枫,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口

怎么獲取二進制字節(jié)流?

1)從ZIP包中讀取腐螟,這很常見愿汰,最終成為日后JAR、EAR乐纸、WAR格式的基礎(chǔ)

2)從網(wǎng)絡(luò)中獲取衬廷,這種場景最典型的應(yīng)用就是Applet

3)運行時計算生成,這種常見使用得最多的就是動態(tài)代理技術(shù)

4)由其他文件生成汽绢,典型場景就是JSP應(yīng)用

5)從數(shù)據(jù)庫中讀取泵督,這種場景相對少一些(中間件服務(wù)器)

數(shù)組類本身不通過類加載器創(chuàng)建,它是由Java虛擬機直接創(chuàng)建的

數(shù)組類的創(chuàng)建過程遵循以下規(guī)則:

1)如果數(shù)組的組件類型(指的是數(shù)組去掉一個維度的類型)是引用類型庶喜,那就遞歸采用上面的加載過程去加載這個組件類型小腊,數(shù)組C將在加載該組件類型的類加載器的類名稱空間上被標識

2)如果數(shù)組的組件類型不是引用類型(列如int[]組數(shù)),Java虛擬機將會把數(shù)組C標識為與引導類加載器關(guān)聯(lián)

3)數(shù)組類的可見性與它的組件類型的可見性一致久窟,如果組件類型不是引用類型秩冈,那數(shù)組類的可見性將默認為public

5.2.2驗證

驗證階段會完成下面4個階段的檢驗動作:文件格式驗證,元數(shù)據(jù)驗證斥扛,字節(jié)碼驗證入问,符號引用驗證

1.文件格式驗證

第一階段要驗證字節(jié)流是否符合Class文件格式的規(guī)范丹锹,并且能被當前版本的虛擬機處理。這一階段可能包括:

1.是否以魔數(shù)oxCAFEBABE開頭

2.主芬失、次版本號是否在當前虛擬機處理范圍之內(nèi)

3.常量池的常量中是否有不被支持的常量類型(檢查常量tag標志)

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

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

6.Class文件中各個部分及文件本身是否有被刪除的或附加的其他信息

這個階段的驗證時基于二進制字節(jié)流進行的楣黍,只有通過類這個階段的驗證后,字節(jié)流才會進入內(nèi)存的方法區(qū)進行存儲棱烂,所以后面的3個驗證階段全部是基于方法區(qū)的存儲結(jié)構(gòu)進行的租漂,不會再直接操作字節(jié)流

2.元數(shù)據(jù)驗證

1.這個類是否有父類(除了java.lang.Object之外,所有的類都應(yīng)當有父類)

2.這個類的父類是否繼承了不允許被繼承的類(被final修飾的類)

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

4.類中的字段颊糜、方法是否與父類產(chǎn)生矛盾(列如覆蓋類父類的final字段,或者出現(xiàn)不符合規(guī)則的方法重載哩治,列如方法參數(shù)都一致,但返回值類型卻不同等)

第二階段的主要目的是對類元數(shù)據(jù)信息進行語義校驗衬鱼,保證不存在不符合Java語言規(guī)范的元數(shù)據(jù)信息

3.字節(jié)碼驗證

第三階段是整個驗證過程中最復(fù)雜的一個階段业筏,主要目的似乎通過數(shù)據(jù)流和控制流分析,確定程序語言是合法的鸟赫、符合邏輯的蒜胖。在第二階段對元數(shù)據(jù)信息中的數(shù)據(jù)類型做完校驗后,這個階段將對類的方法體進行校驗分析抛蚤,保證被校驗類的方法在運行時不會做出危害虛擬機安全的事件翠勉。

1.保證任意時刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作,列如霉颠,列如在操作數(shù)棧放置類一個int類型的數(shù)據(jù)对碌,使用時卻按long類型來加載入本地變量表中

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

3.保證方法體中的類型轉(zhuǎn)換時有效的,列如可以把一個子類對象賦值給父類數(shù)據(jù)類型蒿偎,這個是安全的朽们,但是吧父類對象賦值給子類數(shù)據(jù)類型,甚至把對象賦值給與它毫無繼承關(guān)系诉位、完全不相干的一個數(shù)據(jù)類型骑脱,則是危險和不合法的

4.符號引用驗證

發(fā)生在虛擬機將符號引用轉(zhuǎn)化為直接引用的時候,這個轉(zhuǎn)化動作將在連接的第三階段——解析階段中發(fā)生苍糠。

1.符號引用中通過字符串描述的全限定名是否能找到相對應(yīng)的類

2.在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段

3.符號引用中的類叁丧、字段、方法的訪問性是否可被當前類訪問

對于虛擬機的類加載機制來說岳瞭,驗證階段是非常重要的拥娄,但是不一定必要(因為對程序運行期沒有影響)的階段。如果全部代碼都已經(jīng)被反復(fù)使用和驗證過瞳筏,那么在實施階段就可以考慮使用Xverify:none參數(shù)來關(guān)閉大部分的類驗證措施稚瘾,以縮短虛擬機類加載的時間

5.2.3準備

準備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量都在方法區(qū)中進行分配姚炕。這個時候進行內(nèi)存分配的僅包括類變量(被static修飾的變量)摊欠,而不包括實例變量丢烘,實例變量將會在對象實例化時隨著對象一起分配在Java堆中。其次些椒,這里說的初始值播瞳,在“通常情況”下是數(shù)據(jù)類型的零值。

假設(shè)public?static?int?value?=?123免糕;

那變量value在準備階段過后的初始值為0而不是123赢乓,因為這時候尚未開始執(zhí)行任何Java方法,而把value賦值為123的putstatic指令是程序被編譯后说墨,存放于類構(gòu)造器()方法之中骏全,所以把value賦值為123的動作將在初始化階段才會執(zhí)行苍柏,但是如果使用final修飾尼斧,則在這個階段其初始值設(shè)置為123

5.2.4解析

解析階段是虛擬機將常量池內(nèi)符號引用替換為直接引用的過

5.2.5初始化

類的初始化階段是類加載過程的最后一步,前面的類加載過程中试吁,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器參與之外棺棵,其余動作完全由虛擬機主導和控制。到了初始化階段熄捍,才正真開始執(zhí)行類中定義的Java程序代碼(或者說是字節(jié)碼)

5.3類的加載器

5.3.1雙親委派模型:

只存在兩種不同的類加載器:啟動類加載器(Bootstrap?ClassLoader)烛恤,使用C++實現(xiàn),是虛擬機自身的一部分余耽。另一種是所有其他的類加載器缚柏,使用JAVA實現(xiàn),獨立于JVM碟贾,并且全部繼承自抽象類java.lang.ClassLoader.

啟動類加載器(Bootstrap?ClassLoader)币喧,負責將存放在\lib目錄中的,或者被-Xbootclasspath參數(shù)所制定的路徑中的袱耽,并且是JVM識別的(僅按照文件名識別杀餐,如rt.jar,如果名字不符合朱巨,即使放在lib目錄中也不會被加載)史翘,加載到虛擬機內(nèi)存中,啟動類加載器無法被JAVA程序直接引用冀续。

擴展類加載器琼讽,由sun.misc.Launcher$ExtClassLoader實現(xiàn),負責加載\lib\ext目錄中的洪唐,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫跨琳,開發(fā)者可以直接使用擴展類加載器。

應(yīng)用程序類加載器(Application?ClassLoader)桐罕,由sun.misc.Launcher$AppClassLoader來實現(xiàn)脉让。由于這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值桂敛,所以一般稱它為系統(tǒng)類加載器。負責加載用戶類路徑(ClassPath)上所指定的類庫溅潜,開發(fā)者可以直接使用這個類加載器术唬,如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器滚澜。


這張圖表示類加載器的雙親委派模型(Parents?Delegation?model).?雙親委派模型要求除了頂層的啟動加載類外粗仓,其余的類加載器都應(yīng)當有自己的父類加載器。這里類加載器之間的父子關(guān)系一般不會以繼承的關(guān)系來實現(xiàn)设捐,而是使用組合關(guān)系來復(fù)用父類加載器的代碼借浊。

5.3.2雙親委派模型的工作過程是:

如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類萝招,而是把這個請求委派給父類加載器去完成蚂斤,每一個層次的類加載器都是如此,因此所有的加載請求最終都是應(yīng)該傳送到頂層的啟動類加載器中槐沼,只有當父類加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時曙蒸,子加載器才會嘗試自己去加載。

5.3.3這樣做的好處就是:

Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系岗钩。例如類java.lang.Object,它存放在rt.jar中纽窟,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器進行加載兼吓,因此Object類在程序的各種類加載器環(huán)境中都是同一個類臂港。相反,如果沒有使用雙親委派模型视搏,由各個類加載器自行去加載的話审孽,如果用戶自己編寫了一個稱為java.lang.object的類,并放在程序的ClassPath中凶朗,那系統(tǒng)中將會出現(xiàn)多個不同的Object類瓷胧,Java類型體系中最基礎(chǔ)的行為也就無法保證,應(yīng)用程序也將會變得一片混亂

就是保證某個范圍的類一定是被某個類加載器所加載的棚愤,這就保證在程序中同一個類不會被不同的類加載器加載搓萧。這樣做的一個主要的考量,就是從安全層面上宛畦,杜絕通過使用和JRE相同的類名冒充現(xiàn)有JRE的類達到替換的攻擊方式

六瘸洛、Java內(nèi)存模型與線程

6.1內(nèi)存間的交互操作

關(guān)于主內(nèi)存與工作內(nèi)存之間的具體交互協(xié)議,即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存次和、如何從工作內(nèi)存同步到主內(nèi)存之間的實現(xiàn)細節(jié)反肋,Java內(nèi)存模型定義了以下八種操作來完成:

lock(鎖定):作用于主內(nèi)存的變量,把一個變量標識為一條線程獨占狀態(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í)行引擎耘纱,每當虛擬機遇到一個需要使用變量的值的字節(jié)碼指令時將會執(zhí)行這個操作敬肚。

assign(賦值):作用于工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量束析,每當虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作艳馒。

store(存儲):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個變量的值傳送到主內(nèi)存中畸陡,以便隨后的write的操作鹰溜。

write(寫入):作用于主內(nèi)存的變量虽填,它把store操作從工作內(nèi)存中一個變量的值傳送到主內(nèi)存的變量中丁恭。

如果要把一個變量從主內(nèi)存中復(fù)制到工作內(nèi)存,就需要按順尋地執(zhí)行read和load操作斋日,如果把變量從工作內(nèi)存中同步回主內(nèi)存中牲览,就要按順序地執(zhí)行store和write操作。Java內(nèi)存模型只要求上述操作必須按順序執(zhí)行恶守,而沒有保證必須是連續(xù)執(zhí)行第献。也就是read和load之間,store和write之間是可以插入其他指令的兔港,如對主內(nèi)存中的變量a庸毫、b進行訪問時,可能的順序是read?a衫樊,read?b飒赃,load?b,?load?a科侈。

Java內(nèi)存模型還規(guī)定了在執(zhí)行上述八種基本操作時载佳,必須滿足如下規(guī)則:

不允許read和load、store和write操作之一單獨出現(xiàn)

不允許一個線程丟棄它的最近assign的操作臀栈,即變量在工作內(nèi)存中改變了之后必須同步到主內(nèi)存中蔫慧。

不允許一個線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中。

一個新的變量只能在主內(nèi)存中誕生权薯,不允許在工作內(nèi)存中直接使用一個未被初始化(load或assign)的變量姑躲。即就是對一個變量實施use和store操作之前睡扬,必須先執(zhí)行過了assign和load操作。

一個變量在同一時刻只允許一條線程對其進行l(wèi)ock操作黍析,但lock操作可以被同一條線程重復(fù)執(zhí)行多次威蕉,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作橄仍,變量才會被解鎖韧涨。lock和unlock必須成對出現(xiàn)

如果對一個變量執(zhí)行l(wèi)ock操作,將會清空工作內(nèi)存中此變量的值侮繁,在執(zhí)行引擎使用這個變量前需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值

如果一個變量事先沒有被lock操作鎖定虑粥,則不允許對它執(zhí)行unlock操作;也不允許去unlock一個被其他線程鎖定的變量宪哩。

對一個變量執(zhí)行unlock操作之前娩贷,必須先把此變量同步到主內(nèi)存中(執(zhí)行store和write操作)。

6.2重排序

在執(zhí)行程序時為了提高性能锁孟,編譯器和處理器經(jīng)常會對指令進行重排序彬祖。重排序分成三種類型:

1.編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義放入前提下品抽,可以重新安排語句的執(zhí)行順序储笑。

2.指令級并行的重排序。現(xiàn)代處理器采用了指令級并行技術(shù)來將多條指令重疊執(zhí)行圆恤。如果不存在數(shù)據(jù)依賴性突倍,處理器可以改變語句對應(yīng)機器指令的執(zhí)行順序。

3.內(nèi)存系統(tǒng)的重排序盆昙。由于處理器使用緩存和讀寫緩沖區(qū)羽历,這使得加載和存儲操作看上去可能是在亂序執(zhí)行。

從Java源代碼到最終實際執(zhí)行的指令序列淡喜,會經(jīng)過下面三種重排序:

為了保證內(nèi)存的可見性秕磷,Java編譯器在生成指令序列的適當位置會插入內(nèi)存屏障指令來禁止特定類型的處理器重排序。Java內(nèi)存模型把內(nèi)存屏障分為LoadLoad炼团、LoadStore澎嚣、StoreLoad和StoreStore四種:

6.3對于volatile型變量的特殊規(guī)則

當一個變量定義為volatile之后,它將具備兩種特性:

第一:保證此變量對所有線程的可見性们镜,這里的可見性是指當一條線程修改了這個變量的值币叹,新值對于其他線程來說是可以立即得知的顷啼。普通變量的值在線程間傳遞需要通過主內(nèi)存來完成

由于valatile只能保證可見性亮瓷,在不符合一下兩條規(guī)則的運算場景中,我們?nèi)砸ㄟ^加鎖來保證原子性

1.運算結(jié)果并不依賴變量的當前值柴罐,或者能夠確保只有單一的線程修改變量的值。

2.變量不需要與其他的狀態(tài)變量共同參與不變約束

第二:禁止指令重排序贩汉,普通的變量僅僅會保證在該方法的執(zhí)行過程中所有依賴賦值結(jié)果的地方都能獲取到正確的結(jié)果驱富,而不能保證變量賦值操作的順序與程序代碼中執(zhí)行順序一致,這個就是所謂的線程內(nèi)表現(xiàn)為串行的語義

Java內(nèi)存模型中對volatile變量定義的特殊規(guī)則匹舞。假定T表示一個線程褐鸥,V和W分別表示兩個volatile變量,那么在進行read赐稽、load叫榕、use、assign姊舵、store晰绎、write操作時需要滿足如下的規(guī)則:

1.只有當線程T對變量V執(zhí)行的前一個動作是load的時候,線程T才能對變量V執(zhí)行use動作括丁;并且荞下,只有當線程T對變量V執(zhí)行的后一個動作是use的時候,線程T才能對變量V執(zhí)行l(wèi)oad操作史飞。線程T對變量V的use操作可以認為是與線程T對變量V的load和read操作相關(guān)聯(lián)的尖昏,必須一起連續(xù)出現(xiàn)。這條規(guī)則要求在工作內(nèi)存中构资,每次使用變量V之前都必須先從主內(nèi)存刷新最新值抽诉,用于保證能看到其它線程對變量V所作的修改后的值。

2.只有當線程T對變量V執(zhí)行的前一個動是assign的時候蚯窥,線程T才能對變量V執(zhí)行store操作掸鹅;并且塞帐,只有當線程T對變量V執(zhí)行的后一個動作是store操作的時候拦赠,線程T才能對變量V執(zhí)行assign操作。線程T對變量V的assign操作可以認為是與線程T對變量V的store和write操作相關(guān)聯(lián)的葵姥,必須一起連續(xù)出現(xiàn)荷鼠。這一條規(guī)則要求在工作內(nèi)存中,每次修改V后都必須立即同步回主內(nèi)存中榔幸,用于保證其它線程可以看到自己對變量V的修改允乐。

3.假定操作A是線程T對變量V實施的use或assign動作,假定操作F是操作A相關(guān)聯(lián)的load或store操作削咆,假定操作P是與操作F相應(yīng)的對變量V的read或write操作牍疏;類型地,假定動作B是線程T對變量W實施的use或assign動作拨齐,假定操作G是操作B相關(guān)聯(lián)的load或store操作鳞陨,假定操作Q是與操作G相應(yīng)的對變量V的read或write操作。如果A先于B瞻惋,那么P先于Q厦滤。這條規(guī)則要求valitile修改的變量不會被指令重排序優(yōu)化援岩,保證代碼的執(zhí)行順序與程序的順序相同。

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

Java模型要求lock掏导、unlock享怀、read、load趟咆、assign添瓷、use、store值纱、write這8個操作都具有原子性仰坦,但是對于64為的數(shù)據(jù)類型(long和double),在模型中特別定義了一條相對寬松的規(guī)定:允許虛擬機將沒有被volatile修飾的64位數(shù)據(jù)的讀寫操作分為兩次32為的操作來進行计雌,即允許虛擬機實現(xiàn)選擇可以不保證64位數(shù)據(jù)類型的load悄晃、store、read和write這4個操作的原子性

6.5原子性凿滤、可見性和有序性

原子性:即一個操作或者多個操作?要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷妈橄,要么就都不執(zhí)行。Java內(nèi)存模型是通過在變量修改后將新值同步會主內(nèi)存翁脆,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方式來實現(xiàn)可見性眷蚓,valatile特殊規(guī)則保障新值可以立即同步到祝內(nèi)存中。Synchronized是在對一個變量執(zhí)行unlock之前反番,必須把變量同步回主內(nèi)存中(執(zhí)行store沙热、write操作)。被final修飾的字段在構(gòu)造器中一旦初始化完成罢缸,并且構(gòu)造器沒有吧this的引用傳遞出去篙贸,那在其他線程中就能看見final字段的值

可見性:可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值枫疆,其他線程能夠立即看得到修改的值爵川。

有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。

6.6先行發(fā)生原則

這些先行發(fā)生關(guān)系無須任何同步就已經(jīng)存在息楔,如果不再此列就不能保障順序性寝贡,虛擬機就可以對它們?nèi)我獾剡M行重排序

1.程序次序規(guī)則:在一個線程內(nèi),按照程序代碼順序值依,書寫在前面的操作先行發(fā)生于書寫在后面的操作圃泡。準確的說,應(yīng)該是控制順序而不是程序代碼順序愿险,因為要考慮分支颇蜡。循環(huán)等結(jié)構(gòu)

2.管程鎖定規(guī)則:一個unlock操作先行發(fā)生于后面對同一個鎖的lock操作。這里必須強調(diào)的是同一個鎖,而后面的是指時間上的先后順序

3.Volatile變量規(guī)則:對一個volatile變量的寫操作先行發(fā)生于后面對這個變量的讀操作澡匪,這里的后面同樣是指時間上的先后順序

4.線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每一個動作

5.線程終止規(guī)則:線程中的所有操作都先行發(fā)生于對此線程的終止檢測熔任,我們可以通過Thread.joke()方法結(jié)束、ThradisAlive()的返回值等手段檢測到線程已經(jīng)終止執(zhí)行

6.線程中斷規(guī)則:對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷時間的發(fā)生唁情,可以通過Thread.interrupted()方法檢測到是否有中斷發(fā)生

7.對象終結(jié)規(guī)則:一個對象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的finalize()方法的開始

8.傳遞性:如果操作A先行發(fā)生于操作B疑苔,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結(jié)論

6.7??Java線程調(diào)度

協(xié)同式調(diào)度:線程的執(zhí)行時間由線程本身控制

搶占式調(diào)度:線程的執(zhí)行時間由系統(tǒng)來分配

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

1.新建

2.運行:可能正在執(zhí)行甸鸟〉敕眩可能正在等待CPU為它分配執(zhí)行時間

3.無限期等待:不會被分配CUP執(zhí)行時間,它們要等待被其他線程顯式喚醒

4.限期等待:不會被分配CUP執(zhí)行時間抢韭,它們無須等待被其他線程顯式喚醒薪贫,一定時間會由系統(tǒng)自動喚醒

5.阻塞:阻塞狀態(tài)在等待這獲取到一個排他鎖,這個時間將在另一個線程放棄這個鎖的時候發(fā)生刻恭;等待狀態(tài)就是在等待一段時間瞧省,或者喚醒動作的發(fā)生

6.結(jié)束:已終止線程的線程狀態(tài),線程已經(jīng)結(jié)束執(zhí)行

七鳍贾、線程安全

1鞍匾、不可變:不可變的對象一定是線程安全的、無論是對象的方法實現(xiàn)還是方法的調(diào)用者橡淑,都不需要再采取任何的線程安全保障咆爽。例如:把對象中帶有狀態(tài)的變量都聲明為final梁棠,這樣在構(gòu)造函數(shù)結(jié)束之后,它就是不可變的斗埂。

2符糊、絕對線程安全

3、相對線程安全:相對的線程安全就是我們通常意義上所講的線程安全蜜笤,它需要保證對這個對象單獨的操作是線程安全的濒蒋,我們在調(diào)用的時候不需要做額外的保障措施,但是對于一些特定順序的連續(xù)調(diào)用把兔,就可能需要在調(diào)用端使用額外的同步手段來保證調(diào)用的正確性

4、線程兼容:對象本身并不是線程安全的瓮顽,但是可以通過在調(diào)用端正確地使用同步手段來保證對象在并發(fā)環(huán)境中可以安全使用

5县好、線程對立:是指無論調(diào)用端是否采取了同步措施,都無法在多線程環(huán)境中并發(fā)使用的代碼

7.1線程安全的實現(xiàn)方法

1.互斥同步:

同步是指在多個線程并發(fā)訪問共享數(shù)據(jù)時暖混,保證共享數(shù)據(jù)在同一個時刻只被一個(或者是一些缕贡,使用信號量的時候)線程使用。而互斥是實現(xiàn)同步的一種手段,臨界區(qū)晾咪、互斥量和信號量都是主要的互斥實現(xiàn)方式收擦。互斥是因谍倦,同步是果:互斥是方法塞赂,同步是目的

在Java中,最基本的互斥同步手段就是synchronized關(guān)鍵字昼蛀,它經(jīng)過編譯之后宴猾,會在同步塊的前后分別形成monitorenter和monitorexit這兩個字節(jié)碼指令,這兩個字節(jié)碼都需要一個reference類型的參數(shù)來指明要鎖定和解鎖的對象叼旋。如果Java程序中的synchronized明確指定了對象參數(shù)仇哆,那就是這個對象的reference夫植;如果沒有指明,那就根據(jù)synchronized修飾的是實例方法還是類方法辟拷,去取對應(yīng)的對象實例或Class對象來作為鎖對象。在執(zhí)行monitorenter指令時隅俘,首先要嘗試獲取對象的鎖为居。如果這個對象沒有被鎖定蒙畴,或者當前線程已經(jīng)擁有了那個對象的鎖膳凝,把鎖的計數(shù)器加1,對應(yīng)的在執(zhí)行monitorexit指令時會將鎖計數(shù)器減1著淆,當計數(shù)器為0時,鎖就被釋放独泞。如果獲取對象鎖失敗,哪當前線程就要阻塞等待讲坎,直到對象鎖被另外一個線程釋放為止

Synchronized衫画,ReentrantLock增加了一些高級功能

1.等待可中斷:是指當持有鎖的線程長期不釋放鎖的時候削罩,正在等待的線程可以選擇放棄等待弥激,改為處理其他事情,可中斷特性對處理執(zhí)行時間非常長的同步塊很有幫助

2.公平鎖:是指多個線程在等待同一個鎖時以蕴,必須按照申請鎖的時間順序來依次獲得鎖丛肮;非公平鎖則不能保證這一點宝与,在鎖被釋放時,任何一個等待鎖的線程都有機會獲得鎖榜聂。Synchronized中的鎖是非公平的须肆,ReentrantLock默認情況下也是非公平的豌汇,但可以通過帶布爾值的構(gòu)造函數(shù)要求使用公平鎖

3.鎖綁定多個條件是指一個ReentrantLock對象可以同時綁定多個Condition對象,而在synchronized中佛嬉,鎖對象的wait()和notify()或notifyAll()方法可以實現(xiàn)一個隱含的條件暖呕,如果要和多余一個的條件關(guān)聯(lián)的時候瓤逼,就不得不額外地添加一個鎖霸旗,而ReentrantLock則無須這樣做,只需要多次調(diào)用newCondition方法即可

2.非阻塞同步

3.無同步方案

可重入代碼:也叫純代碼精居,可以在代碼執(zhí)行的任何時刻中斷它,轉(zhuǎn)而去執(zhí)行另外一段代碼(包括遞歸調(diào)用它本身)而在控制權(quán)返回后刮便,原來的程序不會出現(xiàn)任何錯誤辈毯。所有的可重入代碼都是線程安全的谆沃,但是并非所有的線程安全的代碼都是可重入的耕陷。

判斷一個代碼是否具備可重入性:如果一個方法,它的返回結(jié)果是可預(yù)測的嗜诀,只要輸入了相同的數(shù)據(jù)隆敢,就都能返回相同的結(jié)果,那它就滿足可重入性的要求匣屡,當然也就是線程安全的

線程本地存儲:如果一段代碼中所需要的數(shù)據(jù)必須與其他代碼共享捣作,那就看看這些共享數(shù)據(jù)的代碼是否能保證在同一個線程中執(zhí)行?如果能保障也拜,我們就可以把共享數(shù)據(jù)的可見范圍限制在同一個線程之內(nèi)慢哈,這樣卵贱,無須同步也能保證線程之間不出現(xiàn)數(shù)據(jù)爭用的問題

7.2鎖優(yōu)化

適應(yīng)性自旋、鎖消除、鎖粗化踪央、輕量級鎖和偏向鎖

7.2.1自旋鎖與自適應(yīng)自旋

自旋鎖:如果物理機器上有一個以上的處理器镐牺,能讓兩個或以上的線程同時并行執(zhí)行,我們就可以讓后面請求鎖的那個線程稍等一下,但不放棄處理器的執(zhí)行時間检疫,看看持有鎖的線程是否很快就會釋放鎖。為了讓線程等待祷嘶,我們只需讓線程執(zhí)行一個忙循環(huán)(自旋)屎媳,這項技術(shù)就是所謂的自旋鎖

自適應(yīng)自旋轉(zhuǎn):是由前一次在同一個鎖對象上,自旋等待剛剛成功獲得過鎖论巍,并且持有鎖的線程正在運行中烛谊,那么虛擬機就會認為這次自旋也很有可能再次成功,進而它將允許自旋等待持續(xù)相對更長的時間嘉汰。如果對于某個鎖丹禀,自旋很少成功獲得過,那在以后要獲取這個鎖時將可能省略掉自過程残腌,以避免浪費處理器資源日缨。

7.2.2鎖消除

鎖消除是指虛擬機即時編輯器在運行時尚卫,對一些代碼上要求同步,但是被檢測到不可能存在共享數(shù)據(jù)競爭的鎖進行消除。如果在一段代碼中灌侣。推上的所有數(shù)據(jù)都不會逃逸出去從而被其他線程訪問到,那就可以把它們當作棧上數(shù)據(jù)對待甜孤,認為它們是線程私有的而线,同步加鎖自然就無須進行

7.2.3鎖粗化

如果虛擬機檢測到有一串零碎的操作都是對同一對象的加鎖磅网,將會把加鎖同步的范圍擴展(粗化)到整個操作序列的外部

7.2.4輕量級鎖

7.2.5偏向鎖

它的目的是消除無競爭情況下的同步原語,進一步提高程序的運行性能。如果輕量級鎖是在無競爭的情況下使用CAS操作去消除同步使用的互斥量绿语,那偏向鎖就是在無競爭的情況下把這個同步都消除掉稳其,CAS操作都不做了

如果在接下倆的執(zhí)行過程中浑槽,該鎖沒有被其他線程獲取铣卡,則持有偏向鎖的線程將永遠不需要在進行同步

八、逃逸分析

逃逸分析的基本行為就是分析對象動態(tài)作用域:當一個對象在方法中被定義后蛤育,它可能被外部方法所引用瓦糕,例如作為調(diào)用參數(shù)傳遞到其他方法中枷恕,成為方法逃逸。甚至還可能被外部線程訪問到,比如賦值給類變量或可以在其他線程中訪問的實例變量壹蔓,稱為線程逃逸

如果一個對象不會逃逸到方法或線程之外酿联,也就是別的方法或線程無法通過任何途徑訪問到這個對象终息,則可能為這個變量進行一些高效的優(yōu)化

棧上分配:如果確定一個對象不會逃逸出方法外,那讓這個對象在棧上分配內(nèi)存將會是一個不錯的注意贞让,對象所占用的內(nèi)存空間就可以隨棧幀出棧而銷毀周崭。如果能使用棧上分配,那大量的對象就隨著方法的結(jié)束而銷毀了喳张,垃圾收集系統(tǒng)的壓力將會小很多

同步消除:如果確定一個變量不會逃逸出線程续镇,無法被其他線程訪問,那這個變量的讀寫肯定就不會有競爭销部,對這個變量實施的同步措施也就可以消除掉

標量替換:標量就是指一個數(shù)據(jù)無法在分解成更小的數(shù)據(jù)表示了摸航,int、long等及refrence類型等都不能在進一步分解舅桩,它們稱為標量酱虎。

如果一個數(shù)據(jù)可以繼續(xù)分解,就稱為聚合量擂涛,Java中的對象就是最典型的聚合量

如果一個對象不會被外部訪問读串,并且這個對象可以被拆散的化,那程序正整執(zhí)行的時候?qū)⒖赡懿粍?chuàng)建這個對象,而改為直接創(chuàng)建它的若干個被這個方法使用到的成員變量來代替

轉(zhuǎn)自:http://www.cnblogs.com/prayers/p/5515245.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末恢暖,一起剝皮案震驚了整個濱河市排监,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌杰捂,老刑警劉巖舆床,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異琼娘,居然都是意外死亡峭弟,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門脱拼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瞒瘸,“玉大人,你說我怎么就攤上這事熄浓∏槌簦” “怎么了?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵赌蔑,是天一觀的道長俯在。 經(jīng)常有香客問我,道長娃惯,這世上最難降的妖魔是什么跷乐? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮趾浅,結(jié)果婚禮上愕提,老公的妹妹穿的比我還像新娘。我一直安慰自己皿哨,他們只是感情好浅侨,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著证膨,像睡著了一般如输。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上央勒,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天不见,我揣著相機與錄音,去河邊找鬼订歪。 笑死脖祈,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的刷晋。 我是一名探鬼主播盖高,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼慎陵,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了喻奥?” 一聲冷哼從身側(cè)響起席纽,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎撞蚕,沒想到半個月后润梯,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡甥厦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年纺铭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片刀疙。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡舶赔,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出谦秧,到底是詐尸還是另有隱情竟纳,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布疚鲤,位于F島的核電站锥累,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏集歇。R本人自食惡果不足惜桶略,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望诲宇。 院中可真熱鬧删性,春花似錦、人聲如沸焕窝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽它掂。三九已至,卻和暖如春溯泣,著一層夾襖步出監(jiān)牢的瞬間虐秋,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工垃沦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留客给,地道東北人。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓肢簿,卻偏偏與公主長得像靶剑,于是被迫代替她去往敵國和親蜻拨。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353

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

  • 一桩引、運行時數(shù)據(jù)區(qū)域 Java虛擬機管理的內(nèi)存包括幾個運行時數(shù)據(jù)內(nèi)存:方法區(qū)缎讼、虛擬機棧、本地方法棧坑匠、堆血崭、程序計數(shù)器,...
    加油小杜閱讀 1,518評論 1 15
  • 《深入理解Java虛擬機》筆記_第一遍 先取看完這本書(JVM)后必須掌握的部分厘灼。 第一部分 走近 Java 從傳...
    xiaogmail閱讀 5,087評論 1 34
  • 從三月份找實習到現(xiàn)在夹纫,面了一些公司,掛了不少设凹,但最終還是拿到小米舰讹、百度、阿里围来、京東跺涤、新浪、CVTE监透、樂視家的研發(fā)崗...
    時芥藍閱讀 42,239評論 11 349
  • 當我們提到情緒桶错,往往會出現(xiàn)很多消極的描述,比如:內(nèi)心的野獸胀蛮、不理性等等院刁。其實情緒是一種高階的智力,丹尼爾.戈爾曼在...
    Teresa莎閱讀 665評論 0 0
  • 中秋節(jié)已經(jīng)過去不短的時間了粪狼,但是今年的中秋節(jié)在某大型公司十分的特別 內(nèi)部有一個搶購月餅的活動退腥,有一位調(diào)皮的程序員靈...
    彬少灬閱讀 12,930評論 2 6