JMM

1.JMM簡介
java

   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 longvolatile 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線程須要作三件事情分別是A一、A二、A3,而B是另一個線程具備操做B一、B2,若是把參考定位到B線程,那麼對A線程而言,B的操做B一、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、0x10一、0x10二、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 個默認類加載器 bootstrapextension 和 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 運行時能夠緩存這些訪問器以備之後使用,一些應用程序和框架還會緩存反射對象,這進一步增長了它們的本機內存佔用。c++

   4)JNI
  JNI支持本機代碼調用Java方法,反之亦然,Java運行時自己極大依賴於JNI代碼來實現類庫功能,好比文件和網絡I/O,JNI應用程序能夠經過三種方式增長Java運行時對本機內存的使用:
  • JNI應用程序的本機代碼被編譯到共享庫中,或編譯爲加載到進程地址空間中的可執行文件,大型本機應用程序可能僅僅加載就會佔用大量進程地址空間
  • 本機代碼必須與Java運行時共享地址空間,任何本機代碼分配本機代碼執行內存映射都會耗用Java運行時內存
  • 某些JNI函數可能在它們的常規操做中使用本機內存,GetTypeArrayElementsGetTypeArrayRegion函數能夠將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);git

// 得到一個可讀寫的隨機存取文件對象 
RAFile = new RandomAccessFile(filename,"rw");程序員

// 得到相應的文件通道 
fc = RAFile.getChannel();算法

// 取得文件的實際大小,以便映像到共享內存 
size = (int)fc.size();編程

// 得到共享內存緩衝區,該共享內存可讀寫 
mapBuf = fc.map(FileChannel.MAP_RW,0,size);bootstrap

// 獲取頭部消息:存取權限 數組

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_STARTEDSTARTEDFINISHED };
    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復活,並且這種對象是能夠在回收週期進行回收的。
相關文章
相關標籤/搜索