湘潭哪里做網(wǎng)站排名前十的大學(xué)
目錄
一. 內(nèi)存區(qū)域劃分
1.本地方法棧(Native Method Stacks)
2.虛擬機(jī)棧(JVM Stacks)
3.程序計(jì)數(shù)器(Program Counter Register)
4.堆(Heap)
5.元數(shù)據(jù)區(qū)(Metaspace)
二.類加載機(jī)制
1.加載
2.驗(yàn)證
3.準(zhǔn)備
4.解析
5.初始化
"雙親委派模型"
三. GC 垃圾回收機(jī)制
GC實(shí)際工作過(guò)程
1.找到垃圾/判定垃圾
(1).引用計(jì)數(shù)(Java沒(méi)有使用)
(2).可達(dá)性分析(Java使用的)
2.如何清理垃圾
(1).標(biāo)記清除?編輯
(2).復(fù)制算法?編輯
(3).標(biāo)記整理?編輯
"分代回收"
一. 內(nèi)存區(qū)域劃分
正如一個(gè)公司的運(yùn)作模式,首先公司需要一塊場(chǎng)地,再接著需要根據(jù)公司的各個(gè)部分,將這塊場(chǎng)地劃分成一塊一塊的部門,每個(gè)部門都有獨(dú)立的功能。
JVM的內(nèi)存區(qū)域劃分也是如此,JVM啟動(dòng)的時(shí)候會(huì)申請(qǐng)一整個(gè)很大的內(nèi)存區(qū)域(JVM是個(gè)應(yīng)用程序,要從操作系統(tǒng)中申請(qǐng)內(nèi)存),接著根據(jù)需求,將整個(gè)空間分成幾個(gè)部分。
1.本地方法棧(Native Method Stacks)
native表示JVM內(nèi)部的C++代碼(JVM是用C++寫的),所以本地方法棧就是給調(diào)用native方法(JVM內(nèi)部方法)準(zhǔn)備的??臻g
問(wèn)題:C++代碼是不是不用跑在虛擬機(jī)上 答:像C、C++、Go、Rust都是把代碼變成成native code,也就是可以直接被cpu識(shí)別的機(jī)械指令 而Java、Python、PHP為了跨平臺(tái),都是統(tǒng)一翻譯成指定的字節(jié)碼(字節(jié)碼都是一樣的),然后由對(duì)應(yīng)的虛擬機(jī)轉(zhuǎn)換成機(jī)械指令
2.虛擬機(jī)棧(JVM Stacks)
給Java代碼使用的棧
注意:stack棧,在之前數(shù)據(jù)結(jié)構(gòu)中也學(xué)習(xí)過(guò),數(shù)據(jù)結(jié)構(gòu)中的“?!笔恰昂筮M(jìn)先出”的,和這里所說(shuō)的棧不是一個(gè)東西。
這里所提的棧,是JVM中的一個(gè)特定的空間。
- 對(duì)于JVM虛擬機(jī)棧,這里存儲(chǔ)的是方法之間的調(diào)用關(guān)系
- 對(duì)于本地方法棧,存儲(chǔ)的是native方法之間的調(diào)用關(guān)系
整個(gè)棧空間內(nèi)部,可以認(rèn)為是包含很多個(gè)元素,每個(gè)元素表示一個(gè)方法,把這里的每個(gè)元素,稱之為“棧幀”,這一個(gè)棧幀里,會(huì)包含這個(gè)方法的入口地址、方法參數(shù)、返回地址、局部變量.....
?
但是由于函數(shù)調(diào)用中,也有“后進(jìn)先出”的特點(diǎn),因此此處的棧也是后進(jìn)先出的,只是數(shù)據(jù)結(jié)構(gòu)中的棧是一個(gè)更通用的,更廣泛的概念,而JVM中的棧是特指JVM上的一塊內(nèi)存空間。
從內(nèi)存劃分圖中也可以看出,這里的棧有多個(gè),每個(gè)線程有一個(gè)(線程是一個(gè)獨(dú)立的執(zhí)行流),使用jconsole(jdk下bin目錄中)可以查看java進(jìn)程內(nèi)部的情況,點(diǎn)擊線程就可以看到線程調(diào)用棧的情況。
問(wèn)題:這些線程每次創(chuàng)建都分相同空間,那么棧會(huì)不會(huì)被很快就用完了? 答:棧的整體大小一般都不會(huì)很大,但是每個(gè)棧幀的大小也是比較小的,遇到無(wú)限遞歸的代碼會(huì)遇到棧溢出的情況,就正常寫代碼,創(chuàng)建銷毀線程一般是不需要擔(dān)心這個(gè)問(wèn)題的。
3.程序計(jì)數(shù)器(Program Counter Register)
記錄當(dāng)前線程執(zhí)行到哪條指令(很小的一塊存一個(gè)地址),也是每個(gè)線程有一份。
4.堆(Heap)
堆是整個(gè)JVM空間的最大的區(qū)域,new出來(lái)的對(duì)象,都是在堆上的,類的成員對(duì)象也就在堆上了
-
堆 是一個(gè)進(jìn)程只有一份的
-
棧 是每個(gè)線程有一份,一個(gè)進(jìn)程有多個(gè)線程
-
堆,多個(gè)線程用的都是同一個(gè)堆
-
棧,每個(gè)線程用自己的棧
每個(gè)jvm就是一個(gè)java進(jìn)程
5.元數(shù)據(jù)區(qū)(Metaspace)
以前這個(gè)區(qū)域叫做方法區(qū),java8開(kāi)始改為了元數(shù)據(jù)區(qū) 這里存儲(chǔ)的有類對(duì)象、常量池、靜態(tài)成員
元數(shù)據(jù)區(qū)也是一個(gè)進(jìn)程一份,多個(gè)線程共用這一份
問(wèn)題:final修飾的變量在這里存儲(chǔ)嗎? 答:不一定,有一定概率被優(yōu)化成字面值常量,也有可能沒(méi)優(yōu)化
總結(jié)
- 局部變量在棧
- 普通成員變量在堆
- 靜態(tài)成員變量在方法區(qū)/元數(shù)據(jù)區(qū)
二.類加載機(jī)制
準(zhǔn)確的來(lái)說(shuō),類加載就是將.class文件,從文件(硬盤)被加載到內(nèi)存中(元數(shù)據(jù)區(qū))這樣的過(guò)程。
1.加載
- (1).通過(guò)一個(gè)類的全限定名來(lái)獲取這個(gè)類的二進(jìn)制字節(jié)流
- (2).將這個(gè)字節(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ì)象
但是對(duì)應(yīng)上述的要求并不是具體的,JVM的實(shí)現(xiàn)過(guò)程和應(yīng)用是比較靈活的,比如獲取類的二進(jìn)制字節(jié)流,并沒(méi)有說(shuō)明如何獲取,所以就有了從壓縮包中獲取(jar、war、ear),從網(wǎng)路中獲取(Applet),運(yùn)行時(shí)計(jì)算生成(動(dòng)態(tài)代理)
對(duì)于不是數(shù)組的類的加載我們可以自定義去控制字節(jié)流的獲取方式,而數(shù)組類就不一樣了,因?yàn)閿?shù)組類本身不是通過(guò)類加載進(jìn)行創(chuàng)建的,而是由JVM直接創(chuàng)建
2.驗(yàn)證
根據(jù)jvm虛擬機(jī)規(guī)范,檢查.class文件的格式是否符合要求
3.準(zhǔn)備
給類對(duì)象分配內(nèi)存空間(此時(shí)內(nèi)存初始化成全0),在這個(gè)階段里,為靜態(tài)變量分配內(nèi)存并設(shè)置靜態(tài)變量初始值。這里說(shuō)的初始值通常情況下,不是代碼中寫的初始值,而是數(shù)據(jù)類型的零值。代碼中寫的初始值,是在初始化階段賦值的。如果是靜態(tài)常量(被final修飾),這個(gè)階段就會(huì)被直接賦值為代碼中寫的初始值
4.解析
針對(duì)字符串常量進(jìn)行初始化,把符號(hào)引用轉(zhuǎn)為直接引用
字符串常量,需要有一塊內(nèi)存空間存這個(gè)字符的實(shí)際內(nèi)容,還需要一個(gè)引用,來(lái)保存這個(gè)內(nèi)存空間的起始地址。 在類加載之前,字符串常量是處在.class文件中的,這個(gè)時(shí)候沒(méi)有地址這個(gè)概念,此時(shí)這個(gè)"引用"記錄的并非是真正的地址,而是他在文件中的"偏移量"(或者是個(gè)占位符)。 在類加載之后,才真正把這個(gè)字符串常量給放到內(nèi)存中,此時(shí)才有"內(nèi)存地址",這個(gè)原因才能真正賦值成指定的內(nèi)存地址。
5.初始化
真正針對(duì)類對(duì)象里面的內(nèi)容進(jìn)行初始化,加載父類、執(zhí)行靜態(tài)代碼塊的代碼.....
總結(jié)
問(wèn)題:一個(gè)類,什么時(shí)候會(huì)被加載呢? 答:不是java程序一運(yùn)行就將所有的類都加載了,而是真正用到的時(shí)候才加載(賴漢模式),比如構(gòu)造類的實(shí)例、調(diào)用這個(gè)類的靜態(tài)方法/使用靜態(tài)屬性、加載子類,就會(huì)先加載父類。 而一旦加載過(guò)后,后續(xù)再使用就不需要重新加載了。
"雙親委派模型"
JVM 默認(rèn)提供了三個(gè)類加載器
- BootstrapClassLoader 負(fù)責(zé)加載標(biāo)準(zhǔn)庫(kù)中的類
- ExtensionClassLoader 負(fù)責(zé)加載JVM擴(kuò)展庫(kù)中的類
- ApplicationClassLoader 負(fù)責(zé)加載用戶提供的第三方庫(kù),用戶項(xiàng)目代碼中的類
而上述三個(gè)類存在"父子關(guān)系",BootstrapClassLoader為最大,ApplicationClassLoader為最小。
此處的"父子關(guān)系",不是父類、子類,而是每個(gè)ClassLoader中都有一個(gè)parent屬性指向自己的父 類加載器
上述三個(gè)類加載器的工作模式:
首先加載一個(gè)類的時(shí)候,先從ApplicationClassLoader開(kāi)始
但是ApplicationClassLoader會(huì)把加載任務(wù)交給父親,于是ExtensionClassLoader就會(huì)去加載,但是也不是真加載,而是再委托自己的父親去加載
于是BootstrapClassLoader就去加載,他也想委托給父親,但是發(fā)現(xiàn)父親為null,所以就由自己加載,那么BootstrapClassLoader就會(huì)搜索自己負(fù)責(zé)的標(biāo)準(zhǔn)庫(kù)中的類,如果找到就加載,如果沒(méi)找到就交給自己的子類進(jìn)行加載
于是ExtensionClassLoader就去加載,搜索自己負(fù)責(zé)的JVM擴(kuò)展庫(kù)中的類,找到則加載,找不到就交給子類
于是ApplicationClassLoader進(jìn)行加載,搜索用戶用的第三方庫(kù)和項(xiàng)目中的類,找到則加載,找不到的話由于自己沒(méi)有子類,就只能拋出類找不到這種異常
使用這個(gè)順序進(jìn)行,主要的目的據(jù)說(shuō)為了保證Bootstrap能夠先加載,Application能夠后加載,這就避免了用戶創(chuàng)建一些奇怪的類而導(dǎo)致的bug
比如用戶寫了一個(gè)java.lang.String這個(gè)類,此時(shí)能保證JVM加載的還是標(biāo)準(zhǔn)庫(kù)中的類,而不會(huì)加載到用戶寫的這個(gè)類,這樣就能保證即使出現(xiàn)上述問(wèn)題,也不會(huì)讓jvm的代碼混亂,最多就是用戶寫的代碼不生效
三. GC 垃圾回收機(jī)制
首先要知道的是在JVM中進(jìn)行垃圾回收的是"堆",并且GC是以"對(duì)象"為基本單位進(jìn)行回收的
GC回收的是整個(gè)對(duì)象都不再使用的情況,而那種一部分使用,一部分不使用的對(duì)象先不回收了(一個(gè)對(duì)象里有很多屬性,可能其中10個(gè)屬性后面要用,10個(gè)屬性后面再也不用了)
GC實(shí)際工作過(guò)程
1.找到垃圾/判定垃圾
關(guān)鍵思路在于看看到底有沒(méi)有"引用"指向它,Java中使用對(duì)象只有一條路,通過(guò)引用來(lái)使用,如果一個(gè)對(duì)象有引用指向它那么就有可能被用到,如果沒(méi)有引用指向它那么就不可能被用到
那么怎么知道對(duì)象是否有引用指向?
(1).引用計(jì)數(shù)(Java沒(méi)有使用)
給每個(gè)對(duì)象分配一個(gè)計(jì)數(shù)器(整數(shù)),每次創(chuàng)建一個(gè)引用指向該對(duì)象,計(jì)數(shù)器就+1,每次該引用被銷毀了計(jì)數(shù)器就-1
問(wèn)題:這種方法很好理解,但是為什么Java不使用呢?
答: 1.引用計(jì)數(shù)內(nèi)存空間浪費(fèi)的多,每個(gè)對(duì)象都要分配一個(gè)計(jì)數(shù)器,計(jì)數(shù)器按照4個(gè)字節(jié)算,代碼中對(duì)象非常少無(wú)所謂,怕的就是對(duì)象特別多,并且每個(gè)對(duì)象都比較小,那么這個(gè)時(shí)候占用的額外空間就會(huì)很多(一個(gè)對(duì)象的體積是1k,多4個(gè)字節(jié)無(wú)所謂,一個(gè)對(duì)象體積是4個(gè)字節(jié),再多4個(gè)字節(jié)相當(dāng)于體積擴(kuò)大一倍)
2.存在循環(huán)引用問(wèn)題
(2).可達(dá)性分析(Java使用的)
Java中的對(duì)象,都是通過(guò)引用來(lái)指向并訪問(wèn)的,經(jīng)常是一個(gè)引用指向一個(gè)對(duì)象,這個(gè)對(duì)象里的成員又指向別的對(duì)象
整個(gè)Java中的所有對(duì)象,就通過(guò)類似這種樹(shù)形/鏈?zhǔn)浇Y(jié)構(gòu),整體串起來(lái)
可達(dá)性分析,就是把所有這些對(duì)象組織的結(jié)構(gòu)視為樹(shù),從根節(jié)點(diǎn)出發(fā),遍歷樹(shù),所有能被訪問(wèn)到的對(duì)象就標(biāo)記為"可達(dá)"(不能被訪問(wèn)到的,就是不可達(dá))
此處的圖中雖然只有root引用,但是上述6個(gè)對(duì)象都是可達(dá)的
- root-->a
- root.left-->b
- root.right-->c
- root.left.left-->d
- root.left.left.left-->e
小結(jié):可達(dá)性分析需要進(jìn)行類似于"樹(shù)遍歷"整個(gè)操作相比于引用計(jì)數(shù)來(lái)說(shuō)肯定要慢一些的,但是速度慢沒(méi)關(guān)系,上述遍歷操作,并不需要一直執(zhí)行,只需要每隔一段時(shí)間分析一遍即可
進(jìn)行可達(dá)性分析遍歷的起點(diǎn),稱為GCroots
1.棧上的局部變量
2.常量池中的對(duì)象
3.靜態(tài)成員變量
一個(gè)代碼中可能有很多這樣的GCroots,把每個(gè)起點(diǎn)都往下遍歷一遍就完成了掃描過(guò)程
2.如何清理垃圾
(1).標(biāo)記清除
標(biāo)記清理的特點(diǎn)是簡(jiǎn)單粗暴,但是會(huì)導(dǎo)致內(nèi)存碎片化問(wèn)題,被釋放的空閑空間是零散的,不是連續(xù)的
但是我們申請(qǐng)內(nèi)存的要求是需要連續(xù)的空間,總的空閑空間可能很大,但是每個(gè)具體的內(nèi)存空間可能很小,可能導(dǎo)致申請(qǐng)大一點(diǎn)的內(nèi)存就失敗了
例如總的空閑空間是10k,分成1k一個(gè),一共十個(gè),此時(shí)如果需要申請(qǐng)2k的空間,那么就會(huì)申請(qǐng)失敗
(2).復(fù)制算法
復(fù)制算法解決了內(nèi)存的碎片化問(wèn)題
復(fù)制算法把整個(gè)內(nèi)存分為兩半,用一半丟一半
把不是"垃圾"的對(duì)象復(fù)制到另外一半內(nèi)存空間,然后把之前的一半整個(gè)空間刪掉
若是后續(xù)再觸發(fā)GC,那么就在右邊這一半進(jìn)行復(fù)制算法到左邊即可
缺點(diǎn):空間利用率低下,如果垃圾少,有效對(duì)象多,那么復(fù)制成本就比較大了
(3).標(biāo)記整理
標(biāo)記整理解決了復(fù)制算法的缺點(diǎn)
類似于順序表中的刪除中間元素,有元素搬運(yùn)的操作,這樣既保證了空間利用率,也解決了內(nèi)存碎片化問(wèn)題
但是,很明顯,這種做法,效率也不高,元素搬運(yùn)如果需要搬運(yùn)的空間大,那么開(kāi)銷也大
上述的三種方法都有其對(duì)應(yīng)的缺點(diǎn),那么有沒(méi)有比較完美的辦法呢?
"分代回收"
把垃圾回收,分成不同的場(chǎng)景,對(duì)應(yīng)不同的場(chǎng)景使用上述不同的辦法
那么如何定義上述的不同場(chǎng)景呢?也就是分代是如何分的?
這里的分,是基于一個(gè)經(jīng)驗(yàn)規(guī)律:如果一個(gè)東西(對(duì)象),存在的時(shí)間長(zhǎng)了,那么大概率還會(huì)繼續(xù)的長(zhǎng)時(shí)間存在下去(比如C語(yǔ)言從197x年就開(kāi)始誕生了,到如今還依然存在并且活躍,那么我們就有理由相信他還會(huì)存在很長(zhǎng)時(shí)間),而上述規(guī)律對(duì)應(yīng)Java中的對(duì)象也是有效的,java對(duì)象要么就是生命周期特別短,要么就是特別長(zhǎng)
根據(jù)上方的規(guī)律,我們給對(duì)象引入一個(gè)概念"年齡"(這個(gè)年的單位是熬過(guò)的GC的輪次),年齡越大那么存在的時(shí)間就越久
剛new出來(lái)的對(duì)象,年齡是0的對(duì)象,放到伊甸區(qū)(出自圣經(jīng),上帝在伊甸園造小人)
熬過(guò)一輪GC,對(duì)象就要被放到幸存區(qū)了,那么伊甸區(qū)-->幸存區(qū)就使用復(fù)制算法(將有效對(duì)象復(fù)制到幸存區(qū),伊甸區(qū)整體釋放)
到幸存區(qū)后,也要接受GC的考驗(yàn),如果變成垃圾就要被釋放,如果不是垃圾,那么就拷貝到另一個(gè)幸存區(qū)(這兩個(gè)幸存區(qū),同一時(shí)刻值用一個(gè),在兩個(gè)幸存區(qū)反復(fù)橫跳),所有這里也是使用復(fù)制算法(由于幸存區(qū)的體積不大,此處浪費(fèi)的空間也能接受)
如果這個(gè)對(duì)象在幸存區(qū)中GC了很多次了,那么這個(gè)時(shí)候就進(jìn)入到老年代,老年代都是年齡大的對(duì)象,生命周期普遍更長(zhǎng),那么針對(duì)老年代進(jìn)行的GC掃描頻率就低了,如果老年代的對(duì)象是垃圾了,那么就使用標(biāo)記整理的方式進(jìn)行釋放
小結(jié):
上述的GC中典型的垃圾回收算法:如何確定垃圾、如何清理垃圾,這里的策略,實(shí)際上在JVM實(shí)現(xiàn)的時(shí)候,會(huì)有一定的差異,JVM有很多的"垃圾回收實(shí)現(xiàn)"稱為"垃圾回收器",回收器的具體實(shí)現(xiàn)做法,會(huì)按照上述算法思想展開(kāi),不同的垃圾回收器側(cè)重點(diǎn)不同,有的追求GC掃描快、有點(diǎn)追求掃描好、有點(diǎn)追求用戶打擾少(STW短).....