首頁 > Java > java教程 > 主體

JMM java記憶體模型圖文詳解

高洛峰
發布: 2017-03-19 11:47:57
原創
1551 人瀏覽過

  JMM對於一個想要深入了解java的程式猿來說是不可避免的一關,本文偏理論性,盡可能說的通俗易懂,如有不對的地方希望多多指正。

  那我們先說一下jvm的主記憶體分配

  JMM java内存模型图文详解

 

  1 java虛擬機堆疊(java virtual stack)

  虛擬機棧是線程私有的,每個執行緒都有自己的虛擬機棧,是java方法執行的記憶體模型,每個方法執行的時候都會在虛擬機器堆疊上建立一個堆疊幀,堆疊幀是一個資料結構,主要儲存的是方法中的局部變數(基本類型,物件的引用,return Address類型(指向一條字節碼指令的地址)),操作棧(指的就是方法編譯後的操作指令的棧),動態鏈接,方法出口。通常所說的java記憶體分為堆疊和堆,其中所說的棧就是指的虛擬機器棧。但java的記憶體分配並沒有這麼簡單。

  動態連結解釋如下:

  每個堆疊幀都包含一個執行運行時常數池中該棧幀所屬方法的引用,持有這個引用是為了支援方法呼叫過程中的動態連線(Dynamic Linking)。

Class 檔案中存放了大量的符號引用,字節碼中的方法呼叫指令就是以常數池中指向方法的符號引用作為參數。這些符號引用一部分會在類別載入階段或第一次使用時轉換為直接引用,這種轉換稱為靜態解析。另一部分將在每一次運行期間轉換為直接引用,這部分稱為動態連線

  方法出口的解釋如下:

  • 當執行遇到回傳指令,會將回傳值傳遞給上層的方法呼叫者,這種退出的方式稱為正常完成出口(Normal Method Invocation Completion),一般來說,呼叫者的PC計數器可以作為回傳位址。

  • 當執行遇到異常,且目前方法體內沒有處理,就會導致方法退出,此時是沒有回傳值的,稱為異常完成出口(Abrupt Method Invocation Completion ),回傳位址要透過異常處理器表來確定。

  虛擬機器堆疊會出現兩種異常,一種就是常見的OOM 另一種就是StackOverFlowError。 StackOverflowError一般是遞迴呼叫所導致的,堆疊深度在虛擬機器中也是有限制的,否則無限制的遞歸呼叫虛擬機會哭的。 OOM就不用說了,當所請求的記憶體大於目前虛擬機器棧所持有的就會出現OOM(虛擬機棧空間可以動態擴展,但分配給jvm的記憶體也是有限的,所以虛擬機棧也不是無限擴展的)。

  2 本地方法堆疊

  本地方法堆疊和虛擬機器堆疊基本上是類似的,只不過虛擬機器堆疊中執行的是class字節碼,而本地方法棧中執行的就是本地方法的服務,其實就是呼叫一些由c或c++根據不同的os平台所寫的同一個方法的不同的實作。

  3 方法區(method area)

#  方法區是執行緒共享的區域,用於儲存已被虛擬機器載入的類別資訊(類別的字節碼數據,這裡要注意如果你同時加載的類很多的話需要調大方法區的空間,否則會OOM,只是對於類較少的情況下可以那麼做。等機制進行處理,如spring的懶載入機制,盡量避免同時載入過多的類別),常數,靜態變數和即時編譯器(JIT)編譯後的程式碼等資料。方法區其實就是我們所說的永久代區域(只限於hotspot虛擬機的實現機制),之所以說是永久代,是此處的資料幾乎很少進行垃圾回收,原因是加載的類別並不是一時半刻就會消亡,很多方法會根據類別在堆中創建物件,而靜態變數一般是,gc的跟搜尋演算法的root節點,而常數根本不會變的數據,所以都很少進行清理。

      Java虛擬機規範對這個區域的限制也非常寬鬆,除了可以是物理不連續的空間外,也允許固定大小和擴展性,還可以不實現垃圾收集。相對而言,垃圾收集行為在這個區域是比較少出現的(所以常數和靜態變數的定義要多注意)。方法區的記憶體收集還是會出現,不過這個區域的記憶體收集主要是針對常量池的回收和對類型的卸載。

      一般來說方法區的記憶體回收較難令人滿意。當方法區無法滿足記憶體分配需求時將拋出OutOfMemoryError異常。

  4 運行時常數池

#  JDK1.6之前字串#常數池位於方法區之中。 
  JDK1.7字串常數池已經被移到堆疊之中。

 

  java是一種動態連結的語言,在常數池的作用非常重要,在常數池中除了包含程式碼中所定義的各種基本型別(如int、long等等)和物件型(如String陣列)的常數值還,也包含一些以文字形式出現的符號引用,例如:

  類別和介面的全限定名稱;

  欄位的名稱和描述符;

  方法和名稱和描述符。

  在C語言中,如果一個程式要呼叫其它函式庫中的函數,在連接時,函數在函式庫中的位置(即相對於函式庫檔案開頭的偏移量)會被寫在程式中,在執行時,直接去這個位址呼叫函數;

  在Java語言中這樣,一切都是動態的。編譯時,如果發現對其它類方法的調用或者對其它類字段的引用的話,記錄進class文件中的,只能是一個文本形式的符號引用,在連接過程中,虛擬機根據這個文本信息去查找對應的方法或欄位。

  所以,與Java語言中的所謂「常數」不同,class檔案中的「常數」內容很非富,這些常數集中在class中的一個區域存放,一個緊接著一個,這裡就稱為「常量池」。

  java中的常數池技術,是為了方便快速地建立某些物件而出現的,當需要一個物件時,就可以從池中取一個出來(如果池中沒有則建立一個),則在需要重複建立相等變數時節省了很多時間。常量池其實也就是記憶體空間,不同於使用new關鍵字所建立的物件所在的堆空間。

  整個常數池會被JVM的一個索引引用,如同數組裡面的元素集合按照索引訪問一樣,JVM針對這些常數池裡面存儲的信息也是按照索引方式進行,實際上常數池在Java程序的動態連結過程扮演了一個至關重要的角色(上面有講到),下文摘自《深入理解java虛擬機》。

  Class文件中除了有類的版本,字段,方法,接口等信息以外,還有一項信息是常量池用於存儲編譯器生成的各種字面量和符號引用,這部分信息將在類別載入後存放到方法區的運行時常數池中。 Java虛擬機器對類別的每一部分(包括常數池)都有嚴格的規定,每個位元組用於儲存哪種資料都必須有規範上的要求,這樣才能夠被虛擬機器認可,裝載和執行。一般來說,除了保存Class檔案中所述的符號引用外,還會將翻譯出來的直接引用也儲存在執行時間常數池中。

      執行時間常數池相對於Class檔案常數池的另一個重要特徵是具備動態性,Java虛擬機並不要求常數只能在編譯期產生,也就是並非預置入Class檔案常數池的內容才能進入方法區的運行時常數池中,運作期間也可將新的常數放入常量池中。

      常數池是方法區的一部分,所以受到記憶體的限制,當無法申請到足夠記憶體時會拋出OutOfMemoryError異常

  5 堆(heap)

#  堆就是記憶體中最大的一塊區域,唯一用來儲存物件實例的地方。這個地方也是gc演算法主要的戰場。不過隨著JIT(即時編譯)的發展和逃逸技術成熟,並不是所有的物件都在堆上創建。下文摘自《深入理解java虛擬機器》。

  在Java程式語言和環境中,即時編譯器(JIT compiler,just-in-time compiler)是一個把Java的字節碼(包含需要被解釋的指令的程式)轉換成可以直接發送給處理器的指令的程式。當你寫好一個Java程式後,原始語言的語句將由Java編譯器編譯成字節碼,而不是編譯成與某個特定的處理器硬體平台對應的指令碼(比如,Intel的Pentium微處理器或IBM的System/390處理器)。字節碼是可以發送給任何平台並且能在那個平台上運行的獨立於平台的程式碼。

  java的記憶體分配大致就是這個樣子,jvm中也配有很多的參數對上面的資料進行調節。這裡就不進行列舉,會在單獨的一篇gc相關的文章中進行詳細的說明。下面說一下在多核心處理器的時代,jvm是如何處理並發所帶來的問題的。

  並發控制

  多核心的cpu可以並發的執行多個線程,而每個執行緒都有一個自己的本地工作區(其實就是分配給每個核的系統快取和暫存器),儲存從上面主記憶體取得的資料作副本在工作區中運行,如果資料是多執行緒中共享的,而且執行緒之間是不能進行資料交換,這就涉及了共享變數資料不一致的問題。 java透過sychronized volatile Lock鎖定等機制控制共享變數的可見度。

JMM java内存模型图文详解

  synchronized和lock會有單獨的章節分別講解實作機制, 這兩個不用說在可見性和原子性上都得道了保障。而volatile僅保證了數據的可見性,僅當數據在read 和load的時候數據在其他線程中改變會在當前線程中有所感知,如果過了這兩個階段,那隻能不好意思了,數據不一致,(其實volatile所做的就是避免使用快取不將主存上的資料儲存到執行緒工作記憶體中,在read和load階段都是從主記憶體中取得資料這樣就能夠感知到其他執行緒對變數的修改)。 volatile只是在早期的jdk版本中,由於synchronized的性能不好而出現的一個保證可見性的一個解決方案。現在的jdk版本的synchronized和lock都得到了一定的優化,所以一般的情況下是不建議採用volatile變量的,除非你知道你現在用volatile到底在幹什麼,因為它並不能保證並發的正確性。

JMM java内存模型图文详解

read and load 從主記憶體複製變數到目前工作內存,use and assign  執行程式碼,改變共享變數值,store and write 用工作記憶體資料刷新主存相關內容,其中use and assign 可以多次出現。 volitale適合一些冪等操作。這個會在lock的nofairsync的實作中解說。

說到這裡就不得不說一下happen before原則了。它是Java記憶體模型中定義的兩項操作之間的偏序關係,如果操作A先行發生於操作B,其意思是說,在發生操作B之前,操作A產生的影響都能被操作B觀察到,「影響」包括修改了記憶體中共享變數的值、發送了訊息、呼叫了方法等,它與時間上的先後發生基本沒有太大關係。這個原則特別重要,它是判斷資料是否有競爭、執行緒是否安全的主要依據。

  以下是Java記憶體模型中的八條可保證happen—before的規則,它們無需任何同步器協助就已經​​存在,可以在編碼中直接使用。如果兩個操作之間的關係不在此列,並且無法從下列規則推導出來的話,它們就沒有順序性保障,虛擬機可以對它們進行隨機地重排序(jvm為了能夠充分的利用cpu,提高利用率,jvm會將前後無關的程式碼或者說是操作進行重排序,讓那些需要等待IO或其他資源的操作排在後面,而其他能夠瞬間完成的操作放在前面先執行,充分的利用cpu的資源) 。

    1、程式順序規則:在一個單獨的執行緒中,依照程式碼的執行流程順序,(時間上)先執行的操作happen—before(時間上)後執行的操作。

    2、管理鎖定規則:一個unlock操作happen—before後面(時間上的先後順序,下同)對同一個鎖的lock操作。

    3、volatile變數規則:對一個volatile變數的寫入操作happen—before後面對該變數的讀取操作。

    4、執行緒啟動規則:Thread物件的start()方法happen—before此執行緒的每一個動作。

    5、執行緒終止規則:執行緒的所有操作都happen—before對此執行緒的終止偵測,可以透過Thread.join()方法結束、Thread.isAlive()的回傳值等手段偵測到執行緒已經終止執行。

    6、執行緒中斷規則:對執行緒interrupt()方法的呼叫happen—before發生於中斷執行緒的程式碼偵測到中斷時事件的發生。

    7、物件結束規則:一個物件的初始化完成(建構子執行結束)happen—before它的finalize()方法的開始。

    8、傳遞性:如果操作A happen—before操作B,操作B happen—before操作C,那麼可以得出A happen—before操作C。

以上是JMM java記憶體模型圖文詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!