Java堆記憶體是執行緒共享的!面試官:你確定嗎?
阿新 • • 發佈:2020-03-10
Java作為一種面向物件的,跨平臺語言,其物件、記憶體等一直是比較難的知識點,所以,即使是一個Java的初學者,也一定或多或少的對JVM有一些瞭解。可以說,關於JVM的相關知識,基本是每個Java開發者必學的知識點,也是面試的時候必考的知識點。
在JVM的記憶體結構中,比較常見的兩個區域就是堆記憶體和棧記憶體(如無特指,本文提到的棧均指的是虛擬機器棧),關於堆和棧的區別,很多開發者也是如數家珍,有很多書籍,或者網上的文章大概都是這樣介紹的:
> 1、堆是執行緒共享的記憶體區域,棧是執行緒獨享的記憶體區域。
>
> 2、堆中主要存放物件例項,棧中主要存放各種基本資料型別、物件的引用。
但是,作者可以很負責任的告訴大家,**以上兩個結論均不是完全正確的。**
本文首先帶大家瞭解一下為什麼我會說“堆是執行緒共享的記憶體區域,棧是執行緒獨享的記憶體區域。”這句話並不完全正確!?關於JVM記憶體結構的相關知識,大家可以閱讀[JVM記憶體結構 VS Java記憶體模型 VS Java物件模型][1]、[萬萬沒想到,JVM記憶體結構的面試題可以問的這麼難?][2]等文章。
在開始進入正題之前,請允許我問一個和這個問題看似沒有任何關係的問題:**Java物件的記憶體分配過程是如何保證執行緒安全的?**
### Java物件的記憶體分配過程是如何保證執行緒安全的?
我們知道,Java是一門面向物件的語言,我們在Java中使用的物件都需要被創建出來,在Java中,建立一個物件的方法有很多種,但是無論如何,物件在建立過程中,都需要進行記憶體分配。
物件的記憶體分配過程中,主要是物件的引用指向這個記憶體區域,然後進行初始化操作。
但是,因為堆是全域性共享的,因此在同一時間,可能有多個執行緒在堆上申請空間,那麼,在併發場景中,如果兩個執行緒先後把物件引用指向了同一個記憶體區域,怎麼辦。
為了解決這個併發問題,物件的記憶體分配過程就必須進行同步控制。但是我們都知道,**無論是使用哪種同步方案(實際上虛擬機器使用的可能是CAS),都會影響記憶體的分配效率。**
而Java物件的分配是Java中的高頻操作,所有,人們想到另外一個辦法來提升效率。這裡我們重點說一個HotSpot虛擬機器的方案:
> 每個執行緒在Java堆中預先分配一小塊記憶體,然後再給物件分配記憶體的時候,直接在自己這塊”私有”記憶體中分配,當這部分割槽域用完之後,再分配新的”私有”記憶體。
這種方案被稱之為TLAB分配,即Thread Local Allocation Buffer。這部分Buffer是從堆中劃分出來的,但是是本地執行緒獨享的。
### 什麼是TLAB
TLAB是虛擬機器在堆記憶體的eden劃分出來的一塊專用空間,是執行緒專屬的。在虛擬機器的TLAB功能啟動的情況下,線上程初始化時,虛擬機器會為每個執行緒分配一塊TLAB空間,只給當前執行緒使用,這樣每個執行緒都單獨擁有一個空間,如果需要分配記憶體,就在自己的空間上分配,這樣就不存在競爭的情況,可以大大提升分配效率。
注意到上面的描述中"執行緒專屬"、"只給當前執行緒使用"、"每個執行緒單獨擁有"的描述了嗎?
所以說,因為有了TLAB技術,堆記憶體並不是完完全全的執行緒共享,其eden區域中還是有一部分空間是分配給執行緒獨享的。
**這裡值得注意的是,我們說TLAB是執行緒獨享的,但是隻是在“分配”這個動作上是執行緒獨佔的,至於在讀取、垃圾回收等動作上都是執行緒共享的。而且在使用上也沒有什麼區別。**
也就是說,雖然每個執行緒在初始化時都會去堆記憶體中申請一塊TLAB,並不是說這個TLAB區域的記憶體其他執行緒就完全無法訪問了,其他執行緒的讀取還是可以的,只不過無法在這個區域中分配記憶體而已。
並且,在TLAB分配之後,並不影響物件的移動和回收,也就是說,雖然物件剛開始可能通過TLAB分配記憶體,存放在Eden區,但是還是會被垃圾回收或者被移到Survivor Space、Old Gen等。
還有一點需要注意的是,我們說TLAB是在eden區分配的,因為eden區域本身就不太大,而且TLAB空間的記憶體也非常小,預設情況下僅佔有整個Eden空間的1%。所以,必然存在一些大物件是無法在TLAB直接分配。
遇到TLAB中無法分配的大物件,物件還是可能在eden區或者老年代等進行分配的,但是這種分配就需要進行同步控制,這也是為什麼我們經常說:**小的物件比大的物件分配起來更加高效。**
### TLAB帶來的問題
雖然在一定程度上,TLAB大大的提升了物件的分配速度,但是TLAB並不是就沒有任何問題的。
前面我們說過,因為TLAB記憶體區域並不是很大,所以,有可能會經常出現不夠的情況。在《實戰Java虛擬機器》中有這樣一個例子:
比如一個執行緒的TLAB空間有100KB,其中已經使用了80KB,當需要再分配一個30KB的物件時,就無法直接在TLAB中分配,遇到這種情況時,有兩種處理方案:
1、如果一個物件需要的空間大小超過TLAB中剩餘的空間大小,則直接在堆記憶體中對該物件進行記憶體分配。
2、如果一個物件需要的空間大小超過TLAB中剩餘的空間大小,則廢棄當前TLAB,重新申請TLAB空間再次進行記憶體分配。
以上兩個方案各有利弊,如果採用方案1,那麼就可能存在著一種極端情況,就是TLAB只剩下1KB,就會導致後續需要分配的大多數物件都需要在堆記憶體直接分配。
如果採用方案2,也有可能存在頻繁廢棄TLAB,頻繁申請TLAB的情況,而我們知道,雖然在TLAB上分配記憶體是執行緒獨享的,但是TLAB記憶體自己從堆中劃分出來的過程確實可能存在衝突的,所以,TLAB的分配過程其實也是需要併發控制的。而頻繁的TLAB分配就失去了使用TLAB的意義。
**為了解決這兩個方案存在的問題,虛擬機器定義了一個refill_waste的值,這個值可以翻譯為“最大浪費空間”。**
當請求分配的記憶體大於refill_waste的時候,會選擇在堆記憶體中分配。若小於refill_waste值,則會廢棄當前TLAB,重新建立TLAB進行物件記憶體分配。
前面的例子中,TLAB總空間100KB,使用了80KB,剩餘20KB,如果設定的refill_waste的值為25KB,那麼如果新物件的記憶體大於25KB,則直接堆記憶體分配,如果小於25KB,則會廢棄掉之前的那個TLAB,重新分配一個TLAB空間,給新物件分配記憶體。
### TLAB使用的相關引數
TLAB功能是可以選擇開啟或者關閉的,可以通過設定-XX:+/-UseTLAB引數來指定是否開啟TLAB分配。
TLAB預設是eden區的1%,可以通過選項-XX:TLABWasteTargetPercent設定TLAB空間所佔用Eden空間的百分比大小。
預設情況下,TLAB的空間會在執行時不斷調整,使系統達到最佳的執行狀態。如果需要禁用自動調整TLAB的大小,可以使用-XX:-ResizeTLAB來禁用,並且使用-XX:TLABSize來手工指定TLAB的大小。
TLAB的refill_waste也是可以調整的,預設值為64,即表示使用約為1/64空間大小作為refill_waste,使用引數:-XX:TLABRefillWasteFraction來調整。
如果想要觀察TLAB的使用情況,可以使用引數-XX+PringTLAB 進行跟蹤。
### 總結
**為了保證物件的記憶體分配過程中的執行緒安全性,HotSpot虛擬機器提供了一種叫做TLAB(Thread Local Allocation Buffer)的技術。**
線上程初始化時,虛擬機器會為每個執行緒分配一塊TLAB空間,只給當前執行緒使用,當需要分配記憶體時,就在自己的空間上分配,這樣就不存在競爭的情況,可以大大提升分配效率。
**所以,“堆是執行緒共享的記憶體區域”這句話並不完全正確,因為TLAB是堆記憶體的一部分,他在讀取上確實是執行緒共享的,但是在記憶體分分配上,是執行緒獨享的。**
TLAB的空間其實並不大,所以大物件還是可能需要在堆記憶體中直接分配。那麼,物件的記憶體分配步驟就是先嚐試TLAB分配,空間不足之後,再判斷是否應該直接進入老年代,然後再確定是再eden分配還是在老年代分配。
### 多說幾句
相信一部分看完這篇文章之後,可能會覺得作者有點過於“咬文嚼字”、“吹毛求疵”了。可能不乏有些性子急的人只看了開頭就直接翻到文末準備開懟了。
不管你認不認同作者說的:“堆是執行緒共享的記憶體區域這句話並不完全正確”。這其實都不重要,重要的是當提到堆記憶體、提到執行緒共享、提到物件記憶體分配的時候,你可以想到還有個TLAB是比較特殊的,就可以了。
**有些時候,最可怕的不是自己不知道,而是,不知道自己不知道。**
還有就是,TLAB只是HotSpot虛擬機器的一個優化方案,Java虛擬機器規範中也沒有關於TLAB的任何規定。所以,不代表所有的虛擬機器都有這個特性。
本文的概述都是基於HotSpot虛擬機器的,作者也不是故意“以偏概全”,而是因為HotSpot虛擬機器是目前最流行的虛擬機器了,大多數預設情況下,我們討論的時候也都是基於HotSpot的。
哎,每次寫一些技術文章,都會有很多人噴,噴的角度也都是千奇百怪,所以只好多說幾句找補找補了。Anyway,任何形式的討論還是歡迎的,因為即使是噴,也未必有對手!
[1]: http://www.hollischuang.com/archives/2509
[2]: http://www.hollischuang.com/archi