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