電腦上的王者榮耀多大內(nèi)存
在1月14日,王者榮耀迎來(lái)了一波大更新,許多網(wǎng)友甚至表示這就是王者榮耀3.0版本。已經(jīng)更新過(guò)的玩家應(yīng)該能感受到,這次更新的內(nèi)容是非常多的。首先一進(jìn)游戲,就能發(fā)現(xiàn)游戲界面進(jìn)行的調(diào)整。相比之前的界面,新界...
2025.07.02COPYRIGHT ? 2023
粵ICP備2021108052號(hào)
郵箱:611661226@qq.com
留言給我計(jì)算機(jī)組成中內(nèi)存或者叫主存是非常重要的部件。內(nèi)存因?yàn)榈匚惶匾院虲PU直接相連,通過(guò)數(shù)據(jù)總線進(jìn)行數(shù)據(jù)傳輸,并通過(guò)地址總線來(lái)進(jìn)行物理地址的尋址。
除了數(shù)據(jù)總線、地址總線還有控制總線、IO總線等。IO總線是用來(lái)連接各種外設(shè)的,例如USB全稱(chēng)就是通用串行總線。再比如PCIE是目前最常見(jiàn)的IO總線之一。這里放一張B站硬件茶談的一張圖。
圖1-1 硬件圖
圖中CPU和左側(cè)內(nèi)存條直接連,并通過(guò)PCIE總線與下方的PCIE插槽連接,在PCIE插槽上可以插顯卡,網(wǎng)卡,聲卡,硬盤(pán)等等。PCIE帶寬是共享的,如果某個(gè)設(shè)備用了x1路帶寬,則能用的就少一路,因?yàn)楸举|(zhì)上每一路都是串行的。南橋和CPU之間也有PCIE通道,主要是提供給一些帶寬占用很低的外設(shè)。
南橋芯片位于主板上,一般在右下角,有個(gè)被動(dòng)散熱下面壓著。南橋中有個(gè)很重要的設(shè)備就是DMA控制器,或者叫DMAC。DMA直接內(nèi)存訪問(wèn),意思就是DMAC能夠直接訪問(wèn)內(nèi)存。即一般進(jìn)行IO的時(shí)候,cpu會(huì)把總線完全交給DMAC(DMAC和CPU會(huì)分時(shí)掌控總線),DMAC訪問(wèn)設(shè)備如磁盤(pán),將數(shù)據(jù)讀到內(nèi)存中,因?yàn)榇藭r(shí)接管了總線,所以可以寫(xiě)內(nèi)存。在這個(gè)過(guò)程中CPU可以進(jìn)行其他的任務(wù)。這也是異步IO、非阻塞IO等理論的基礎(chǔ)。
計(jì)算機(jī)常考題:
圖1-2-1 題目1
圖1-2-2 題目2
win32程序從程序上能操作的邏輯地址空間有4G這么大(雖然實(shí)際可能用不了那么多),4G的邏輯地址需要全部映射到物理內(nèi)存上。映射的最小單位如果是字節(jié)的話,映射表將會(huì)非常大,且效率低下。提出page概念,即最小的映射單位是一個(gè)page,一頁(yè)一般是4K這樣的大小,我的機(jī)器是這樣的,所以下面程序demo中頁(yè)大小都是4K。
顯然邏輯空間可能比實(shí)際要大,但是只要程序沒(méi)有用那么多內(nèi)存,就不需要去映射那么多page,且就算用了那么多內(nèi)存,也可以映射到磁盤(pán)上。
邏輯頁(yè)是抽象的,需要映射到物理的頁(yè)上,才能完成對(duì)內(nèi)存的操作。我們把邏輯頁(yè)叫頁(yè)(page)物理頁(yè)叫幀(page frame)。頁(yè)號(hào)-幀號(hào)的映射表叫頁(yè)表(page table)。
圖2-1 頁(yè)表映射
因?yàn)槊總€(gè)程序看到的邏輯地址空間都很大,所以程序變多了之后,程序使用的內(nèi)存大于了物理內(nèi)存,此時(shí)一般通過(guò)將部分"不著急使用"的頁(yè)映射到磁盤(pán)的方式來(lái)解決。所以頁(yè)表中映射項(xiàng)可能是磁盤(pán)。
圖2-2 頁(yè)表映射
同時(shí)每個(gè)進(jìn)程都有自己的專(zhuān)屬頁(yè)表,如下:
圖2-3 多進(jìn)程的頁(yè)表
一種實(shí)際情況,4G邏輯地址有32bit地址空間,假設(shè)pageSize=4K偏移量占12bit,因而頁(yè)表的邏輯頁(yè)號(hào)有20bit。再假設(shè)實(shí)際內(nèi)存條只有256M 28bit地址空間 12bit偏移量 16bit頁(yè)號(hào)。
邏輯地址0x 00001 1a3,去映射的時(shí)候00001就是邏輯頁(yè)號(hào),去查頁(yè)表發(fā)現(xiàn)映射到真實(shí)頁(yè)幀號(hào)00f3,然后偏移量不變還是1a3,最終就找到這個(gè)物理內(nèi)存內(nèi)容了。
圖2-4 頁(yè)表的映射過(guò)程
這個(gè)過(guò)程中,可能會(huì)出現(xiàn)映射的幀號(hào)是disk,即映射到了磁盤(pán)上。此時(shí)會(huì)觸發(fā)缺頁(yè)異常,進(jìn)入內(nèi)核態(tài),內(nèi)核從磁盤(pán)中讀取缺的這頁(yè)內(nèi)容,將其加載到物理內(nèi)存中。但是物理內(nèi)存的幀有可能所有幀都滿了,此時(shí)就需要逐出不太"重要"的幀。
逐出的過(guò)程需要判斷當(dāng)前物理頁(yè)(幀)是否是臟的(臟:與磁盤(pán)中內(nèi)容不一致,即從磁盤(pán)加載到物理內(nèi)存后被改過(guò)就是臟的),如果是臟的還需要更新磁盤(pán)中的內(nèi)容保證一致。
逐出后就騰出了位置給從磁盤(pán)中讀到的這頁(yè)的數(shù)據(jù),然后需要更新頁(yè)表的這一項(xiàng)的映射關(guān)系,將磁盤(pán)改為幀號(hào),然后重新進(jìn)行查頁(yè)表這一步。
邏輯層的作用:極大的降低了內(nèi)存碎片;借助磁盤(pán)可以實(shí)現(xiàn)"無(wú)限的內(nèi)存";各個(gè)進(jìn)程間內(nèi)存的安全性等。
一個(gè)地址中“住”的是一字節(jié)(8bit)的數(shù)據(jù)。
上面提到了邏輯-物理頁(yè)的映射,這就是頁(yè)表,但是上面的頁(yè)表其實(shí)除了簡(jiǎn)單的頁(yè)號(hào)映射,還存儲(chǔ)了其他一些屬性:是否有效,讀寫(xiě)權(quán)限,修改位,訪問(wèn)位(淘汰算法和TLB中用),是否是臟(被修改過(guò)就是臟的,因?yàn)樗陀脖P(pán)上的數(shù)據(jù)不一致),是否允許被高速緩存等等。
頁(yè)表存于主存中,每個(gè)進(jìn)程都有自己的頁(yè)表。
上面可以看到基于頁(yè)表的尋址,需要兩次訪問(wèn)主存(頁(yè)表是存在主存的),效率低下。為了提高速度,引入了快表,快表是頁(yè)表項(xiàng)的緩存,將最近一次的映射項(xiàng)存入快表,因?yàn)榭臻g有限所以需要逐出最老的那一項(xiàng)。快表的設(shè)計(jì)是基于經(jīng)驗(yàn):程序經(jīng)常訪問(wèn)的page一般就那幾個(gè),不會(huì)經(jīng)常頻繁的更換特別多的頁(yè)。
快表可能存于硬件MMU中(也可能是軟件TLB),一般只有8-256條,每個(gè)進(jìn)程都有自己的快表。
另一個(gè)值得討論的話題是頁(yè)表占用空間太大,上面例子中(32位程序256M機(jī)器pageSize4K)頁(yè)號(hào)有20bit即2百萬(wàn)個(gè),所以需要有1百萬(wàn)條,每條大小如果只算邏輯頁(yè)號(hào)(20bit)和物理頁(yè)號(hào)(16bit)的話:
36bit * 2^20 = 4.5MB
如果有64個(gè)這樣的程序在運(yùn)行...后果可想而知。
一種很好的解決方法是多級(jí)頁(yè)表,第一級(jí)頁(yè)表用于尋找第二級(jí)頁(yè)表的編號(hào)。<20bit-16bit>的單級(jí)映射可以改成<10bit-10bit>和<10bit-6bit>兩級(jí)映射。此時(shí)占用內(nèi)存為
20bit * 2^10 + 16bit * 2^20 = 2M
嚴(yán)格意義的分段是,每一段的虛擬地址都是從0開(kāi)始。然后頁(yè)表是段號(hào)+頁(yè)號(hào)來(lái)映射幀號(hào)的。但是這種形式已經(jīng)被廢棄了,只有x86 32位的intel的cpu還保留了這種段頁(yè)結(jié)合的方式,即嚴(yán)格意義的分段已經(jīng)用的很少。
那為什么還經(jīng)常聽(tīng)到段的概念?現(xiàn)在所說(shuō)的段一般是程序在邏輯層面保留的概念,對(duì)邏輯地址有個(gè)粗略的劃分,便于程序編寫(xiě),但是并不影響os的內(nèi)存管理(還是分頁(yè)管理)。
以32位程序?yàn)槔谶壿嬁臻g中最高的0xc0000000 - 0xffffffff這1G的內(nèi)存是給內(nèi)核留出的,這部分是所有進(jìn)程共享的。剩余3G內(nèi)存從低到高分別是Text、Data、Heap、Lib、Stack。64位程序則遠(yuǎn)大于這里的值。
Heap是從低往高增長(zhǎng),Stack是從高往低增長(zhǎng),且有個(gè)最大限制。Data存儲(chǔ)靜態(tài)變量Text存儲(chǔ)程序二進(jìn)制碼,Lib存儲(chǔ)庫(kù)函數(shù)需要占用的內(nèi)存,多個(gè)程序如果都使用了相同的庫(kù),內(nèi)存是共用的(共享內(nèi)存)。各個(gè)部分的留有隨機(jī)的一段偏移量,可以保護(hù)程序,這也使得每次重新執(zhí)行程序的時(shí)候變量所在的內(nèi)存地址總是不同的。
圖2-5 32位系統(tǒng)下內(nèi)存地址的組成
分段是邏輯空間上的,不影響分頁(yè)的內(nèi)存管理方式,后面進(jìn)行分頁(yè),映射到物理內(nèi)存上各部分跨多個(gè)頁(yè)其實(shí)并不連續(xù)。
cpu的三級(jí)緩存扮演著緩存主存數(shù)據(jù)的作用,而cache在內(nèi)存管理中的位置是怎樣的呢?
PIPT,物理級(jí)cache,cpu分析完映射關(guān)系,先到cache找有沒(méi)有該物理地址的cache。這樣會(huì)非常的慢,但是所有進(jìn)程可以共享cache。
VIVT,邏輯級(jí)cache,cpu直接通過(guò)邏輯地址找cache,miss后再查T(mén)LB頁(yè)表這些。這樣很快,但是邏輯地址只能對(duì)當(dāng)期進(jìn)程使用,其他進(jìn)程完全不能復(fù)用,尤其是庫(kù)函數(shù)這種共享的不能利用好cache。
VIPT,將兩者結(jié)合,用邏輯地址查找cache,cache中數(shù)據(jù)部分前面添加一個(gè)對(duì)應(yīng)物理地址的tag。這樣拿到這個(gè)tag后到tlb、頁(yè)表中查看下這個(gè)對(duì)應(yīng)關(guān)系是否正確,如果正確就直接讀cache。這樣速度和共享性都是折中的。
以上三種方式各有優(yōu)劣,在不同的cpu中可能使用的不一樣。
很多人想當(dāng)然的會(huì)認(rèn)為32位系統(tǒng)的虛擬地址是32位,這是沒(méi)錯(cuò)的,但是64位系統(tǒng)下真正的可用的虛擬地址卻不到64位。
#include int main(){int x = 10;printf("%p",&x)}
圖2-6 C語(yǔ)言打印地址
明顯看到是48位,雖然這個(gè)指針大小是8byte,但是只有48bit是有效的地址位,前面是多個(gè)0。通過(guò)cat /proc/cpuinfo最后幾行能看到物理地址和虛擬地址的大小,這主要是cpu單方面定制的,我的這臺(tái)機(jī)器是13年買(mǎi)的intel 酷睿i5 3230的CPU。當(dāng)然我的系統(tǒng)內(nèi)存只有2G,其實(shí)物理地址不會(huì)有43位,只是cpu最多支持43位物理地址。
圖2-7 cpuinfo中的虛擬地址和物理地址
小細(xì)節(jié):棧是僅次于內(nèi)核的高位地址,參考圖2-5. 所以看到前面這個(gè)地址基本能推算出分給內(nèi)核的虛擬空間應(yīng)該是0xffff ffff ffff - 0x8000 0000 0000。
在生活中我們經(jīng)常看到各種內(nèi)存的種類(lèi),比如在linux調(diào)用free -h的時(shí)候可以看到圖2-6的分類(lèi)。
在linux中通過(guò)free -h可以看到當(dāng)前系統(tǒng)的內(nèi)存情況:
圖2-8 free指令下的內(nèi)存分類(lèi)
mem是物理內(nèi)存,swap是交換分區(qū),是用來(lái)將內(nèi)存暫時(shí)放到磁盤(pán)上的。
total總內(nèi)存大小,used用戶(hù)使用的內(nèi)存大小,free空閑的內(nèi)存大小,shared共享內(nèi)存大小,buff/cache文件緩存大小,available可用內(nèi)存大小是free和buff/cache加起來(lái)。
total = used(含shared) + free+ buff/cache
這里需要理解buff/cache,他們?cè)诶弦恍┑膬?nèi)核中是分開(kāi)顯示的分別是buffer cache和page cache,都是對(duì)磁盤(pán)的緩存。其中buffer cache是硬件層面,對(duì)磁盤(pán)塊中的數(shù)據(jù)進(jìn)行緩存,緩存的單位當(dāng)然也是塊。而page cache是文件系統(tǒng)層面,對(duì)文件進(jìn)行緩存,緩存單位就是頁(yè)。buffer cache的提出非常的早,兩者并存時(shí)會(huì)遇到重復(fù)緩存了相同的內(nèi)容的情況。
較新的內(nèi)核已經(jīng)將兩者合并,或者說(shuō)將buffer cache合到了page cache。雖然也還是能緩存磁盤(pán)塊,但是存儲(chǔ)單位也是頁(yè)了。并且buffer使用前會(huì)先檢查page cahce是否已經(jīng)緩存了對(duì)應(yīng)內(nèi)容,如果是則直接指過(guò)去。在機(jī)器維度查看內(nèi)存的時(shí)候也能發(fā)現(xiàn)BufferCache都是0,因?yàn)槎己系搅藀ageCache,有Buffer的都是很老的內(nèi)核的機(jī)器。
buff/cache占用大,會(huì)不會(huì)影響后續(xù)程序申請(qǐng)內(nèi)存?
不會(huì),一旦用戶(hù)程序需要申請(qǐng)內(nèi)存,buff/cache就會(huì)釋放掉一部分。換句話說(shuō)buff/cache是在內(nèi)存比較空閑的時(shí)候,盡量利用一下來(lái)加速文件讀寫(xiě)的。如果有大哥需要用內(nèi)存,是會(huì)拱手讓出的。
如果想進(jìn)一步了解兩者的演化,這篇文章從內(nèi)核源碼的角度展示了,幾個(gè)理成本版本下buff cache 和 page cache的變化。
在windows任務(wù)管理器中又可以看到下圖的幾種狀態(tài)的內(nèi)存叫法,而在Jprofile查看jvm內(nèi)存的時(shí)候也有圖2-8的一些叫法。
圖2-9 windows任務(wù)管理器內(nèi)存分類(lèi)
圖2-10 jprofile內(nèi)存分類(lèi)
已提交的意思是已經(jīng)向操作系統(tǒng)申請(qǐng)了這么多的內(nèi)存,操作系統(tǒng)可以已經(jīng)給了這么多內(nèi)存了,但是也可能沒(méi)有給那么多。貼一張微軟自己的解釋如圖
圖2-11 幾種內(nèi)存的解釋
提交的內(nèi)存因?yàn)槭翘摂M內(nèi)存,并不一定系統(tǒng)會(huì)立刻給這么多,所以可能提交遠(yuǎn)超過(guò)物理內(nèi)存上限的大小。我之前看過(guò)一個(gè)視頻,小哥用malloc申請(qǐng)了130000+GB的內(nèi)存程序才退出,而如果在malloc后給申請(qǐng)的地址填寫(xiě)值,事情就不那么順利了。感興趣可以去看下這個(gè)視頻。當(dāng)然不了解C語(yǔ)言也沒(méi)關(guān)系我在本文后半段會(huì)用java的Unsafe同樣申請(qǐng)超過(guò)物理上限的內(nèi)存大小做demo。
內(nèi)核態(tài)、用戶(hù)態(tài)、內(nèi)核空間、用戶(hù)空間,是經(jīng)常說(shuō)起的概念。因?yàn)椴僮飨到y(tǒng)不允許用戶(hù)直接操作硬件,所以需要用戶(hù)程序通知內(nèi)核,內(nèi)核幫你下達(dá)指令給硬件。在進(jìn)行讀文件的時(shí)候,就需要用到磁盤(pán)這個(gè)設(shè)備,所以需要進(jìn)入內(nèi)核態(tài),將文件內(nèi)容讀到內(nèi)核buffer,然后拷貝到用戶(hù)buffer并從內(nèi)核態(tài)切換為用戶(hù)態(tài),程序才能真正拿到數(shù)據(jù)。
用戶(hù)態(tài)進(jìn)內(nèi)核態(tài),一般有三種觸發(fā)條件,中斷、異常和系統(tǒng)調(diào)用,中斷和異常有時(shí)候界限比較模糊,例如缺頁(yè)中斷也有地方叫缺頁(yè)異常。這里我們引出了系統(tǒng)調(diào)用,大多數(shù)需要主動(dòng)操作或讀寫(xiě)硬件的都是通過(guò)系統(tǒng)調(diào)用。例如讀寫(xiě)文件的open/read/write是系統(tǒng)調(diào)用,網(wǎng)絡(luò)傳輸常見(jiàn)的select/poll/epoll也是系統(tǒng)調(diào)用,申請(qǐng)內(nèi)存的malloc底層也是通過(guò)brk或mmap這倆系統(tǒng)調(diào)用實(shí)現(xiàn)的。
系統(tǒng)調(diào)用伴隨了很多設(shè)計(jì)的優(yōu)化,例如通過(guò)epoll等系統(tǒng)調(diào)用實(shí)現(xiàn)的IO多路復(fù)用提高了網(wǎng)絡(luò)包的處理效率,mmap、sendfile等系統(tǒng)調(diào)用實(shí)現(xiàn)的零拷貝,減少了用戶(hù)空間和內(nèi)核空間之間的數(shù)據(jù)拷貝和上下文切換次數(shù)等等。在java的NIO中有大量的函數(shù)是直接封裝了系統(tǒng)調(diào)用。
malloc小于128K(閾值可修改)的內(nèi)存時(shí),用的是brk申請(qǐng)內(nèi)存。C語(yǔ)言中sbrk(可函數(shù))是brk(系統(tǒng)調(diào)用)的簡(jiǎn)單封裝,下面代碼打印的值可以看出first因?yàn)樯暾?qǐng)了0大小,所以和second指針位置相同。而third則表示的是second的尾部地址。可以看到虛擬地址是連續(xù)分配的,brk其實(shí)就是向上擴(kuò)展heap的上界,配合查看圖2-5。
#include #include int main(){void *first = sbrk(0);void *second = sbrk(1);void *third = sbrk(0);printf("%p\n",first);printf("%p\n",second);printf("%p\n",third);}
圖3-1 brk代碼輸出
如果此時(shí)在 third+1地址處去初始化一個(gè)int值,是可以成功的,并不報(bào)錯(cuò)。
#include #include int main(){void *first = sbrk(0);void *second = sbrk(1);void *third = sbrk(0);int *p = (int *)third+1;*p = 1;}
這是因?yàn)轫?yè)大小是4K,sbrk(1)其實(shí)也是申請(qǐng)一頁(yè),所以third+1位置也是安全的。如果我們將second這行改為4096,那就是另一個(gè)故事了,會(huì)觸發(fā)段錯(cuò)誤。
void *second = sbrk(4096);
圖3-2 brk代碼輸出2
malloc大于128K的內(nèi)存時(shí),用的是mmap。
// addr傳NULL則不關(guān)心起始地址,關(guān)心地址的話應(yīng)傳個(gè)4k的倍數(shù),不然也會(huì)歸到4k倍數(shù)的起始地址。void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);//釋放內(nèi)存munmapint munmap(void *addr, size_t length);
mmap用法有兩種,一種是將文件映射到內(nèi)存,另一種空文件映射,也就是把fd傳入-1,就會(huì)從映射區(qū)申請(qǐng)到一塊內(nèi)存。malloc就是調(diào)用的第二種實(shí)現(xiàn)。
#include #include #include int main(){int* a =(int *) mmap(NULL, 100 * 4096, PROT_READ| PROT_WRITE, MAP_PRIVATE| MAP_ANONYMOUS, -1, 0);int* b =a;for(int i=0;i<100;i++){b = (void *)a + (i*4096);*b =1;}while(1){sleep(1);}}
這里提交400K內(nèi)存的申請(qǐng),并且在每頁(yè)中都進(jìn)行內(nèi)存的使用。可以看到不映射文件的話觸發(fā)的是minflt次數(shù)是100次。
圖3-3 進(jìn)程的內(nèi)存minflt
這里是mmap內(nèi)存的惰性加載,一開(kāi)始mmap100頁(yè)時(shí)其實(shí)都沒(méi)有分配給進(jìn)程,在用到的時(shí)候開(kāi)始真正拿到內(nèi)存,此時(shí)觸發(fā)minflt缺頁(yè),因?yàn)椴皇怯成涞奈募挥脧拇疟P(pán)中調(diào)內(nèi)存,所以是小錯(cuò)誤。但是仍是消耗性能的。
如果mmap是映射的磁盤(pán)文件,也會(huì)惰性加載,在初次加載或者頁(yè)被逐出后再加載的時(shí)候,也會(huì)缺頁(yè),這個(gè)時(shí)候就不是小錯(cuò)誤minflt了,而是majflt。例如下面使用mmap來(lái)讀文件。
#include #include #include #include #include #include int main(){sleep(4);int fd = open("./1.txt", O_RDONLY, S_IRUSR|S_IWUSR);struct stat sb;if(fstat(fd, &sb) == -1){perror("cannot get file size\n");}printf("file size is %ld\n",sb.st_size);char *file_in_memory = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);for(int i=0;i
下圖是線程監(jiān)聽(tīng)的結(jié)果,為了方便觀察我在開(kāi)始讀之前sleep 4s。可以看到紅框第一行,有一次majflt,這是第一次去讀文件,直接觸發(fā)了缺頁(yè)異常,且指向磁盤(pán)。是最耗時(shí)的錯(cuò)誤。
圖3-3-2 進(jìn)程mmap讀文件引發(fā)majflt
read和mmap都可以讀文件,前者有狀態(tài)轉(zhuǎn)換和多次拷貝,但是后者有缺頁(yè)中斷。在單純讀磁盤(pán)文件場(chǎng)景,兩者其實(shí)沒(méi)法在孰優(yōu)孰劣上有定論。
共享內(nèi)存是進(jìn)程間通信的一種方式,(管道 信號(hào) 信號(hào)量 套接字也是進(jìn)程通信的方式)。共享內(nèi)存的例子比比皆是,windows下最明顯,比如這個(gè)上傳文件的對(duì)話框就是共享內(nèi)存里的,同一時(shí)間windows下不會(huì)彈出兩個(gè)該對(duì)話框。再比如動(dòng)態(tài)鏈接庫(kù),也是共享內(nèi)存中的,多個(gè)進(jìn)程可以共享,兩個(gè)進(jìn)程mmap相同文件的方式可以實(shí)現(xiàn)共享內(nèi)存,shmget則是更廣泛的共享內(nèi)存的系統(tǒng)調(diào)用。
圖3-4 共享內(nèi)存的典型例子
共享內(nèi)存原理就是兩個(gè)進(jìn)程中頁(yè),映射到了相同的幀。代碼這里不寫(xiě)了,直接參考geeks這篇的代碼。
jvm內(nèi)存結(jié)構(gòu)主要如圖4-1.本文不想對(duì)“常考”的知識(shí)點(diǎn)再次進(jìn)行講解,網(wǎng)上有大量的文章來(lái)講內(nèi)存結(jié)構(gòu)各自的用途和GC相關(guān)的內(nèi)容,這里我就不展開(kāi)講了。下面幾節(jié)會(huì)講一些比較"冷門(mén)"的知識(shí)。
圖4-1 java的內(nèi)存五區(qū)
在另一篇講計(jì)算java對(duì)象大小的文章中提到,java對(duì)象是由對(duì)象頭,對(duì)象內(nèi)容組成,并且是8字節(jié)對(duì)齊的。其中對(duì)象頭有以下三部分組成:
我們這里來(lái)看下Klass,有沒(méi)有想過(guò)我們反射的時(shí)候操作的都是Class對(duì)象而不是這里的Klass,兩者關(guān)系是:
Klass是C++對(duì)象InstanceKlass,里面有個(gè)_java_mirror字段指向?qū)?yīng)的Class對(duì)象。
圖4-2 java對(duì)象頭指向metaspace
這里還提到了指針壓縮,64位系統(tǒng),如果jvm堆內(nèi)存小于32GB是可以開(kāi)啟指針壓縮的,此時(shí)Klass指針只需要4個(gè)字節(jié),同時(shí)對(duì)象指針也只需要4個(gè)字節(jié)。這里會(huì)衍生出兩個(gè)問(wèn)題:
第一個(gè)就是4字節(jié)最多表示2^32個(gè)地址,每個(gè)地址里住的是一個(gè)字節(jié),所以只能表示4GB,怎么還說(shuō)32G下都能壓縮呢?
因?yàn)椋荷厦嫣岬綄?duì)象都是8字節(jié)對(duì)齊,所以每個(gè)地址里住的是8字節(jié),所以可以表示32GB,實(shí)際地址移3位。
第二個(gè)問(wèn)題就是普通對(duì)象指針壓縮Compressed Object Pointers (“CompressedOops”),壓縮的是java堆上的對(duì)象的指針(引用)大小,而對(duì)象頭指向的是Klass,這是個(gè)C++的結(jié)構(gòu),這個(gè)指針也壓縮了嗎?
是的,CompressOops和CompressKlass是相伴而生,默認(rèn)同時(shí)開(kāi)啟的,Klass這部分需要連續(xù)的<4G的內(nèi)存,因?yàn)槭荂++結(jié)構(gòu),沒(méi)有8字節(jié)對(duì)齊限制,所以4字節(jié)只能在4G內(nèi)存上尋址,默認(rèn)大小是1G。
metaspace存儲(chǔ)的是類(lèi)的元數(shù)據(jù)信息,上面提到的Klass就是在metaspace中的,一般開(kāi)啟壓縮的metaspace有CompressClassSpace和NonClassSpace兩部分組成,其中前者內(nèi)存占用較少,是后者的5-100分之一,前者又叫壓縮類(lèi)空間,實(shí)際上這部分內(nèi)存本身并沒(méi)有壓縮,只是對(duì)象頭中記錄的指向這里的指針進(jìn)行了壓縮。
圖4-3 metaspace兩部分:非類(lèi)區(qū)和壓縮類(lèi)空間
壓縮類(lèi)空間中Klass是c++的對(duì)象有著很多元數(shù)據(jù)字段,vtable是記錄虛方法指針,itable是接口方法指針。Non-class中則記錄了更詳細(xì)的元數(shù)據(jù)信息。開(kāi)啟指針壓縮后,如果設(shè)置MaxMetaspaceSize參數(shù)實(shí)際上是限定的Non-class部分的大小,而不包括壓縮類(lèi)空間。通過(guò)Jprofile中也能發(fā)現(xiàn)Metaspace只包括Non-class部分,那為什么我上來(lái)說(shuō)Metaspace有兩部分呢,主要是從概念上講兩者都是元數(shù)據(jù),在國(guó)外很多文章中也都?xì)w為了Metaspace。這里只需要注意這個(gè)小細(xì)節(jié)就可以了。設(shè)置MaxMetaspaceSize參數(shù)也可以對(duì)壓縮類(lèi)空間起到間接的限制,因?yàn)榍懊嬲f(shuō)了Non-class部分是class部分的n倍。
圖4-4 指針壓縮開(kāi)啟時(shí) 非堆
將壓縮類(lèi)空間和非類(lèi)空間分開(kāi)的原因之一,就是壓縮類(lèi)空間是對(duì)象關(guān)聯(lián)的,只有4G上限,而將更多其他元數(shù)據(jù)剝離出去后,元空間可以遠(yuǎn)超過(guò)4G。而如果不開(kāi)啟指針壓縮,其實(shí)兩者就沒(méi)必要分開(kāi)了。關(guān)閉指針壓縮后,-XX:-UseCompressedOops 兩部分會(huì)合為一個(gè)。統(tǒng)稱(chēng)Metaspace
圖4-5 指針壓縮關(guān)閉時(shí)非堆
一個(gè)新的類(lèi)在需要被加載的時(shí)候,會(huì)使用ClassLoader在元空間申請(qǐng)內(nèi)存,并存儲(chǔ)類(lèi)的元數(shù)據(jù)信息。
元空間的內(nèi)存是ClassLoader持有的,所以說(shuō)只有對(duì)應(yīng)的ClassLoader卸載掉的時(shí)候才會(huì)釋放。ClassLoader又是需要他所加載的類(lèi)都消失的時(shí)候才能消失。一般是伴隨在一次GC的過(guò)程中進(jìn)行這個(gè)釋放。另外元空間如果超過(guò)了上限也會(huì)導(dǎo)致OOM。
當(dāng)然會(huì)導(dǎo)致OOM,所以metaspace限制大小的配置,需要根據(jù)程序謹(jǐn)慎定制。一般通過(guò)不斷創(chuàng)建新的類(lèi),如加載新類(lèi)(如hsf配置中下發(fā)groovy文件就會(huì)動(dòng)態(tài)的加載新的class),或者動(dòng)態(tài)代理類(lèi)(spring中的增強(qiáng)類(lèi)都是動(dòng)態(tài)代理類(lèi))都會(huì)導(dǎo)致metaspace的增長(zhǎng)。
cglib cglib 3.2.4
//設(shè)置metaspace大小:-XX:MaxMetaspaceSize=200mpublic class T {public static void main(String[] args) {while (true) {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(Object.class);enhancer.setUseCache(false);enhancer.setCallback((FixedValue)()->":)");enhancer.create();}}}
監(jiān)視會(huì)發(fā)現(xiàn)壓縮類(lèi)空間和非類(lèi)空間都在增大,后者在200M上有道紅線,在2分鐘左右溢出,程序掛掉,這個(gè)程序中壓縮類(lèi)空間大概是分類(lèi)的六分之一。
圖4-5a 壓縮類(lèi)空間
圖4-5b 非類(lèi)空間
上面的CodeCache和Metaspace毫無(wú)疑問(wèn)是jvm管理下的堆外空間。但是除了這些常規(guī)的堆外空間,jvm還可以使用一些native方法,直接申請(qǐng)堆外內(nèi)存。
例如做這么個(gè)demo,我們?cè)O(shè)置一個(gè)簡(jiǎn)單的java程序的堆大小是10M,此時(shí)用jprofile查看內(nèi)存堆提交了10M實(shí)際使用9M多,堆外提交了12M實(shí)際使用11M左右。所以算下來(lái)是20M+。直接查看進(jìn)程內(nèi)存會(huì)略大于這個(gè)值,因?yàn)檫@個(gè)20M是虛擬機(jī)內(nèi)部的內(nèi)存,本身運(yùn)行還是需要一些額外內(nèi)存的,進(jìn)程提交的內(nèi)存有90M,實(shí)際使用內(nèi)存47M
圖4-6 進(jìn)程的提交內(nèi)存和實(shí)際內(nèi)存
接下來(lái)我們使用Unsafe申請(qǐng)1G堆外內(nèi)存(也可以用NIO中的ByteBuffer.allocateDirect())
public static void main(String[] args) throws InterruptedException, IllegalAccessException, NoSuchFieldException {Field f = Unsafe.class.getDeclaredField("theUnsafe");f.setAccessible(true);Unsafe us = (Unsafe) f.get(null);long addr = us.allocateMemory(1024 * 1024 * 1024);System.out.println("Hello World");System.out.println(addr);while(true){Thread.sleep(1000L);}}
可以看到提交的內(nèi)存1G多,實(shí)際使用內(nèi)存也是47M。
圖4-7 進(jìn)程的提交內(nèi)存和實(shí)際內(nèi)存2
我甚至可以調(diào)整申請(qǐng)65G的內(nèi)存,要知道我的電腦也只有64G的內(nèi)存,但這仍不會(huì)報(bào)錯(cuò),可以看到提交的內(nèi)存已經(jīng)超過(guò)了物理內(nèi)存上限,但是得益于前面講的虛擬內(nèi)存的管理模式,使得應(yīng)用申請(qǐng)了超過(guò)物理大小的內(nèi)存,而如果真的使用起來(lái)的話,會(huì)有頁(yè)置換來(lái)協(xié)調(diào)。
圖4-8 進(jìn)程可以提交超過(guò)現(xiàn)實(shí)存在的內(nèi)存
上面的提交內(nèi)存很大但是實(shí)際使用內(nèi)存卻并不大:
圖4-9 任務(wù)管理器此時(shí)的狀態(tài)
Unsafe是很危險(xiǎn)的一個(gè)類(lèi),不建議使用。但是可以幫助我們理解有些框架是如何工作的。比如前一陣子看的Ehcache就提供了堆外緩存就是用類(lèi)似Unsafe申請(qǐng)的。堆外緩存需要自己實(shí)現(xiàn)序列化,因?yàn)閁nsafe設(shè)置內(nèi)存只能設(shè)置01字節(jié)碼不能設(shè)置為java對(duì)象。
堆外緩存的好處:緩存一般是短時(shí)間不需要清理的,如果在堆上則肯定會(huì)進(jìn)入老年代,占用固定的一大塊空間,使得觸發(fā)full GC的門(mén)檻降低了,很容易到了那個(gè)門(mén)限值。而且GC過(guò)程中還要去遍歷這些對(duì)象,效率較低。
堆外內(nèi)存的壞處:序列化需要自己實(shí)現(xiàn),清理也需要自己實(shí)現(xiàn),訪問(wèn)速度比heap要慢。
在1月14日,王者榮耀迎來(lái)了一波大更新,許多網(wǎng)友甚至表示這就是王者榮耀3.0版本。已經(jīng)更新過(guò)的玩家應(yīng)該能感受到,這次更新的內(nèi)容是非常多的。首先一進(jìn)游戲,就能發(fā)現(xiàn)游戲界面進(jìn)行的調(diào)整。相比之前的界面,新界...
2025.07.02所謂虛擬內(nèi)存,是計(jì)算機(jī)的一種內(nèi)存管理技術(shù)。它能在硬盤(pán)上生成虛擬內(nèi)存空間,來(lái)彌補(bǔ)我們物理內(nèi)存不足的缺陷。此功能在當(dāng)年電腦內(nèi)存普遍比較低的年代非常有用,只不過(guò)如今內(nèi)存已經(jīng)白菜價(jià),很多人的電腦基本都上了8G...
2025.07.02又是很長(zhǎng)時(shí)間沒(méi)更新的一個(gè)系列。文筆確實(shí)差一些。不過(guò)內(nèi)容都是實(shí)實(shí)在在的硬貨。別看我名字起的水,東西可不水的。這次說(shuō)內(nèi)存條。顯卡、內(nèi)存條和固態(tài)硬盤(pán)可以說(shuō)是老電腦升級(jí)躲不開(kāi)的話題。因?yàn)檫@三個(gè)零件更換起來(lái)是最...
2025.07.03Win7系統(tǒng)虛擬內(nèi)存怎么打開(kāi)?如果電腦內(nèi)存經(jīng)常提示不足的話,我們可以通過(guò)開(kāi)啟虛擬內(nèi)存來(lái)解決,下面就給大家介紹Win7開(kāi)啟虛擬內(nèi)存的方法。解決方法:1、在桌面上右擊我的電腦,選擇屬性。2、點(diǎn)擊高級(jí)系統(tǒng)設(shè)...
2025.07.03經(jīng)常有人找小庫(kù),說(shuō)自己的老電腦想升級(jí)一下,問(wèn)我是不是加內(nèi)存就可以了?這種問(wèn)題,三言?xún)烧Z(yǔ)說(shuō)不清,今天蟈蟈就來(lái)給大家聊聊老電腦升級(jí)的問(wèn)題,希望對(duì)你有所幫助!事先說(shuō)一下,如果你買(mǎi)的電腦已有10年以上歷史,那...
2025.07.03