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