隨筆 - 100  文章 - 50  trackbacks - 0
          <2012年4月>
          25262728293031
          1234567
          891011121314
          15161718192021
          22232425262728
          293012345

          常用鏈接

          留言簿(3)

          隨筆分類

          隨筆檔案

          文章分類

          文章檔案

          收藏夾

          我收藏的一些文章!

          搜索

          •  

          最新評論

          閱讀排行榜

          評論排行榜

          本文章節:
          1.JMM簡介
          2.堆和棧
          3.本機內存
          4.防止內存泄漏

          1.JMM簡介
            i.內存模型概述
            Java平臺自動集成了線程以及多處理器技術,這種集成程度比Java以前誕生的計算機語言要厲害很多,該語言針對多種異構平臺的平臺獨立性而使用的多線程技術支持也是具有開拓性的一面,有時候在開發Java同步和線程安全要求很嚴格的程序時,往往容易混淆的一個概念就是內存模型。究竟什么是內存模型?內存模型描述了程序中各個變量(實例域、靜態域和數組元素)之間的關系,以及在實際計算機系統中將變量存儲到內存和從內存中取出變量這樣的底層細節,對象最終是存儲在內存里面的,這點沒有錯,但是編譯器、運行庫、處理器或者系統緩存可以有特權在變量指定內存位置存儲或者取出變量的值。【JMM】(Java Memory Model的縮寫)允許編譯器和緩存以數據在處理器特定的緩存(或寄存器)和主存之間移動的次序擁有重要的特權,除非程序員使用了finalsynchronized明確請求了某些可見性的保證。
            1)JSR133:
            在Java語言規范里面指出了JMM是一個比較開拓性的嘗試,這種嘗試視圖定義一個一致的、跨平臺的內存模型,但是它有一些比較細微而且很重要的缺點。其實Java語言里面比較容易混淆的關鍵字主要是synchronizedvolatile,也因為這樣在開發過程中往往開發者會忽略掉這些規則,這也使得編寫同步代碼比較困難。
            JSR133本身的目的是為了修復原本JMM的一些缺陷而提出的,其本身的制定目標有以下幾個:
          • 保留目前JVM的安全保證,以進行類型的安全檢查
          • 提供out-of-thin-air safety無中生有安全性,這樣“正確同步的”應該被正式而且直觀地定義
          • 程序員要有信心開發多線程程序,當然沒有其他辦法使得并發程序變得很容易開發,但是該規范的發布主要目標是為了減輕程序員理解內存模型中的一些細節負擔
          • 提供大范圍的流行硬件體系結構上的高性能JVM實現,現在的處理器在它們的內存模型上有著很大的不同,JMM應該能夠適合于實際的盡可能多的體系結構而不以性能為代價,這也是Java跨平臺型設計的基礎
          • 提供一個同步的習慣用法,以允許發布一個對象使他不用同步就可見,這種情況又稱為初始化安全(initialization safety)的新的安全保證
          • 對現有代碼應該只有最小限度的影響
            2)同步、異步【這里僅僅指概念上的理解,不牽涉到計算機底層基礎的一些操作】
            在系統開發過程,經常會遇到這幾個基本概念,不論是網絡通訊、對象之間的消息通訊還是Web開發人員常用的Http請求都會遇到這樣幾個概念,經常有人提到Ajax是異步通訊方式,那么究竟怎樣的方式是這樣的概念描述呢?
            同步:同步就是在發出一個功能調用的時候,在沒有得到響應之前,該調用就不返回,按照這樣的定義,其實大部分程序的執行都是同步調用的,一般情況下,在描述同步和異步操作的時候,主要是指代需要其他部件協作處理或者需要協作響應的一些任務處理。比如有一個線程A,在A執行的過程中,可能需要B提供一些相關的執行數據,當然觸發B響應的就是A向B發送一個請求或者說對B進行一個調用操作,如果A在執行該操作的時候是同步的方式,那么A就會停留在這個位置等待B給一個響應消息,在B沒有任何響應消息回來的時候,A不能做其他事情,只能等待,那么這樣的情況,A的操作就是一個同步的簡單說明。
            異步:步就是在發出一個功能調用的時候,不需要等待響應,繼續進行它該做的事情,一旦得到響應了過后給予一定的處理,但是不影響正常的處理過程的一種方式。比如有一個線程A,在A執行的過程中,同樣需要B提供一些相關數據或者操作,當A向B發送一個請求或者對B進行調用操作過后,A不需要繼續等待,而是執行A自己應該做的事情,一旦B有了響應過后會通知A,A接受到該異步請求的響應的時候會進行相關的處理,這種情況下A的操作就是一個簡單的異步操作。
            3)可見性、可排序性
            Java內存模型的兩個關鍵概念:可見性(Visibility可排序性(Ordering
            開發過多線程程序的程序員都明白,synchronized關鍵字強制實施一個線程之間的互斥鎖(相互排斥,該互斥鎖防止每次有多個線程進入一個給定監控器所保護的同步語句塊,也就是說在該情況下,執行程序代碼所獨有的某些內存是獨占模式其他的線程是不能針對它執行過程所獨占的內存進行訪問的,這種情況稱為該內存不可見。但是在該模型的同步模式中,還有另外一個方面:JMM中指出了,JVM在處理該強制實施的時候可以提供一些內存的可見規則,在該規則里面,它確保當存在一個同步塊時,緩存被更新,當輸入一個同步塊時,緩存失效。因此在JVM內部提供給定監控器保護的同步塊之中,一個線程所寫入的值對于其余所有的執行由同一個監控器保護的同步塊線程來說是可見的,這就是一個簡單的可見性的描述。這種機器保證編譯器不會把指令從一個同步塊的內部移到外部,雖然有時候它會把指令由外部移動到內部。JMM在缺省情況下不做這樣的保證——只要有多個線程訪問相同變量時必須使用同步。簡單總結:
            可見性就是在多核或者多線程運行過程中內存的一種共享模式,在JMM模型里面,通過并發線程修改變量值的時候,必須將線程變量同步回主存過后,其他線程才可能訪問到。
            【*:簡單講,內存的可見性使內存資源可以共享,當一個線程執行的時候它所占有的內存,如果它占有的內存資源是可見的,那么這時候其他線程在一定規則內是可以訪問該內存資源的,這種規則是由JMM內部定義的,這種情況下內存的該特性稱為其可見性。】
            可排序性提供了內存內部的訪問順序,在不同的程序針對不同的內存塊進行訪問的時候,其訪問不是無序的,比如有一個內存塊,A和B需要訪問的時候,JMM會提供一定的內存分配策略有序地分配它們使用的內存,而在內存的調用過程也會變得有序地進行,內存的折中性質可以簡單理解為有序性。而在Java多線程程序里面,JMM通過Java關鍵字volatile來保證內存的有序訪問。
            ii.JMM結構:
            1)簡單分析:
            Java語言規范中提到過,JVM中存在一個主存區(Main Memory或Java Heap Memory,Java中所有變量都是存在主存中的,對于所有線程進行共享,而每個線程又存在自己的工作內存(Working Memory,工作內存中保存的是主存中某些變量的拷貝,線程對所有變量的操作并非發生在主存區,而是發生在工作內存中,而線程之間是不能直接相互訪問,變量在程序中的傳遞,是依賴主存來完成的。而在多核處理器下,大部分數據存儲在高速緩存中,如果高速緩存不經過內存的時候,也是不可見的一種表現。在Java程序中,內存本身是比較昂貴的資源,其實不僅僅針對Java應用程序,對操作系統本身而言內存也屬于昂貴資源,Java程序在性能開銷過程中有幾個比較典型的可控制的來源。synchronizedvolatile關鍵字提供的內存中模型的可見性保證程序使用一個特殊的、存儲關卡(memory barrier的指令,來刷新緩存,使緩存無效,刷新硬件的寫緩存并且延遲執行的傳遞過程,無疑該機制會對Java程序的性能產生一定的影響。
            JMM的最初目的,就是為了能夠支持多線程程序設計的,每個線程可以認為是和其他線程不同的CPU上運行,或者對于多處理器的機器而言,該模型需要實現的就是使得每一個線程就像運行在不同的機器、不同的CPU或者本身就不同的線程上一樣,這種情況實際上在項目開發中是常見的。對于CPU本身而言,不能直接訪問其他CPU的寄存器,模型必須通過某種定義規則來使得線程和線程在工作內存中進行相互調用而實現CPU本身對其他CPU、或者說線程對其他線程的內存中資源的訪問,而表現這種規則的運行環境一般為運行該程序的運行宿主環境(操作系統、服務器、分布式系統等),而程序本身表現就依賴于編寫該程序的語言特性,這里也就是說用Java編寫的應用程序在內存管理中的實現就是遵循其部分原則,也就是前邊提及到的JMM定義了Java語言針對內存的一些的相關規則。然而,雖然設計之初是為了能夠更好支持多線程,但是該模型的應用和實現當然不局限于多處理器,而在JVM編譯器編譯Java編寫的程序的時候以及運行期執行該程序的時候,對于單CPU的系統而言,這種規則也是有效的,這就是是上邊提到的線程和線程之間的內存策略。JMM本身在描述過程沒有提過具體的內存地址以及在實現該策略中的實現方法是由JVM的哪一個環節(編譯器、處理器、緩存控制器、其他)提供的機制來實現的,甚至針對一個開發非常熟悉的程序員,也不一定能夠了解它內部對于類、對象、方法以及相關內容的一些具體可見的物理結構。相反,JMM定義了一個線程與主存之間的抽象關系,其實從上邊的圖可以知道,每一個線程可以抽象成為一個工作內存(抽象的高速緩存和寄存器),其中存儲了Java的一些值,該模型保證了Java里面的屬性、方法、字段存在一定的數學特性,按照該特性,該模型存儲了對應的一些內容,并且針對這些內容進行了一定的序列化以及存儲排序操作,這樣使得Java對象在工作內存里面被JVM順利調用,(當然這是比較抽象的一種解釋)既然如此,大多數JMM的規則在實現的時候,必須使得主存和工作內存之間的通信能夠得以保證,而且不能違反內存模型本身的結構,這是語言在設計之處必須考慮到的針對內存的一種設計方法。這里需要知道的一點是,這一切的操作在Java語言里面都是依靠Java語言自身來操作的,因為Java針對開發人員而言,內存的管理在不需要手動操作的情況下本身存在內存的管理策略,這也是Java自己進行內存管理的一種優勢。
            [1]原子性(Atomicity):
            這一點說明了該模型定義的規則針對原子級別的內容存在獨立的影響,對于模型設計最初,這些規則需要說明的僅僅是最簡單的讀取和存儲單元寫入的的一些操作,這種原子級別的包括——實例、靜態變量、數組元素,只是在該規則中不包括方法中的局部變量。
            [2]可見性(Visibility):
            在該規則的約束下,定義了一個線程在哪種情況下可以訪問另外一個線程或者影響另外一個線程,從JVM的操作上講包括了從另外一個線程的可見區域讀取相關數據以及將數據寫入到另外一個線程內。
            [3]可排序性(Ordering):
            該規則將會約束任何一個違背了規則調用的線程在操作過程中的一些順序,排序問題主要圍繞了讀取、寫入和賦值語句有關的序列。
            如果在該模型內部使用了一致的同步性的時候,這些屬性中的每一個屬性都遵循比較簡單的原則:和所有同步的內存塊一樣,每個同步塊之內的任何變化都具備了原子性以及可見性,和其他同步方法以及同步塊遵循同樣一致的原則,而且在這樣的一個模型內,每個同步塊不能使用同一個鎖,在整個程序的調用過程是按照編寫的程序指定指令運行的。即使某一個同步塊內的處理可能會失效,但是該問題不會影響到其他線程的同步問題,也不會引起連環失效。簡單講:當程序運行的時候使用了一致的同步性的時候,每個同步塊有一個獨立的空間以及獨立的同步控制器和鎖機制,然后對外按照JVM的執行指令進行數據的讀寫操作。這種情況使得使用內存的過程變得非常嚴謹!
            如果不使用同步或者說使用同步不一致這里可以理解為異步,但不一定是異步操作,該程序執行的答案就會變得極其復雜。而且在這樣的情況下,該內存模型處理的結果比起大多數程序員所期望的結果而言就變得十分脆弱,甚至比起JVM提供的實現都脆弱很多。因為這樣所以出現了Java針對該內存操作的最簡單的語言規范來進行一定的習慣限制,排除該情況發生的做法在于:
            JVM線程必須依靠自身來維持對象的可見性以及對象自身應該提供相對應的操作而實現整個內存操作的三個特性,而不是僅僅依靠特定的修改對象狀態的線程來完成如此復雜的一個流程。
            【*:綜上所屬,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線程需要做三件事情分別是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]
            它的最高有效字節MSBMost Significant Byte為:[Xw-1,Xw-2,...,Xw-8]
            最低有效字節LSBLeast 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引用以及返回值都占據了一個數組中的局部變量的條目,而byteshortchar則在存儲到局部變量的時候是先轉化成為int再進行操作的,則longdouble則是在這樣一個數組里面使用了兩個元素的空間大小,在局部變量里面存儲基本數據類型的時候使用的就是這樣的結構。舉個例子:
          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 運行時可以緩存這些訪問器以備以后使用,一些應用程序和框架還會緩存反射對象,這進一步增加了它們的本機內存占用。

            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();
                      forint 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>();
                  forint 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):
            不通過強引用訪問的對象,即不是強可及對象,但是可以通過軟引用訪問的對象就成為軟可及對象,軟可及對象就需要使用類SoftReferencejava.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):
            不是強可及對象同樣也不是軟可及對象,僅僅通過弱引用WeakReferencejava.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):
            不是強可及對象,也不是軟可及對象,同樣不是弱可及對象,之所以把虛可及對象放到最后來講,主要也是因為它的特殊性,有時候我們又稱之為“幽靈對象”,已經結束的,可以通過虛引用來訪問該對象。我們使用類PhantomReferencejava.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 reachableWeakReference 的 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,Vimplements Map<K,V>
          {
              private static class Entry<K,Vextends WeakReference<Kimplements 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復活,而且這種對象是可以在回收周期進行回收的。

          posted on 2012-04-26 00:19 fly 閱讀(553) 評論(0)  編輯  收藏

          只有注冊用戶登錄后才能發表評論。


          網站導航:
           
          主站蜘蛛池模板: 靖宇县| 麻阳| 于田县| 白河县| 隆回县| 萍乡市| 洛南县| 太仆寺旗| 新野县| 汝城县| 梓潼县| 营山县| 盈江县| 靖安县| 乌兰察布市| 达尔| 蕲春县| 东港市| 华坪县| 潜山县| 原平市| 吉安市| 西安市| 桐城市| 正宁县| 阿鲁科尔沁旗| 昌宁县| 弥勒县| 博乐市| 旬阳县| 嘉义市| 天峻县| 秦安县| 彝良县| 泾源县| 阆中市| 徐闻县| 龙泉市| 阿拉善右旗| 涿州市| 龙门县|