一、運(yùn)行時(shí)數(shù)據(jù)區(qū)域
Java虛擬機(jī)管理的內(nèi)存包括幾個(gè)運(yùn)行時(shí)數(shù)據(jù)內(nèi)存:方法區(qū)、虛擬機(jī)棧、本地方法棧、堆、程序計(jì)數(shù)器,其中方法區(qū)和堆是由線程共享的數(shù)據(jù)區(qū),其他幾個(gè)是線程隔離的數(shù)據(jù)區(qū)
1.1?程序計(jì)數(shù)器
程序計(jì)數(shù)器是一塊較小的內(nèi)存,他可以看做是當(dāng)前線程所執(zhí)行的行號(hào)指示器。字節(jié)碼解釋器工作的時(shí)候就是通過改變這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼的指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來完成。如果線程正在執(zhí)行的是一個(gè)Java方法,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是Native方法,這個(gè)計(jì)數(shù)器則為空。此內(nèi)存區(qū)域是唯一一個(gè)在Java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemotyError情況的區(qū)域
1.2?Java虛擬機(jī)棧
虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀用于儲(chǔ)存局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。每個(gè)方法從調(diào)用直至完成的過程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過程。
棧內(nèi)存就是虛擬機(jī)棧,或者說是虛擬機(jī)棧中局部變量表的部分
局部變量表存放了編輯期可知的各種基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對(duì)象引用(refrence)類型和returnAddress類型(指向了一條字節(jié)碼指令的地址)
其中64位長度的long和double類型的數(shù)據(jù)會(huì)占用兩個(gè)局部變量空間,其余的數(shù)據(jù)類型只占用1個(gè)。
Java虛擬機(jī)規(guī)范對(duì)這個(gè)區(qū)域規(guī)定了兩種異常狀況:如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常。如果虛擬機(jī)擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存,就會(huì)跑出OutOfMemoryError異常
1.3?本地方法棧
本地方法棧和虛擬機(jī)棧發(fā)揮的作用是非常類似的,他們的區(qū)別是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機(jī)使用到的Native方法服務(wù)
本地方法棧區(qū)域也會(huì)拋出StackOverflowError和OutOfMemoryErroy異常
1.4?Java堆
堆是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊。Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)的時(shí)候創(chuàng)建,此內(nèi)存區(qū)域的唯一目的是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存。所有的對(duì)象實(shí)例和數(shù)組都在堆上分配
Java堆是垃圾收集器管理的主要區(qū)域。Java堆細(xì)分為新生代和老年代
不管怎樣,劃分的目的都是為了更好的回收內(nèi)存,或者更快地分配內(nèi)存
Java堆可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可。如果在堆中沒有完成實(shí)例分配,并且堆也無法在擴(kuò)展時(shí)將會(huì)拋出OutOfMemoryError異常
1.5?方法區(qū)
方法區(qū)它用于儲(chǔ)存已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)
除了Java堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴(kuò)展外,還可以選擇不實(shí)現(xiàn)垃圾收集。這個(gè)區(qū)域的內(nèi)存回收目標(biāo)主要是針對(duì)常量池的回收和對(duì)類型的卸載
當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時(shí),將拋出OutOfMemoryErroy異常
1.6?運(yùn)行時(shí)常量池
它是方法區(qū)的一部分。Class文件中除了有關(guān)的版本、字段、方法、接口等描述信息外、還有一項(xiàng)信息是常量池,用于存放編輯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放
Java語言并不要求常量一定只有編輯期才能產(chǎn)生,也就是可能將新的常量放入池中,這種特性被開發(fā)人員利用得比較多的便是String類的intern()方法
當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí)會(huì)拋出OutOfMemoryError異常
二、hotspot虛擬機(jī)對(duì)象
2.1對(duì)象的創(chuàng)建
1.檢查
虛擬機(jī)遇到一條new指令時(shí),首先將去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類是否已經(jīng)被加載、解析和初始化過。如果沒有,那必須先執(zhí)行相應(yīng)的類加載過程
2.分配內(nèi)存
接下來將為新生對(duì)象分配內(nèi)存,為對(duì)象分配內(nèi)存空間的任務(wù)等同于把一塊確定的大小的內(nèi)存從Java堆中劃分出來。
假設(shè)Java堆中內(nèi)存是絕對(duì)規(guī)整的,所有用過的內(nèi)存放在一遍,空閑的內(nèi)存放在另一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器,那所分配內(nèi)存就僅僅是把那個(gè)指針指向空閑空間那邊挪動(dòng)一段與對(duì)象大小相等的距離,這個(gè)分配方式叫做“指針碰撞”
如果Java堆中的內(nèi)存并不是規(guī)整的,已使用的內(nèi)存和空閑的內(nèi)存相互交錯(cuò),那就沒辦法簡單地進(jìn)行指針碰撞了,虛擬機(jī)就必須維護(hù)一個(gè)列表,記錄上哪些內(nèi)存塊是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新列表上的記錄,這種分配方式成為“空閑列表”
選擇那種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
3.?Init
執(zhí)行new指令之后會(huì)接著執(zhí)行Init方法,進(jìn)行初始化,這樣一個(gè)對(duì)象才算產(chǎn)生出來
2.2對(duì)象的內(nèi)存布局
在HotSpot虛擬機(jī)中,對(duì)象在內(nèi)存中儲(chǔ)存的布局可以分為3塊區(qū)域:對(duì)象頭、實(shí)例數(shù)據(jù)和對(duì)齊填充
對(duì)象頭包括兩部分:
a)?儲(chǔ)存對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼、GC分帶年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí)間戳
b)?另一部分是指類型指針,即對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對(duì)象是那個(gè)類的實(shí)例
2.3?對(duì)象的訪問定位
使用句柄訪問
Java堆中將會(huì)劃分出一塊內(nèi)存來作為句柄池,reference中存儲(chǔ)的就是對(duì)象的句柄地址,而句柄中包含了對(duì)象實(shí)例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址
優(yōu)勢(shì):reference中存儲(chǔ)的是穩(wěn)點(diǎn)的句柄地址,在對(duì)象被移動(dòng)(垃圾收集時(shí)移動(dòng)對(duì)象是非常普遍的行為)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而reference本身不需要修改
使用直接指針訪問
Java堆對(duì)象的布局就必須考慮如何訪問類型數(shù)據(jù)的相關(guān)信息,而refreence中存儲(chǔ)的直接就是對(duì)象的地址
優(yōu)勢(shì):速度更快,節(jié)省了一次指針定位的時(shí)間開銷,由于對(duì)象的訪問在Java中非常頻繁,因此這類開銷積少成多后也是一項(xiàng)非常可觀的執(zhí)行成本
三、OutOfMemoryError異常
3.1?Java堆溢出
Java堆用于存儲(chǔ)對(duì)象實(shí)例,只要不斷的創(chuàng)建對(duì)象,并且保證GCRoots到對(duì)象之間有可達(dá)路徑來避免垃圾回收機(jī)制清除這些對(duì)象,那么在數(shù)量到達(dá)最大堆的容量限制后就會(huì)產(chǎn)生內(nèi)存溢出異常
如果是內(nèi)存泄漏,可進(jìn)一步通過工具查看泄漏對(duì)象到GC?Roots的引用鏈。于是就能找到泄露對(duì)象是通過怎樣的路徑與GC?Roots相關(guān)聯(lián)并導(dǎo)致垃圾收集器無法自動(dòng)回收它們的。掌握了泄漏對(duì)象的類型信息及GC?Roots引用鏈的信息,就可以比較準(zhǔn)確地定位出泄漏代碼的位置
如果不存在泄露,換句話說,就是內(nèi)存中的對(duì)象確實(shí)都還必須存活著,那就應(yīng)當(dāng)檢查虛擬機(jī)的堆參數(shù)(-Xmx與-Xms),與機(jī)器物理內(nèi)存對(duì)比看是否還可以調(diào)大,從代碼上檢查是否存在某些對(duì)象生命周期過長、持有狀態(tài)時(shí)間過長的情況,嘗試減少程序運(yùn)行期的內(nèi)存消耗
3.2?虛擬機(jī)棧和本地方法棧溢出
對(duì)于HotSpot來說,雖然-Xoss參數(shù)(設(shè)置本地方法棧大小)存在,但實(shí)際上是無效的,棧容量只由-Xss參數(shù)設(shè)定。關(guān)于虛擬機(jī)棧和本地方法棧,在Java虛擬機(jī)規(guī)范中描述了兩種異常:
如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的最大深度,將拋出StackOverflowError
如果虛擬機(jī)在擴(kuò)展棧時(shí)無法申請(qǐng)到足夠的內(nèi)存空間,則拋出OutOfMemoryError異常
在單線程下,無論由于棧幀太大還是虛擬機(jī)棧容量太小,當(dāng)內(nèi)存無法分配的時(shí)候,虛擬機(jī)拋出的都是StackOverflowError異常
如果是多線程導(dǎo)致的內(nèi)存溢出,與棧空間是否足夠大并不存在任何聯(lián)系,這個(gè)時(shí)候每個(gè)線程的棧分配的內(nèi)存越大,反而越容易產(chǎn)生內(nèi)存溢出異常。解決的時(shí)候是在不能減少線程數(shù)或更換64為的虛擬機(jī)的情況下,就只能通過減少最大堆和減少棧容量來換取更多的線程
3.3?方法區(qū)和運(yùn)行時(shí)常量池溢出
String.intern()是一個(gè)Native方法,它的作用是:如果字符串常量池中已經(jīng)包含一個(gè)等于此String對(duì)象的字符串,則返回代表池中這個(gè)字符串的String對(duì)象;否則,將此String對(duì)象包含的字符串添加到常量池中,并且返回此String對(duì)象的引用
由于常量池分配在永久代中,可以通過-XX:PermSize和-XX:MaxPermSize限制方法區(qū)大小,從而間接限制其中常量池的容量。
Intern():
JDK1.6?intern方法會(huì)把首次遇到的字符串實(shí)例復(fù)制到永久代,返回的也是永久代中這個(gè)字符串實(shí)例的引用,而由StringBuilder創(chuàng)建的字符串實(shí)例在Java堆上,所以必然不是一個(gè)引用
JDK1.7?intern()方法的實(shí)現(xiàn)不會(huì)再復(fù)制實(shí)例,只是在常量池中記錄首次出現(xiàn)的實(shí)例引用,因此intern()返回的引用和由StringBuilder創(chuàng)建的那個(gè)字符串實(shí)例是同一個(gè)
四、垃圾收集
程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧3個(gè)區(qū)域隨線程而生,隨線程而滅,在這幾個(gè)區(qū)域內(nèi)就不需要過多考慮回收的問題,因?yàn)榉椒ńY(jié)束或者線程結(jié)束時(shí),內(nèi)存自然就跟隨著回收了
1.判斷對(duì)象存活
4.1.1?引用計(jì)數(shù)器法
給對(duì)象添加一個(gè)引用計(jì)數(shù)器,每當(dāng)由一個(gè)地方引用它時(shí),計(jì)數(shù)器值就加1;當(dāng)引用失效時(shí),計(jì)數(shù)器值就減1;任何時(shí)刻計(jì)數(shù)器為0的對(duì)象就是不可能再被使用的
4.1.2?可達(dá)性分析算法
通過一系列的成為“GC?Roots”的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過的路徑成為引用鏈,當(dāng)一個(gè)對(duì)象到GC?ROOTS沒有任何引用鏈相連時(shí),則證明此對(duì)象時(shí)不可用的
Java語言中GC?Roots的對(duì)象包括下面幾種:
1.虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象
2.方法區(qū)中類靜態(tài)屬性引用的對(duì)象
3.方法區(qū)中常量引用的對(duì)象
4.本地方法棧JNI(Native方法)引用的對(duì)象
2.引用
強(qiáng)引用就是在程序代碼之中普遍存在的,類似Object?obj?=?new?Object()這類的引用,只要強(qiáng)引用還存在,垃圾收集器永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象
軟引用用來描述一些還有用但并非必須的元素。對(duì)于它在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會(huì)把這些對(duì)象列進(jìn)回收范圍之中進(jìn)行第二次回收,如果這次回收還沒有足夠的內(nèi)存才會(huì)拋出內(nèi)存溢出異常
弱引用用來描述非必須對(duì)象的,但是它的強(qiáng)度比軟引用更弱一些,被引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生之前,當(dāng)垃圾收集器工作時(shí),無論當(dāng)前內(nèi)存是否足夠都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象
虛引用的唯一目的就是能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知
3.Finalize方法
任何一個(gè)對(duì)象的finalize()方法都只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次,如果對(duì)象面臨下一次回收,它的finalize()方法不會(huì)被再次執(zhí)行,因此第二段代碼的自救行動(dòng)失敗了
4.3.1回收方法區(qū)
永久代的垃圾收集主要回收兩部分內(nèi)容:廢棄常量和無用的類
廢棄常量:假如一個(gè)字符串a(chǎn)bc已經(jīng)進(jìn)入了常量池中,如果當(dāng)前系統(tǒng)沒有任何一個(gè)String對(duì)象abc,也就是沒有任何Stirng對(duì)象引用常量池的abc常量,也沒有其他地方引用的這個(gè)字面量,這個(gè)時(shí)候發(fā)生內(nèi)存回收這個(gè)常量就會(huì)被清理出常量池
無用的類:
1.該類所有的實(shí)例都已經(jīng)被回收,就是Java堆中不存在該類的任何實(shí)例
2.加載該類的ClassLoader已經(jīng)被回收
3.該類對(duì)用的java.lang.Class對(duì)象沒有在任何地方被引用,無法再任何地方通過反射訪問該類的方法
4.垃圾收集算法
4.4.1?標(biāo)記—清除算法
算法分為標(biāo)記和清除兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對(duì)象、
不足:一個(gè)是效率問題,標(biāo)記和清除兩個(gè)過程的效率都不高;另一個(gè)是空間問題,標(biāo)記清楚之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致以后再程序運(yùn)行過程中需要分配較大的對(duì)象時(shí),無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作
4.4.2?復(fù)制算法
他將可用內(nèi)存按照容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。這樣使得每次都是對(duì)整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收,內(nèi)存分配時(shí)也就不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動(dòng)堆頂指針,按順序分配內(nèi)存即可
不足:將內(nèi)存縮小為了原來的一半
實(shí)際中我們并不需要按照1:1比例來劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor
當(dāng)另一個(gè)Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對(duì)象時(shí),這些對(duì)象將直接通過分配擔(dān)保機(jī)制進(jìn)入老年代
4.4.3標(biāo)記整理算法
讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存
4.4.4分代收集算法
只是根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊。一般是把java堆分為新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴āT谛律校看卫占瘯r(shí)都發(fā)現(xiàn)有大批對(duì)象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集。而老年代中因?yàn)閷?duì)象存活率高、沒有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用標(biāo)記清理或者標(biāo)記整理算法來進(jìn)行回收
5.垃圾收集器
a)Serial收集器:
這個(gè)收集器是一個(gè)單線程的收集器,但它的單線程的意義不僅僅說明它會(huì)只使用一個(gè)COU或一條收集線程去完成垃圾收集工作,更重要的是它在進(jìn)行垃圾收集時(shí),必須暫停其他所有的工作線程,直到它手機(jī)結(jié)束
b)ParNew收集器:
Serial收集器的多線程版本,除了使用了多線程進(jìn)行收集之外,其余行為和Serial收集器一樣
并行:指多條垃圾收集線程并行工作,但此時(shí)用戶線程仍然處于等待狀態(tài)
并發(fā):指用戶線程與垃圾收集線程同時(shí)執(zhí)行(不一定是并行的,可能會(huì)交替執(zhí)行),用戶程序在繼續(xù)執(zhí)行,而垃圾收集程序運(yùn)行于另一個(gè)CPU上
c)Parallel?Scavenge
收集器是一個(gè)新生代收集器,它是使用復(fù)制算法的收集器,又是并行的多線程收集器。
吞吐量:就是CPU用于運(yùn)行用戶代碼的時(shí)間與CPU總消耗時(shí)間的比值。即吞吐量=運(yùn)行用戶代碼時(shí)間/(運(yùn)行用戶代碼時(shí)間+垃圾收集時(shí)間)
d)Serial?Old收集器:
是Serial收集器的老年代版本,是一個(gè)單線程收集器,使用標(biāo)記整理算法
e)Parallel?Old收集器:
Parallel?Old是Paraller?Seavenge收集器的老年代版本,使用多線程和標(biāo)記整理算法
f)CMS收集器:
CMS收集器是基于標(biāo)記清除算法實(shí)現(xiàn)的,整個(gè)過程分為4個(gè)步驟:
1.初始標(biāo)記2.并發(fā)標(biāo)記3.重新標(biāo)記4.并發(fā)清除
優(yōu)點(diǎn):并發(fā)收集、低停頓
缺點(diǎn):
1.CMS收集器對(duì)CPU資源非常敏感,CMS默認(rèn)啟動(dòng)的回收線程數(shù)是(CPU數(shù)量+3)/4,
2.CMS收集器無法處理浮動(dòng)垃圾,可能出現(xiàn)Failure失敗而導(dǎo)致一次Full?G場(chǎng)地產(chǎn)生
3.CMS是基于標(biāo)記清除算法實(shí)現(xiàn)的
g)G1收集器:
它是一款面向服務(wù)器應(yīng)用的垃圾收集器
1.并行與并發(fā):利用多CPU縮短STOP-The-World停頓的時(shí)間
2.分代收集
3.空間整合:不會(huì)產(chǎn)生內(nèi)存碎片
4.可預(yù)測(cè)的停頓
運(yùn)作方式:初始標(biāo)記,并發(fā)標(biāo)記,最終標(biāo)記,篩選回收
6.內(nèi)存分配與回收策略
4.6.1對(duì)象優(yōu)先在Eden分配:
大多數(shù)情況對(duì)象在新生代Eden區(qū)分配,當(dāng)Eden區(qū)沒有足夠空間進(jìn)行分配時(shí),虛擬機(jī)將發(fā)起一次Minor?GC
4.6.2大對(duì)象直接進(jìn)入老年代:
所謂大對(duì)象就是指需要大量連續(xù)內(nèi)存空間的Java對(duì)象,最典型的大對(duì)象就是那種很長的字符串以及數(shù)組。這樣做的目的是避免Eden區(qū)及兩個(gè)Servivor之間發(fā)生大量的內(nèi)存復(fù)制
4.6.3長期存活的對(duì)象將進(jìn)入老年代
如果對(duì)象在Eden區(qū)出生并且盡力過一次Minor?GC后仍然存活,并且能夠被Servivor容納,將被移動(dòng)到Servivor空間中,并且把對(duì)象年齡設(shè)置成為1.對(duì)象在Servivor區(qū)中每熬過一次Minor?GC,年齡就增加1歲,當(dāng)它的年齡增加到一定程度(默認(rèn)15歲),就將會(huì)被晉級(jí)到老年代中
4.6.4動(dòng)態(tài)對(duì)象年齡判定
為了更好地適應(yīng)不同程序的內(nèi)存狀況,虛擬機(jī)并不是永遠(yuǎn)地要求對(duì)象的年齡必須達(dá)到了MaxTenuringThreshold才能晉級(jí)到老年代,如果在Servivor空間中相同年齡所有對(duì)象的大小總和大于Survivor空間的一半,年齡大于或等于該年齡的對(duì)象就可以直接進(jìn)入到老年代,無須登到MaxTenuringThreshold中要求的年齡
4.6.4空間分配擔(dān)保:
在發(fā)生Minor?GC之前,虛擬機(jī)會(huì)檢查老年代最大可用的連續(xù)空間是否大于新生代所有對(duì)象總空間,如果這個(gè)條件成立,那么Minor?DC可以確保是安全的。如果不成立,則虛擬機(jī)會(huì)查看HandlePromotionFailure設(shè)置值是否允許擔(dān)保失敗。如果允許那么會(huì)繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于晉級(jí)到老年代對(duì)象的平均大小,如果大于,將嘗試進(jìn)行一次Minor?GC,盡管這次MinorGC是有風(fēng)險(xiǎn)的:如果小于,或者HandlePromotionFailure設(shè)置不允許冒險(xiǎn),那這時(shí)也要改為進(jìn)行一次Full?GC
五、虛擬機(jī)類加載機(jī)制
虛擬機(jī)吧描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型,這就是虛擬機(jī)的類加載機(jī)制
在Java語言里面,類型的加載。連接和初始化過程都是在程序運(yùn)行期間完成的
5.1類加載的時(shí)機(jī)
類被加載到虛擬機(jī)內(nèi)存中開始,到卸載為止,整個(gè)生命周期包括:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載7個(gè)階段
加載、驗(yàn)證、準(zhǔn)備、初始化和卸載這5個(gè)階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以再初始化階段之后再開始,這個(gè)是為了支持Java語言運(yùn)行時(shí)綁定(也成為動(dòng)態(tài)綁定或晚期綁定)
虛擬機(jī)規(guī)范規(guī)定有且只有5種情況必須立即對(duì)類進(jìn)行初始化:
1.遇到new、getstatic、putstatic或invokestatic這4條字節(jié)碼指令時(shí),如果類沒有進(jìn)行過初始化,則需要觸發(fā)其初始化。生成這4條指令的最常見的Java代碼場(chǎng)景是:使用new關(guān)鍵字實(shí)例化對(duì)象的時(shí)候、讀取或設(shè)置一個(gè)類的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候,以及調(diào)用一個(gè)類的靜態(tài)方法的時(shí)候
2.使用java.lang.reflect包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化
3.當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化
4.當(dāng)虛擬機(jī)啟動(dòng)時(shí)候,用戶需要指定一個(gè)要執(zhí)行的主類(包含main()方法的那個(gè)類),虛擬機(jī)會(huì)先初始化這個(gè)主類
5.當(dāng)使用JDK1.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ā)其初始化
被動(dòng)引用:
1.通過子類引用父類的靜態(tài)字段,不會(huì)導(dǎo)致子類初始化
2.通過數(shù)組定義來引用類,不會(huì)觸發(fā)此類的初始化
3.常量在編譯階段會(huì)存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用到定義常量的類,因此不會(huì)觸發(fā)定義常量的類的初始化
接口的初始化:接口在初始化時(shí),并不要求其父接口全部完成類初始化,只有在正整使用到父接口的時(shí)候(如引用接口中定義的常量)才會(huì)初始化
5.2類加載的過程
5.2.1加載
1)通過一個(gè)類的全限定名類獲取定義此類的二進(jìn)制字節(jié)流
2)將這字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
3)在內(nèi)存中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口
怎么獲取二進(jìn)制字節(jié)流?
1)從ZIP包中讀取,這很常見,最終成為日后JAR、EAR、WAR格式的基礎(chǔ)
2)從網(wǎng)絡(luò)中獲取,這種場(chǎng)景最典型的應(yīng)用就是Applet
3)運(yùn)行時(shí)計(jì)算生成,這種常見使用得最多的就是動(dòng)態(tài)代理技術(shù)
4)由其他文件生成,典型場(chǎng)景就是JSP應(yīng)用
5)從數(shù)據(jù)庫中讀取,這種場(chǎng)景相對(duì)少一些(中間件服務(wù)器)
數(shù)組類本身不通過類加載器創(chuàng)建,它是由Java虛擬機(jī)直接創(chuàng)建的
數(shù)組類的創(chuàng)建過程遵循以下規(guī)則:
1)如果數(shù)組的組件類型(指的是數(shù)組去掉一個(gè)維度的類型)是引用類型,那就遞歸采用上面的加載過程去加載這個(gè)組件類型,數(shù)組C將在加載該組件類型的類加載器的類名稱空間上被標(biāo)識(shí)
2)如果數(shù)組的組件類型不是引用類型(列如int[]組數(shù)),Java虛擬機(jī)將會(huì)把數(shù)組C標(biāo)識(shí)為與引導(dǎo)類加載器關(guān)聯(lián)
3)數(shù)組類的可見性與它的組件類型的可見性一致,如果組件類型不是引用類型,那數(shù)組類的可見性將默認(rèn)為public
5.2.2驗(yàn)證
驗(yàn)證階段會(huì)完成下面4個(gè)階段的檢驗(yàn)動(dòng)作:文件格式驗(yàn)證,元數(shù)據(jù)驗(yàn)證,字節(jié)碼驗(yàn)證,符號(hào)引用驗(yàn)證
1.文件格式驗(yàn)證
第一階段要驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理。這一階段可能包括:
1.是否以魔數(shù)oxCAFEBABE開頭
2.主、次版本號(hào)是否在當(dāng)前虛擬機(jī)處理范圍之內(nèi)
3.常量池的常量中是否有不被支持的常量類型(檢查常量tag標(biāo)志)
4.指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量
5.CONSTANT_Itf8_info型的常量中是否有不符合UTF8編碼的數(shù)據(jù)
6.Class文件中各個(gè)部分及文件本身是否有被刪除的或附加的其他信息
這個(gè)階段的驗(yàn)證時(shí)基于二進(jìn)制字節(jié)流進(jìn)行的,只有通過類這個(gè)階段的驗(yàn)證后,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)進(jìn)行存儲(chǔ),所以后面的3個(gè)驗(yàn)證階段全部是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)進(jìn)行的,不會(huì)再直接操作字節(jié)流
2.元數(shù)據(jù)驗(yàn)證
1.這個(gè)類是否有父類(除了java.lang.Object之外,所有的類都應(yīng)當(dāng)有父類)
2.這個(gè)類的父類是否繼承了不允許被繼承的類(被final修飾的類)
3.如果這個(gè)類不是抽象類,是否實(shí)現(xiàn)類其父類或接口之中要求實(shí)現(xiàn)的所有方法
4.類中的字段、方法是否與父類產(chǎn)生矛盾(列如覆蓋類父類的final字段,或者出現(xiàn)不符合規(guī)則的方法重載,列如方法參數(shù)都一致,但返回值類型卻不同等)
第二階段的主要目的是對(duì)類元數(shù)據(jù)信息進(jìn)行語義校驗(yàn),保證不存在不符合Java語言規(guī)范的元數(shù)據(jù)信息
3.字節(jié)碼驗(yàn)證
第三階段是整個(gè)驗(yàn)證過程中最復(fù)雜的一個(gè)階段,主要目的似乎通過數(shù)據(jù)流和控制流分析,確定程序語言是合法的、符合邏輯的。在第二階段對(duì)元數(shù)據(jù)信息中的數(shù)據(jù)類型做完校驗(yàn)后,這個(gè)階段將對(duì)類的方法體進(jìn)行校驗(yàn)分析,保證被校驗(yàn)類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的事件。
1.保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作,列如,列如在操作數(shù)棧放置類一個(gè)int類型的數(shù)據(jù),使用時(shí)卻按long類型來加載入本地變量表中
2.保證跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上
3.保證方法體中的類型轉(zhuǎn)換時(shí)有效的,列如可以把一個(gè)子類對(duì)象賦值給父類數(shù)據(jù)類型,這個(gè)是安全的,但是吧父類對(duì)象賦值給子類數(shù)據(jù)類型,甚至把對(duì)象賦值給與它毫無繼承關(guān)系、完全不相干的一個(gè)數(shù)據(jù)類型,則是危險(xiǎn)和不合法的
4.符號(hào)引用驗(yàn)證
發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候,這個(gè)轉(zhuǎn)化動(dòng)作將在連接的第三階段——解析階段中發(fā)生。
1.符號(hào)引用中通過字符串描述的全限定名是否能找到相對(duì)應(yīng)的類
2.在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段
3.符號(hào)引用中的類、字段、方法的訪問性是否可被當(dāng)前類訪問
對(duì)于虛擬機(jī)的類加載機(jī)制來說,驗(yàn)證階段是非常重要的,但是不一定必要(因?yàn)閷?duì)程序運(yùn)行期沒有影響)的階段。如果全部代碼都已經(jīng)被反復(fù)使用和驗(yàn)證過,那么在實(shí)施階段就可以考慮使用Xverify:none參數(shù)來關(guān)閉大部分的類驗(yàn)證措施,以縮短虛擬機(jī)類加載的時(shí)間
5.2.3準(zhǔn)備
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量都在方法區(qū)中進(jìn)行分配。這個(gè)時(shí)候進(jìn)行內(nèi)存分配的僅包括類變量(被static修飾的變量),而不包括實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在Java堆中。其次,這里說的初始值通常下是數(shù)據(jù)類型的零值。
假設(shè)public?static?int?value?=?123;
那變量value在準(zhǔn)備階段過后的初始值為0而不是123,因?yàn)檫@時(shí)候尚未開始執(zhí)行任何Java方法,而把value賦值為123的putstatic指令是程序被編譯后,存放于類構(gòu)造器()方法之中,所以把value賦值為123的動(dòng)作將在初始化階段才會(huì)執(zhí)行,但是如果使用final修飾,則在這個(gè)階段其初始值設(shè)置為123
5.2.4解析
解析階段是虛擬機(jī)將常量池內(nèi)符號(hào)引用替換為直接引用的過
5.2.5初始化
類的初始化階段是類加載過程的最后一步,前面的類加載過程中,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器參與之外,其余動(dòng)作完全由虛擬機(jī)主導(dǎo)和控制。到了初始化階段,才正真開始執(zhí)行類中定義的Java程序代碼(或者說是字節(jié)碼)
5.3類的加載器
5.3.1雙親委派模型:
只存在兩種不同的類加載器:啟動(dòng)類加載器(Bootstrap?ClassLoader),使用C++實(shí)現(xiàn),是虛擬機(jī)自身的一部分。另一種是所有其他的類加載器,使用JAVA實(shí)現(xiàn),獨(dú)立于JVM,并且全部繼承自抽象類java.lang.ClassLoader.
啟動(dòng)類加載器(Bootstrap?ClassLoader),負(fù)責(zé)將存放在\lib目錄中的,或者被-Xbootclasspath參數(shù)所制定的路徑中的,并且是JVM識(shí)別的(僅按照文件名識(shí)別,如rt.jar,如果名字不符合,即使放在lib目錄中也不會(huì)被加載),加載到虛擬機(jī)內(nèi)存中,啟動(dòng)類加載器無法被JAVA程序直接引用。
擴(kuò)展類加載器,由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn),負(fù)責(zé)加載\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫,開發(fā)者可以直接使用擴(kuò)展類加載器。
應(yīng)用程序類加載器(Application?ClassLoader),由sun.misc.Launcher$AppClassLoader來實(shí)現(xiàn)。由于這個(gè)類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般稱它為系統(tǒng)類加載器。負(fù)責(zé)加載用戶類路徑(ClassPath)上所指定的類庫,開發(fā)者可以直接使用這個(gè)類加載器,如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個(gè)就是程序中默認(rèn)的類加載器。
這張圖表示類加載器的雙親委派模型(Parents?Delegation?model).雙親委派模型要求除了頂層的啟動(dòng)加載類外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器。,這里類加載器之間的父子關(guān)系一般不會(huì)以繼承的關(guān)系來實(shí)現(xiàn),而是使用組合關(guān)系來復(fù)用父類加載器的代碼。
5.3.2雙親委派模型的工作過程是:
如果一個(gè)類加載器收到了類加載的請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類,而是把這個(gè)請(qǐng)求委派給父類加載器去完成,每一個(gè)層次的類加載器都是如此,因此所有的加載請(qǐng)求最終都是應(yīng)該傳送到頂層的啟動(dòng)類加載器中,只有當(dāng)父類加載器反饋?zhàn)约簾o法完成這個(gè)加載請(qǐng)求(它的搜索范圍中沒有找到所需的類)時(shí),子加載器才會(huì)嘗試自己去加載。
5.3.3這樣做的好處就是:
Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系。例如類java.lang.Object,它存放在rt.jar中,無論哪一個(gè)類加載器要加載這個(gè)類,最終都是委派給處于模型最頂端的啟動(dòng)類加載器進(jìn)行加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個(gè)類。相反,如果沒有使用雙親委派模型,由各個(gè)類加載器自行去加載的話,如果用戶自己編寫了一個(gè)稱為java.lang.object的類,并放在程序的ClassPath中,那系統(tǒng)中將會(huì)出現(xiàn)多個(gè)不同的Object類,Java類型體系中最基礎(chǔ)的行為也就無法保證,應(yīng)用程序也將會(huì)變得一片混亂
就是保證某個(gè)范圍的類一定是被某個(gè)類加載器所加載的,這就保證在程序中同一個(gè)類不會(huì)被不同的類加載器加載。這樣做的一個(gè)主要的考量,就是從安全層面上,杜絕通過使用和JRE相同的類名冒充現(xiàn)有JRE的類達(dá)到替換的攻擊方式
六、Java內(nèi)存模型與線程
6.1內(nèi)存間的交互操作
關(guān)于主內(nèi)存與工作內(nèi)存之間的具體交互協(xié)議,即一個(gè)變量如何從主內(nèi)存拷貝到工作內(nèi)存、如何從工作內(nèi)存同步到主內(nèi)存之間的實(shí)現(xiàn)細(xì)節(jié),Java內(nèi)存模型定義了以下八種操作來完成:
lock(鎖定):作用于主內(nèi)存的變量,把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占狀態(tài)。
unlock(解鎖):作用于主內(nèi)存變量,把一個(gè)處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
read(讀取):作用于主內(nèi)存變量,把一個(gè)變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動(dòng)作使用
load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
use(使用):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作。
assign(賦值):作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作。
store(存儲(chǔ)):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的write的操作。
write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中一個(gè)變量的值傳送到主內(nèi)存的變量中。
如果要把一個(gè)變量從主內(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之間是可以插入其他指令的,如對(duì)主內(nèi)存中的變量a、b進(jìn)行訪問時(shí),可能的順序是read?a,read?b,load?b,load?a。
Java內(nèi)存模型還規(guī)定了在執(zhí)行上述八種基本操作時(shí),必須滿足如下規(guī)則:
不允許read和load、store和write操作之一單獨(dú)出現(xiàn)
不允許一個(gè)線程丟棄它的最近assign的操作,即變量在工作內(nèi)存中改變了之后必須同步到主內(nèi)存中。
不允許一個(gè)線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中。
一個(gè)新的變量只能在主內(nèi)存中誕生,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load或assign)的變量。即就是對(duì)一個(gè)變量實(shí)施use和store操作之前,必須先執(zhí)行過了assign和load操作。
一個(gè)變量在同一時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會(huì)被解鎖。lock和unlock必須成對(duì)出現(xiàn)
如果對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作,將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量前需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值
如果一個(gè)變量事先沒有被lock操作鎖定,則不允許對(duì)它執(zhí)行unlock操作;也不允許去unlock一個(gè)被其他線程鎖定的變量。
對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步到主內(nèi)存中(執(zhí)行store和write操作)。
6.2重排序
在執(zhí)行程序時(shí)為了提高性能,編譯器和處理器經(jīng)常會(huì)對(duì)指令進(jìn)行重排序。重排序分成三種類型:
1.編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義放入前提下,可以重新安排語句的執(zhí)行順序。
2.指令級(jí)并行的重排序。現(xiàn)代處理器采用了指令級(jí)并行技術(shù)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。
3.內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。
從Java源代碼到最終實(shí)際執(zhí)行的指令序列,會(huì)經(jīng)過下面三種重排序:
為了保證內(nèi)存的可見性,Java編譯器在生成指令序列的適當(dāng)位置會(huì)插入內(nèi)存屏障指令來禁止特定類型的處理器重排序。Java內(nèi)存模型把內(nèi)存屏障分為LoadLoad、LoadStore、StoreLoad和StoreStore四種:
6.3對(duì)于volatile型變量的特殊規(guī)則
當(dāng)一個(gè)變量定義為volatile之后,它將具備兩種特性:
第一:保證此變量對(duì)所有線程的可見性,這里的可見性是指當(dāng)一條線程修改了這個(gè)變量的值,新值對(duì)于其他線程來說是可以立即得知的。普通變量的值在線程間傳遞需要通過主內(nèi)存來完成
由于valatile只能保證可見性,在不符合一下兩條規(guī)則的運(yùn)算場(chǎng)景中,我們?nèi)砸ㄟ^加鎖來保證原子性
1.運(yùn)算結(jié)果并不依賴變量的當(dāng)前值,或者能夠確保只有單一的線程修改變量的值。
2.變量不需要與其他的狀態(tài)變量共同參與不變約束
第二:禁止指令重排序,普通的變量僅僅會(huì)保證在該方法的執(zhí)行過程中所有依賴賦值結(jié)果的地方都能獲取到正確的結(jié)果,而不能保證變量賦值操作的順序與程序代碼中執(zhí)行順序一致,這個(gè)就是所謂的線程內(nèi)表現(xiàn)為串行的語義
Java內(nèi)存模型中對(duì)volatile變量定義的特殊規(guī)則。假定T表示一個(gè)線程,V和W分別表示兩個(gè)volatile變量,那么在進(jìn)行read、load、use、assign、store、write操作時(shí)需要滿足如下的規(guī)則:
1.只有當(dāng)線程T對(duì)變量V執(zhí)行的前一個(gè)動(dòng)作是load的時(shí)候,線程T才能對(duì)變量V執(zhí)行use動(dòng)作;并且,只有當(dāng)線程T對(duì)變量V執(zhí)行的后一個(gè)動(dòng)作是use的時(shí)候,線程T才能對(duì)變量V執(zhí)行l(wèi)oad操作。線程T對(duì)變量V的use操作可以認(rèn)為是與線程T對(duì)變量V的load和read操作相關(guān)聯(lián)的,必須一起連續(xù)出現(xiàn)。這條規(guī)則要求在工作內(nèi)存中,每次使用變量V之前都必須先從主內(nèi)存刷新最新值,用于保證能看到其它線程對(duì)變量V所作的修改后的值。
2.只有當(dāng)線程T對(duì)變量V執(zhí)行的前一個(gè)動(dòng)是assign的時(shí)候,線程T才能對(duì)變量V執(zhí)行store操作;并且,只有當(dāng)線程T對(duì)變量V執(zhí)行的后一個(gè)動(dòng)作是store操作的時(shí)候,線程T才能對(duì)變量V執(zhí)行assign操作。線程T對(duì)變量V的assign操作可以認(rèn)為是與線程T對(duì)變量V的store和write操作相關(guān)聯(lián)的,必須一起連續(xù)出現(xiàn)。這一條規(guī)則要求在工作內(nèi)存中,每次修改V后都必須立即同步回主內(nèi)存中,用于保證其它線程可以看到自己對(duì)變量V的修改。
3.假定操作A是線程T對(duì)變量V實(shí)施的use或assign動(dòng)作,假定操作F是操作A相關(guān)聯(lián)的load或store操作,假定操作P是與操作F相應(yīng)的對(duì)變量V的read或write操作;類型地,假定動(dòng)作B是線程T對(duì)變量W實(shí)施的use或assign動(dòng)作,假定操作G是操作B相關(guān)聯(lián)的load或store操作,假定操作Q是與操作G相應(yīng)的對(duì)變量V的read或write操作。如果A先于B,那么P先于Q。這條規(guī)則要求valitile修改的變量不會(huì)被指令重排序優(yōu)化,保證代碼的執(zhí)行順序與程序的順序相同。
6.4對(duì)于long和double型變量的特殊規(guī)則
Java模型要求lock、unlock、read、load、assign、use、store、write這8個(gè)操作都具有原子性,但是對(duì)于64為的數(shù)據(jù)類型(long和double),在模型中特別定義了一條相對(duì)寬松的規(guī)定:允許虛擬機(jī)將沒有被volatile修飾的64位數(shù)據(jù)的讀寫操作分為兩次32為的操作來進(jìn)行,即允許虛擬機(jī)實(shí)現(xiàn)選擇可以不保證64位數(shù)據(jù)類型的load、store、read和write這4個(gè)操作的原子性
6.5原子性、可見性和有序性
原子性:即一個(gè)操作或者多個(gè)操作?要么全部執(zhí)行并且執(zhí)行的過程不會(huì)被任何因素打斷,要么就都不執(zhí)行。Java內(nèi)存模型是通過在變量修改后將新值同步會(huì)主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方式來實(shí)現(xiàn)可見性,valatile特殊規(guī)則保障新值可以立即同步到祝內(nèi)存中。Synchronized是在對(duì)一個(gè)變量執(zhí)行unlock之前,必須把變量同步回主內(nèi)存中(執(zhí)行store、write操作)。被final修飾的字段在構(gòu)造器中一旦初始化完成,并且構(gòu)造器沒有吧this的引用傳遞出去,那在其他線程中就能看見final字段的值
可見性:可見性是指當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值。
有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
6.6先行發(fā)生原則
這些先行發(fā)生關(guān)系無須任何同步就已經(jīng)存在,如果不再此列就不能保障順序性,虛擬機(jī)就可以對(duì)它們?nèi)我獾剡M(jìn)行重排序
1.程序次序規(guī)則:在一個(gè)線程內(nèi),按照程序代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。準(zhǔn)確的說,應(yīng)該是控制順序而不是程序代碼順序,因?yàn)橐紤]分支。循環(huán)等結(jié)構(gòu)
2.管程鎖定規(guī)則:一個(gè)unlock操作先行發(fā)生于后面對(duì)同一個(gè)鎖的lock操作。這里必須強(qiáng)調(diào)的是同一個(gè)鎖,而后面的是指時(shí)間上的先后順序
3.Volatile變量規(guī)則:對(duì)一個(gè)volatile變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作,這里的后面同樣是指時(shí)間上的先后順序
4.線程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法先行發(fā)生于此線程的每一個(gè)動(dòng)作
5.線程終止規(guī)則:線程中的所有操作都先行發(fā)生于對(duì)此線程的終止檢測(cè),我們可以通過Thread.joke()方法結(jié)束、ThradisAlive()的返回值等手段檢測(cè)到線程已經(jīng)終止執(zhí)行
6.線程中斷規(guī)則:對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷時(shí)間的發(fā)生,可以通過Thread.interrupted()方法檢測(cè)到是否有中斷發(fā)生
7.對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成(構(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í)行時(shí)間由線程本身控制
搶占式調(diào)度:線程的執(zhí)行時(shí)間由系統(tǒng)來分配
6.8狀態(tài)轉(zhuǎn)換
1.新建
2.運(yùn)行:可能正在執(zhí)行。可能正在等待CPU為它分配執(zhí)行時(shí)間
3.無限期等待:不會(huì)被分配CUP執(zhí)行時(shí)間,它們要等待被其他線程顯式喚醒
4.限期等待:不會(huì)被分配CUP執(zhí)行時(shí)間,它們無須等待被其他線程顯式喚醒,一定時(shí)間會(huì)由系統(tǒng)自動(dòng)喚醒
5.阻塞:阻塞狀態(tài)在等待這獲取到一個(gè)排他鎖,這個(gè)時(shí)間將在另一個(gè)線程放棄這個(gè)鎖的時(shí)候發(fā)生;等待狀態(tài)就是在等待一段時(shí)間,或者喚醒動(dòng)作的發(fā)生
6.結(jié)束:已終止線程的線程狀態(tài),線程已經(jīng)結(jié)束執(zhí)行
七、線程安全
1、不可變:不可變的對(duì)象一定是線程安全的、無論是對(duì)象的方法實(shí)現(xiàn)還是方法的調(diào)用者,都不需要再采取任何的線程安全保障。例如:把對(duì)象中帶有狀態(tài)的變量都聲明為final,這樣在構(gòu)造函數(shù)結(jié)束之后,它就是不可變的。
2、絕對(duì)線程安全
3、相對(duì)線程安全:相對(duì)的線程安全就是我們通常意義上所講的線程安全,它需要保證對(duì)這個(gè)對(duì)象單獨(dú)的操作是線程安全的,我們?cè)谡{(diào)用的時(shí)候不需要做額外的保障措施,但是對(duì)于一些特定順序的連續(xù)調(diào)用,就可能需要在調(diào)用端使用額外的同步手段來保證調(diào)用的正確性
4、線程兼容:對(duì)象本身并不是線程安全的,但是可以通過在調(diào)用端正確地使用同步手段來保證對(duì)象在并發(fā)環(huán)境中可以安全使用
5、線程對(duì)立:是指無論調(diào)用端是否采取了同步措施,都無法在多線程環(huán)境中并發(fā)使用的代碼
7.1線程安全的實(shí)現(xiàn)方法
1.互斥同步:
同步是指在多個(gè)線程并發(fā)訪問共享數(shù)據(jù)時(shí),保證共享數(shù)據(jù)在同一個(gè)時(shí)刻只被一個(gè)(或者是一些,使用信號(hào)量的時(shí)候)線程使用。而互斥是實(shí)現(xiàn)同步的一種手段,臨界區(qū)、互斥量和信號(hào)量都是主要的互斥實(shí)現(xiàn)方式。互斥是因,同步是果:互斥是方法,同步是目的
在Java中,最基本的互斥同步手段就是synchronized關(guān)鍵字,它經(jīng)過編譯之后,會(huì)在同步塊的前后分別形成monitorenter和monitorexit這兩個(gè)字節(jié)碼指令,這兩個(gè)字節(jié)碼都需要一個(gè)reference類型的參數(shù)來指明要鎖定和解鎖的對(duì)象。如果Java程序中的synchronized明確指定了對(duì)象參數(shù),那就是這個(gè)對(duì)象的reference;如果沒有指明,那就根據(jù)synchronized修飾的是實(shí)例方法還是類方法,去取對(duì)應(yīng)的對(duì)象實(shí)例或Class對(duì)象來作為鎖對(duì)象。在執(zhí)行monitorenter指令時(shí),首先要嘗試獲取對(duì)象的鎖。如果這個(gè)對(duì)象沒有被鎖定,或者當(dāng)前線程已經(jīng)擁有了那個(gè)對(duì)象的鎖,把鎖的計(jì)數(shù)器加1,對(duì)應(yīng)的在執(zhí)行monitorexit指令時(shí)會(huì)將鎖計(jì)數(shù)器減1,當(dāng)計(jì)數(shù)器為0時(shí),鎖就被釋放。如果獲取對(duì)象鎖失敗,哪當(dāng)前線程就要阻塞等待,直到對(duì)象鎖被另外一個(gè)線程釋放為止
Synchronized,ReentrantLock增加了一些高級(jí)功能
1.等待可中斷:是指當(dāng)持有鎖的線程長期不釋放鎖的時(shí)候,正在等待的線程可以選擇放棄等待,改為處理其他事情,可中斷特性對(duì)處理執(zhí)行時(shí)間非常長的同步塊很有幫助
2.公平鎖:是指多個(gè)線程在等待同一個(gè)鎖時(shí),必須按照申請(qǐng)鎖的時(shí)間順序來依次獲得鎖;非公平鎖則不能保證這一點(diǎn),在鎖被釋放時(shí),任何一個(gè)等待鎖的線程都有機(jī)會(huì)獲得鎖。Synchronized中的鎖是非公平的,ReentrantLock默認(rèn)情況下也是非公平的,但可以通過帶布爾值的構(gòu)造函數(shù)要求使用公平鎖
3.鎖綁定多個(gè)條件是指一個(gè)ReentrantLock對(duì)象可以同時(shí)綁定多個(gè)Condition對(duì)象,而在synchronized中,鎖對(duì)象的wait()和notify()或notifyAll()方法可以實(shí)現(xiàn)一個(gè)隱含的條件,如果要和多余一個(gè)的條件關(guān)聯(lián)的時(shí)候,就不得不額外地添加一個(gè)鎖,而ReentrantLock則無須這樣做,只需要多次調(diào)用newCondition方法即可
2.非阻塞同步
3.無同步方案
可重入代碼:也叫純代碼,可以在代碼執(zhí)行的任何時(shí)刻中斷它,轉(zhuǎn)而去執(zhí)行另外一段代碼(包括遞歸調(diào)用它本身)而在控制權(quán)返回后,原來的程序不會(huì)出現(xiàn)任何錯(cuò)誤。所有的可重入代碼都是線程安全的,但是并非所有的線程安全的代碼都是可重入的。
判斷一個(gè)代碼是否具備可重入性:如果一個(gè)方法,它的返回結(jié)果是可預(yù)測(cè)的,只要輸入了相同的數(shù)據(jù),就都能返回相同的結(jié)果,那它就滿足可重入性的要求,當(dāng)然也就是線程安全的
線程本地存儲(chǔ):如果一段代碼中所需要的數(shù)據(jù)必須與其他代碼共享,那就看看這些共享數(shù)據(jù)的代碼是否能保證在同一個(gè)線程中執(zhí)行?如果能保障,我們就可以把共享數(shù)據(jù)的可見范圍限制在同一個(gè)線程之內(nèi),這樣,無須同步也能保證線程之間不出現(xiàn)數(shù)據(jù)爭用的問題
7.2鎖優(yōu)化
適應(yīng)性自旋、鎖消除、鎖粗化、輕量級(jí)鎖和偏向鎖
7.2.1自旋鎖與自適應(yīng)自旋
自旋鎖:如果物理機(jī)器上有一個(gè)以上的處理器,能讓兩個(gè)或以上的線程同時(shí)并行執(zhí)行,我們就可以讓后面請(qǐng)求鎖的那個(gè)線程稍等一下,但不放棄處理器的執(zhí)行時(shí)間,看看持有鎖的線程是否很快就會(huì)釋放鎖。為了讓線程等待,我們只需讓線程執(zhí)行一個(gè)忙循環(huán)(自旋),這項(xiàng)技術(shù)就是所謂的自旋鎖
自適應(yīng)自旋轉(zhuǎn):是由前一次在同一個(gè)鎖對(duì)象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運(yùn)行中,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也很有可能再次成功,進(jìn)而它將允許自旋等待持續(xù)相對(duì)更長的時(shí)間。如果對(duì)于某個(gè)鎖,自旋很少成功獲得過,那在以后要獲取這個(gè)鎖時(shí)將可能省略掉自過程,以避免浪費(fèi)處理器資源。
7.2.2鎖消除
鎖消除是指虛擬機(jī)即時(shí)編輯器在運(yùn)行時(shí),對(duì)一些代碼上要求同步,但是被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭的鎖進(jìn)行消除。如果在一段代碼中。推上的所有數(shù)據(jù)都不會(huì)逃逸出去從而被其他線程訪問到,那就可以把它們當(dāng)作棧上數(shù)據(jù)對(duì)待,認(rèn)為它們是線程私有的,同步加鎖自然就無須進(jìn)行
7.2.3鎖粗化
如果虛擬機(jī)檢測(cè)到有一串零碎的操作都是對(duì)同一對(duì)象的加鎖,將會(huì)把加鎖同步的范圍擴(kuò)展(粗化)到整個(gè)操作序列的外部
7.2.4輕量級(jí)鎖
7.2.5偏向鎖
它的目的是消除無競(jìng)爭情況下的同步原語,進(jìn)一步提高程序的運(yùn)行性能。如果輕量級(jí)鎖是在無競(jìng)爭的情況下使用CAS操作去消除同步使用的互斥量,那偏向鎖就是在無競(jìng)爭的情況下把這個(gè)同步都消除掉,CAS操作都不做了
如果在接下倆的執(zhí)行過程中,該鎖沒有被其他線程獲取,則持有偏向鎖的線程將永遠(yuǎn)不需要在進(jìn)行同步
八、逃逸分析
逃逸分析的基本行為就是分析對(duì)象動(dòng)態(tài)作用域:當(dāng)一個(gè)對(duì)象在方法中被定義后,它可能被外部方法所引用,例如作為調(diào)用參數(shù)傳遞到其他方法中,成為方法逃逸。甚至還可能被外部線程訪問到,比如賦值給類變量或可以在其他線程中訪問的實(shí)例變量,稱為線程逃逸
如果一個(gè)對(duì)象不會(huì)逃逸到方法或線程之外,也就是別的方法或線程無法通過任何途徑訪問到這個(gè)對(duì)象,則可能為這個(gè)變量進(jìn)行一些高效的優(yōu)化
棧上分配:如果確定一個(gè)對(duì)象不會(huì)逃逸出方法外,那讓這個(gè)對(duì)象在棧上分配內(nèi)存將會(huì)是一個(gè)不錯(cuò)的注意,對(duì)象所占用的內(nèi)存空間就可以隨棧幀出棧而銷毀。如果能使用棧上分配,那大量的對(duì)象就隨著方法的結(jié)束而銷毀了,垃圾收集系統(tǒng)的壓力將會(huì)小很多
同步消除:如果確定一個(gè)變量不會(huì)逃逸出線程,無法被其他線程訪問,那這個(gè)變量的讀寫肯定就不會(huì)有競(jìng)爭,對(duì)這個(gè)變量實(shí)施的同步措施也就可以消除掉
標(biāo)量替換:標(biāo)量就是指一個(gè)數(shù)據(jù)無法在分解成更小的數(shù)據(jù)表示了,int、long等及refrence類型等都不能在進(jìn)一步分解,它們稱為標(biāo)量。
如果一個(gè)數(shù)據(jù)可以繼續(xù)分解,就稱為聚合量,Java中的對(duì)象就是最典型的聚合量
如果一個(gè)對(duì)象不會(huì)被外部訪問,并且這個(gè)對(duì)象可以被拆散的化,那程序正整執(zhí)行的時(shí)候?qū)⒖赡懿粍?chuàng)建這個(gè)對(duì)象,而改為直接創(chuàng)建它的若干個(gè)被這個(gè)方法使用到的成員變量來代替