又是一年秋招季轩猩,哎呀媽呀我被虐的慘來~這不卷扮,前幾陣失蹤沒更新博客荡澎,其實是我偷偷把時間用在復(fù)習(xí)課本了(霧
堅持在社區(qū)分享博客也很久了,由于過去的文章有很多疏漏之處晤锹,很多大佬都在評論指出我的過錯摩幔,我很開心也很失望,開心的是有大家?guī)臀抑赋鲥e誤鞭铆,失望的鄙人學(xué)識淺薄總沒法做到完美或衡。總之车遂,歡迎評論區(qū)各種pr~
好封断,回到正題。復(fù)習(xí)的時候舶担,無意間看到j(luò)ava虛擬機(jī)的有關(guān)知識點坡疼,我產(chǎn)生了非常濃厚的興趣,今天我來結(jié)合計算機(jī)內(nèi)存模型的相關(guān)知識衣陶,與Java內(nèi)存模型柄瑰、Java對象模型、JVM內(nèi)存結(jié)構(gòu)等相關(guān)的知識串聯(lián)起來剪况,本篇文章共1.5W字狱意,分享給大家,感謝閱讀拯欧。
想要解鎖更多新姿勢详囤?請訪問我的個人博客https://blog.tengshe789.tech/(??
計算機(jī)內(nèi)存
相信每個人都有一臺電腦,也有diy電腦的經(jīng)歷「渥鳎現(xiàn)在一臺功能強(qiáng)大的diy電腦大概3k就能組裝起來藏姐,一個i5-8400 的cpu 869元,DDR4 內(nèi)存 1200塊錢该贾,b360主板300元 散熱器50元 機(jī)械硬盤200元 350w電源300元 機(jī)箱100元 羔杨,沒錯,只要3k就能拿到一個性能強(qiáng)大的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)在強(qiáng)大的AMD TR-2990WX就是4die cpu揩抡,每個die里面有8個核心(core)
- core:也就是物理核心了户侥。core這個詞是英特爾起的镀琉,起初是為了與競爭對手AMD區(qū)別開,后面用的多了也淡了蕊唐。
- thread:就是硬件線程數(shù)屋摔。一個程序執(zhí)行可能需要多個線程一起進(jìn)行~而現(xiàn)在也就比較強(qiáng)大的超線程技術(shù),過去的cpu往往一個cpu核心只支持一個線程替梨,現(xiàn)在一些強(qiáng)大的cpu中凡壤,就譬如IBM 的POWER 9 ,支持8核心32個線程(平均一個核心4個線程)耙替,理論性能非常強(qiáng)大。
總結(jié)一下曹体,以明星cpu AMD TR-2990WX作為栗子俗扇,這個cpu使用一個socket,一個socket里面有4個die箕别,總共32個物理核心64個線程
cpu緩存
我們都知道铜幽,cpu將要處理的數(shù)據(jù)會放到內(nèi)存中保存,可是串稀,為什么會這樣除抛,將內(nèi)存緩存硬盤行不行呢?
答案當(dāng)然是不行的母截。cpu的處理速度很強(qiáng)大到忽,內(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ù)其實是在電容里面的椭微,電容需要充放電才能進(jìn)行讀寫操作吧慢,這就導(dǎo)致其讀寫數(shù)據(jù)就有比較大的延遲問題。
- 存儲可以看錯一個二維數(shù)組赏表,每個存儲單元都有其行地址列地址检诗。SRAM的容量很小匈仗,其存儲單元比較短(行列短),可以一次性傳輸?shù)絊RAM中逢慌;而DRAM悠轩,需要分別傳送行列地址。
- SRAM的頻率和cpu頻率比較接近攻泼;而DRAM的頻率和cpu差距比較大火架。
近代的緩存通常被集成到cpu當(dāng)中,為了適應(yīng)性能與成本的需要忙菠,現(xiàn)實中的緩存往往使用金字塔型多級緩存架構(gòu)何鸡。也就是當(dāng)CPU要讀取一個數(shù)據(jù)時,首先從一級緩存中查找牛欢,如果沒有找到再從二級緩存中查找骡男,如果還是沒有就從三級緩存或內(nèi)存中查找。
下面是英特爾最近以來用的初代skylake架構(gòu)
可以看到傍睹,每個個核心有專屬的L1隔盛,L2緩存,他們共享一個L3緩存拾稳。如果cpu如果要訪問內(nèi)存中的數(shù)據(jù)吮炕,必須要經(jīng)過L1,L2,L3,LLC(或者L4)四層緩存。
緩存一致性問題
最開始的cpu访得,其實只是一個核心一個線程的龙亲,當(dāng)時根本不需要考慮緩存一致性問題,單線程悍抑,也就是cpu核心的緩存只被一個線程訪問俱笛。緩存獨占,不會出現(xiàn)訪問沖突等問題传趾。
后來超線程技術(shù)來到我們視野迎膜,''單核CPU多線程'',也就是進(jìn)程中的多個線程會同時訪問進(jìn)程中的共享數(shù)據(jù)浆兰,CPU將某塊內(nèi)存加載到緩存后磕仅,不同線程在訪問相同的物理地址的時候,都會映射到相同的緩存位置簸呈,這樣即使發(fā)生線程的切換榕订,緩存仍然不會失效。但由于任何時刻只能有一個線程在執(zhí)行蜕便,因此不會出現(xiàn)緩存訪問沖突劫恒。
時代不斷發(fā)展,“多核CPU多線程”來了,即多個線程訪問進(jìn)程中的某個共享內(nèi)存两嘴,且這多個線程分別在不同的核心上執(zhí)行丛楚,則每個核心都會在各自的caehe中保留一份共享內(nèi)存的緩沖。由于多核是可以并行的憔辫,可能會出現(xiàn)多個線程同時寫各自的緩存的情況趣些,而各自的cache之間的數(shù)據(jù)就有可能不同。
這就是我們說的緩存一致性問題贰您。
目前公認(rèn)最好的解決方案是英特爾的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é)的時間基本相同栖茉。
機(jī)智的intel就規(guī)定了篮绿,cpu緩存中最小的存儲單元是緩存行cache line
,在x86的cpu中吕漂,一個cache line
儲存64字節(jié)亲配,每一級的緩存都會被劃分成許多組cache line
。
緩存工作原理請看??維基百科
接下來我們看看MESI規(guī)范惶凝,這其實是用四種緩存行狀態(tài)命名的吼虎,我們定義了CPU中每個緩存行使用4種狀態(tài)進(jìn)行標(biāo)記(使用額外的兩位(bit)表示),分別是:
-
M: 被修改(Modified)
該緩存行只被緩存在該CPU的緩存中苍鲜,并且是被修改過的(dirty),即與主存中的數(shù)據(jù)不一致思灰,該緩存行中的內(nèi)存需要在未來的某個時間點(允許其它CPU讀取請主存中相應(yīng)內(nèi)存之前)寫回(write back)主存。當(dāng)被寫回主存之后混滔,該緩存行的狀態(tài)會變成獨享(exclusive)狀態(tài)洒疚。
-
E: 獨享的(Exclusive)
該緩存行只被緩存在該CPU的緩存中,它是未被修改過的(clean)坯屿,與主存中數(shù)據(jù)一致油湖。該狀態(tài)可以在任何時刻當(dāng)有其它CPU讀取該內(nèi)存時變成共享狀態(tài)(shared)。同樣地领跛,當(dāng)CPU修改該緩存行中內(nèi)容時乏德,該狀態(tài)可以變成Modified狀態(tài)。
-
S: 共享的(Shared)
該狀態(tài)意味著該緩存行可能被多個CPU緩存吠昭,并且各個緩存中的數(shù)據(jù)與主存數(shù)據(jù)一致(clean)喊括,當(dāng)有一個CPU修改該緩存行中胧瓜,其它CPU中該緩存行可以被作廢(變成無效狀態(tài)(Invalid))。
-
I: 無效的(Invalid)
該緩存是無效的(可能有其它CPU修改了該緩存行)瘾晃。
然而贷痪,只是有這四種狀態(tài)也會帶來一定的問題。下面引用一下oracle的文檔蹦误。
同時更新來自不同處理器的相同緩存代碼行中的單個元素會使整個緩存代碼行無效劫拢,即使這些更新在邏輯上是彼此獨立的。每次對緩存代碼行的單個元素進(jìn)行更新時强胰,都會將此代碼行標(biāo)記為無效舱沧。其他訪問同一代碼行中不同元素的處理器將看到該代碼行已標(biāo)記為無效。即使所訪問的元素未被修改偶洋,也會強(qiáng)制它們從內(nèi)存或其他位置獲取該代碼行的較新副本熟吏。這是因為基于緩存代碼行保持緩存一致性,而不是針對單個元素的玄窝。因此牵寺,互連通信和開銷方面都將有所增長。并且恩脂,正在進(jìn)行緩存代碼行更新的時候帽氓,禁止訪問該代碼行中的元素。
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要互斥的對這幾個變量進(jìn)行操作岁钓,很有可能要互相搶占資源,導(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)管理著一個隊列,每個處理器依次處理隊列中的進(jìn)程芬为。如果兩個處理器同時請求訪問一個資源(例如同一段內(nèi)存地址)萄金,由硬件蟀悦、軟件的鎖機(jī)制去解決資源爭用問題媚朦。
[所謂對稱多處理器結(jié)構(gòu),是指服務(wù)器中多個 CPU 對稱工作日戈,無主次或從屬關(guān)系询张。各 CPU 共享相同的物理內(nèi)存,每個 CPU 訪問內(nèi)存中的任何地址所需時間是相同的浙炼,因此 SMP 也被稱為一致存儲器訪問結(jié)構(gòu) (UMA : Uniform Memory Access) 份氧。對 SMP 服務(wù)器進(jìn)行擴(kuò)展的方式包括增加內(nèi)存、使用更快的 CPU 弯屈、增加 CPU 蜗帜、擴(kuò)充 I/O( 槽口數(shù)與總線數(shù) ) 以及添加更多的外部設(shè)備 ( 通常是磁盤存儲 ) 捻激。
SMP 服務(wù)器的主要特征是共享耙蔑,系統(tǒng)中所有資源 (CPU 、內(nèi)存楚午、 I/O 等 ) 都是共享的。也正是由于這種特征湘捎,導(dǎo)致了 SMP 服務(wù)器的主要問題诀豁,那就是它的擴(kuò)展能力非常有限。對于 SMP 服務(wù)器而言窥妇,每一個共享的環(huán)節(jié)都可能造成 SMP 服務(wù)器擴(kuò)展時的瓶頸舷胜,而最受限制的則是內(nèi)存。由于每個 CPU 必須通過相同的內(nèi)存總線訪問相同的內(nèi)存資源活翩,因此隨著 CPU 數(shù)量的增加烹骨,內(nèi)存訪問沖突將迅速增加,最終會造成 CPU 資源的浪費(fèi)纱新,使 CPU 性能的有效性大大降低展氓。實驗證明, SMP 服務(wù)器 CPU 利用率最好的情況是 2 至 4 個 CPU 脸爱。
- NUMA(Non-Uniform Memory Access)
由于 SMP 在擴(kuò)展能力上的限制遇汞,人們開始探究如何進(jìn)行有效地擴(kuò)展從而構(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) 進(jìn)行連接和信息交互埋凯,因此每個 CPU 可以訪問整個系統(tǒng)的內(nèi)存 ( 這是 NUMA 系統(tǒng)與 MPP 系統(tǒng)的重要差別 ) 。顯然扫尖,訪問本地內(nèi)存的速度將遠(yuǎn)遠(yuǎn)高于訪問遠(yuǎn)地內(nèi)存 ( 系統(tǒng)內(nèi)其它節(jié)點的內(nèi)存 ) 的速度白对,這也是非一致存儲訪問 NUMA 的由來。由于這個特點换怖,為了更好地發(fā)揮系統(tǒng)性能甩恼,開發(fā)應(yīng)用程序時需要盡量減少不同 CPU 模塊之間的信息交互。
利用 NUMA 技術(shù)沉颂,可以較好地解決原來 SMP 系統(tǒng)的擴(kuò)展問題条摸,在一個物理服務(wù)器內(nèi)可以支持上百個 CPU 。比較典型的 NUMA 服務(wù)器的例子包括 HP 的 Superdome 铸屉、 SUN15K 钉蒲、 IBMp690 等。
但 NUMA 技術(shù)同樣有一定缺陷彻坛,由于訪問遠(yuǎn)地內(nèi)存的延時遠(yuǎn)遠(yuǎn)超過本地內(nèi)存顷啼,因此當(dāng) 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 提供了另外一種進(jìn)行系統(tǒng)擴(kuò)展的方式,它由多個 SMP 服務(wù)器通過一定的節(jié)點互聯(lián)網(wǎng)絡(luò)進(jìn)行連接煮嫌,協(xié)同工作笛谦,完成相同的任務(wù),從用戶的角度來看是一個服務(wù)器系統(tǒng)昌阿。其基本特征是由多個 SMP 服務(wù)器 ( 每個 SMP 服務(wù)器稱節(jié)點 ) 通過節(jié)點互聯(lián)網(wǎng)絡(luò)連接而成饥脑,每個節(jié)點只訪問自己的本地資源 ( 內(nèi)存、存儲等 ) 懦冰,是一種完全無共享 (Share Nothing) 結(jié)構(gòu)灶轰,因而擴(kuò)展能力最好,理論上其擴(kuò)展無限制刷钢,目前的技術(shù)可實現(xiàn) 512 個節(jié)點互聯(lián)笋颤,數(shù)千個 CPU 。目前業(yè)界對節(jié)點互聯(lián)網(wǎng)絡(luò)暫無標(biāo)準(zhǔn)内地,如 NCR 的 Bynet 伴澄, IBM 的 SPSwitch ,它們都采用了不同的內(nèi)部實現(xiàn)機(jī)制阱缓。但節(jié)點互聯(lián)網(wǎng)僅供 MPP 服務(wù)器內(nèi)部使用非凌,對用戶而言是透明的。
在 MPP 系統(tǒng)中茬祷,每個 SMP 節(jié)點也可以運(yùn)行自己的操作系統(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ù)雜的機(jī)制來調(diào)度和平衡各個節(jié)點的負(fù)載和并行處理過程最盅。目前一些基于 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é)點的負(fù)載督函。
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)存的訪問都能保證效果一致的機(jī)制及規(guī)范阔墩。就像JSR-133: Java Memory Model and Thread Specification 中描述了嘿架,JMM是和多線程相關(guān)的,他描述了一組規(guī)則或規(guī)范啸箫,這個規(guī)范定義了一個線程對共享變量的寫入時對另一個線程是可見的耸彪。
那么,簡單總結(jié)下忘苛,Java的多線程之間是通過共享內(nèi)存進(jìn)行通信的蝉娜,而由于采用共享內(nèi)存進(jìn)行通信,在通信過程中會存在一系列如可見性扎唾、原子性召川、順序性等問題,而JMM就是圍繞著多線程通信以及與其相關(guān)的一系列特性而建立的模型胸遇。JMM定義了一些語法集荧呐,這些語法集映射到Java語言中就是volatile
、synchronized
等關(guān)鍵字纸镊。
在JMM中倍阐,我們把多個線程間通信的共享內(nèi)存稱之為主內(nèi)存,而在并發(fā)編程中多個線程都維護(hù)了一個自己的本地內(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)原理是钥。
可見性
可見性是指當(dāng)多個線程訪問同一個變量時,一個線程修改了這個變量的值缅叠,其他線程能夠立即看得到修改的值悄泥。
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代碼是要運(yùn)行在虛擬機(jī)上的摆屯,而虛擬機(jī)在執(zhí)行Java程序的過程中會把所管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域邻遏,這些區(qū)域都有各自的用途。下面我們來說說JVM運(yùn)行時內(nèi)存區(qū)域結(jié)構(gòu)
JVM運(yùn)行時內(nèi)存區(qū)域結(jié)構(gòu)
在《Java虛擬機(jī)規(guī)范(Java SE 8)》中描述了JVM運(yùn)行時內(nèi)存區(qū)域結(jié)構(gòu)如下:
1.程序計數(shù)器
程序計數(shù)器(Program Counter Register)虐骑,也有稱作為PC寄存器准验。想必學(xué)過匯編語言的朋友對程序計數(shù)器這個概念并不陌生,在匯編語言中廷没,程序計數(shù)器是指CPU中的寄存器糊饱,它保存的是程序當(dāng)前執(zhí)行的指令的地址(也可以說保存下一條指令的所在存儲單元的地址),當(dāng)CPU需要執(zhí)行指令時颠黎,需要從程序計數(shù)器中得到當(dāng)前需要執(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ù)器中保存的是當(dāng)前需要執(zhí)行的指令的地址钠导;如果線程執(zhí)行的是native方法,則程序計數(shù)器中的值是undefined森瘪。
由于程序計數(shù)器中存儲的數(shù)據(jù)所占空間的大小不會隨程序的執(zhí)行而發(fā)生改變牡属,因此,對于程序計數(shù)器是不會發(fā)生內(nèi)存溢出現(xiàn)象(OutOfMemory)的扼睬。
2.Java棧
Java棧也稱作虛擬機(jī)棧(Java Vitual Machine Stack)逮栅,也就是我們常常所說的棧,跟C語言的數(shù)據(jù)段中的棧類似窗宇。事實上措伐,Java棧是Java方法執(zhí)行的內(nèi)存模型。為什么這么說呢军俊?下面就來解釋一下其中的原因侥加。
Java棧中存放的是一個個的棧幀,每個棧幀對應(yīng)一個被調(diào)用的方法粪躬,在棧幀中包括局部變量表(Local Variables)官硝、操作數(shù)棧(Operand Stack)、指向當(dāng)前方法所屬的類的運(yùn)行時常量池(運(yùn)行時常量池的概念在方法區(qū)部分會談到)的引用(Reference to runtime constant pool)短蜕、方法返回地址(Return Address)和一些額外的附加信息氢架。當(dāng)線程執(zhí)行一個方法時,就會隨之創(chuàng)建一個對應(yīng)的棧幀朋魔,并將建立的棧幀壓棧岖研。當(dāng)方法執(zhí)行完畢之后,便會將棧幀出棧。因此可知孙援,線程當(dāng)前執(zhí)行的方法所對應(yīng)的棧幀必定位于Java棧的頂部害淤。講到這里,大家就應(yīng)該會明白為什么 在 使用 遞歸方法的時候容易導(dǎo)致棧內(nèi)存溢出的現(xiàn)象了以及為什么棧區(qū)的空間不用程序員去管理了(當(dāng)然在Java中拓售,程序員基本不用關(guān)系到內(nèi)存分配和釋放的事情窥摄,因為Java有自己的垃圾回收機(jī)制),這部分空間的分配和釋放都是由系統(tǒng)自動實施的础淤。對于所有的程序設(shè)計語言來說崭放,棧這部分空間對程序員來說是不透明的。下圖表示了一個Java棧的模型:
局部變量表鸽凶,顧名思義币砂,想必不用解釋大家應(yīng)該明白它的作用了吧。就是用來存儲方法中的局部變量(包括在方法中聲明的非靜態(tài)變量以及函數(shù)形參)玻侥。對于基本數(shù)據(jù)類型的變量决摧,則直接存儲它的值,對于引用類型的變量凑兰,則存的是指向?qū)ο蟮囊谜谱>植孔兞勘淼拇笮≡诰幾g器就可以確定其大小了,因此在程序執(zhí)行期間局部變量表的大小是不會改變的姑食。
操作數(shù)棧波岛,想必學(xué)過數(shù)據(jù)結(jié)構(gòu)中的棧的朋友想必對表達(dá)式求值問題不會陌生,棧最典型的一個應(yīng)用就是用來對表達(dá)式求值矢门。想想一個線程執(zhí)行方法的過程中盆色,實際上就是不斷執(zhí)行語句的過程,而歸根到底就是進(jìn)行計算的過程祟剔。因此可以這么說隔躲,程序中的所有計算過程都是在借助于操作數(shù)棧來完成的。
指向運(yùn)行時常量池的引用物延,因為在方法執(zhí)行的過程中有可能需要用到類中的常量宣旱,所以必須要有一個引用指向運(yùn)行時常量。
方法返回地址叛薯,當(dāng)一個方法執(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)作強(qiáng)制規(guī)定笼蛛,虛擬機(jī)可以自由實現(xiàn)它。在HotSopt虛擬機(jī)中直接就把本地方法棧和Java棧合二為一蛉鹿。
4.堆
在C語言中滨砍,堆這部分空間是唯一一個程序員可以管理的內(nèi)存區(qū)域。程序員可以通過malloc函數(shù)和free函數(shù)在堆上申請和釋放空間妖异。那么在Java中是怎么樣的呢惋戏?
Java中的堆是用來存儲對象本身的以及數(shù)組(當(dāng)然,數(shù)組引用是存放在Java棧中的)随闺。只不過和C語言中的不同日川,在Java中蔓腐,程序員基本不用去關(guān)心空間釋放的問題矩乐,Java的垃圾回收機(jī)制會自動進(jìn)行處理。因此這部分空間也是Java垃圾收集器管理的主要區(qū)域回论。另外散罕,堆是被所有線程共享的,在JVM中只有一個堆傀蓉。
5.方法區(qū)
方法區(qū)在JVM中也是一個非常重要的區(qū)域欧漱,它與堆一樣,是被線程共享的區(qū)域葬燎。在方法區(qū)中误甚,存儲了每個類的信息(包括類的名稱、方法信息谱净、字段信息)窑邦、靜態(tài)變量、常量以及編譯器編譯后的代碼等壕探。
在Class文件中除了類的字段冈钦、方法、接口等描述信息外李请,還有一項信息是常量池瞧筛,用來存儲編譯期間生成的字面量和符號引用。
在方法區(qū)中有一個非常重要的部分就是運(yùn)行時常量池导盅,它是每一個類或接口的常量池的運(yùn)行時表示形式较幌,在類和接口被加載到JVM后,對應(yīng)的運(yùn)行時常量池就被創(chuàng)建出來白翻。當(dāng)然并非Class文件常量池中的內(nèi)容才能進(jìn)入運(yùn)行時常量池乍炉,在運(yùn)行期間也可將新的常量放入運(yùn)行時常量池中,比如String的intern方法。
在JVM規(guī)范中恩急,沒有強(qiáng)制要求方法區(qū)必須實現(xiàn)垃圾回收杉畜。很多人習(xí)慣將方法區(qū)稱為“永久代”,是因為HotSpot虛擬機(jī)以永久代來實現(xiàn)方法區(qū)衷恭,從而JVM的垃圾收集器可以像管理堆區(qū)一樣管理這部分區(qū)域此叠,從而不需要專門為這部分設(shè)計垃圾回收機(jī)制。不過自從JDK7之后随珠,Hotspot虛擬機(jī)便將運(yùn)行時常量池從永久代移除了灭袁。
Java對象模型的內(nèi)存布局
java是一種面向?qū)ο蟮恼Z言,而Java對象在JVM中的存儲也是有一定的結(jié)構(gòu)的窗看。而這個關(guān)于Java對象自身的存儲模型稱之為Java對象模型茸歧。
HotSpot虛擬機(jī)中,設(shè)計了一個OOP-Klass Model显沈。OOP(Ordinary Object Pointer)指的是普通對象指針软瞎,而Klass用來描述對象實例的具體類型。
每一個Java類拉讯,在被JVM加載的時候涤浇,JVM會給這個類創(chuàng)建一個instanceKlass
,保存在方法區(qū)魔慷,用來在JVM層表示該Java類只锭。當(dāng)我們在Java代碼中,使用new創(chuàng)建一個對象的時候院尔,JVM會創(chuàng)建一個instanceOopDesc
對象蜻展,對象在內(nèi)存中存儲的布局可以分為3塊區(qū)域:對象頭(Header)、 實例數(shù)據(jù)(Instance Data)和對齊填充(Padding)邀摆。
- 對象頭:標(biāo)記字(32位虛擬機(jī)4B纵顾,64位虛擬機(jī)8B) + 類型指針(32位虛擬機(jī)4B,64位虛擬機(jī)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位虛擬機(jī)來說讥脐,對象大小必須是8B的整數(shù)倍遭居,不夠的話需要占位填充
JVM內(nèi)存垃圾收集器
為了理解現(xiàn)有收集器,我們需要先了解一些術(shù)語旬渠。最基本的垃圾收集涉及識別不再使用的內(nèi)存并使其可重用【闫迹現(xiàn)代收集器在幾個階段進(jìn)行這一過程,對于這些階段我們往往有如下描述:
- 并行- 在JVM運(yùn)行時告丢,同時存在應(yīng)用程序線程和垃圾收集器線程枪蘑。 并行階段是由多個gc線程執(zhí)行,即gc工作在它們之間分配岖免。 不涉及GC線程是否需要暫停應(yīng)用程序線程岳颇。
- 串行- 串行階段僅在單個gc線程上執(zhí)行。與之前一樣颅湘,它也沒有說明GC線程是否需要暫停應(yīng)用程序線程话侧。
- STW - STW階段,應(yīng)用程序線程被暫停闯参,以便gc執(zhí)行其工作瞻鹏。 當(dāng)應(yīng)用程序因為GC暫停時,這通常是由于Stop The World階段赢赊。
- 并發(fā) -如果一個階段是并發(fā)的乙漓,那么GC線程可以和應(yīng)用程序線程同時進(jìn)行级历。 并發(fā)階段很復(fù)雜释移,因為它們需要在階段完成之前處理可能使工作無效(譯者注:因為是并發(fā)進(jìn)行的,GC線程在完成一階段的同時寥殖,應(yīng)用線程也在工作產(chǎn)生操作內(nèi)存玩讳,所以需要額外處理)的應(yīng)用程序線程筝闹。
- 增量 -如果一個階段是增量的大刊,那么它可以運(yùn)行一段時間之后由于某些條件提前終止队询,例如需要執(zhí)行更高優(yōu)先級的gc階段清蚀,同時仍然完成生產(chǎn)性工作吕粹。 增量階段與需要完全完成的階段形成鮮明對比耿戚。
Serial收集器
Serial收集器是最基本的收集器瞳收,這是一個單線程收集器涮阔,它仍然是JVM在Client模式下的默認(rèn)新生代收集器叮盘。它有著優(yōu)于其他收集器的地方:簡單而高效(與其他收集器的單線程比較)秩贰,Serial收集器由于沒有線程交互的開銷,專心只做垃圾收集自然也獲得最高的效率柔吼。在用戶桌面場景下毒费,分配給JVM的內(nèi)存不會太多,停頓時間完全可以在幾十到一百多毫秒之間愈魏,只要收集不頻繁觅玻,這是完全可以接受的想际。
ParNew收集器
ParNew是Serial的多線程版本,在回收算法溪厘、對象分配原則上都是一致的胡本。ParNew收集器是許多運(yùn)行在Server模式下的默認(rèn)新生代垃圾收集器,其主要在于除了Serial收集器畸悬,目前只有ParNew收集器能夠與CMS收集器配合工作打瘪。
Parallel Scavenge收集器
Parallel Scavenge收集器是一個新生代垃圾收集器,其使用的算法是復(fù)制算法傻昙,也是并行的多線程收集器闺骚。
Parallel Scavenge 收集器更關(guān)注可控制的吞吐量,吞吐量等于運(yùn)行用戶代碼的時間/(運(yùn)行用戶代碼的時間+垃圾收集時間)妆档。直觀上僻爽,只要最大的垃圾收集停頓時間越小,吞吐量是越高的贾惦,但是GC停頓時間的縮短是以犧牲吞吐量和新生代空間作為代價的胸梆。比如原來10秒收集一次,每次停頓100毫秒须板,現(xiàn)在變成5秒收集一次碰镜,每次停頓70毫秒。停頓時間下降的同時习瑰,吞吐量也下降了绪颖。
停頓時間越短就越適合需要與用戶交互的程序;而高吞吐量則可以最高效的利用CPU的時間甜奄,盡快的完成計算任務(wù)柠横,主要適用于后臺運(yùn)算。
Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本课兄,也是一個單線程收集器牍氛,采用“標(biāo)記-整理算法”進(jìn)行回收。其運(yùn)行過程與Serial收集器一樣烟阐。
Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本搬俊,使用多線程和標(biāo)記-整理算法進(jìn)行垃圾回收。其通常與Parallel Scavenge收集器配合使用蜒茄,“吞吐量優(yōu)先”收集器是這個組合的特點唉擂,在注重吞吐量和CPU資源敏感的場合,都可以使用這個組合扩淀。
CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短停頓時間為目標(biāo)的收集器楔敌,CMS收集器采用標(biāo)記--清除算法,運(yùn)行在老年代驻谆。主要包含以下幾個步驟:
- 初始標(biāo)記
- 并發(fā)標(biāo)記
- 重新標(biāo)記
- 并發(fā)清除
其中初始標(biāo)記和重新標(biāo)記仍然需要“Stop the world”卵凑。初始標(biāo)記僅僅標(biāo)記GC Root能直接關(guān)聯(lián)的對象庆聘,并發(fā)標(biāo)記就是進(jìn)行GC Root Tracing過程,而重新標(biāo)記則是為了修正并發(fā)標(biāo)記期間勺卢,因用戶程序繼續(xù)運(yùn)行而導(dǎo)致標(biāo)記變動的那部分對象的標(biāo)記記錄伙判。
由于整個過程中最耗時的并發(fā)標(biāo)記和并發(fā)清除,收集線程和用戶線程一起工作黑忱,所以總體上來說宴抚,CMS收集器回收過程是與用戶線程并發(fā)執(zhí)行的。雖然CMS優(yōu)點是并發(fā)收集甫煞、低停頓菇曲,很大程度上已經(jīng)是一個不錯的垃圾收集器,但是還是有三個顯著的缺點:
- CMS收集器對CPU資源很敏感抚吠。在并發(fā)階段常潮,雖然它不會導(dǎo)致用戶線程停頓,但是會因為占用一部分線程(CPU資源)而導(dǎo)致應(yīng)用程序變慢楷力。
- CMS收集器不能處理浮動垃圾喊式。所謂的“浮動垃圾”,就是在并發(fā)標(biāo)記階段萧朝,由于用戶程序在運(yùn)行岔留,那么自然就會有新的垃圾產(chǎn)生,這部分垃圾被標(biāo)記過后检柬,CMS無法在當(dāng)次集中處理它們献联,只好在下一次GC的時候處理,這部分未處理的垃圾就稱為“浮動垃圾”厕吉。也是由于在垃圾收集階段程序還需要運(yùn)行酱固,即還需要預(yù)留足夠的內(nèi)存空間供用戶使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎填滿才進(jìn)行收集头朱,需要預(yù)留一部分空間提供并發(fā)收集時程序運(yùn)作使用。要是CMS預(yù)留的內(nèi)存空間不能滿足程序的要求龄减,這是JVM就會啟動預(yù)備方案:臨時啟動Serial Old收集器來收集老年代项钮,這樣停頓的時間就會很長。
- 由于CMS使用標(biāo)記--清除算法希停,所以在收集之后會產(chǎn)生大量內(nèi)存碎片烁巫。當(dāng)內(nèi)存碎片過多時,將會給分配大對象帶來困難宠能,這是就會進(jìn)行Full GC亚隙。
G1收集器
G1收集器與CMS相比有很大的改進(jìn):
· G1收集器采用標(biāo)記--整理算法實現(xiàn)。
· 可以非常精確地控制停頓违崇。
? G1收集器可以實現(xiàn)在基本不犧牲吞吐量的情況下完成低停頓的內(nèi)存回收阿弃,這是由于它極力的避免全區(qū)域的回收诊霹,G1收集器將Java堆(包括新生代和老年代)劃分為多個區(qū)域(Region),并在后臺維護(hù)一個優(yōu)先列表渣淳,每次根據(jù)允許的時間脾还,優(yōu)先回收垃圾最多的區(qū)域 。
ZGC收集器
Java 11 新加入的ZGC垃圾收集器號稱可以達(dá)到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
钞钙。 我們稍后解釋它們的用途。著色指針的一個問題是声离,當(dāng)您需要取消著色時芒炼,它需要額外的工作(因為需要屏蔽信息位)。 像SPARC這樣的平臺有內(nèi)置硬件支持指針屏蔽所以不是問題术徊,而對于x86平臺來說本刽,ZGC團(tuán)隊使用了簡潔的多重映射技巧。
多重映射
要了解多重映射的工作原理赠涮,我們需要簡要解釋虛擬內(nèi)存和物理內(nèi)存之間的區(qū)別子寓。 物理內(nèi)存是系統(tǒng)可用的實際內(nèi)存,通常是安裝的DRAM芯片的容量笋除。 虛擬內(nèi)存是抽象的斜友,這意味著應(yīng)用程序?qū)ΓㄍǔJ歉綦x的)物理內(nèi)存有自己的視圖。 操作系統(tǒng)負(fù)責(zé)維護(hù)虛擬內(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源代碼中有一個很好的圖表可以說明這一點也殖。讀屏障
讀屏障是每當(dāng)應(yīng)用程序線程從堆加載引用時運(yùn)行的代碼片段(即訪問對象上的非原生字段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ù)。
標(biāo)記
現(xiàn)在我們了解了這兩種新技術(shù)是什么纠修,讓我們來看看ZG的GC循環(huán)胳嘲。
GC循環(huán)的第一部分是標(biāo)記。標(biāo)記包括查找和標(biāo)記運(yùn)行中的應(yīng)用程序可以訪問的所有堆對象扣草,換句話說了牛,查找不是垃圾的對象。
ZGC的標(biāo)記分為三個階段辰妙。 第一階段是STW鹰祸,其中GC roots被標(biāo)記為活對象。 GC roots類似于局部變量密浑,通過它可以訪問堆上其他對象蛙婴。 如果一個對象不能通過遍歷從roots開始的對象圖來訪問,那么應(yīng)用程序也就無法訪問它肴掷,則該對象被認(rèn)為是垃圾敬锐。從roots訪問的對象集合稱為Live集。GC roots標(biāo)記步驟非常短呆瞻,因為roots的總數(shù)通常比較小。
該階段完成后径玖,應(yīng)用程序恢復(fù)執(zhí)行痴脾,ZGC開始下一階段,該階段同時遍歷對象圖并標(biāo)記所有可訪問的對象梳星。 在此階段期間赞赖,讀屏障針使用掩碼測試所有已加載的引用滚朵,該掩碼確定它們是否已標(biāo)記或尚未標(biāo)記,如果尚未標(biāo)記引用前域,則將其添加到隊列以進(jìn)行標(biāo)記辕近。
在遍歷完成之后,有一個最終的匿垄,時間很短的的Stop The World階段移宅,這個階段處理一些邊緣情況(我們現(xiàn)在將它忽略),該階段完成之后標(biāo)記階段就完成了椿疗。
重定位
GC循環(huán)的下一個主要部分是重定位漏峰。重定位涉及移動活動對象以釋放部分堆內(nèi)存。 為什么要移動對象而不是填補(bǔ)空隙届榄? 有些GC實際是這樣做的浅乔,但是它導(dǎo)致了一個不幸的后果,即分配內(nèi)存變得更加昂貴铝条,因為當(dāng)需要分配內(nèi)存時靖苇,內(nèi)存分配器需要找到可以放置對象的空閑空間。 相比之下班缰,如果可以釋放大塊內(nèi)存贤壁,那么分配內(nèi)存就很簡單,只需要將指針遞增新對象所需的內(nèi)存大小即可鲁捏。
ZGC將堆分成許多頁面芯砸,在此階段開始時,它同時選擇一組需要重定位活動對象的頁面给梅。選擇重定位集后假丧,會出現(xiàn)一個Stop The World暫停,其中ZGC重定位該集合中root對象动羽,并將他們的引用映射到新位置包帚。與之前的Stop The World步驟一樣,此處涉及的暫停時間僅取決于root的數(shù)量以及重定位集的大小與對象的總活動集的比率运吓,這通常相當(dāng)小渴邦。所以不像很多收集器那樣,暫停時間隨堆增加而增加拘哨。
移動root后谋梭,下一階段是并發(fā)重定位。 在此階段倦青,GC線程遍歷重定位集并重新定位其包含的頁中所有對象瓮床。 如果應(yīng)用程序線程試圖在GC重新定位對象之前加載它們,那么應(yīng)用程序線程也可以重定位該對象,這可以通過讀屏障(在從堆加載引用時觸發(fā))
這可確保應(yīng)用程序看到的所有引用都已更新隘庄,并且應(yīng)用程序不可能同時對重定位的對象進(jìn)行操作踢步。
GC線程最終將對重定位集中的所有對象重定位,然而可能仍有引用指向這些對象的舊位置丑掺。 GC可以遍歷對象圖并重新映射這些引用到新位置获印,但是這一步代價很高昂。 因此這一步與下一個標(biāo)記階段合并在一起街州。在下一個GC周期的標(biāo)記階段遍歷對象對象圖的時候兼丰,如果發(fā)現(xiàn)未重映射的引用,則將其重新映射菇肃,然后標(biāo)記為活動狀態(tài)地粪。
JVM內(nèi)存優(yōu)化
在《深入理解Java虛擬機(jī)》一書中講了很多jvm優(yōu)化思路,下面我來簡單說說琐谤。
java內(nèi)存抖動
堆內(nèi)存都有一定的大小蟆技,能容納的數(shù)據(jù)是有限制的,當(dāng)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負(fù)責(zé)將虛址映射為物理地址窄瘟。MMU的引入,解決了對物理內(nèi)存的限制趟卸,對程序來說寞肖,就像自己在使用4G內(nèi)存一樣纲酗。
內(nèi)存分頁(Paging)是在使用MMU的基礎(chǔ)上衰腌,提出的一種內(nèi)存管理機(jī)制新蟆。它將虛擬地址和物理地址按固定大小(4K)分割成頁(page)和頁幀(page frame)右蕊,并保證頁與頁幀的大小相同琼稻。這種機(jī)制,從數(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)存,肯定慢于直接訪問寄存器的寒锚。為了進(jìn)一步優(yōu)化性能劫映,現(xiàn)代CPU架構(gòu)引入了TLB
(Translation lookaside buffer,頁表寄存器緩沖)刹前,用來緩存一部分經(jīng)常訪問的頁表內(nèi)容 泳赋。
為什么要支持大內(nèi)存分頁?
TLB是有限的腮郊,這點毫無疑問摹蘑。當(dāng)超出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)存使用性能
強(qiáng)軟弱虛
- 強(qiáng)引用:
只要引用存在较曼,垃圾回收器永遠(yuǎn)不會回收
Object obj = new Object();
//可直接通過obj取得對應(yīng)的對象 如obj.equels(new Object());
而這樣 obj對象對后面new Object的一個強(qiáng)引用磷斧,只有當(dāng)obj這個引用被釋放之后,對象才會被釋放掉捷犹,這也是我們經(jīng)常所用到的編碼形式弛饭。
- 軟引用(可以實現(xiàn)緩存):
非必須引用,內(nèi)存溢出之前進(jìn)行回收萍歉,可以通過以下代碼實現(xiàn)
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();//有時候會返回null
這時候sf是對obj的一個軟引用侣颂,通過sf.get()方法可以取到這個對象,當(dāng)然枪孩,當(dāng)這個對象被標(biāo)記為需要回收的對象時憔晒,則返回null;軟引用主要用戶實現(xiàn)類似緩存的功能销凑,在內(nèi)存足夠的情況下直接通過軟引用取值丛晌,無需從繁忙的真實來源查詢數(shù)據(jù),提升速度斗幼;當(dāng)內(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();//返回是否被垃圾回收器標(biāo)記為即將回收的垃圾
弱引用是在第二次垃圾回收時回收,短時間內(nèi)通過弱引用取對應(yīng)的數(shù)據(jù)桐经,可以取到毁兆,當(dāng)執(zhí)行過第二次垃圾回收時,將返回null阴挣。弱引用主要用于監(jiān)控對象是否已經(jīng)被垃圾回收器標(biāo)記為即將回收的垃圾气堕,可以通過弱引用的isEnQueued方法返回對象是否被垃圾回收器標(biāo)記。
- 虛引用:
垃圾回收時回收畔咧,無法通過引用取到對象值茎芭,可以通過如下代碼實現(xiàn)
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();//永遠(yuǎn)返回null
pf.isEnQueued();//返回是否從內(nèi)存中已經(jīng)刪除
虛引用是每次垃圾回收的時候都會被回收,通過虛引用的get方法永遠(yuǎn)獲取到的數(shù)據(jù)為null誓沸,因此也被成為幽靈引用梅桩。虛引用主要用于檢測對象是否已經(jīng)從內(nèi)存中刪除。
優(yōu)化
簡單來說拜隧,可以使用軟引用還引用數(shù)量巨大的對象宿百,詳情請參考http://www.cnblogs.com/JavaArchitect/p/8685993.html
總結(jié)
此篇文章總共1.5W字趁仙,我從計算機(jī)物理內(nèi)存體系講到了java內(nèi)存模型,在通過java內(nèi)存模型引出了JVM內(nèi)存的相關(guān)知識點垦页。覺得寫的好的請給個贊雀费。本篇文章我會率先發(fā)布在我的個人博客,隨后會在掘金等平臺相繼發(fā)出外臂。最后坐儿,非常感謝你的閱讀~
參考資料
文中的各種超鏈接
《深入理解Java虛擬機(jī)》
《Java并發(fā)編程的藝術(shù)》
《架構(gòu)解密從分布式到微服務(wù)》
SMP、NUMA宋光、MPP體系結(jié)構(gòu)介紹
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