JVM 記憶體模型 記憶體分配,JVM鎖

desaco發表於2016-05-20

1.瞭解 Java 虛擬機器記憶體模型
2.揭開 Java 物件記憶體分配的祕密
3.Java 虛擬機器的鎖優化策略

> Java記憶體管理與記憶體模型

Java記憶體管理-

http://www.wjdiankong.cn/java%E8%99%9A%E6%8B%9F%E6%9C%BA%E8%A7%A3%E6%9E%90%E7%AF%87%E4%B9%8B-%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/

[java 執行緒] Java記憶體模型深度解讀- http://blog.csdn.net/weixin_36210698/article/details/61619610
java記憶體模型和GC機制:http://blog.csdn.net/ithomer/article/details/6252552
-- 堆中存放物件(物件例項),棧中存放資料(變數 方法及物件的引用)。
  堆溢位(Out Of Memory error):如果虛擬機器棧可以動態擴充,在擴充的時無法申請到足夠的記憶體,將會丟擲OutOfMemoryError異常。棧溢位(Stack Overflow error):如果執行緒請求的棧深度大於棧所允許的深度,將丟擲StackOverflowError異常.

-- 優化Dalvik虛擬機器的堆記憶體分配--http://blog.csdn.net/junjieking/article/details/6899944
Java 記憶體模型及GC原理- http://blog.csdn.net/ithomer/article/details/6252552

https://i.iter01.com/images/619b82b13746b4a5176bbf5ce092faaaa7bd6c65e7190ba5222c3cd5ff9551b5.png https://i.iter01.com/images/e66a1ee35a859b6324c4fb833b689e185da9ebb92ce7cce7868221399dd1439b.png

-- JVM管理的記憶體區域分為幾個模組:
  1,程式計數器(Program Counter Register):程式計數器是一個比較小的記憶體區域,用於指示當前執行緒所執行的位元組碼執行到了第幾行,可以理解為是當前執行緒的行號指示器。位元組碼直譯器在工作時,會通過改變這個計數器的值來取下一條語句指令。
 每個程式計數器只用來記錄一個執行緒的行號,所以它是執行緒私有(一個執行緒就有一個程式計數器)的。
   如果程式執行的是一個Java方法,則計數器記錄的是正在執行的虛擬機器位元組碼指令地址;如果正在執行的是一個本地(native,由C語言編寫 完成)方法,則計數器的值為Undefined,由於程式計數器只是記錄當前指令地址,所以不存在記憶體溢位的情況,因此,程式計數器也是所有JVM記憶體區 域中唯一一個沒有定義OutOfMemoryError的區域。

  2,虛擬機器棧(JVM Stack):一個執行緒的每個方法在執行的同時,都會建立一個棧幀(Statck Frame),棧幀中儲存的有區域性變數表、操作站、動態連結、方法出口等,當方法被呼叫時,棧幀在JVM棧中入棧,當方法執行完成時,棧幀出棧。
   區域性變數表中儲存著方法的相關區域性變數,包括各種基本資料型別,物件的引用,返回地址等。在區域性變數表中,只有long和double型別會佔用2個區域性變數空間(Slot,對於32位機器,一個Slot就是32個bit),其它都是1個Slot。需要注意的是,區域性變數表是在編譯時就已經確定 好的,方法執行所需要分配的空間在棧幀中是完全確定的,在方法的生命週期內都不會改變。
   虛擬機器棧中定義了兩種異常,如果執行緒呼叫的棧深度大於虛擬機器允許的最大深度,則丟擲StatckOverFlowError(棧溢位);不過多 數Java虛擬機器都允許動態擴充套件虛擬機器棧的大小(有少部分是固定長度的),所以執行緒可以一直申請棧,知道記憶體不足,此時,會丟擲 OutOfMemoryError(記憶體溢位)。
   每個執行緒對應著一個虛擬機器棧,因此虛擬機器棧也是執行緒私有的。

  3,本地方法棧(Native Method Statck):本地方法棧在作用,執行機制,異常型別等方面都與虛擬機器棧相同,唯一的區別是:虛擬機器棧是執行Java方法的,而本地方法棧是用來執行native方法的,在很多虛擬機器中(如Sun的JDK預設的HotSpot虛擬機器),會將本地方法棧與虛擬機器棧放在一起使用。
  本地方法棧也是執行緒私有的。

  4,堆區(Heap):堆區是理解Java GC機制最重要的區域,沒有之一。在JVM所管理的記憶體中,堆區是最大的一塊,堆區也是Java GC機制所管理的主要記憶體區域,堆區由所有執行緒共享,在虛擬機器啟動時建立。堆區的存在是為了儲存物件例項,原則上講,所有的物件都在堆區上分配記憶體(不過現代技術裡,也不是這麼絕對的,也有棧上直接分配的)。
  一般的,根據Java虛擬機器規範規定,堆記憶體需要在邏輯上是連續的(在物理上不需要),在實現時,可以是固定大小的,也可以是可擴充套件的,目前主流的虛擬機器堆區都是可擴充套件的。如果在執行垃圾回收之後,仍沒有足夠的記憶體分配,也不能再擴充套件,將會丟擲OutOfMemoryError:Java heap space異常。
    “Java記憶體分配機制”。

  5,方法區(Method Area):在Java虛擬機器規範中,將方法區作為堆的一個邏輯部分來對待,但事實上,方法區並不是堆(Non-Heap);另外,不少人的部落格中,將Java GC的分代收集機制分為3個代:青年代,老年代,永久代,這些作者將方法區定義為“永久代”,這是因為,對於之前的HotSpot Java虛擬機器的實現方式中,將分代收集的思想擴充套件到了方法區,並將方法區設計成了永久代。不過,除HotSpot之外的多數虛擬機器,並不將方法區當做永久代,HotSpot本身,也計劃取消永久代。本文中,由於筆者主要使用Oracle JDK6.0,因此仍將使用永久代一詞。
  方法區是各個執行緒共享的區域,用於儲存已經被虛擬機器載入的類資訊(即載入類時需要載入的資訊,包括版本、field、方法、介面等資訊)、final常量、靜態變數、編譯器即時編譯的程式碼等。
  方法區在物理上也不需要是連續的,可以選擇固定大小或可擴充套件大小,並且方法區比堆還多了一個限制:可以選擇是否執行垃圾收集。一般的,方法區上 執行的垃圾收集是很少的,這也是方法區被稱為永久代的原因之一(HotSpot),但這也不代表著在方法區上完全沒有垃圾收集,其上的垃圾收集主要是針對常量池的記憶體回收和對已載入類的解除安裝。
  在方法區上進行垃圾收集,條件苛刻而且相當困難,效果也不令人滿意,所以一般不做太多考慮,可以留作以後進一步深入研究時使用。
  在方法區上定義了OutOfMemoryError:PermGen space異常,在記憶體不足時丟擲。
  執行時常量池(Runtime Constant Pool)是方法區的一部分,用於儲存編譯期就生成的字面常量、符號引用、翻譯出來的直接引用(符號引用就是編碼是用字串表示某個變數、介面的位置,直接引用就是根據符號引用翻譯出來的地址,將在類連結階段完成翻譯);執行時常量池除了儲存編譯期常量外,也可以儲存在執行時間產生的常量(比如String類的intern()方法,作用是String維護了一個常量池,如果呼叫的字元“abc”已經在常量池中,則返回池中的字串地址,否則,新建一個常量加入池中,並返回地址)。

  6,直接記憶體(Direct Memory):直接記憶體並不是JVM管理的記憶體,可以這樣理解,直接記憶體,就是 JVM以外的機器記憶體,比如,你有4G的記憶體,JVM佔用了1G,則其餘的3G就是直接記憶體,JDK中有一種基於通道(Channel)和緩衝區 (Buffer)的記憶體分配方式,將由C語言實現的native函式庫分配在直接記憶體中,用儲存在JVM堆中的DirectByteBuffer來引用。 由於直接記憶體收到本機器記憶體的限制,所以也可能出現OutOfMemoryError的異常。

-- 關於主記憶體與工作記憶體之間的具體互動協議,即一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步到主記憶體之間的實現細節,Java記憶體模型定義了以下八種操作來完成:
  1.lock(鎖定):作用於主記憶體的變數,把一個變數標識為一條執行緒獨佔狀態。
  2.unlock(解鎖):作用於主記憶體變數,把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。
  3.read(讀取):作用於主記憶體變數,把一個變數值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用
  4.load(載入):作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
  5.use(使用):作用於工作記憶體的變數,把工作記憶體中的一個變數值傳遞給執行引擎,每當虛擬機器遇到一個需要使用變數的值的位元組碼指令時將會執行這個操作。
  6.assign(賦值):作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦值給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
  7.store(儲存):作用於工作記憶體的變數,把工作記憶體中的一個變數的值傳送到主記憶體中,以便隨後的write的操作。
  8.write(寫入):作用於主記憶體的變數,它把store操作從工作記憶體中一個變數的值傳送到主記憶體的變數中。

Java記憶體模型還規定了在執行上述八種基本操作時,必須滿足如下規則:
  1.不允許read和load、store和write操作之一單獨出現
  2.不允許一個執行緒丟棄它的最近assign的操作,即變數在工作記憶體中改變了之後必須同步到主記憶體中。
  3.不允許一個執行緒無原因地(沒有發生過任何assign操作)把資料從工作記憶體同步回主記憶體中。
  4.一個新的變數只能在主記憶體中誕生,不允許在工作記憶體中直接使用一個未被初始化(load或assign)的變數。即就是對一個變數實施use和store操作之前,必須先執行過了assign和load操作。
  5.一個變數在同一時刻只允許一條執行緒對其進行lock操作,lock和unlock必須成對出現
  6.如果對一個變數執行lock操作,將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前需要重新執行load或assign操作初始化變數的值
  7.如果一個變數事先沒有被lock操作鎖定,則不允許對它執行unlock操作;也不允許去unlock一個被其他執行緒鎖定的變數。
  8.對一個變數執行unlock操作之前,必須先把此變數同步到主記憶體中(執行store和write操作)。
  Java記憶體模型把記憶體屏障分為LoadLoad、LoadStore、StoreLoad和StoreStore等4種。

  記憶體可見性問題. Java執行緒之間的通訊由Java記憶體模型(本文簡稱為JMM)控制,JMM決定一個執行緒對共享變數的寫入何時對另一個執行緒可見。從抽象的角度來看,JMM定義了執行緒和主記憶體之間的抽象關係:執行緒之間的共享變數儲存在主記憶體(main memory)中,每個執行緒都有一個私有的本地記憶體(local memory),本地記憶體中儲存了該執行緒以讀/寫共享變數的副本。本地記憶體是JMM的一個抽象概念,並不真實存在。它涵蓋了快取,寫緩衝區,暫存器以及其他的硬體和編譯器優化。

-- 深入理解Java記憶體模型(一)——基礎 http://www.infoq.com/cn/articles/java-memory-model-1
與程式設計師密切相關的happens-before規則如下:
  程式順序規則:一個執行緒中的每個操作,happens- before 於該執行緒中的任意後續操作。
  監視器鎖規則:對一個監視器鎖的解鎖,happens- before 於隨後對這個監視器鎖的加鎖。
  volatile變數規則:對一個volatile域的寫,happens- before 於任意後續對這個volatile域的讀。
  傳遞性:如果A happens- before B,且B happens- before C,那麼A happens- before C。

-- Java記憶體模型 http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html
Java記憶體模型與Java執行緒的實現原理 http://blog.csdn.net/sunxianghuang/article/details/51920794
  多工和高併發是衡量一臺計算機處理器的能力重要指標之一。一般衡量一個伺服器效能的高低好壞,使用每秒事務處理數(Transactions Per Second,TPS)這個指標比較能說明問題,它代表著一秒內伺服器平均能響應的請求數,而TPS值與程式的併發能力有著非常密切的關係。在討論Java記憶體模型和執行緒之前,先簡單介紹一下硬體的效率與一致性。

-- 在Java中,它的記憶體管理包括兩方面:記憶體分配(建立Java物件的時候)和記憶體回收。瞭解JVM,才能寫出更高效,充分利用有限的記憶體的程式。
   - 記憶體管理優化小技巧:
  1)儘量使用直接量,eg:String javaStr = "小學徒的成長曆程";
  2)使用StringBuilder和StringBuffer進行字串連線等操作;
  3)儘早釋放無用物件;
  4)儘量少使用靜態變數;
  5)快取常用的物件:可以使用開源的開源快取實現,eg:OSCache,Ehcache;
  6)儘量不使用finalize()方法;
  7)在必要的時候可以考慮使用軟引用SoftReference。

 -- 執行時資料區域(runtime data area):

 1、棧區(stack),由編譯器自動分配釋放,存放函式的引數值,區域性變數的值等。其操作方式類似於資料結構中的棧。  
 2、堆區(heap),一般由程式設計師分配釋放,若程式設計師不釋放,程式結束時可能由OS回收。注意它與資料結構中的堆是兩回事,分配方式倒是類似於連結串列。  
 3、全域性區(static,靜態區),全域性變數和靜態變數的儲存是放在一塊的,初始化的全域性變數和靜態變數在一塊區域,未初始化的全域性變數和未初始化的靜態變數在相鄰的另一塊區域。程式結束後由系統釋放。  
 4、文字常量區,常量字串就是放在這裡的,程式結束後由系統釋放  
 5、程式程式碼區,存放函式體的二進位制程式碼。

-- Java 陣列及其記憶體管理 - http://geek.csdn.net/news/detail/124635
  java 中的陣列是靜態的 ,即初始化後,它所佔的記憶體空間、陣列長度是不變的。而且必須先 初始化 後使用。
  陣列變數存在棧區,陣列物件存在堆記憶體,只能通過引用來訪問堆記憶體中的資料。
  引用變數本質上是一個指標,只要通過引用變數訪問屬性或呼叫方法,該引用變數就會由它所引用的物件替換。
  不要同時使用靜態和動態初始化,即同時指定陣列長度和元素初始值.
  java 中的陣列變數只是引用變數,他並不是陣列的本身,只要讓陣列變數指向有效的陣列物件,即可使用該陣列變數.
  引用型別的陣列元素依然是一用型別的,它儲存的是引用,指向另一塊記憶體,該記憶體中儲存了引用變數所引用的物件(包括陣列和java物件)。 多維陣列的本質是一維陣列。

-- Java物件及其記憶體管理- http://geek.csdn.net/news/detail/124632
java中的記憶體管理分為兩個方面:
 記憶體分配:指建立java物件時JVM為該物件在堆空間中所分配的記憶體空間。
 記憶體回收:指java 物件失去引用,變成垃圾時,JVM的垃圾回收機制自動清理該物件,並回收該物件所佔用的記憶體。
  雖然JVM 內建了垃圾回收機制,但仍可能導致記憶體洩露、資源洩露等,所以我們不能肆無忌憚的建立物件。此外,垃圾回收機制是由一個後臺執行緒完成,也是很消耗效能的。

-- 在執行程式時為了提高效能,編譯器和處理器經常會對指令進行重排序。重排序分成三種型別:
 1.編譯器優化的重排序。編譯器在不改變單執行緒程式語義放入前提下,可以重新安排語句的執行順序。
 2.指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。
 3.記憶體系統的重排序。由於處理器使用快取和讀寫緩衝區,這使得載入和儲存操作看上去可能是在亂序執行。

-- Java的堆記憶體和堆外記憶體
JVM可以使用的記憶體分外2種:堆記憶體和堆外記憶體.(java.nio.DirectByteBuffer),堆外記憶體(off-heap),堆記憶體(on-heap)
JDK5.0之後,程式碼中能直接操作本地記憶體的方式有2種:使用未公開的Unsafe和NIO包下ByteBuffer。

-- 堆外快取的開源實現。查詢了一些資料後瞭解到的主要有:
  1.Ehcache 3.0:3.0基於其商業公司一個非開源的堆外元件的實現。

  2.Chronical Map:OpenHFT包括很多類庫,使用這些類庫很少產生垃圾,並且應用程式使用這些類庫後也很少發生Minor GC。類庫主要包括:Chronicle Map,Chronicle Queue等等。http://www.mvnjar.com/net.openhft/chronicle-queue/jar.html
  3.OHC:來源於Cassandra 3.0, Apache v2。

  4.Ignite: 一個規模巨集大的記憶體計算框架,屬於Apache專案。

  堆記憶體由JVM自己管理,堆外記憶體必須要由我們自己釋放;堆記憶體的消耗速度遠遠小於堆外記憶體的消耗,但要命的是必須先釋放堆記憶體中的物件,才能釋放堆外記憶體,但是我們又不能強制JVM釋放堆記憶體。
  Direct Memory的回收機制:Direct Memory是受GC控制的,例如ByteBuffer bb = ByteBuffer.allocateDirect(1024),這段程式碼的執行會在堆外佔用1k的記憶體,Java堆內只會佔用一個物件的指標引用的大小,堆外的這1k的空間只有當bb物件被回收時,才會被回收,這裡會發現一個明顯的不對稱現象,就是堆外可能佔用了很多,而堆內沒佔用多少,導致還沒觸發GC,那就很容易出現Direct Memory造成實體記憶體耗光
  Direct ByteBuffer分配出去的記憶體其實也是由GC負責回收的,而不像Unsafe是完全自行管理的,Hotspot在GC時會掃描Direct ByteBuffer物件是否有引用,如沒有則同時也會回收其佔用的堆外記憶體。

Java堆外記憶體的使用- http://www.importnew.com/14292.html
java獲取Unsafe類的例項和取消eclipse編譯的錯誤和警告- http://blog.csdn.net/aitangyong/article/details/38276681
Java堆外記憶體之突破JVM枷鎖- http://www.cnblogs.com/xing901022/p/5215458.html
JVM原始碼分析之堆外記憶體完全解讀- http://www.open-open.com/lib/view/open1431570358826.html
Lawery還介紹了OpenHFT公司提供三個開源庫:Chronicle Queue、Chronicle Map和Thread Affinity.

-- Java的棧記憶體和堆記憶體
Java把記憶體分成兩種,一種叫做棧記憶體,一種叫做堆記憶體.
  在函式中定義的一些基本型別的變數和物件的引用變數都是在函式的棧記憶體中分配。堆記憶體用於存放由new建立的物件和陣列。在堆中分配的記憶體,由java虛擬機器自動垃圾回收器來管理。堆主要用來存放物件的,棧主要是用來執行程式的.當在一段程式碼塊定義一個變數時,Java 就在棧中為這個變數分配記憶體空間,當超過變數的作用域後,Java 會自動釋放掉為該變數分配的記憶體空間,該記憶體空間可以立即被另作它用。在堆中產生了一個陣列或者物件之後,還可以在棧中定義一個特殊的變數,讓棧中的這個變數的取值等於陣列或物件在堆記憶體中的首地址,棧中的這個變數就成了陣列或物件的引用變數,以後就可以在程式中使用棧中的引用變數來訪問堆中的陣列或者物件,引用變數就相當於是為陣列或者物件起的一個名稱。引用變數是普通的變數,定義時在棧中分配,引用變數在程式執行到其作用域之外後被釋放。而陣列和物件本身在堆中分配,即使程式執行到使用 new 產生陣列或者物件的語句所在的程式碼塊之外,陣列和物件本身佔據的記憶體不會被釋放,陣列和物件在沒有引用變數指向它的時候,才變為垃圾,不能在被使用,但仍然佔據記憶體空間不放,在隨後的一個不確定的時間被垃圾回收器收走(釋放掉)。這也是 Java 比較佔記憶體的原因,實際上,棧中的變數指向堆記憶體中的變數,這就是 Java 中的指標!
    按照編譯原理的觀點,程式執行時的記憶體分配有三種策略,分別是靜態的,棧式的,和堆式的.靜態儲存分配要求在編譯時能知道所有變數的儲存要求,棧式儲存分配要求在過程的入口處必須知道所有的儲存要求,而堆式儲存分配則專門負責在編譯時或執行時模組入口處都無法確定儲存要求的資料結構的記憶體分配,比如可變長度串和物件例項.堆由大片的可利用塊或空閒塊組成,堆中的記憶體可以按照任意順序分配和釋放.
-- 簡單通俗的講,一個完整的Java程式執行過程會涉及以下記憶體區域:
 1.暫存器:JVM內部虛擬暫存器,存取速度非常快,程式不可控制。
 2.棧:儲存區域性變數的值,包括:1.用來儲存基本資料型別的值;2.儲存類的例項,即堆區物件的引用(指標)。也可以用來儲存載入方法時的幀。
 3.堆:用來存放動態產生的資料,比如new出來的物件。注意建立出來的物件只包含屬於各自的成員變數,並不包括成員方法。因為同一個類的物件擁有各自的成員變數,儲存在各自的堆中,但是他們共享該類的方法,並不是每建立一個物件就把成員方法複製一次。
 4.常量池:JVM為每個已載入的型別維護一個常量池,常量池就是這個型別用到的常量的一個有序集合。包括直接常量(基本型別,String)和對其他型別、方法、欄位的符號引用。池中的資料和陣列一樣通過索引訪問。由於常量池包含了一個型別所有的對其他型別、方法、欄位的符號引用,所以常量池在Java的動態連結中起了核心作用。常量池存在於堆中。
 5.程式碼段:用來存放從硬碟上讀取的源程式程式碼。
 6.資料段:用來存放static定義的靜態成員。

  Java的堆是一個執行時資料區,類的(物件從中分配空間。這些物件通過new、newarray、anewarray和multianewarray等指令建立,它們不需要程式程式碼來顯式的釋放。堆是由垃圾回收來負責的,堆的優勢是可以動態地分配記憶體大小,生存期也不必事先告訴編譯器,因為它是在執行時動態分配記憶體的,Java的垃圾收集器會自動收走這些不再使用的資料。但缺點是,由於要在執行時動態分配記憶體,存取速度較慢。
    棧的優勢是,存取速度比堆要快,僅次於暫存器,棧資料可以共享。但缺點是,存在棧中的資料大小與生存期必須是確定的,缺乏靈活性。棧中主要存放一些基本型別的變數(int, short, long, byte, float, double, boolean, char)和物件控制程式碼。棧有一個很重要的特殊性,就是存在棧中的資料可以共享。

棧是一種“先進後出”的一種資料結構,有壓棧出棧兩種操作方式。棧主要分為兩類:靜態棧;動態棧 
【靜態棧】靜態棧的核心是陣列,類似於一個連續記憶體的陣列,我們只能操作其棧頂元素。
【動態棧】靜態棧的核心是陣列,類似於一個連續記憶體的陣列,我們只能操作其棧頂節點。

   靜態變數 和 區域性變數是以壓棧出棧的方式分配記憶體的,系統會在一個程式碼段中分配和回收區域性變數,實際上每個程式碼段、函式都是一個或多個巢狀的棧,我們不需要手動管理棧區記憶體。 
   動態記憶體是一種堆排序的方式分配記憶體的,記憶體分配好後便不會自動回收,需要程式設計師手動回收。否則就會造成記憶體洩漏,記憶體越用越少。 

-- java程式中的變數,大體可以分為 成員變數 和 區域性變數 。其中區域性變數可分為如下三類:
  1.形參:在方法名中定義的變數,有方法呼叫者負責為其賦值,隨著方法的結束而消亡。 
  2.方法內區域性變數:在方法內定義的變數,必須在方法內對其進行初始化。它從初始化完成後開始生效,隨著方法結束而消亡。 
  3.程式碼塊內區域性變數 :在程式碼塊內定義的變數,必須在程式碼塊內對其顯示初始化。從初始化完成後生效,隨著程式碼塊的結束而消亡。 
  區域性變數的作用時間很短暫,他們被存在棧記憶體中。類體內定義的變數為成員變數。如果使用 static 修飾,則為靜態變數或者類變數,否則成為非靜態變數或者例項變數。
  多個處理器運算任務都涉及同一塊主存,需要一種協議可以保障資料的一致性,這類協議有MSI、MESI、MOSI及Dragon Protocol等。Java虛擬機器記憶體模型中定義的記憶體訪問操作與硬體的快取訪問操作是具有可比性的
> JVM 鎖
深入理解多執行緒,Java虛擬機器的鎖優化技術- https://blog.csdn.net/hollis_chuang/article/details/80195359
  在程式碼中,需要加鎖的時候,我們提倡儘量減小鎖的粒度,這樣可以避免不必要的阻塞。
  對於synchronized同步方法,JVM採用ACC_SYNCHRONIZED標記符來實現同步。 對於同步程式碼塊。JVM採用monitorenter、monitorexit兩個指令來實現同步。synchronized同步程式碼塊使用monitorenter和monitorexit兩個指令實現。

  管程 (英語:Monitors,也稱為監視器) 是一種程式結構,結構內的多個子程式(物件或模組)形成的多個工作執行緒互斥訪問共享資源。這些共享資源一般是硬體裝置或一群變數。管程實現了在一個時間點,最多隻有一個執行緒在執行管程的某個子程式。與那些通過修改資料結構實現互斥訪問的併發程式設計相比,管程實現很大程度上簡化了程式設計。 管程提供了一種機制,執行緒可以臨時放棄互斥訪問,等待某些條件得到滿足後,重新獲得執行權恢復它的互斥訪問。

Monitor其實是一種同步工具,也可以說是一種同步機制,它通常被描述為一個物件,主要特點是:
  物件的所有方法都被“互斥”的執行。好比一個Monitor只有一個執行“許可”,任一個執行緒進入任何一個方法都需要獲得這個“許可”,離開時把許可歸還。
  通常提供singal機制:允許正持有“許可”的執行緒暫時放棄“許可”,等待某個謂詞成真(條件變數),而條件成立後,當前程式可以“通知”正在等待這個條件變數的執行緒,讓他可以重新去獲得執行許可。

sychronized加鎖的時候,會呼叫objectMonitor的enter方法,解鎖的時候會呼叫exit方法。事實上,只有在JDK1.6之前,synchronized的實現才會直接呼叫ObjectMonitor的enter和exit,這種鎖被稱之為重量級鎖。為什麼說這種方式操作鎖很重呢?
  Java的執行緒是對映到作業系統原生執行緒之上的,如果要阻塞或喚醒一個執行緒就需要作業系統的幫忙,這就要從使用者態轉換到核心態,因此狀態轉換需要花費很多的處理器時間,對於程式碼簡單的同步塊(如被synchronized修飾的get 或set方法)狀態轉換消耗的時間有可能比使用者程式碼執行的時間還要長,所以說synchronized是java語言中一個重量級的操縱。
  所以,在JDK1.6中出現對鎖進行了很多的優化,進而出現輕量級鎖,偏向鎖,鎖消除,適應性自旋鎖,鎖粗化(自旋鎖在1.4就有 只不過預設的是關閉的,jdk1.6是預設開啟的),這些操作都是為了線上程之間更高效的共享資料 ,解決競爭問題。

高效併發是從JDK 1.5 到 JDK 1.6的一個重要改進,HotSpot虛擬機器開發團隊在這個版本中花費了很大的精力去對Java中的鎖進行優化,如適應性自旋、鎖消除、鎖粗化、輕量級鎖和偏向鎖等。這些技術都是為了線上程之間更高效的共享資料,以及解決競爭問題。
  自旋、鎖消除以及鎖粗化等技術。 輕量級鎖和偏向鎖.作為一個Java開發,你只需要知道你想在加鎖的時候使用synchronized就可以了,具體的鎖的優化是虛擬機器根據競爭情況自行決定的。也就是說,在JDK 1.5 以後,我們即將介紹的這些概念,都被封裝在synchronized中了。
  自Java 6/Java 7開始,Java虛擬機器對內部鎖的實現進行了一些優化。這些優化主要包括鎖消除(Lock Elision)、鎖粗化(Lock Coarsening)、偏向鎖(Biased Locking)以及適應性自旋鎖(Adaptive Locking)。這些優化僅在Java虛擬機器server模式下起作用(即執行Java程式時我們可能需要在命令列中指定Java虛擬機器引數“-server”以開啟這些優化)。

> Java記憶體模型即Java Memory Model,簡稱JMM。
  Java記憶體模型的主要目標是定義程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體中取出變數這樣底層細節。此處的變數與Java程式設計時所說的變數不一樣,指包括了例項欄位、靜態欄位和構成陣列物件的元素,但是不包括區域性變數與方法引數,後者是執行緒私有的,不會被共享。
  Java記憶體模型中規定了所有的變數都儲存在主記憶體中,每條執行緒還有自己的工作記憶體(可以與前面將的處理器的快取記憶體類比),執行緒的工作記憶體中儲存了該執行緒使用到的變數到主記憶體副本拷貝,執行緒對變數的所有操作(讀取、賦值)都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數。不同執行緒之間無法直接訪問對方工作記憶體中的變數,執行緒間變數值的傳遞均需要在主記憶體來完成。

關於主記憶體與工作記憶體之間的具體互動協議,即一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步到主記憶體之間的實現細節,Java記憶體模型定義了以下八種操作來完成:
lock(鎖定):作用於主記憶體的變數,把一個變數標識為一條執行緒獨佔狀態。
unlock(解鎖):作用於主記憶體變數,把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。
read(讀取):作用於主記憶體變數,把一個變數值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用
load(載入):作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
use(使用):作用於工作記憶體的變數,把工作記憶體中的一個變數值傳遞給執行引擎,每當虛擬機器遇到一個需要使用變數的值的位元組碼指令時將會執行這個操作。
assign(賦值):作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦值給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
store(儲存):作用於工作記憶體的變數,把工作記憶體中的一個變數的值傳送到主記憶體中,以便隨後的write的操作。
write(寫入):作用於主記憶體的變數,它把store操作從工作記憶體中一個變數的值傳送到主記憶體的變數中。

與程式設計師密切相關的happens-before規則如下:
1、程式順序規則:一個執行緒中的每個操作,happens-before於該執行緒中任意的後續操作。
2、監視器鎖規則:對一個鎖的解鎖操作,happens-before於隨後對這個鎖的加鎖操作。
3、volatile域規則:對一個volatile域的寫操作,happens-before於任意執行緒後續對這個volatile域的讀。
4、傳遞性規則:如果 A happens-before B,且 B happens-before C,那麼A happens-before C。

指令重排序:
1、編譯器優化重排序:編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序。
2、指令級並行的重排序:如果不存l在資料依賴性,處理器可以改變語句對應機器指令的執行順序。
3、記憶體系統的重排序:處理器使用快取和讀寫緩衝區,這使得載入和儲存操作看上去可能是在亂序執行。

 JVM主要管理兩種型別記憶體:堆和非堆,堆記憶體(Heap Memory)是在 Java 虛擬機器啟動時建立,非堆記憶體(Non-heap Memory)是在JVM堆之外的記憶體。堆是Java程式碼可及的記憶體,留給開發人員使用的;非堆是JVM留給自己用的,包含方法區、JVM內部處理或優化所需的記憶體(如 JITCompiler,Just-in-time Compiler,即時編譯後的程式碼快取)、每個類結構(如執行時常數池、欄位和方法資料)以及方法和構造方法的程式碼。

GC基本原理:GC(Garbage Collection),是JAVA/.NET中的垃圾收集器。
Java是由C++發展來的,它擯棄了C++中一些繁瑣容易出錯的東西,引入了計數器的概念,其中有一條就是這個GC機制(C#借鑑了JAVA).
1) 在Young Generation中,有一個叫Eden Space的空間,主要是用來存放新生的物件,還有兩個Survivor Spaces(from、to),它們的大小總是一樣,它們用來存放每次垃圾回收後存活下來的物件。
2) 在Old Generation中,主要存放應用程式中生命週期長的記憶體物件。
3) 在Young Generation塊中,垃圾回收一般用Copying的演算法,速度快。每次GC的時候,存活下來的物件首先由Eden拷貝到某個SurvivorSpace,當Survivor Space空間滿了後,剩下的live物件就被直接拷貝到OldGeneration中去。因此,每次GC後,Eden記憶體塊會被清空。
4) 在Old Generation塊中,垃圾回收一般用mark-compact的演算法,速度慢些,但減少記憶體要求。
5) 垃圾回收分多級,0級為全部(Full)的垃圾回收,會回收OLD段中的垃圾;1級或以上為部分垃圾回收,只會回收Young中的垃圾,記憶體溢位通常發生於OLD段或Perm段垃圾回收後,仍然無記憶體空間容納新的Java物件的情況。

  JMM決定一個執行緒對共享變數的寫入何時對另一個執行緒可見。從抽象的角度來看,JMM定義了執行緒和主記憶體之間的抽象關係:執行緒之間的共享變數儲存在主記憶體(main memory)中,每個執行緒都有一個私有的本地記憶體(local memory),本地記憶體中儲存了該執行緒以讀/寫共享變數的副本。本地記憶體是JMM的一個抽象概念,並不真實存在。它涵蓋了快取,寫緩衝區,暫存器以及其他的硬體和編譯器優化。
  JMM規定了所有的變數都儲存在主記憶體(Main Memory)中。每個執行緒還有自己的工作記憶體(Working Memory),執行緒的工作記憶體中儲存了該執行緒使用到的變數的主記憶體的副本拷貝,執行緒對變數的所有操作(讀取、賦值等)都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數(volatile變數仍然有工作記憶體的拷貝,但是由於它特殊的操作順序性規定,所以看起來如同直接在主記憶體中讀寫訪問一般)。不同的執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒之間值的傳遞都需要通過主記憶體來完成。

在JVM內部,Java記憶體模型把記憶體分成了兩部分:執行緒棧區和堆區.

相關文章