###(未完成)###
????Java的運(yùn)行原理雖然在我們開發(fā)人員中觉渴,不一定會(huì)進(jìn)行干涉检痰。但是了解其運(yùn)行原理愤惰,則能更高效地避免或定位到相對(duì)應(yīng)的Bug苇经。本文主要通過以下幾個(gè)方面進(jìn)行闡述:
????1、Java虛擬機(jī)的數(shù)據(jù)區(qū)域組成與簡介
????2宦言、對(duì)象的創(chuàng)建與查找
????3扇单、垃圾回收機(jī)制(GC)與內(nèi)存分配管理
????4、線程與內(nèi)存模型
????希望借由這幾個(gè)方面能夠有比較詳細(xì)的闡述蜡励,使得更好地理解Java虛擬機(jī)的運(yùn)行機(jī)制令花。
1阻桅、Java虛擬機(jī)的數(shù)據(jù)區(qū)域組成與簡介
????我們知道每一個(gè)Java應(yīng)用運(yùn)行后凉倚,系統(tǒng)都會(huì)創(chuàng)建一個(gè)Java虛擬機(jī)對(duì)整個(gè)程序進(jìn)行運(yùn)轉(zhuǎn),每個(gè)Java的虛擬機(jī)運(yùn)行時(shí)嫂沉,主要分成5個(gè)數(shù)據(jù)區(qū):PC(程序計(jì)數(shù)器)稽寒、虛擬機(jī)棧(VM Stack)、本地方法區(qū)棧(Native Method Stack)趟章、堆(Heap)杏糙、方法區(qū)(Method Area)慎王。從眾所周知,每個(gè)應(yīng)用內(nèi)很可能會(huì)存在多個(gè)線程宏侍,而這5個(gè)數(shù)據(jù)區(qū)赖淤,前三者是線程所獨(dú)有的(即每個(gè)線程都會(huì)各自擁有,不進(jìn)行線程間共享)谅河。但是后兩者(堆咱旱、方法區(qū))則是線程間共有的。我們看下圖:
? ? 從上圖圖1我們可知虛擬機(jī)的數(shù)據(jù)區(qū)及其各自負(fù)責(zé)的一些數(shù)據(jù)劝赔,圖二則更能生動(dòng)地展示多線程情況下诡右,數(shù)據(jù)區(qū)各自的位置與獨(dú)立性熬尺。在這里,我們簡單地解釋一下各個(gè)數(shù)據(jù)區(qū)的責(zé)任作用:
? ? 1诸典、PC(程序計(jì)數(shù)器)
? ? 一塊很小的內(nèi)存空間,存儲(chǔ)了下一條需要執(zhí)行的字節(jié)碼指令地址崎苗。比如分支狐粱、跳轉(zhuǎn)、循環(huán)等都需要依賴其計(jì)數(shù)器才能完成胆数。它是屬于線程私有的脑奠,但是多線程并發(fā)的實(shí)現(xiàn)是通過線程輪換并分配執(zhí)行時(shí)間完成的,所以在JVM中幅慌,某一時(shí)間內(nèi)是只有一條指令在執(zhí)行的宋欺。而它也是唯一一個(gè)在Java規(guī)范中,OutOfMememory未曾定義發(fā)生的區(qū)域(即不會(huì)在該區(qū)域發(fā)生OutOfMememory異常)胰伍。
? ? 2齿诞、虛擬機(jī)棧(VM Stack)
? ? 這個(gè)數(shù)據(jù)區(qū)主要對(duì)應(yīng)了Java對(duì)象方法,主要存儲(chǔ)方法的信息骂租,局部變量祷杈,方法出口等。它是由一個(gè)個(gè)棧幀所組成的渗饮,在每次調(diào)用Java方法的時(shí)候創(chuàng)建一個(gè)棧幀但汞,然后放入虛擬機(jī)棧中,采取的是后入先出(LIFO)的方式互站,即最新調(diào)用的方法優(yōu)先執(zhí)行私蕾。它是屬于線程所私有,整個(gè)虛擬機(jī)棧作用也造成其生命周期跟線程的生命周期是相同的胡桃。 (ps:這里的局部變量表中存儲(chǔ)局部變量值(基本數(shù)據(jù)類型)踩叭,局部變量(實(shí)例對(duì)象)引用。)? ?
? ? 由于后進(jìn)先出的機(jī)制,虛擬機(jī)棧頂端的棧(被稱作當(dāng)前活動(dòng)棧幀)容贝,是該虛擬機(jī)棧中唯一活躍(執(zhí)行)的棧幀自脯。由此我們可以知道,一個(gè)棧幀從入棧到出棧斤富,對(duì)應(yīng)的是一個(gè)方法的整個(gè)過程膏潮,其記錄的信息自然也是整個(gè)方法所需要和產(chǎn)生的信息。具體有:
? ? 這里說一下棧幀里的局部變量表满力,它包含的主要是:編譯期間就知道的8種基本數(shù)據(jù)類型戏罢、對(duì)象引用和 returnAddress 類型(指向了一條字節(jié)碼指令的地址)
????3、本地方法棧
? ? 又稱本地方法區(qū)脚囊,跟虛擬機(jī)棧的區(qū)別在于:虛擬機(jī)棧服務(wù)于Java對(duì)象方法龟糕,而本地方法棧則服務(wù)于本地方法(Native)。
????也因此無論是虛擬機(jī)棧悔耘,或者本地方法區(qū)都存在這兩種異常的可能:
? ? (1)StackOverflowError:線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度讲岁;
? ? (2)OutOfMemoryError:如果虛擬機(jī)棧可以動(dòng)態(tài)擴(kuò)展衬以,而擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存缓艳。
? ? 4、堆(Heap)
? ? 幾乎所有的對(duì)象實(shí)例(需要new出來的看峻,基本數(shù)據(jù)類型除外的所有對(duì)象類型)阶淘、數(shù)組內(nèi)容存放區(qū)域。因其變化性互妓,對(duì)于一般情況溪窒,對(duì)堆所分配的內(nèi)存是整個(gè)虛擬機(jī)的數(shù)據(jù)區(qū)中最大的一塊。也因此是垃圾回收(GC)主要運(yùn)作區(qū)域冯勉,OOM最容易發(fā)生的區(qū)域澈蚌。
? ? 我們知道,堆是所有線程所共享的灼狰,也因此內(nèi)部會(huì)對(duì)線程分配出私有的數(shù)據(jù)緩沖區(qū)(TLAB)宛瞄。對(duì)于每個(gè)數(shù)據(jù)緩沖區(qū)來說,它所分配的內(nèi)存區(qū)域不一定在物理?xiàng)l件上是完整的一塊內(nèi)存區(qū)域交胚,只要在邏輯上是完整的一塊區(qū)域即可份汗。
? ? 5、方法區(qū)(Method Area)
? ??屬于線程共享內(nèi)存區(qū)域蝴簇,存儲(chǔ)已被虛擬機(jī)加載的類信息杯活、常量、靜態(tài)變量军熏、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)轩猩。其組成為:
2卷扮、對(duì)象的創(chuàng)建與查找
? ? 在這一塊主要了解一下荡澎,整個(gè)數(shù)據(jù)的創(chuàng)建于管理流程均践,對(duì)于后面的垃圾回收、內(nèi)存相關(guān)的問題打一下基礎(chǔ)摩幔。對(duì)象的創(chuàng)建彤委,最常見的就是通過new指令來獲取。創(chuàng)建對(duì)象的流程或衡,大致上分為兩個(gè)部分:初始化焦影、實(shí)例化。
? ? 到此封断,初始化已經(jīng)完成斯辰,接下來就是實(shí)例化了。實(shí)例化其實(shí)也有幾個(gè)階段:申請(qǐng)內(nèi)存來存儲(chǔ)成員變量坡疼,在申請(qǐng)成功之前彬呻,這些成員變量的值為默認(rèn)零值。等到內(nèi)存申請(qǐng)成功了柄瑰,才將代碼中設(shè)置的初始值賦加過去闸氮。實(shí)例化后獲得一個(gè)對(duì)象實(shí)例,而對(duì)象實(shí)例主要有:對(duì)象頭(Header)教沾、實(shí)例數(shù)據(jù)(Instance Data)和對(duì)齊填充(Padding)蒲跨。????
? ? (1)對(duì)象頭:包括了對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù)(GC年代標(biāo)記、哈希碼授翻、鎖狀態(tài)標(biāo)記等)+類型指針(對(duì)象所屬的哪個(gè)類)
? ? (2)實(shí)例數(shù)據(jù):代碼中各字段內(nèi)容
? ? (3)對(duì)齊填充:不是必然需要或悲,主要是占位,保證對(duì)象大小是某個(gè)字節(jié)的整數(shù)倍堪唐。
? ? 對(duì)象的內(nèi)容存儲(chǔ)是毋庸置疑的隆箩,但外部持有的引用指向的有兩種可能:1、對(duì)象的實(shí)例內(nèi)容羔杨;2捌臊、對(duì)象的句柄(其實(shí)就是一個(gè)指向?qū)嵗齼?nèi)容的引用,比1類多了一層句柄而已)兜材。前者優(yōu)點(diǎn)是訪問速度快理澎,后者的優(yōu)點(diǎn)是當(dāng)對(duì)象需要頻繁GC的時(shí)候?qū)?nèi)存比較友好。
3曙寡、垃圾回收機(jī)制(GC)與內(nèi)存分配管理
? ? 從第一節(jié)數(shù)據(jù)區(qū)域的劃分我們可以知道糠爬,PC、虛擬機(jī)棧举庶、本地方法棧會(huì)隨著線程的結(jié)束而回收执隧,但是屬于線程共有的堆和方法區(qū)則一直存在著,為了解放開發(fā)人員手動(dòng)回收內(nèi)存操作的負(fù)累,這也就造成了內(nèi)部需要在運(yùn)行時(shí)對(duì)不需要再繼續(xù)存在的變量——垃圾進(jìn)行回收镀琉。而垃圾回收最主要的難點(diǎn)就是峦嗤,怎么分辨哪些對(duì)象是有用,而哪些對(duì)象是垃圾需要進(jìn)行回收屋摔。因此提出了幾種算法烁设。在這之前,我們先對(duì)引用類別進(jìn)行了解一下钓试。
? ?3.1 引用的類型
????a装黑、強(qiáng)引用:通常是new指令所返回,如果一個(gè)對(duì)象的強(qiáng)引用存在弓熏,就不會(huì)被GC所回收恋谭;
????b、軟引用:SoftReference挽鞠,平時(shí)不會(huì)對(duì)存在軟引用的對(duì)象進(jìn)行回收箕别,但是當(dāng)內(nèi)存不足的時(shí)候(內(nèi)存溢出),則會(huì)將只存在軟引用的對(duì)象列入回收范圍內(nèi)滞谢;
????c串稀、弱引用:WeakReference,每次GC的時(shí)候都會(huì)對(duì)弱引用進(jìn)行回收狮杨,也因此每次弱引用的存活時(shí)間都是每次GC之間母截;
????d、虛引用:PhantomReference ,無法通過虛引用獲取一個(gè)對(duì)象的實(shí)例橄教,為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知清寇。
? ?3.2 判斷對(duì)象存活的算法
? ? a、引用計(jì)數(shù)法
? ??給對(duì)象添加一個(gè)引用計(jì)數(shù)器护蝶。但是難以解決循環(huán)引用問題华烟。
? ? b、可達(dá)性算法
? ? 以'GC Roots'作為起點(diǎn)持灰,將對(duì)象的引用陸續(xù)加入形成一顆樹(如obj1與GC Roots相連盔夜,obj2與obj1相連)。當(dāng)能夠通過GC Roots到達(dá)某個(gè)節(jié)點(diǎn)堤魁,則該對(duì)象仍舊有用喂链,否則視為垃圾。而可作為GC Roots的對(duì)象有這幾種:虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象妥泉、方法區(qū)中類靜態(tài)屬性引用的對(duì)象椭微、方法區(qū)中常量引用的對(duì)象、本地方法棧中 JNI(即一般說的 Native 方法) 引用的對(duì)象盲链。
? ? 在可達(dá)性算法中蝇率,要把對(duì)象進(jìn)行回收其實(shí)經(jīng)過兩次檢查:首先將不可達(dá)的對(duì)象放入回收考慮范圍(標(biāo)記起來)迟杂,之后再回收考慮范圍的對(duì)象(被標(biāo)記的對(duì)象)再進(jìn)行finalize()的檢查:沒有覆蓋finalize()方法,或者finalize() 方法已經(jīng)被虛擬機(jī)調(diào)用過本慕。將打上需要回收的標(biāo)志排拷。當(dāng)然,每個(gè)對(duì)象的finalize()只會(huì)被系統(tǒng)調(diào)用一次间狂,也就是說對(duì)象能夠在finalize()方法中最后“拯救自己”一次攻泼,但也僅僅只有一次火架。
????3.3 對(duì)象回收的算法
? ? a鉴象、標(biāo)記清除法:這個(gè)就是字面意思,將需要回收的對(duì)象進(jìn)行標(biāo)記何鸡,然后將標(biāo)記的對(duì)象直接回收清除就行了纺弊。優(yōu)點(diǎn)是簡單,缺點(diǎn)就是效率不高骡男,而且會(huì)造成內(nèi)存碎片較多(內(nèi)存碎片過多則可能會(huì)在下一次需要一個(gè)較大內(nèi)存的對(duì)象因無法找到一個(gè)連續(xù)的內(nèi)存觸發(fā)新一次的GC)淆游;
? ? b、復(fù)制算法:將內(nèi)存分成兩塊區(qū)域隔盛,每次只GC其中一塊犹菱,然后將存活的對(duì)象復(fù)制到另一塊上面去,輪流使用吮炕。由于每次GC一般都能回收大量的對(duì)象腊脱,也因此提出了比較優(yōu)化的方案:將內(nèi)存分成兩種類型(Eden 、Survivor)龙亲,3塊區(qū)域(Eden :Survivor:Survivor=8:1:1)陕凹,由此每一次運(yùn)行時(shí)候?qū)⒖臻e出一塊Survivor區(qū)域(約是所占內(nèi)存區(qū)域的10%)備用。
????我們假設(shè)名稱為E塊鳄炉、S1塊杜耙,S2塊,此次當(dāng)GC的時(shí)候?qū)⒋婊畹膶?duì)象放到E塊和S1塊拂盯,S2塊備用佑女。當(dāng)GC對(duì)E塊和S1塊進(jìn)行清理回收的時(shí)候,將存活的對(duì)象復(fù)制放入S2塊谈竿。這樣E塊和S1塊空出來了珊豹,這時(shí)候?qū)塊和S2塊用于這次的運(yùn)行產(chǎn)生的對(duì)象存儲(chǔ),S1塊作為備用(Survivor依次備用)榕订。
? ? 優(yōu)點(diǎn):高效店茶、實(shí)現(xiàn)也算簡單,也不會(huì)產(chǎn)生內(nèi)存碎片劫恒,適合生命周期比較短的(如新生代類)對(duì)象進(jìn)行管理贩幻;
? ? 缺點(diǎn):除了增加復(fù)制的時(shí)間花銷之外轿腺,還有就是必須浪費(fèi)一部分內(nèi)存(最初是要浪費(fèi)一半的內(nèi)存空間,即使是優(yōu)化后也需要浪費(fèi)10%的內(nèi)存空間)丛楚。如果每次存活的對(duì)象(老年代類的對(duì)象)較多的時(shí)候族壳,就加大了不小的負(fù)擔(dān)。在優(yōu)化方案中趣些,當(dāng)存活的對(duì)象超過10%的時(shí)候仿荆,仍舊需要通過分代算法進(jìn)行補(bǔ)足(依賴于老生代進(jìn)行分配擔(dān)保,所以大對(duì)象直接進(jìn)入老年代)坏平。
? ? c拢操、標(biāo)記整理法:對(duì)存活的對(duì)象進(jìn)行標(biāo)記,然后移動(dòng)到內(nèi)存的一端舶替,之后再對(duì)剩余的內(nèi)存空間進(jìn)行清理令境。優(yōu)點(diǎn)就是每次都能夠整理出比較連續(xù)完整的內(nèi)存空間,缺點(diǎn)就是需要增加存活對(duì)象移動(dòng)的成本顾瞪。
?????3.4 內(nèi)存分配與回收策略
????由于對(duì)象的生命周期可能存在較大差距舔庶,一刀切地采用某種算法弊端就十分明顯。因此引入歸類的方法陈醒,將對(duì)象分代進(jìn)行管理惕橙,在不同代類的對(duì)象管理方法各自采用不同的算法,其中分成:新生代(年輕代)钉跷、老生代(老年代)弥鹦、永生代。
? ? (1)新生代:該內(nèi)存區(qū)域主要用于存放新創(chuàng)建的對(duì)象尘应,該區(qū)域的內(nèi)存容量相對(duì)會(huì)較小惶凝,垃圾回收也比較頻繁。采用的算法為復(fù)制算法(8:1:1優(yōu)化版)犬钢。
? ? (2)老生代:該內(nèi)存區(qū)域主要存放JVM中生命周期較長的對(duì)象(經(jīng)過幾次的Young Gen的垃圾回收后仍然存在)苍鲜,該區(qū)域內(nèi)存相對(duì)較大,垃圾回收也相對(duì)不頻繁玷犹,采用的是標(biāo)記整理法混滔。由于老生代的回收速度會(huì)比較慢,所以要盡量避免老生代的垃圾回收歹颓。
? ? (3)永生代:該內(nèi)存區(qū)域主要存放類定義坯屿、字節(jié)碼和常量等很少會(huì)變更(或者說不會(huì)變更)的信息。該內(nèi)存區(qū)域是由JVM在運(yùn)行時(shí)根據(jù)應(yīng)用程序使用的類來填充的巍扛。此外领跛,Java SE類庫和方法也存儲(chǔ)在這里。如果JVM發(fā)現(xiàn)某些類不再需要撤奸,并且其他類可能需要空間吠昭,則這些類可能會(huì)被回收喊括。
? ? 在新生代中,如果存活的對(duì)象總量超過了內(nèi)存的10%矢棚,需要老生代進(jìn)行分擔(dān)即:該情況下會(huì)對(duì)對(duì)象的年齡進(jìn)行判斷郑什,根據(jù)年齡閾值將超過年齡的對(duì)象轉(zhuǎn)移到老生代中。也因此老生代中的對(duì)象年齡不一定都是相同的蒲肋,甚至一些挺“年輕的”對(duì)象也可能直接進(jìn)入老生代——占據(jù)(或者說需要)大內(nèi)存的對(duì)象蘑拯。
????這里我們將內(nèi)存分配與回收策略統(tǒng)計(jì)一下:
4、線程與內(nèi)存模型
? ? 線程與進(jìn)程的定義就不多說了兜粘,這里看看線程并發(fā)的安全必須依賴于以下幾個(gè)方面:
????(1)原子性:提供了一種互斥訪問申窘,同一時(shí)刻只能有一個(gè)線程對(duì)它進(jìn)行操作;
? ? (2)可見性:一個(gè)線程對(duì)主內(nèi)存對(duì)修改可以及時(shí)的被其他線程觀察到妹沙;
? ? (3)一致性:一個(gè)線程觀察其他線程的指令行執(zhí)行順序偶洋,由于指令重新排序的存在熟吏,該觀察結(jié)果一般雜亂無序距糖;
? ? 這里補(bǔ)充一個(gè)概念——指令重排:處理器為了提高程序運(yùn)行效率,可能會(huì)對(duì)輸入代碼進(jìn)行優(yōu)化牵寺。所以在保證程序執(zhí)行結(jié)果+代碼順序執(zhí)行結(jié)果一致的前提下悍引,處理器并不保證每一句代碼執(zhí)行順序都會(huì)按照代碼中的順序來。比如int a=0; int b=1; a++帽氓;b=a+5;(將上述代碼視為4行)中趣斤,第1、2句代碼誰先執(zhí)行并無任何影響黎休,所以在處理器的時(shí)候可能會(huì)先執(zhí)行第2句再執(zhí)行第一句浓领。所以指令重排可能會(huì)打亂代碼的執(zhí)行順序,只要這個(gè)執(zhí)行順序不用想程序或者代碼順序的執(zhí)行結(jié)果势腮。?
? ? 上面就是線程捎拯、工作內(nèi)存與主內(nèi)存的基本交互泪幌,至于工作內(nèi)存與主內(nèi)存的操作包括lock/unlock、read署照、load祸泪、user、store建芙、write等操作没隘,具體的操作對(duì)象與目的就不多說了。這里補(bǔ)充一個(gè)線程并發(fā)的原則——happen - before(先行發(fā)生原則)禁荸,它可以理解為:1右蒲、如果A先行發(fā)生于B微王,則A造成的事件影響(如修改了共享變量、調(diào)用方法等)對(duì)B是可見的品嚣;2炕倘、即使A、B存在happen - before關(guān)系翰撑,在保證執(zhí)行結(jié)果或者代碼順序結(jié)果的前提下罩旋,不保證執(zhí)行順序(也就是說不一定一定要按照Happens-Before原則制定的順序來執(zhí)行,比如指令重排)
? ? 4.1?volatile 關(guān)鍵字
? ? 保證可見性不保證原子性
? ? 4.2?synchronized 關(guān)鍵字
????同步塊大家都比較熟悉眶诈,通過 synchronized 關(guān)鍵字來實(shí)現(xiàn)涨醋,所有加上synchronized 和 塊語句,在多線程訪問的時(shí)候逝撬,同一時(shí)刻只能有一個(gè)線程能夠用浴骂,屬于重量級(jí)的線程安全機(jī)制。synchronized 修飾的方法 或者 代碼塊宪潮。