導(dǎo)言:
在數(shù)據(jù)結(jié)構(gòu)中,我們第一了解到了?;蚨褩?,它的結(jié)構(gòu)特點(diǎn)是什么呢?先進(jìn)后出,它的特點(diǎn)有什么用呢?我們?cè)谀睦锟梢允褂玫綏=Y(jié)構(gòu),棧結(jié)構(gòu)那么簡(jiǎn)單,使用這么久了為什么不用其它結(jié)構(gòu)替代?
一.程序在內(nèi)存中的分布
作為一個(gè)程序猿,我們應(yīng)該會(huì)常常跟代碼打交道,那么我們所編寫的程序或代碼,是怎么跑起來的,操作系統(tǒng)怎么調(diào)用的,怎么劃分的我們可以使用一個(gè)簡(jiǎn)單的圖來了解一下:
在圖片中,把我們的內(nèi)存一共分為了兩個(gè)部分,一個(gè)是操作系統(tǒng)的內(nèi)核區(qū),另外一個(gè)就是用戶區(qū)
對(duì)于操作系統(tǒng):?操作系統(tǒng)(英語:Operating System,縮寫:OS)是管理計(jì)算機(jī)硬件與軟件資源的系統(tǒng)軟件,同時(shí)也是計(jì)算機(jī)系統(tǒng)的內(nèi)核與基石。操作系統(tǒng)需要處理如管理與配置內(nèi)存、決定系統(tǒng)資源供需的優(yōu)先次序、控制輸入與輸出設(shè)備、操作網(wǎng)絡(luò)與管理文件系統(tǒng)等基本事務(wù)。操作系統(tǒng)也提供一個(gè)讓用戶與系統(tǒng)交互的操作界面。
操作系統(tǒng)內(nèi)核要負(fù)責(zé):程序調(diào)用,驅(qū)動(dòng)調(diào)用,內(nèi)存管理和分配,有興趣可以學(xué)學(xué)操作系統(tǒng),我們這里主要是看看用戶內(nèi)存區(qū)
用戶內(nèi)存區(qū)主要是用戶對(duì)計(jì)算機(jī)發(fā)出的指令,計(jì)算機(jī)要做出相應(yīng)的操作,用戶區(qū)也是我們最大化操作計(jì)算機(jī)的地方,啟動(dòng)一個(gè)APP,打開網(wǎng)站,鼠標(biāo)單擊,Linux中的一次 dir指令等等,都是我們?nèi)?,用戶在進(jìn)行操作和管理
我們寫的程序或代碼也是運(yùn)行在用戶空間上的
程序在用戶空間上加載出來六個(gè)部分:
- 代碼段
- 只讀數(shù)據(jù)段
- 初始化數(shù)據(jù)段
- 未初始化數(shù)據(jù)段
- heap堆
- stack棧
代碼段:就是我們寫的源生的一些語言指令,在操作系統(tǒng)也可以叫做臨界區(qū),他在運(yùn)行是不可改變的,又是純的
只讀數(shù)據(jù)段:就是我們?cè)诔绦蛑袑懙囊恍┏A?,比如c或Java中使用const修飾的變量,它們是全局唯一且不可變的,包括語言本省有一些自定義的宏都是不可變的
初始化和未初始化的數(shù)據(jù)段:都是一些申請(qǐng)的全局變量,有些是未初始化的,有些是初始化的,在程序運(yùn)行的時(shí)候就會(huì)被調(diào)用,都是數(shù)據(jù)段范圍
heap堆:這就是我們的重點(diǎn)了,它的作用是動(dòng)態(tài)申請(qǐng)空間的程序存放的地方,比如Java中我們new了一個(gè)對(duì)象,對(duì)象都存放在堆中的,還有c語言中malloc函數(shù),它們所申請(qǐng)的東西都是存放在堆中的
stack棧:這是程序運(yùn)行時(shí)被調(diào)用最頻繁的了,它的結(jié)構(gòu)很適合存儲(chǔ)不長(zhǎng)時(shí)間存在的變量,以及斷點(diǎn)記錄,每當(dāng)一個(gè)函數(shù)執(zhí)行完,它就會(huì)釋放此次函數(shù)所產(chǎn)生的空間
補(bǔ)充:
我們仔細(xì)去看看圖片就會(huì)發(fā)現(xiàn),堆空間是向上擴(kuò)張的,??臻g是向下擴(kuò)張的,除了這兩個(gè)空間,其它的空間都是靜態(tài)資源,即在編譯時(shí)分配的空間在執(zhí)行的時(shí)候所需的空間還是那么大
??臻g是緊貼內(nèi)核區(qū)的,因?yàn)闂T诔绦蜻\(yùn)行時(shí)本身就需要大量的操作,即使在內(nèi)存中,把數(shù)據(jù)送到cpu或內(nèi)核都是要開銷的,所以為了使計(jì)算機(jī)可以快起來,棧作為操作多的結(jié)構(gòu)就被選擇緊貼內(nèi)核
堆中存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)普遍都很大,包括對(duì)象這些操作集合,它們兩者都向中間靠,會(huì)不會(huì)相遇呢?其實(shí)中間的數(shù)據(jù)區(qū)空間是很大的,絕大多數(shù)情況不會(huì)
棧中存儲(chǔ)的都是值類型,指針也是值類型
二.JVM中的內(nèi)存模型
內(nèi)存模型分析
?在JVM中,
??臻g,負(fù)責(zé)的就是存放著函數(shù)調(diào)用產(chǎn)生的局部變量,每當(dāng)一個(gè)函數(shù)被調(diào)用它所產(chǎn)生的內(nèi)存開銷都會(huì)在棧中,隨著函數(shù)的結(jié)束會(huì)被銷毀,但是我們知道,在函數(shù)執(zhí)行的時(shí)候會(huì)有對(duì)象的創(chuàng)建,對(duì)象大多數(shù)情況都是存在堆空間的,
那么函數(shù)怎么執(zhí)行對(duì)象呢?其實(shí)它會(huì)在棧中存放一個(gè)指針,用于指向某個(gè)對(duì)象,在棧中存在的是一個(gè)鏈接地址
堆空間是共有的,也就是一個(gè)線程創(chuàng)建出來的對(duì)象,另外一個(gè)對(duì)象可以繼續(xù)調(diào)用,對(duì)空間的執(zhí)行結(jié)構(gòu)就比棧更加復(fù)雜,它伴隨著內(nèi)存清理,堆空間的對(duì)象一旦不使用了就會(huì)被jvm的GC機(jī)制給清理掉,它和C語言的free()釋放空間一樣,都是對(duì)堆空間進(jìn)行操作,由于棧空間會(huì)被系統(tǒng)接管,所以很少有對(duì)棧空間的操作
本地方法棧,其實(shí)和Java語言沒什么關(guān)系,它是用于其它語言和Java兼容的,我們導(dǎo)入了其它語言的方法,就會(huì)被存在這里,包括C++等,jvm就是c++寫出來的,它交由jvm執(zhí)行而專門設(shè)置的一個(gè)棧
程序計(jì)數(shù)器,就和它的名字一樣,它是用來記錄程序執(zhí)行到那個(gè)位置的,它的所占空間需求很小,它會(huì)對(duì)??臻g的函數(shù)開始點(diǎn)和結(jié)束點(diǎn)進(jìn)行記錄
方法區(qū),它是具體的實(shí)現(xiàn),也是我們自定義的一些靜態(tài)方法存儲(chǔ)的地方,與之對(duì)應(yīng)還有一個(gè)元空間的東西,它和方法區(qū)的區(qū)別就是,方法區(qū)注重編寫接口,而元空間注重實(shí)現(xiàn)這些接口,在jdk1.8以前元空間是在堆空間的一塊區(qū)域,1.8以后就把它從堆空間中獨(dú)立出來了
??臻g(線程私有)
?我們可以看到一次函數(shù)執(zhí)行中,從括號(hào)開始,就會(huì)為傳進(jìn)來的參數(shù)創(chuàng)建空間,如果函數(shù)內(nèi)部有單獨(dú)聲明的變量,在這時(shí)候也會(huì)去申請(qǐng)空間
函數(shù)的計(jì)算階段,沒有額外的內(nèi)存開銷,如果對(duì)已有的變量進(jìn)行賦值,也是在開始創(chuàng)建的空間上完成覆蓋,這里a = 11進(jìn)行賦值,其實(shí)這個(gè)a去覆蓋了剛開始傳進(jìn)來空間開辟的a =10,所以沒有額外的空間開銷
當(dāng)遇到調(diào)用函數(shù)的結(jié)束大括號(hào) } 時(shí),就會(huì)結(jié)束函數(shù),相應(yīng)的在棧空間內(nèi)就會(huì)刪除相應(yīng)的在函數(shù)運(yùn)行時(shí)開辟的空間,所以一次調(diào)用完成以后,就像最左邊一樣,棧內(nèi)的空間就僅有主函數(shù)了
我們可以看看函數(shù)執(zhí)行的結(jié)果:
?我們?cè)趂un函數(shù)不是把值已經(jīng)改變成11了嘛,為什么主函數(shù)打印最后執(zhí)行還是輸出自己定義的a = 10呢?
這就是棧空間的特點(diǎn),基于這個(gè)特點(diǎn),我們可以很容易就實(shí)現(xiàn)了局部變量和全局變量,我們來細(xì)細(xì)的品一下這段代碼內(nèi)存的分布:
?從這個(gè)實(shí)例中我們可換一種思維來理解局部變量,或者說全局變量和局部變量本意上都是一樣的,在棧中也具有同等地位,不同的是局部變量的特點(diǎn)是朝生夕死的,而全局變量在棧中活到了最后
這也是為什么靜態(tài)變量,常量一般都是全局作用域,它們?cè)诔绦蜷_始的時(shí)候就已經(jīng)被壓到棧內(nèi)了,棧的特點(diǎn)是先進(jìn)后出,也就是說要把常量和靜態(tài)資源拿出來需要把絕大部分的變量方法和參數(shù)都要拿出來,符合這種情況的就只有程序快結(jié)束的時(shí)候,主函數(shù)也到了大括號(hào) }?
每執(zhí)行完成一個(gè)函數(shù)就刪除它所開辟的空間,對(duì)內(nèi)存利用的效率也是很高的
堆空間(程序公共訪問)
不同于??臻g的線程私有,所在堆中的對(duì)象都可以被任何棧中指針訪問,棧結(jié)構(gòu)總歸是有序的,但是堆空間是無序存儲(chǔ)的,不像??臻g有先進(jìn)后出這一特點(diǎn)
對(duì)象在堆空間中如果內(nèi)部屬性為基本變量,就會(huì)是自己就是一個(gè)實(shí)體,但是如果內(nèi)部屬性涵蓋了其它對(duì)象,它會(huì)以一個(gè)指針的形式去指向那個(gè)對(duì)象,如果對(duì)象存在,否則再去自己造一個(gè)對(duì)象指向它,很顯然是為了減少對(duì)象頻繁構(gòu)建的大量開銷
?我們可以看到圖中,id=1是直接賦值到自己的person對(duì)象內(nèi)部的,但是String類型的卻是用指針指向,因?yàn)镾tring也是一個(gè)對(duì)象,不是基本類型,對(duì)象都會(huì)以指針的形式去指向
且堆空間是公用的,也就是我們person對(duì)象執(zhí)行完成以后,其它對(duì)象需要用到String對(duì)象的時(shí)候,會(huì)直接鏈向這兩個(gè)String,而不是自己去創(chuàng)建
可能這些鏈接來自于不同的私有棧,但是都是可以的,對(duì)于堆的清理,我們就要知道jvm的GC機(jī)制了,它是專門負(fù)責(zé)清理堆中不會(huì)再使用的對(duì)象的,會(huì)使用到的對(duì)象還是一樣的會(huì)繼續(xù)存放再堆空間中
棧和堆與對(duì)象的關(guān)系
既然對(duì)象和變量是分別存在堆和棧中的,我們?cè)诤瘮?shù)執(zhí)行的時(shí)候主要還是對(duì)棧操作,那么當(dāng)棧中需要使用到對(duì)象的時(shí)候怎么辦呢?
?如圖,因?yàn)闂V兄淮鎯?chǔ)基本類型,所以使用棧要使用對(duì)象的時(shí)候,它會(huì)用一個(gè)指針去指向堆中的對(duì)象,一般對(duì)象的大小是不確定的,每個(gè)對(duì)象有它自己的大小,所以棧中存儲(chǔ)指針是基本類型它和int大小是一樣的,對(duì)棧的管理也更進(jìn)一步
值得注意的是,當(dāng)棧中開辟的函數(shù)空間被清理了以后,堆中的空間不會(huì)立馬被清理,可能其它的棧正在調(diào)用,或者還沒有觸發(fā)jvm的GC機(jī)制
這樣的設(shè)定又變相的解決了堆中對(duì)象的共享問題
省流:??臻g中存儲(chǔ)的都是值類型,指針也是值類型,而堆中存儲(chǔ)的都是對(duì)象
三.JVM的GC機(jī)制(堆特別篇)
我們從jvm的內(nèi)存模型中分析了,堆的中產(chǎn)生的對(duì)象在一個(gè)棧使用完了以后不會(huì)立即清理,而是要等到GC機(jī)制來清理,我們就來看看GC機(jī)制是怎么清理的
- 首先我們得知道什么樣的情況可以清理
- 正在使用的不能清理
- 間接被其它對(duì)象調(diào)用的不能清理
- 本地方法棧和方法區(qū)(元空間)的對(duì)象不能清理
除了這些情況其它都是可以清理的,這樣GC機(jī)制就會(huì)啟動(dòng)去清理這些不在此范圍內(nèi)的對(duì)象
清理方案(三種)
1.標(biāo)記清理
標(biāo)記清理指的是GC對(duì)堆中的對(duì)象先進(jìn)行掃描,那些對(duì)象沒有用了可以清理的就打上標(biāo)記,然后掃描完了統(tǒng)一清理有標(biāo)記的
?這種方式執(zhí)行起來簡(jiǎn)單,實(shí)現(xiàn)邏輯清楚,但是有個(gè)很明顯的缺點(diǎn),在刪除完成后,會(huì)有內(nèi)碎片,相信學(xué)過操作系統(tǒng)的堆內(nèi)碎片一定很熟悉,尤其是程序的對(duì)象不一,會(huì)導(dǎo)致內(nèi)碎片越來越多
2.標(biāo)記整理
標(biāo)記整理是基于標(biāo)記清理實(shí)現(xiàn)的,它在清理完成以后會(huì)把對(duì)象的位置向前移動(dòng),使得后面可以空出來大片區(qū)域
?標(biāo)記整理的方式,的確非常符合我們對(duì)堆空間的清理,但是它的開銷很大,每次清理都伴隨著大量的移動(dòng)
3.分區(qū)復(fù)制
?分區(qū)指的是把堆空間分為兩個(gè)部分,在進(jìn)行清理標(biāo)記的時(shí)候,把沒有標(biāo)記的分到另外一個(gè)半?yún)^(qū),表示對(duì)象還在使用,然后把此半?yún)^(qū)的全部清理掉,以供它們交替工作
?分區(qū)復(fù)制的缺點(diǎn)也很明顯,它需要兩倍的內(nèi)存,開銷也是非常大的
JVM中的GC機(jī)制
那么在jvm中,它的對(duì)象清理機(jī)制是如何實(shí)現(xiàn)的呢?肯定比上面三種更合理
?文章來源地址http://www.zghlxwxcb.cn/news/detail-471022.html
?jvm中的堆空間大致被分為了兩個(gè)比較大的部分,一個(gè)是老年代,即old,一個(gè)是年輕代,即young
年輕代中裝的是E區(qū)和S區(qū),E區(qū)即Eden,想必大家都很熟悉,就是伊甸園的意思,是亞當(dāng)和夏娃偷食禁果產(chǎn)生新生命的地方,在jvm中也是一樣的每個(gè)新new出來的對(duì)象都是出生在E區(qū)的,S區(qū)對(duì)應(yīng)的就是Survivor,即幸存的意思
老年代中裝的是在S區(qū)存活6次都沒被清理掉的對(duì)象,因?yàn)殚_發(fā)者認(rèn)為6次都沒被清理掉,說明這個(gè)對(duì)象可能會(huì)存活更久或者存活到到最后
young區(qū)工作過程
?我們知道了E區(qū)是裝載新對(duì)象的地方,當(dāng)Eden區(qū)快要裝滿時(shí),觸發(fā)GC的時(shí)候,就會(huì)有一次掃描標(biāo)記,然后將沒有標(biāo)記的復(fù)制到S區(qū)中區(qū),表示存活的對(duì)象,然后清理全部的E區(qū)和另外的一個(gè)S區(qū),我們有兩個(gè)S區(qū)其實(shí)是交替工作的,這次復(fù)制到S0區(qū),下次必復(fù)制到S1區(qū),
E區(qū)要滿的時(shí)候就會(huì)觸發(fā)youngGC因?yàn)槭窃趛oung區(qū)觸發(fā) 的也很好理解,每一次復(fù)制都會(huì)有一個(gè)年齡標(biāo)志,當(dāng)一個(gè)對(duì)象達(dá)到6歲的時(shí)候就會(huì)被復(fù)制到old區(qū)中區(qū),表示它會(huì)存活更長(zhǎng)或者存活到最后
我們可以看到S區(qū)比E區(qū)要小,其實(shí)在JVM的設(shè)定中S0:S1:E 是 1:1:8,也就是E區(qū)要比S區(qū)大很多,這是因?yàn)閷?duì)象也滿足朝生夕死的特點(diǎn),每次觸發(fā)GC的時(shí)候大部分的對(duì)象都會(huì)被標(biāo)記清理
補(bǔ)充:
還有一個(gè)FullGC,指的是old區(qū)要滿的時(shí)候觸發(fā)的,old區(qū)要是滿了,會(huì)造成程序異常終止,JVM會(huì)專門來處理FullGC,同時(shí)會(huì)順帶的觸發(fā)youngGC,所以每次FullGC必youngGC,但是一般不會(huì)FullGC,因?yàn)閛ld區(qū)很大,
FullGC的清理方式是標(biāo)記整理,也就是那三種清理方法的前兩種,而youngGC是復(fù)制也就是第三種清理方法,youngGC是專門為對(duì)象的朝生夕死而設(shè)計(jì)的
JVM中的垃圾收集器(了解)
由于我們的堆是有兩部分構(gòu)成的,老年代和年輕代,所以垃圾收集器也是分別有兩種:
解釋:?
Serial是單線程的移動(dòng)復(fù)制用于young區(qū),與之對(duì)應(yīng)的就是Serial? Old 是標(biāo)記整理用于old區(qū),會(huì)發(fā)生STW
PawNew是多線程的移動(dòng)復(fù)制和Serial其它都一樣,而CMS則是服務(wù)器段old區(qū)使用最多的垃圾回收器他不會(huì)造成STW
parallel Scavenge 則是更注重吞吐量,在客戶端使用最多的就是它,單次執(zhí)行很滿,但整體的效率很高,與之對(duì)應(yīng)就是old區(qū)的Parallel? Old
?注:STW指的是stop-the-world,簡(jiǎn)稱 STW,指的是 GC 事件發(fā)生過程中,會(huì)產(chǎn)生應(yīng)用程序的停頓。停頓產(chǎn)生時(shí)整個(gè)應(yīng)用程序線程都會(huì)被暫停,沒有任何響應(yīng),有點(diǎn)像卡死的感覺,這個(gè)停頓稱為 STW。
由于young區(qū)的空間小,所以STW時(shí)間短,但是old區(qū)要是發(fā)生了GC,STW停頓時(shí)間很長(zhǎng),這也是服務(wù)器端為什么要使用CMS的原因,它不會(huì)發(fā)生STW現(xiàn)象,因?yàn)樗捎玫氖菢?biāo)記清理,但是缺點(diǎn)我們也知道,會(huì)有內(nèi)碎片
young區(qū)的算法都是移動(dòng)復(fù)制,就不用說了,并且會(huì)發(fā)生短暫的STW,我們可以著重看看CMS,這個(gè)服務(wù)器端常用的垃圾收集器
CMS:
- 初次標(biāo)記,GCRoot對(duì)象,會(huì)發(fā)生STW
- 并發(fā)標(biāo)記,所有old區(qū)的對(duì)象
- 從新標(biāo)記,修正第二步(有可能在標(biāo)記的時(shí)候又產(chǎn)生了廢棄對(duì)象)會(huì)發(fā)生STW
- 并發(fā)清理,標(biāo)記清理
此外,G1垃圾收集器是從jdk1.9開始沿用的,它對(duì)堆的劃分就不像這幾種那么規(guī)律了,它提出了一個(gè)堆空間的新型劃分概念,有興趣的朋友可以去了解一下
四.拓展
Java逃逸分析機(jī)制:
逃逸分析在jdk6以后是默認(rèn)開啟的
逃逸分析指的是它會(huì)分析對(duì)象如果是在當(dāng)前函數(shù)使用完了不存,因而改為在棧上申請(qǐng)空間,而棧運(yùn)行完就清理,所以不需要等GC,大大緩解了GC的壓力
如果不是當(dāng)前函數(shù)范圍則不會(huì)在棧上,而是在堆上申請(qǐng)
對(duì)象的大小計(jì)算:
?非數(shù)組對(duì)象,頭部大小是固定的12字節(jié),數(shù)組對(duì)象則多了4字節(jié)為16字節(jié)
內(nèi)容會(huì)根據(jù)具體的基本類型和其它對(duì)象大小而定
如果我此對(duì)象中有兩個(gè)int變量,那么對(duì)象大小就是 12+8=20,但結(jié)果會(huì)是24,因?yàn)镴ava為了使對(duì)象在空間上對(duì)齊,會(huì)對(duì)不滿足8倍數(shù)的大小對(duì)象強(qiáng)制擴(kuò)大,24是8的倍數(shù)所以可以,
這里就會(huì)有個(gè)小tips,那你會(huì)發(fā)現(xiàn)一個(gè)對(duì)象中兩個(gè)int型和三個(gè)int型的對(duì)象大小是一樣的
如果此對(duì)象的一個(gè)屬性是對(duì)象的的話,會(huì)取決于被引用對(duì)象的基本類型和它包含的其它對(duì)象
分析:
地址:是此對(duì)象在堆中的實(shí)際位置
標(biāo)記:記錄了hash值,是否有鎖,年齡(是否進(jìn)入old區(qū)使用的)
數(shù)組長(zhǎng)度:記錄了數(shù)組的下標(biāo),這也是為什么數(shù)組對(duì)象長(zhǎng)度為一個(gè)int的大小,它們是等字節(jié)的文章來源:http://www.zghlxwxcb.cn/news/detail-471022.html
?
到了這里,關(guān)于《數(shù)據(jù)結(jié)構(gòu)》之棧和堆結(jié)構(gòu)及JVM簡(jiǎn)析的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!