java記憶體模型

Java平台自動集成了執行緒以及多處理器技術,這種集成程度比Java以前誕生的計算機語言要厲害很多,該語言針對多種異構平台的平台獨立性而使用的多執行緒技術支持也是具有開拓性的一面,有時候在開發Java同步和執行緒安全要求很嚴格的程式時,往往容易混淆的一個概念就是記憶體模型。究竟什麼是記憶體模型?記憶體模型描述了程中各個變數(實例域、靜態域和數組元素)之間的關係,以及在實際計算機系統中將變數存儲到記憶體和從記憶體中取出變數這樣的底層細節,對象最終是存儲在記憶體裡面的,這點沒有錯,但是編譯器、運行庫、處理器或者系統快取可以有特權在變數指定記憶體位置存儲或者取出變數的值。【JMM】(Java Memory Model的縮寫)允許編譯器和快取以數據在處理器特定的快取(或暫存器)和主存之間移動的次序擁有重要的特權,除非程式設計師使用了volatile或synchronized明確請求了某些可見性的保證。

基本介紹

  • 中文名:java記憶體模型
  • 外文名:Java Memory Model 
  • 術語:JMM
JMM簡介,記憶體模型概述,JMM結構:,原始JMM缺陷:,堆和棧,java對象的記憶體分配,

JMM簡介

記憶體模型概述

1)JSR133:
Java語言規範裡面指出了JMM是一個比較開拓性的嘗試,這種嘗試視圖定義一個一致的、跨平台的記憶體模型,但是它有一些比較細微而且很重要的缺點。其實Java語言裡面比較容易混淆的關鍵字主要是synchronizedvolatile,也因為這樣在開發過程中往往開發者會忽略掉這些規則,這也使得編寫同步代碼比較困難。
JSR133本身的目的是為了修復原本JMM的一些缺陷而提出的,其本身的制定目標有以下幾個:
  • 保留目JVM的安全保證,以進行類型的安全檢查
  • 提供(out-of-thin-air safety)無中生有安全性,這樣“正確同步的”應該被正式而且直觀地定義
  • 程式設計師要有信心開發多執行緒程式,當然沒有其他辦法使得並發程式變得很容易開發,但是該規範的發布主要目標是為了減輕程式設計師理解記憶體模型中的一些細節負擔
  • 提供大範圍的流行硬體體系結構上的高性能JVM實現,現在的處理器在它們的記憶體模型上有著很大的不同,JMM應該能夠適合於實際的儘可能多的體系結構而不以性能為代價,這也是Java跨平台型設計的基礎
  • 提供一個同步的習慣用法,以允許發布一個對象使他不用同步就可見,這種情況又稱為初始化安全(initialization safety)的新的安全保證
  • 對現有代碼應該只有最小限度的影響
2)同步、異步【這裡僅僅指概念上的理解,不牽涉到計算機底層基礎的一些操作】:
在系統開發過程,經常會遇到這幾個基本概念,不論是網路通訊、對象之間的訊息通訊還是Web開發人員常用的Http請求都會遇到這樣幾個概念,經常有人提到Ajax是異步通訊方式,那么究竟怎樣的方式是這樣的概念描述呢?
同步:同步就是在發出一個功能調用的時候,在沒有得到回響之前,該調用就不返回,按照這樣的定義,其實大部分程式的執行都是同步調用的,一般情況下,在描述同步和異步操作的時候,主要是指代需要其他部件協作處理或者需要協作回響的一些任務處理。比如有一個執行緒A,在A執行的過程中,可能需要B提供一些相關的執行數據,當然觸發B回響的就是A向B傳送一個請求或者說對B進行一個調用操作,如果A在執行該操作的時候是同步的方式,那么A就會停留在這個位置等待B給一個回響訊息,在B沒有任何回響訊息回來的時候,A不能做其他事情,只能等待,那么這樣的情況,A的操作就是一個同步的簡單說明。
異步:異步就是在發出一個功能調用的時候,不需要等待回響,繼續進行它該做的事情,一旦得到回響了過後給予一定的處理,但是不影響正常的處理過程的一種方式。比如有一個執行緒A,在A執行的過程中,同樣需要B提供一些相關數據或者操作,當A向B傳送一個請求或者對B進行調用操作過後,A不需要繼續等待,而是執行A自己應該做的事情,一旦B有了回響過後會通知A,A接受到該異步請求的回響的時候會進行相關的處理,這種情況下A的操作就是一個簡單的異步操作。
3)可見性、可排序性
Java記憶體模型的兩個關鍵概念:可見性(Visibility可排序性(Ordering)
開發過多執行緒程式的程式設計師都明白,synchronized關鍵字強制實施一個執行緒之間的互斥鎖(相互排斥),該互斥鎖防止每次有多個執行緒進入一個給定監控器所保護的同步語句塊,也就是說在該情況下,執行程式代碼所獨有的某些記憶體是獨占模式其他的執行緒是不能針對它執行過程所獨占的記憶體進行訪問的,這種情況稱為該記憶體不可見。但是在該模型的同步模式中,還有另外一個方面:JMM中指出了,JVM在處理該強制實施的時候可以提供一些記憶體的可見規則,在該規則裡面,它確保當存在一個同步塊時,快取被更新,當輸入一個同步塊時,快取失效。因此在JVM內部提供給定監控器保護的同步塊之中,一個執行緒所寫入的值對於其餘所有的執行由同一個監控器保護的同步塊執行緒來說是可見的,這就是一個簡單的可見性的描述。這種機器保證編譯器不會把指令從一個同步塊的內部移到外部,雖然有時候它會把指令由外部移動到內部。JMM在預設情況下不做這樣的保證——只要有多個執行緒訪問相同變數時必須使用同步。簡單總結:
可見性就是在多核或者多執行緒運行過程中記憶體的一種共享模式,在JMM模型裡面,通過並發執行緒修改變數值的時候,必須將執行緒變數同步回主存過後,其他執行緒才可能訪問到。
可排序性提供了記憶體內部的訪問順序,在不同的程式針對不同的記憶體塊進行訪問的時候,其訪問不是無序,比如有一個記憶體塊,A和B需要訪問的時候,JMM會提供一定的記憶體分配策略有序地分配它們使用的記憶體,而在記憶體的調用過程也會變得有序地進行,記憶體的折中性質可以簡單理解為有序性。而在Java多執行緒程式裡面,JMM通過Java關鍵字volatile來保證記憶體的有序訪問。

JMM結構:

1)簡單分析:
Java語言規範中提到過,JVM中存在一個主存區(Main Memory或Java Heap Memory),Java中所有變數都是存在主存中的,對於所有執行緒進行共享,而每個執行緒又存在自己的工作記憶體(Working Memory),工作記憶體中保存的是主存中某些變數的拷貝,執行緒對所有變數的操作並非發生在主存區,而是發生在工作記憶體中,而執行緒之間是不能直接相互訪問,變數在程式中的傳遞,是依賴主存來完成的。而在多核處理器下,大部分數據存儲在高速快取中,如果高速快取不經過記憶體的時候,也是不可見的一種表現。在Java程式中,記憶體本身是比較昂貴的資源,其實不僅僅針對Java應用程式,對作業系統本身而言記憶體也屬於昂貴資源,Java程式在性能開銷過程中有幾個比較典型的可控制的來源。synchronizedvolatile關鍵字提供的記憶體中模型的可見性保證程式使用一個特殊的、存儲關卡(memory barrier)的指令,來刷新快取,使快取無效,刷新硬體的寫快取並且延遲執行的傳遞過程,無疑該機制會對Java程式的性能產生一定的影響。
JMM的最初目的,就是為了能夠支持多執行緒程式設計的,每個執行緒可以認為是和其他執行緒不同的CPU上運行,或者對於多處理器的機器而言,該模型需要實現的就是使得每一個執行緒就像運行在不同的機器、不同的CPU或者本身就不同的執行緒上一樣,這種情況實際上在項目開發中是常見的。對於CPU本身而言,不能直接訪問其他CPU的暫存器,模型必須通過某種定義規則來使得執行緒和執行緒在工作記憶體中進行相互調用而實現CPU本身對其他CPU、或者說執行緒對其他執行緒的記憶體中資源的訪問,而表現這種規則的運行環境一般為運行該程式的運行宿主環境(作業系統、伺服器、分散式系統等),而程式本身表現就依賴於編寫該程式的語言特性,這裡也就是說用Java編寫的應用程式在記憶體管理中的實現就是遵循其部分原則,也就是前邊提及到的JMM定義了Java語言針對記憶體的一些的相關規則。然而,雖然設計之初是為了能夠更好支持多執行緒,但是該模型的套用和實現當然不局限於多處理器,而在JVM編譯器編譯Java編寫的程式的時候以及運行期執行該程式的時候,對於單CPU的系統而言,這種規則也是有效的,這就是是上邊提到的執行緒和執行緒之間的記憶體策略。JMM本身在描述過程沒有提過具體的記憶體地址以及在實現該策略中的實現方法是由JVM的哪一個環節(編譯器、處理器、快取控制器、其他)提供的機制來實現的,甚至針對一個開發非常熟悉的程式設計師,也不一定能夠了解它內部對於類、對象、方法以及相關內容的一些具體可見的物理結構。相反,JMM定義了一個執行緒與主存之間的抽象關係,其實從上邊的圖可以知道,每一個執行緒可以抽象成為一個工作記憶體(抽象的高速快取和暫存器),其中存儲了Java的一些值,該模型保證了Java裡面的屬性、方法、欄位存在一定的數學特性,按照該特性,該模型存儲了對應的一些內容,並且針對這些內容進行了一定的序列化以及存儲排序操作,這樣使得Java對象在工作記憶體裡面被JVM順利調用,(當然這是比較抽象的一種解釋)既然如此,大多數JMM的規則在實現的時候,必須使得主存和工作記憶體之間的通信能夠得以保證,而且不能違反記憶體模型本身的結構,這是語言在設計之處必須考慮到的針對記憶體的一種設計方法。這裡需要知道的一點是,這一切的操作在Java語言裡面都是依靠Java語言自身來操作的,因為Java針對開發人員而言,記憶體的管理在不需要手動操作的情況下本身存在記憶體的管理策略,這也是Java自己進行記憶體管理的一種優勢。
[1]原子性(Atomicity):
這一點說明了該模型定義的規則針對原子級別的內容存在獨立的影響,對於模型設計最初,這些規則需要說明的僅僅是最簡單的讀取存儲單元寫入的的一些操作,這種原子級別的包括——實例、靜態變數數組元素,只是在該規則中不包括方法中的局部變數
[2]可見性(Visibility):
在該規則的約束下,定義了一個執行緒在哪種情況下可以訪問另外一個執行緒或者影響另外一個執行緒,從JVM的操作上講包括了從另外一個執行緒的可見區域讀取相關數據以及將數據寫入到另外一個執行緒內。
[3]可排序性(Ordering):
該規則將會約束任何一個違背了規則調用的執行緒在操作過程中的一些順序,排序問題主要圍繞了讀取、寫入和賦值語句有關的序列。
如果在該模型內部使用了一致的同步性的時候,這些屬性中的每一個屬性都遵循比較簡單的原則:和所有同步的記憶體塊一樣,每個同步塊之內的任何變化都具備了原子性以及可見性,和其他同步方法以及同步塊遵循同樣一致的原則,而且在這樣的一個模型內,每個同步塊不能使用同一個鎖,在整個程式的調用過程是按照編寫的程式指定指令運行的。即使某一個同步塊內的處理可能會失效,但是該問題不會影響到其他執行緒的同步問題,也不會引起連環失效。簡單講:當程式運行的時候使用了一致的同步性的時候,每個同步塊有一個獨立的空間以及獨立的同步控制器和鎖機制,然後對外按照JVM的執行指令進行數據的讀寫操作。這種情況使得使用記憶體的過程變得非常嚴謹!
如果不使用同步或者說使用同步不一致(這裡可以理解為異步,但不一定是異步操作),該程式執行的答案就會變得極其複雜。而且在這樣的情況下,該記憶體模型處理的結果比起大多數程式設計師所期望的結果而言就變得十分脆弱,甚至比起JVM提供的實現都脆弱很多。因為這樣所以出現了Java針對該記憶體操作的最簡單的語言規範來進行一定的習慣限制,排除該情況發生的做法在於:
JVM執行緒必須依靠自身來維持對象的可見性以及對象自身應該提供相對應的操作而實現整個記憶體操作的三個特性,而不是僅僅依靠特定的修改對象狀態的執行緒來完成如此複雜的一個流程。
[4]三個特性的解析(針對JMM內部):
原子性(Atomicity):
訪問存儲單元內的任何類型的欄位的值以及對其更新操作的時候,除開long類型和double類型,其他類型的欄位是必須要保證其原子性的,這些欄位也包括為對象服務的引用。此外,該原子性規則擴展可以延伸到基於long和double的另外兩種類型volatile longvolatile doublevolatilejava關鍵字,沒有被volatile聲明的long類型以及double類型的欄位值雖然不保證其JMM中的原子性,但是是被允許的。針對non-long/non-double的欄位在表達式中使用的時候,JMM的原子性有這樣一種規則:如果你獲得或者初始化該值或某一些值的時候,這些值是由其他執行緒寫入,而且不是從兩個或者多個執行緒產生的數據在同一時間戳混合寫入的時候,該欄位的原子性JVM內部是必須得到保證的。也就是說JMM在定義JVM原子性的時候,只要在該規則不違反的條件下,JVM本身不去理睬該數據的值是來自於什麼執行緒,因為這樣使得Java語言在並行運算的設計的過程中針對多執行緒的原子性設計變得極其簡單,而且即使開發人員沒有考慮到最終的程式也沒有太大的影響。再次解釋一下:這裡的原子性指的是原子級別的操作,比如最小的一塊記憶體的讀寫操作,可以理解為Java語言最終編譯過後最接近記憶體的最底層的操作單元,這種讀寫操作的數據單元不是變數的值,而是本機碼,也就是前邊在講《Java基礎知識》中提到的由運行器解釋的時候生成的Native Code
可見性(Visibility):
當一個執行緒需要修改另外執行緒的可見單元的時候必須遵循以下原則:
  • 一個寫入執行緒釋放的同步鎖和緊隨其後進行讀取的讀執行緒的同步鎖是同一個
    從本質上講,釋放鎖操作強迫它的隸屬執行緒【釋放鎖的執行緒】從工作記憶體中的寫入快取裡面刷新(專業上講這裡不應該是刷新,可以理解為提供)數據(flush操作),然後獲取鎖操作使得另外一個執行緒【獲得鎖的執行緒】直接讀取前一個執行緒可訪問域(也就是可見區域)的欄位的值。因為該鎖內部提供了一個同步方法或者同步塊,該同步內容具有執行緒排他性,這樣就使得上邊兩個操作只能針對單一執行緒在同步內容內部進行操作,這樣就使得所有操作該內容的單一執行緒具有該同步內容(加鎖的同步方法或者同步塊)內的執行緒排他性,這種情況的交替也可以理解為具有“短暫記憶效應”。
    這裡需要理解的是同步雙重含義:使用鎖機制允許基於高層同步協定進行處理操作,這是最基本的同步;同時系統記憶體(很多時候這裡是指基於機器指令的底層存儲關卡memory barrier,前邊提到過)在處理同步的時候能夠跨執行緒操作,使得執行緒和執行緒之間的數據是同步的。這樣的機制也折射出一點,並行編程相對於順序編程而言,更加類似於分散式編程。後一種同步可以作為JMM機制中的方法在一個執行緒中運行的效果展示,注意這裡不是多個執行緒運行的效果展示,因為它反應了該執行緒願意傳送或者接受的雙重操作,並且使得它自己的可見區域可以提供給其他執行緒運行或者更新,從這個角度來看,使用鎖和訊息傳遞可以視為相互之間的變數同步,因為相對其他執行緒而言,它的操作針對其他執行緒也是對等的。
  • 一旦某個欄位被申明為volatile,在任何一個寫入執行緒在工作記憶體中刷新快取的之前需要進行進一步的記憶體操作,也就是說針對這樣的欄位進行立即刷新,可以理解為這種volatile不會出現一般變數的快取操作,而讀取執行緒每次必須根據前一個執行緒的可見域裡面重新讀取該變數的值,而不是直接讀取。
  • 當某個執行緒第一次去訪問某個對象的域的時候,它要么初始化該對象的值,要么從其他寫入執行緒可見域裡面去讀取該對象的值;這裡結合上邊理解,在滿足某種條件下,該執行緒對某對象域的值的讀取是直接讀取,有些時候卻需要重新讀取。
    這裡需要小心一點的是,在並發編程裡面,不好的一個實踐就是使用一個合法引用去引用不完全構造的對象,這種情況在從其他寫入執行緒可見域裡面進行數據讀取的時候發生頻率比較高。從編程角度上講,在構造函式裡面開啟一個新的執行緒是有一定的風險的,特別是該類是屬於一個可子類化的類的時候。Thread.start由調用執行緒啟動,然後由獲得該啟動的執行緒釋放鎖具有相同的“短暫記憶效應”,如果一個實現了Runnable接口的超類在子類構造子執行之前調用了Thread(this).start()方法,那么就可能使得該對象線上程方法run執行之前並沒有被完全初始化,這樣就使得一個指向該對象的合法引用去引用了不完全構造的一個對象。同樣的,如果創建一個新的執行緒T並且啟動該執行緒,然後再使用執行緒T來創建對象X,這種情況就不能保證X對象裡面所有的屬性針對執行緒T都是可見的除非是在所有針對X對象的引用中進行同步處理,或者最好的方法是在T執行緒啟動之前創建對象X。
  • 若一個執行緒終止,所有的變數值都必須從工作記憶體中刷到主存,比如,如果一個同步執行緒因為另一個使用Thread.join方法的執行緒而終止,那么該執行緒的可見域針對那個執行緒而言其發生的改變以及產生的一些影響是需要保證可知道的。
注意:如果在同一個執行緒裡面通過方法調用去傳一個對象的引用是絕對不會出現上邊提及到的可見性問題的。JMM保證所有上邊的規定以及關於記憶體可見性特性的描述——一個特殊的更新、一個特定欄位的修改都是某個執行緒針對其他執行緒的一個“可見性”的概念,最終它發生的場所在記憶體模型中Java執行緒和執行緒之間,至於這個發生時間可以是一個任意長的時間,但是最終會發生,也就是說,Java記憶體模型中的可見性的特性主要是針對執行緒和執行緒之間使用記憶體的一種規則和約定,該約定由JMM定義。
不僅僅如此,該模型還允許不同步的情況下可見性特性。比如針對一個執行緒提供一個對象或者欄位訪問域的原始值進行操作,而針對另外一個執行緒提供一個對象或者欄位刷新過後的值進行操作。同樣也有可能針對一個執行緒讀取一個原始的值以及引用對象的對象內容,針對另外一個執行緒讀取一個刷新過後的值或者刷新過後的引用。
儘管如此,上邊的可見性特性分析的一些特徵在跨執行緒操作的時候是有可能失敗的,而且不能夠避免這些故障發生。這是一個不爭的事實,使用同步多執行緒的代碼並不能絕對保證執行緒安全的行為,只是允許某種規則對其操作進行一定的限制,但是在最新的JVM實現以及最新的Java平台中,即使是多個處理器,通過一些工具進行可見性的測試發現其實是很少發生故障的。跨執行緒共享CPU的共享快取的使用,其缺陷就在於影響了編譯器的最佳化操作,這也體現了強有力的快取一致性使得硬體的價值有所提升,因為它們之間的關係線上程與執行緒之間的複雜度變得更高。這種方式使得可見度的自由測試顯得更加不切實際,因為這些錯誤的發生極為罕見,或者說在平台上我們開發過程中根本碰不到。在並行程開發中,不使用同步導致失敗的原因也不僅僅是對可見度的不良把握導致的,導致其程式失敗的原因是多方面的,包括快取一致性、記憶體一致性問題等。
可排序性(Ordering):
可排序規則線上程與執行緒之間主要有下邊兩點:
  • 從操作執行緒的角度看來,如果所有的指令執行都是按照普通順序進行,那么對於一個順序運行的程式而言,可排序性也是順序的
  • 從其他操作執行緒的角度看來,排序性如同在這個執行緒中運行在非同步方法中的一個“間諜”,所以任何事情都有可能發生。唯一有用的限制是同步方法和同步塊的相對排序,就像操作volatile欄位一樣,總是保留下來使用
【*:如何理解這裡“間諜”的意思,可以這樣理解,排序規則在本執行緒裡面遵循了第一條法則,但是對其他執行緒而言,某個執行緒自身的排序特性可能使得它不定地訪問執行執行緒的可見域,而使得該執行緒對本身在執行的執行緒產生一定的影響。舉個例子,A執行緒需要做三件事情分別是A1、A2、A3,而B是另外一個執行緒具有操作B1、B2,如果把參考定位到B執行緒,那么對A執行緒而言,B的操作B1、B2有可能隨時會訪問到A的可見區域,比如A有一個可見區域a,A1就是把a修改稱為1,但是B執行緒在A執行緒調用了A1過後,卻訪問了a並且使用B1或者B2操作使得a發生了改變,變成了2,那么當A按照排序性進行A2操作讀取到a的值的時候,讀取到的是2而不是1,這樣就使得程式最初設計的時候A執行緒的初衷發生了改變,就是排序被打亂了,那么B執行緒對A執行緒而言,其身份就是“間諜”,而且需要注意到一點,B執行緒的這些操作不會和A之間存在等待關係,那么B執行緒的這些操作就是異步操作,所以針對執行執行緒A而言,B的身份就是“非同步方法中的‘間諜’。】
同樣的,這僅僅是一個最低限度的保障性質,在任何給定的程式或者平台,開發中有可能發現更加嚴格的排序,但是開發人員在設計程式的時候不能依賴這種排序,如果依賴它們會發現測試難度會成指數級遞增,而且在複合規定的時候會因為不同的特性使得JVM的實現因為不符合設計初衷而失敗。
注意:第一點在JLS(Java Language Specification)的所有討論中也是被採用的,例如算數表達式一般情況都是從上到下、從左到右的順序,但是這一點需要理解的是,從其他操作執行緒的角度看來這一點又具有不確定性,對執行緒內部而言,其記憶體模型本身是存在排序性的。【*:這裡討論的排序是最底層的記憶體裡面執行的時候的NativeCode的排序,不是說按照順序執行的Java代碼具有的有序性質,本文主要分析的是JVM的記憶體模型,所以希望讀者明白這裡指代的討論單元是記憶體區。】

原始JMM缺陷:

JMM最初設計的時候存在一定的缺陷,這種缺陷雖然現有的JVM平台已經修復,但是這裡不得不提及,也是為了讀者更加了解JMM的設計思路,這一個小節的概念可能會牽涉到很多更加深入的知識,如果讀者不能讀懂沒有關係先看了文章後邊的章節再返回來看也可以。
1)問題1:不可變對象不是不可變的
學過Java的朋友都應該知道Java中的不可變對象,這一點在本文最後講解String類的時候也會提及,而JMM最初設計的時候,這個問題一直都存在,就是:不可變對象似乎可以改變它們的值(這種對象的不可變指通過使用final關鍵字來得到保證),(Publis Service Reminder:讓一個對象的所有欄位都為final並不一定使得這個對象不可變——所有類型還必須是原始類型而不能是對象的引用。而不可變對象被認為不要求同步的。但是,因為在將記憶體寫方面的更改從一個執行緒傳播到另外一個執行緒的時候存在潛在的延遲,這樣就使得有可能存在一種競態條件,即允許一個執行緒首先看到不可變對象的一個值,一段時間之後看到的是一個不同的值。這種情況以前怎么發生的呢?在JDK 1.4中的String實現里,這兒基本有三個重要的決定性欄位:對字元數組的引用、長度和描述字元串的開始數組的偏移量。String就是以這樣的方式在JDK 1.4中實現的,而不是只有字元數組,因此字元數組可以在多個String和StringBuffer對象之間共享,而不需要在每次創建一個String的時候都拷貝到一個新的字元數組裡。假設有下邊的代碼:
String s1 = "/usr/tmp";
String s2 = s1.substring(4); // "/tmp"
這種情況下,字元串s2將具有大小為4的長度和偏移量,但是它將和s1共享“/usr/tmp”裡面的同一字元數組,在String構造函式運行之前,Object的構造函式將用它們默認的值初始化所有的欄位,包括決定性的長度和偏移欄位。當String構造函式運行的時候,字元串長度和偏移量被設定成所需要的值。但是在舊的記憶體模型中,因為缺乏同步,有可能另一個執行緒會臨時地看到偏移量欄位具有初始默認值0,而後又看到正確的值4,結果是s2的值從“/usr”變成了“/tmp”,這並不是我們真正的初衷,這個問題就是原始JMM的第一個缺陷所在,因為在原始JMM模型裡面這是合理而且合法的,JDK 1.4以下的版本都允許這樣做。
2)問題2:重新排序的易失性和非易失性存儲
另一個主要領域是與volatile欄位的記憶體操作重新排序有關,這個領域中現有的JMM引起了一些比較混亂的結果。現有的JMM表明易失性的讀和寫是直接和主存打交道的,這樣避免了把值存儲到暫存器或者繞過處理器特定的快取,這使得多個執行緒一般能看見一個給定變數最新的值。可是,結果是這種volatile定義並沒有最初想像中那樣如願以償,並且導致了volatile的重大混亂。為了在缺乏同步的情況下提供較好的性能,編譯器、運行時和快取通常是允許進行記憶體的重新排序操作的,只要當前執行的執行緒分辨不出它們的區別。(這就是within-thread as-if-serial semantics[執行緒內似乎是串列]的解釋)但是,易失性的讀和寫是完全跨執行緒安排的,編譯器或快取不能在彼此之間重新排序易失性的讀和寫。遺憾的是,通過參考普通變數的讀寫,JMM允許易失性的讀和寫被重排序,這樣以為著開發人員不能使用易失性標誌作為操作已經完成的標誌。比如:
Map configOptions;
char[] configText;
volatile boolean initialized = false;
// 執行緒1
configOptions = new HashMap();
configText = readConfigFile(filename);
processConfigOptions(configText,configOptions);
initialized = true;
// 執行緒2
while(!initialized)
sleep();
這裡的思想是使用易失性變數initialized擔任守衛來表明一套別的操作已經完成了,這是一個很好的思想,但是不能在JMM下工作,因為舊的JMM允許非易失性的寫(比如寫到configOptions欄位,以及寫到由configOptions引用Map的欄位中)與易失性的寫一起重新排序,因此另外一個執行緒可能會看到initialized為true,但是對於configOptions欄位或它所引用的對象還沒有一個一致的或者說當前的針對記憶體的視圖變數,volatile的舊語義只承諾在讀和寫的變數的可見性,而不承諾其他變數,雖然這種方法更加有效的實現,但是結果會和我們設計之初大相逕庭。

堆和棧

記憶體管理Java語言中是JVM自動操作的,當JVM發現某些對象不再需要的時候,就會對該對象占用的記憶體進行重分配(釋放)操作,而且使得分配出來的記憶體能夠提供給所需要的對象。在一些程式語言裡面,記憶體管理是一個程式的職責,但是書寫過C++的程式設計師很清楚,如果該程式需要自己來書寫很有可能引起很嚴重的錯誤或者說不可預料的程式行為,最終大部分開發時間都花在了調試這種程式以及修復相關錯誤上。一般情況下在Java程式開發過程把手動記憶體管理稱為顯示記憶體管理,而顯示記憶體管理經常發生的一個情況就是引用懸掛——也就是說有可能在重新分配過程釋放掉了一個被某個對象引用正在使用的記憶體空間,釋放掉該空間過後,該引用就處於懸掛狀態。如果這個被懸掛引用指向的對象試圖進行原來對象(因為這個時候該對象有可能已經不存在了)進行操作的時候,由於該對象本身的記憶體空間已經被手動釋放掉了,這個結果是不可預知的。顯示記憶體管理另外一個常見的情況是記憶體泄漏,當某些引用不再引用該記憶體對象的時候,而該對象原本占用的記憶體並沒有被釋放,這種情況簡言為記憶體泄漏。比如,如果針對某個鍊表進行了記憶體分配,而因為手動分配不當,僅僅讓引用指向了某個元素所處的記憶體空間,這樣就使得其他鍊表中的元素不能再被引用而且使得這些元素所處的記憶體讓應用程式處於不可達狀態而且這些對象所占有的記憶體也不能夠被再使用,這個時候就發生了記憶體泄漏。而這種情況一旦在程式中發生,就會一直消耗系統的可用記憶體直到可用記憶體耗盡,而針對計算機而言記憶體泄漏的嚴重程度大了會使得本來正常運行的程式直接因為記憶體不足而中斷,並不是Java程式裡面出現Exception那么輕量級。
在以前的編程過程中,手動記憶體管理帶了電腦程式不可避免的錯誤,而且這種錯誤對電腦程式是毀滅性的,所以記憶體管理就成為了一個很重要的話題,但是針對大多數純面向對象語言而言,比如Java,提供了語言本身具有的記憶體特性:自動化記憶體管理,這種語言提供了一個程式垃圾回收器(Garbage Collector[GC]),自動記憶體管理提供了一個抽象的接口以及更加可靠的代碼使得記憶體能夠在程式裡面進行合理的分配。最常見的情況就是垃圾回收器避免了懸掛引用的問題,因為一旦這些對象沒有被任何引用“可達”的時候,也就是這些對象在JVM記憶體池裡面成為了不可引用對象,該垃圾回收器會直接回收掉這些對象占用的記憶體,當然這些對象必須滿足垃圾回收器回收的某些對象規則,而垃圾回收器在回收的時候會自動釋放掉這些記憶體。不僅僅如此,垃圾回收器同樣會解決記憶體泄漏問題。

java對象的記憶體分配

(1) 暫存器(register)。這是最快的保存區域,這是主要由於它位於處理器內部。然而,暫存器的數量十分有限,所以暫存器是需要由編譯器分配的。我們對此沒有直接的控制權,也不可能在自己的程式里找到暫存器存在的任何蹤跡。
(2) 堆疊(stack)。位於通用RAM(隨機訪問存儲器)中。可通過它的“堆疊指針” 獲得處理的直接支持。堆疊指針若向下移,會創建新的記憶體;若向上移,則會釋放那些記憶體。這是一種特別快、特別有效的數據保存方式,僅次於暫存器。創建程式時,Java編譯器必須準確地知道堆疊內保存的所有數據的“長度”以及“存在時間” 。這是由於它必須生成相應的代碼,以便向上和向下移動指針。這一限制無疑影響了程式的靈活性,所以儘管有些Java 數據要保存在堆疊里— — 特別是對象句柄(也稱對象的引用),但Java對象並不放到其中。
(3) 堆(heap)。一種通用性的記憶體池(也在RAM區域),其中保存了Java對象。和堆疊不同的是,“記憶體堆”或“堆”(Heap )最吸引人的地方在於編譯器不必知道要從堆里分配多少存儲空間,也不必知道存儲的數據要在堆里停留多長的時間。因此,用堆保存數據時會得到更大的靈活性。要求創建一個對象時,只需用new 命令編制相關的代碼即可。執行這些代碼時,會在堆里自動進行數據的保存。當然,為達到這種靈活性,必然會付出一定的代價。在堆里分配存儲空間時會花掉更長的時間!
(4) 靜態存儲(static storage)。這兒的“靜態”(Static)是指“位於固定位置”(儘管也在RAM 里)。程式運行期間,靜態存儲的數據將隨時等候調用。可用static關鍵字指出一個對象的特定元素是靜態的。但Java 對象本身永遠都不會置入靜態存儲空間。
(5) 常數存儲(constant storage)。常數值通常直接置於程式代碼內部。這樣做是安全的,因為它們永遠都不會改變。
(6) 非RAM 存儲(non-storage-RAM)。若數據完全獨立於一個程式之外,則程式不運行時仍可存在,並在程式的控制範圍之外。其中兩個最主要的例子便是“ 流式對象”和“固定對象” 。對於流式對象,對象會變成位元組流,通常會發給另一台機器。而對於固定對象,對象保存在磁碟中。即使程式中止運行,它們仍可保持自己的狀態不變。對於這些類型的數據存儲,一個特別有用的技巧就是它們能存在於其他媒體中。一旦需要,甚至能將它們恢復成普通的、基於RAM的對象。Java 1.1提供了對輕量級持久化(Lightweight persistence)的支持。未來的版本甚至可能提供更完整的方案。

相關詞條

熱門詞條

聯絡我們