做為java開發(fā)者,平時(shí)工作中打交道最多的就是JVM了办陷,JVM是Java Virtual Machine(Java虛擬機(jī))的縮寫囤热,JVM是一種用于計(jì)算設(shè)備的規(guī)范,它是一個(gè)虛構(gòu)出來(lái)的計(jì)算機(jī)厌殉,是通過(guò)在實(shí)際的計(jì)算機(jī)上仿真模擬各種計(jì)算機(jī)功能來(lái)實(shí)現(xiàn)的。引入Java語(yǔ)言虛擬機(jī)后侈咕,Java語(yǔ)言在不同平臺(tái)上運(yùn)行時(shí)不需要重新編譯公罕。Java語(yǔ)言使用Java虛擬機(jī)屏蔽了與具體平臺(tái)相關(guān)的信息,使得Java語(yǔ)言編譯程序只需生成在Java虛擬機(jī)上運(yùn)行的目標(biāo)代碼(字節(jié)碼)耀销,就可以在多種平臺(tái)上不加修改地運(yùn)行楼眷。
市面上常見(jiàn)的主流JVM虛擬機(jī)有HotSpot VM、J9 VM熊尉、JRockit VM(JDK8中已與HotSpot合并)罐柳、Zing VM等,本文內(nèi)容主要基于HotSpot VM狰住。
JAVA類加載過(guò)程
類加載的過(guò)程包括了加載张吉、驗(yàn)證、準(zhǔn)備催植、解析肮蛹、初始化五個(gè)階段勺择。其中準(zhǔn)備、驗(yàn)證伦忠、解析三個(gè)部分統(tǒng)稱為連接省核。
加載
查找并加載類的二進(jìn)制數(shù)據(jù)按照虛擬機(jī)所需的格式存儲(chǔ)在方法區(qū)之中。
- 通過(guò)一個(gè)類的全限定名來(lái)獲取其定義的二進(jìn)制字節(jié)流昆码。
- 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)芳撒。
- 在Java堆中生成一個(gè)代表這個(gè)類的 java.lang.Class對(duì)象,作為對(duì)方法區(qū)中這些數(shù)據(jù)的訪問(wèn)入口未桥。
驗(yàn)證
確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求笔刹,并且不會(huì)危害虛擬機(jī)自身的安全。
- 文件格式驗(yàn)證:驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范冬耿;例如:是否以 0xCAFEBABE開頭舌菜、主次版本號(hào)是否在當(dāng)前虛擬機(jī)的處理范圍之內(nèi)、常量池中的常量是否有不被支持的類型亦镶。
- 元數(shù)據(jù)驗(yàn)證:對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析(注意:對(duì)比javac編譯階段的語(yǔ)義分析)日月,以保證其描述的信息符合Java語(yǔ)言規(guī)范的要求;例如:這個(gè)類是否有父類缤骨,除了 java.lang.Object之外爱咬。
- 字節(jié)碼驗(yàn)證:通過(guò)數(shù)據(jù)流和控制流分析,確定程序語(yǔ)義是合法的绊起、符合邏輯的精拟。
- 符號(hào)引用驗(yàn)證:確保解析動(dòng)作能正確執(zhí)行。
驗(yàn)證階段是非常重要的虱歪,但不是必須的蜂绎,它對(duì)程序運(yùn)行期沒(méi)有影響。如果所引用的類經(jīng)過(guò)反復(fù)驗(yàn)證笋鄙,那么可以考慮采用-Xverifynone參數(shù)來(lái)關(guān)閉大部分的類驗(yàn)證措施师枣,以縮短虛擬機(jī)類加載的時(shí)間。
準(zhǔn)備
正式為類變量(static成員變量)分配內(nèi)存并設(shè)置類變量初始值的階段萧落,這些內(nèi)存都將在方法區(qū)中分配践美。
- 內(nèi)存分配的僅包括類變量(static),而不包括實(shí)例變量找岖,實(shí)例變量會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一塊分配在Java堆中陨倡。
- 初始值通常情況下是數(shù)據(jù)類型的默認(rèn)值(如0、0L宣增、false等)玫膀,不是被在Java代碼中被顯式地賦予的值矛缨。如
public static int a = 1
在準(zhǔn)備階段過(guò)后的值為0而不是1爹脾。賦值為1的動(dòng)作將在初始化階段才會(huì)執(zhí)行帖旨。 - 如果類字段為常量類字段,那么在準(zhǔn)備階段變量值會(huì)被初始化為指定的常量值灵妨。如
public static final int a = 1
在準(zhǔn)備階段過(guò)后的值就是1解阅。
解析
解析階段是把常量池內(nèi)的符號(hào)引用替換成直接引用的過(guò)程。包括對(duì)類或接口泌霍、字段货抄、類方法、接口方法朱转、方法類型蟹地、方法句柄和調(diào)用點(diǎn)限定符7類符號(hào)引用進(jìn)行解析。
- 符號(hào)引用(Symbolic References):符號(hào)引用以一組符號(hào)來(lái)描述所引用的目標(biāo)藤为,符號(hào)可以是任何形式的字面量怪与,只要可以唯一定位到目標(biāo)即可。符號(hào)引用于內(nèi)存布局無(wú)關(guān)缅疟,所以所引用的對(duì)象不一定需要已經(jīng)加載到內(nèi)存中分别。
- 直接引用(Direct References):直接引用時(shí)直接指向目標(biāo)的指針、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄存淫,有了直接引用耘斩,那么它一定已經(jīng)存在于內(nèi)存中了。
初始化
初始化階段桅咆,才真正開始執(zhí)行類中定義的java程序代碼(字節(jié)碼)括授,為類的靜態(tài)變量賦予正確的初始值。
對(duì)類變量進(jìn)行初始值設(shè)定的方式:
- 聲明類變量時(shí)指定初始值
- 使用靜態(tài)代碼塊為類變量指定初始值
靜態(tài)語(yǔ)句塊只能訪問(wèn)到定義在靜態(tài)語(yǔ)句塊之前的變量岩饼,定義在它之后的變量刽脖,在前面的靜態(tài)語(yǔ)句塊可以賦值,但是不能訪問(wèn)
public class Test{ static{ a = 0; System.out.println(a); // IDE將提示 Illegal forward reference } static int a = 1; }
初始化步驟:
- 假如這個(gè)類還沒(méi)有被加載和連接忌愚,則程序先加載并連接該類
- 假如該類的直接父類還沒(méi)有被初始化曲管,則先初始化其直接父類
- 假如類中有初始化語(yǔ)句,則系統(tǒng)依次執(zhí)行這些初始化語(yǔ)句
JVM內(nèi)存結(jié)構(gòu)
- 程序計(jì)數(shù)器: 一塊較小的內(nèi)存空間硕糊,可以看做是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器院水。
- jvm棧:即線程棧(每個(gè)線程都有自己的棧),棧幀為加載的每個(gè)方法(入棧操作即方法入棧)或for循環(huán)等;棧用來(lái)保存基本數(shù)據(jù)類型的值,方法內(nèi)對(duì)象的引用(指針)简十,方法參數(shù)引用檬某,常量池引用等。
- jvm本地方法棧: 針對(duì)Native方法的棧螟蝙。(HotSopt虛擬機(jī)中直接就把本地方法棧和Java棧合二為一)
- jvm元數(shù)據(jù)空間:存儲(chǔ)class的元數(shù)據(jù)信息恢恼,使用的是Direct Memory,即本地內(nèi)存胰默,默認(rèn)情況下场斑,大小只受可用的本地內(nèi)存限制漓踢。
- jvm 堆:
- 新生代:1個(gè)Eden區(qū)和2個(gè)Survivor區(qū)(分別叫From和To)。
- Eden: 保持著新創(chuàng)建的對(duì)象
- From:上一次Young GC后的To,保存著每次Young GC后存活的對(duì)象漏隐,每次GC后對(duì)象的年齡會(huì)加1,到一定閾值會(huì)回直接放到老年代喧半,沒(méi)有到達(dá)閾值的放到To Survivor中。
- To: To Survivor被填滿后會(huì)將對(duì)象一次性移動(dòng)到老年代中青责,然后To和From Survivor區(qū)相互交換挺据。
- 老年代: 存放的都是一些生命周期較長(zhǎng)的對(duì)象。
- 新生代:1個(gè)Eden區(qū)和2個(gè)Survivor區(qū)(分別叫From和To)。
JVM垃圾回收算法
所有的垃圾收集算法都面臨同一個(gè)問(wèn)題脖隶,那就是找出應(yīng)用程序不可到達(dá)的內(nèi)存塊扁耐,將其釋放,這里面得不可到達(dá)主要是指應(yīng)用程序已經(jīng)沒(méi)有內(nèi)存塊的引用了产阱,而在JAVA中做葵,某個(gè)對(duì)象對(duì)應(yīng)用程序是可到達(dá)的是指:這個(gè)對(duì)象被根(根主要是指類的靜態(tài)變量,或者活躍在所有線程棧的對(duì)象的引用)引用或者對(duì)象被另一個(gè)可到達(dá)的對(duì)象引用心墅。
- 復(fù)制算法
此算法把內(nèi)存空間劃為兩個(gè)相等的區(qū)域酿矢,每次只使用其中一個(gè)區(qū)域。垃圾回收時(shí)怎燥,遍歷當(dāng)前使用區(qū)域瘫筐,把正在使用中的對(duì)象復(fù)制到另外一個(gè)區(qū)域中。此算法每次只處理正在使用中的對(duì)象铐姚,因此復(fù)制成本比較小策肝,同時(shí)復(fù)制過(guò)去以后還能進(jìn)行相應(yīng)的內(nèi)存整理,不會(huì)出現(xiàn)“碎片”問(wèn)題隐绵。當(dāng)然之众,此算法的缺點(diǎn)也是很明顯的,就是需要兩倍內(nèi)存空間依许。 - 標(biāo)記清除
此算法執(zhí)行分兩階段棺禾。第一階段從引用根節(jié)點(diǎn)開始標(biāo)記所有被引用的對(duì)象,第二階段遍歷整個(gè)堆峭跳,把未標(biāo)記的對(duì)象清除膘婶。此算法需要暫停整個(gè)應(yīng)用,同時(shí)蛀醉,會(huì)產(chǎn)生內(nèi)存碎片悬襟。 - 標(biāo)記整理
此算法結(jié)合了“標(biāo)記-清除”和“復(fù)制”兩個(gè)算法的優(yōu)點(diǎn)。也是分兩階段拯刁,第一階段從根節(jié)點(diǎn)開始標(biāo)記所有被引用對(duì)象脊岳,第二階段遍歷整個(gè)堆,把清除未標(biāo)記對(duì)象并且把存活對(duì)象“壓縮”到堆的其中一塊,按順序排放割捅。此算法避免了“標(biāo)記-清除”的碎片問(wèn)題奶躯,同時(shí)也避免了“復(fù)制”算法的空間問(wèn)題。
jvm中g(shù)c算法 年輕代:復(fù)制算法棺牧; 老年代:標(biāo)記清除巫糙,標(biāo)記整理朗儒。
java對(duì)象內(nèi)存申請(qǐng):
Java堆是被所有線程共享的一塊內(nèi)存區(qū)域颊乘,主要用于存放對(duì)象實(shí)例,在堆上為對(duì)象分配內(nèi)存就是把一塊大小確定的內(nèi)存從堆內(nèi)存中劃分出來(lái)醉锄,將對(duì)象放進(jìn)去乏悄。
- 指針碰撞法:已分配的內(nèi)存和空閑內(nèi)存分別在不同的一側(cè),通過(guò)一個(gè)指針指向分界點(diǎn)恳不,當(dāng)需要分配內(nèi)存時(shí)檩小,把指針往空閑的一端移動(dòng)與對(duì)象大小相等的距離即可,對(duì)于堆空間是連續(xù)的來(lái)說(shuō)可以直接偏移指針即可烟勋,一般來(lái)說(shuō)采用標(biāo)記整理算法的堆空間是連續(xù)的规求。
- 空閑列表法:對(duì)于堆空間已分配的內(nèi)存和空閑內(nèi)存相互交錯(cuò)不連續(xù)時(shí)需要由jvm來(lái)維護(hù)一個(gè)“空閑列表”用來(lái)記錄那些區(qū)域內(nèi)存是空閑的。
對(duì)象創(chuàng)建在虛擬機(jī)中是非常頻繁的行為卵惦,即使是僅僅修改一個(gè)指針?biāo)赶虻奈恢米柚祝诓l(fā)情況下也并不是線程安全的,可能出現(xiàn)正在給對(duì)象A分配內(nèi)存沮尿,指針還沒(méi)來(lái)得及修改丛塌,對(duì)象B又同時(shí)使用了原來(lái)的指針來(lái)分配內(nèi)存的情況。解決這個(gè)問(wèn)題有兩種方案:一種是對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理——實(shí)際上虛擬機(jī)采用CAS配上失敗重試的方式保證更新操作的原子性,該種方式在并發(fā)較高時(shí)對(duì)CPU時(shí)間片占用較多一般不會(huì)使用畜疾;另一種是把內(nèi)存分配的動(dòng)作按照線程劃分在不同的空間之中進(jìn)行赴邻,即每個(gè)線程在Java堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)啡捶。
JVM常用啟動(dòng)參數(shù)及含義
-server:指定jvm以server模式運(yùn)行姥敛;Client模式啟動(dòng)速度較快,Server模式啟動(dòng)較慢瞎暑;但是啟動(dòng)進(jìn)入穩(wěn)定期長(zhǎng)期運(yùn)行之后Server模式的程序運(yùn)行速度比Client要快很多徒溪。
-Xms:初始堆大小
-Xmx:最大堆大小
-Xmn:設(shè)置年輕代大小,不能超過(guò)-Xmx
-Xss:設(shè)置線程棧大小,默認(rèn)為1024KB金顿,對(duì)于高并發(fā)應(yīng)用且線程中存放的局部變量信息不多可降低該值以提高有更多內(nèi)存來(lái)開辟新的線程臊泌。
-XX:NewRatio: 設(shè)置年輕代和年老代的比值。如:為3揍拆,表示年輕代與年老代比值為1:3渠概,年輕代占整個(gè)年輕代年老代和的1/4
-XX:SurvivorRatio: 年輕代中Eden區(qū)與兩個(gè)Survivor區(qū)的比值。注意Survivor區(qū)有兩個(gè)。如:3播揪,表示Eden:Survivor=3:2贮喧,一個(gè)Survivor區(qū)占整個(gè)年輕代的1/5
-XX:MaxMetaspaceSize: 設(shè)置最大元數(shù)據(jù)空間。
-XX:MaxMetaspaceSize: 設(shè)置初始元數(shù)據(jù)空間猪狈。
-XX:+UseConcMarkSweepGC :老年代使用CMS收集器
-XX:CMSInitiatingOccupancyFraction:CMS垃圾收集器箱沦,當(dāng)老年代內(nèi)存使用達(dá)到指定配置比例時(shí),觸發(fā)CMS垃圾回收
-XX:+UseCMSInitiatingOccupancyOnly: 指定HotSpot-VM總是使用-XX:CMSInitiatingOccupancyFraction的值作為old的空間使用率限制來(lái)啟動(dòng)CMS垃圾回收雇庙。如果沒(méi)有使用-XX:+UseCMSInitiatingOccupancyOnly谓形,那么HotSpot-VM只是利用這個(gè)值來(lái)啟動(dòng)第一次CMS垃圾回收,后面都是使用HotSpot-VM自動(dòng)計(jì)算出來(lái)的值疆前。
JVM進(jìn)程正常退出的條件
線程類型
- 用戶線程:普通線程又可以稱為用戶線程
- 后臺(tái)線程:執(zhí)行setDaemon(true)的線程寒跳,在后臺(tái)提供一種通用服務(wù)的線程,并且這種線程并不屬于程序中不可或缺的部分竹椒。
當(dāng)不存在用戶線程的時(shí)候童太,JVM進(jìn)程就會(huì)退出。
可能出現(xiàn)FullGC的情況
- 老年代被寫滿
- 元數(shù)據(jù)空間超過(guò)設(shè)置 -XX:MaxMetaspaceSize時(shí)
- System.gc()被顯示調(diào)用
- 上一次GC之后Heap的各域分配策略動(dòng)態(tài)變化
Minor胸完、Young书释、Major GC的區(qū)別
- Minor GC發(fā)生在Eden區(qū)
- Young GC發(fā)生在Eden、Survivor0赊窥、Survivor1區(qū)
- Major GC發(fā)生在Old區(qū)