Java堆內存是線程共享的!面試官:你肯定嗎?

Java做爲一種面向對象的,跨平臺語言,其對象、內存等一直是比較難的知識點,因此,即便是一個Java的初學者,也必定或多或少的對JVM有一些瞭解。能夠說,關於JVM的相關知識,基本是每一個Java開發者必學的知識點,也是面試的時候必考的知識點。面試

在JVM的內存結構中,比較常見的兩個區域就是堆內存和棧內存(如無特指,本文提到的棧均指的是虛擬機棧),關於堆和棧的區別,不少開發者也是如數家珍,有不少書籍,或者網上的文章大概都是這樣介紹的:安全

一、堆是線程共享的內存區域,棧是線程獨享的內存區域。併發

二、堆中主要存放對象實例,棧中主要存放各類基本數據類型、對象的引用。優化

可是,做者能夠很負責任的告訴你們,以上兩個結論均不是徹底正確的。線程

本文首先帶你們瞭解一下爲何我會說「堆是線程共享的內存區域,棧是線程獨享的內存區域。」這句話並不徹底正確!?關於JVM內存結構的相關知識,你們能夠閱讀JVM內存結構 VS Java內存模型 VS Java對象模型萬萬沒想到,JVM內存結構的面試題能夠問的這麼難?等文章。翻譯

在開始進入正題以前,請容許我問一個和這個問題看似沒有任何關係的問題:Java對象的內存分配過程是如何保證線程安全的?3d

Java對象的內存分配過程是如何保證線程安全的?

咱們知道,Java是一門面向對象的語言,咱們在Java中使用的對象都須要被建立出來,在Java中,建立一個對象的方法有不少種,可是不管如何,對象在建立過程當中,都須要進行內存分配。cdn

對象的內存分配過程當中,主要是對象的引用指向這個內存區域,而後進行初始化操做。對象

可是,由於堆是全局共享的,所以在同一時間,可能有多個線程在堆上申請空間,那麼,在併發場景中,若是兩個線程前後把對象引用指向了同一個內存區域,怎麼辦。blog

爲了解決這個併發問題,對象的內存分配過程就必須進行同步控制。可是咱們都知道,不管是使用哪一種同步方案(實際上虛擬機使用的多是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中分配,遇到這種狀況時,有兩種處理方案:

一、若是一個對象須要的空間大小超過TLAB中剩餘的空間大小,則直接在堆內存中對該對象進行內存分配。

二、若是一個對象須要的空間大小超過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,任何形式的討論仍是歡迎的,由於即便是噴,也未必有對手!

相關文章
相關標籤/搜索