一绞愚、運(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位長(zhǎng)度的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語(yǔ)言并不要求常量一定只有編輯期才能產(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ò)瞻润,那就沒辦法簡(jiǎn)單地進(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ì)象生命周期過長(zhǎng)覆旭、持有狀態(tài)時(shí)間過長(zhǎng)的情況退子,嘗試減少程序運(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ī)的情況下,就只能通過減少最大堆和減少棧容量來?yè)Q取更多的線程
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ì)再?gòu)?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語(yǔ)言中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ì)象就是那種很長(zhǎng)的字符串以及數(shù)組珊搀。這樣做的目的是避免Eden區(qū)及兩個(gè)Servivor之間發(fā)生大量的內(nèi)存復(fù)制
4.6.3長(zhǎng)期存活的對(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ǔ)言里面了袁,類型的加載。連接和初始化過程都是在程序運(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ǔ)言運(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)語(yǔ)言支持時(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ù)庫(kù)中讀取忆肾,這種場(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ǔ)義校驗(yàn)刹帕,保證不存在不符合Java語(yǔ)言規(guī)范的元數(shù)據(jù)信息
3.字節(jié)碼驗(yàn)證
第三階段是整個(gè)驗(yàn)證過程中最復(fù)雜的一個(gè)階段,主要目的似乎通過數(shù)據(jù)流和控制流分析谎替,確定程序語(yǔ)言是合法的偷溺、符合邏輯的。在第二階段對(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.在指定類中是否存在符合方法的字段描述符以及簡(jiǎn)單名稱所描述的方法和字段
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雙親委派模型:
只存在兩種不同的類加載器:?jiǎn)?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)變量所指定的路徑中的所有類庫(kù)窝爪,開發(fā)者可以直接使用擴(kuò)展類加載器弛车。
應(yīng)用程序類加載器(Application?ClassLoader),由sun.misc.Launcher$AppClassLoader來實(shí)現(xiàn)蒲每。由于這個(gè)類加載器是ClassLoader中的getSystemClassLoader()方法的返回值纷跛,所以一般稱它為系統(tǒng)類加載器。負(fù)責(zé)加載用戶類路徑(ClassPath)上所指定的類庫(kù)邀杏,開發(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)化的重排序棒拂。編譯器在不改變單線程程序語(yǔ)義放入前提下伞梯,可以重新安排語(yǔ)句的執(zhí)行順序。
2.指令級(jí)并行的重排序≈闾耄現(xiàn)代處理器采用了指令級(jí)并行技術(shù)來將多條指令重疊執(zhí)行谜诫。如果不存在數(shù)據(jù)依賴性,處理器可以改變語(yǔ)句對(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)存屏障分為L(zhǎng)oadLoad辣之、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)變量共同參與不變約束
第二:禁止指令重排序惯退,普通的變量?jī)H僅會(huì)保證在該方法的執(zhí)行過程中所有依賴賦值結(jié)果的地方都能獲取到正確的結(jié)果,而不能保證變量賦值操作的順序與程序代碼中執(zhí)行順序一致从藤,這個(gè)就是所謂的線程內(nèi)表現(xiàn)為串行的語(yǔ)義
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)持有鎖的線程長(zhǎng)期不釋放鎖的時(shí)候金吗,正在等待的線程可以選擇放棄等待,改為處理其他事情,可中斷特性對(duì)處理執(zhí)行時(shí)間非常長(zhǎng)的同步塊很有幫助
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ù)爭(zhēng)用的問題
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ì)更長(zhǎng)的時(shí)間荚坞。如果對(duì)于某個(gè)鎖遣钳,自旋很少成功獲得過垦江,那在以后要獲取這個(gè)鎖時(shí)將可能省略掉自過程飞蚓,以避免浪費(fèi)處理器資源呈宇。
7.2.2鎖消除
鎖消除是指虛擬機(jī)即時(shí)編輯器在運(yùn)行時(shí)党饮,對(duì)一些代碼上要求同步述雾,但是被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhē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)爭(zhēng)情況下的同步原語(yǔ)凉驻,進(jìn)一步提高程序的運(yùn)行性能腻要。如果輕量級(jí)鎖是在無競(jìng)爭(zhēng)的情況下使用CAS操作去消除同步使用的互斥量,那偏向鎖就是在無競(jìng)爭(zhē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)爭(zhē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è)方法使用到的成員變量來代替