JVM線程模型詳解

1、 jvm內存模型

在描述jvm線程模型之前,我們先深入的理解下,jvm內存模型。在jvm1.8之前,jvm的邏輯結構和物理結構是對應的。即Jvm在初始化的時候,會爲堆(heap),棧(stack),元數據區(matespace)分配指定的內存大小,Jvm線程啓動的時候會向服務器申請指定的內存地址空間進行分配。在jdk1.8之後,使用了G1垃圾回收器,邏輯上依然存在堆,棧,元數據區。但是在物理結構上,G1採用了分區(Region)的思路,將整個堆空間分成若干個大小相等的內存區域,每次分配對象空間將逐段地使用內存。因此,在堆的使用上,G1並不要求對象的存儲一定是物理上連續的,只要邏輯上連續即可;每個分區也不會確定地爲某個代服務,可以按需在年輕代和老年代之間切換。啓動時可以通過參數-XX:G1HeapRegionSize=n可指定分區大小(1MB~32MB,且必須是2的冪),默認將整堆劃分爲2048個分區。


1.1、 jvm內存模型介紹

這裏寫圖片描述

1.2、 Jvm堆

Java堆是和Java應用程序最密切的內存空間,幾乎所有的對象都放到堆中。並且堆完全由Jvm管理,通過垃圾回收機制,垃圾對象會被自動清理,而不需顯式的釋放。
根據垃圾回收機制的不同,Java堆通常被分爲以下的集中不同的結構。
這裏寫圖片描述

構成 描述
New Generation 由 Eden + Survivor (From Space + To Space)組成
Eden 所以的new出來的新對象都存放到Eden區
Survivor Space Eden每次垃圾清理過後,任然沒又被清理的對象,會轉移到交換區中
Old Generation 在交換區中未被清理的對象(默認清理18次標記),將轉移到老年代。

1.3、 Jvm棧

Java棧是一塊線程私有的內存空間,Java棧和線程執行密切相關。線程的執行基本單位就是函數調用,每次函數調用的數據就會通過Java棧傳遞。
  Java棧與數據結構上的棧有着類似的含義,它是一塊先進後出的數據結構,只支持出棧和入棧的兩種操作。在Java棧中保存的主要內容爲棧幀。每次調用一個函數,都會有一個對應的棧幀被壓入Java棧。每一個函數調用結束,都會有一個棧幀被彈出Java棧。例如:
這裏寫圖片描述
  如圖所示,每次調用一個函數都會被當做棧幀壓入到棧中。其中每一個棧幀對應一個函數。由於每次調用函數都會生成一個棧幀,從而佔用一定的棧空間。如果線程中存在大量的遞歸操作,會頻繁的壓棧,導致棧的深入過於深入,當棧的空間被消耗殆盡的時候,會拋出StackOverflowError棧溢出錯誤。
當函數執行結束返回時,棧幀從Java棧中被彈出。Java方法有兩種返回的方式,一種是正常函數返回,即使用 return; 另外一種是拋出異常。不管哪種方式,都會導致棧幀被彈出。

1.3.1、 局部變量表

局部變量表示棧幀的重要組成部分之一。它用於保存函數已經局部變量。局部變量表中的變量只有在當前的函數中調用有效,當調用函數結束以後,隨着函數棧幀的銷燬,局部變量表也隨之銷燬。

1.3.2、 操作數棧

操作數棧也是棧幀中重要的內容之一,它主要保存計算過程中的結果,同事作爲計算過程臨時變量的存儲空間。
操作數棧也是一個先進後出的數據結構,只支持入棧和出棧的兩種操作,Java的很多字節碼指令都是通過操作數棧進行參數傳遞的。比如iadd指令,它就會在操作數棧中彈出兩個整數進行加法計算,計算結果會被入棧。入下圖所示:
這裏寫圖片描述

1.3.3、 幀數據區

每個棧幀都包含一個指向運行時常量池中該棧幀所屬性方法的引用,持有這個引用是爲了支持方法調用過程中的動態連接。在Class文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用爲參數。這些符號引用一部分會在類加載階段或第一次使用的時候轉化爲直接引用,這種轉化稱爲靜態解析。另外一部分將在每一次的運行期期間轉化爲直接引用,這部分稱爲動態連接


1.4、 Jvm方法區(jdk1.8元數據區)

它主要存放一些虛擬機加載的類信息,常量,靜態變量,即使編譯器後的代碼等數據。根據Java虛擬機規範的規定,當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。

1.4.1、 運行時常量池

運行時常量區是方法區的一部分。用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。還會有一些符號引用轉換的直接引用一保存在運行時常量池中。
運行時常量池具備動態性,也就是運行期間也可以將新的常量放入池中,例如String.intern()方法。當常量池無法再申請到內存時,會拋出OutOfMemoryError異常


2、 jvm線程模型

通過Jvm內存模型,我們可以發現,Jvm其實就是操作系統的一種鏡像。是軟件層次的虛擬機。其中我們隊Jvm內存模型分析可知:堆,方法區是線程共有的;棧是每個線程私有的。
討論Java內存模型和線程之前,先簡單介紹一下硬件的效率與一致性

由於計算機的存儲設備與處理器的運算能力之間有幾個數量級的差距,所以現代計算機系統都不得不加入一層讀寫速度儘可能接近處理器運算速度的高速緩存(cache)來作爲內存與處理器之間的緩衝:將運算需要使用到的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回內存之中沒這樣處理器就無需等待緩慢的內存讀寫了。
  
基於高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,但是引入了一個新的問題:緩存一致性(Cache Coherence)。在多處理器系統中,每個處理器都有自己的高速緩存,而他們又共享同一主存,如下圖所示:多個處理器運算任務都涉及同一塊主存,需要一種協議可以保障數據的一致性,這類協議有MSI、MESI、MOSI及Dragon Protocol等。Java虛擬機內存模型中定義的內存訪問操作與硬件的緩存訪問操作是具有可比性的,後續將介紹Java內存模型。
  這裏寫圖片描述
  除此之外,爲了使得處理器內部的運算單元能竟可能被充分利用,處理器可能會對輸入代碼進行亂起執行(Out-Of-Order Execution)優化,處理器會在計算之後將對亂序執行的代碼進行結果重組,保證結果準確性。與處理器的亂序執行優化類似,Java虛擬機的即時編譯器中也有類似的指令重排序(Instruction Recorder)優化。


2.1、 Java內存模型

定義Java內存模型並不是一件容易的事情,這個模型必須定義得足夠嚴謹,才能讓Java的併發操作不會產生歧義;但是,也必須得足夠寬鬆,使得虛擬機的實現能有足夠的自由空間去利用硬件的各種特性(寄存器、高速緩存等)來獲取更好的執行速度。經過長時間的驗證和修補,在JDK1.5發佈後,Java內存模型就已經成熟和完善起來了。

2.1.1、 主內存與工作內存

Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣底層細節。此處的變量與Java編程時所說的變量不一樣,指包括了實例字段、靜態字段和構成數組對象的元素,但是不包括局部變量與方法參數,後者是線程私有的,不會被共享。
  Java內存模型中規定了所有的變量都存儲在主內存中,每條線程還有自己的工作內存(可以與前面將的處理器的高速緩存類比),線程的工作內存中保存了該線程使用到的變量到主內存副本拷貝,線程對變量的所有操作(讀取、賦值)都必須在工作內存中進行,而不能直接讀寫主內存中的變量。不同線程之間無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要在主內存來完成,線程、主內存和工作內存的交互關係如下圖所示,和上圖很類似
  這裏寫圖片描述

2.1.2、 java內存變量的交互操作

主內存變量和工作內存變量之間的交互過程如下:
  這裏寫圖片描述


3、 線程安全

通過上面分析,可以發現,每個線程都擁有自己的工作內存,工作內存是線程私有的。所以每個線程對堆中的共享變量進行修改對其他的線程而言是不可見的。
  Java內存模型中,程序(進程)擁有一塊內存空間,可以被所有的線程共享,即MainMemory(主內存:堆);而每個線程又有一塊獨立的內存空間,即WorkingMemory(工作內存:棧)。普通情況下,當線程需要對某一共享變量進行修改時,通常會進行如下的過程:
1.從主內存中拷貝變量的一份副本,並裝載到工作內存中;
2.在工作內存中執行代碼,修改副本的值;
3.用工作內存中的副本值更新主存中的相關變量值。
  所謂“線程安全”,即多個線程同時執行同一段代碼時,不會出現不確定的或者與單線程條件下不一致的結果。通常,下列三種條件居其一的併發訪問被JVM認爲是線程安全的:

  1. 有final關鍵字修飾且已被賦值;
  2. 有volatile關鍵字修飾;
  3. 有鎖保護(synchronized、ReentrantLock等)。

參考博客:
http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章