又是一年秋招季,哎呀媽呀我被虐的慘來~這不,前幾陣失蹤沒更新博客,其實是我偷偷把時間用在復(fù)習(xí)課本了(霧
堅持在社區(qū)分享博客也很久了,由于過去的文章有很多疏漏之處,很多大佬都在評論指出我的過錯,我很開心也很失望,開心的是有大家?guī)臀抑赋鲥e誤,失望的鄙人學(xué)識淺薄總沒法做到完美。總之,歡迎評論區(qū)各種pr~
好,回到正題。復(fù)習(xí)的時候,無意間看到j(luò)ava虛擬機的有關(guān)知識點,我產(chǎn)生了非常濃厚的興趣,今天我來結(jié)合計算機內(nèi)存模型的相關(guān)知識,與Java內(nèi)存模型、Java對象模型、JVM內(nèi)存結(jié)構(gòu)等相關(guān)的知識串聯(lián)起來,本篇文章共1.5W字,分享給大家,感謝閱讀。
想要解鎖更多新姿勢?請訪問我的個人博客https://blog.tengshe789.tech/(??
計算機內(nèi)存
相信每個人都有一臺電腦,也有diy電腦的經(jīng)歷。現(xiàn)在一臺功能強大的diy電腦大概3k就能組裝起來,一個i5-8400 的cpu 869元,DDR4 內(nèi)存 1200塊錢,b360主板300元 散熱器50元 機械硬盤200元 350w電源300元 機箱100元 ,沒錯,只要3k就能拿到一個性能強大的6C6T電腦。
要說一臺PC中最重要的部件是什么?大家看價格也會看明白,是cpu和內(nèi)存,下面我來介紹一下cpu和內(nèi)存之間的關(guān)系。
cpu與內(nèi)存緩存的千絲萬縷
cpu相關(guān)術(shù)語
首先說明一下相關(guān)的cpu術(shù)語:
- socket:cpu插在主板上那個槽與cpu稱作一個socket。
- Die:核心(Die)又稱為內(nèi)核,是cpu的物理組成部分之一。cpu也會分為多die cpu與單die cpu,譬如我們現(xiàn)在強大的AMD TR-2990WX就是4die cpu,每個die里面有8個核心(core)
- core:也就是物理核心了。core這個詞是英特爾起的,起初是為了與競爭對手AMD區(qū)別開,后面用的多了也淡了。
- thread:就是硬件線程數(shù)。一個程序執(zhí)行可能需要多個線程一起進行~而現(xiàn)在也就比較強大的超線程技術(shù),過去的cpu往往一個cpu核心只支持一個線程,現(xiàn)在一些強大的cpu中,就譬如IBM 的POWER 9 ,支持8核心32個線程(平均一個核心4個線程),理論性能非常強大。
總結(jié)一下,以明星cpu AMD TR-2990WX作為栗子,這個cpu使用一個socket,一個socket里面有4個die,總共32個物理核心64個線程
cpu緩存
我們都知道,cpu將要處理的數(shù)據(jù)會放到內(nèi)存中保存,可是,為什么會這樣,將內(nèi)存緩存硬盤行不行呢?
答案當然是不行的。cpu的處理速度很強大,內(nèi)存的速度雖然非常快速但是根本跟不上cpu的步伐,所以,就出現(xiàn)的緩存。與來自DRAM家族的內(nèi)存不同,緩存SRAM與內(nèi)存最大的特點是,特別快,容量小,結(jié)構(gòu)復(fù)雜,成本也高。
造成內(nèi)存和緩存性能差異,主要有以下原因:
- DRAM儲存一位數(shù)據(jù)只需要一個電容加上一個晶體管,而SRAM需要6個晶體管。由于DRAM保存數(shù)據(jù)其實是在電容里面的,電容需要充放電才能進行讀寫操作,這就導(dǎo)致其讀寫數(shù)據(jù)就有比較大的延遲問題。
- 存儲可以看錯一個二維數(shù)組,每個存儲單元都有其行地址列地址。SRAM的容量很小,其存儲單元比較短(行列短),可以一次性傳輸?shù)絊RAM中;而DRAM,需要分別傳送行列地址。
- SRAM的頻率和cpu頻率比較接近;而DRAM的頻率和cpu差距比較大。
近代的緩存通常被集成到cpu當中,為了適應(yīng)性能與成本的需要,現(xiàn)實中的緩存往往使用金字塔型多級緩存架構(gòu)。也就是當CPU要讀取一個數(shù)據(jù)時,首先從一級緩存中查找,如果沒有找到再從二級緩存中查找,如果還是沒有就從三級緩存或內(nèi)存中查找。
下面是英特爾最近以來用的初代skylake架構(gòu)
可以看到,每個個核心有專屬的L1,L2緩存,他們共享一個L3緩存。如果cpu如果要訪問內(nèi)存中的數(shù)據(jù),必須要經(jīng)過L1,L2,L3,LLC(或者L4)四層緩存。
緩存一致性問題
最開始的cpu,其實只是一個核心一個線程的,當時根本不需要考慮緩存一致性問題,單線程,也就是cpu核心的緩存只被一個線程訪問。緩存獨占,不會出現(xiàn)訪問沖突等問題。
后來超線程技術(shù)來到我們視野,''單核CPU多線程'',也就是進程中的多個線程會同時訪問進程中的共享數(shù)據(jù),CPU將某塊內(nèi)存加載到緩存后,不同線程在訪問相同的物理地址的時候,都會映射到相同的緩存位置,這樣即使發(fā)生線程的切換,緩存仍然不會失效。但由于任何時刻只能有一個線程在執(zhí)行,因此不會出現(xiàn)緩存訪問沖突。
時代不斷發(fā)展,“多核CPU多線程”來了,即多個線程訪問進程中的某個共享內(nèi)存,且這多個線程分別在不同的核心上執(zhí)行,則每個核心都會在各自的caehe中保留一份共享內(nèi)存的緩沖。由于多核是可以并行的,可能會出現(xiàn)多個線程同時寫各自的緩存的情況,而各自的cache之間的數(shù)據(jù)就有可能不同。
這就是我們說的緩存一致性問題。
目前公認最好的解決方案是英特爾的MESI協(xié)議,下面我們著重介紹。
MESI協(xié)議
首先說說I/O操作的單位問題,大部分人都知道,在內(nèi)存中操作I/O不是以字節(jié)為單位,而是以“塊”為單位,這是為什么呢?
其實這是因為I/O操作的數(shù)據(jù)訪問有空間連續(xù)性特征,即需要訪問內(nèi)存空間很多數(shù)據(jù),但是I/O操作比較慢,讀一個字節(jié)和讀N個字節(jié)的時間基本相同。
機智的intel就規(guī)定了,cpu緩存中最小的存儲單元是緩存行cache line
,在x86的cpu中,一個cache line
儲存64字節(jié),每一級的緩存都會被劃分成許多組cache line
。
緩存工作原理請看??維基百科
接下來我們看看MESI規(guī)范,這其實是用四種緩存行狀態(tài)命名的,我們定義了CPU中每個緩存行使用4種狀態(tài)進行標記(使用額外的兩位(bit)表示),分別是:
-
M: 被修改(Modified)
該緩存行只被緩存在該CPU的緩存中,并且是被修改過的(dirty),即與主存中的數(shù)據(jù)不一致,該緩存行中的內(nèi)存需要在未來的某個時間點(允許其它CPU讀取請主存中相應(yīng)內(nèi)存之前)寫回(write back)主存。當被寫回主存之后,該緩存行的狀態(tài)會變成獨享(exclusive)狀態(tài)。
-
E: 獨享的(Exclusive)
該緩存行只被緩存在該CPU的緩存中,它是未被修改過的(clean),與主存中數(shù)據(jù)一致。該狀態(tài)可以在任何時刻當有其它CPU讀取該內(nèi)存時變成共享狀態(tài)(shared)。同樣地,當CPU修改該緩存行中內(nèi)容時,該狀態(tài)可以變成Modified狀態(tài)。
-
S: 共享的(Shared)
該狀態(tài)意味著該緩存行可能被多個CPU緩存,并且各個緩存中的數(shù)據(jù)與主存數(shù)據(jù)一致(clean),當有一個CPU修改該緩存行中,其它CPU中該緩存行可以被作廢(變成無效狀態(tài)(Invalid))。
-
I: 無效的(Invalid)
該緩存是無效的(可能有其它CPU修改了該緩存行)。
然而,只是有這四種狀態(tài)也會帶來一定的問題。下面引用一下oracle的文檔。
同時更新來自不同處理器的相同緩存代碼行中的單個元素會使整個緩存代碼行無效,即使這些更新在邏輯上是彼此獨立的。每次對緩存代碼行的單個元素進行更新時,都會將此代碼行標記為無效。其他訪問同一代碼行中不同元素的處理器將看到該代碼行已標記為無效。即使所訪問的元素未被修改,也會強制它們從內(nèi)存或其他位置獲取該代碼行的較新副本。這是因為基于緩存代碼行保持緩存一致性,而不是針對單個元素的。因此,互連通信和開銷方面都將有所增長。并且,正在進行緩存代碼行更新的時候,禁止訪問該代碼行中的元素。
MESI協(xié)議,可以保證緩存的一致性,但是無法保證實時性。這種情況稱為偽共享。
偽共享問題
偽共享問題其實在Java中是真實存在的一個問題。假設(shè)有如下所示的java class
class MyObiect{
long a;
long b;
long c;
}
按照java規(guī)范,MyObiect對象是在堆空間中分配的,a、b、c這三個變量在內(nèi)存空間中是近鄰,分別占8字節(jié),長度之和為24字節(jié)。而我們的x86的緩存行是64字節(jié),這三個變量完全有可能會在一個緩存行中,并且被兩個不同的cpu核心共享!
根據(jù)MESI協(xié)議,如果不同物理核心cpu中的線程1和線程2要互斥的對這幾個變量進行操作,很有可能要互相搶占資源,導(dǎo)致原來的并行變成串行,大大降低了系統(tǒng)的并發(fā)性,這就是緩存的偽共享。
解決偽共享
其實解決偽共享很簡單,只需要將這幾個變量分別放到不同的緩存行即可。在java8中,就已經(jīng)提供了普適性的解決方案,即采用@Contended
注解來保證對象中的變量或者屬性不在一個緩存行中~
@Contended
class VolatileObiect{
volatile long a = 1L;
volatile long b = 2L;
volatile long c = 3L;
}
內(nèi)存不一致性問題
上面我說了MESI協(xié)議在多核心cpu中解決緩存一致性的問題,下面我們說說cpu的內(nèi)存不一致性問題。
三種cpu架構(gòu)
首先,要了解三個名詞:
- SMP(Symmetric Multi-Processor)
[SMP ,對稱多處理系統(tǒng)內(nèi)有許多緊耦合多處理器,在這樣的系統(tǒng)中,所有的CPU共享全部資源,如總線,內(nèi)存和I/O系統(tǒng)等,操作系統(tǒng)或管理數(shù)據(jù)庫的復(fù)本只有一個,這種系統(tǒng)有一個最大的特點就是共享所有資源。多個CPU之間沒有區(qū)別,平等地訪問內(nèi)存、外設(shè)、一個操作系統(tǒng)。操作系統(tǒng)管理著一個隊列,每個處理器依次處理隊列中的進程。如果兩個處理器同時請求訪問一個資源(例如同一段內(nèi)存地址),由硬件、軟件的鎖機制去解決資源爭用問題。
[所謂對稱多處理器結(jié)構(gòu),是指服務(wù)器中多個 CPU 對稱工作,無主次或從屬關(guān)系。各 CPU 共享相同的物理內(nèi)存,每個 CPU 訪問內(nèi)存中的任何地址所需時間是相同的,因此 SMP 也被稱為一致存儲器訪問結(jié)構(gòu) (UMA : Uniform Memory Access) 。對 SMP 服務(wù)器進行擴展的方式包括增加內(nèi)存、使用更快的 CPU 、增加 CPU 、擴充 I/O( 槽口數(shù)與總線數(shù) ) 以及添加更多的外部設(shè)備 ( 通常是磁盤存儲 ) 。
SMP 服務(wù)器的主要特征是共享,系統(tǒng)中所有資源 (CPU 、內(nèi)存、 I/O 等 ) 都是共享的。也正是由于這種特征,導(dǎo)致了 SMP 服務(wù)器的主要問題,那就是它的擴展能力非常有限。對于 SMP 服務(wù)器而言,每一個共享的環(huán)節(jié)都可能造成 SMP 服務(wù)器擴展時的瓶頸,而最受限制的則是內(nèi)存。由于每個 CPU 必須通過相同的內(nèi)存總線訪問相同的內(nèi)存資源,因此隨著 CPU 數(shù)量的增加,內(nèi)存訪問沖突將迅速增加,最終會造成 CPU 資源的浪費,使 CPU 性能的有效性大大降低。實驗證明, SMP 服務(wù)器 CPU 利用率最好的情況是 2 至 4 個 CPU 。
- NUMA(Non-Uniform Memory Access)
由于 SMP 在擴展能力上的限制,人們開始探究如何進行有效地擴展從而構(gòu)建大型系統(tǒng)的技術(shù), NUMA 就是這種努力下的結(jié)果之一。利用 NUMA 技術(shù),可以把幾十個 CPU( 甚至上百個 CPU) 組合在一個服務(wù)器內(nèi)。其NUMA 服務(wù)器 CPU 模塊結(jié)構(gòu)如圖所示:
NUMA 服務(wù)器的基本特征是具有多個 CPU 模塊,每個 CPU 模塊由多個 CPU( 如 4 個 ) 組成,并且具有獨立的本地內(nèi)存、 I/O 槽口等。由于其節(jié)點之間可以通過互聯(lián)模塊 ( 如稱為 Crossbar Switch) 進行連接和信息交互,因此每個 CPU 可以訪問整個系統(tǒng)的內(nèi)存 ( 這是 NUMA 系統(tǒng)與 MPP 系統(tǒng)的重要差別 ) 。顯然,訪問本地內(nèi)存的速度將遠遠高于訪問遠地內(nèi)存 ( 系統(tǒng)內(nèi)其它節(jié)點的內(nèi)存 ) 的速度,這也是非一致存儲訪問 NUMA 的由來。由于這個特點,為了更好地發(fā)揮系統(tǒng)性能,開發(fā)應(yīng)用程序時需要盡量減少不同 CPU 模塊之間的信息交互。
利用 NUMA 技術(shù),可以較好地解決原來 SMP 系統(tǒng)的擴展問題,在一個物理服務(wù)器內(nèi)可以支持上百個 CPU 。比較典型的 NUMA 服務(wù)器的例子包括 HP 的 Superdome 、 SUN15K 、 IBMp690 等。
但 NUMA 技術(shù)同樣有一定缺陷,由于訪問遠地內(nèi)存的延時遠遠超過本地內(nèi)存,因此當 CPU 數(shù)量增加時,系統(tǒng)性能無法線性增加。如 HP 公司發(fā)布 Superdome 服務(wù)器時,曾公布了它與 HP 其它 UNIX 服務(wù)器的相對性能值,結(jié)果發(fā)現(xiàn), 64 路 CPU 的 Superdome (NUMA 結(jié)構(gòu) ) 的相對性能值是 20 ,而 8 路 N4000( 共享的 SMP 結(jié)構(gòu) ) 的相對性能值是 6.3 。從這個結(jié)果可以看到, 8 倍數(shù)量的 CPU 換來的只是 3 倍性能的提升。
- MPP(Massive Parallel Processing)
和 NUMA 不同, MPP 提供了另外一種進行系統(tǒng)擴展的方式,它由多個 SMP 服務(wù)器通過一定的節(jié)點互聯(lián)網(wǎng)絡(luò)進行連接,協(xié)同工作,完成相同的任務(wù),從用戶的角度來看是一個服務(wù)器系統(tǒng)。其基本特征是由多個 SMP 服務(wù)器 ( 每個 SMP 服務(wù)器稱節(jié)點 ) 通過節(jié)點互聯(lián)網(wǎng)絡(luò)連接而成,每個節(jié)點只訪問自己的本地資源 ( 內(nèi)存、存儲等 ) ,是一種完全無共享 (Share Nothing) 結(jié)構(gòu),因而擴展能力最好,理論上其擴展無限制,目前的技術(shù)可實現(xiàn) 512 個節(jié)點互聯(lián),數(shù)千個 CPU 。目前業(yè)界對節(jié)點互聯(lián)網(wǎng)絡(luò)暫無標準,如 NCR 的 Bynet , IBM 的 SPSwitch ,它們都采用了不同的內(nèi)部實現(xiàn)機制。但節(jié)點互聯(lián)網(wǎng)僅供 MPP 服務(wù)器內(nèi)部使用,對用戶而言是透明的。
在 MPP 系統(tǒng)中,每個 SMP 節(jié)點也可以運行自己的操作系統(tǒng)、數(shù)據(jù)庫等。但和 NUMA 不同的是,它不存在異地內(nèi)存訪問的問題。換言之,每個節(jié)點內(nèi)的 CPU 不能訪問另一個節(jié)點的內(nèi)存。節(jié)點之間的信息交互是通過節(jié)點互聯(lián)網(wǎng)絡(luò)實現(xiàn)的,這個過程一般稱為數(shù)據(jù)重分配 (Data Redistribution) 。
但是 MPP 服務(wù)器需要一種復(fù)雜的機制來調(diào)度和平衡各個節(jié)點的負載和并行處理過程。目前一些基于 MPP 技術(shù)的服務(wù)器往往通過系統(tǒng)級軟件 ( 如數(shù)據(jù)庫 ) 來屏蔽這種復(fù)雜性。舉例來說, NCR 的 Teradata 就是基于 MPP 技術(shù)的一個關(guān)系數(shù)據(jù)庫軟件,基于此數(shù)據(jù)庫來開發(fā)應(yīng)用時,不管后臺服務(wù)器由多少個節(jié)點組成,開發(fā)人員所面對的都是同一個數(shù)據(jù)庫系統(tǒng),而不需要考慮如何調(diào)度其中某幾個節(jié)點的負載。
MPP (Massively Parallel Processing),大規(guī)模并行處理系統(tǒng),這樣的系統(tǒng)是由許多松耦合的處理單元組成的,要注意的是這里指的是處理單元而不是處理器。每個單元內(nèi)的CPU都有自己私有的資源,如總線,內(nèi)存,硬盤等。在每個單元內(nèi)都有操作系統(tǒng)和管理數(shù)據(jù)庫的實例復(fù)本。這種結(jié)構(gòu)最大的特點在于不共享資源。
NUMA結(jié)構(gòu)下的緩存一致性
要知道,MESI協(xié)議解決的是傳統(tǒng)SMP結(jié)構(gòu)下緩存的一致性,為了在NUMA架構(gòu)也實現(xiàn)緩存一致性,intel引入了MESI的一個拓展協(xié)議--MESIF,但是目前并沒有什么資料,也沒法研究,更多消息請查閱intel的wiki。
Java內(nèi)存模型
起因
我們寫程序,為什么要考慮內(nèi)存模型呢,我們前面說了,緩存一致性問題、內(nèi)存一致問題是硬件的不斷升級導(dǎo)致的。解決問題,最簡單直接的做法就是廢除CPU緩存,讓CPU直接和主存交互。但是,這么做雖然可以保證多線程下的并發(fā)問題。但是,這就有點時代倒退了。
所以,為了保證并發(fā)編程中可以滿足原子性、可見性及有序性。有一個重要的概念,那就是——內(nèi)存模型。
即為了保證共享內(nèi)存的正確性(可見性、有序性、原子性),需要內(nèi)存模型來定義了共享內(nèi)存系統(tǒng)中多線程程序讀寫操作行為的相應(yīng)規(guī)范~
JMM
Java內(nèi)存模型是根據(jù)英文Java Memory Model(JMM)翻譯過來的。其實JMM并不像JVM內(nèi)存結(jié)構(gòu)一樣是真實存在的。它是一種符合內(nèi)存模型規(guī)范的,屏蔽了各種硬件和操作系統(tǒng)的訪問差異的,保證了Java程序在各種平臺下對內(nèi)存的訪問都能保證效果一致的機制及規(guī)范。就像JSR-133: Java Memory Model and Thread Specification 中描述了,JMM是和多線程相關(guān)的,他描述了一組規(guī)則或規(guī)范,這個規(guī)范定義了一個線程對共享變量的寫入時對另一個線程是可見的。
那么,簡單總結(jié)下,Java的多線程之間是通過共享內(nèi)存進行通信的,而由于采用共享內(nèi)存進行通信,在通信過程中會存在一系列如可見性、原子性、順序性等問題,而JMM就是圍繞著多線程通信以及與其相關(guān)的一系列特性而建立的模型。JMM定義了一些語法集,這些語法集映射到Java語言中就是volatile
、synchronized
等關(guān)鍵字。
在JMM中,我們把多個線程間通信的共享內(nèi)存稱之為主內(nèi)存,而在并發(fā)編程中多個線程都維護了一個自己的本地內(nèi)存(這是個抽象概念),其中保存的數(shù)據(jù)是主內(nèi)存中的數(shù)據(jù)拷貝。而JMM主要是控制本地內(nèi)存和主內(nèi)存之間的數(shù)據(jù)交互的。
在Java中,JMM是一個非常重要的概念,正是由于有了JMM,Java的并發(fā)編程才能避免很多問題。
JMM應(yīng)用
了解Java多線程的朋友都知道,在Java中提供了一系列和并發(fā)處理相關(guān)的關(guān)鍵字,比如volatile
、synchronized
、final
、concurrent
包等。其實這些就是Java內(nèi)存模型封裝了底層的實現(xiàn)后提供給我們使用的一些關(guān)鍵字。
在開發(fā)多線程的代碼的時候,我們可以直接使用synchronized
等關(guān)鍵字來控制并發(fā),從來就不需要關(guān)心底層的編譯器優(yōu)化、緩存一致性等問題。所以,Java內(nèi)存模型,除了定義了一套規(guī)范,還提供了一系列原語,封裝了底層實現(xiàn)后,供開發(fā)者直接使用。
并發(fā)編程要解決原子性、有序性和可見性的問題,我們就再來看下,在Java中,分別使用什么方式來保證。
原子性
原子性是指在一個操作中就是cpu不可以在中途暫停然后再調(diào)度,既不被中斷操作,要不執(zhí)行完成,要不就不執(zhí)行。
JMM提供保證了訪問基本數(shù)據(jù)類型的原子性(其實在寫一個工作內(nèi)存變量到主內(nèi)存是分主要兩步:store、write),但是實際業(yè)務(wù)處理場景往往是需要更大的范圍的原子性保證。
在Java中,為了保證原子性,提供了兩個高級的字節(jié)碼指令monitorenter
和monitorexit
,而這兩個字節(jié)碼,在Java中對應(yīng)的關(guān)鍵字就是synchronized
。
因此,在Java中可以使用synchronized
來保證方法和代碼塊內(nèi)的操作是原子性的。這里推薦一篇文章深入理解Java并發(fā)之synchronized實現(xiàn)原理。
可見性
可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
Java內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值的這種依賴主內(nèi)存作為傳遞媒介的方式來實現(xiàn)的。
Java中的volatile
關(guān)鍵字提供了一個功能,那就是被其修飾的變量在被修改后可以立即同步到主內(nèi)存,被其修飾的變量在每次是用之前都從主內(nèi)存刷新。因此,可以使用volatile
來保證多線程操作時變量的可見性。
除了volatile
,Java中的synchronized
和final
、static
三個關(guān)鍵字也可以實現(xiàn)可見性。下面分享一下我的讀書筆記:
有序性
有序性即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
在Java中,可以使用synchronized
和volatile
來保證多線程之間操作的有序性。實現(xiàn)方式有所區(qū)別:
volatile
關(guān)鍵字會禁止指令重排。synchronized
關(guān)鍵字保證同一時刻只允許一條線程操作。
好了,這里簡單的介紹完了Java并發(fā)編程中解決原子性、可見性以及有序性可以使用的關(guān)鍵字。讀者可能發(fā)現(xiàn)了,好像synchronized
關(guān)鍵字是萬能的,他可以同時滿足以上三種特性,這其實也是很多人濫用synchronized
的原因。
但是synchronized
是比較影響性能的,雖然編譯器提供了很多鎖優(yōu)化技術(shù),但是也不建議過度使用。
JVM
我們都知道,Java代碼是要運行在虛擬機上的,而虛擬機在執(zhí)行Java程序的過程中會把所管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域,這些區(qū)域都有各自的用途。下面我們來說說JVM運行時內(nèi)存區(qū)域結(jié)構(gòu)
JVM運行時內(nèi)存區(qū)域結(jié)構(gòu)
在《Java虛擬機規(guī)范(Java SE 8)》中描述了JVM運行時內(nèi)存區(qū)域結(jié)構(gòu)如下:
1.程序計數(shù)器
程序計數(shù)器(Program Counter Register),也有稱作為PC寄存器。想必學(xué)過匯編語言的朋友對程序計數(shù)器這個概念并不陌生,在匯編語言中,程序計數(shù)器是指CPU中的寄存器,它保存的是程序當前執(zhí)行的指令的地址(也可以說保存下一條指令的所在存儲單元的地址),當CPU需要執(zhí)行指令時,需要從程序計數(shù)器中得到當前需要執(zhí)行的指令所在存儲單元的地址,然后根據(jù)得到的地址獲取到指令,在得到指令之后,程序計數(shù)器便自動加1或者根據(jù)轉(zhuǎn)移指針得到下一條指令的地址,如此循環(huán),直至執(zhí)行完所有的指令。
雖然JVM中的程序計數(shù)器并不像匯編語言中的程序計數(shù)器一樣是物理概念上的CPU寄存器,但是JVM中的程序計數(shù)器的功能跟匯編語言中的程序計數(shù)器的功能在邏輯上是等同的,也就是說是用來指示 執(zhí)行哪條指令的。
由于在JVM中,多線程是通過線程輪流切換來獲得CPU執(zhí)行時間的,因此,在任一具體時刻,一個CPU的內(nèi)核只會執(zhí)行一條線程中的指令,因此,為了能夠使得每個線程都在線程切換后能夠恢復(fù)在切換之前的程序執(zhí)行位置,每個線程都需要有自己獨立的程序計數(shù)器,并且不能互相被干擾,否則就會影響到程序的正常執(zhí)行次序。因此,可以這么說,程序計數(shù)器是每個線程所私有的。
在JVM規(guī)范中規(guī)定,如果線程執(zhí)行的是非native方法,則程序計數(shù)器中保存的是當前需要執(zhí)行的指令的地址;如果線程執(zhí)行的是native方法,則程序計數(shù)器中的值是undefined。
由于程序計數(shù)器中存儲的數(shù)據(jù)所占空間的大小不會隨程序的執(zhí)行而發(fā)生改變,因此,對于程序計數(shù)器是不會發(fā)生內(nèi)存溢出現(xiàn)象(OutOfMemory)的。
2.Java棧
Java棧也稱作虛擬機棧(Java Vitual Machine Stack),也就是我們常常所說的棧,跟C語言的數(shù)據(jù)段中的棧類似。事實上,Java棧是Java方法執(zhí)行的內(nèi)存模型。為什么這么說呢?下面就來解釋一下其中的原因。
Java棧中存放的是一個個的棧幀,每個棧幀對應(yīng)一個被調(diào)用的方法,在棧幀中包括局部變量表(Local Variables)、操作數(shù)棧(Operand Stack)、指向當前方法所屬的類的運行時常量池(運行時常量池的概念在方法區(qū)部分會談到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些額外的附加信息。當線程執(zhí)行一個方法時,就會隨之創(chuàng)建一個對應(yīng)的棧幀,并將建立的棧幀壓棧。當方法執(zhí)行完畢之后,便會將棧幀出棧。因此可知,線程當前執(zhí)行的方法所對應(yīng)的棧幀必定位于Java棧的頂部。講到這里,大家就應(yīng)該會明白為什么 在 使用 遞歸方法的時候容易導(dǎo)致棧內(nèi)存溢出的現(xiàn)象了以及為什么棧區(qū)的空間不用程序員去管理了(當然在Java中,程序員基本不用關(guān)系到內(nèi)存分配和釋放的事情,因為Java有自己的垃圾回收機制),這部分空間的分配和釋放都是由系統(tǒng)自動實施的。對于所有的程序設(shè)計語言來說,棧這部分空間對程序員來說是不透明的。下圖表示了一個Java棧的模型:
局部變量表,顧名思義,想必不用解釋大家應(yīng)該明白它的作用了吧。就是用來存儲方法中的局部變量(包括在方法中聲明的非靜態(tài)變量以及函數(shù)形參)。對于基本數(shù)據(jù)類型的變量,則直接存儲它的值,對于引用類型的變量,則存的是指向?qū)ο蟮囊谩>植孔兞勘淼拇笮≡诰幾g器就可以確定其大小了,因此在程序執(zhí)行期間局部變量表的大小是不會改變的。
操作數(shù)棧,想必學(xué)過數(shù)據(jù)結(jié)構(gòu)中的棧的朋友想必對表達式求值問題不會陌生,棧最典型的一個應(yīng)用就是用來對表達式求值。想想一個線程執(zhí)行方法的過程中,實際上就是不斷執(zhí)行語句的過程,而歸根到底就是進行計算的過程。因此可以這么說,程序中的所有計算過程都是在借助于操作數(shù)棧來完成的。
指向運行時常量池的引用,因為在方法執(zhí)行的過程中有可能需要用到類中的常量,所以必須要有一個引用指向運行時常量。
方法返回地址,當一個方法執(zhí)行完畢之后,要返回之前調(diào)用它的地方,因此在棧幀中必須保存一個方法返回地址。
由于每個線程正在執(zhí)行的方法可能不同,因此每個線程都會有一個自己的Java棧,互不干擾。
3.本地方法棧
本地方法棧與Java棧的作用和原理非常相似。區(qū)別只不過是Java棧是為執(zhí)行Java方法服務(wù)的,而本地方法棧則是為執(zhí)行本地方法(Native Method)服務(wù)的。在JVM規(guī)范中,并沒有對本地方發(fā)展的具體實現(xiàn)方法以及數(shù)據(jù)結(jié)構(gòu)作強制規(guī)定,虛擬機可以自由實現(xiàn)它。在HotSopt虛擬機中直接就把本地方法棧和Java棧合二為一。
4.堆
在C語言中,堆這部分空間是唯一一個程序員可以管理的內(nèi)存區(qū)域。程序員可以通過malloc函數(shù)和free函數(shù)在堆上申請和釋放空間。那么在Java中是怎么樣的呢?
Java中的堆是用來存儲對象本身的以及數(shù)組(當然,數(shù)組引用是存放在Java棧中的)。只不過和C語言中的不同,在Java中,程序員基本不用去關(guān)心空間釋放的問題,Java的垃圾回收機制會自動進行處理。因此這部分空間也是Java垃圾收集器管理的主要區(qū)域。另外,堆是被所有線程共享的,在JVM中只有一個堆。
5.方法區(qū)
方法區(qū)在JVM中也是一個非常重要的區(qū)域,它與堆一樣,是被線程共享的區(qū)域。在方法區(qū)中,存儲了每個類的信息(包括類的名稱、方法信息、字段信息)、靜態(tài)變量、常量以及編譯器編譯后的代碼等。
在Class文件中除了類的字段、方法、接口等描述信息外,還有一項信息是常量池,用來存儲編譯期間生成的字面量和符號引用。
在方法區(qū)中有一個非常重要的部分就是運行時常量池,它是每一個類或接口的常量池的運行時表示形式,在類和接口被加載到JVM后,對應(yīng)的運行時常量池就被創(chuàng)建出來。當然并非Class文件常量池中的內(nèi)容才能進入運行時常量池,在運行期間也可將新的常量放入運行時常量池中,比如String的intern方法。
在JVM規(guī)范中,沒有強制要求方法區(qū)必須實現(xiàn)垃圾回收。很多人習(xí)慣將方法區(qū)稱為“永久代”,是因為HotSpot虛擬機以永久代來實現(xiàn)方法區(qū),從而JVM的垃圾收集器可以像管理堆區(qū)一樣管理這部分區(qū)域,從而不需要專門為這部分設(shè)計垃圾回收機制。不過自從JDK7之后,Hotspot虛擬機便將運行時常量池從永久代移除了。
Java對象模型的內(nèi)存布局
java是一種面向?qū)ο蟮恼Z言,而Java對象在JVM中的存儲也是有一定的結(jié)構(gòu)的。而這個關(guān)于Java對象自身的存儲模型稱之為Java對象模型。
HotSpot虛擬機中,設(shè)計了一個OOP-Klass Model。OOP(Ordinary Object Pointer)指的是普通對象指針,而Klass用來描述對象實例的具體類型。
每一個Java類,在被JVM加載的時候,JVM會給這個類創(chuàng)建一個instanceKlass
,保存在方法區(qū),用來在JVM層表示該Java類。當我們在Java代碼中,使用new創(chuàng)建一個對象的時候,JVM會創(chuàng)建一個instanceOopDesc
對象,對象在內(nèi)存中存儲的布局可以分為3塊區(qū)域:對象頭(Header)、 實例數(shù)據(jù)(Instance Data)和對齊填充(Padding)。
- 對象頭:標記字(32位虛擬機4B,64位虛擬機8B) + 類型指針(32位虛擬機4B,64位虛擬機8B)+ [數(shù)組長(對于數(shù)組對象才需要此部分信息)]
- 實例數(shù)據(jù):存儲的是真正有效數(shù)據(jù),如各種字段內(nèi)容,各字段的分配策略為longs/doubles、ints、shorts/chars、bytes/boolean、oops(ordinary object pointers),相同寬度的字段總是被分配到一起,便于之后取數(shù)據(jù)。父類定義的變量會出現(xiàn)在子類定義的變量的前面。
- 對齊填充:對于64位虛擬機來說,對象大小必須是8B的整數(shù)倍,不夠的話需要占位填充
JVM內(nèi)存垃圾收集器
為了理解現(xiàn)有收集器,我們需要先了解一些術(shù)語。最基本的垃圾收集涉及識別不再使用的內(nèi)存并使其可重用。現(xiàn)代收集器在幾個階段進行這一過程,對于這些階段我們往往有如下描述:
- 并行- 在JVM運行時,同時存在應(yīng)用程序線程和垃圾收集器線程。 并行階段是由多個gc線程執(zhí)行,即gc工作在它們之間分配。 不涉及GC線程是否需要暫停應(yīng)用程序線程。
- 串行- 串行階段僅在單個gc線程上執(zhí)行。與之前一樣,它也沒有說明GC線程是否需要暫停應(yīng)用程序線程。
- STW - STW階段,應(yīng)用程序線程被暫停,以便gc執(zhí)行其工作。 當應(yīng)用程序因為GC暫停時,這通常是由于Stop The World階段。
- 并發(fā) -如果一個階段是并發(fā)的,那么GC線程可以和應(yīng)用程序線程同時進行。 并發(fā)階段很復(fù)雜,因為它們需要在階段完成之前處理可能使工作無效(譯者注:因為是并發(fā)進行的,GC線程在完成一階段的同時,應(yīng)用線程也在工作產(chǎn)生操作內(nèi)存,所以需要額外處理)的應(yīng)用程序線程。
- 增量 -如果一個階段是增量的,那么它可以運行一段時間之后由于某些條件提前終止,例如需要執(zhí)行更高優(yōu)先級的gc階段,同時仍然完成生產(chǎn)性工作。 增量階段與需要完全完成的階段形成鮮明對比。
Serial收集器
Serial收集器是最基本的收集器,這是一個單線程收集器,它仍然是JVM在Client模式下的默認新生代收集器。它有著優(yōu)于其他收集器的地方:簡單而高效(與其他收集器的單線程比較),Serial收集器由于沒有線程交互的開銷,專心只做垃圾收集自然也獲得最高的效率。在用戶桌面場景下,分配給JVM的內(nèi)存不會太多,停頓時間完全可以在幾十到一百多毫秒之間,只要收集不頻繁,這是完全可以接受的。
ParNew收集器
ParNew是Serial的多線程版本,在回收算法、對象分配原則上都是一致的。ParNew收集器是許多運行在Server模式下的默認新生代垃圾收集器,其主要在于除了Serial收集器,目前只有ParNew收集器能夠與CMS收集器配合工作。
Parallel Scavenge收集器
Parallel Scavenge收集器是一個新生代垃圾收集器,其使用的算法是復(fù)制算法,也是并行的多線程收集器。
Parallel Scavenge 收集器更關(guān)注可控制的吞吐量,吞吐量等于運行用戶代碼的時間/(運行用戶代碼的時間+垃圾收集時間)。直觀上,只要最大的垃圾收集停頓時間越小,吞吐量是越高的,但是GC停頓時間的縮短是以犧牲吞吐量和新生代空間作為代價的。比如原來10秒收集一次,每次停頓100毫秒,現(xiàn)在變成5秒收集一次,每次停頓70毫秒。停頓時間下降的同時,吞吐量也下降了。
停頓時間越短就越適合需要與用戶交互的程序;而高吞吐量則可以最高效的利用CPU的時間,盡快的完成計算任務(wù),主要適用于后臺運算。
Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本,也是一個單線程收集器,采用“標記-整理算法”進行回收。其運行過程與Serial收集器一樣。
Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和標記-整理算法進行垃圾回收。其通常與Parallel Scavenge收集器配合使用,“吞吐量優(yōu)先”收集器是這個組合的特點,在注重吞吐量和CPU資源敏感的場合,都可以使用這個組合。
CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短停頓時間為目標的收集器,CMS收集器采用標記--清除算法,運行在老年代。主要包含以下幾個步驟:
- 初始標記
- 并發(fā)標記
- 重新標記
- 并發(fā)清除
其中初始標記和重新標記仍然需要“Stop the world”。初始標記僅僅標記GC Root能直接關(guān)聯(lián)的對象,并發(fā)標記就是進行GC Root Tracing過程,而重新標記則是為了修正并發(fā)標記期間,因用戶程序繼續(xù)運行而導(dǎo)致標記變動的那部分對象的標記記錄。
由于整個過程中最耗時的并發(fā)標記和并發(fā)清除,收集線程和用戶線程一起工作,所以總體上來說,CMS收集器回收過程是與用戶線程并發(fā)執(zhí)行的。雖然CMS優(yōu)點是并發(fā)收集、低停頓,很大程度上已經(jīng)是一個不錯的垃圾收集器,但是還是有三個顯著的缺點:
- CMS收集器對CPU資源很敏感。在并發(fā)階段,雖然它不會導(dǎo)致用戶線程停頓,但是會因為占用一部分線程(CPU資源)而導(dǎo)致應(yīng)用程序變慢。
- CMS收集器不能處理浮動垃圾。所謂的“浮動垃圾”,就是在并發(fā)標記階段,由于用戶程序在運行,那么自然就會有新的垃圾產(chǎn)生,這部分垃圾被標記過后,CMS無法在當次集中處理它們,只好在下一次GC的時候處理,這部分未處理的垃圾就稱為“浮動垃圾”。也是由于在垃圾收集階段程序還需要運行,即還需要預(yù)留足夠的內(nèi)存空間供用戶使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎填滿才進行收集,需要預(yù)留一部分空間提供并發(fā)收集時程序運作使用。要是CMS預(yù)留的內(nèi)存空間不能滿足程序的要求,這是JVM就會啟動預(yù)備方案:臨時啟動Serial Old收集器來收集老年代,這樣停頓的時間就會很長。
- 由于CMS使用標記--清除算法,所以在收集之后會產(chǎn)生大量內(nèi)存碎片。當內(nèi)存碎片過多時,將會給分配大對象帶來困難,這是就會進行Full GC。
G1收集器
G1收集器與CMS相比有很大的改進:
· G1收集器采用標記--整理算法實現(xiàn)。
· 可以非常精確地控制停頓。
? G1收集器可以實現(xiàn)在基本不犧牲吞吐量的情況下完成低停頓的內(nèi)存回收,這是由于它極力的避免全區(qū)域的回收,G1收集器將Java堆(包括新生代和老年代)劃分為多個區(qū)域(Region),并在后臺維護一個優(yōu)先列表,每次根據(jù)允許的時間,優(yōu)先回收垃圾最多的區(qū)域 。
ZGC收集器
Java 11 新加入的ZGC垃圾收集器號稱可以達到10ms 以下的 GC 停頓,ZGC給Hotspot Garbage Collectors增加了兩種新技術(shù):著色指針和讀屏障。下面引用國外文章說的內(nèi)容:
著色指針
著色指針是一種將信息存儲在指針(或使用Java術(shù)語引用)中的技術(shù)。因為在64位平臺上(ZGC僅支持64位平臺),指針可以處理更多的內(nèi)存,因此可以使用一些位來存儲狀態(tài)。 ZGC將限制最大支持4Tb堆(42-bits),那么會剩下22位可用,它目前使用了4位:
finalizable
,remap
,mark0
和mark1
。 我們稍后解釋它們的用途。著色指針的一個問題是,當您需要取消著色時,它需要額外的工作(因為需要屏蔽信息位)。 像SPARC這樣的平臺有內(nèi)置硬件支持指針屏蔽所以不是問題,而對于x86平臺來說,ZGC團隊使用了簡潔的多重映射技巧。
多重映射
要了解多重映射的工作原理,我們需要簡要解釋虛擬內(nèi)存和物理內(nèi)存之間的區(qū)別。 物理內(nèi)存是系統(tǒng)可用的實際內(nèi)存,通常是安裝的DRAM芯片的容量。 虛擬內(nèi)存是抽象的,這意味著應(yīng)用程序?qū)ΓㄍǔJ歉綦x的)物理內(nèi)存有自己的視圖。 操作系統(tǒng)負責(zé)維護虛擬內(nèi)存和物理內(nèi)存范圍之間的映射,它通過使用頁表和處理器的內(nèi)存管理單元(MMU)和轉(zhuǎn)換查找緩沖器(TLB)來實現(xiàn)這一點,后者轉(zhuǎn)換應(yīng)用程序請求的地址。
多重映射涉及將不同范圍的虛擬內(nèi)存映射到同一物理內(nèi)存。 由于設(shè)計中只有一個
remap
,mark0
和mark1
在任何時間點都可以為1,因此可以使用三個映射來完成此操作。 ZGC源代碼中有一個很好的圖表可以說明這一點。讀屏障
讀屏障是每當應(yīng)用程序線程從堆加載引用時運行的代碼片段(即訪問對象上的非原生字段non-primitive field):
void printName( Person person ) { String name = person.name; // 這里觸發(fā)讀屏障 // 因為需要從heap讀取引用 // System.out.println(name); // 這里沒有直接觸發(fā)讀屏障 }
在上面的代碼中,String name = person.name 訪問了堆上的person引用,然后將引用加載到本地的name變量。此時觸發(fā)讀屏障。 Systemt.out那行不會直接觸發(fā)讀屏障,因為沒有來自堆的引用加載(name是局部變量,因此沒有從堆加載引用)。 但是System和out,或者println內(nèi)部可能會觸發(fā)其他讀屏障。
這與其他GC使用的寫屏障形成對比,例如G1。讀屏障的工作是檢查引用的狀態(tài),并在將引用(或者甚至是不同的引用)返回給應(yīng)用程序之前執(zhí)行一些工作。 在ZGC中,它通過測試加載的引用來執(zhí)行此任務(wù),以查看是否設(shè)置了某些位。 如果通過了測試,則不執(zhí)行任何其他工作,如果失敗,則在將引用返回給應(yīng)用程序之前執(zhí)行某些特定于階段的任務(wù)。
標記
現(xiàn)在我們了解了這兩種新技術(shù)是什么,讓我們來看看ZG的GC循環(huán)。
GC循環(huán)的第一部分是標記。標記包括查找和標記運行中的應(yīng)用程序可以訪問的所有堆對象,換句話說,查找不是垃圾的對象。
ZGC的標記分為三個階段。 第一階段是STW,其中GC roots被標記為活對象。 GC roots類似于局部變量,通過它可以訪問堆上其他對象。 如果一個對象不能通過遍歷從roots開始的對象圖來訪問,那么應(yīng)用程序也就無法訪問它,則該對象被認為是垃圾。從roots訪問的對象集合稱為Live集。GC roots標記步驟非常短,因為roots的總數(shù)通常比較小。
該階段完成后,應(yīng)用程序恢復(fù)執(zhí)行,ZGC開始下一階段,該階段同時遍歷對象圖并標記所有可訪問的對象。 在此階段期間,讀屏障針使用掩碼測試所有已加載的引用,該掩碼確定它們是否已標記或尚未標記,如果尚未標記引用,則將其添加到隊列以進行標記。
在遍歷完成之后,有一個最終的,時間很短的的Stop The World階段,這個階段處理一些邊緣情況(我們現(xiàn)在將它忽略),該階段完成之后標記階段就完成了。
重定位
GC循環(huán)的下一個主要部分是重定位。重定位涉及移動活動對象以釋放部分堆內(nèi)存。 為什么要移動對象而不是填補空隙? 有些GC實際是這樣做的,但是它導(dǎo)致了一個不幸的后果,即分配內(nèi)存變得更加昂貴,因為當需要分配內(nèi)存時,內(nèi)存分配器需要找到可以放置對象的空閑空間。 相比之下,如果可以釋放大塊內(nèi)存,那么分配內(nèi)存就很簡單,只需要將指針遞增新對象所需的內(nèi)存大小即可。
ZGC將堆分成許多頁面,在此階段開始時,它同時選擇一組需要重定位活動對象的頁面。選擇重定位集后,會出現(xiàn)一個Stop The World暫停,其中ZGC重定位該集合中root對象,并將他們的引用映射到新位置。與之前的Stop The World步驟一樣,此處涉及的暫停時間僅取決于root的數(shù)量以及重定位集的大小與對象的總活動集的比率,這通常相當小。所以不像很多收集器那樣,暫停時間隨堆增加而增加。
移動root后,下一階段是并發(fā)重定位。 在此階段,GC線程遍歷重定位集并重新定位其包含的頁中所有對象。 如果應(yīng)用程序線程試圖在GC重新定位對象之前加載它們,那么應(yīng)用程序線程也可以重定位該對象,這可以通過讀屏障(在從堆加載引用時觸發(fā))
這可確保應(yīng)用程序看到的所有引用都已更新,并且應(yīng)用程序不可能同時對重定位的對象進行操作。
GC線程最終將對重定位集中的所有對象重定位,然而可能仍有引用指向這些對象的舊位置。 GC可以遍歷對象圖并重新映射這些引用到新位置,但是這一步代價很高昂。 因此這一步與下一個標記階段合并在一起。在下一個GC周期的標記階段遍歷對象對象圖的時候,如果發(fā)現(xiàn)未重映射的引用,則將其重新映射,然后標記為活動狀態(tài)。
JVM內(nèi)存優(yōu)化
在《深入理解Java虛擬機》一書中講了很多jvm優(yōu)化思路,下面我來簡單說說。
java內(nèi)存抖動
堆內(nèi)存都有一定的大小,能容納的數(shù)據(jù)是有限制的,當Java堆的大小太大時,垃圾收集會啟動停止堆中不再應(yīng)用的對象,來釋放內(nèi)存。現(xiàn)在,內(nèi)存抖動這個術(shù)語可用于描述在極短時間內(nèi)分配給對象的過程。 具體如何優(yōu)化請谷歌查詢~
jvm大頁內(nèi)存
什么是內(nèi)存分頁?
CPU是通過尋址來訪問內(nèi)存的。32位CPU的尋址寬度是 0~0xFFFFFFFF,即4G,也就是說可支持的物理內(nèi)存最大是4G。但在實踐過程中,程序需要使用4G內(nèi)存,而可用物理內(nèi)存小于4G,導(dǎo)致程序不得不降低內(nèi)存占用。為了解決此類問題,現(xiàn)代CPU引入了MMU
(Memory Management Unit,內(nèi)存管理單元)。
MMU
的核心思想是利用虛擬地址替代物理地址,即CPU尋址時使用虛址,由MMU負責(zé)將虛址映射為物理地址。MMU的引入,解決了對物理內(nèi)存的限制,對程序來說,就像自己在使用4G內(nèi)存一樣。
內(nèi)存分頁(Paging)是在使用MMU的基礎(chǔ)上,提出的一種內(nèi)存管理機制。它將虛擬地址和物理地址按固定大小(4K)分割成頁(page)和頁幀(page frame),并保證頁與頁幀的大小相同。這種機制,從數(shù)據(jù)結(jié)構(gòu)上,保證了訪問內(nèi)存的高效,并使OS能支持非連續(xù)性的內(nèi)存分配。在程序內(nèi)存不夠用時,還可以將不常用的物理內(nèi)存頁轉(zhuǎn)移到其他存儲設(shè)備上,比如磁盤,這就是虛擬內(nèi)存。
要知道,虛擬地址與物理地址需要通過映射,才能使CPU正常工作。而映射就需要存儲映射表。在現(xiàn)代CPU架構(gòu)中,映射關(guān)系通常被存儲在物理內(nèi)存上一個被稱之為頁表(page table)的地方。 頁表是被存儲在內(nèi)存中的,CPU通過總線訪問內(nèi)存,肯定慢于直接訪問寄存器的。為了進一步優(yōu)化性能,現(xiàn)代CPU架構(gòu)引入了TLB
(Translation lookaside buffer,頁表寄存器緩沖),用來緩存一部分經(jīng)常訪問的頁表內(nèi)容 。
為什么要支持大內(nèi)存分頁?
TLB是有限的,這點毫無疑問。當超出TLB的存儲極限時,就會發(fā)生 TLB miss,于是OS就會命令CPU去訪問內(nèi)存上的頁表。如果頻繁的出現(xiàn)TLB miss,程序的性能會下降地很快。
為了讓TLB可以存儲更多的頁地址映射關(guān)系,我們的做法是調(diào)大內(nèi)存分頁大小。
如果一個頁4M,對比一個頁4K,前者可以讓TLB多存儲1000個頁地址映射關(guān)系,性能的提升是比較可觀的。
開啟JVM大頁內(nèi)存
JVM啟用時加參數(shù) -XX:LargePageSizeInBytes=10m 如果JDK是在1.5 update5以前的,還需要加 -XX:+UseLargePages,作用是啟用大內(nèi)存頁支持。
通過軟引用和弱引用提升JVM內(nèi)存使用性能
強軟弱虛
- 強引用:
只要引用存在,垃圾回收器永遠不會回收
Object obj = new Object();
//可直接通過obj取得對應(yīng)的對象 如obj.equels(new Object());
而這樣 obj對象對后面new Object的一個強引用,只有當obj這個引用被釋放之后,對象才會被釋放掉,這也是我們經(jīng)常所用到的編碼形式。
- 軟引用(可以實現(xiàn)緩存):
非必須引用,內(nèi)存溢出之前進行回收,可以通過以下代碼實現(xiàn)
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();//有時候會返回null
這時候sf是對obj的一個軟引用,通過sf.get()方法可以取到這個對象,當然,當這個對象被標記為需要回收的對象時,則返回null;軟引用主要用戶實現(xiàn)類似緩存的功能,在內(nèi)存足夠的情況下直接通過軟引用取值,無需從繁忙的真實來源查詢數(shù)據(jù),提升速度;當內(nèi)存不足時,自動刪除這部分緩存數(shù)據(jù),從真正的來源查詢這些數(shù)據(jù)。
- 弱引用(用來在回調(diào)函數(shù)中防止內(nèi)存泄露):
第二次垃圾回收時回收,可以通過如下代碼實現(xiàn)
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();//有時候會返回null
wf.isEnQueued();//返回是否被垃圾回收器標記為即將回收的垃圾
弱引用是在第二次垃圾回收時回收,短時間內(nèi)通過弱引用取對應(yīng)的數(shù)據(jù),可以取到,當執(zhí)行過第二次垃圾回收時,將返回null。弱引用主要用于監(jiān)控對象是否已經(jīng)被垃圾回收器標記為即將回收的垃圾,可以通過弱引用的isEnQueued方法返回對象是否被垃圾回收器標記。
- 虛引用:
垃圾回收時回收,無法通過引用取到對象值,可以通過如下代碼實現(xiàn)
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();//永遠返回null
pf.isEnQueued();//返回是否從內(nèi)存中已經(jīng)刪除
虛引用是每次垃圾回收的時候都會被回收,通過虛引用的get方法永遠獲取到的數(shù)據(jù)為null,因此也被成為幽靈引用。虛引用主要用于檢測對象是否已經(jīng)從內(nèi)存中刪除。
優(yōu)化
簡單來說,可以使用軟引用還引用數(shù)量巨大的對象,詳情請參考http://www.cnblogs.com/JavaArchitect/p/8685993.html
總結(jié)
此篇文章總共1.5W字,我從計算機物理內(nèi)存體系講到了java內(nèi)存模型,在通過java內(nèi)存模型引出了JVM內(nèi)存的相關(guān)知識點。覺得寫的好的請給個贊。本篇文章我會率先發(fā)布在我的個人博客,隨后會在掘金等平臺相繼發(fā)出。最后,非常感謝你的閱讀~
參考資料
文中的各種超鏈接
《深入理解Java虛擬機》
《Java并發(fā)編程的藝術(shù)》
《架構(gòu)解密從分布式到微服務(wù)》
Stefan Karlsson和PerLiden Jfokus的演講(請用正確的姿勢魔法上網(wǎng))
聲明
【版權(quán)申明】此片為原創(chuàng)內(nèi)容,使用MIT授權(quán)條款,請遵守對應(yīng)的義務(wù),即被授權(quán)人有義務(wù)在所有副本中都必須包含版權(quán)聲明。謝謝合作~
想要解鎖更多新姿勢?請訪問我的個人博客https://blog.tengshe789.tech/(??
github社區(qū)地址https://github.com/tengshe789/,歡迎互fo