文章較長,主要講解了JVM的整個(gè)流程哥放,其次點(diǎn)了Dalvik與JVM的區(qū)別及ART
JVM標(biāo)題下
Class文件結(jié)構(gòu) -> JVM內(nèi)存模型 -> 類加載器 -> 類加載過程 -> 類的引用方式 -> 內(nèi)存分配策略 -> GC -> 對象的引用類型 -> 類卸載
先前知識(shí)
眾所周知java是一種跨平臺(tái)的語言歼指,但實(shí)際上跨平臺(tái)的并不是java而是JVM。
JVM(Java Virtual Machine)是一種虛擬機(jī)甥雕,用來將由java文件編譯成的class字節(jié)碼文件再編譯成機(jī)器語言踩身,供機(jī)器識(shí)別。有了JVM中間人的存在就不需要直接與操作系統(tǒng)打交道社露,且不同的操作系統(tǒng)有不同的JVM挟阻,于是就屏蔽了操作系統(tǒng)間的差異,從而使java成為跨平臺(tái)語言峭弟。
DVM又是什么附鸽?
Dalvik Virtual Machine簡稱DVM也是一種虛擬機(jī),是專門為Android平臺(tái)開發(fā)的瞒瘸,它與JVM是有差別的坷备。
Dalvik基于寄存器,而JVM 基于棧情臭。性能有很大的提升击你∮褡椋基于寄存器的虛擬機(jī)對于更大的程序來說,在它們編譯的時(shí)候丁侄,花費(fèi)的時(shí)間更短惯雳。
寄存器的概念
寄存器是中央處理器內(nèi)的組成部分。寄存器是有限存貯容量的高速存貯部件鸿摇,它們可用來暫存指令石景、數(shù)據(jù)和位址。在中央處理器的控制部件中拙吉,包含的寄存器有指令寄存器(IR)和程序計(jì)數(shù)器(PC)潮孽,在中央處理器的算術(shù)及邏輯部件中,包含的寄存器有累加器(ACC)
棧的概念
棧是線程獨(dú)有的筷黔,保存其運(yùn)行狀態(tài)和局部自動(dòng)變量的(所以多線程中局部變量都是相互獨(dú)立的往史,不同于類變量)。棧在線程開始的時(shí)候初始化(線程的Start方法佛舱,初始化分配棧)椎例,每個(gè)線程的棧互相獨(dú)立请祖。每個(gè)函數(shù)都有自己的棧订歪,棧被用來在函數(shù)之間傳遞參數(shù)。操作系統(tǒng)在切換線程的時(shí)候會(huì)自動(dòng)的切換棧肆捕,就是切換SS/ESP寄存器刷晋。棧空間不需要在高級(jí)語言里面顯式的分配和釋放慎陵。
JVM
java的使用流程:
- 編寫.java文件
- 編譯成.class
- 打包成.jar(Java Archive) .war(Web Archive)使用
- 命令行則直接使用.class
其實(shí).jar和.war是.class文件的壓縮包眼虱,其中還包含了不同的配置文件,使用時(shí)通過類加載器取其內(nèi)部的.class字節(jié)碼文件加載到JVM席纽。
Class文件結(jié)構(gòu)
JVM接收的最初數(shù)據(jù)是class字節(jié)碼文件蒙幻,由.java文件編譯產(chǎn)生。并不是只有java語言可以編譯成class文件胆筒,其他語言也是可以(Scala、Groovy)诈豌。
class文件是由8位為一組的字節(jié)為基本單位仆救,構(gòu)成的二進(jìn)制文件,為什么是二進(jìn)制文件呢矫渔?
- 機(jī)器語言為二進(jìn)制彤蔽,所以使用便捷;
- 占用空間小庙洼,3.1415927用文本文件存儲(chǔ)需要將各個(gè)位轉(zhuǎn)成ASCII碼再存儲(chǔ)需占用9字節(jié)顿痪,二進(jìn)制文件存儲(chǔ)只需四字節(jié)镊辕;
- 存儲(chǔ)數(shù)據(jù)精確不會(huì)丟失。
結(jié)構(gòu)如上圖蚁袭,最上方為起始位征懈,內(nèi)容包含了.java文件的信息。
類型
class文件中只有兩種類型:無符號(hào)數(shù)和表
無符號(hào)數(shù)為基本類型揩悄,有:u1卖哎、u2、u4删性、u8亏娜,數(shù)字代表字節(jié)數(shù)。無符號(hào)數(shù)可以代表數(shù)字蹬挺、索引引用维贺、數(shù)量值,或者按照UTF-8編碼構(gòu)成字符串值
表則是由基本類型和表構(gòu)成的類型巴帮,屬于組合類型
magic
文件最初的4個(gè)字節(jié)溯泣,稱為魔數(shù),是計(jì)算機(jī)識(shí)別文件類型的依據(jù)晰韵,不同于感官发乔,.word .png .avi這種通過擴(kuò)展名識(shí)別文件類型的方式,計(jì)算機(jī)識(shí)別多數(shù)文件是通過文件頭部的魔數(shù)雪猪。
這種做法的優(yōu)點(diǎn)在于安全性栏尚,文件的擴(kuò)展名可以人為隨意的修改,也許并不會(huì)造成文件的不可用(早年間的“圖種”一詞不知多少人有經(jīng)歷)只恨,也可能造成文件不可用译仗,但文件的類型在文件創(chuàng)建之初就被賦予魔數(shù)的話,就可以大限度的保證文件的安全性官觅。
version
表示此.class的版本信息纵菌,有minor_version和major_version兩種類型共占了4字節(jié)。
不同版本Java有不同的特性休涤,產(chǎn)生的class結(jié)構(gòu)也會(huì)不同咱圆,JVM通過識(shí)別版本從而確定是否可識(shí)別此文件。JVM是向下兼容的功氨,如果.class版本過高則不能運(yùn)行(Unsupported major.minor version **)序苏。
constant
constant_pool為常量池,用來存放常量捷凄,constant_pool_count為池中的計(jì)數(shù)忱详。
constant_pool的索引從1開始,當(dāng)指針不想引用此constant_pool時(shí)則將指針指向0跺涤,此操作簡單(賦值永遠(yuǎn)比刪除簡單)匈睁。
常量池中有兩大類常量類型:字面量和符號(hào)引用
- 字面量為java中的基本數(shù)據(jù)類型
- 符號(hào)引用:以一組符號(hào)來描述所引用的目標(biāo)监透,引用的目標(biāo)并不一定已經(jīng)加載到內(nèi)存中,在類加載過程的解析階段JVM將常量池內(nèi)的符號(hào)引用替換為直接引用航唆。類型有:
1胀蛮、CONSTANT_Class_info 類和接口的全限定名(該名稱在所有類型中唯一標(biāo)識(shí)該類型)
2、CONSTANT_Fieldref_info 字段的名稱和描述符
3佛点、CONSTANT_Methodref_info 方法的名稱和描述符
常量池中每一項(xiàng)常量都是一個(gè)表
講解:
class A{
int i=9;
int b=new B();
}
class B{
}
編譯時(shí)會(huì)產(chǎn)生A.class和B.class醇滥,此時(shí)A.class有兩個(gè)常量9和B。JVM加載A.class時(shí)超营,將由于常量9屬于字面量即基本數(shù)據(jù)類型鸳玩,直接放入常量池。
到常量B時(shí)演闭,由于常量B不屬于字面量即基本數(shù)據(jù)類型不跟,所以此時(shí)產(chǎn)生一個(gè)符號(hào)引用來代表常量B。
等到A.class加載到了解析階段米碰,需要將符號(hào)引用改為直接引用窝革,但找不到符號(hào)引用B的直接引用,
在使用階段吕座,由于A對象主動(dòng)引用了B類虐译,所以JVM通過類加載器開始加載B.class(同樣的加載步驟),并創(chuàng)建了B對象吴趴,并將符號(hào)引用B改為B對象的直接引用漆诽。
access
access_flags即java中的類修飾符
類的身份信息
一個(gè)類要有類名,關(guān)系要有extends和implement锣枝。java中類是單繼承厢拭,所以除Object外所有類都有一個(gè)父類,而接口則可以有多實(shí)現(xiàn)撇叁。
this_class是這個(gè)類的全限定名
super_class是這個(gè)類父類的全限定名
interfaces是這個(gè)類實(shí)現(xiàn)接口的集合
interfaces_count是這個(gè)類實(shí)現(xiàn)接口的數(shù)量
fields
fields_count表示fields表中的數(shù)量
fields是表結(jié)構(gòu)用來存放字段供鸠,字段即為類中聲明的變量。字段包括了類級(jí)變量或?qū)嵗?jí)變量陨闹,static修飾符為判斷依據(jù)楞捂。
public static final transient String str = "Hello World";
一個(gè)字段包含的信息有:
- 作用域(public、private趋厉、protected修飾符)
- 類級(jí)變量還是實(shí)例級(jí)變量(static修飾符)
- 可變性(final)
- 并發(fā)可見性(volatile修飾符寨闹,是否強(qiáng)制從主內(nèi)存讀寫)
- 可否序列化(transient修飾符)
- 字段數(shù)據(jù)類型(基本類型、對象觅廓、數(shù)組)
- 字段名稱(str)
一個(gè)字段有多種修飾符,每種修飾符只有兩種狀態(tài):有涵但、沒有杈绸,所以采用標(biāo)志位來表示最為合理帖蔓。字段的其他信息,叫什么名字瞳脓、被定義為什么數(shù)據(jù)類型塑娇,這些都是無法固定的,所以引用常量池中的常量來描述劫侧。
access_flags:修飾符
name_index:簡單名稱
指變量名埋酬,存放在常量池烧栋。例如字段str的簡單名稱“str”珍特。
descriptor_index:描述符
描述字段的類型
例子:
java.lang.String[][] —— [[Ljava/lang/String
int[] —— [I
String s —— Ljava/lang/String
attributes:屬性集合,以用于描述某些場景專有的信息魔吐。
上面的類型只定義了變量信息扎筒,那變量的初始賦值操作呢?
賦值操作是將常量賦值給變量嗜桌,常量有字面量和符號(hào)引用辞色,字面量會(huì)在常量池中骨宠,符號(hào)引用依據(jù)情況會(huì)在解析或使用階段改為直接引用。
字段賦值的時(shí)機(jī):
a:對于非靜態(tài)的field字段的賦值將會(huì)出現(xiàn)在實(shí)例構(gòu)造方法<init>()中
b:對于靜態(tài)的field字段雳灵,有兩個(gè)選擇:1棕所、在類構(gòu)造方法<cinit>()中進(jìn)行;
2悯辙、使用ConstantValue屬性進(jìn)行賦值
編譯器對于靜態(tài)field字段的初始化賦值策略:如果final和static同時(shí)修飾一個(gè)字段琳省,并且這個(gè)字段是基本類型或者String類型的,
那么編譯器在編譯這個(gè)字段的時(shí)候躲撰,會(huì)在對應(yīng)的field_info結(jié)構(gòu)體中增加一個(gè)ConstantValue類型的結(jié)構(gòu)體针贬,在賦值的時(shí)候使用這個(gè)ConstantValue進(jìn)行賦值;如果該field字段并沒有被final修飾拢蛋,或者不是基本類型或者String類型桦他,那么將在類構(gòu)造方法中賦值。
對于全局變量的值是被編譯在構(gòu)造器中賦值的
methods
methods_count表示methods表中的數(shù)量
methods是表結(jié)構(gòu)用來存放方法谆棱,表結(jié)構(gòu)和字段的表結(jié)構(gòu)一致
由于部分關(guān)鍵字相對于變量和方法是有區(qū)別的
例子:
int indexOf(char[] source,int sourceOffset,int sourceCount,char[] targetOffset,int targetCount,int fromIndex) —— ([CII[CII)I
方法內(nèi)部的代碼存到什么地方了快压?
attributes:屬性表
在字段表和方法表中都有屬性表
屬性表所能識(shí)別的屬性有
方法中到具體代碼就存放在方法表中屬性表的Code屬性中
參考 https://blog.csdn.net/sinat_37138973/article/details/54378263
在了解了字節(jié)碼文件的結(jié)構(gòu)后圆仔,JVM要想使用此文件,首先要將其加載到內(nèi)存蔫劣,那內(nèi)存結(jié)構(gòu)是怎樣的呢坪郭?
JVM內(nèi)存模型
- PC
與CPU中的PC不同,CPU中的PC是記錄即將執(zhí)行的下條指令的地址脉幢,而JVM中記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址歪沃,且執(zhí)行native方法時(shí)PC為空 - 虛擬機(jī)棧
每個(gè)方法被執(zhí)行的時(shí)候都會(huì)創(chuàng)建一個(gè)棧幀,用于存儲(chǔ)局部變量表嫌松、操作棧沪曙、動(dòng)態(tài)鏈接、方法出口等信息豆瘫。每一個(gè)方法被調(diào)用直至執(zhí)行完成的過程珊蟀,就對應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過程。
如果線程請求的棧深度大于虛擬機(jī)所允許的深度外驱,將拋出StackOverflowError 異常育灸;如果虛擬機(jī)棧可以動(dòng)態(tài)擴(kuò)展當(dāng)擴(kuò)展時(shí)無法申請到足夠的內(nèi)存時(shí)會(huì)拋出OutOfMemoryError 異常昵宇。 - 本地方法棧
為虛擬機(jī)使用到的Native方法服務(wù)
本地方法棧區(qū)域也會(huì)拋出StackOverflowError 和OutOfMemoryError異常磅崭。 - 方法區(qū)
用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量瓦哎、靜態(tài)變量砸喻、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時(shí)蒋譬,將拋出OutOfMemoryError 異常割岛。 - 堆
存放對象實(shí)例,如果在堆中沒有內(nèi)存完成實(shí)例分配犯助,并且堆也無法再擴(kuò)展時(shí)癣漆,將會(huì)拋出OutOfMemoryError 異常。
在知道了class字節(jié)碼文件結(jié)構(gòu)和JVM內(nèi)存模型后剂买,需要一個(gè)過程將字節(jié)碼文件加載到內(nèi)存惠爽。
類加載器
負(fù)責(zé)將字節(jié)碼文件載入到內(nèi)存,
BootstrapClassLoader – JRE/lib/rt.jar
ExtensionClassLoader – JRE/lib/ext或者java.ext.dirs指向的目錄
ApplicationClassLoader – CLASSPATH環(huán)境變量, 由-classpath或-cp選項(xiàng)定義,或者是JAR中的Manifest的classpath屬性定義
從上至下依次為父子關(guān)系瞬哼,并不是繼承關(guān)系婚肆。
機(jī)制
委托機(jī)制
當(dāng)加載B.class時(shí),請求首先發(fā)到ApplicationClassLoader坐慰,ApplicationClassLoader看都不看就交給父親ExtensionClassLoader较性,ExtensionClassLoader也是看都不看就交給父親BootstrapClassLoader,BootstrapClassLoader為始祖了,于是在自己的管轄區(qū)內(nèi)查找看有沒有B.class赞咙,有就加載沒有就告訴兒子ExtensionClassLoader永毅,你自己處理,ExtensionClassLoader收到父親的信息后在自己的管轄區(qū)內(nèi)查找B.class人弓,有就加載沒有就告訴兒子ApplicationClassLoader,你自己處理着逐,ApplicationClassLoader收到父親的信息后在自己的管轄區(qū)內(nèi)查找B.class崔赌,有就加載沒有就ClassNotFoundException。可見性機(jī)制
子類加載器可以看到父類加載器加載的類單一性機(jī)制
因委托機(jī)制的關(guān)系耸别,一個(gè)類(唯一的全限定名)只能被一個(gè)類加載器加載一次
加載方式
顯式加載
通過class.forname()等方法健芭,顯式加載需要的類隱式加載
程序在運(yùn)行過程中當(dāng)碰到通過new等方式生成對象時(shí),隱式調(diào)用類裝載器加載對應(yīng)的類到j(luò)vm中
自定義類加載器
以上三種類加載器秀姐,在某些場景下就不適用慈迈。由于以上三種類加載器都是加載指定位置的class,當(dāng)加載異地加密的class時(shí)就無法使用省有,此時(shí)需要自定義類加載器痒留,加載指定位置的class到內(nèi)存并在執(zhí)行解密后使用。
有了class字節(jié)碼文件結(jié)構(gòu)蠢沿、JVM內(nèi)存模型和類加載器這三個(gè)部分的初識(shí)伸头,接著就是三個(gè)獨(dú)立部分合作的場景:類加載過程
類加載過程
類加載器將字節(jié)碼文件載入JVM內(nè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對象作為方法區(qū)這些數(shù)據(jù)的訪問入口恤磷。
類加載器參與的階段。驗(yàn)證
確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求野宜,并且不會(huì)危害虛擬機(jī)自身的安全扫步。主要驗(yàn)證過程包括:文件格式驗(yàn)證(魔數(shù)、版本號(hào))匈子,元數(shù)據(jù)驗(yàn)證(類關(guān)系)河胎,字節(jié)碼驗(yàn)證(數(shù)據(jù)流、控制流)以及符號(hào)引用驗(yàn)證(引用可達(dá))旬牲。準(zhǔn)備
正式為類變量(static變量)分配內(nèi)存并設(shè)置類變量初始值的階段仿粹。關(guān)于準(zhǔn)備階段為類變量設(shè)置零值的唯一例外就是當(dāng)這個(gè)類變量同時(shí)也被final修飾,那么在編譯時(shí)原茅,就會(huì)直接為這個(gè)常量賦上目標(biāo)值吭历。解析
解析時(shí)虛擬機(jī)將常量池中的符號(hào)引用替換為直接引用。初始化
初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程擂橘。類構(gòu)造器<clinit>()方法是由編譯器自動(dòng)收藏類中的所有類變量的賦值動(dòng)作和靜態(tài)語句塊(static塊)中的語句合并產(chǎn)生晌区。
當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化。
虛擬機(jī)會(huì)保證一個(gè)類的<clinit>()方法在多線程環(huán)境中被正確加鎖和同步敬飒。
一個(gè)項(xiàng)目径簿、jar包、war包中有數(shù)百成千上萬的字節(jié)碼文件灾馒,一個(gè)字節(jié)碼文件只能被加載一次(類加載器的委托機(jī)制),JVM是一次性加載全部文件的嗎遣总?肯定不是睬罗,具體的實(shí)現(xiàn)由不同的JVM自由發(fā)揮,但對于初始化階段JVM有明確要求(被引用)旭斥,自然初始化之前的階段也必須完成容达。
類的引用方式
主動(dòng)引用
1、當(dāng)使用new關(guān)鍵字實(shí)例化對象時(shí)垂券,當(dāng)讀取或者設(shè)置一個(gè)類的靜態(tài)字段(被final修飾的除外)時(shí)花盐,以及當(dāng)調(diào)用一個(gè)類的靜態(tài)方法時(shí)(比如構(gòu)造方法就是靜態(tài)方法),如果類未初始化菇爪,則需先初始化算芯。
2、通過反射機(jī)制對類進(jìn)行調(diào)用時(shí)凳宙,如果類未初始化也祠,則需先初始化。
3近速、當(dāng)初始化一個(gè)類時(shí)诈嘿,如果其父類未初始化,先初始化父類削葱。
4奖亚、用戶指定的執(zhí)行主類(含main方法的那個(gè)類)在虛擬機(jī)啟動(dòng)時(shí)會(huì)先被初始化。
被動(dòng)引用
除了上面這4種方式析砸,所有引用類的方式都不會(huì)觸發(fā)初始化昔字,稱為被動(dòng)引用。如:
通過子類引用父類的靜態(tài)字段首繁,不會(huì)導(dǎo)致子類初始化作郭;
通過數(shù)組定義來引用類,不會(huì)觸發(fā)此類的初始化弦疮;
引用類的靜態(tài)常量不會(huì)觸發(fā)定義常量的類的初始化夹攒,因?yàn)槌A吭诰幾g階段已經(jīng)被放到常量池中了。
初始化的對象會(huì)被放在什么地方呢胁塞?
內(nèi)存分配策略
兩個(gè)存儲(chǔ)位置:本地線程緩存TLAB和堆
新對象產(chǎn)生時(shí)首先檢查本地線程是否開啟了緩存咏尝,是則存儲(chǔ)在TLAB压语,否則去堆中尋找位置。
堆又分了:Eden编检、兩個(gè)Survivor胎食、Tenured共4個(gè)區(qū),Eden與Survivor大小比是8:1允懂,Eden和Survivor稱為新生代厕怜,Tenured稱為老年代(JDK8已經(jīng)沒有持久代了)
當(dāng)新對象產(chǎn)生時(shí),存放在Eden蕾总,當(dāng)Eden放不下時(shí)觸發(fā)Minor GC酣倾,將Eden中存活的對象復(fù)制到一Survivor中。繼續(xù)存放對象到Eden谤专,當(dāng)Eden放不下時(shí)觸發(fā)Minor GC,將Eden和非空閑Survivor中存活的對象復(fù)制到空閑Survivor中午绳,往復(fù)操作置侍。每經(jīng)過一次Minor GC,對象的年齡加1拦焚,當(dāng)對象年齡達(dá)到閥值(默認(rèn)15)進(jìn)入Tenured蜡坊。如果在Minor GC期間發(fā)現(xiàn)存活對象無法放入空閑的Survivor區(qū),則會(huì)通過空間分配擔(dān)保機(jī)制使對象提前進(jìn)入Tenured赎败。如果在Survivor空間中的相同年齡的所有對象大小的總和大于Survivor空間的一半秕衙,年齡大于和等于該年的對象就可以直接進(jìn)入老年代,無需等到指定的閥值僵刮。
空間分配擔(dān)保機(jī)制:
在執(zhí)行Minor GC前, VM會(huì)首先檢查Tenured是否有足夠的空間存放新生代尚存活對象据忘,由于新生代使用復(fù)制收集算法,為了提升內(nèi)存利用率搞糕,只使用了其中一個(gè)Survivor作為輪換備份勇吊,因此當(dāng)出現(xiàn)大量對象在Minor GC后仍然存活的情況時(shí),就需要老年代進(jìn)行分配擔(dān)保窍仰,讓Survivor無法容納的對象直接進(jìn)入老年代汉规,但前提是老年代需要有足夠的空間容納這些存活對象。但存活對象的大小在實(shí)際完成GC前是無法明確知道的驹吮,因此Minor GC前针史,VM會(huì)先首先檢查老年代連續(xù)空間是否大于新生代對象總大小或歷次晉升的平均大小,如果條件成立, 則進(jìn)行Minor GC碟狞,否則進(jìn)行Full GC(讓老年代騰出更多空間)啄枕。然而取歷次晉升的對象的平均大小也是有一定風(fēng)險(xiǎn)的,如果某次Minor GC存活后的對象突增族沃,遠(yuǎn)遠(yuǎn)高于平均值的話射亏,依然可能導(dǎo)致?lián)J?Handle Promotion Failure近忙,老年代也無法存放這些對象了),此時(shí)就只好在失敗后重新發(fā)起一次Full GC(讓老年代騰出更多空間)智润。
分代的唯一理由就是優(yōu)化GC性能及舍,讓GC在固定區(qū)域工作。
GC
- Minor GC
在年輕代(Eden和Survivor)中執(zhí)行的GC - Major GC
在老年代(Tenured)中執(zhí)行的GC - Full GC
清理整個(gè)堆空間包括年輕代和老年代
垃圾回收最重要的一點(diǎn)是如何判斷對象為垃圾窟绷?
可達(dá)性分析算法
通過一系列稱為GC Roots的對象作為起點(diǎn)锯玛,然后向下搜索,搜索所走過的路徑稱為引用鏈/Reference Chain兼蜈,當(dāng)一個(gè)對象到GC Roots沒有任何引用鏈相連時(shí)攘残,即該對象不可達(dá),也就說明此對象是不可用的为狸,如圖:Object5歼郭、6、7雖然互有關(guān)聯(lián)辐棒,但它們到GC Roots是不可達(dá)的病曾,因此也會(huì)被判定為可回收的對象。
回收時(shí)的回收算法:
分代
根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊漾根,如JVM中的新生代泰涂、老年代,這樣就可以根據(jù)各年代特點(diǎn)分別采用最適當(dāng)?shù)腉C算法:
在新生代:每次垃圾收集都能發(fā)現(xiàn)大批對象已死辐怕,只有少量存活逼蒙。因此選用復(fù)制算法,只需要付出少量存活對象的復(fù)制成本就可以完成收集寄疏。
在老年代:因?yàn)閷ο蟠婊盥矢呤抢巍]有額外空間對它進(jìn)行分配擔(dān)保,就必須采用“標(biāo)記—清理”或“標(biāo)記—整理”算法來進(jìn)行回收陕截,不必進(jìn)行內(nèi)存復(fù)制妖泄,且直接騰出空閑內(nèi)存。
新生代-復(fù)制算法
該算法的核心是將可用內(nèi)存按容量劃分為大小相等的兩塊艘策,每次只用其中一塊蹈胡,當(dāng)這一塊的內(nèi)存用完,就將還存活的對象復(fù)制到另外一塊上面朋蔫,然后把已使用過的內(nèi)存空間一次清理掉罚渐。
這使得每次只對其中一塊內(nèi)存進(jìn)行回收,分配也就不用考慮內(nèi)存碎片等復(fù)雜情況驯妄,實(shí)現(xiàn)簡單且運(yùn)行高效荷并。
但由于新生代中的98%的對象都是生存周期極短的,因此并不需完全按照1:1的比例劃分新生代空間青扔,所以新生代劃分為一塊較大的Eden區(qū)和兩塊較小的Survivor區(qū)源织。
老年代-標(biāo)記清除算法
該算法分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所有需要回收的對象(可達(dá)性分析)翩伪, 在標(biāo)記完成后統(tǒng)一清理掉所有被標(biāo)記的對象。
該算法會(huì)有以下兩個(gè)問題:
1谈息、效率問題:標(biāo)記和清除過程的效率都不高缘屹;
2、空間問題:標(biāo)記清除后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片侠仇,空間碎片太多可能會(huì)導(dǎo)致在運(yùn)行過程中需要分配較大對象時(shí)無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集轻姿。
老年代-標(biāo)記整理算法
標(biāo)記清除算法會(huì)產(chǎn)生內(nèi)存碎片問題,而復(fù)制算法需要有額外的內(nèi)存擔(dān)甭叽叮空間互亮,于是針對老年代的特點(diǎn),又有了標(biāo)記整理算法余素。標(biāo)記整理算法的標(biāo)記過程與標(biāo)記清除算法相同豹休,但后續(xù)步驟不再對可回收對象直接清理,而是讓所有存活的對象都向一端移動(dòng)桨吊,然后清理掉端邊界以外的內(nèi)存威根。
分區(qū)
將整個(gè)堆空間劃分為連續(xù)的不同小區(qū)間,每個(gè)小區(qū)間獨(dú)立使用屏积,獨(dú)立回收。這樣做的好處是可以控制一次回收多少個(gè)小區(qū)間磅甩。
在相同條件下炊林,堆空間越大,一次GC耗時(shí)就越長卷要,從而產(chǎn)生的停頓也越長渣聚。為了更好地控制GC產(chǎn)生的停頓時(shí)間,將一塊大的內(nèi)存區(qū)域分割為多個(gè)小塊僧叉,根據(jù)目標(biāo)停頓時(shí)間奕枝,每次合理地回收若干個(gè)小區(qū)間(而不是整個(gè)堆),從而減少一次GC所產(chǎn)生的停頓瓶堕。
對象逃逸(點(diǎn)到為止)
本該銷毀的對象隘道,逃到了它處。
public class A {
public static Object obj;
public void globalVariableEscape() { // 給全局變量賦值郎笆,發(fā)生逃逸
obj = new Object();//new的對象本該在棧幀出棧時(shí)銷毀谭梗,但被外部static引用導(dǎo)致進(jìn)入方法區(qū)常量池
}
public Object methodEscape() { // 方法返回值,發(fā)生逃逸
return new Object();//new的對象本該在棧幀出棧時(shí)銷毀宛蚓,但被外部方法或線程引用激捏,導(dǎo)致對象只能在外部方法棧幀出棧或線程銷毀時(shí)被清理
}
public void instanceEscape() { // 實(shí)例引用發(fā)生逃逸
b = new B(this); //(示意而已凄吏,并不準(zhǔn)確)新建B對象時(shí)引用了A對象远舅,除B使用外闰蛔,其余無引用A,此時(shí)本可以回收A图柏,但B卻引用導(dǎo)致無法回收序六。循環(huán)引用就是A在引用B,導(dǎo)致互相引用都不能被回收爆办。
}
}
public class B {
public static Object obj;
public void instance(A a) { // 引用傳入的實(shí)例
obj = a;
}
}
對象的引用類型(強(qiáng)軟弱虛)
JVM中真正將一個(gè)對象判死刑至少需要經(jīng)歷兩次標(biāo)記過程:
第一個(gè)過程难咕,可達(dá)性分析算法
第二個(gè)過程,判斷這個(gè)對象是否需要執(zhí)行finalize()方法距辆。
第一次GC時(shí)余佃,對象在經(jīng)歷了可達(dá)性分析算法被標(biāo)記后,若對象重寫了finalize()方法且沒被執(zhí)行過則會(huì)被放入F-Queue隊(duì)列中跨算,否則回收爆土。
第二次GC時(shí),JVM會(huì)有一個(gè)優(yōu)先級(jí)比較低的線程去執(zhí)行隊(duì)列中對象的finalize()方法诸蚕,執(zhí)行只是觸發(fā)finalize()方法并不會(huì)確保方法一定完成步势,防止死循環(huán)或異常等情況導(dǎo)致對象不可被回收,這時(shí)第二次標(biāo)記完成對象被回收背犯。
只有當(dāng)對象存在引用鏈連接GC Roots時(shí)才確保不會(huì)被回收坏瘩,即對象為強(qiáng)引用。那么有些對象漠魏,我們希望在內(nèi)存充足的情況下不要回收倔矾,在內(nèi)存不足的時(shí)候再將其回收掉。如果只有強(qiáng)引用柱锹,那這個(gè)對象永遠(yuǎn)都不會(huì)被回收哪自。于是有了軟引用、弱引用禁熏、虛引用的概念壤巷。
- 強(qiáng)引用
即使OOM也不會(huì)被回收 - 軟引用
內(nèi)存不足時(shí)才會(huì)被回收 - 弱引用
只要GC就會(huì)被回收 - 虛引用
唯一的作用就是監(jiān)聽被回收
參考 https://blog.csdn.net/huachao1001/article/details/51547290
類卸載
卸載需要滿足三個(gè)條件:
1、該類所有的實(shí)例已經(jīng)被回收
2瞧毙、加載該類的ClassLoder已經(jīng)被回收
3胧华、該類對應(yīng)的java.lang.Class對象沒有被引用
JVM自帶的根類加載器、擴(kuò)展類加載器和系統(tǒng)類加載器宙彪,JVM本身會(huì)始終引用這些類加載器撑柔,因此條件2不會(huì)形成。
而這些類加載器則會(huì)始終引用它們所加載的類對象您访,因此條件3也不會(huì)形成铅忿。
唯一會(huì)被卸載的類只有自定義的類加載器加載的類。
Dalvik(DVM)
為什么大篇幅講JVM灵汪,因?yàn)镈alvik虛擬機(jī)是Google按照J(rèn)VM虛擬機(jī)規(guī)范定制的虛擬機(jī)檀训,所應(yīng)用的多是處理能力柑潦、內(nèi)存、和存儲(chǔ)等處理能力受限的設(shè)備峻凫,更符合移動(dòng)設(shè)備的環(huán)境要求渗鬼。
架構(gòu)
JVM基于棧架構(gòu),Dalvik基于寄存器架構(gòu)荧琼,因此讀寫速度較快
空間
可執(zhí)行程序的字節(jié)碼不同
JVM:java -> class -> jar
DVM:java -> class -> dex -> apk
jar由多個(gè)class構(gòu)成譬胎,而dex是由多個(gè)class合并構(gòu)成,消除了數(shù)據(jù)冗余節(jié)省了空間命锄,但合并后方法數(shù)變多堰乔,產(chǎn)生了方法數(shù)受限(65535)的問題。
沙盒
Dalvik虛擬機(jī)允許在內(nèi)存中創(chuàng)建多個(gè)實(shí)例脐恩,以隔離不同的應(yīng)用程序镐侯。這樣,當(dāng)一個(gè)應(yīng)用程序在自己的進(jìn)程中崩潰后驶冒,不會(huì)影響其它進(jìn)程的運(yùn)行苟翻。
ART
在Dalvik下,應(yīng)用每次運(yùn)行都需要通過即時(shí)編譯器(JIT)將字節(jié)碼轉(zhuǎn)換為機(jī)器碼骗污,即每次都要編譯加運(yùn)行崇猫,所以啟動(dòng)時(shí)間長。
ART則在應(yīng)用安裝時(shí)就預(yù)編譯字節(jié)碼到機(jī)器語言需忿,所以安裝過程較Dalvik時(shí)間長存儲(chǔ)空間占用更大诅炉,但應(yīng)用每次運(yùn)行時(shí)不需要編譯減少了CPU的負(fù)擔(dān),啟動(dòng)時(shí)間短贴谎。
補(bǔ)充內(nèi)容
JVM運(yùn)行參數(shù)
JVM參數(shù)
-Xms 初始堆內(nèi)存大小
-Xmx 最大堆內(nèi)存大小
-Xmn 新生代大小
-Xss 每個(gè)線程的堆棧大小
-XX:PermSize 非堆內(nèi)存汞扎,永久代大屑疚取(>256M)
-XX:MaxPermSize 非堆內(nèi)存擅这,永久代最大大小
-XX:SurvivorRatio Eden: Survivor大小
-XX:PretenureSizeThreshold 手動(dòng)設(shè)定大對象的標(biāo)準(zhǔn)
-XX:MaxTenuringThreshold 設(shè)置年齡閥值
-XX:+HandlePromotionFailure +開啟-禁用空間分配擔(dān)保
打印GC信息
-verbose:gc
-XX:+PrintGCDetails
設(shè)置使用的垃圾回收器,不同場景下適合不同的垃圾回收器
-XX:+UseSerialGC
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
JVM監(jiān)控
JDK自帶工具有
jps:java process status Java進(jìn)程狀態(tài)
jps -l 運(yùn)行主類全名
jps -m main所接收參數(shù)
jps -v jvm所接收參數(shù)jstat:監(jiān)控運(yùn)行程序的各項(xiàng)信息景鼠,參數(shù)很多自行百度
jinfo:查看調(diào)整虛擬機(jī)各項(xiàng)參數(shù)
jmap:堆信息工具
jhat:JVM Heap Analysis Tool
jstack:用來生成線程快照
jconsole:圖形界面的監(jiān)控工具
visualVM:功能最強(qiáng)大的工具仲翎,需要單獨(dú)下載
Tip
類加載過程中:加載與驗(yàn)證是并行執(zhí)行的