內存?java模型?jvm結構?一篇文章讓你所有看懂!

又是一年秋招季,哎呀媽呀我被虐的慘來~這不,前幾陣失蹤沒更新博客,實際上是我偷偷把時間用在複習課本了(霧html

堅持在社區分享博客也好久了,因爲過去的文章有不少疏漏之處,不少大佬都在評論指出個人過錯,我很開心也很失望,開心的是有你們幫我指出錯誤,失望的鄙人學識淺薄總無法作到完美。總之,歡迎評論區各類pr~java

好,回到正題。複習的時候,無心間看到java虛擬機的有關知識點,我產生了很是濃厚的興趣,今天我來結合計算機內存模型的相關知識,與Java內存模型Java對象模型JVM內存結構等相關的知識串聯起來,本篇文章共1.5W字,分享給你們,感謝閱讀。git

想要解鎖更多新姿式?請訪問個人我的博客https://blog.tengshe789.tech/(😘程序員

計算機內存

相信每一個人都有一臺電腦,也有diy電腦的經歷。如今一臺功能強大的diy電腦大概3k就能組裝起來,一個i5-8400 的cpu 869元,DDR4 內存 1200塊錢,b360主板300元 散熱器50元 機械硬盤200元 350w電源300元 機箱100元 ,沒錯,只要3k就能拿到一個性能強大的6C6T電腦。github

要說一臺PC中最重要的部件是什麼?你們看價格也會看明白,是cpu和內存,下面我來介紹一下cpu和內存之間的關係。算法

cpu與內存緩存的千絲萬縷

cpu相關術語

首先說明一下相關的cpu術語:數據庫

  • socket:cpu插在主板上那個槽與cpu稱做一個socket。
  • Die:核心(Die)又稱爲內核,是cpu的物理組成部分之一。cpu也會分爲多die cpu與單die cpu,譬如咱們如今強大的AMD TR-2990WX就是4die cpu,每一個die裏面有8個核心(core)
  • core:也就是物理核心了。core這個詞是英特爾起的,起初是爲了與競爭對手AMD區別開,後面用的多了也淡了。
  • thread:就是硬件線程數。一個程序執行可能須要多個線程一塊兒進行~而如今也就比較強大的超線程技術,過去的cpu每每一個cpu核心只支持一個線程,如今一些強大的cpu中,就譬如IBM 的POWER 9 ,支持8核心32個線程(平均一個核心4個線程),理論性能很是強大。

總結一下,以明星cpu AMD TR-2990WX做爲栗子,這個cpu使用一個socket,一個socket裏面有4個die,總共32個物理核心64個線程編程

cpu緩存

咱們都知道,cpu將要處理的數據會放到內存中保存,但是,爲何會這樣,將內存緩存硬盤行不行呢?數組

答案固然是不行的。cpu的處理速度很強大,內存的速度雖然很是快速可是根本跟不上cpu的步伐,因此,就出現的緩存。與來自DRAM家族的內存不一樣,緩存SRAM與內存最大的特色是,特別快,容量小,結構複雜,成本也高。緩存

形成內存和緩存性能差別,主要有如下緣由:

  1. DRAM儲存一位數據只須要一個電容加上一個晶體管,而SRAM須要6個晶體管。因爲DRAM保存數據實際上是在電容裏面的,電容須要充放電才能進行讀寫操做,這就致使其讀寫數據就有比較大的延遲問題。
  2. 存儲能夠看錯一個二維數組,每一個存儲單元都有其行地址列地址。SRAM的容量很小,其存儲單元比較短(行列短),能夠一次性傳輸到SRAM中;而DRAM,須要分別傳送行列地址。
  3. SRAM的頻率和cpu頻率比較接近;而DRAM的頻率和cpu差距比較大。

近代的緩存一般被集成到cpu當中,爲了適應性能與成本的須要,現實中的緩存每每使用金字塔型多級緩存架構。也就是當CPU要讀取一個數據時,首先從一級緩存中查找,若是沒有找到再從二級緩存中查找,若是仍是沒有就從三級緩存或內存中查找。

下面是英特爾最近以來用的初代skylake架構

skl架構

能夠看到,每一個個核心有專屬的L1,L2緩存,他們共享一個L3緩存。若是cpu若是要訪問內存中的數據,必需要通過L1,L2,L3,LLC(或者L4)四層緩存。

緩存一致性問題

最開始的cpu,其實只是一個核心一個線程的,當時根本不須要考慮緩存一致性問題,單線程,也就是cpu核心的緩存只被一個線程訪問。緩存獨佔,不會出現訪問衝突等問題。

後來超線程技術來到咱們視野,''單核CPU多線程'',也就是進程中的多個線程會同時訪問進程中的共享數據,CPU將某塊內存加載到緩存後,不一樣線程在訪問相同的物理地址的時候,都會映射到相同的緩存位置,這樣即便發生線程的切換,緩存仍然不會失效。但因爲任什麼時候刻只能有一個線程在執行,所以不會出現緩存訪問衝突。

時代不斷髮展,**「多核CPU多線程」**來了,即多個線程訪問進程中的某個共享內存,且這多個線程分別在不一樣的核心上執行,則每一個核心都會在各自的caehe中保留一份共享內存的緩衝。因爲多核是能夠並行的,可能會出現多個線程同時寫各自的緩存的狀況,而各自的cache之間的數據就有可能不一樣。

這就是咱們說的緩存一致性問題。

目前公認最好的解決方案是英特爾的MESI協議,下面咱們着重介紹。

MESI協議

首先說說I/O操做的單位問題,大部分人都知道,在內存中操做I/O不是以字節爲單位,而是以「塊」爲單位,這是爲何呢?

其實這是由於I/O操做的數據訪問有空間連續性特徵,即須要訪問內存空間不少數據,可是I/O操做比較慢,讀一個字節和讀N個字節的時間基本相同。

機智的intel就規定了,cpu緩存中最小的存儲單元是緩存行cache line,在x86的cpu中,一個cache line儲存64字節,每一級的緩存都會被劃分紅許多組cache line

緩存工做原理請看👉維基百科

接下來咱們看看MESI規範,這實際上是用四種緩存行狀態命名的,咱們定義了CPU中每一個緩存行使用4種狀態進行標記(使用額外的兩位(bit)表示),分別是:

  • M: 被修改(Modified)

    該緩存行只被緩存在該CPU的緩存中,而且是被修改過的(dirty),即與主存中的數據不一致,該緩存行中的內存須要在將來的某個時間點(容許其它CPU讀取請主存中相應內存以前)寫回(write back)主存。當被寫回主存以後,該緩存行的狀態會變成獨享(exclusive)狀態。

  • E: 獨享的(Exclusive)

    該緩存行只被緩存在該CPU的緩存中,它是未被修改過的(clean),與主存中數據一致。該狀態能夠在任什麼時候刻當有其它CPU讀取該內存時變成共享狀態(shared)。一樣地,當CPU修改該緩存行中內容時,該狀態能夠變成Modified狀態。

  • S: 共享的(Shared)

    該狀態意味着該緩存行可能被多個CPU緩存,而且各個緩存中的數據與主存數據一致(clean),當有一個CPU修改該緩存行中,其它CPU中該緩存行能夠被做廢(變成無效狀態(Invalid))。

  • I: 無效的(Invalid)

    該緩存是無效的(可能有其它CPU修改了該緩存行)。

mesi

然而,只是有這四種狀態也會帶來必定的問題。下面引用一下oracle的文檔

同時更新來自不一樣處理器的相同緩存代碼行中的單個元素會使整個緩存代碼行無效,即便這些更新在邏輯上是彼此獨立的。每次對緩存代碼行的單個元素進行更新時,都會將此代碼行標記爲無效。其餘訪問同一代碼行中不一樣元素的處理器將看到該代碼行已標記爲無效。即便所訪問的元素未被修改,也會強制它們從內存或其餘位置獲取該代碼行的較新副本。這是由於基於緩存代碼行保持緩存一致性,而不是針對單個元素的。所以,互連通訊和開銷方面都將有所增加。而且,正在進行緩存代碼行更新的時候,禁止訪問該代碼行中的元素。

MESI協議,能夠保證緩存的一致性,可是沒法保證明時性。這種狀況稱爲僞共享

僞共享問題

僞共享問題其實在Java中是真實存在的一個問題。假設有以下所示的java class

class MyObiect{
    long a;
    long b;
    long c;
}
複製代碼

按照java規範,MyObiect對象是在堆空間中分配的,a、b、c這三個變量在內存空間中是近鄰,分別佔8字節,長度之和爲24字節。而咱們的x86的緩存行是64字節,這三個變量徹底有可能會在一個緩存行中,而且被兩個不一樣的cpu核心共享!

根據MESI協議,若是不一樣物理核心cpu中的線程1和線程2要互斥的對這幾個變量進行操做,頗有可能要互相搶佔資源,致使原來的並行變成串行,大大下降了系統的併發性,這就是緩存的僞共享。

解決僞共享

其實解決僞共享很簡單,只須要將這幾個變量分別放到不一樣的緩存行便可。在java8中,就已經提供了普適性的解決方案,即採用@Contended註解來保證對象中的變量或者屬性不在一個緩存行中~

@Contended
class VolatileObiect{
    volatile long a = 1L;
    volatile long b = 2L;
    volatile long c = 3L;
}
複製代碼

內存不一致性問題

上面我說了MESI協議在多核心cpu中解決緩存一致性的問題,下面咱們說說cpu的內存不一致性問題。

三種cpu架構

首先,要了解三個名詞:

  • SMP(Symmetric Multi-Processor)

SMP ,對稱多處理系統內有許多緊耦合多處理器,在這樣的系統中,全部的CPU共享所有資源,如總線,內存和I/O系統等,操做系統或管理數據庫的複本只有一個,這種系統有一個最大的特色就是共享全部資源。多個CPU之間沒有區別,平等地訪問內存、外設、一個操做系統。操做系統管理着一個隊列,每一個處理器依次處理隊列中的進程。若是兩個處理器同時請求訪問一個資源(例如同一段內存地址),由硬件、軟件的鎖機制去解決資源爭用問題。

[

clip_image001

所謂對稱多處理器結構,是指服務器中多個 CPU 對稱工做,無主次或從屬關係。各 CPU 共享相同的物理內存,每一個 CPU 訪問內存中的任何地址所需時間是相同的,所以 SMP 也被稱爲一致存儲器訪問結構 (UMA : Uniform Memory Access) 。對 SMP 服務器進行擴展的方式包括增長內存、使用更快的 CPU 、增長 CPU 、擴充 I/O( 槽口數與總線數 ) 以及添加更多的外部設備 ( 一般是磁盤存儲 ) 。

SMP 服務器的主要特徵是共享,系統中全部資源 (CPU 、內存、 I/O 等 ) 都是共享的。也正是因爲這種特徵,致使了 SMP 服務器的主要問題,那就是它的擴展能力很是有限。對於 SMP 服務器而言,每個共享的環節均可能形成 SMP 服務器擴展時的瓶頸,而最受限制的則是內存。因爲每一個 CPU 必須經過相同的內存總線訪問相同的內存資源,所以隨着 CPU 數量的增長,內存訪問衝突將迅速增長,最終會形成 CPU 資源的浪費,使 CPU 性能的有效性大大下降。實驗證實, SMP 服務器 CPU 利用率最好的狀況是 2 至 4 個 CPU 。

[

clip_image002

  • NUMA(Non-Uniform Memory Access)

  因爲 SMP 在擴展能力上的限制,人們開始探究如何進行有效地擴展從而構建大型系統的技術, NUMA 就是這種努力下的結果之一。利用 NUMA 技術,能夠把幾十個 CPU( 甚至上百個 CPU) 組合在一個服務器內。其NUMA 服務器 CPU 模塊結構如圖所示:

clip_image003

NUMA 服務器的基本特徵是具備多個 CPU 模塊,每一個 CPU 模塊由多個 CPU( 如 4 個 ) 組成,而且具備獨立的本地內存、 I/O 槽口等。因爲其節點之間能夠經過互聯模塊 ( 如稱爲 Crossbar Switch) 進行鏈接和信息交互,所以每一個 CPU 能夠訪問整個系統的內存 ( 這是 NUMA 系統與 MPP 系統的重要差異 ) 。顯然,訪問本地內存的速度將遠遠高於訪問遠地內存 ( 系統內其它節點的內存 ) 的速度,這也是非一致存儲訪問 NUMA 的由來。因爲這個特色,爲了更好地發揮系統性能,開發應用程序時須要儘可能減小不一樣 CPU 模塊之間的信息交互。

利用 NUMA 技術,能夠較好地解決原來 SMP 系統的擴展問題,在一個物理服務器內能夠支持上百個 CPU 。比較典型的 NUMA 服務器的例子包括 HP 的 Superdome 、 SUN15K 、 IBMp690 等。

  但 NUMA 技術一樣有必定缺陷,因爲訪問遠地內存的延時遠遠超過本地內存,所以當 CPU 數量增長時,系統性能沒法線性增長。如 HP 公司發佈 Superdome 服務器時,曾公佈了它與 HP 其它 UNIX 服務器的相對性能值,結果發現, 64 路 CPU 的 Superdome (NUMA 結構 ) 的相對性能值是 20 ,而 8 路 N4000( 共享的 SMP 結構 ) 的相對性能值是 6.3 。從這個結果能夠看到, 8 倍數量的 CPU 換來的只是 3 倍性能的提高。

  • MPP(Massive Parallel Processing)

  和 NUMA 不一樣, MPP 提供了另一種進行系統擴展的方式,它由多個 SMP 服務器經過必定的節點互聯網絡進行鏈接,協同工做,完成相同的任務,從用戶的角度來看是一個服務器系統。其基本特徵是由多個 SMP 服務器 ( 每一個 SMP 服務器稱節點 ) 經過節點互聯網絡鏈接而成,每一個節點只訪問本身的本地資源 ( 內存、存儲等 ) ,是一種徹底無共享 (Share Nothing) 結構,於是擴展能力最好,理論上其擴展無限制,目前的技術可實現 512 個節點互聯,數千個 CPU 。目前業界對節點互聯網絡暫無標準,如 NCR 的 Bynet , IBM 的 SPSwitch ,它們都採用了不一樣的內部實現機制。但節點互聯網僅供 MPP 服務器內部使用,對用戶而言是透明的。

  在 MPP 系統中,每一個 SMP 節點也能夠運行本身的操做系統、數據庫等。但和 NUMA 不一樣的是,它不存在異地內存訪問的問題。換言之,每一個節點內的 CPU 不能訪問另外一個節點的內存。節點之間的信息交互是經過節點互聯網絡實現的,這個過程通常稱爲數據重分配 (Data Redistribution) 。

可是 MPP 服務器須要一種複雜的機制來調度和平衡各個節點的負載和並行處理過程。目前一些基於 MPP 技術的服務器每每經過系統級軟件 ( 如數據庫 ) 來屏蔽這種複雜性。舉例來講, NCR 的 Teradata 就是基於 MPP 技術的一個關係數據庫軟件,基於此數據庫來開發應用時,無論後臺服務器由多少個節點組成,開發人員所面對的都是同一個數據庫系統,而不須要考慮如何調度其中某幾個節點的負載。

MPP (Massively Parallel Processing),大規模並行處理系統,這樣的系統是由許多鬆耦合的處理單元組成的,要注意的是這裏指的是處理單元而不是處理器。每一個單元內的CPU都有本身私有的資源,如總線,內存,硬盤等。在每一個單元內都有操做系統和管理數據庫的實例複本。這種結構最大的特色在於不共享資源。

NUMA結構下的緩存一致性

要知道,MESI協議解決的是傳統SMP結構下緩存的一致性,爲了在NUMA架構也實現緩存一致性,intel引入了MESI的一個拓展協議--MESIF,可是目前並無什麼資料,也無法研究,更多消息請查閱intel的wiki。

Java內存模型

原由

咱們寫程序,爲何要考慮內存模型呢,咱們前面說了,緩存一致性問題、內存一致問題是硬件的不斷升級致使的。解決問題,最簡單直接的作法就是廢除CPU緩存,讓CPU直接和主存交互。可是,這麼作雖然能夠保證多線程下的併發問題。可是,這就有點時代倒退了。

因此,爲了保證併發編程中能夠知足原子性、可見性及有序性。有一個重要的概念,那就是——內存模型。

即爲了保證共享內存的正確性(可見性、有序性、原子性),須要內存模型來定義了共享內存系統中多線程程序讀寫操做行爲的相應規範~

JMM

Java內存模型是根據英文Java Memory Model(JMM)翻譯過來的。其實JMM並不像JVM內存結構同樣是真實存在的。它是一種符合內存模型規範的,屏蔽了各類硬件和操做系統的訪問差別的,保證了Java程序在各類平臺下對內存的訪問都能保證效果一致的機制及規範。就像JSR-133: Java Memory Model and Thread Specification 中描述了,JMM是和多線程相關的,他描述了一組規則或規範,這個規範定義了一個線程對共享變量的寫入時對另外一個線程是可見的。

那麼,簡單總結下,Java的多線程之間是經過共享內存進行通訊的,而因爲採用共享內存進行通訊,在通訊過程當中會存在一系列如可見性、原子性、順序性等問題,而JMM就是圍繞着多線程通訊以及與其相關的一系列特性而創建的模型。JMM定義了一些語法集,這些語法集映射到Java語言中就是volatilesynchronized等關鍵字。

在JMM中,咱們把多個線程間通訊的共享內存稱之爲主內存,而在併發編程中多個線程都維護了一個本身的本地內存(這是個抽象概念),其中保存的數據是主內存中的數據拷貝。而JMM主要是控制本地內存和主內存之間的數據交互的

JMM

在Java中,JMM是一個很是重要的概念,正是因爲有了JMM,Java的併發編程才能避免不少問題。

JMM應用

瞭解Java多線程的朋友都知道,在Java中提供了一系列和併發處理相關的關鍵字,好比volatilesynchronizedfinalconcurrent包等。其實這些就是Java內存模型封裝了底層的實現後提供給咱們使用的一些關鍵字。

在開發多線程的代碼的時候,咱們能夠直接使用synchronized等關鍵字來控制併發,歷來就不須要關心底層的編譯器優化、緩存一致性等問題。因此,Java內存模型,除了定義了一套規範,還提供了一系列原語,封裝了底層實現後,供開發者直接使用。

併發編程要解決原子性、有序性和可見性的問題,咱們就再來看下,在Java中,分別使用什麼方式來保證。

原子性

原子性是指在一個操做中就是cpu不能夠在中途暫停而後再調度,既不被中斷操做,要不執行完成,要不就不執行。

JMM提供保證了訪問基本數據類型的原子性(其實在寫一個工做內存變量到主內存是分主要兩步:store、write),可是實際業務處理場景每每是須要更大的範圍的原子性保證。

在Java中,爲了保證原子性,提供了兩個高級的字節碼指令monitorentermonitorexit,而這兩個字節碼,在Java中對應的關鍵字就是synchronized

所以,在Java中可使用synchronized來保證方法和代碼塊內的操做是原子性的。這裏推薦一篇文章深刻理解Java併發之synchronized實現原理

可見性

可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看獲得修改的值。

Java內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值的這種依賴主內存做爲傳遞媒介的方式來實現的。

Java中的volatile關鍵字提供了一個功能,那就是被其修飾的變量在被修改後能夠當即同步到主內存,被其修飾的變量在每次是用以前都從主內存刷新。所以,可使用volatile來保證多線程操做時變量的可見性。

除了volatile,Java中的synchronizedfinalstatic三個關鍵字也能夠實現可見性。下面分享一下個人讀書筆記:

1538374970743

有序性

有序性即程序執行的順序按照代碼的前後順序執行。

在Java中,可使用synchronizedvolatile來保證多線程之間操做的有序性。實現方式有所區別:

volatile關鍵字會禁止指令重排。synchronized關鍵字保證同一時刻只容許一條線程操做。

好了,這裏簡單的介紹完了Java併發編程中解決原子性、可見性以及有序性可使用的關鍵字。讀者可能發現了,好像synchronized關鍵字是萬能的,他能夠同時知足以上三種特性,這其實也是不少人濫用synchronized的緣由。

可是synchronized是比較影響性能的,雖然編譯器提供了不少鎖優化技術,可是也不建議過分使用。

JVM

咱們都知道,Java代碼是要運行在虛擬機上的,而虛擬機在執行Java程序的過程當中會把所管理的內存劃分爲若干個不一樣的數據區域,這些區域都有各自的用途。下面咱們來講說JVM運行時內存區域結構

JVM運行時內存區域結構

在《Java虛擬機規範(Java SE 8)》中描述了JVM運行時內存區域結構以下:

JVM

1.程序計數器

  程序計數器(Program Counter Register),也有稱做爲PC寄存器。想必學過彙編語言的朋友對程序計數器這個概念並不陌生,在彙編語言中,程序計數器是指CPU中的寄存器,它保存的是程序當前執行的指令的地址(也能夠說保存下一條指令的所在存儲單元的地址),當CPU須要執行指令時,須要從程序計數器中獲得當前須要執行的指令所在存儲單元的地址,而後根據獲得的地址獲取到指令,在獲得指令以後,程序計數器便自動加1或者根據轉移指針獲得下一條指令的地址,如此循環,直至執行完全部的指令。

  雖然JVM中的程序計數器並不像彙編語言中的程序計數器同樣是物理概念上的CPU寄存器,可是JVM中的程序計數器的功能跟彙編語言中的程序計數器的功能在邏輯上是等同的,也就是說是用來指示 執行哪條指令的。

  因爲在JVM中,多線程是經過線程輪流切換來得到CPU執行時間的,所以,在任一具體時刻,一個CPU的內核只會執行一條線程中的指令,所以,爲了可以使得每一個線程都在線程切換後可以恢復在切換以前的程序執行位置,每一個線程都須要有本身獨立的程序計數器,而且不能互相被幹擾,不然就會影響到程序的正常執行次序。所以,能夠這麼說,程序計數器是每一個線程所私有的。

  在JVM規範中規定,若是線程執行的是非native方法,則程序計數器中保存的是當前須要執行的指令的地址;若是線程執行的是native方法,則程序計數器中的值是undefined。

  因爲程序計數器中存儲的數據所佔空間的大小不會隨程序的執行而發生改變,所以,對於程序計數器是不會發生內存溢出現象(OutOfMemory)的。

2.Java棧

  Java棧也稱做虛擬機棧(Java Vitual Machine Stack),也就是咱們經常所說的棧,跟C語言的數據段中的棧相似。事實上,Java棧是Java方法執行的內存模型。爲何這麼說呢?下面就來解釋一下其中的緣由。

  Java棧中存放的是一個個的棧幀,每一個棧幀對應一個被調用的方法,在棧幀中包括局部變量表(Local Variables)、操做數棧(Operand Stack)、指向當前方法所屬的類的運行時常量池(運行時常量池的概念在方法區部分會談到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些額外的附加信息。當線程執行一個方法時,就會隨之建立一個對應的棧幀,並將創建的棧幀壓棧。當方法執行完畢以後,便會將棧幀出棧。所以可知,線程當前執行的方法所對應的棧幀一定位於Java棧的頂部。講到這裏,你們就應該會明白爲何 在 使用 遞歸方法的時候容易致使棧內存溢出的現象了以及爲何棧區的空間不用程序員去管理了(固然在Java中,程序員基本不用關係到內存分配和釋放的事情,由於Java有本身的垃圾回收機制),這部分空間的分配和釋放都是由系統自動實施的。對於全部的程序設計語言來講,棧這部分空間對程序員來講是不透明的。下圖表示了一個Java棧的模型:

img

  局部變量表,顧名思義,想必不用解釋你們應該明白它的做用了吧。就是用來存儲方法中的局部變量(包括在方法中聲明的非靜態變量以及函數形參)。對於基本數據類型的變量,則直接存儲它的值,對於引用類型的變量,則存的是指向對象的引用。局部變量表的大小在編譯器就能夠肯定其大小了,所以在程序執行期間局部變量表的大小是不會改變的。

  操做數棧,想必學過數據結構中的棧的朋友想必對錶達式求值問題不會陌生,棧最典型的一個應用就是用來對錶達式求值。想一想一個線程執行方法的過程當中,實際上就是不斷執行語句的過程,而歸根到底就是進行計算的過程。所以能夠這麼說,程序中的全部計算過程都是在藉助於操做數棧來完成的。

  指向運行時常量池的引用,由於在方法執行的過程當中有可能須要用到類中的常量,因此必需要有一個引用指向運行時常量。

  方法返回地址,當一個方法執行完畢以後,要返回以前調用它的地方,所以在棧幀中必須保存一個方法返回地址。

  因爲每一個線程正在執行的方法可能不一樣,所以每一個線程都會有一個本身的Java棧,互不干擾。

3.本地方法棧

  本地方法棧與Java棧的做用和原理很是類似。區別只不過是Java棧是爲執行Java方法服務的,而本地方法棧則是爲執行本地方法(Native Method)服務的。在JVM規範中,並無對本地方發展的具體實現方法以及數據結構做強制規定,虛擬機能夠自由實現它。在HotSopt虛擬機中直接就把本地方法棧和Java棧合二爲一。

4.

  在C語言中,堆這部分空間是惟一一個程序員能夠管理的內存區域。程序員能夠經過malloc函數和free函數在堆上申請和釋放空間。那麼在Java中是怎麼樣的呢?

  Java中的堆是用來存儲對象自己的以及數組(固然,數組引用是存放在Java棧中的)。只不過和C語言中的不一樣,在Java中,程序員基本不用去關心空間釋放的問題,Java的垃圾回收機制會自動進行處理。所以這部分空間也是Java垃圾收集器管理的主要區域。另外,堆是被全部線程共享的,在JVM中只有一個堆。

5.方法區

  方法區在JVM中也是一個很是重要的區域,它與堆同樣,是被線程共享的區域。在方法區中,存儲了每一個類的信息(包括類的名稱、方法信息、字段信息)、靜態變量、常量以及編譯器編譯後的代碼等。

  在Class文件中除了類的字段、方法、接口等描述信息外,還有一項信息是常量池,用來存儲編譯期間生成的字面量和符號引用。

  在方法區中有一個很是重要的部分就是運行時常量池,它是每個類或接口的常量池的運行時表示形式,在類和接口被加載到JVM後,對應的運行時常量池就被建立出來。固然並不是Class文件常量池中的內容才能進入運行時常量池,在運行期間也可將新的常量放入運行時常量池中,好比String的intern方法。

  在JVM規範中,沒有強制要求方法區必須實現垃圾回收。不少人習慣將方法區稱爲「永久代」,是由於HotSpot虛擬機以永久代來實現方法區,從而JVM的垃圾收集器能夠像管理堆區同樣管理這部分區域,從而不須要專門爲這部分設計垃圾回收機制。不過自從JDK7以後,Hotspot虛擬機便將運行時常量池從永久代移除了。

Java對象模型的內存佈局

java是一種面向對象的語言,而Java對象在JVM中的存儲也是有必定的結構的。而這個關於Java對象自身的存儲模型稱之爲Java對象模型。

HotSpot虛擬機中,設計了一個OOP-Klass Model。OOP(Ordinary Object Pointer)指的是普通對象指針,而Klass用來描述對象實例的具體類型。

每個Java類,在被JVM加載的時候,JVM會給這個類建立一個instanceKlass,保存在方法區,用來在JVM層表示該Java類。當咱們在Java代碼中,使用new建立一個對象的時候,JVM會建立一個instanceOopDesc對象,對象在內存中存儲的佈局能夠分爲3塊區域:對象頭(Header)、 實例數據(Instance Data)和對齊填充(Padding)。

  1. 對象頭:標記字(32位虛擬機4B,64位虛擬機8B) + 類型指針(32位虛擬機4B,64位虛擬機8B)+ [數組長(對於數組對象才須要此部分信息)]
  2. 實例數據:存儲的是真正有效數據,如各類字段內容,各字段的分配策略爲longs/doubles、ints、shorts/chars、bytes/boolean、oops(ordinary object pointers),相同寬度的字段老是被分配到一塊兒,便於以後取數據。父類定義的變量會出如今子類定義的變量的前面。
  3. 對齊填充:對於64位虛擬機來講,對象大小必須是8B的整數倍,不夠的話須要佔位填充

JVM內存垃圾收集器

爲了理解現有收集器,咱們須要先了解一些術語。最基本的垃圾收集涉及識別再也不使用的內存並使其可重用。現代收集器在幾個階段進行這一過程,對於這些階段咱們每每有以下描述:

  • 並行- 在JVM運行時,同時存在應用程序線程和垃圾收集器線程。 並行階段是由多個gc線程執行,即gc工做在它們之間分配。 不涉及GC線程是否須要暫停應用程序線程。
  • 串行- 串行階段僅在單個gc線程上執行。與以前同樣,它也沒有說明GC線程是否須要暫停應用程序線程。
  • STW - STW階段,應用程序線程被暫停,以便gc執行其工做。 當應用程序由於GC暫停時,這一般是因爲Stop The World階段。
  • 併發 -若是一個階段是併發的,那麼GC線程能夠和應用程序線程同時進行。 併發階段很複雜,由於它們須要在階段完成以前處理可能使工做無效(譯者注:由於是併發進行的,GC線程在完成一階段的同時,應用線程也在工做產生操做內存,因此須要額外處理)的應用程序線程。
  • 增量 -若是一個階段是增量的,那麼它能夠運行一段時間以後因爲某些條件提早終止,例如須要執行更高優先級的gc階段,同時仍然完成生產性工做。 增量階段與須要徹底完成的階段造成鮮明對比。

Serial收集器

Serial收集器是最基本的收集器,這是一個單線程收集器,它仍然是JVM在Client模式下的默認新生代收集器。它有着優於其餘收集器的地方:簡單而高效(與其餘收集器的單線程比較),Serial收集器因爲沒有線程交互的開銷,專心只作垃圾收集天然也得到最高的效率。在用戶桌面場景下,分配給JVM的內存不會太多,停頓時間徹底能夠在幾十到一百多毫秒之間,只要收集不頻繁,這是徹底能夠接受的。

ParNew收集器

ParNew是Serial的多線程版本,在回收算法、對象分配原則上都是一致的。ParNew收集器是許多運行在Server模式下的默認新生代垃圾收集器,其主要在於除了Serial收集器,目前只有ParNew收集器可以與CMS收集器配合工做。

Parallel Scavenge收集器

Parallel Scavenge收集器是一個新生代垃圾收集器,其使用的算法是複製算法,也是並行的多線程收集器。

Parallel Scavenge 收集器更關注可控制的吞吐量,吞吐量等於運行用戶代碼的時間/(運行用戶代碼的時間+垃圾收集時間)。直觀上,只要最大的垃圾收集停頓時間越小,吞吐量是越高的,可是GC停頓時間的縮短是以犧牲吞吐量和新生代空間做爲代價的。好比原來10秒收集一次,每次停頓100毫秒,如今變成5秒收集一次,每次停頓70毫秒。停頓時間降低的同時,吞吐量也降低了。

停頓時間越短就越適合須要與用戶交互的程序;而高吞吐量則能夠最高效的利用CPU的時間,儘快的完成計算任務,主要適用於後臺運算。

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,也是一個單線程收集器,採用「標記-整理算法」進行回收。其運行過程與Serial收集器同樣。

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和標記-整理算法進行垃圾回收。其一般與Parallel Scavenge收集器配合使用,「吞吐量優先」收集器是這個組合的特色,在注重吞吐量和CPU資源敏感的場合,均可以使用這個組合。

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短停頓時間爲目標的收集器,CMS收集器採用標記--清除算法,運行在老年代。主要包含如下幾個步驟:

  • 初始標記
  • 併發標記
  • 從新標記
  • 併發清除

其中初始標記和從新標記仍然須要「Stop the world」。初始標記僅僅標記GC Root能直接關聯的對象,併發標記就是進行GC Root Tracing過程,而從新標記則是爲了修正併發標記期間,因用戶程序繼續運行而致使標記變更的那部分對象的標記記錄。

因爲整個過程當中最耗時的併發標記和併發清除,收集線程和用戶線程一塊兒工做,因此整體上來講,CMS收集器回收過程是與用戶線程併發執行的。雖然CMS優勢是併發收集、低停頓,很大程度上已是一個不錯的垃圾收集器,可是仍是有三個顯著的缺點:

  1. CMS收集器對CPU資源很敏感。在併發階段,雖然它不會致使用戶線程停頓,可是會由於佔用一部分線程(CPU資源)而致使應用程序變慢。
  2. CMS收集器不能處理浮動垃圾。所謂的「浮動垃圾」,就是在併發標記階段,因爲用戶程序在運行,那麼天然就會有新的垃圾產生,這部分垃圾被標記事後,CMS沒法在當次集中處理它們,只好在下一次GC的時候處理,這部分未處理的垃圾就稱爲「浮動垃圾」。也是因爲在垃圾收集階段程序還須要運行,即還須要預留足夠的內存空間供用戶使用,所以CMS收集器不能像其餘收集器那樣等到老年代幾乎填滿才進行收集,須要預留一部分空間提供併發收集時程序運做使用。要是CMS預留的內存空間不能知足程序的要求,這是JVM就會啓動預備方案:臨時啓動Serial Old收集器來收集老年代,這樣停頓的時間就會很長。
  3. 因爲CMS使用標記--清除算法,因此在收集以後會產生大量內存碎片。當內存碎片過多時,將會給分配大對象帶來困難,這是就會進行Full GC。

G1收集器

G1收集器與CMS相比有很大的改進:

· G1收集器採用標記--整理算法實現。

· 能夠很是精確地控制停頓。

​ G1收集器能夠實如今基本不犧牲吞吐量的狀況下完成低停頓的內存回收,這是因爲它極力的避免全區域的回收,G1收集器將Java堆(包括新生代和老年代)劃分爲多個區域(Region),並在後臺維護一個優先列表,每次根據容許的時間,優先回收垃圾最多的區域 。

ZGC收集器

Java 11 新加入的ZGC垃圾收集器號稱能夠達到10ms 如下的 GC 停頓,ZGC給Hotspot Garbage Collectors增長了兩種新技術:着色指針和讀屏障。下面引用國外文章說的內容:

着色指針

着色指針是一種將信息存儲在指針(或使用Java術語引用)中的技術。由於在64位平臺上(ZGC僅支持64位平臺),指針能夠處理更多的內存,所以可使用一些位來存儲狀態。 ZGC將限制最大支持4Tb堆(42-bits),那麼會剩下22位可用,它目前使用了4位: finalizableremapmark0mark1。 咱們稍後解釋它們的用途。

着色指針的一個問題是,當您須要取消着色時,它須要額外的工做(由於須要屏蔽信息位)。 像SPARC這樣的平臺有內置硬件支持指針屏蔽因此不是問題,而對於x86平臺來講,ZGC團隊使用了簡潔的多重映射技巧。

多重映射

要了解多重映射的工做原理,咱們須要簡要解釋虛擬內存和物理內存之間的區別。 物理內存是系統可用的實際內存,一般是安裝的DRAM芯片的容量。 虛擬內存是抽象的,這意味着應用程序對(一般是隔離的)物理內存有本身的視圖。 操做系統負責維護虛擬內存和物理內存範圍之間的映射,它經過使用頁表和處理器的內存管理單元(MMU)和轉換查找緩衝器(TLB)來實現這一點,後者轉換應用程序請求的地址。

多重映射涉及將不一樣範圍的虛擬內存映射到同一物理內存。 因爲設計中只有一個remapmark0mark1在任什麼時候間點均可覺得1,所以可使用三個映射來完成此操做。 ZGC源代碼中有一個很好的圖表能夠說明這一點。

讀屏障

讀屏障是每當應用程序線程從堆加載引用時運行的代碼片斷(即訪問對象上的非原生字段non-primitive field):

void printName( Person person ) {
    String name = person.name;  // 這裏觸發讀屏障
                                // 由於須要從heap讀取引用 
                                // 
    System.out.println(name);   // 這裏沒有直接觸發讀屏障
}
複製代碼

在上面的代碼中,String name = person.name 訪問了堆上的person引用,而後將引用加載到本地的name變量。此時觸發讀屏障。 Systemt.out那行不會直接觸發讀屏障,由於沒有來自堆的引用加載(name是局部變量,所以沒有從堆加載引用)。 可是System和out,或者println內部可能會觸發其餘讀屏障。

這與其餘GC使用的寫屏障造成對比,例如G1。讀屏障的工做是檢查引用的狀態,並在將引用(或者甚至是不一樣的引用)返回給應用程序以前執行一些工做。 在ZGC中,它經過測試加載的引用來執行此任務,以查看是否設置了某些位。 若是經過了測試,則不執行任何其餘工做,若是失敗,則在將引用返回給應用程序以前執行某些特定於階段的任務。

標記

如今咱們瞭解了這兩種新技術是什麼,讓咱們來看看ZG的GC循環。

GC循環的第一部分是標記。標記包括查找和標記運行中的應用程序能夠訪問的全部堆對象,換句話說,查找不是垃圾的對象。

ZGC的標記分爲三個階段。 第一階段是STW,其中GC roots被標記爲活對象。 GC roots相似於局部變量,經過它能夠訪問堆上其餘對象。 若是一個對象不能經過遍歷從roots開始的對象圖來訪問,那麼應用程序也就沒法訪問它,則該對象被認爲是垃圾。從roots訪問的對象集合稱爲Live集。GC roots標記步驟很是短,由於roots的總數一般比較小。

該階段完成後,應用程序恢復執行,ZGC開始下一階段,該階段同時遍歷對象圖並標記全部可訪問的對象。 在此階段期間,讀屏障針使用掩碼測試全部已加載的引用,該掩碼肯定它們是否已標記或還沒有標記,若是還沒有標記引用,則將其添加到隊列以進行標記。

在遍歷完成以後,有一個最終的,時間很短的的Stop The World階段,這個階段處理一些邊緣狀況(咱們如今將它忽略),該階段完成以後標記階段就完成了。

重定位

GC循環的下一個主要部分是重定位。重定位涉及移動活動對象以釋放部分堆內存。 爲何要移動對象而不是填補空隙? 有些GC實際是這樣作的,可是它致使了一個不幸的後果,即分配內存變得更加昂貴,由於當須要分配內存時,內存分配器須要找到能夠放置對象的空閒空間。 相比之下,若是能夠釋放大塊內存,那麼分配內存就很簡單,只須要將指針遞增新對象所需的內存大小便可。

ZGC將堆分紅許多頁面,在此階段開始時,它同時選擇一組須要重定位活動對象的頁面。選擇重定位集後,會出現一個Stop The World暫停,其中ZGC重定位該集合中root對象,並將他們的引用映射到新位置。與以前的Stop The World步驟同樣,此處涉及的暫停時間僅取決於root的數量以及重定位集的大小與對象的總活動集的比率,這一般至關小。因此不像不少收集器那樣,暫停時間隨堆增長而增長。

移動root後,下一階段是併發重定位。 在此階段,GC線程遍歷重定位集並從新定位其包含的頁中全部對象。 若是應用程序線程試圖在GC從新定位對象以前加載它們,那麼應用程序線程也能夠重定位該對象,這能夠經過讀屏障(在從堆加載引用時觸發)

這可確保應用程序看到的全部引用都已更新,而且應用程序不可能同時對重定位的對象進行操做。

GC線程最終將對重定位集中的全部對象重定位,然而可能仍有引用指向這些對象的舊位置。 GC能夠遍歷對象圖並從新映射這些引用到新位置,可是這一步代價很高昂。 所以這一步與下一個標記階段合併在一塊兒。在下一個GC週期的標記階段遍歷對象對象圖的時候,若是發現未重映射的引用,則將其從新映射,而後標記爲活動狀態。

JVM內存優化

在《深刻理解Java虛擬機》一書中講了不少jvm優化思路,下面我來簡單說說。

java內存抖動

堆內存都有必定的大小,能容納的數據是有限制的,當Java堆的大小太大時,垃圾收集會啓動中止堆中再也不應用的對象,來釋放內存。如今,內存抖動這個術語可用於描述在極短期內分配給對象的過程。 具體如何優化請谷歌查詢~

jvm大頁內存

什麼是內存分頁?

CPU是經過尋址來訪問內存的。32位CPU的尋址寬度是 0~0xFFFFFFFF,即4G,也就是說可支持的物理內存最大是4G。但在實踐過程當中,程序須要使用4G內存,而可用物理內存小於4G,致使程序不得不下降內存佔用。爲了解決此類問題,現代CPU引入了MMU(Memory Management Unit,內存管理單元)。

MMU 的核心思想是利用虛擬地址替代物理地址,即CPU尋址時使用虛址,由MMU負責將虛址映射爲物理地址。MMU的引入,解決了對物理內存的限制,對程序來講,就像本身在使用4G內存同樣。

內存分頁(Paging)是在使用MMU的基礎上,提出的一種內存管理機制。它將虛擬地址和物理地址按固定大小(4K)分割成頁(page)和頁幀(page frame),並保證頁與頁幀的大小相同。這種機制,從數據結構上,保證了訪問內存的高效,並使OS能支持非連續性的內存分配。在程序內存不夠用時,還能夠將不經常使用的物理內存頁轉移到其餘存儲設備上,好比磁盤,這就是虛擬內存。

要知道,虛擬地址與物理地址須要經過映射,才能使CPU正常工做。而映射就須要存儲映射表。在現代CPU架構中,映射關係一般被存儲在物理內存上一個被稱之爲頁表(page table)的地方。 頁表是被存儲在內存中的,CPU經過總線訪問內存,確定慢於直接訪問寄存器的。爲了進一步優化性能,現代CPU架構引入了TLB(Translation lookaside buffer,頁表寄存器緩衝),用來緩存一部分常常訪問的頁表內容 。

爲何要支持大內存分頁?

TLB是有限的,這點毫無疑問。當超出TLB的存儲極限時,就會發生 TLB miss,因而OS就會命令CPU去訪問內存上的頁表。若是頻繁的出現TLB miss,程序的性能會降低地很快。

爲了讓TLB能夠存儲更多的頁地址映射關係,咱們的作法是調大內存分頁大小。

若是一個頁4M,對比一個頁4K,前者可讓TLB多存儲1000個頁地址映射關係,性能的提高是比較可觀的。

開啓JVM大頁內存

JVM啓用時加參數 -XX:LargePageSizeInBytes=10m 若是JDK是在1.5 update5之前的,還須要加 -XX:+UseLargePages,做用是啓用大內存頁支持。

經過軟引用和弱引用提高JVM內存使用性能

強軟弱虛
  1. 強引用:

只要引用存在,垃圾回收器永遠不會回收

Object obj = new Object();

//可直接經過obj取得對應的對象 如obj.equels(new Object());

而這樣 obj對象對後面new Object的一個強引用,只有當obj這個引用被釋放以後,對象纔會被釋放掉,這也是咱們常常所用到的編碼形式。

  1. 軟引用(能夠實現緩存):

非必須引用,內存溢出以前進行回收,能夠經過如下代碼實現

Object obj = new Object();

SoftReference<Object> sf = new SoftReference<Object>(obj);

obj = null;

sf.get();//有時候會返回null
複製代碼

這時候sf是對obj的一個軟引用,經過sf.get()方法能夠取到這個對象,固然,當這個對象被標記爲須要回收的對象時,則返回null;軟引用主要用戶實現相似緩存的功能,在內存足夠的狀況下直接經過軟引用取值,無需從繁忙的真實來源查詢數據,提高速度;當內存不足時,自動刪除這部分緩存數據,從真正的來源查詢這些數據。

  1. 弱引用(用來在回調函數中防止內存泄露):

第二次垃圾回收時回收,能夠經過以下代碼實現

Object obj = new Object();

WeakReference<Object> wf = new WeakReference<Object>(obj);

obj = null;

wf.get();//有時候會返回null

wf.isEnQueued();//返回是否被垃圾回收器標記爲即將回收的垃圾
複製代碼

弱引用是在第二次垃圾回收時回收,短期內經過弱引用取對應的數據,能夠取到,當執行過第二次垃圾回收時,將返回null。弱引用主要用於監控對象是否已經被垃圾回收器標記爲即將回收的垃圾,能夠經過弱引用的isEnQueued方法返回對象是否被垃圾回收器標記。

  1. 虛引用:

垃圾回收時回收,沒法經過引用取到對象值,能夠經過以下代碼實現

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();//永遠返回null
pf.isEnQueued();//返回是否從內存中已經刪除
複製代碼

虛引用是每次垃圾回收的時候都會被回收,經過虛引用的get方法永遠獲取到的數據爲null,所以也被成爲幽靈引用。虛引用主要用於檢測對象是否已經從內存中刪除。

優化

簡單來講,可使用軟引用還引用數量巨大的對象,詳情請參考http://www.cnblogs.com/JavaArchitect/p/8685993.html

總結

此篇文章總共1.5W字,我從計算機物理內存體系講到了java內存模型,在經過java內存模型引出了JVM內存的相關知識點。以爲寫的好的請給個贊。本篇文章我會率先發布在個人我的博客,隨後會在掘金等平臺相繼發出。最後,很是感謝你的閱讀~

參考資料

文中的各類超連接

《深刻理解Java虛擬機》

《Java併發編程的藝術》

《架構解密從分佈式到微服務》

SMP、NUMA、MPP體系結構介紹

ZGC原理(請用正確的姿式魔法上網觀看)

Stefan Karlsson和PerLiden Jfokus的演講(請用正確的姿式魔法上網)

聲明

【版權申明】此片爲原創內容,使用MIT受權條款,請遵照對應的義務,即被受權人有義務在全部副本中都必須包含版權聲明。謝謝合做~

想要解鎖更多新姿式?請訪問個人我的博客https://blog.tengshe789.tech/(😘 github社區地址https://github.com/tengshe789/,歡迎互fo

相關文章
相關標籤/搜索