NO IMAGE

轉載於 作者:BangQ

http://www.cnblogs.com/BangQ/



本文章節:

1.JMM簡介

2.堆和棧
3.本機記憶體
4.防止記憶體洩漏
 
1.JMM簡介

  i.記憶體模型概述
  Java平臺自動整合了執行緒以及多處理器技術,這種整合程度比Java以前誕生的計算機語言要厲害很多,該語言針對多種異構平臺的平臺獨立性而使用的多執行緒技術支援也是具有開拓性的一面,有時候在開發Java同步和執行緒安全要求很嚴格的程式時,往往容易混淆的一個概念就是記憶體模型。究竟什麼是記憶體模型?記憶體模型描述了程式中各個變數(例項域、靜態域和陣列元素)之間的關係,以及在實際計算機系統中將變數儲存到記憶體和從記憶體中取出變數這樣的底層細節,物件最終是儲存在記憶體裡面的,這點沒有錯,但是編譯器、執行庫、處理器或者系統快取可以有特權在變數指定記憶體位置儲存或者取出變數的值。【JMM】(Java
Memory Model的縮寫)允許編譯器和快取以資料在處理器特定的快取(或暫存器)和主存之間移動的次序擁有重要的特權,除非程式設計師使用了final或synchronized明確請求了某些可見性的保證。
  1)JSR133:
  在Java語言規範裡面指出了JMM是一個比較開拓性的嘗試,這種嘗試檢視定義一個一致的、跨平臺的記憶體模型,但是它有一些比較細微而且很重要的缺點。其實Java語言裡面比較容易混淆的關鍵字主要是synchronized和volatile,也因為這樣在開發過程中往往開發者會忽略掉這些規則,這也使得編寫同步程式碼比較困難。

  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模型裡面,通過併發執行緒修改變數值的時候,必須將執行緒變數同步回主存過後,其他執行緒才可能訪問到。
  【*:簡單講,記憶體的可見性使記憶體資源可以共享,當一個執行緒執行的時候它所佔有的記憶體,如果它佔有的記憶體資源是可見的,那麼這時候其他執行緒在一定規則內是可以訪問該記憶體資源的,這種規則是由JMM內部定義的,這種情況下記憶體的該特性稱為其可見性。】
  可排序性提供了記憶體內部的訪問順序,在不同的程式針對不同的記憶體塊進行訪問的時候,其訪問不是無序的,比如有一個記憶體塊,A和B需要訪問的時候,JMM會提供一定的記憶體分配策略有序地分配它們使用的記憶體,而在記憶體的呼叫過程也會變得有序地進行,記憶體的折中性質可以簡單理解為有序性。而在Java多執行緒程式裡面,JMM通過Java關鍵字volatile來保證記憶體的有序訪問。
  ii.JMM結構:
  1)簡單分析:
  Java語言規範中提到過,JVM中存在一個主存區(Main Memory或Java Heap Memory),Java中所有變數都是存在主存中的,對於所有執行緒進行共享,而每個執行緒又存在自己的工作記憶體(Working
Memory),工作記憶體中儲存的是主存中某些變數的拷貝,執行緒對所有變數的操作並非發生在主存區,而是發生在工作記憶體中,而執行緒之間是不能直接相互訪問,變數在程式中的傳遞,是依賴主存來完成的。而在多核處理器下,大部分資料儲存在快取記憶體中,如果快取記憶體不經過記憶體的時候,也是不可見的一種表現。在Java程式中,記憶體本身是比較昂貴的資源,其實不僅僅針對Java應用程式,對作業系統本身而言記憶體也屬於昂貴資源,Java程式在效能開銷過程中有幾個比較典型的可控制的來源。synchronized和volatile關鍵字提供的記憶體中模型的可見性保證程式使用一個特殊的、儲存關卡(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執行緒必須依靠自身來維持物件的可見性以及物件自身應該提供相對應的操作而實現整個記憶體操作的三個特性,而不是僅僅依靠特定的修改物件狀態的執行緒來完成如此複雜的一個流程。
  【*:綜上所屬,JMM在JVM內部實現的結構就變得相對複雜,當然一般的Java初學者可以不用瞭解得這麼深入。】
  [4]三個特性的解析(針對JMM內部):
  原子性(Atomicity):
  訪問儲存單元內的任何型別的欄位的值以及對其更新操作的時候,除開long型別和double型別,其他型別的欄位是必須要保證其原子性的,這些欄位也包括為物件服務的引用。此外,該原子性規則擴充套件可以延伸到基於long和double的另外兩種型別:volatile
long和volatile double(volatile為java關鍵字),沒有被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的記憶體模型,所以希望讀者明白這裡指代的討論單元是記憶體區。】
  iii.原始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的舊語義只承諾在讀和寫的變數的可見性,而不承諾其他變數,雖然這種方法更加有效的實現,但是結果會和我們設計之初大相徑庭。
 
2.堆和棧
  i.Java記憶體管理簡介:
  記憶體管理在Java語言中是JVM自動操作的,當JVM發現某些物件不再需要的時候,就會對該物件佔用的記憶體進行重分配(釋放)操作,而且使得分配出來的記憶體能夠提供給所需要的物件。在一些程式語言裡面,記憶體管理是一個程式的職責,但是書寫過C++的程式設計師很清楚,如果該程式需要自己來書寫很有可能引起很嚴重的錯誤或者說不可預料的程式行為,最終大部分開發時間都花在了除錯這種程式以及修複相關錯誤上。一般情況下在Java程式開發過程把手動記憶體管理稱為顯示記憶體管理,而顯示記憶體管理經常發生的一個情況就是引用懸掛——也就是說有可能在重新分配過程釋放掉了一個被某個物件引用正在使用的記憶體空間,釋放掉該空間過後,該引用就處於懸掛狀態。如果這個被懸掛引用指向的物件試圖進行原來物件(因為這個時候該物件有可能已經不存在了)進行操作的時候,由於該物件本身的記憶體空間已經被手動釋放掉了,這個結果是不可預知的。顯示記憶體管理另外一個常見的情況是記憶體洩漏,當某些引用不再引用該記憶體物件的時候,而該物件原本佔用的記憶體並沒有被釋放,這種情況簡言為記憶體洩漏。比如,如果針對某個連結串列進行了記憶體分配,而因為手動分配不當,僅僅讓引用指向了某個元素所處的記憶體空間,這樣就使得其他連結串列中的元素不能再被引用而且使得這些元素所處的記憶體讓應用程式處於不可達狀態而且這些物件所佔有的記憶體也不能夠被再使用,這個時候就發生了記憶體洩漏。而這種情況一旦在程式中發生,就會一直消耗系統的可用記憶體直到可用記憶體耗盡,而針對計算機而言記憶體洩漏的嚴重程度大了會使得本來正常執行的程式直接因為記憶體不足而中斷,並不是Java程式裡面出現Exception那麼輕量級。
  在以前的程式設計過程中,手動記憶體管理帶了計算機程式不可避免的錯誤,而且這種錯誤對計算機程式是毀滅性的,所以記憶體管理就成為了一個很重要的話題,但是針對大多數純面嚮物件語言而言,比如Java,提供了語言本身具有的記憶體特性:自動化記憶體管理,這種語言提供了一個程式垃圾回收器(Garbage
Collector[GC]),自動記憶體管理提供了一個抽象的介面以及更加可靠的程式碼使得記憶體能夠在程式裡面進行合理的分配。最常見的情況就是垃圾回收器避免了懸掛引用的問題,因為一旦這些物件沒有被任何引用“可達”的時候,也就是這些物件在JVM的記憶體池裡面成為了不可引用物件,該垃圾回收器會直接回收掉這些物件佔用的記憶體,當然這些物件必須滿足垃圾回收器回收的某些物件規則,而垃圾回收器在回收的時候會自動釋放掉這些記憶體。不僅僅如此,垃圾回收器同樣會解決記憶體洩漏問題。
  ii.詳解堆和棧[圖片以及部分內容來自《Inside JVM》]:
  1)通用簡介
  [編譯原理]學過編譯原理的人都明白,程式執行時有三種記憶體分配策略:靜態的、棧式的、堆式的
  靜態儲存——是指在編譯時就能夠確定每個資料目標在執行時的儲存空間需求,因而在編譯時就可以給它們分配固定的記憶體空間。這種分配策略要求程式程式碼中不允許有可變資料結構的存在,也不允許有巢狀或者遞迴的結構出現,因為它們都會導致編譯程式無法計算準確的儲存空間。
  棧式儲存——該分配可成為動態儲存分配,是由一個類似於堆疊的執行棧來實現的,和靜態儲存的分配方式相反,在棧式儲存方案中,程式對資料區的需求在編譯時是完全未知的,只有到了執行的時候才能知道,但是規定在執行中進入一個程式模組的時候,必須知道該程式模組所需要的資料區的大小才能分配其記憶體。和我們在資料結構中所熟知的棧一樣,棧式儲存分配按照先進後出的原則進行分配。
  堆式儲存——堆式儲存分配則專門負責在編譯時或執行時模組入口處都無法確定儲存要求的資料結構的記憶體分配,比如可變長度串和物件例項,堆由大片的可利用塊或空閒塊組成,堆中的記憶體可以按照任意順序分配和釋放。
  [C++語言]對比C++語言裡面,程式佔用的記憶體分為下邊幾個部分:
  [1]棧區(Stack):由編譯器自動分配釋放,存放函式的引數值,區域性變數的值等。其操作方式類似於資料結構中的棧。我們在程式中定義的區域性變數就是存放在棧裡,當區域性變數的生命週期結束的時候,它所佔的記憶體會被自動釋放。
  [2]堆區(Heap):一般由程式設計師分配和釋放,若程式設計師不釋放,程式結束時可能由OS回收。注意它與資料結構中的堆是兩回事,分配方式倒是類似於連結串列。我們在程式中使用c++中new或者c中的malloc申請的一塊記憶體,就是在heap上申請的,在使用完畢後,是需要我們自己動手釋放的,否則就會產生“記憶體洩露”的問題。
  [3]全域性區(靜態區)(Static):全域性變數和靜態變數的儲存是放在一塊的,初始化的全域性變數和靜態變數在一塊區域,未初始化的全域性變數和未初始化的靜態變數在相鄰的另一塊區域。程式結束後由系統釋放。
  [4]文字常量區:常量字串就是放在這裡的,程式結束後由系統釋放。在Java中對應有一個字串常量池。
  [5]程式程式碼區:存放函式體的二進位制程式碼
  2)JVM結構【堆、棧解析】:
  在Java虛擬機器規範中,一個虛擬機器例項的行為主要描述為:子系統、記憶體區域、資料型別和指令,這些元件在描述了抽象的JVM內部的一個抽象結構。與其說這些組成部分的目的是進行JVM內部結構的一種支配,更多的是提供一種嚴格定義實現的外部行為,該規範定義了這些抽象組成部分以及相互作用的任何Java虛擬機器執行所需要的行為。下圖描述了JVM內部的一個結構,其中主要包括主要的子系統、記憶體區域,如同以前在《Java基礎知識》中描述的:Java虛擬機器有一個類載入器作為JVM的子系統,類載入器針對Class進行檢測以鑑定完全合格的類介面,而JVM內部也有一個執行引擎:

  當JVM執行一個程式的時候,它的記憶體需要用來儲存很多內容,包括位元組碼、以及從類檔案中提取出來的一些附加資訊、以及程式中例項化的物件、方法引數、返回值、區域性變數以及計算的中間結果。JVM的記憶體組織需要在不同的執行時資料區進行以上的幾個操作,下邊針對上圖裡面出現的幾個執行時資料區進行詳細解析:一些執行時資料區共享了所有應用程式執行緒和其他特有的單個執行緒,每個JVM例項有一個方法區和一個記憶體堆,這些是共同在虛擬機器內執行的執行緒。在Java程式裡面,每個新的執行緒啟動過後,它就會被JVM在內部分配自己的PC暫存器[PC
registers](程式計數器器)和Java堆疊(Java stacks)。若該執行緒正在執行一個非本地Java方法,在PC暫存器的值指示下一條指令執行,該執行緒在Java記憶體棧中儲存了非本地Java方法呼叫狀態,其狀態包括區域性變數、被呼叫的引數、它的返回值、以及中間計算結果。而本地方法呼叫的狀態則是儲存在獨立的本地方法記憶體棧裡面(native
method stacks),這種情況下使得這些本地方法和其他記憶體執行時資料區的內容儘可能保證和其他記憶體執行時資料區獨立,而且該方法的呼叫更靠近作業系統,這些方法執行的位元組碼有可能根據作業系統環境的不同使得其編譯出來的本地位元組碼的結構也有一定的差異。JVM中的記憶體棧是一個棧幀的組合,一個棧幀包含了某個Java方法呼叫的狀態,當某個執行緒呼叫方法的時候,JVM就會將一個新的幀壓入到Java記憶體棧,當方法呼叫完成過後,JVM將會從記憶體棧中移除該棧幀。JVM裡面不存在一個可以存放中間計算資料結果值的暫存器,其內部指令集使用Java棧空間來儲存中間計算的資料結果值,這種做法的設計是為了保持Java虛擬機器的指令集緊湊,使得與暫存器原理能夠緊密結合並且進行操作。

  1)方法區(Method Area)
  在JVM例項中,對裝載的型別資訊是儲存在一個邏輯方法記憶體區中,當Java虛擬機器載入了一個型別的時候,它會跟著這個Class的型別去路徑裡面查詢對應的Class檔案,類載入器讀取類檔案(線性二進位制資料),然後將該檔案傳遞給Java虛擬機器,JVM從二進位制資料中提取資訊並且將這些資訊儲存在方法區,而類中宣告(靜態)變數就是來自於方法區中儲存的資訊。在JVM裡面用什麼樣的方式儲存該資訊是由JVM設計的時候決定的,例如:當資料進入方法的時候,多類檔案位元組的儲存量以Big-Endian(第一次最重要的位元組)的順序儲存,儘管如此,一個虛擬機器可以用任何方式針對這些資料進行儲存操作,若它儲存在一個Little-Endian處理器上,設計的時候就有可能將多檔案位元組的值按照Little-Endian順尋儲存。
  ——【$Big-Endian和Little-Endian】——
  程式儲存資料過程中,如果資料是跨越多個位元組物件就必須有一種約定:
  • 它的地址是多少:對於跨越多個位元組的物件,一般它所佔的位元組都是連續的,它的地址等於它所佔位元組最低地址,這種情況連結串列可能儲存的僅僅是表頭
  • 它的位元組在記憶體中是如何組織的
  比如:int x,它的地址為0x100,那麼它佔據了記憶體中的0x100、0x101、0x102、0x103四個位元組,所以一般情況我們覺得int是4個位元組。上邊只是記憶體組織的一種情況,多位元組物件在記憶體中的組織有兩種約定,還有一種情況:若一個整數為W位,它的表示如下:
  每一位表示為:[Xw-1,Xw-2,…,X1,X0]
  它的最高有效位元組MSB(Most Significant Byte)為:[Xw-1,Xw-2,…,Xw-8]
  最低有效位元組LSB(Least Significant Byte)為:[X7,X6,…,X0]
  其餘位元組則位於LSB和MSB之間

  LSB和MSB誰位於記憶體的最低地址,即代表了該物件的地址,這樣就引出了Big-Endian和Little-Endian的問題,如果LSB在MSB前,LSB是最低地址,則該機器是小端,反之則是大端。DES(Digital Equipment Corporation,現在是Compaq公司的一部分)和Intel機器(x86平臺)一般採用小端,IBM、Motorola(Power PC)、Sun的機器一般採用大端。當然這種不能代表所有情況,有的CPU既能工作於小端、又可以工作於大端,比如ARM、Alpha、摩托羅拉的PowerPC,這些情況根據具體的處理器型號有所不同。但是大部分作業系統(Windows、FreeBSD、Linux)一般都是Little
Endian的,少部分系統(Mac OS)是Big Endian的,所以用什麼方式儲存還得依賴宿主作業系統環境。

  由上圖可以看到,對映訪問(“寫32位地址的0”)主要是由暫存器到記憶體、由記憶體到暫存器的一種資料對映方式,Big-Endian在上圖可以看出的原子記憶體單位(Atomic Unit)在系統記憶體中的增長方向為從左到右,而Little-Endian的地址增長方向為從右到左。舉個例子:

  若要儲存資料0x0A0B0C0D:
  Big-Endian:
  以8位為一個儲存單位,其儲存的地址增長為:

  上圖中可以看出MSB的值儲存了0x0A,這種情況下資料的高位是從記憶體的低地址開始儲存的,然後從左到右開始增長,第二位0x0B就是儲存在第二位的,如果是按照16位為一個儲存單位,其儲存方式又為:

  則可以看到Big-Endian的對映地址方式為:

 

  MSB:在計算機中,最高有效位(MSB)是指位值的儲存位置為轉換為二進位制資料後的最大值,MSB有時候在Big-Endian的架構中稱為最左最大資料位,這種情況下再往左邊的記憶體位則不是資料位了,而是有效位數位置的最高符號位,不僅僅如此,MSB也可以對應一個二進位制符號位的符號位補碼標記:“1”的含義為負,“0”的含義為正。最高位代表了“最重要位元組”,也就是說當某些多位元組資料擁有了最大值的時候它就是儲存的時候最高位資料的位元組對應的記憶體位置:

  Little-Endian:

  與Big-Endian相對的就是Little-Endian的儲存方式,同樣按照8位為一個儲存單位上邊的資料0x0A0B0C0D儲存格式為:

  可以看到LSB的值儲存的0x0D,也就是資料的最低位是從記憶體的低地址開始儲存的,它的高位是從右到左的順序逐漸增加記憶體分配空間進行儲存的,如果按照十六位為儲存單位儲存格式為:

  從上圖可以看到最低的16位的儲存單位裡面儲存的值為0x0C0D,接著才是0x0A0B,這樣就可以看到按照資料從高位到低位在記憶體中儲存的時候是從右到左進行遞增儲存的,實際上可以從寫記憶體的順序來理解,實際上資料儲存在記憶體中無非在使用的時候是寫記憶體和讀記憶體,針對LSB的方式最好的書面解釋就是向左增加來看待,如果真正在進行記憶體讀寫的時候使用這樣的順序,其意義就體現出來了:

  按照這種讀寫格式,0x0D儲存在最低記憶體地址,而從右往左的增長就可以看到LSB儲存的資料為0x0D,和初衷吻合,則十六位的儲存就可以按照下邊的格式來解釋:

  實際上從上邊的儲存還會考慮到另外一個問題,如果按照這種方式從右往左的方式進行儲存,如果是遇到Unicode文字就和從左到右的語言顯示方式相反。比如一個單詞“XRAY”,使用Little-Endian的方式儲存格式為:

  使用這種方式進行記憶體讀寫的時候就會發現計算機語言和語言本身的順序會有衝突,這種衝突主要是以使用語言的人的習慣有關,而書面化的語言從左到右就可以知道其衝突是不可避免的。我們一般使用語言的閱讀方式都是從左到右,而低端儲存(Little-Endian)的這種記憶體讀寫的方式使得我們最終從計算機裡面讀取字元需要進行倒序,而且考慮另外一個問題,如果是針對中文而言,一個字元是兩個位元組,就會出現整體順序和每一個位的順序會進行兩次倒序操作,這種方式真正在製作處理器的時候也存在一種計算上的衝突,而針對使用文字從左到右進行閱讀的國家而言,從右到左的方式(Big-Endian)則會有這樣的文字衝突,另外一方面,儘管有很多國家使用語言是從右到左,但是僅僅和Big-Endian的方式存在衝突,這些國家畢竟佔少數,所以可以理解的是,為什麼主流的系統都是使用的Little-Endian的方式

  【*:這裡不解釋Middle-Endian的方式以及Mixed-Endian的方式】
  LSB:在計算機中,最低有效位是一個二進位制給予單位的整數,位的位置確定了該資料是一個偶數還是奇數,LSB有時被稱為最右位。在使用具體位二進位制數之內,常見的儲存方式就是每一位儲存1或者0的方式,從0向上到1每一位元逢二進一的儲存方式。LSB的這種特性用來指定單位位,而不是位的數字,而這種方式也有可能產生一定的混亂。

  ——以上是關於Big-Endian和Little-Endian的簡單講解——

  JVM虛擬機器將搜尋和使用型別的一些資訊也儲存在方法區中以方便應用程式載入讀取該資料。設計者在設計過程也考慮到要方便JVM進行Java應用程式的快速執行,而這種取捨主要是為了程式在執行過程中記憶體不足的情況能夠通過一定的取捨去彌補記憶體不足的情況。在JVM內部,所有的執行緒共享相同的方法區,因此,訪問方法區的資料結構必須是執行緒安全的,如果兩個執行緒都試圖去呼叫去找一個名為Lava的類,比如Lava還沒有被載入,只有一個執行緒可以載入該類而另外的執行緒只能夠等待。方法區的大小在分配過程中是不固定的,隨著Java應用程式的執行,JVM可以調整其大小,需要注意一點,方法區的記憶體不需要是連續的,因為方法區記憶體可以分配在記憶體堆中,即使是虛擬機器JVM例項物件自己所在的記憶體堆也是可行的,而在實現過程是允許程式設計師自身來指定方法區的初始化大小的。
  同樣的,因為Java本身的自動記憶體管理,方法區也會被垃圾回收的,Java程式可以通過類擴充套件動態載入器物件,類可以成為“未引用”向垃圾回收器進行申請,如果一個類是“未引用”的,則該類就可能被解除安裝,
  而方法區針對具體的語言特性有幾種資訊是儲存在方法區內的:
  【型別資訊】:
  • 型別的完全限定名(java.lang.String格式)
  • 型別的完全限定名的直接父類的完全限定名(除非這個父類的型別是一個介面或者java.lang.Object)
  • 不論型別是一個類或者介面
  • 型別的修飾符(例如public、abstract、final)
  • 任何一個直接超類介面的完全限定名的列表
  在JVM和類檔名的內部,型別名一般都是完全限定名(java.lang.String)格式,在Java原始檔裡面,完全限定名必須加入包字首,而不是我們在開發過程寫的簡單類名,而在方法上,只要是符合Java語言規範的類的完全限定名都可以,而JVM可能直接進行解析,比如:(java.lang.String)在JVM內部名稱為java/lang/String,這就是我們在異常捕捉的時候經常看到的ClassNotFoundException的異常裡面類資訊的名稱格式。
  除此之外,還必須為每一種載入過的型別在JVM內進行儲存,下邊的資訊不儲存在方法區內,下邊的章節會一一說明
  • 型別常量池
  • 欄位資訊
  • 方法資訊
  • 所有定義在Class內部的(靜態)變數資訊,除開常量
  • 一個ClassLoader的引用
  • Class的引用
  【常量池】
  針對型別載入的型別資訊,JVM將這些儲存在常量池裡,常量池是一個根據型別定義的常量的有序常量集,包括字面量(String、Integer、Float常量)以及符號引用(型別、欄位、方法),整個長量池會被JVM的一個索引引用,如同陣列裡面的元素集合按照索引訪問一樣,JVM針對這些常量池裡面儲存的資訊也是按照索引方式進行。實際上長量池在Java程式的動態連結過程起到了一個至關重要的作用。
  【欄位資訊】
  針對欄位的型別資訊,下邊的資訊是儲存在方法區裡面的:
  • 欄位名
  • 欄位型別
  • 欄位修飾符(public,private,protected,static,final,volatile,transient)
  【方法資訊】
  針對方法資訊,下邊資訊儲存在方法區上:
  • 方法名
  • 方法的返回型別(包括void)
  • 方法引數的型別、數目以及順序
  • 方法修飾符(public,private,protected,static,final,synchronized,native,abstract)
  針對非本地方法,還有些附加方法資訊需要儲存在方法區內:
  • 方法位元組碼
  • 方法中區域性變數區的大小、方法棧幀
  • 異常表
  【類變數】
  類變數在一個類的多個例項之間共享,這些變數直接和類相關,而不是和類的例項相關,(定義過程簡單理解為類裡面定義的static型別的變數),針對類變數,其邏輯部分就是儲存在方法區內的。在JVM使用這些類之前,JVM先要在方法區裡面為定義的non-final變數分配記憶體空間;常量(定義為final)則在JVM內部則不是以同樣的方式來進行儲存的,儘管針對常量而言,一個final的類變數是擁有它自己的常量池,作為常量池裡面的儲存某部分,類常量是儲存在方法區內的,而其邏輯部分則不是按照上邊的類變數的方式來進行記憶體分配的。雖然non-final類變數是作為這些型別宣告中儲存資料的某一部分,final變數儲存為任何使用它型別的一部分的資料格式進行簡單儲存。
  【ClassLoader引用】
  對於每種型別的載入,JVM必須檢測其型別是否符合了JVM的語言規範,對於通過類載入器載入的物件型別,JVM必須儲存對類的引用,而這些針對類載入器的引用是作為了方法區裡面的型別資料部分進行儲存的。
  【類Class的引用】
  JVM在載入了任何一個型別過後會建立一個java.lang.Class的例項,虛擬機器必須通過一定的途徑來引用該型別對應的一個Class的例項,並且將其儲存在方法區內
  【方法表】
  為了提高訪問效率,必須仔細的設計儲存在方法區中的資料資訊結構。除了以上討論的結構,jvm的實現者還新增一些其他的資料結構,如方法表【下邊會說明】。
  2)記憶體棧(Stack):
  當一個新執行緒啟動的時候,JVM會為Java執行緒建立每個執行緒的獨立記憶體棧,如前所言Java的記憶體棧是由棧幀構成,棧幀本身處於遊離狀態,在JVM裡面,棧幀的操作只有兩種:出棧和入棧。正在被執行緒執行的方法一般稱為當前執行緒方法,而該方法的棧幀就稱為當前幀,而在該方法內定義的類稱為當前類,常量池也稱為當前常量池。當執行一個方法如此的時候,JVM保留當前類和當前常量池的跟蹤,當虛擬機器遇到了儲存在棧幀中的資料上的操作指令的時候,它就執行當前幀的操作。當一個執行緒呼叫某個Java方法時,虛擬機器建立並且將一個新幀壓入到記憶體堆疊中,而這個壓入到記憶體棧中的幀成為當前棧幀,當該方法執行的時候,JVM使用記憶體棧來儲存引數、區域性變數、中間計算結果以及其他相關資料。方法在執行過程有可能因為兩種方式而結束:如果一個方法返回完成就屬於方法執行的正常結束,如果在這個過程丟擲異常而結束,可以稱為非正常結束,不論是正常結束還是異常結束,JVM都會彈出或者丟棄該棧幀,則上一幀的方法就成為了當前幀。
  在JVM中,Java執行緒的棧資料是屬於某個執行緒獨有的,其他的執行緒不能夠修改或者通過其他方式來訪問該執行緒的棧幀,正因為如此這種情況不用擔心多執行緒同步訪問Java的區域性變數,當一個執行緒呼叫某個方法的時候,方法的區域性變數是在方法內部進行的Java棧幀的儲存,只有當前執行緒可以訪問該區域性變數,而其他執行緒不能隨便訪問該記憶體棧裡面儲存的資料。記憶體棧內的棧幀資料和方法區以及記憶體堆一樣,Java棧的棧幀不需要分配在連續的堆疊內,或者說它們可能是在堆,或者兩者組合分配,實際資料用於表示Java堆疊和棧幀結構是JVM本身的設計結構決定的,而且在程式設計過程可以允許程式設計師指定一個用於Java堆疊的初始大小以及最大、最小尺寸。
  【概念區分】
  • 記憶體棧:這裡的記憶體棧和物理結構記憶體堆疊有點點區別,是記憶體裡面資料儲存的一種抽象資料結構。從作業系統上講,在程式執行過程對記憶體的使用本身常用的資料結構就是記憶體堆疊,而這裡的記憶體堆疊指代的就是JVM在使用記憶體過程整個記憶體的儲存結構,多指記憶體的物理結構,而Java記憶體棧不是指代的一個物理結構,更多的時候指代的是一個抽象結構,就是符合JVM語言規範的記憶體棧的一個抽象結構。因為實體記憶體堆疊結構和Java記憶體棧的抽象模型結構本身比較相似,所以我們在學習過程就正常把這兩種結構放在一起考慮了,而且二者除了概念上有一點點小的區別,理解成為一種結構對於初學者也未嘗不可,所以實際上也可以覺得二者沒有太大的本質區別。但是在學習的時候最好分清楚記憶體堆疊和Java記憶體棧的一小點細微的差距,前者是物理概念和本身模型,後者是抽象概念和本身模型的一個共同體。而記憶體堆疊更多的說法可以理解為一個記憶體塊,因為記憶體塊可以通過索引和指標進行資料結構的組合,記憶體棧就是記憶體塊針對資料結構的一種表示,而記憶體堆則是記憶體塊的另外一種資料結構的表示,這樣理解更容易區分記憶體棧和記憶體堆疊(記憶體塊)的概念。
  • 棧幀:棧幀是記憶體棧裡面的最小單位,指的是記憶體棧裡面每一個最小記憶體儲存單元,它針對記憶體棧僅僅做了兩個操作:入棧和出棧,一般情況下:所說的堆疊幀和棧幀倒是一個概念,所以在理解上記得加以區分
  • 記憶體堆:這裡的記憶體堆和記憶體棧是相對應的,其實記憶體堆裡面的資料也是儲存在系統記憶體堆疊裡面的,只是它使用了另外一種方式來進行堆裡面記憶體的管理,而本章題目要講到的就是Java語言本身的記憶體堆和記憶體棧,而這兩個概念都是抽象的概念模型,而且是相對的。
  棧幀:棧幀主要包括三個部分:區域性變數、運算元棧幀(操作幀)和幀資料(資料幀)。本地變數和運算元幀的大小取決於需要,這些大小是在編譯時就決定的,並且在每個方法的類檔案資料中進行分配,幀的資料大小則不一樣,它雖然也是在編譯時就決定的但是它的大小和本身程式碼實現有關。當JVM呼叫一個Java方法的時候,它會檢查類的資料來確定在本地變數和操作方法要求的棧大小,它計算該方法所需要的記憶體大小,然後將這些資料分配好記憶體空間壓入到記憶體堆疊中。
  棧幀——區域性變數:區域性變數是以Java棧幀組合成為的一個以零為基的陣列,使用區域性變數的時候使用的實際上是一個包含了0的一個基於索引的陣列結構。int型別、float、引用以及返回值都佔據了一個陣列中的區域性變數的條目,而byte、short、char則在儲存到區域性變數的時候是先轉化成為int再進行操作的,則long和double則是在這樣一個陣列裡面使用了兩個元素的空間大小,在區域性變數裡面儲存基本資料型別的時候使用的就是這樣的結構。舉個例子:
class Example3a{
    public static int runClassMethod(int i,long l,float f,double d,Object o,byte b)
    {
        return 0;
    }
    public int runInstanceMethod(char c,double d,short s,boolean b)
    {
        return 0;
    }
}

  棧幀——操作幀:和區域性變數一樣,操作幀也是一組有組織的陣列的儲存結構,但是和區域性變數不一樣的是這個不是通過陣列的索引訪問的,而是直接進行的入棧和出棧的操作,當操作指令直接壓入了操作棧幀過後,從棧幀裡面出來的資料會直接在出棧的時候被讀取和使用。除了程式計數器以外,操作幀也是可以直接被指令訪問到的,JVM裡面沒有暫存器。處理操作幀的時候Java虛擬機器是基於記憶體棧的而不是基於暫存器的,因為它在操作過程是直接對記憶體棧進行操作而不是針對暫存器進行操作。而JVM內部的指令也可以來源於其他地方比如緊接著操作符以及運算元的位元組碼流或者直接從常量池裡面進行操作。JVM指令其實真正在操作過程的焦點是集中在記憶體棧棧幀的操作幀上的。JVM指令將操作幀作為一個工作空間,有許多指令都是從操作幀裡面出棧讀取的,對指令進行操作過後將操作幀的計算結果重新壓入記憶體堆疊內。比如iadd指令將兩個整數壓入到操作幀裡面,然後將兩個運算元進行相加,相加的時候從記憶體棧裡面讀取兩個運算元的值,然後進行運算,最後將運算結果重新存入到記憶體堆疊裡面。舉個簡單的例子:

begin
iload_0 //將整數型別的區域性變數0壓入到記憶體棧裡面
iload_1 //將整數型別的區域性變數1壓入到記憶體棧裡面
iadd     //將兩個變數出棧讀取,然後進行相加操作,將結果重新壓入棧中
istore_2 //將最終輸出結果放在另外一個區域性變數裡面
end
  綜上所述,就是整個計算過程針對記憶體的一些操作內容,而整體的結構可以用下圖來描述:

  棧幀——資料幀:除了區域性變數和操作幀以外,Java棧幀還包括了資料幀,用於支援常量池、普通的方法返回以及異常丟擲等,這些資料都是儲存在Java記憶體棧幀的資料幀中的。很多JVM的指令集實際上使用的都是常量池裡面的一些條目,一些指令,只是把int、long、float、double或者String從常量池裡面壓入到Java棧幀的操作幀上邊,一些指令使用常量池來管理類或者陣列的例項化操作、欄位的訪問控制、或者方法的呼叫,其他的指令就用來決定常量池條目中記錄的某一特定物件是否某一類或者常量池項中指定的介面。常量池會判斷型別、欄位、方法、類、介面、類欄位以及引用是如何在JVM進行符號化描述,而這個過程由JVM本身進行對應的判斷。這裡就可以理解JVM如何來判斷我們通常說的:“原始變數儲存在記憶體棧上,而引用的物件儲存在記憶體堆上邊。”除了常量池判斷幀資料符號化描述特性以外,這些資料幀必須在JVM正常執行或者異常執行過程輔助它進行處理操作。如果一個方法是正常結束的,JVM必須恢復棧幀呼叫方法的資料幀,而且必須設定PC暫存器指向呼叫方法後邊等待的指令完成該呼叫方法的位置。如果該方法存在返回值,JVM也必須將這個值壓入到操作幀裡面以提供給需要這些資料的方法進行呼叫。不僅僅如此,資料幀也必須提供一個方法呼叫的異常表,當JVM在方法中丟擲異常而非正常結束的時候,該異常表就用來存放異常資訊。

  3)記憶體堆(Heap):
  當一個Java應用程式在執行的時候在程式中建立一個物件或者一個陣列的時候,JVM會針對該物件和陣列分配一個新的記憶體堆空間。但是在JVM例項內部,只存在一個記憶體堆例項,所有的依賴該JVM的Java應用程式都需要共享該堆例項,而Java應用程式本身在執行的時候它自己包含了一個由JVM虛擬機器例項分配的自己的堆空間,而在應用程式啟動的時候,任何一個Java應用程式都會得到JVM分配的堆空間,而且針對每一個Java應用程式,這些執行Java應用程式的堆空間都是相互獨立的。這裡所提及到的共享堆例項是指JVM在初始化執行的時候整體堆空間只有一個,這個是Java語言平臺直接從作業系統上能夠拿到的整體堆空間,所以的依賴該JVM的程式都可以得到這些記憶體空間,但是針對每一個獨立的Java應用程式而言,這些堆空間是相互獨立的,每一個Java應用程式在執行最初都是依靠JVM來進行堆空間的分配的。即使是兩個相同的Java應用程式,一旦在執行的時候處於不同的作業系統程序(一般為java.exe)中,它們各自分配的堆空間都是獨立的,不能相互訪問,只是兩個Java應用程序初始化拿到的堆空間來自JVM的分配,而JVM是從最初的記憶體堆例項裡面分配出來的。在同一個Java應用程式裡面如果出現了不同的執行緒,則是可以共享每一個Java應用程式拿到的記憶體堆空間的,這也是為什麼在開發多執行緒程式的時候,針對同一個Java應用程式必須考慮執行緒安全問題,因為在一個Java程序裡面所有的執行緒是可以共享這個程序拿到的堆空間的資料的。但是Java記憶體堆有一個特性,就是JVM擁有針對新的物件分配記憶體的指令,但是它卻不包含釋放該記憶體空間的指令,當然開發過程可以在Java原始碼中顯示釋放記憶體或者說在JVM位元組碼中進行顯示的記憶體釋放,但是JVM僅僅只是檢測堆空間中是否有引用不可達(不可以引用)的物件,然後將接下來的操作交給垃圾回收器來處理。
  物件表示:
  JVM規範裡面並沒有提及到Java物件如何在堆空間中表示和描述,物件表示可以理解為設計JVM的工程師在最初考慮到物件呼叫以及垃圾回收器針對物件的判斷而獨立的一種Java物件在記憶體中的儲存結構,該結構是由設計最初考慮的。針對一個建立的類例項而言,它內部定義的例項變數以及它的超類以及一些相關的核心資料,是必須通過一定的途徑進行該物件內部儲存以及表示的。當開發過程給定了一個物件引用的時候,JVM必須能夠通過這個引用快速從物件堆空間中去拿到該物件能夠訪問的資料內容。也就是說,堆空間內物件的儲存結構必須為外圍物件引用提供一種可以訪問該物件以及控制該物件的介面使得引用能夠順利地呼叫該物件以及相關操作。因此,針對堆空間的物件,分配的記憶體中往往也包含了一些指向方法區的指標,因為從整體儲存結構上講,方法區似乎儲存了很多原子級別的內容,包括方法區內最原始最單一的一些變數:比如類欄位、欄位資料、型別資料等等。而JVM本身針對堆空間的管理存在兩種設計結構:
  【1】設計一:
  堆空間的設計可以劃分為兩個部分:一個處理池和一個物件池,一個物件的引用可以拿到處理池的一個本地指標,而處理池主要分為兩個部分:一個指向物件池裡面的指標以及一個指向方法區的指標。這種結構的優勢在於JVM在處理物件的時候,更加能夠方便地組合堆碎片以使得所有的資料被更加方便地進行呼叫。當JVM需要將一個物件移動到物件池的時候,它僅僅需要更新該物件的指標到一個新的物件池的記憶體地址中就可以完成了,然後在處理池中針對該物件的內部結構進行相對應的處理工作。不過這樣的方法也會出現一個缺點就是在處理一個物件的時候針對物件的訪問需要提供兩個不同的指標,這一點可能不好理解,其實可以這樣講,真正在物件處理過程存在一個根據時間戳有區別的物件狀態,而物件在移動、更新以及建立的整個過程中,它的處理池裡面總是包含了兩個指標,一個指標是指向物件內容本身,一個指標是指向了方法區,因為一個完整的對外的物件是依靠這兩部分被引用指標引用到的,而我們開發過程是不能夠操作處理池的兩個指標的,只有引用指標我們可以通過外圍程式設計拿到。如果Java是按照這種設計進行物件儲存,這裡的引用指標就是平時提及到的“Java的引用”,只是JVM在引用指標還做了一定的封裝,這種封裝的規則是JVM本身設計的時候做的,它就通過這種結構在外圍進行一次封裝,比如Java引用不具備直接操作記憶體地址的能力就是該封裝的一種限制規則。這種設計的結構圖如下:
  【2】設計二:
  另外一種堆空間設計就是使用物件引用拿到的本地指標,將該指標直接指向繫結好的物件的例項資料,這些資料裡面僅僅包含了一個指向方法區原子級別的資料去拿到該例項相關資料,這種情況下只需要引用一個指標來訪問物件例項資料,但是這樣的情況使得物件的移動以及物件的資料更新變得更加複雜。當JVM需要移動這些資料以及進行堆記憶體碎片的整理的時候,就必須直接更新該物件所有執行時的資料區,這種情況可以用下圖進行表示:
  JVM需要從一個物件引用來獲得該引用能夠引用的物件資料存在多個原因,當一個程式試圖將一個物件的引用轉換成為另外一個型別的時候,JVM就會檢查兩個引用指向的物件是否存在父子類關係,並且檢查兩個引用引用到的物件是否能夠進行型別轉換,而且所有這種型別的轉換必須執行同樣的一個操作:instanceof操作,在上邊兩種情況下,JVM都必須要去分析引用指向的物件內部的資料。當一個程式呼叫了一個例項方法的時候,JVM就必須進行動態繫結操作,它必須選擇呼叫方法的引用型別,是一個基於類的方法呼叫還是一個基於物件的方法呼叫,要做到這一點,它又要獲取該物件的唯一引用才可以。不管物件的實現是使用什麼方式來進行物件描述,都是在針對記憶體中關於該物件的方法表進行操作,因為使用這樣的方式加快了例項針對方法的呼叫,而且在JVM內部實現的時候這樣的機制使得其執行表現比較良好,所以方法表的設計在JVM整體結構中發揮了極其重要的作用。關於方法表的存在與否,在JVM規範裡面沒有嚴格說明,也有可能真正在實現過程只是一個抽象概念,物理層它根本不存在,針對放發表實現對於一個建立的例項而言,它本身具有不太高的記憶體需要求,如果該實現裡面使用了方法表,則物件的方法表應該是可以很快被外圍引用訪問到的。

  有一種辦法就是通過物件引用連線到方法表的時候,如下圖:

  該圖表明,在每個指標指向一個物件的時候,實際上是使用的一個特殊的資料結構,這些特殊的結構包括幾個部分:

  • 一個指向該物件類所有資料的指標
  • 該物件的方法表
  實際上從圖中可以看出,方法表就是一個指標陣列,它的每一個元素包含了一個指標,針對每個物件的方法都可以直接通過該指標在方法區中找到匹配的資料進行相關呼叫,而這些方法表需要包括的內容如下:
  • 方法記憶體堆疊段空間中操作棧的大小以及區域性變數
  • 方法位元組碼
  • 一個方法的異常表
  這些資訊使得JVM足夠針對該方法進行呼叫,在呼叫過程,這種結構也能夠方便子類物件的方法直接通過指標引用到父類的一些方法定義,也就是說指標在記憶體空間之內通過JVM本身的呼叫使得父類的一些方法表也可以同樣的方式被呼叫,當然這種呼叫過程避免不了兩個物件之間的型別檢查,但是這樣的方式就使得繼承的實現變得更加簡單,而且方法表提供的這些資料足夠引用對物件進行帶有任何OO特徵的物件操作。
  另外一種資料在上邊的途中沒有顯示出來,也是從邏輯上講記憶體堆中的物件的真實資料結構——物件的鎖。這一點可能需要關聯到JMM模型中講的進行理解。JVM中的每一個物件都是和一個鎖(互斥)相關聯的,這種結構使得該物件可以很容易支援多執行緒訪問,而且該物件的物件鎖一次只能被一個執行緒訪問。當一個執行緒在執行的時候具有某個物件的鎖的時候,僅僅只有這個執行緒可以訪問該物件的例項變數,其他執行緒如果需要訪問該例項的例項變數就必須等待這個執行緒將它佔有的物件鎖釋放過後才能夠正常訪問,如果一個執行緒請求了一個被其他執行緒佔有的物件鎖,這個請求執行緒也必須等到該鎖被釋放過後才能夠拿到這個物件的物件鎖。一旦這個執行緒擁有了一個物件鎖過後,它自己可以多次向同一個鎖傳送物件的鎖請求,但是如果它要使得被該執行緒鎖住的物件可以被其他鎖訪問到的話就需要同樣的釋放鎖的次數,比如執行緒A請求了物件B的物件鎖三次,那麼A將會一直佔有B物件的物件鎖,直到它將該物件鎖釋放了三次。
  很多物件也可能在整個生命週期都沒有被物件鎖鎖住過,在這樣的情況下物件鎖相關的資料是不需要物件內部實現的,除非有執行緒向該物件請求了物件鎖,否則這個物件就沒有該物件鎖的儲存結構。所以上邊的實現圖可以知道,很多實現不包括指向物件鎖的“鎖資料”,鎖資料的實現必須要等待某個執行緒向該物件傳送了物件鎖請求過後,而且是在第一次鎖請求過後才會被實現。這個結構中,JVM卻能夠間接地通過一些辦法針對物件的鎖進行管理,比如把物件鎖放在基於物件地址的搜尋樹上邊。實現了鎖結構的物件中,每一個Java物件邏輯上都在記憶體中成為了一個等待集,這樣就使得所有的執行緒在鎖結構裡面針對物件內部資料可以獨立操作,等待集就使得每個執行緒能夠獨立於其他執行緒去完成一個共同的設計目標以及程式執行的最終結果,這樣就使得多執行緒的執行緒獨享資料以及執行緒共享資料機制很容易實現。
  不僅僅如此,針對記憶體堆物件還必須存在一個物件的映象,該映象的主要目的是提供給垃圾回收器進行監控操作,垃圾回收器是通過物件的狀態來判斷該物件是否被應用,同樣它需要針對堆內的物件進行監控。而當監控過程垃圾回收器收到物件回收的事件觸發的時候,雖然使用了不同的垃圾回收演算法,不論使用什麼演算法都需要通過獨有的機制來判斷物件目前處於哪種狀態,然後根據物件狀態進行操作。開發過程程式設計師往往不會去仔細分析當一個物件引用設定成為null了過後虛擬機器內部的操作,但實際上Java裡面的引用往往不像我們想像中那麼簡單,Java引用中的虛引用、弱引用就是使得Java引用在顯示提交可回收狀態的情況下對記憶體堆中的物件進行的反向監控,這些引用可以監視到垃圾回收器回收該物件的過程。垃圾回收器本身的實現也是需要記憶體堆中的物件能夠提供相對應的資料的。其實這個位置到底JVM裡面是否使用了完整的Java物件的映象還是使用的一個映象索引我沒有去仔細分析過,總之是在堆結構裡面存在著堆內物件的一個類似拷貝的映象機制,使得垃圾回收器能夠順利回收不再被引用的物件。
  4)記憶體棧和記憶體堆的實現原理探測【該部分為不確定概念】:
  實際上不論是記憶體棧結構、方法區還是記憶體堆結構,歸根到底使用的是作業系統的記憶體,作業系統的記憶體結構可以理解為記憶體塊,常用的抽象方式就是一個記憶體堆疊,而JVM在OS上邊安裝了過後,就在啟動Java程式的時候按照配置檔案裡面的內容向作業系統申請記憶體空間,該記憶體空間會按照JVM內部的方法提供相應的結構調整。
  記憶體棧應該是很容易理解的結構實現,一般情況下,記憶體棧是保持連續的,但是不絕對,記憶體棧申請到的地址實際上很多情況下都是連續的,而每個地址的最小單位是按照計算機位來算的,該計算機位裡面只有兩種狀態1和0,而記憶體棧的使用過程就是典型的類似C++裡面的普通指標結構的使用過程,直接針對指標進行++或者–操作就修改了該指標針對記憶體的偏移量,而這些偏移量就使得該指標可以呼叫不同的記憶體棧中的資料。至於針對記憶體棧傳送的指令就是常見的計算機指令,而這些指令就使得該指標針對記憶體棧的棧幀進行指令傳送,比如傳送操作指令、變數讀取等等,直接就使得記憶體棧的呼叫變得更加簡單,而且棧幀在接受了該資料過後就知道到底針對棧幀內部的哪一個部分進行呼叫,是操作幀、資料幀還是區域性變數。
  記憶體堆實際上在作業系統裡面使用了雙向連結串列的資料結構,雙向連結串列的結構使得即使記憶體堆不具有連續性,每一個堆空間裡面的連結串列也可以進入下一個堆空間,而作業系統本身在整理記憶體堆的時候會做一些簡單的操作,然後通過每一個記憶體堆的雙向連結串列就使得記憶體堆更加方便。而且堆空間不需要有序,甚至說有序不影響堆空間的儲存結構,因為它歸根到底是在記憶體塊上邊進行實現的,記憶體塊本身是一個堆疊結構,只是該記憶體堆疊裡面的塊如何分配不由JVM決定,是由作業系統已經最開始分配好了,也就是最小儲存單位。然後JVM拿到從作業系統申請的堆空間過後,先進行初始化操作,然後就可以直接使用了。
  常見的對程式有影響的記憶體問題主要是兩種:溢位和記憶體洩漏,上邊已經講過了記憶體洩漏,其實從記憶體的結構分析,洩漏這種情況很難甚至說不可能發生在棧空間裡面,其主要原因是棧空間本身很難出現懸停的記憶體,因為棧空間的儲存結構有可能是記憶體的一個地址陣列,所以在訪問棧空間的時候使用的都是索引或者下標或者就是最原始的出棧和入棧的操作,這些操作使得棧裡面很難出現像堆空間一樣的記憶體懸停(也就是引用懸掛)問題。堆空間懸停的記憶體是因為棧中存放的引用的變化,其實引用可以理解為從棧到堆的一個指標,當該指標發生變化的時候,堆記憶體碎片就有可能產生,而這種情況下在原始語言裡面就經常發生記憶體洩漏的情況,因為這些懸停的堆空間在系統裡面是不能夠被任何本地指標引用到,就使得這些物件在未被回收的時候脫離了可操作區域並且佔用了系統資源。
  棧溢位問題一直都是計算機領域裡面的一個安全性問題,這裡不做深入討論,說多了就偏離主題了,而記憶體洩漏是程式設計師最容易理解的記憶體問題,還有一個問題來自於我一個黑客朋友就是:堆溢位現象,這種現象可能更加複雜。
  其實Java裡面的記憶體結構,最初看來就是堆和棧的結合,實際上可以這樣理解,實際上物件的實際內容才存在物件池裡面,而有關物件的其他東西有可能會儲存於方法區,而平時使用的時候的引用是存在記憶體棧上的,這樣就更加容易理解它內部的結構,不僅僅如此,有時候還需要考慮到Java裡面的一些欄位和屬性到底是物件域的還是類域的,這個也是一個比較複雜的問題。
  二者的區別簡單總結一下:
  • 管理方式:JVM自己可以針對記憶體棧進行管理操作,而且該記憶體空間的釋放是編譯器就可以操作的內容,而堆空間在Java中JVM本身執行引擎不會對其進行釋放操作,而是讓垃圾回收器進行自動回收
  • 空間大小:一般情況下棧空間相對於堆空間而言比較小,這是由棧空間裡面儲存的資料以及本身需要的資料特性決定的,而堆空間在JVM堆例項進行分配的時候一般大小都比較大,因為堆空間在一個Java程式中需要儲存太多的Java物件資料
  • 碎片相關:針對堆空間而言,即使垃圾回收器能夠進行自動堆記憶體回收,但是堆空間的活動量相對棧空間而言比較大,很有可能存在長期的堆空間分配和釋放操作,而且垃圾回收器不是實時的,它有可能使得堆空間的記憶體碎片主鍵累積起來。針對棧空間而言,因為它本身就是一個堆疊的資料結構,它的操作都是一一對應的,而且每一個最小單位的結構棧幀和堆空間內複雜的記憶體結構不一樣,所以它一般在使用過程很少出現記憶體碎片。
  • 分配方式:一般情況下,棧空間有兩種分配方式:靜態分配和動態分配,靜態分配是本身由編譯器分配好了,而動態分配可能根據情況有所不同,而堆空間卻是完全的動態分配的,是一個執行時級別的記憶體分配。而棧空間分配的記憶體不需要我們考慮釋放問題,而堆空間即使在有垃圾回收器的前提下還是要考慮其釋放問題。
  • 效率:因為記憶體塊本身的排列就是一個典型的堆疊結構,所以棧空間的效率自然比起堆空間要高很多,而且計算機底層記憶體空間本身就使用了最基礎的堆疊結構使得棧空間和底層結構更加符合,它的操作也變得簡單就是最簡單的兩個指令:入棧和出棧;棧空間針對堆空間而言的弱點是靈活程度不夠,特別是在動態管理的時候。而堆空間最大的優勢在於動態分配,因為它在計算機底層實現可能是一個雙向連結串列結構,所以它在管理的時候操作比棧空間複雜很多,自然它的靈活度就高了,但是這樣的設計也使得堆空間的效率不如棧空間,而且低很多。
 
3.本機記憶體[部分內容來源於IBM開發中心]
  Java堆空間是在編寫Java程式中被我們使用得最頻繁的記憶體空間,平時開發過程,開發人員一定遇到過OutOfMemoryError,這種結果有可能來源於Java堆空間的記憶體洩漏,也可能是因為堆的大小不夠而導致的,有時候這些錯誤是可以依靠開發人員修復的,但是隨著Java程式需要處理越來越多的併發程式,可能有些錯誤就不是那麼容易處理了。有些時候即使Java堆空間沒有滿也可能丟擲錯誤,這種情況下需要了解的就是JRE(Java
Runtime Environment)內部到底發生了什麼。Java本身的執行宿主環境並不是作業系統,而是Java虛擬機器,Java虛擬機器本身是用C編寫的本機程式,自然它會呼叫到本機資源,最常見的就是針對本機記憶體的呼叫。本機記憶體是可以用於執行時程序的,它和Java應用程式使用的Java堆記憶體不一樣,每一種虛擬化資源都必須儲存在本機記憶體裡面,包括虛擬機器本身執行的資料,這樣也意味著主機的硬體和作業系統在本機記憶體的限制將直接影響到Java應用程式的效能。
  i.Java執行時如何使用本機記憶體:
  1)堆空間和垃圾回收
  Java執行時是一個作業系統程序(Windows下一般為java.exe),該環境提供的功能會受一些位置的使用者程式碼驅動,這雖然提高了執行時在處理資源的靈活性,但是無法預測每種情況下執行時環境需要何種資源,這一點Java堆空間講解中已經提到過了。在Java命令列可以使用-Xmx和-Xms來控制堆空間初始配置,mx表示堆空間的最大大小,ms表示初始化大小,這也是上提到的啟動Java的配置檔案可以配置的內容。儘管邏輯記憶體堆可以根據堆上的物件數量和在GC上花費的時間增加或者減少,但是使用本機記憶體的大小是保持不變的,而且由-Xms的值指定,大部分GC演算法都是依賴被分配的連續記憶體塊的堆空間,因此不能在堆需要擴大的時候分配更多的本機記憶體,所有的堆記憶體必須保留下來,請注意這裡說的不是Java堆記憶體空間是本機記憶體。
  本機記憶體保留和本機記憶體分配不一樣,本機記憶體被保留的時候,無法使用實體記憶體或者其他儲存器作為備用記憶體,儘管保留地址空間塊不會耗盡物理資源,但是會阻止記憶體用於其他用途,由保留從未使用過的記憶體導致的洩漏和洩漏分配的記憶體造成的問題其嚴重程度差不多,但使用的堆區域縮小時,一些垃圾回收器會回收堆空間的一部分內容,從而減少實體記憶體的使用。對於維護Java堆的記憶體管理系統,需要更多的本機記憶體來維護它的狀態,進行垃圾收集的時候,必須分配資料結構來跟蹤空閒儲存空間和進度記錄,這些資料結構的確切大小和性質因實現的不同而有所差異。
  2)JIT
  JIT編譯器在執行時編譯Java位元組碼來優化本機可執行程式碼,這樣極大提高了Java執行時的速度,並且支援Java應用程式與原生代碼相當的速度執行。位元組碼編譯使用本機記憶體,而且JIT編譯器的輸入(位元組碼)和輸出(可執行程式碼)也必須儲存在本機記憶體裡面,包含了多個經過JIT編譯的方法的Java程式會比一些小型應用程式使用更多的本機記憶體。
  3)類和類載入器
  Java 應用程式由一些類組成,這些類定義物件結構和方法邏輯。Java 應用程式也使用 Java 執行時類庫(比如 java.lang.String)中的類,也可以使用第三方庫。這些類需要儲存在記憶體中以備使用。儲存類的方式取決於具體實現。Sun JDK 使用永久生成(permanent generation,PermGen)堆區域,從最基本的層面來看,使用更多的類將需要使用更多記憶體。(這可能意味著您的本機記憶體使用量會增加,或者您必須明確地重新設定
PermGen 或共享類快取等區域的大小,以裝入所有類)。記住,不僅您的應用程式需要載入到記憶體中,框架、應用伺服器、第三方庫以及包含類的 Java 執行時也會按需載入並佔用空間。Java 執行時可以解除安裝類來回收空間,但是只有在非常嚴酷的條件下才會這樣做,不能解除安裝單個類,而是解除安裝類載入器,隨其載入的所有類都會被解除安裝。只有在以下情況下才能解除安裝類載入器
  • Java 堆不包含對錶示該類載入器的 java.lang.ClassLoader 物件的引用。
  • Java 堆不包含對錶示類載入器載入的類的任何 java.lang.Class 物件的引用。
  • 在 Java 堆上,該類載入器載入的任何類的所有物件都不再存活(被引用)。

  需要注意的是,Java 執行時為所有 Java 應用程式建立的 3 個預設類載入器( bootstrap、extension 和 application )都不可能滿足這些條件,因此,任何系統類(比如
java.lang.String)或通過應用程式類載入器載入的任何應用程式類都不能在執行時釋放。即使類載入器適合進行收集,執行時也只會將收集類載入器作為 GC 週期的一部分。一些實現只會在某些 GC 週期中解除安裝類載入器,也可能在執行時生成類,而不去釋放它。許多 Java EE 應用程式使用 JavaServer Pages (JSP)
技術來生成 Web 頁面。使用 JSP 會為執行的每個 .jsp 頁面生成一個類,並且這些類會在載入它們的類載入器的整個生存期中一直存在 —— 這個生存期通常是 Web 應用程式的生存期。另一種生成類的常見方法是使用 Java 反射。反射的工作方式因 Java 實現的不同而不同,當使用 java.lang.reflect API 時,Java 執行時必須將一個反射物件(比如
java.lang.reflect.Field)的方法連線到被反射到的物件或類。這可以通過使用 Java 本機介面(Java Native Interface,JNI)訪問器來完成,這種方法需要的設定很少,但是速度緩慢,也可以在執行時為您想要反射到的每種物件型別動態構建一個類。後一種方法在設定上更慢,但執行速度更快,非常適合於經常反射到一個特定類的應用程式。Java 執行時在最初幾次反射到一個類時使用 JNI 方法,但當使用了若干次 JNI
方法之後,訪問器會膨脹為位元組碼訪問器,這涉及到構建類並通過新的類載入器進行載入。執行多次反射可能導致建立了許多訪問器類和類載入器,保持對反射物件的引用會導致這些類一直存活,並繼續佔用空間,因為建立位元組碼訪問器非常緩慢,所以 Java 執行時可以快取這些訪問器以備以後使用,一些應用程式和框架還會快取反射物件,這進一步增加了它們的本機記憶體佔用。

  4)JNI
  JNI支援本機程式碼呼叫Java方法,反之亦然,Java執行時本身極大依賴於JNI程式碼來實現類庫功能,比如檔案和網路I/O,JNI應用程式可以通過三種方式增加Java執行時對本機記憶體的使用:
  • JNI應用程式的本機程式碼被編譯到共享庫中,或編譯為載入到程序地址空間中的可執行檔案,大型本機應用程式可能僅僅載入就會佔用大量程序地址空間
  • 本機程式碼必須與Java執行時共享地址空間,任何本機程式碼分配或本機程式碼執行的記憶體對映都會耗用Java執行時記憶體
  • 某些JNI函式可能在它們的常規操作中使用本機記憶體,GetTypeArrayElements和GetTypeArrayRegion函式可以將Java堆複製到本機記憶體緩衝區中,提供給原生代碼使用,是否複製資料依賴於執行時實現,通過這種方式訪問大量Java堆資料就可能使用大量的本機記憶體堆空間
  5)NIO
  JDK 1.4開始新增了新的I/O類,引入了一種基於通道和緩衝區執行I/O的新方式,就像Java堆上的記憶體支援I/O緩衝區一樣,NIO新增了對直接ByteBuffer的支援,ByteBuffer受本機記憶體而不是Java堆的支援,直接ByteBuffer可以直接傳遞到本機作業系統庫函式,以執行I/O,這種情況雖然提高了Java程式在I/O的執行效率,但是會對本機記憶體進行直接的記憶體開銷。ByteBuffer直接操作和非直接操作的區別如下:

  對於在何處儲存直接 ByteBuffer 資料,很容易產生混淆。應用程式仍然在 Java 堆上使用一個物件來編排 I/O 操作,但持有該資料的緩衝區將儲存在本機記憶體中,Java 堆物件僅包含對本機堆緩衝區的引用。非直接 ByteBuffer 將其資料儲存在 Java 堆上的 byte[] 陣列中。直接ByteBuffer物件會自動清理本機緩衝區,但這個過程只能作為Java堆GC的一部分執行,它不會自動影響施加在本機上的壓力。GC僅在Java堆被填滿,以至於無法為堆分配請求提供服務的時候,或者在Java應用程式中顯示請求它發生。

  6)執行緒:
  應用程式中的每個執行緒都需要記憶體來儲存器堆疊(用於在呼叫函式時持有區域性變數並維護狀態的記憶體區域)。每個 Java 執行緒都需要堆疊空間來執行。根據實現的不同,Java 執行緒可以分為本機執行緒和 Java 堆疊。除了堆疊空間,每個執行緒還需要為執行緒本地儲存(thread-local
storage)和內部資料結構提供一些本機記憶體。儘管每個執行緒使用的記憶體量非常小,但對於擁有數百個執行緒的應用程式來說,執行緒堆疊的總記憶體使用量可能非常大。如果執行的應用程式的執行緒數量比可用於處理它們的處理器數量多,效率通常很低,並且可能導致糟糕的效能和更高的記憶體佔用。
  ii.本機記憶體耗盡:
  Java執行時善於以不同的方式來處理Java堆空間的耗盡和本機堆空間的耗盡,但是這兩種情形具有類似症狀,當Java堆空間耗盡的時候,Java應用程式很難正常執行,因為Java應用程式必須通過分配物件來完成工作,只要Java堆被填滿,就會出現糟糕的GC效能,並且丟擲OutOfMemoryError。相反,一旦
Java 執行時開始執行並且應用程式處於穩定狀態,它可以在本機堆完全耗盡之後繼續正常執行,不一定會發生奇怪的行為,因為需要分配本機記憶體的操作比需要分配 Java 堆的操作少得多。儘管需要本機記憶體的操作因 JVM 實現不同而異,但也有一些操作很常見:啟動執行緒、載入類以及執行某種型別的網路和檔案
I/O。本機記憶體不足行為與 Java 堆記憶體不足行為也不太一樣,因為無法對本機堆分配進行控制,儘管所有 Java 堆分配都在 Java 記憶體管理系統控制之下,但任何本機程式碼(無論其位於 JVM、Java 類庫還是應用程式程式碼中)都可能執行本機記憶體分配,而且會失敗。嘗試進行分配的程式碼然後會處理這種情況,無論設計人員的意圖是什麼:它可能通過 JNI 介面丟擲一個 OutOfMemoryError,在螢幕上輸出一條訊息,發生無提示失敗並在稍後再試一次,或者執行其他操作。
  iii.例子:
  這篇文章一致都在講概念,這裡既然提到了ByteBuffer,先提供一個簡單的例子演示該類的使用:
  ——[$]使用NIO讀取txt檔案——
package org.susan.java.io;
 
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
 
public class ExplicitChannelRead {
    public static void main(String args[]){
        FileInputStream fileInputStream;
        FileChannel fileChannel;
        long fileSize;
        ByteBuffer byteBuffer;
        try{
            fileInputStream = new FileInputStream("D://read.txt");
            fileChannel = fileInputStream.getChannel();
            fileSize = fileChannel.size();
            byteBuffer = ByteBuffer.allocate((int)fileSize);
            fileChannel.read(byteBuffer);
            byteBuffer.rewind();
            for( int i = 0; i < fileSize; i++ )
                System.out.print((char)byteBuffer.get());
            fileChannel.close();
            fileInputStream.close();
        }catch(IOException ex){
            ex.printStackTrace();
        }
    }
}
  在讀取檔案的路徑放上該txt檔案裡面寫入:Hello World,上邊這段程式碼就是使用NIO的方式讀取檔案系統上的檔案,這段程式的輸入就為:
Hello World
  ——[$]獲取ByteBuffer上的位元組轉換為Byte陣列——
package org.susan.java.io;
 
import java.nio.ByteBuffer;
 
public class ByteBufferToByteArray {
    public static void main(String args[]) throws Exception{
        // 從byte陣列建立ByteBuffer
        byte[] bytes = new byte[10];
        ByteBuffer buffer = ByteBuffer.wrap(bytes);
 
        // 在position和limit,也就是ByteBuffer緩衝區的首尾之間讀取位元組
        bytes = new byte[buffer.remaining()];
        buffer.get(bytes, 0, bytes.length);
 
        // 讀取所有ByteBuffer內的位元組
        buffer.clear();
        bytes = new byte[buffer.capacity()];
        buffer.get(bytes, 0, bytes.length);
    }
}
  上邊程式碼就是從ByteBuffer到byte陣列的轉換過程,有了這個過程在開發過程中可能更加方便,ByteBuffer的詳細講解我保留到IO部分,這裡僅僅是涉及到了一些,所以提供兩段例項程式碼。
  iv.共享記憶體:
  在Java語言裡面,沒有共享記憶體的概念,但是在某些引用中,共享記憶體卻很受用,例如Java語言的分散式系統,存著大量的Java分散式共享物件,很多時候需要查詢這些物件的狀態,以檢視系統是否執行正常或者瞭解這些物件目前的一些統計資料和狀態。如果使用的是網路通訊的方式,顯然會增加應用的額外開銷,也增加了不必要的應用程式設計,如果是共享記憶體方式,則可以直接通過共享記憶體檢視到所需要的物件的資料和統計資料,從而減少一些不必要的麻煩。
  1)共享記憶體特點:
  • 可以被多個程序開啟訪問
  • 讀寫操作的程序在執行讀寫操作的時候其他程序不能進行寫操作
  • 多個程序可以交替對某一個共享記憶體執行寫操作
  • 一個程序執行了記憶體寫操作過後,不影響其他程序對該記憶體的訪問,同時其他程序對更新後的記憶體具有可見性
  • 在程序執行寫操作時如果異常退出,對其他程序的寫操作禁止自動解除
  • 相對共享檔案,資料訪問的方便性和效率  
  2)出現情況:
  • 獨佔的寫操作,相應有獨佔的寫操作等待佇列。獨佔的寫操作本身不會發生資料的一致性問題;
  • 共享的寫操作,相應有共享的寫操作等待佇列。共享的寫操作則要注意防止發生資料的一致性問題;
  • 獨佔的讀操作,相應有共享的讀操作等待佇列;
  • 共享的讀操作,相應有共享的讀操作等待佇列;
  3)Java中共享記憶體的實現:
  JDK 1.4裡面的MappedByteBuffer為開發人員在Java中實現共享記憶體提供了良好的方法,該緩衝區實際上是一個磁碟檔案的記憶體映象,二者的變化會保持同步,即記憶體資料發生變化過後會立即反應到磁碟檔案中,這樣會有效地保證共享記憶體的實現,將共享檔案和磁碟檔案簡歷聯絡的是檔案通道類:FileChannel,該類的加入是JDK為了統一外圍裝置的訪問方法,並且加強了多執行緒對同一檔案進行存取的安全性,這裡可以使用它來建立共享記憶體用,它建立了共享記憶體和磁碟檔案之間的一個通道。開啟一個檔案可使用RandomAccessFile類的getChannel方法,該方法直接返回一個檔案通道,該檔案通道由於對應的檔案設為隨機存取,一方面可以進行讀寫兩種操作,另外一個方面使用它不會破壞映象檔案的內容。這裡,如果使用FileOutputStream和FileInputStream則不能理想地實現共享記憶體的要求,因為這兩個類同時實現自由讀寫很困難。
  下邊程式碼段實現了上邊提及的共享記憶體功能
// 獲得一個只讀的隨機存取檔案物件
RandomAccessFile RAFile = new RandomAccessFile(filename,"r");
// 獲得相應的檔案通道
FileChannel fc = RAFile.getChannel();
// 取得檔案的實際大小
int size = (int)fc.size();

// 獲得共享記憶體緩衝區,該共享記憶體只讀 

MappedByteBuffer mapBuf = fc.map(FileChannel.MAP_RO,0,size);

// 獲得一個可讀寫的隨機存取檔案物件 

RAFile = new RandomAccessFile(filename,"rw");

// 獲得相應的檔案通道 

fc = RAFile.getChannel();

// 取得檔案的實際大小,以便映像到共享記憶體 

size = (int)fc.size();

// 獲得共享記憶體緩衝區,該共享記憶體可讀寫 

mapBuf = fc.map(FileChannel.MAP_RW,0,size);

// 獲取頭部訊息:存取許可權 

mode = mapBuf.getInt(); 
  如果多個應用映象使用同一檔名的共享記憶體,則意味著這多個應用共享了同一記憶體資料,這些應用對於檔案可以具有同等存取許可權,一個應用對資料的重新整理會更新到多個應用中。為了防止多個應用同時對共享記憶體進行寫操作,可以在該共享記憶體的頭部資訊加入寫操作標記,該共享檔案的頭部基本資訊至少有:
  • 共享記憶體長度
  • 共享記憶體目前的存取模式
  共享檔案的頭部資訊是私有資訊,多個應用可以對同一個共享記憶體執行寫操作,執行寫操作和結束寫操作的時候,可以使用如下方法:
public boolean startWrite()
{
    if(mode == 0) // 這裡mode代表共享記憶體的存取模式,為0代表可寫
    {
        mode = 1; // 意味著別的應用不可寫
        mapBuf.flip();
        mapBuf.putInt(mode);    //寫入共享記憶體的頭部資訊
        return true;
    }
    else{
        return false; //表明已經有應用在寫該共享記憶體了,本應用不能夠針對共享記憶體再做寫操作
    }

}
 
public boolean stopWrite()
{
    mode = 0; // 釋放寫許可權
    mapBuf.flip();
    mapBuf.putInt(mode);    //寫入共享記憶體頭部資訊
    return true;

}
  【*:上邊提供了對共享記憶體執行寫操作過程的兩個方法,這兩個方法其實理解起來很簡單,真正需要思考的是一個針對存取模式的設定,其實這種機制和最前面提到的記憶體的鎖模式有點類似,一旦當mode(存取模式)設定稱為可寫的時候,startWrite才能返回true,不僅僅如此,某個應用程式在向共享記憶體寫入資料的時候還會修改其存取模式,因為如果不修改的話就會導致其他應用同樣針對該記憶體是可寫的,這樣就使得共享記憶體的實現變得混亂,而在停止寫操作stopWrite的時候,需要將mode設定稱為1,也就是上邊註釋段提到的釋放寫許可權。】
  關於鎖的知識這裡簡單做個補充【*:上邊程式碼的這種模式可以理解為一種簡單的鎖模式】:一般情況下,計算機程式設計中會經常遇到鎖模式,在整個鎖模式過程中可以將鎖分為兩類(這裡只是輔助理解,不是嚴格的鎖分類)——共享鎖和排他鎖(也稱為獨佔鎖),鎖的定位是定位於針對所有與計算機有關的資源比如記憶體、檔案、儲存空間等,針對這些資源都可能出現鎖模式。在上邊堆和棧一節講到了Java物件鎖,其實不僅僅是物件,只要是計算機中會出現寫入和讀取共同操作的資源,都有可能出現鎖模式。
  共享鎖——當應用程式獲得了資源的共享鎖的時候,那麼應用程式就可以直接訪問該資源,資源的共享鎖可以被多個應用程式拿到,在Java裡面執行緒之間有時候也存在物件的共享鎖,但是有一個很明顯的特徵,也就是記憶體共享鎖只能讀取資料,不能夠寫入資料,不論是什麼資源,當應用程式僅僅只能拿到該資源的共享鎖的時候,是不能夠針對該資源進行寫操作的。
  獨佔鎖——當應用程式獲得了資源的獨佔鎖的時候,應用程式訪問該資源在共享鎖上邊多了一個許可權就是寫許可權,針對資源本身而言,一個資源只有一把獨佔鎖,也就是說一個資源只能同時被一個應用或者一個執行程式碼程式允許寫操作,Java執行緒中的物件寫操作也是這個道理,若某個應用拿到了獨佔鎖的時候,不僅僅可以讀取資源裡面的資料,而且可以向該資源進行資料寫操作。
  資料一致性——當資源同時被應用進行讀寫訪問的時候,有可能會出現資料一致性問題,比如A應用拿到了資源R1的獨佔鎖,B應用拿到了資源R1的共享鎖,A在針對R1進行寫操作,而兩個應用的操作——A的寫操作和B的讀操作出現了一個時間差,s1的時候B讀取了R1的資源,s2的時候A寫入了資料修改了R1的資源,s3的時候B又進行了第二次讀,而兩次讀取相隔時間比較短暫而且初衷沒有考慮到A在B的讀取過程修改了資源,這種情況下針對鎖模式就需要考慮到資料一致性問題。獨佔鎖的排他性在這裡的意思是該鎖只能被一個應用獲取,獲取過程只能由這個應用寫入資料到資源內部,除非它釋放該鎖,否則其他拿不到鎖的應用是無法對資源進行寫入操作的。
  按照上邊的思路去理解程式碼裡面實現共享記憶體的過程就更加容易理解了。
  如果執行寫操作的應用異常中止,那麼映像檔案的共享記憶體將不再能執行寫操作。為了在應用異常中止後,寫操作禁止標誌自動消除,必須讓執行的應用獲知退出的應用。在多執行緒應用中,可以用同步方法獲得這樣的效果,但是在多程序中,同步是不起作用的。方法可以採用的多種技巧,這裡只是描述一可能的實現:採用檔案鎖的方式。寫共享記憶體應用在獲得對一個共享記憶體寫許可權的時候,除了判斷頭部資訊的寫許可權標誌外,還要判斷一個臨時的鎖檔案是否可以得到,如果可以得到,則即使頭部資訊的寫許可權標誌為1(上述),也可以啟動寫許可權,其實這已經表明寫許可權獲得的應用已經異常退出,這段程式碼如下:
// 開啟一個臨時檔案,注意統一共享記憶體,該檔名必須相同,可以在共享檔名後邊新增“.lock”字尾
RandomAccessFile files = new RandomAccessFile("memory.lock","rw");
// 獲取檔案通道
FileChannel lockFileChannel = files.getChannel();
// 獲取檔案的獨佔鎖,該方法不產生任何阻塞直接返回
FileLock fileLock = lockFileChannel.tryLock();
// 如果為空表示已經有應用佔有了
if( fileLock == null ){
    // …不可寫
}else{
    // …可以執行寫操作
}
  4)共享記憶體的應用:
  在Java中,共享記憶體一般有兩種應用:
  [1]永久物件配置——在java伺服器應用中,使用者可能會在執行過程中配置一些引數,而這些引數需要永久 有效,當伺服器應用重新啟動後,這些配置引數仍然可以對應用起作用。這就可以用到該文 中的共享記憶體。該共享記憶體中儲存了伺服器的執行引數和一些物件執行特性。可以在應用啟動時讀入以啟用以前配置的引數。
  [2]查詢共享資料——一個應用(例 sys.java)是系統的服務程序,其系統的執行狀態記錄在共享記憶體中,其中執行狀態可能是不斷變化的。為了隨時瞭解系統的執行狀態,啟動另一個應用(例 mon.java),該應用查詢該共享記憶體,彙報系統的執行狀態。
  v.小節:
  提供本機記憶體以及共享記憶體的知識,主要是為了讓讀者能夠更順利地理解JVM內部記憶體模型的物理原理,包括JVM如何和作業系統在記憶體這個級別進行互動,理解了這些內容就讓讀者對Java記憶體模型的認識會更加深入,而且不容易遺忘。其實Java的記憶體模型遠不及我們想象中那麼簡單,而且其結構極端複雜,看過《Inside JVM》的朋友應該就知道,結合JVM指令集去寫點小程式碼測試.class檔案的裡層結構也不失為一種好玩的學習方法。
  
4.防止記憶體洩漏
  Java中會有記憶體洩漏,聽起來似乎是很不正常的,因為Java提供了垃圾回收器針對記憶體進行自動回收,但是Java還是會出現記憶體洩漏的。
  i.什麼是Java中的記憶體洩漏:
  在Java語言中,記憶體洩漏就是存在一些被分配的物件,這些物件有兩個特點:這些物件可達,即在物件記憶體的有向圖中存在通路可以與其相連;其次,這些物件是無用的,即程式以後不會再使用這些物件了。如果物件滿足這兩個條件,該物件就可以判定為Java中的記憶體洩漏,這些物件不會被GC回收,然而它卻佔用記憶體,這就是Java語言中的記憶體洩漏。Java中的記憶體洩漏和C++中的記憶體洩漏還存在一定的區別,在C++裡面,記憶體洩漏的範圍更大一些,有些物件被分配了記憶體空間,但是卻不可達,由於C++中沒有GC,這些記憶體將會永遠收不回來,在Java中這些不可達物件則是被GC負責回收的,因此程式設計師不需要考慮這一部分的記憶體洩漏。二者的圖如下:

  因此按照上邊的分析,Java語言中也是存在記憶體洩漏的,但是其記憶體洩漏範圍比C++要小很多,因為Java裡面有個特殊程式回收所有的不可達物件:垃圾回收器。對於程式設計師來說,GC基本是透明的,不可見的。雖然,我們只有幾個函式可以訪問GC,例如執行GC的函式System.gc(),但是根據Java語言規範定義,該函式不保證JVM的垃圾收集器一定會執行。因為,不同的JVM實現者可能使用不同的演算法管理GC。通常,GC的執行緒的優先順序別較低,JVM呼叫GC的策略也有很多種,有的是記憶體使用到達一定程度時,GC才開始工作,也有定時執行的,有的是平緩執行GC,有的是中斷式執行GC。但通常來說,我們不需要關心這些。除非在一些特定的場合,GC的執行影響應用程式的效能,例如對於基於Web的實時系統,如網路遊戲等,使用者不希望GC突然中斷應用程式執行而進行垃圾回收,那麼我們需要調整GC的引數,讓GC能夠通過平緩的方式釋放記憶體,例如將垃圾回收分解為一系列的小步驟執行,Sun提供的HotSpot
JVM就支援這一特性。

  舉個例子:
  ——[$]記憶體洩漏的例子——
package org.susan.java.collection;
 
import java.util.Vector;
 
public class VectorMemoryLeak {
    public static void main(String args[]){
        Vector<String> vector = new Vector<String>();
        for( int i = 0; i < 1000; i++ ){
            String tempString = new String();
            vector.add(tempString);
            tempString = null;
        }
    }
}
  從上邊這個例子可以看到,迴圈申請了String物件,並且將申請的物件放入了一個Vector中,如果僅僅是釋放物件本身,因為Vector仍然引用了該物件,所以這個物件對CG來說是不可回收的,因此如果物件加入到Vector後,還必須從Vector刪除才能夠回收,最簡單的方式是將Vector引用設定成null。實際上這些物件已經沒有用了,但是還是被程式碼裡面的引用引用到了,這種情況GC拿它就沒有了任何辦法,這樣就可以導致了記憶體洩漏。
  【*:Java語言因為提供了垃圾回收器,照理說是不會出現記憶體洩漏的,Java裡面導致記憶體洩漏的主要原因就是,先前申請了記憶體空間而忘記了釋放。如果程式中存在對無用物件的引用,這些物件就會駐留在記憶體中消耗記憶體,因為無法讓GC判斷這些物件是否可達。如果存在物件的引用,這個物件就被定義為“有效的活動狀態”,同時不會被釋放,要確定物件所佔記憶體被回收,必須要確認該物件不再被使用。典型的做法就是把物件資料成員設定成為null或者中集合中移除,當區域性變數不需要的情況則不需要顯示宣告為null。】
  ii.常見的Java記憶體洩漏
  1)全域性集合:
  在大型應用程式中存在各種各樣的全域性資料倉儲是很普遍的,比如一個JNDI樹或者一個Session table(會話表),在這些情況下,必須注意管理儲存庫的大小,必須有某種機制從儲存庫中移除不再需要的資料。
  [$]解決:
  [1]常用的解決方法是週期運作清除作業,該作業會驗證倉庫中的資料然後清楚一切不需要的資料
  [2]另外一種方式是反向連結計數,集合負責統計集合中每個入口的反向連結資料,這要求反向連結告訴集合合適會退出入口,當反向連結數目為零的時候,該元素就可以移除了。
  2)快取:
  快取一種用來快速查詢已經執行過的操作結果的資料結構。因此,如果一個操作執行需要比較多的資源並會多次被使用,通常做法是把常用的輸入資料的操作結果進行快取,以便在下次呼叫該操作時使用快取的資料。快取通常都是以動態方式實現的,如果快取設定不正確而大量使用快取的話則會出現記憶體溢位的後果,因此需要將所使用的記憶體容量與檢索資料的速度加以平衡。
  [$]解決:
  [1]常用的解決途徑是使用java.lang.ref.SoftReference類堅持將物件放入快取,這個方法可以保證當虛擬機器用完記憶體或者需要更多堆的時候,可以釋放這些物件的引用。
  3)類載入器:
  Java類裝載器的使用為記憶體洩漏提供了許多可乘之機。一般來說類裝載器都具有複雜結構,因為類裝載器不僅僅是隻與"常規"物件引用有關,同時也和物件內部的引用有關。比如資料變數,方法和各種類。這意味著只要存在對資料變數,方法,各種類和物件的類裝載器,那麼類裝載器將駐留在JVM中。既然類裝載器可以同很多的類關聯,同時也可以和靜態資料變數關聯,那麼相當多的記憶體就可能發生洩漏。
  iii.Java引用【摘錄自前邊的《Java引用總結》】:
  Java中的物件引用主要有以下幾種型別:
  1)強可及物件(strongly reachable):
  可以通過強引用訪問的物件,一般來說,我們平時寫程式碼的方式都是使用的強引用物件,比如下邊的程式碼段:
  StringBuilder builder= new StringBuilder();
  上邊程式碼部分引用obj這個引用將引用記憶體堆中的一個物件,這種情況下,只要obj的引用存在,垃圾回收器就永遠不會釋放該物件的儲存空間。這種物件我們又成為強引用(Strong references),這種強引用方式就是Java語言的原生的Java引用,我們幾乎每天程式設計的時候都用到。上邊程式碼JVM儲存了一個StringBuilder型別的物件的強引用在變數builder呢。強引用和GC的互動是這樣的,如果一個物件通過強引用可達或者通過強引用鏈可達的話這種物件就成為強可及物件,這種情況下的物件垃圾回收器不予理睬。如果我們開發過程不需要垃圾回器回收該物件,就直接將該物件賦為強引用,也是普通的程式設計方法。
  2)軟可及物件(softly reachable):
  不通過強引用訪問的物件,即不是強可及物件,但是可以通過軟引用訪問的物件就成為軟可及物件,軟可及物件就需要使用類SoftReference(java.lang.ref.SoftReference)。此種型別的引用主要用於記憶體比較敏感的快取記憶體,而且此種引用還是具有較強的引用功能,當記憶體不夠的時候GC會回收這類記憶體,因此如果記憶體充足的時候,這種引用通常不會被回收的。不僅僅如此,這種引用物件在JVM裡面保證在丟擲OutOfMemory異常之前,設定成為null。通俗地講,這種型別的引用保證在JVM記憶體不足的時候全部被清除,但是有個關鍵在於:垃圾收集器在執行時是否釋放軟可及物件是不確定的,而且使用垃圾回收演算法並不能保證一次性尋找到所有的軟可及物件。當垃圾回收器每次執行的時候都可以隨意釋放不是強可及物件佔用的記憶體,如果垃圾回收器找到了軟可及物件過後,可能會進行以下操作:
  • 將SoftReference物件的referent域設定成為null,從而使該物件不再引用heap物件。
  • SoftReference引用過的記憶體堆上的物件一律被生命為finalizable。
  • 當記憶體堆上的物件finalize()方法被執行而且該物件佔用的記憶體被釋放,SoftReference物件就會被新增到它的ReferenceQueue,前提條件是ReferenceQueue本身是存在的。
  既然Java裡面存在這樣的物件,那麼我們在編寫程式碼的時候如何建立這樣的物件呢?建立步驟如下:
  先建立一個物件,並使用普通引用方式【強引用】,然後再建立一個SoftReference來引用該物件,最後將普通引用設定為null,通過這樣的方式,這個物件就僅僅保留了一個SoftReference引用,同時這種情況我們所建立的物件就是SoftReference物件。一般情況下,我們可以使用該引用來完成Cache功能,就是前邊說的用於快取記憶體,保證最大限度使用記憶體而不會引起記憶體洩漏的情況。下邊的程式碼段:
  public static void main(String args[])
  {
    //建立一個強可及物件
    A a = new A();
    //建立這個物件的軟引用SoftReference
    SoftReference sr = new SoftReference(a);
    //將強引用設定為空,以遍垃圾回收器回收強引用
    a = null;
    //下次使用該物件的操作
    if( sr != null ){
      a = (A)sr.get();
    }else{
      //這種情況就是由於記憶體過低,已經將軟引用釋放了,因此需要重新裝載一次
      a = new A();
      sr = new SoftReference(a);
    }
  }
  軟引用技術使得Java系統可以更好地管理記憶體,保持系統穩定,防止記憶體洩漏,避免系統崩潰,因此在處理一些記憶體佔用大而且生命週期長使用不頻繁的物件可以使用該技術。
  3)弱可及物件(weakly reachable):
  不是強可及物件同樣也不是軟可及物件,僅僅通過弱引用WeakReference(java.lang.ref.WeakReference)訪問的物件,這種物件的用途在於規範化對映(canonicalized mapping),對於生存週期相對比較長而且重新建立的時候開銷少的物件,弱引用也比較有用,和軟引用物件不同的是,垃圾回收器如果碰到了弱可及物件,將釋放WeakReference物件的記憶體,但是垃圾回收器需要執行很多次才能夠找到弱可及物件。弱引用物件在使用的時候,可以配合ReferenceQueue類使用,如果弱引用被回收,JVM就會把這個弱引用加入到相關的引用佇列中去。最簡單的弱引用方法如以下程式碼:
  WeakReference weakWidget = new WeakReference(classA);
  在上邊程式碼裡面,當我們使用weakWidget.get()來獲取classA的時候,由於弱引用本身是無法阻止垃圾回收的,所以我們也許會拿到一個null為返回。【*:這裡提供一個小技巧,如果我們希望取得某個物件的資訊,但是又不影響該物件的垃圾回收過程,我們就可以使用WeakReference來記住該物件,一般我們在開發偵錯程式和優化器的時候使用這個是很好的一個手段。】
  如果上邊的程式碼部分,我們通過weakWidget.get()返回的是null就證明該物件已經被垃圾回收器回收了,而這種情況下弱引用物件就失去了使用價值,GC就會定義為需要進行清除工作。這種情況下弱引用無法引用任何物件,所以在JVM裡面就成為了一個死引用,這就是為什麼我們有時候需要通過ReferenceQueue類來配合使用的原因,使用了ReferenceQueue過後,就使得我們更加容易監視該引用的物件,如果我們通過一ReferenceQueue類來構造一個弱引用,當弱引用的物件已經被回收的時候,系統將自動使用物件引用佇列來代替物件引用,而且我們可以通過ReferenceQueue類的執行來決定是否真正要從垃圾回收器裡面將該死引用(Dead
Reference)清除。
  弱引用程式碼段:
  //建立普通引用物件
  MyObject object = new MyObject();
  //建立一個引用佇列
  ReferenceQueue rq = new ReferenceQueue();
  //使用引用佇列建立MyObject的弱引用
  WeakReference wr = new WeakReference(object,rq);
  這裡提供兩個實在的場景來描述弱引用的相關用法:
  [1]你想給物件附加一些資訊,於是你用一個 Hashtable 把物件和附加資訊關聯起來。你不停的把物件和附加資訊放入 Hashtable 中,但是當物件用完的時候,你不得不把物件再從 Hashtable 中移除,否則它佔用的記憶體變不會釋放。萬一你忘記了,那麼沒有從 Hashtable 中移除的物件也可以算作是記憶體洩漏。理想的狀況應該是當物件用完時,Hashtable
中的物件會自動被垃圾收集器回收,不然你就是在做垃圾回收的工作。
  [2]你想實現一個圖片快取,因為載入圖片的開銷比較大。你將圖片物件的引用放入這個快取,以便以後能夠重新使用這個物件。但是你必須決定快取中的哪些圖片不再需要了,從而將引用從快取中移除。不管你使用什麼管理快取的演算法,你實際上都在處理垃圾收集的工作,更簡單的辦法(除非你有特殊的需求,這也應該是最好的辦法)是讓垃圾收集器來處理,由它來決定回收哪個物件。 
  當Java回收器遇到了弱引用的時候有可能會執行以下操作:
  • 將WeakReference物件的referent域設定成為null,從而使該物件不再引用heap物件。
  • WeakReference引用過的記憶體堆上的物件一律被生命為finalizable。
  • 當記憶體堆上的物件finalize()方法被執行而且該物件佔用的記憶體被釋放,WeakReference物件就會被新增到它的ReferenceQueue,前提條件是ReferenceQueue本身是存在的。
  4)清除:
  當引用物件的referent域設定為null,並且引用類在記憶體堆中引用的物件宣告為可結束的時候,該物件就可以清除,清除不做過多的講述
  5)虛可及物件(phantomly reachable):
  不是強可及物件,也不是軟可及物件,同樣不是弱可及物件,之所以把虛可及物件放到最後來講,主要也是因為它的特殊性,有時候我們又稱之為“幽靈物件”,已經結束的,可以通過虛引用來訪問該物件。我們使用類PhantomReference(java.lang.ref.PhantomReference)來訪問,這個類只能用於跟蹤被引用物件進行的收集,同樣的,可以用於執行per-mortern清除操作。PhantomReference必須與ReferenceQueue類一起使用。需要使用ReferenceQueue是因為它能夠充當通知機制,當垃圾收集器確定了某個物件是虛可及物件的時候,PhantomReference物件就被放在了它的ReferenceQueue上,這就是一個通知,表明PhantomReference引用的物件已經結束,可以收集了,一般情況下我們剛好在物件記憶體在回收之前採取該行為。這種引用不同於弱引用和軟引用,這種方式通過get()獲取到的物件總是返回null,僅僅當這些物件在ReferenceQueue佇列裡面的時候,我們可以知道它所引用的哪些對物件是死引用(Dead
Reference)。而這種引用和弱引用的區別在於:
  弱引用(WeakReference)是在物件不可達的時候儘快進入ReferenceQueue佇列的,在finalization方法執行和垃圾回收之前是確實會發生的,理論上這類物件是不正確的物件,但是WeakReference物件可以繼續保持Dead狀態,
  虛引用(PhantomReference)是在物件確實已經從實體記憶體中移除過後才進入的ReferenceQueue佇列,而且get()方法會一直返回null
  當垃圾回收器遇到了虛引用的時候將有可能執行以下操作:
  • PhantomReference引用過的heap物件宣告為finalizable;
  • 虛引用在堆物件釋放之前就新增到了它的ReferenceQueue裡面,這種情況使得我們可以在堆物件被回收之前採取操作【*:再次提醒,PhantomReference物件必須經過關聯的ReferenceQueue來建立,就是說必須和ReferenceQueue類配合操作】
  看似沒有用處的虛引用,有什麼用途呢?
  • 首先,我們可以通過虛引用知道物件究竟什麼時候真正從記憶體裡面移除的,而且這也是唯一的途徑。
  • 虛引用避過了finalize()方法,因為對於此方法的執行而言,虛引用真正引用到的物件是異常物件,若在該方法內要使用物件只能重建。一般情況垃圾回收器會輪詢兩次,一次標記為finalization,第二次進行真實的回收,而往往標記工作不能實時進行,或者垃圾回收其會等待一個物件去標記finalization。這種情況很有可能引起MemoryOut,而使用虛引用這種情況就會完全避免。因為虛引用在引用物件的過程不會去使得這個物件由Dead復活,而且這種物件是可以在回收週期進行回收的。
  在JVM內部,虛引用比起使用finalize()方法更加安全一點而且更加有效。而finaliaze()方法回收在虛擬機器裡面實現起來相對簡單,而且也可以處理大部分工作,所以我們仍然使用這種方式來進行物件回收的掃尾操作,但是有了虛引用過後我們可以選擇是否手動操作該物件使得程式更加高效完美。
  iv.防止記憶體洩漏[來自IBM開發中心]:
  1)使用軟引用阻止洩漏:
  [1]在Java語言中有一種形式的記憶體洩漏稱為物件遊離(Object Loitering):
  ——[$]物件遊離——
// 注意,這段程式碼屬於概念說明程式碼,實際應用中不要模仿
public class LeakyChecksum{
    private byte[] byteArray;
    public synchronized int getFileCheckSum(String filename)
    {
        int len = getFileSize(filename);
        if( byteArray == null || byteArray.length < len )
            byteArray = new byte[len];
        readFileContents(filename,byteArray);
        // 計算該檔案的值然後返回該物件
    }
}
  上邊的程式碼是類LeakyChecksum用來說明物件遊離的概念,裡面有一個getFileChecksum()方法用來計算檔案內容校驗和,getFileCheckSum方法將檔案內容讀取到緩衝區中計算校驗和,更加直觀的實現就是簡單地將緩衝區作為getFileChecksum中的本地變數分配,但是上邊這個版本比這種版本更加“聰明”,不是將緩衝區緩衝在例項中欄位中減少記憶體churn。該“優化”通常不帶來預期的好處,物件分配比很多人期望的更加便宜。(還要注意,將緩衝區從本地變數提升到例項變數,使得類若不帶有附加的同步,就不再是執行緒安全的了。直觀的實現不需要將
getFileChecksum() 宣告為 synchronized,並且會在同時呼叫時提供更好的可伸縮性。)
  這個類存在很多的問題,但是我們著重來看記憶體洩漏。快取緩衝區的決定很可能是根據這樣的假設得出的,即該類將在一個程式中被呼叫許多次,因此它應該更加有效,以重用緩衝區而不是重新分配它。但是結果是,緩衝區永遠不會被釋放,因為它對程式來說總是可及的(除非LeakyChecksum物件被垃圾收集了)。更壞的是,它可以增長,卻不可以縮小,所以 LeakyChecksum 將永久保持一個與所處理的最大檔案一樣大小的緩衝區。退一萬步說,這也會給垃圾收集器帶來壓力,並且要求更頻繁的收集;為計算未來的校驗和而保持一個大型緩衝區並不是可用記憶體的最有效利用。LeakyChecksum
中問題的原因是,緩衝區對於 getFileChecksum() 操作來說邏輯上是本地的,但是它的生命週期已經被人為延長了,因為將它提升到了例項欄位。因此,該類必須自己管理緩衝區的生命週期,而不是讓 JVM 來管理。
  這裡可以提供一種策略就是使用Java裡面的軟引用:
  弱引用如何可以給應用程式提供當物件被程式使用時另一種到達該物件的方法,但是不會延長物件的生命週期。Reference 的另一個子類——軟引用——可滿足一個不同卻相關的目的。其中弱引用允許應用程式建立不妨礙垃圾收集的引用,軟引用允許應用程式通過將一些物件指定為
“expendable” 而利用垃圾收集器的幫助。儘管垃圾收集器在找出哪些記憶體在由應用程式使用哪些沒在使用方面做得很好,但是確定可用記憶體的最適當使用還是取決於應用程式。如果應用程式做出了不好的決定,使得物件被保持,那麼效能會受到影響,因為垃圾收集器必須更加辛勤地工作,以防止應用程式消耗掉所有記憶體。快取記憶體是一種常見的效能優化,允許應用程式重用以前的計算結果,而不是重新進行計算。快取記憶體是 CPU 利用和記憶體使用之間的一種折衷,這種折衷理想的平衡狀態取決於有多少記憶體可用。若快取記憶體太少,則所要求的效能優勢無法達到;若太多,則效能會受到影響,因為太多的記憶體被用於快取記憶體上,導致其他用途沒有足夠的可用記憶體。因為垃圾收集器比應用程式更適合決定記憶體需求,所以應該利用垃圾收集器在做這些決定方面的幫助,這就是件引用所要做的。如果一個物件惟一剩下的引用是弱引用或軟引用,那麼該物件是軟可及的(softly
reachable)。垃圾收集器並不像其收集弱可及的物件一樣儘量地收集軟可及的物件,相反,它只在真正 “需要” 記憶體時才收集軟可及的物件。軟引用對於垃圾收集器來說是這樣一種方式,即 “只要記憶體不太緊張,我就會保留該物件。但是如果記憶體變得真正緊張了,我就會去收集並處理這個物件。” 垃圾收集器在可以丟擲OutOfMemoryError 之前需要清除所有的軟引用。通過使用一個軟引用來管理快取記憶體的緩衝區,可以解決 LeakyChecksum中的問題,如上邊程式碼所示。現在,只要不是特別需要記憶體,緩衝區就會被保留,但是在需要時,也可被垃圾收集器回收:
  ——[$]使用軟引用修復上邊程式碼段——
public class CachingChecksum
{
    private SoftReference<byte[]> bufferRef;
    public synchronized int getFileChecksum(String filename)
    {
        int len = getFileSize(filename);
        byte[] byteArray = bufferRef.get();
        if( byteArray == null || byteArray.length < len )
        {
            byteArray = new byte[len];
            bufferRef.set(byteArray);
        }
        readFileContents(filename,byteArray);
    }
}
  一種廉價快取:
  CachingChecksum使用一個軟引用來快取單個物件,並讓 JVM 處理從快取中取走物件時的細節。類似地,軟引用也經常用於 GUI 應用程式中,用於快取點陣圖圖形。是否可使用軟引用的關鍵在於,應用程式是否可從大量快取的資料恢復。如果需要快取不止一個物件,您可以使用一個 Map,但是可以選擇如何使用軟引用。您可以將快取作為 Map<K,
SoftReference<V>> 或SoftReference<Map<K,V>> 管理。後一種選項通常更好一些,因為它給垃圾收集器帶來的工作更少,並且允許在特別需要記憶體時以較少的工作回收整個快取。弱引用有時會錯誤地用於取代軟引用,用於構建快取,但是這會導致差的快取效能。在實踐中,弱引用將在物件變得弱可及之後被很快地清除掉——通常是在快取的物件再次用到之前——因為小的垃圾收集執行得很頻繁。對於在效能上非常依賴快取記憶體的應用程式來說,軟引用是一個不管用的手段,它確實不能取代能夠提供靈活終止期、複製和事務型快取記憶體的複雜的快取記憶體框架。但是作為一種
“廉價(cheap and dirty)” 的快取記憶體機制,它對於降低價格是很有吸引力的。正如弱引用一樣,軟引用也可建立為具有一個相關的引用佇列,引用在被垃圾收集器清除時進入佇列。引用佇列對於軟引用來說,沒有對弱引用那麼有用,但是它們可以用於發出管理警報,說明應用程式開始缺少記憶體。
  2)垃圾回收對引用的處理:
  弱引用和軟引用都擴充套件了抽象的 Reference 類虛引用(phantom references),引用物件被垃圾收集器特殊地看待。垃圾收集器在跟蹤堆期間遇到一個 Reference 時,不會標記或跟蹤該引用物件,而是在已知活躍的 Reference 物件的佇列上放置一個 Reference。在跟蹤之後,垃圾收集器就識別軟可及的物件——這些物件上除了軟引用外,沒有任何強引用。垃圾收集器然後根據當前收集所回收的記憶體總量和其他策略考慮因素,判斷軟引用此時是否需要被清除。將被清除的軟引用如果具有相應的引用佇列,就會進入佇列。其餘的軟可及物件(沒有清除的物件)然後被看作一個根集(root
set),堆跟蹤繼續使用這些新的根,以便通過活躍的軟引用而可及的物件能夠被標記。處理軟引用之後,弱可及物件的集合被識別 —— 這樣的物件上不存在強引用或軟引用。這些物件被清除和加入佇列。所有 Reference 型別在加入佇列之前被清除,所以處理事後檢查(post-mortem)清除的執行緒永遠不會具有 referent 物件的訪問權,而只具有Reference 物件的訪問權。因此,當 References 與引用佇列一起使用時,通常需要細分適當的引用型別,並將它直接用於您的設計中(與 WeakHashMap 一樣,它的 Map.Entry 擴充套件了 WeakReference)或者儲存對需要清除的實體的引用。
  3)使用弱引用堵住記憶體洩漏:
  [1]全域性Map造成的記憶體洩漏:
  無意識物件保留最常見的原因是使用 Map 將後設資料與臨時物件(transient object)相關聯。假定一個物件具有中等生命週期,比分配它的那個方法呼叫的生命週期長,但是比應用程式的生命週期短,如客戶機的套接字連線。需要將一些後設資料與這個套接字關聯,如生成連線的使用者的標識。在建立 Socket 時是不知道這些資訊的,並且不能將資料新增到 Socket 物件上,因為不能控制 Socket 類或者它的子類。這時,典型的方法就是在一個全域性 Map 中儲存這些資訊:
public class SocketManager{
    private Map<Socket,User> m = new HashMap<Socket,User>();
    public void setUser(Socket s,User u)
    {
        m.put(s,u);
    }
    public User getUser(Socket s){
        return m.get(s);
    }
    public void removeUser(Socket s){
        m.remove(s);
    }
}
 
SocketManager socketManager;
//…
socketManager.setUser(socket,user);
  這種方法的問題是後設資料的生命週期需要與套接字的生命週期掛鉤,但是除非準確地知道什麼時候程式不再需要這個套接字,並記住從 Map 中刪除相應的對映,否則,Socket 和 User 物件將會永遠留在 Map 中,遠遠超過響應了請求和關閉套接字的時間。這會阻止 Socket 和User 物件被垃圾收集,即使應用程式不會再使用它們。這些物件留下來不受控制,很容易造成程式在長時間執行後記憶體爆滿。除了最簡單的情況,在幾乎所有情況下找出什麼時候
Socket 不再被程式使用是一件很煩人和容易出錯的任務,需要人工對記憶體進行管理。
  [2]弱引用記憶體洩漏程式碼:
  程式有記憶體洩漏的第一個跡象通常是它丟擲一個 OutOfMemoryError,或者因為頻繁的垃圾收集而表現出糟糕的效能。幸運的是,垃圾收集可以提供能夠用來診斷記憶體洩漏的大量資訊。如果以 -verbose:gc 或者 -Xloggc 選項呼叫 JVM,那麼每次 GC 執行時在控制檯上或者日誌檔案中會列印出一個診斷資訊,包括它所花費的時間、當前堆使用情況以及恢復了多少記憶體。記錄 GC 使用情況並不具有干擾性,因此如果需要分析記憶體問題或者調優垃圾收集器,在生產環境中預設啟用
GC 日誌是值得的。有工具可以利用 GC 日誌輸出並以圖形方式將它顯示出來,JTune 就是這樣的一種工具。觀察 GC 之後堆大小的圖,可以看到程式記憶體使用的趨勢。對於大多數程式來說,可以將記憶體使用分為兩部分:baseline 使用和 current load 使用。對於伺服器應用程式,baseline 使用就是應用程式在沒有任何負荷、但是已經準備好接受請求時的記憶體使用,current load 使用是在處理請求過程中使用的、但是在請求處理完成後會釋放的記憶體。只要負荷大體上是恆定的,應用程式通常會很快達到一個穩定的記憶體使用水平。如果在應用程式已經完成了其初始化並且負荷沒有增加的情況下,記憶體使用持續增加,那麼程式就可能在處理前面的請求時保留了生成的物件。
public class MapLeaker{
    public ExecuteService exec = Executors.newFixedThreadPool(5);
    public Map<Task,TaskStatus> taskStatus
        = Collections.synchronizedMap(new HashMap<Task,TaskStatus>());
    private Random random = new Random();
    private enum TaskStatus { NOT_STARTED, STARTED, FINISHED };
    private class Task implements Runnable{
        private int[] numbers = new int[random.nextInt(200)];
        public void run()
        {
            int[] temp = new int[random.nextInt(10000)];
            taskStatus.put(this,TaskStatus.STARTED);
            doSomework();
            taskStatus.put(this,TaskStatus.FINISHED);
        }
    }
    public Task newTask()
    {
        Task t = new Task();
        taskStatus.put(t,TaskStatus.NOT_STARTED);
        exec.execute(t);
        return t;
    }
}
  [3]使用弱引用堵住記憶體洩漏:
  SocketManager 的問題是 Socket-User 對映的生命週期應當與 Socket 的生命週期相匹配,但是語言沒有提供任何容易的方法實施這項規則。這使得程式不得不使用人工記憶體管理的老技術。幸運的是,從 JDK 1.2 開始,垃圾收集器提供了一種宣告這種物件生命週期依賴性的方法,這樣垃圾收集器就可以幫助我們防止這種記憶體洩漏——利用弱引用。弱引用是對一個物件(稱為 referent)的引用的持有者。使用弱引用後,可以維持對
referent 的引用,而不會阻止它被垃圾收集。當垃圾收集器跟蹤堆的時候,如果對一個物件的引用只有弱引用,那麼這個 referent 就會成為垃圾收集的候選物件,就像沒有任何剩餘的引用一樣,而且所有剩餘的弱引用都被清除。(只有弱引用的物件稱為弱可及(weakly reachable))WeakReference 的 referent 是在構造時設定的,在沒有被清除之前,可以用 get() 獲取它的值。如果弱引用被清除了(不管是
referent 已經被垃圾收集了,還是有人呼叫了 WeakReference.clear()),get() 會返回 null。相應地,在使用其結果之前,應當總是檢查get() 是否返回一個非 null 值,因為 referent 最終總是會被垃圾收集的。用一個普通的(強)引用拷貝一個物件引用時,限制 referent
的生命週期至少與被拷貝的引用的生命週期一樣長。如果不小心,那麼它可能就與程式的生命週期一樣——如果將一個物件放入一個全域性集合中的話。另一方面,在建立對一個物件的弱引用時,完全沒有擴充套件 referent 的生命週期,只是在物件仍然存活的時候,保持另一種到達它的方法。弱引用對於構造弱集合最有用,如那些在應用程式的其餘部分使用物件期間儲存關於這些物件的後設資料的集合——這就是 SocketManager 類所要做的工作。因為這是弱引用最常見的用法,WeakHashMap 也被新增到 JDK 1.2 的類庫中,它對鍵(而不是對值)使用弱引用。如果在一個普通
HashMap 中用一個物件作為鍵,那麼這個物件在對映從 Map 中刪除之前不能被回收,WeakHashMap 使您可以用一個物件作為 Map 鍵,同時不會阻止這個物件被垃圾收集。下邊的程式碼給出了 WeakHashMap 的 get() 方法的一種可能實現,它展示了弱引用的使用:
public class WeakHashMap<K,V> implements Map<K,V>
{
    private static class Entry<K,V> extends WeakReference<K> implements Map.Entry<K,V>
    {
        private V value;
        private final int hash;
        private Entry<K,V> next;
        // …
    }
 
    public V get(Object key)
    {
        int hash = getHash(key);
        Entry<K,V> e = getChain(hash);
        while(e != null)
        {
            k eKey = e.get();
            if( e.hash == hash && (key == eKey || key.equals(eKey)))
                return e.value;
            e = e.next;
        }
        return null;
    }
}
  呼叫 WeakReference.get() 時,它返回一個對 referent 的強引用(如果它仍然存活的話),因此不需要擔心對映在 while 迴圈體中消失,因為強引用會防止它被垃圾收集。WeakHashMap 的實現展示了弱引用的一種常見用法——一些內部物件擴充套件 WeakReference。其原因在下面一節討論引用佇列時會得到解釋。在向 WeakHashMap 中新增對映時,請記住對映可能會在以後“脫離”,因為鍵被垃圾收集了。在這種情況下,get()
返回 null,這使得測試 get() 的返回值是否為 null 變得比平時更重要了。
  [4]使用WeakHashMap堵住洩漏
  在 SocketManager 中防止洩漏很容易,只要用 WeakHashMap 代替 HashMap 就行了,如下邊程式碼所示。(如果 SocketManager 需要執行緒安全,那麼可以用 Collections.synchronizedMap() 包裝 WeakHashMap)。當對映的生命週期必須與鍵的生命週期聯絡在一起時,可以使用這種方法。不過,應當小心不濫用這種技術,大多數時候還是應當使用普通的 HashMap 作為 Map 的實現。
public class SocketManager{
    private Map<Socket,User> m = new WeakHashMap<Socket,User>();
    public void setUser(Socket s, User s)
    {
        m.put(s,u);
    }
    public User getUser(Socket s)
    {
        return m.get(s);
    }
}
  引用佇列:
  WeakHashMap 用弱引用承載對映鍵,這使得應用程式不再使用鍵物件時它們可以被垃圾收集,get() 實現可以根據 WeakReference.get() 是否返回 null 來區分死的對映和活的對映。但是這只是防止 Map 的記憶體消耗在應用程式的生命週期中不斷增加所需要做的工作的一半,還需要做一些工作以便在鍵物件被收集後從 Map 中刪除死項。否則,Map 會充滿對應於死鍵的項。雖然這對於應用程式是不可見的,但是它仍然會造成應用程式耗盡記憶體,因為即使鍵被收集了,Map.Entry 和值物件也不會被收集。可以通過週期性地掃描 Map,對每一個弱引用呼叫 get(),並在
get() 返回 null 時刪除那個對映而消除死對映。但是如果 Map 有許多活的項,那麼這種方法的效率很低。如果有一種方法可以在弱引用的 referent 被垃圾收集時發出通知就好了,這就是引用佇列的作用。引用佇列是垃圾收集器嚮應用程式返回關於物件生命週期的資訊的主要方法。弱引用有兩個建構函式:一個只取 referent 作為引數,另一個還取引用佇列作為引數。如果用關聯的引用佇列建立弱引用,在 referent 成為
GC 候選物件時,這個引用物件(不是referent)就在引用清除後加入 到引用佇列中。之後,應用程式從引用佇列提取引用並瞭解到它的 referent 已被收集,因此可以進行相應的清理活動,如去掉已不在弱集合中的物件的項。(引用佇列提供了與 BlockingQueue 同樣的出列模式 ——polled、timed blocking 和 untimed
blocking。)WeakHashMap 有一個名為 expungeStaleEntries() 的私有方法,大多數 Map 操作中會呼叫它,它去掉引用佇列中所有失效的引用,並刪除關聯的對映。
  4)關於Java中引用思考:
  先觀察一個列表:
級別 回收時間 用途 生存時間
強引用 從來不會被回收 物件的一般狀態 JVM停止執行時終止
軟引用 在記憶體不足時 在客戶端移除物件引用過後,除非再次啟用,否則就放在記憶體敏感的快取中 記憶體不足時終止
弱引用 在垃圾回收時,也就是客戶端已經移除了強引用,但是這種情況下記憶體還是客戶端引用可達的 阻止自動刪除不需要用的物件 GC執行後終止
虛引用[幽靈引用] 物件死亡之前,就是進行finalize()方法呼叫附近 特殊的清除過程 不定,當finalize()函式執行過後再回收,有可能之前就已經被回收了。

  可以這樣理解:

  SoftReference:假定垃圾回收器確定在某一時間點某個物件是軟可到達物件。這時,它可以選擇自動清除針對該物件的所有軟引用,以及通過強引用鏈,從其可以到達該物件的針對任何其他軟可到達物件的所有軟引用。在同一時間或晚些時候,它會將那些已經向引用佇列註冊的新清除的軟引用加入佇列。 軟可到達物件的所有軟引用都要保證在虛擬機器丟擲 OutOfMemoryError 之前已經被清除。否則,清除軟引用的時間或者清除不同物件的一組此類引用的順序將不受任何約束。然而,虛擬機器實現不鼓勵清除最近訪問或使用過的軟引用。 此類的直接例項可用於實現簡單快取;該類或其派生的子類還可用於更大型的資料結構,以實現更復雜的快取。只要軟引用的指示物件是強可到達物件,即正在實際使用的物件,就不會清除軟引用。例如,通過保持最近使用的項的強指示物件,並由垃圾回收器決定是否放棄剩餘的項,複雜的快取可以防止放棄最近使用的項。一般來說,WeakReference我們用來防止記憶體洩漏,保證記憶體物件被VM回收。

  WeakReference:弱引用物件,它們並不禁止其指示物件變得可終結,並被終結,然後被回收。弱引用最常用於實現規範化的對映。假定垃圾回收器確定在某一時間點上某個物件是弱可到達物件。這時,它將自動清除針對此物件的所有弱引用,以及通過強引用鏈和軟引用,可以從其到達該物件的針對任何其他弱可到達物件的所有弱引用。同時它將宣告所有以前的弱可到達物件為可終結的。在同一時間或晚些時候,它將那些已經向引用佇列註冊的新清除的弱引用加入佇列。 SoftReference多用作來實現cache機制,保證cache的有效性。
  PhantomReference:虛引用物件,在回收器確定其指示物件可另外回收之後,被加入佇列。虛引用最常見的用法是以某種可能比使用 Java 終結機制更靈活的方式來指派 pre-mortem 清除操作。如果垃圾回收器確定在某一特定時間點上虛引用的指示物件是虛可到達物件,那麼在那時或者在以後的某一時間,它會將該引用加入佇列。為了確保可回收的物件仍然保持原狀,虛引用的指示物件不能被檢索:虛引用的
get 方法總是返回 null。與軟引用和弱引用不同,虛引用在加入佇列時並沒有通過垃圾回收器自動清除。通過虛引用可到達的物件將仍然保持原狀,直到所有這類引用都被清除,或者它們都變得不可到達。
  以下是不確定概念
  【*:Java引用的深入部分一直都是討論得比較多的話題,上邊大部分為摘錄整理,這裡再談談我個人的一些看法。從整個JVM框架結構來看,Java的引用和垃圾回收器形成了針對Java記憶體堆的一個物件的“閉包管理集”,其中在基本程式碼裡面常用的就是強引用,強引用主要使用目的是就是程式設計的正常邏輯,這是所有的開發人員最容易理解的,而弱引用和軟引用的作用是比較耐人尋味的。按照引用強弱,其排序可以為:強引用——軟引用——弱引用——虛引用,為什麼這樣寫呢,實際上針對垃圾回收器而言,強引用是它絕對不會隨便去動的區域,因為在記憶體堆裡面的物件,只有當前物件不是強引用的時候,該物件才會進入垃圾回收器的目標區域。
  軟引用又可以理解為“記憶體應急引用”,也就是說它和GC是完整地配合操作的,為了防止記憶體洩漏,當GC在回收過程出現記憶體不足的時候,軟引用會被優先回收,從垃圾回收演算法上講,軟引用在設計的時候是很容易被垃圾回收器發現的。為什麼軟引用是處理告訴快取的優先選擇的,主要有兩個原因:第一,它對記憶體非常敏感,從抽象意義上講,我們甚至可以任何它和記憶體的變化緊緊繫結到一起操作的,因為記憶體一旦不足的時候,它會優先向垃圾回收器報警以提示記憶體不足;第二,它會盡量保證系統在OutOfMemoryError之前將物件直接設定成為不可達,以保證不會出現記憶體溢位的情況;所以使用軟引用來處理Java引用裡面的快取記憶體是很不錯的選擇。其實軟引用不僅僅和記憶體敏感,實際上和垃圾回收器的互動也是敏感的,這點可以這樣理解,因為當記憶體不足的時候,軟引用會報警,而這種報警會提示垃圾回收器針對目前的一些記憶體進行清除操作,而在有軟引用存在的記憶體堆裡面,垃圾回收器會第一時間反應,否則就會MemoryOut了。按照我們正常的思維來考慮,垃圾回收器針對我們呼叫System.gc()的時候,是不會輕易理睬的,因為僅僅是收到了來自強引用層程式碼的請求,至於它是否回收還得看JVM內部環境的條件是否滿足,但是如果是軟引用的方式去申請垃圾回收器會優先反應,只是我們在開發過程不能控制軟引用對垃圾回收器傳送垃圾回收申請,而JVM規範裡面也指出了軟引用不會輕易傳送申請到垃圾回收器。這裡還需要解釋的一點的是軟引用傳送申請不是說軟引用像我們呼叫System.gc()這樣直接申請垃圾回收,而是說軟引用會設定物件引用為null,而垃圾回收器針對該引用的這種做法也會優先響應,我們可以理解為是軟引用物件在向垃圾回收器傳送申請。反應快並不代表垃圾回收器會實時反應,還是會在尋找軟引用引用到的物件的時候遵循一定的回收規則,反應快在這裡的解釋是相對強引用設定物件為null,當軟引用設定物件為null的時候,該物件的被收集的優先順序比較高。
  弱引用是一種比軟引用相對複雜的引用,其實弱引用和軟引用都是Java程式可以控制的,也就是說可以通過程式碼直接使得引用針對弱可及物件以及軟可及物件是可引用的,軟引用和弱引用引用的物件實際上通過一定的程式碼操作是可重新啟用的,只是一般不會做這樣的操作,這樣的用法違背了最初的設計。弱引用和軟引用在垃圾回收器的目標範圍有一點點不同的就是,使用垃圾回收演算法是很難找到弱引用的,也就是說弱引用用來監控垃圾回收的整個流程也是一種很好的選擇,它不會影響垃圾回收的正常流程,這樣就可以規範化整個物件從設定為null了過後的一個生命週期的程式碼監控。而且因為弱引用是否存在對垃圾回收整個流程都不會造成影響,可以這樣認為,垃圾回收器找得到弱引用,該引用的物件就會被回收,如果找不到弱引用,一旦等到GC完成了垃圾回收過後,弱引用引用的物件佔用的記憶體也會自動釋放,這就是軟引用在垃圾回收過後的自動終止。
  最後談談虛引用,虛引用應該是JVM裡面最厲害的一種引用,它的厲害在於它可以在物件的記憶體從實體記憶體中清除掉了過後再引用該物件,也就是說當虛引用引用到物件的時候,這個物件實際已經從實體記憶體堆中清除掉了,如果我們不用手動對物件死亡或者瀕臨死亡進行處理的話,JVM會預設呼叫finalize函式,但是虛引用存在於該函式附近的生命週期內,所以可以手動對物件的這個範圍的週期進行監控。它之所以稱為“幽靈引用”就是因為該物件的實體記憶體已經不存在的,我個人覺得JVM儲存了一個物件狀態的映象索引,而這個映象索引裡面包含了物件在這個生命週期需要的所有內容,這裡的所需要就是這個生命週期內需要的物件資料內容,也就是物件死亡和瀕臨死亡之前finalize函式附近,至於強引用所需要的其他物件附加內容是不需要在這個映象裡面包含的,所以即使實體記憶體不存在,還是可以通過虛引用監控到該物件的,只是這種情況是否可以讓物件重新啟用為強引用我就不敢說了。因為虛引用在引用物件的過程不會去使得這個物件由Dead復活,而且這種物件是可以在回收週期進行回收的。】