<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

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

    常用鏈接

    留言簿(3)

    隨筆分類

    隨筆檔案

    文章分類

    文章檔案

    收藏夾

    我收藏的一些文章!

    搜索

    •  

    最新評(píng)論

    閱讀排行榜

    評(píng)論排行榜

    本文章節(jié):
    1.JMM簡介
    2.堆和棧
    3.本機(jī)內(nèi)存
    4.防止內(nèi)存泄漏

    1.JMM簡介
      i.內(nèi)存模型概述
      Java平臺(tái)自動(dòng)集成了線程以及多處理器技術(shù),這種集成程度比Java以前誕生的計(jì)算機(jī)語言要厲害很多,該語言針對多種異構(gòu)平臺(tái)的平臺(tái)獨(dú)立性而使用的多線程技術(shù)支持也是具有開拓性的一面,有時(shí)候在開發(fā)Java同步和線程安全要求很嚴(yán)格的程序時(shí),往往容易混淆的一個(gè)概念就是內(nèi)存模型。究竟什么是內(nèi)存模型?內(nèi)存模型描述了程序中各個(gè)變量(實(shí)例域、靜態(tài)域和數(shù)組元素)之間的關(guān)系,以及在實(shí)際計(jì)算機(jī)系統(tǒng)中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié),對象最終是存儲(chǔ)在內(nèi)存里面的,這點(diǎn)沒有錯(cuò),但是編譯器、運(yùn)行庫、處理器或者系統(tǒng)緩存可以有特權(quán)在變量指定內(nèi)存位置存儲(chǔ)或者取出變量的值?!?strong>JMM】(Java Memory Model的縮寫)允許編譯器和緩存以數(shù)據(jù)在處理器特定的緩存(或寄存器)和主存之間移動(dòng)的次序擁有重要的特權(quán),除非程序員使用了finalsynchronized明確請求了某些可見性的保證。
      1)JSR133:
      在Java語言規(guī)范里面指出了JMM是一個(gè)比較開拓性的嘗試,這種嘗試視圖定義一個(gè)一致的、跨平臺(tái)的內(nèi)存模型,但是它有一些比較細(xì)微而且很重要的缺點(diǎn)。其實(shí)Java語言里面比較容易混淆的關(guān)鍵字主要是synchronizedvolatile,也因?yàn)檫@樣在開發(fā)過程中往往開發(fā)者會(huì)忽略掉這些規(guī)則,這也使得編寫同步代碼比較困難。
      JSR133本身的目的是為了修復(fù)原本JMM的一些缺陷而提出的,其本身的制定目標(biāo)有以下幾個(gè):
    • 保留目前JVM的安全保證,以進(jìn)行類型的安全檢查
    • 提供out-of-thin-air safety無中生有安全性,這樣“正確同步的”應(yīng)該被正式而且直觀地定義
    • 程序員要有信心開發(fā)多線程程序,當(dāng)然沒有其他辦法使得并發(fā)程序變得很容易開發(fā),但是該規(guī)范的發(fā)布主要目標(biāo)是為了減輕程序員理解內(nèi)存模型中的一些細(xì)節(jié)負(fù)擔(dān)
    • 提供大范圍的流行硬件體系結(jié)構(gòu)上的高性能JVM實(shí)現(xiàn),現(xiàn)在的處理器在它們的內(nèi)存模型上有著很大的不同,JMM應(yīng)該能夠適合于實(shí)際的盡可能多的體系結(jié)構(gòu)而不以性能為代價(jià),這也是Java跨平臺(tái)型設(shè)計(jì)的基礎(chǔ)
    • 提供一個(gè)同步的習(xí)慣用法,以允許發(fā)布一個(gè)對象使他不用同步就可見,這種情況又稱為初始化安全(initialization safety)的新的安全保證
    • 對現(xiàn)有代碼應(yīng)該只有最小限度的影響
      2)同步、異步【這里僅僅指概念上的理解,不牽涉到計(jì)算機(jī)底層基礎(chǔ)的一些操作】
      在系統(tǒng)開發(fā)過程,經(jīng)常會(huì)遇到這幾個(gè)基本概念,不論是網(wǎng)絡(luò)通訊、對象之間的消息通訊還是Web開發(fā)人員常用的Http請求都會(huì)遇到這樣幾個(gè)概念,經(jīng)常有人提到Ajax是異步通訊方式,那么究竟怎樣的方式是這樣的概念描述呢?
      同步:同步就是在發(fā)出一個(gè)功能調(diào)用的時(shí)候,在沒有得到響應(yīng)之前,該調(diào)用就不返回,按照這樣的定義,其實(shí)大部分程序的執(zhí)行都是同步調(diào)用的,一般情況下,在描述同步和異步操作的時(shí)候,主要是指代需要其他部件協(xié)作處理或者需要協(xié)作響應(yīng)的一些任務(wù)處理。比如有一個(gè)線程A,在A執(zhí)行的過程中,可能需要B提供一些相關(guān)的執(zhí)行數(shù)據(jù),當(dāng)然觸發(fā)B響應(yīng)的就是A向B發(fā)送一個(gè)請求或者說對B進(jìn)行一個(gè)調(diào)用操作,如果A在執(zhí)行該操作的時(shí)候是同步的方式,那么A就會(huì)停留在這個(gè)位置等待B給一個(gè)響應(yīng)消息,在B沒有任何響應(yīng)消息回來的時(shí)候,A不能做其他事情,只能等待,那么這樣的情況,A的操作就是一個(gè)同步的簡單說明。
      異步:步就是在發(fā)出一個(gè)功能調(diào)用的時(shí)候,不需要等待響應(yīng),繼續(xù)進(jìn)行它該做的事情,一旦得到響應(yīng)了過后給予一定的處理,但是不影響正常的處理過程的一種方式。比如有一個(gè)線程A,在A執(zhí)行的過程中,同樣需要B提供一些相關(guān)數(shù)據(jù)或者操作,當(dāng)A向B發(fā)送一個(gè)請求或者對B進(jìn)行調(diào)用操作過后,A不需要繼續(xù)等待,而是執(zhí)行A自己應(yīng)該做的事情,一旦B有了響應(yīng)過后會(huì)通知A,A接受到該異步請求的響應(yīng)的時(shí)候會(huì)進(jìn)行相關(guān)的處理,這種情況下A的操作就是一個(gè)簡單的異步操作。
      3)可見性、可排序性
      Java內(nèi)存模型的兩個(gè)關(guān)鍵概念:可見性(Visibility可排序性(Ordering
      開發(fā)過多線程程序的程序員都明白,synchronized關(guān)鍵字強(qiáng)制實(shí)施一個(gè)線程之間的互斥鎖(相互排斥,該互斥鎖防止每次有多個(gè)線程進(jìn)入一個(gè)給定監(jiān)控器所保護(hù)的同步語句塊,也就是說在該情況下,執(zhí)行程序代碼所獨(dú)有的某些內(nèi)存是獨(dú)占模式,其他的線程是不能針對它執(zhí)行過程所獨(dú)占的內(nèi)存進(jìn)行訪問的,這種情況稱為該內(nèi)存不可見。但是在該模型的同步模式中,還有另外一個(gè)方面:JMM中指出了,JVM在處理該強(qiáng)制實(shí)施的時(shí)候可以提供一些內(nèi)存的可見規(guī)則,在該規(guī)則里面,它確保當(dāng)存在一個(gè)同步塊時(shí),緩存被更新,當(dāng)輸入一個(gè)同步塊時(shí),緩存失效。因此在JVM內(nèi)部提供給定監(jiān)控器保護(hù)的同步塊之中,一個(gè)線程所寫入的值對于其余所有的執(zhí)行由同一個(gè)監(jiān)控器保護(hù)的同步塊線程來說是可見的,這就是一個(gè)簡單的可見性的描述。這種機(jī)器保證編譯器不會(huì)把指令從一個(gè)同步塊的內(nèi)部移到外部,雖然有時(shí)候它會(huì)把指令由外部移動(dòng)到內(nèi)部。JMM在缺省情況下不做這樣的保證——只要有多個(gè)線程訪問相同變量時(shí)必須使用同步。簡單總結(jié):
      可見性就是在多核或者多線程運(yùn)行過程中內(nèi)存的一種共享模式,在JMM模型里面,通過并發(fā)線程修改變量值的時(shí)候,必須將線程變量同步回主存過后,其他線程才可能訪問到。
      【*:簡單講,內(nèi)存的可見性使內(nèi)存資源可以共享,當(dāng)一個(gè)線程執(zhí)行的時(shí)候它所占有的內(nèi)存,如果它占有的內(nèi)存資源是可見的,那么這時(shí)候其他線程在一定規(guī)則內(nèi)是可以訪問該內(nèi)存資源的,這種規(guī)則是由JMM內(nèi)部定義的,這種情況下內(nèi)存的該特性稱為其可見性?!?/span>
      可排序性提供了內(nèi)存內(nèi)部的訪問順序,在不同的程序針對不同的內(nèi)存塊進(jìn)行訪問的時(shí)候,其訪問不是無序的,比如有一個(gè)內(nèi)存塊,A和B需要訪問的時(shí)候,JMM會(huì)提供一定的內(nèi)存分配策略有序地分配它們使用的內(nèi)存,而在內(nèi)存的調(diào)用過程也會(huì)變得有序地進(jìn)行,內(nèi)存的折中性質(zhì)可以簡單理解為有序性。而在Java多線程程序里面,JMM通過Java關(guān)鍵字volatile來保證內(nèi)存的有序訪問。
      ii.JMM結(jié)構(gòu):
      1)簡單分析:
      Java語言規(guī)范中提到過,JVM中存在一個(gè)主存區(qū)(Main Memory或Java Heap Memory,Java中所有變量都是存在主存中的,對于所有線程進(jìn)行共享,而每個(gè)線程又存在自己的工作內(nèi)存(Working Memory,工作內(nèi)存中保存的是主存中某些變量的拷貝,線程對所有變量的操作并非發(fā)生在主存區(qū),而是發(fā)生在工作內(nèi)存中,而線程之間是不能直接相互訪問,變量在程序中的傳遞,是依賴主存來完成的。而在多核處理器下,大部分?jǐn)?shù)據(jù)存儲(chǔ)在高速緩存中,如果高速緩存不經(jīng)過內(nèi)存的時(shí)候,也是不可見的一種表現(xiàn)。在Java程序中,內(nèi)存本身是比較昂貴的資源,其實(shí)不僅僅針對Java應(yīng)用程序,對操作系統(tǒng)本身而言內(nèi)存也屬于昂貴資源,Java程序在性能開銷過程中有幾個(gè)比較典型的可控制的來源。synchronizedvolatile關(guān)鍵字提供的內(nèi)存中模型的可見性保證程序使用一個(gè)特殊的、存儲(chǔ)關(guān)卡(memory barrier的指令,來刷新緩存,使緩存無效,刷新硬件的寫緩存并且延遲執(zhí)行的傳遞過程,無疑該機(jī)制會(huì)對Java程序的性能產(chǎn)生一定的影響。
      JMM的最初目的,就是為了能夠支持多線程程序設(shè)計(jì)的,每個(gè)線程可以認(rèn)為是和其他線程不同的CPU上運(yùn)行,或者對于多處理器的機(jī)器而言,該模型需要實(shí)現(xiàn)的就是使得每一個(gè)線程就像運(yùn)行在不同的機(jī)器、不同的CPU或者本身就不同的線程上一樣,這種情況實(shí)際上在項(xiàng)目開發(fā)中是常見的。對于CPU本身而言,不能直接訪問其他CPU的寄存器,模型必須通過某種定義規(guī)則來使得線程和線程在工作內(nèi)存中進(jìn)行相互調(diào)用而實(shí)現(xiàn)CPU本身對其他CPU、或者說線程對其他線程的內(nèi)存中資源的訪問,而表現(xiàn)這種規(guī)則的運(yùn)行環(huán)境一般為運(yùn)行該程序的運(yùn)行宿主環(huán)境(操作系統(tǒng)、服務(wù)器、分布式系統(tǒng)等),而程序本身表現(xiàn)就依賴于編寫該程序的語言特性,這里也就是說用Java編寫的應(yīng)用程序在內(nèi)存管理中的實(shí)現(xiàn)就是遵循其部分原則,也就是前邊提及到的JMM定義了Java語言針對內(nèi)存的一些的相關(guān)規(guī)則。然而,雖然設(shè)計(jì)之初是為了能夠更好支持多線程,但是該模型的應(yīng)用和實(shí)現(xiàn)當(dāng)然不局限于多處理器,而在JVM編譯器編譯Java編寫的程序的時(shí)候以及運(yùn)行期執(zhí)行該程序的時(shí)候,對于單CPU的系統(tǒng)而言,這種規(guī)則也是有效的,這就是是上邊提到的線程和線程之間的內(nèi)存策略。JMM本身在描述過程沒有提過具體的內(nèi)存地址以及在實(shí)現(xiàn)該策略中的實(shí)現(xiàn)方法是由JVM的哪一個(gè)環(huán)節(jié)(編譯器、處理器、緩存控制器、其他)提供的機(jī)制來實(shí)現(xiàn)的,甚至針對一個(gè)開發(fā)非常熟悉的程序員,也不一定能夠了解它內(nèi)部對于類、對象、方法以及相關(guān)內(nèi)容的一些具體可見的物理結(jié)構(gòu)。相反,JMM定義了一個(gè)線程與主存之間的抽象關(guān)系,其實(shí)從上邊的圖可以知道,每一個(gè)線程可以抽象成為一個(gè)工作內(nèi)存(抽象的高速緩存和寄存器),其中存儲(chǔ)了Java的一些值,該模型保證了Java里面的屬性、方法、字段存在一定的數(shù)學(xué)特性,按照該特性,該模型存儲(chǔ)了對應(yīng)的一些內(nèi)容,并且針對這些內(nèi)容進(jìn)行了一定的序列化以及存儲(chǔ)排序操作,這樣使得Java對象在工作內(nèi)存里面被JVM順利調(diào)用,(當(dāng)然這是比較抽象的一種解釋)既然如此,大多數(shù)JMM的規(guī)則在實(shí)現(xiàn)的時(shí)候,必須使得主存和工作內(nèi)存之間的通信能夠得以保證,而且不能違反內(nèi)存模型本身的結(jié)構(gòu),這是語言在設(shè)計(jì)之處必須考慮到的針對內(nèi)存的一種設(shè)計(jì)方法。這里需要知道的一點(diǎn)是,這一切的操作在Java語言里面都是依靠Java語言自身來操作的,因?yàn)镴ava針對開發(fā)人員而言,內(nèi)存的管理在不需要手動(dòng)操作的情況下本身存在內(nèi)存的管理策略,這也是Java自己進(jìn)行內(nèi)存管理的一種優(yōu)勢。
      [1]原子性(Atomicity):
      這一點(diǎn)說明了該模型定義的規(guī)則針對原子級(jí)別的內(nèi)容存在獨(dú)立的影響,對于模型設(shè)計(jì)最初,這些規(guī)則需要說明的僅僅是最簡單的讀取和存儲(chǔ)單元寫入的的一些操作,這種原子級(jí)別的包括——實(shí)例、靜態(tài)變量、數(shù)組元素,只是在該規(guī)則中不包括方法中的局部變量。
      [2]可見性(Visibility):
      在該規(guī)則的約束下,定義了一個(gè)線程在哪種情況下可以訪問另外一個(gè)線程或者影響另外一個(gè)線程,從JVM的操作上講包括了從另外一個(gè)線程的可見區(qū)域讀取相關(guān)數(shù)據(jù)以及將數(shù)據(jù)寫入到另外一個(gè)線程內(nèi)。
      [3]可排序性(Ordering):
      該規(guī)則將會(huì)約束任何一個(gè)違背了規(guī)則調(diào)用的線程在操作過程中的一些順序,排序問題主要圍繞了讀取、寫入和賦值語句有關(guān)的序列。
      如果在該模型內(nèi)部使用了一致的同步性的時(shí)候,這些屬性中的每一個(gè)屬性都遵循比較簡單的原則:和所有同步的內(nèi)存塊一樣,每個(gè)同步塊之內(nèi)的任何變化都具備了原子性以及可見性,和其他同步方法以及同步塊遵循同樣一致的原則,而且在這樣的一個(gè)模型內(nèi),每個(gè)同步塊不能使用同一個(gè)鎖,在整個(gè)程序的調(diào)用過程是按照編寫的程序指定指令運(yùn)行的。即使某一個(gè)同步塊內(nèi)的處理可能會(huì)失效,但是該問題不會(huì)影響到其他線程的同步問題,也不會(huì)引起連環(huán)失效。簡單講:當(dāng)程序運(yùn)行的時(shí)候使用了一致的同步性的時(shí)候,每個(gè)同步塊有一個(gè)獨(dú)立的空間以及獨(dú)立的同步控制器和鎖機(jī)制,然后對外按照J(rèn)VM的執(zhí)行指令進(jìn)行數(shù)據(jù)的讀寫操作。這種情況使得使用內(nèi)存的過程變得非常嚴(yán)謹(jǐn)!
      如果不使用同步或者說使用同步不一致這里可以理解為異步,但不一定是異步操作,該程序執(zhí)行的答案就會(huì)變得極其復(fù)雜。而且在這樣的情況下,該內(nèi)存模型處理的結(jié)果比起大多數(shù)程序員所期望的結(jié)果而言就變得十分脆弱,甚至比起JVM提供的實(shí)現(xiàn)都脆弱很多。因?yàn)檫@樣所以出現(xiàn)了Java針對該內(nèi)存操作的最簡單的語言規(guī)范來進(jìn)行一定的習(xí)慣限制,排除該情況發(fā)生的做法在于:
      JVM線程必須依靠自身來維持對象的可見性以及對象自身應(yīng)該提供相對應(yīng)的操作而實(shí)現(xiàn)整個(gè)內(nèi)存操作的三個(gè)特性,而不是僅僅依靠特定的修改對象狀態(tài)的線程來完成如此復(fù)雜的一個(gè)流程。
      【*:綜上所屬,JMM在JVM內(nèi)部實(shí)現(xiàn)的結(jié)構(gòu)就變得相對復(fù)雜,當(dāng)然一般的Java初學(xué)者可以不用了解得這么深入?!?/span>
      [4]三個(gè)特性的解析(針對JMM內(nèi)部):
      原子性(Atomicity):
      訪問存儲(chǔ)單元內(nèi)的任何類型的字段的值以及對其更新操作的時(shí)候,除開long類型和double類型,其他類型的字段是必須要保證其原子性的,這些字段也包括為對象服務(wù)的引用。此外,該原子性規(guī)則擴(kuò)展可以延伸到基于long和double的另外兩種類型volatile longvolatile double(volatile為java關(guān)鍵字),沒有被volatile聲明的long類型以及double類型的字段值雖然不保證其JMM中的原子性,但是是被允許的。針對non-long/non-double的字段在表達(dá)式中使用的時(shí)候,JMM的原子性有這樣一種規(guī)則:如果你獲得或者初始化該值或某一些值的時(shí)候,這些值是由其他線程寫入,而且不是從兩個(gè)或者多個(gè)線程產(chǎn)生的數(shù)據(jù)在同一時(shí)間戳混合寫入的時(shí)候,該字段的原子性在JVM內(nèi)部是必須得到保證的。也就是說JMM在定義JVM原子性的時(shí)候,只要在該規(guī)則不違反的條件下,JVM本身不去理睬該數(shù)據(jù)的值是來自于什么線程,因?yàn)檫@樣使得Java語言在并行運(yùn)算的設(shè)計(jì)的過程中針對多線程的原子性設(shè)計(jì)變得極其簡單,而且即使開發(fā)人員沒有考慮到最終的程序也沒有太大的影響。再次解釋一下:這里的原子性指的是原子級(jí)別的操作,比如最小的一塊內(nèi)存的讀寫操作,可以理解為Java語言最終編譯過后最接近內(nèi)存的最底層的操作單元,這種讀寫操作的數(shù)據(jù)單元不是變量的值,而是本機(jī)碼,也就是前邊在講《Java基礎(chǔ)知識(shí)》中提到的由運(yùn)行器解釋的時(shí)候生成的Native Code。
      可見性(Visibility):
      當(dāng)一個(gè)線程需要修改另外線程的可見單元的時(shí)候必須遵循以下原則:
    • 一個(gè)寫入線程釋放的同步鎖和緊隨其后進(jìn)行讀取的讀線程的同步鎖是同一個(gè)
      從本質(zhì)上講,釋放鎖操作強(qiáng)迫它的隸屬線程釋放鎖的線程從工作內(nèi)存中的寫入緩存里面刷新(專業(yè)上講這里不應(yīng)該是刷新,可以理解為提供)數(shù)據(jù)(flush操作),然后獲取鎖操作使得另外一個(gè)線程獲得鎖的線程直接讀取前一個(gè)線程可訪問域(也就是可見區(qū)域)的字段的值。因?yàn)樵撴i內(nèi)部提供了一個(gè)同步方法或者同步塊,該同步內(nèi)容具有線程排他性這樣就使得上邊兩個(gè)操作只能針對單一線程在同步內(nèi)容內(nèi)部進(jìn)行操作,這樣就使得所有操作該內(nèi)容的單一線程具有該同步內(nèi)容(加鎖的同步方法或者同步塊)內(nèi)的線程排他性,這種情況的交替也可以理解為具有短暫記憶效應(yīng)”。
      這里需要理解的是同步雙重含義使用鎖機(jī)制允許基于高層同步協(xié)議進(jìn)行處理操作,這是最基本的同步;同時(shí)系統(tǒng)內(nèi)存(很多時(shí)候這里是指基于機(jī)器指令的底層存儲(chǔ)關(guān)卡memory barrier,前邊提到過)在處理同步的時(shí)候能夠跨線程操作,使得線程和線程之間的數(shù)據(jù)是同步的。這樣的機(jī)制也折射出一點(diǎn),并行編程相對于順序編程而言,更加類似于分布式編程。后一種同步可以作為JMM機(jī)制中的方法在一個(gè)線程中運(yùn)行的效果展示,注意這里不是多個(gè)線程運(yùn)行的效果展示,因?yàn)樗磻?yīng)了該線程愿意發(fā)送或者接受的雙重操作,并且使得它自己的可見區(qū)域可以提供給其他線程運(yùn)行或者更新,從這個(gè)角度來看,使用消息傳遞可以視為相互之間的變量同步,因?yàn)橄鄬ζ渌€程而言,它的操作針對其他線程也是對等的。
    • 一旦某個(gè)字段被申明為volatile,在任何一個(gè)寫入線程在工作內(nèi)存中刷新緩存的之前需要進(jìn)行進(jìn)一步的內(nèi)存操作,也就是說針對這樣的字段進(jìn)行立即刷新,可以理解為這種volatile不會(huì)出現(xiàn)一般變量的緩存操作,而讀取線程每次必須根據(jù)前一個(gè)線程的可見域里面重新讀取該變量的值,而不是直接讀取
    • 當(dāng)某個(gè)線程第一次去訪問某個(gè)對象的域的時(shí)候,它要么初始化該對象的值,要么從其他寫入線程可見域里面去讀取該對象的值這里結(jié)合上邊理解,在滿足某種條件下,該線程對某對象域的值的讀取是直接讀取,有些時(shí)候卻需要重新讀取。
      這里需要小心一點(diǎn)的是,在并發(fā)編程里面,不好的一個(gè)實(shí)踐就是使用一個(gè)合法引用去引用不完全構(gòu)造的對象,這種情況在從其他寫入線程可見域里面進(jìn)行數(shù)據(jù)讀取的時(shí)候發(fā)生頻率比較高。從編程角度上講,在構(gòu)造函數(shù)里面開啟一個(gè)新的線程是有一定的風(fēng)險(xiǎn)的,特別是該類是屬于一個(gè)可子類化的類的時(shí)候。Thread.start由調(diào)用線程啟動(dòng),然后由獲得該啟動(dòng)的線程釋放鎖具有相同的“短暫記憶效應(yīng)”,如果一個(gè)實(shí)現(xiàn)了Runnable接口的超類在子類構(gòu)造子執(zhí)行之前調(diào)用了Thread(this).start()方法,那么就可能使得該對象在線程方法run執(zhí)行之前并沒有被完全初始化這樣就使得一個(gè)指向該對象的合法引用去引用了不完全構(gòu)造的一個(gè)對象。同樣的,如果創(chuàng)建一個(gè)新的線程T并且啟動(dòng)該線程,然后再使用線程T來創(chuàng)建對象X,這種情況就不能保證X對象里面所有的屬性針對線程T都是可見的除非是在所有針對X對象的引用中進(jìn)行同步處理,或者最好的方法是在T線程啟動(dòng)之前創(chuàng)建對象X。
    • 若一個(gè)線程終止,所有的變量值都必須從工作內(nèi)存中刷到主存比如,如果一個(gè)同步線程因?yàn)榱硪粋€(gè)使用Thread.join方法的線程而終止,那么該線程的可見域針對那個(gè)線程而言其發(fā)生的改變以及產(chǎn)生的一些影響是需要保證可知道的。
      注意:如果在同一個(gè)線程里面通過方法調(diào)用去傳一個(gè)對象的引用是絕對不會(huì)出現(xiàn)上邊提及到的可見性問題的。JMM保證所有上邊的規(guī)定以及關(guān)于內(nèi)存可見性特性的描述——一個(gè)特殊的更新、一個(gè)特定字段的修改都是某個(gè)線程針對其他線程的一個(gè)“可見性”的概念,最終它發(fā)生的場所在內(nèi)存模型中Java線程和線程之間,至于這個(gè)發(fā)生時(shí)間可以是一個(gè)任意長的時(shí)間,但是最終會(huì)發(fā)生,也就是說,Java內(nèi)存模型中的可見性的特性主要是針對線程和線程之間使用內(nèi)存的一種規(guī)則和約定,該約定由JMM定義。
      不僅僅如此,該模型還允許不同步的情況下可見性特性。比如針對一個(gè)線程提供一個(gè)對象或者字段訪問域的原始值進(jìn)行操作,而針對另外一個(gè)線程提供一個(gè)對象或者字段刷新過后的值進(jìn)行操作。同樣也有可能針對一個(gè)線程讀取一個(gè)原始的值以及引用對象的對象內(nèi)容,針對另外一個(gè)線程讀取一個(gè)刷新過后的值或者刷新過后的引用。
      盡管如此,上邊的可見性特性分析的一些特征在跨線程操作的時(shí)候是有可能失敗的,而且不能夠避免這些故障發(fā)生。這是一個(gè)不爭的事實(shí),使用同步多線程的代碼并不能絕對保證線程安全的行為,只是允許某種規(guī)則對其操作進(jìn)行一定的限制,但是在最新的JVM實(shí)現(xiàn)以及最新的Java平臺(tái)中,即使是多個(gè)處理器,通過一些工具進(jìn)行可見性的測試發(fā)現(xiàn)其實(shí)是很少發(fā)生故障的??缇€程共享CPU的共享緩存的使用,其缺陷就在于影響了編譯器的優(yōu)化操作,這也體現(xiàn)了強(qiáng)有力的緩存一致性使得硬件的價(jià)值有所提升,因?yàn)樗鼈冎g的關(guān)系在線程與線程之間的復(fù)雜度變得更高。這種方式使得可見度的自由測試顯得更加不切實(shí)際,因?yàn)檫@些錯(cuò)誤的發(fā)生極為罕見,或者說在平臺(tái)上我們開發(fā)過程中根本碰不到。在并行程開發(fā)中,不使用同步導(dǎo)致失敗的原因也不僅僅是對可見度的不良把握導(dǎo)致的,導(dǎo)致其程序失敗的原因是多方面的,包括緩存一致性、內(nèi)存一致性問題等。
      可排序性(Ordering):
      可排序規(guī)則在線程與線程之間主要有下邊兩點(diǎn):
    • 從操作線程的角度看來,如果所有的指令執(zhí)行都是按照普通順序進(jìn)行,那么對于一個(gè)順序運(yùn)行的程序而言,可排序性也是順序的
    • 從其他操作線程的角度看來,排序性如同在這個(gè)線程中運(yùn)行在非同步方法中的一個(gè)“間諜”,所以任何事情都有可能發(fā)生。唯一有用的限制是同步方法和同步塊的相對排序,就像操作volatile字段一樣,總是保留下來使用
      【*:如何理解這里“間諜”的意思,可以這樣理解,排序規(guī)則在本線程里面遵循了第一條法則,但是對其他線程而言,某個(gè)線程自身的排序特性可能使得它不定地訪問執(zhí)行線程的可見域,而使得該線程對本身在執(zhí)行的線程產(chǎn)生一定的影響。舉個(gè)例子,A線程需要做三件事情分別是A1、A2、A3,而B是另外一個(gè)線程具有操作B1、B2,如果把參考定位到B線程,那么對A線程而言,B的操作B1、B2有可能隨時(shí)會(huì)訪問到A的可見區(qū)域,比如A有一個(gè)可見區(qū)域a,A1就是把a(bǔ)修改稱為1,但是B線程在A線程調(diào)用了A1過后,卻訪問了a并且使用B1或者B2操作使得a發(fā)生了改變,變成了2,那么當(dāng)A按照排序性進(jìn)行A2操作讀取到a的值的時(shí)候,讀取到的是2而不是1,這樣就使得程序最初設(shè)計(jì)的時(shí)候A線程的初衷發(fā)生了改變,就是排序被打亂了,那么B線程對A線程而言,其身份就是“間諜”,而且需要注意到一點(diǎn),B線程的這些操作不會(huì)和A之間存在等待關(guān)系,那么B線程的這些操作就是異步操作,所以針對執(zhí)行線程A而言,B的身份就是“非同步方法中的‘間諜’?!?/span>
      同樣的,這僅僅是一個(gè)最低限度的保障性質(zhì),在任何給定的程序或者平臺(tái),開發(fā)中有可能發(fā)現(xiàn)更加嚴(yán)格的排序,但是開發(fā)人員在設(shè)計(jì)程序的時(shí)候不能依賴這種排序,如果依賴它們會(huì)發(fā)現(xiàn)測試難度會(huì)成指數(shù)級(jí)遞增,而且在復(fù)合規(guī)定的時(shí)候會(huì)因?yàn)椴煌奶匦允沟肑VM的實(shí)現(xiàn)因?yàn)椴环显O(shè)計(jì)初衷而失敗。
      注意:第一點(diǎn)在JLS(Java Language Specification)的所有討論中也是被采用的,例如算數(shù)表達(dá)式一般情況都是從上到下、從左到右的順序,但是這一點(diǎn)需要理解的是,從其他操作線程的角度看來這一點(diǎn)又具有不確定性,對線程內(nèi)部而言,其內(nèi)存模型本身是存在排序性的。【*:這里討論的排序是最底層的內(nèi)存里面執(zhí)行的時(shí)候的NativeCode的排序,不是說按照順序執(zhí)行的Java代碼具有的有序性質(zhì),本文主要分析的是JVM的內(nèi)存模型,所以希望讀者明白這里指代的討論單元是內(nèi)存區(qū)。】
      iii.原始JMM缺陷:
      JMM最初設(shè)計(jì)的時(shí)候存在一定的缺陷,這種缺陷雖然現(xiàn)有的JVM平臺(tái)已經(jīng)修復(fù),但是這里不得不提及,也是為了讀者更加了解JMM的設(shè)計(jì)思路,這一個(gè)小節(jié)的概念可能會(huì)牽涉到很多更加深入的知識(shí),如果讀者不能讀懂沒有關(guān)系先看了文章后邊的章節(jié)再返回來看也可以。
      1)問題1:不可變對象不是不可變的
      學(xué)過Java的朋友都應(yīng)該知道Java中的不可變對象,這一點(diǎn)在本文最后講解String類的時(shí)候也會(huì)提及,而JMM最初設(shè)計(jì)的時(shí)候,這個(gè)問題一直都存在,就是:不可變對象似乎可以改變它們的值(這種對象的不可變指通過使用final關(guān)鍵字來得到保證),(Publis Service Reminder:讓一個(gè)對象的所有字段都為final并不一定使得這個(gè)對象不可變——所有類型還必須是原始類型而不能是對象的引用。而不可變對象被認(rèn)為不要求同步的。但是,因?yàn)樵趯?nèi)存寫方面的更改從一個(gè)線程傳播到另外一個(gè)線程的時(shí)候存在潛在的延遲,這樣就使得有可能存在一種競態(tài)條件,即允許一個(gè)線程首先看到不可變對象的一個(gè)值,一段時(shí)間之后看到的是一個(gè)不同的值。這種情況以前怎么發(fā)生的呢?在JDK 1.4中的String實(shí)現(xiàn)里,這兒基本有三個(gè)重要的決定性字段:對字符數(shù)組的引用、長度和描述字符串的開始數(shù)組的偏移量。String就是以這樣的方式在JDK 1.4中實(shí)現(xiàn)的,而不是只有字符數(shù)組,因此字符數(shù)組可以在多個(gè)String和StringBuffer對象之間共享,而不需要在每次創(chuàng)建一個(gè)String的時(shí)候都拷貝到一個(gè)新的字符數(shù)組里。假設(shè)有下邊的代碼:
    String s1 = "/usr/tmp";
    String s2 = s1.substring(4); // "/tmp"
      這種情況下,字符串s2將具有大小為4的長度和偏移量,但是它將和s1共享“/usr/tmp”里面的同一字符數(shù)組,在String構(gòu)造函數(shù)運(yùn)行之前,Object的構(gòu)造函數(shù)將用它們默認(rèn)的值初始化所有的字段,包括決定性的長度和偏移字段。當(dāng)String構(gòu)造函數(shù)運(yùn)行的時(shí)候,字符串長度和偏移量被設(shè)置成所需要的值。但是在舊的內(nèi)存模型中,因?yàn)槿狈ν?,有可能另一個(gè)線程會(huì)臨時(shí)地看到偏移量字段具有初始默認(rèn)值0,而后又看到正確的值4,結(jié)果是s2的值從“/usr”變成了“/tmp”,這并不是我們真正的初衷,這個(gè)問題就是原始JMM的第一個(gè)缺陷所在,因?yàn)樵谠糐MM模型里面這是合理而且合法的,JDK 1.4以下的版本都允許這樣做。
      2)問題2:重新排序的易失性和非易失性存儲(chǔ)
      另一個(gè)主要領(lǐng)域是與volatile字段的內(nèi)存操作重新排序有關(guān),這個(gè)領(lǐng)域中現(xiàn)有的JMM引起了一些比較混亂的結(jié)果?,F(xiàn)有的JMM表明易失性的讀和寫是直接和主存打交道的,這樣避免了把值存儲(chǔ)到寄存器或者繞過處理器特定的緩存,這使得多個(gè)線程一般能看見一個(gè)給定變量最新的值。可是,結(jié)果是這種volatile定義并沒有最初想象中那樣如愿以償,并且導(dǎo)致了volatile的重大混亂。為了在缺乏同步的情況下提供較好的性能,編譯器、運(yùn)行時(shí)和緩存通常是允許進(jìn)行內(nèi)存的重新排序操作的,只要當(dāng)前執(zhí)行的線程分辨不出它們的區(qū)別。(這就是within-thread as-if-serial semantics[線程內(nèi)似乎是串行]的解釋)但是,易失性的讀和寫是完全跨線程安排的,編譯器或緩存不能在彼此之間重新排序易失性的讀和寫。遺憾的是,通過參考普通變量的讀寫,JMM允許易失性的讀和寫被重排序,這樣以為著開發(fā)人員不能使用易失性標(biāo)志作為操作已經(jīng)完成的標(biāo)志。比如:
    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擔(dān)任守衛(wèi)來表明一套別的操作已經(jīng)完成了,這是一個(gè)很好的思想,但是不能在JMM下工作,因?yàn)榕f的JMM允許非易失性的寫(比如寫到configOptions字段,以及寫到由configOptions引用Map的字段中)與易失性的寫一起重新排序,因此另外一個(gè)線程可能會(huì)看到initialized為true,但是對于configOptions字段或它所引用的對象還沒有一個(gè)一致的或者說當(dāng)前的針對內(nèi)存的視圖變量,volatile的舊語義只承諾在讀和寫的變量的可見性,而不承諾其他變量,雖然這種方法更加有效的實(shí)現(xiàn),但是結(jié)果會(huì)和我們設(shè)計(jì)之初大相徑庭。

    2.堆和棧
      i.Java內(nèi)存管理簡介:
      內(nèi)存管理在Java語言中是JVM自動(dòng)操作的,當(dāng)JVM發(fā)現(xiàn)某些對象不再需要的時(shí)候,就會(huì)對該對象占用的內(nèi)存進(jìn)行重分配(釋放)操作,而且使得分配出來的內(nèi)存能夠提供給所需要的對象。在一些編程語言里面,內(nèi)存管理是一個(gè)程序的職責(zé),但是書寫過C++的程序員很清楚,如果該程序需要自己來書寫很有可能引起很嚴(yán)重的錯(cuò)誤或者說不可預(yù)料的程序行為,最終大部分開發(fā)時(shí)間都花在了調(diào)試這種程序以及修復(fù)相關(guān)錯(cuò)誤上。一般情況下在Java程序開發(fā)過程把手動(dòng)內(nèi)存管理稱為顯示內(nèi)存管理,而顯示內(nèi)存管理經(jīng)常發(fā)生的一個(gè)情況就是引用懸掛——也就是說有可能在重新分配過程釋放掉了一個(gè)被某個(gè)對象引用正在使用的內(nèi)存空間,釋放掉該空間過后,該引用就處于懸掛狀態(tài)。如果這個(gè)被懸掛引用指向的對象試圖進(jìn)行原來對象(因?yàn)檫@個(gè)時(shí)候該對象有可能已經(jīng)不存在了)進(jìn)行操作的時(shí)候,由于該對象本身的內(nèi)存空間已經(jīng)被手動(dòng)釋放掉了,這個(gè)結(jié)果是不可預(yù)知的。顯示內(nèi)存管理另外一個(gè)常見的情況是內(nèi)存泄漏,當(dāng)某些引用不再引用該內(nèi)存對象的時(shí)候,而該對象原本占用的內(nèi)存并沒有被釋放,這種情況簡言為內(nèi)存泄漏。比如,如果針對某個(gè)鏈表進(jìn)行了內(nèi)存分配,而因?yàn)槭謩?dòng)分配不當(dāng),僅僅讓引用指向了某個(gè)元素所處的內(nèi)存空間,這樣就使得其他鏈表中的元素不能再被引用而且使得這些元素所處的內(nèi)存讓應(yīng)用程序處于不可達(dá)狀態(tài)而且這些對象所占有的內(nèi)存也不能夠被再使用,這個(gè)時(shí)候就發(fā)生了內(nèi)存泄漏。而這種情況一旦在程序中發(fā)生,就會(huì)一直消耗系統(tǒng)的可用內(nèi)存直到可用內(nèi)存耗盡,而針對計(jì)算機(jī)而言內(nèi)存泄漏的嚴(yán)重程度大了會(huì)使得本來正常運(yùn)行的程序直接因?yàn)閮?nèi)存不足而中斷,并不是Java程序里面出現(xiàn)Exception那么輕量級(jí)。
      在以前的編程過程中,手動(dòng)內(nèi)存管理帶了計(jì)算機(jī)程序不可避免的錯(cuò)誤,而且這種錯(cuò)誤對計(jì)算機(jī)程序是毀滅性的,所以內(nèi)存管理就成為了一個(gè)很重要的話題,但是針對大多數(shù)純面向?qū)ο笳Z言而言,比如Java,提供了語言本身具有的內(nèi)存特性:自動(dòng)化內(nèi)存管理,這種語言提供了一個(gè)程序垃圾回收器(Garbage Collector[GC]),自動(dòng)內(nèi)存管理提供了一個(gè)抽象的接口以及更加可靠的代碼使得內(nèi)存能夠在程序里面進(jìn)行合理的分配。最常見的情況就是垃圾回收器避免了懸掛引用的問題,因?yàn)橐坏┻@些對象沒有被任何引用“可達(dá)”的時(shí)候,也就是這些對象在JVM的內(nèi)存池里面成為了不可引用對象,該垃圾回收器會(huì)直接回收掉這些對象占用的內(nèi)存,當(dāng)然這些對象必須滿足垃圾回收器回收的某些對象規(guī)則,而垃圾回收器在回收的時(shí)候會(huì)自動(dòng)釋放掉這些內(nèi)存。不僅僅如此,垃圾回收器同樣會(huì)解決內(nèi)存泄漏問題。
      ii.詳解堆和棧[圖片以及部分內(nèi)容來自《Inside JVM》]
      1)通用簡介
      [編譯原理]學(xué)過編譯原理的人都明白,程序運(yùn)行時(shí)有三種內(nèi)存分配策略:靜態(tài)的、棧式的、堆式的
      靜態(tài)存儲(chǔ)——是指在編譯時(shí)就能夠確定每個(gè)數(shù)據(jù)目標(biāo)在運(yùn)行時(shí)的存儲(chǔ)空間需求,因而在編譯時(shí)就可以給它們分配固定的內(nèi)存空間。這種分配策略要求程序代碼中不允許有可變數(shù)據(jù)結(jié)構(gòu)的存在,也不允許有嵌套或者遞歸的結(jié)構(gòu)出現(xiàn),因?yàn)樗鼈兌紩?huì)導(dǎo)致編譯程序無法計(jì)算準(zhǔn)確的存儲(chǔ)空間。
      棧式存儲(chǔ)——該分配可成為動(dòng)態(tài)存儲(chǔ)分配,是由一個(gè)類似于堆棧的運(yùn)行棧來實(shí)現(xiàn)的,和靜態(tài)存儲(chǔ)的分配方式相反,在棧式存儲(chǔ)方案中,程序?qū)?shù)據(jù)區(qū)的需求在編譯時(shí)是完全未知的,只有到了運(yùn)行的時(shí)候才能知道,但是規(guī)定在運(yùn)行中進(jìn)入一個(gè)程序模塊的時(shí)候,必須知道該程序模塊所需要的數(shù)據(jù)區(qū)的大小才能分配其內(nèi)存。和我們在數(shù)據(jù)結(jié)構(gòu)中所熟知的棧一樣,棧式存儲(chǔ)分配按照先進(jìn)后出的原則進(jìn)行分配。
      堆式存儲(chǔ)——堆式存儲(chǔ)分配則專門負(fù)責(zé)在編譯時(shí)或運(yùn)行時(shí)模塊入口處都無法確定存儲(chǔ)要求的數(shù)據(jù)結(jié)構(gòu)的內(nèi)存分配,比如可變長度串和對象實(shí)例,堆由大片的可利用塊或空閑塊組成,堆中的內(nèi)存可以按照任意順序分配和釋放。
      [C++語言]對比C++語言里面,程序占用的內(nèi)存分為下邊幾個(gè)部分:
      [1]棧區(qū)(Stack由編譯器自動(dòng)分配釋放,存放函數(shù)的參數(shù)值,局部變量的值等。其操作方式類似于數(shù)據(jù)結(jié)構(gòu)中的棧。我們在程序中定義的局部變量就是存放在棧里,當(dāng)局部變量的生命周期結(jié)束的時(shí)候,它所占的內(nèi)存會(huì)被自動(dòng)釋放。
      [2]堆區(qū)(Heap一般由程序員分配和釋放,若程序員不釋放,程序結(jié)束時(shí)可能由OS回收。注意它與數(shù)據(jù)結(jié)構(gòu)中的堆是兩回事,分配方式倒是類似于鏈表。我們在程序中使用c++中new或者c中的malloc申請的一塊內(nèi)存,就是在heap上申請的,在使用完畢后,是需要我們自己動(dòng)手釋放的,否則就會(huì)產(chǎn)生“內(nèi)存泄露”的問題。
      [3]全局區(qū)(靜態(tài)區(qū))(Static:全局變量和靜態(tài)變量的存儲(chǔ)是放在一塊的,初始化的全局變量和靜態(tài)變量在一塊區(qū)域,未初始化的全局變量和未初始化的靜態(tài)變量在相鄰的另一塊區(qū)域。程序結(jié)束后由系統(tǒng)釋放。
      [4]文字常量區(qū):常量字符串就是放在這里的,程序結(jié)束后由系統(tǒng)釋放。在Java中對應(yīng)有一個(gè)字符串常量池。
      [5]程序代碼區(qū):存放函數(shù)體的二進(jìn)制代碼
      2)JVM結(jié)構(gòu)【堆、棧解析】:
      在Java虛擬機(jī)規(guī)范中,一個(gè)虛擬機(jī)實(shí)例的行為主要描述為:子系統(tǒng)內(nèi)存區(qū)域數(shù)據(jù)類型指令,這些組件在描述了抽象的JVM內(nèi)部的一個(gè)抽象結(jié)構(gòu)。與其說這些組成部分的目的是進(jìn)行JVM內(nèi)部結(jié)構(gòu)的一種支配,更多的是提供一種嚴(yán)格定義實(shí)現(xiàn)的外部行為,該規(guī)范定義了這些抽象組成部分以及相互作用的任何Java虛擬機(jī)執(zhí)行所需要的行為。下圖描述了JVM內(nèi)部的一個(gè)結(jié)構(gòu),其中主要包括主要的子系統(tǒng)、內(nèi)存區(qū)域,如同以前在《Java基礎(chǔ)知識(shí)》中描述的:Java虛擬機(jī)有一個(gè)類加載器作為JVM的子系統(tǒng),類加載器針對Class進(jìn)行檢測以鑒定完全合格的類接口,而JVM內(nèi)部也有一個(gè)執(zhí)行引擎:
      當(dāng)JVM運(yùn)行一個(gè)程序的時(shí)候,它的內(nèi)存需要用來存儲(chǔ)很多內(nèi)容,包括字節(jié)碼、以及從類文件中提取出來的一些附加信息、以及程序中實(shí)例化的對象、方法參數(shù)、返回值、局部變量以及計(jì)算的中間結(jié)果。JVM的內(nèi)存組織需要在不同的運(yùn)行時(shí)數(shù)據(jù)區(qū)進(jìn)行以上的幾個(gè)操作,下邊針對上圖里面出現(xiàn)的幾個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)進(jìn)行詳細(xì)解析:一些運(yùn)行時(shí)數(shù)據(jù)區(qū)共享了所有應(yīng)用程序線程和其他特有的單個(gè)線程,每個(gè)JVM實(shí)例有一個(gè)方法區(qū)和一個(gè)內(nèi)存堆,這些是共同在虛擬機(jī)內(nèi)運(yùn)行的線程。在Java程序里面,每個(gè)新的線程啟動(dòng)過后,它就會(huì)被JVM在內(nèi)部分配自己的PC寄存器[PC registers]程序計(jì)數(shù)器器)和Java堆棧Java stacks)。若該線程正在執(zhí)行一個(gè)非本地Java方法,在PC寄存器的值指示下一條指令執(zhí)行,該線程在Java內(nèi)存棧中保存了非本地Java方法調(diào)用狀態(tài),其狀態(tài)包括局部變量、被調(diào)用的參數(shù)、它的返回值、以及中間計(jì)算結(jié)果。而本地方法調(diào)用的狀態(tài)則是存儲(chǔ)在獨(dú)立的本地方法內(nèi)存棧里面(native method stacks),這種情況下使得這些本地方法和其他內(nèi)存運(yùn)行時(shí)數(shù)據(jù)區(qū)的內(nèi)容盡可能保證和其他內(nèi)存運(yùn)行時(shí)數(shù)據(jù)區(qū)獨(dú)立,而且該方法的調(diào)用更靠近操作系統(tǒng),這些方法執(zhí)行的字節(jié)碼有可能根據(jù)操作系統(tǒng)環(huán)境的不同使得其編譯出來的本地字節(jié)碼的結(jié)構(gòu)也有一定的差異。JVM中的內(nèi)存棧是一個(gè)棧幀的組合,一個(gè)棧幀包含了某個(gè)Java方法調(diào)用的狀態(tài),當(dāng)某個(gè)線程調(diào)用方法的時(shí)候,JVM就會(huì)將一個(gè)新的幀壓入到Java內(nèi)存棧,當(dāng)方法調(diào)用完成過后,JVM將會(huì)從內(nèi)存棧中移除該棧幀。JVM里面不存在一個(gè)可以存放中間計(jì)算數(shù)據(jù)結(jié)果值的寄存器,其內(nèi)部指令集使用Java??臻g來存儲(chǔ)中間計(jì)算的數(shù)據(jù)結(jié)果值,這種做法的設(shè)計(jì)是為了保持Java虛擬機(jī)的指令集緊湊,使得與寄存器原理能夠緊密結(jié)合并且進(jìn)行操作。
      1)方法區(qū)(Method Area
      在JVM實(shí)例中,對裝載的類型信息是存儲(chǔ)在一個(gè)邏輯方法內(nèi)存區(qū)中,當(dāng)Java虛擬機(jī)加載了一個(gè)類型的時(shí)候,它會(huì)跟著這個(gè)Class的類型去路徑里面查找對應(yīng)的Class文件,類加載器讀取類文件(線性二進(jìn)制數(shù)據(jù)),然后將該文件傳遞給Java虛擬機(jī),JVM從二進(jìn)制數(shù)據(jù)中提取信息并且將這些信息存儲(chǔ)在方法區(qū),而類中聲明(靜態(tài))變量就是來自于方法區(qū)中存儲(chǔ)的信息。在JVM里面用什么樣的方式存儲(chǔ)該信息是由JVM設(shè)計(jì)的時(shí)候決定的,例如:當(dāng)數(shù)據(jù)進(jìn)入方法的時(shí)候,多類文件字節(jié)的存儲(chǔ)量以Big-Endian(第一次最重要的字節(jié))的順序存儲(chǔ),盡管如此,一個(gè)虛擬機(jī)可以用任何方式針對這些數(shù)據(jù)進(jìn)行存儲(chǔ)操作,若它存儲(chǔ)在一個(gè)Little-Endian處理器上,設(shè)計(jì)的時(shí)候就有可能將多文件字節(jié)的值按照Little-Endian順尋存儲(chǔ)。
      ——【$Big-Endian和Little-Endian】——
      程序存儲(chǔ)數(shù)據(jù)過程中,如果數(shù)據(jù)是跨越多個(gè)字節(jié)對象就必須有一種約定:
    • 它的地址是多少:對于跨越多個(gè)字節(jié)的對象,一般它所占的字節(jié)都是連續(xù)的,它的地址等于它所占字節(jié)最低地址,這種情況鏈表可能存儲(chǔ)的僅僅是表頭
    • 它的字節(jié)在內(nèi)存中是如何組織的
      比如:int x,它的地址為0x100,那么它占據(jù)了內(nèi)存中的0x100、0x101、0x102、0x103四個(gè)字節(jié),所以一般情況我們覺得int是4個(gè)字節(jié)。上邊只是內(nèi)存組織的一種情況,多字節(jié)對象在內(nèi)存中的組織有兩種約定,還有一種情況:若一個(gè)整數(shù)為W位,它的表示如下:
      每一位表示為:[Xw-1,Xw-2,...,X1,X0]
      它的最高有效字節(jié)MSBMost Significant Byte為:[Xw-1,Xw-2,...,Xw-8]
      最低有效字節(jié)LSBLeast Significant Byte為:[X7,X6,...,X0]
      其余字節(jié)則位于LSB和MSB之間
      LSB和MSB誰位于內(nèi)存的最低地址,即代表了該對象的地址,這樣就引出了Big-Endian和Little-Endian的問題,如果LSB在MSB前,LSB是最低地址,則該機(jī)器是小端,反之則是大端。DES(Digital Equipment Corporation,現(xiàn)在是Compaq公司的一部分)和Intel機(jī)器(x86平臺(tái))一般采用小端,IBM、Motorola(Power PC)、Sun的機(jī)器一般采用大端。當(dāng)然這種不能代表所有情況,有的CPU既能工作于小端、又可以工作于大端,比如ARM、Alpha、摩托羅拉的PowerPC,這些情況根據(jù)具體的處理器型號(hào)有所不同。但是大部分操作系統(tǒng)(Windows、FreeBSD、Linux)一般都是Little Endian的,少部分系統(tǒng)(Mac OS)是Big Endian的,所以用什么方式存儲(chǔ)還得依賴宿主操作系統(tǒng)環(huán)境。
      由上圖可以看到,映射訪問(“寫32位地址的0”)主要是由寄存器到內(nèi)存、由內(nèi)存到寄存器的一種數(shù)據(jù)映射方式,Big-Endian在上圖可以看出的原子內(nèi)存單位(Atomic Unit)在系統(tǒng)內(nèi)存中的增長方向?yàn)?strong>從左到右,而Little-Endian的地址增長方向?yàn)?strong>從右到左。舉個(gè)例子:
      若要存儲(chǔ)數(shù)據(jù)0x0A0B0C0D
      Big-Endian:
      以8位為一個(gè)存儲(chǔ)單位,其存儲(chǔ)的地址增長為:
      上圖中可以看出MSB的值存儲(chǔ)了0x0A,這種情況下數(shù)據(jù)的高位是從內(nèi)存的低地址開始存儲(chǔ)的,然后從左到右開始增長,第二位0x0B就是存儲(chǔ)在第二位的,如果是按照16位為一個(gè)存儲(chǔ)單位,其存儲(chǔ)方式又為:
      則可以看到Big-Endian的映射地址方式為:
     
      MSB:在計(jì)算機(jī)中,最高有效位(MSB)是指位值的存儲(chǔ)位置為轉(zhuǎn)換為二進(jìn)制數(shù)據(jù)后的最大值,MSB有時(shí)候在Big-Endian的架構(gòu)中稱為最左最大數(shù)據(jù)位,這種情況下再往左邊的內(nèi)存位則不是數(shù)據(jù)位了,而是有效位數(shù)位置的最高符號(hào)位,不僅僅如此,MSB也可以對應(yīng)一個(gè)二進(jìn)制符號(hào)位的符號(hào)位補(bǔ)碼標(biāo)記:“1”的含義為負(fù),“0”的含義為正。最高位代表了“最重要字節(jié)”,也就是說當(dāng)某些多字節(jié)數(shù)據(jù)擁有了最大值的時(shí)候它就是存儲(chǔ)的時(shí)候最高位數(shù)據(jù)的字節(jié)對應(yīng)的內(nèi)存位置:
      Little-Endian:
      與Big-Endian相對的就是Little-Endian的存儲(chǔ)方式,同樣按照8位為一個(gè)存儲(chǔ)單位上邊的數(shù)據(jù)0x0A0B0C0D存儲(chǔ)格式為:
      可以看到LSB的值存儲(chǔ)的0x0D,也就是數(shù)據(jù)的最低位是從內(nèi)存的低地址開始存儲(chǔ)的,它的高位是從右到左的順序逐漸增加內(nèi)存分配空間進(jìn)行存儲(chǔ)的,如果按照十六位為存儲(chǔ)單位存儲(chǔ)格式為:
      從上圖可以看到最低的16位的存儲(chǔ)單位里面存儲(chǔ)的值為0x0C0D,接著才是0x0A0B,這樣就可以看到按照數(shù)據(jù)從高位到低位在內(nèi)存中存儲(chǔ)的時(shí)候是從右到左進(jìn)行遞增存儲(chǔ)的,實(shí)際上可以從寫內(nèi)存的順序來理解,實(shí)際上數(shù)據(jù)存儲(chǔ)在內(nèi)存中無非在使用的時(shí)候是寫內(nèi)存讀內(nèi)存,針對LSB的方式最好的書面解釋就是向左增加來看待,如果真正在進(jìn)行內(nèi)存讀寫的時(shí)候使用這樣的順序,其意義就體現(xiàn)出來了:
      按照這種讀寫格式,0x0D存儲(chǔ)在最低內(nèi)存地址,而從右往左的增長就可以看到LSB存儲(chǔ)的數(shù)據(jù)為0x0D,和初衷吻合,則十六位的存儲(chǔ)就可以按照下邊的格式來解釋:
      實(shí)際上從上邊的存儲(chǔ)還會(huì)考慮到另外一個(gè)問題,如果按照這種方式從右往左的方式進(jìn)行存儲(chǔ),如果是遇到Unicode文字就和從左到右的語言顯示方式相反。比如一個(gè)單詞“XRAY”,使用Little-Endian的方式存儲(chǔ)格式為:
      使用這種方式進(jìn)行內(nèi)存讀寫的時(shí)候就會(huì)發(fā)現(xiàn)計(jì)算機(jī)語言和語言本身的順序會(huì)有沖突,這種沖突主要是以使用語言的人的習(xí)慣有關(guān),而書面化的語言從左到右就可以知道其沖突是不可避免的。我們一般使用語言的閱讀方式都是從左到右,而低端存儲(chǔ)(Little-Endian)的這種內(nèi)存讀寫的方式使得我們最終從計(jì)算機(jī)里面讀取字符需要進(jìn)行倒序,而且考慮另外一個(gè)問題,如果是針對中文而言,一個(gè)字符是兩個(gè)字節(jié),就會(huì)出現(xiàn)整體順序和每一個(gè)位的順序會(huì)進(jìn)行兩次倒序操作,這種方式真正在制作處理器的時(shí)候也存在一種計(jì)算上的沖突,而針對使用文字從左到右進(jìn)行閱讀的國家而言,從右到左的方式(Big-Endian)則會(huì)有這樣的文字沖突,另外一方面,盡管有很多國家使用語言是從右到左,但是僅僅和Big-Endian的方式存在沖突,這些國家畢竟占少數(shù),所以可以理解的是,為什么主流的系統(tǒng)都是使用的Little-Endian的方式
      【*:這里不解釋Middle-Endian的方式以及Mixed-Endian的方式】
      LSB:在計(jì)算機(jī)中,最低有效位是一個(gè)二進(jìn)制給予單位的整數(shù),位的位置確定了該數(shù)據(jù)是一個(gè)偶數(shù)還是奇數(shù),LSB有時(shí)被稱為最右位。在使用具體位二進(jìn)制數(shù)之內(nèi),常見的存儲(chǔ)方式就是每一位存儲(chǔ)1或者0的方式,從0向上到1每一比特逢二進(jìn)一的存儲(chǔ)方式。LSB的這種特性用來指定單位位,而不是位的數(shù)字,而這種方式也有可能產(chǎn)生一定的混亂。
      ——以上是關(guān)于Big-Endian和Little-Endian的簡單講解——
      JVM虛擬機(jī)將搜索和使用類型的一些信息也存儲(chǔ)在方法區(qū)中以方便應(yīng)用程序加載讀取該數(shù)據(jù)。設(shè)計(jì)者在設(shè)計(jì)過程也考慮到要方便JVM進(jìn)行Java應(yīng)用程序的快速執(zhí)行,而這種取舍主要是為了程序在運(yùn)行過程中內(nèi)存不足的情況能夠通過一定的取舍去彌補(bǔ)內(nèi)存不足的情況。在JVM內(nèi)部,所有的線程共享相同方法區(qū),因此,訪問方法區(qū)的數(shù)據(jù)結(jié)構(gòu)必須是線程安全的,如果兩個(gè)線程都試圖去調(diào)用去找一個(gè)名為Lava的類,比如Lava還沒有被加載,只有一個(gè)線程可以加載該類而另外的線程只能夠等待。方法區(qū)的大小在分配過程中是不固定的,隨著Java應(yīng)用程序的運(yùn)行,JVM可以調(diào)整其大小,需要注意一點(diǎn),方法區(qū)的內(nèi)存不需要是連續(xù)的,因?yàn)榉椒▍^(qū)內(nèi)存可以分配內(nèi)存堆中,即使是虛擬機(jī)JVM實(shí)例對象自己所在的內(nèi)存堆也是可行的,而在實(shí)現(xiàn)過程是允許程序員自身來指定方法區(qū)的初始化大小的。
      同樣的,因?yàn)镴ava本身的自動(dòng)內(nèi)存管理,方法區(qū)也會(huì)被垃圾回收的,Java程序可以通過類擴(kuò)展動(dòng)態(tài)加載器對象,類可以成為“未引用”向垃圾回收器進(jìn)行申請,如果一個(gè)類是“未引用”的,則該類就可能被卸載,
      而方法區(qū)針對具體的語言特性有幾種信息是存儲(chǔ)在方法區(qū)內(nèi)的:
      【類型信息】
    • 類型的完全限定名(java.lang.String格式)
    • 類型的完全限定名的直接父類的完全限定名(除非這個(gè)父類的類型是一個(gè)接口或者java.lang.Object)
    • 不論類型是一個(gè)類或者接口
    • 類型的修飾符(例如public、abstract、final)
    • 任何一個(gè)直接超類接口的完全限定名的列表
      在JVM和類文件名的內(nèi)部,類型名一般都是完全限定名(java.lang.String)格式,在Java源文件里面,完全限定名必須加入包前綴,而不是我們在開發(fā)過程寫的簡單類名,而在方法上,只要是符合Java語言規(guī)范的類的完全限定名都可以,而JVM可能直接進(jìn)行解析,比如:(java.lang.String)在JVM內(nèi)部名稱為java/lang/String,這就是我們在異常捕捉的時(shí)候經(jīng)常看到的ClassNotFoundException的異常里面類信息的名稱格式。
      除此之外,還必須為每一種加載過的類型在JVM內(nèi)進(jìn)行存儲(chǔ),下邊的信息不存儲(chǔ)在方法區(qū)內(nèi),下邊的章節(jié)會(huì)一一說明
    • 類型常量池
    • 字段信息
    • 方法信息
    • 所有定義在Class內(nèi)部的(靜態(tài))變量信息,除開常量
    • 一個(gè)ClassLoader的引用
    • Class的引用
      【常量池】
      針對類型加載的類型信息,JVM將這些存儲(chǔ)在常量池里,常量池是一個(gè)根據(jù)類型定義的常量的有序常量集,包括字面量(String、Integer、Float常量)以及符號(hào)引用(類型、字段、方法),整個(gè)長量池會(huì)被JVM的一個(gè)索引引用,如同數(shù)組里面的元素集合按照索引訪問一樣,JVM針對這些常量池里面存儲(chǔ)的信息也是按照索引方式進(jìn)行。實(shí)際上長量池在Java程序的動(dòng)態(tài)鏈接過程起到了一個(gè)至關(guān)重要的作用。
      【字段信息】
      針對字段的類型信息,下邊的信息是存儲(chǔ)在方法區(qū)里面的:
    • 字段名
    • 字段類型
    • 字段修飾符(public,private,protected,static,final,volatile,transient
      【方法信息】
      針對方法信息,下邊信息存儲(chǔ)在方法區(qū)上:
    • 方法名
    • 方法的返回類型(包括void
    • 方法參數(shù)的類型、數(shù)目以及順序
    • 方法修飾符(public,private,protected,static,final,synchronized,native,abstract
      針對非本地方法,還有些附加方法信息需要存儲(chǔ)在方法區(qū)內(nèi):
    • 方法字節(jié)碼
    • 方法中局部變量區(qū)的大小、方法棧幀
    • 異常表
      【類變量】
      類變量在一個(gè)類的多個(gè)實(shí)例之間共享,這些變量直接和類相關(guān),而不是和類的實(shí)例相關(guān),(定義過程簡單理解為類里面定義的static類型的變量),針對類變量,其邏輯部分就是存儲(chǔ)在方法區(qū)內(nèi)的。在JVM使用這些類之前,JVM先要在方法區(qū)里面為定義的non-final變量分配內(nèi)存空間;常量(定義為final)則在JVM內(nèi)部則不是以同樣的方式來進(jìn)行存儲(chǔ)的,盡管針對常量而言,一個(gè)final的類變量是擁有它自己的常量池,作為常量池里面的存儲(chǔ)某部分,類常量是存儲(chǔ)在方法區(qū)內(nèi)的,而其邏輯部分則不是按照上邊的類變量的方式來進(jìn)行內(nèi)存分配的。雖然non-final類變量是作為這些類型聲明中存儲(chǔ)數(shù)據(jù)的某一部分,final變量存儲(chǔ)為任何使用它類型的一部分的數(shù)據(jù)格式進(jìn)行簡單存儲(chǔ)。
      【ClassLoader引用】
      對于每種類型的加載,JVM必須檢測其類型是否符合了JVM的語言規(guī)范,對于通過類加載器加載的對象類型,JVM必須存儲(chǔ)對類的引用,而這些針對類加載器的引用是作為了方法區(qū)里面的類型數(shù)據(jù)部分進(jìn)行存儲(chǔ)的。
      【類Class的引用】
      JVM在加載了任何一個(gè)類型過后會(huì)創(chuàng)建一個(gè)java.lang.Class的實(shí)例,虛擬機(jī)必須通過一定的途徑來引用該類型對應(yīng)的一個(gè)Class的實(shí)例,并且將其存儲(chǔ)在方法區(qū)內(nèi)
      【方法表】
      為了提高訪問效率,必須仔細(xì)的設(shè)計(jì)存儲(chǔ)在方法區(qū)中的數(shù)據(jù)信息結(jié)構(gòu)。除了以上討論的結(jié)構(gòu),jvm的實(shí)現(xiàn)者還添加一些其他的數(shù)據(jù)結(jié)構(gòu),如方法表【下邊會(huì)說明
      2)內(nèi)存棧(Stack):
      當(dāng)一個(gè)新線程啟動(dòng)的時(shí)候,JVM會(huì)為Java線程創(chuàng)建每個(gè)線程的獨(dú)立內(nèi)存棧,如前所言Java的內(nèi)存棧是由棧幀構(gòu)成,棧幀本身處于游離狀態(tài),在JVM里面,棧幀的操作只有兩種:出棧入棧。正在被線程執(zhí)行的方法一般稱為當(dāng)前線程方法,而該方法的棧幀就稱為當(dāng)前幀,而在該方法內(nèi)定義的類稱為當(dāng)前類,常量池也稱為當(dāng)前常量池。當(dāng)執(zhí)行一個(gè)方法如此的時(shí)候,JVM保留當(dāng)前類和當(dāng)前常量池的跟蹤,當(dāng)虛擬機(jī)遇到了存儲(chǔ)在棧幀中的數(shù)據(jù)上的操作指令的時(shí)候,它就執(zhí)行當(dāng)前幀的操作。當(dāng)一個(gè)線程調(diào)用某個(gè)Java方法時(shí),虛擬機(jī)創(chuàng)建并且將一個(gè)新幀壓入到內(nèi)存堆棧中,而這個(gè)壓入到內(nèi)存棧中的幀成為當(dāng)前棧幀,當(dāng)該方法執(zhí)行的時(shí)候,JVM使用內(nèi)存棧來存儲(chǔ)參數(shù)、局部變量、中間計(jì)算結(jié)果以及其他相關(guān)數(shù)據(jù)。方法在執(zhí)行過程有可能因?yàn)閮煞N方式而結(jié)束:如果一個(gè)方法返回完成就屬于方法執(zhí)行的正常結(jié)束,如果在這個(gè)過程拋出異常而結(jié)束,可以稱為非正常結(jié)束,不論是正常結(jié)束還是異常結(jié)束,JVM都會(huì)彈出或者丟棄該棧幀,則上一幀的方法就成為了當(dāng)前幀。
      在JVM中,Java線程的棧數(shù)據(jù)是屬于某個(gè)線程獨(dú)有的,其他的線程不能夠修改或者通過其他方式來訪問該線程的棧幀,正因?yàn)槿绱诉@種情況不用擔(dān)心多線程同步訪問Java的局部變量,當(dāng)一個(gè)線程調(diào)用某個(gè)方法的時(shí)候,方法的局部變量是在方法內(nèi)部進(jìn)行的Java棧幀的存儲(chǔ),只有當(dāng)前線程可以訪問該局部變量,而其他線程不能隨便訪問該內(nèi)存棧里面存儲(chǔ)的數(shù)據(jù)。內(nèi)存棧內(nèi)的棧幀數(shù)據(jù)和方法區(qū)以及內(nèi)存堆一樣,Java棧的棧幀不需要分配在連續(xù)的堆棧內(nèi),或者說它們可能是在堆,或者兩者組合分配,實(shí)際數(shù)據(jù)用于表示Java堆棧和棧幀結(jié)構(gòu)是JVM本身的設(shè)計(jì)結(jié)構(gòu)決定的,而且在編程過程可以允許程序員指定一個(gè)用于Java堆棧的初始大小以及最大、最小尺寸。
      【概念區(qū)分】
    • 內(nèi)存棧:這里的內(nèi)存棧和物理結(jié)構(gòu)內(nèi)存堆棧有點(diǎn)點(diǎn)區(qū)別,是內(nèi)存里面數(shù)據(jù)存儲(chǔ)的一種抽象數(shù)據(jù)結(jié)構(gòu)。從操作系統(tǒng)上講,在程序執(zhí)行過程對內(nèi)存的使用本身常用的數(shù)據(jù)結(jié)構(gòu)就是內(nèi)存堆棧,而這里的內(nèi)存堆棧指代的就是JVM在使用內(nèi)存過程整個(gè)內(nèi)存的存儲(chǔ)結(jié)構(gòu),多指內(nèi)存的物理結(jié)構(gòu),而Java內(nèi)存棧不是指代的一個(gè)物理結(jié)構(gòu),更多的時(shí)候指代的是一個(gè)抽象結(jié)構(gòu),就是符合JVM語言規(guī)范的內(nèi)存棧的一個(gè)抽象結(jié)構(gòu)。因?yàn)槲锢韮?nèi)存堆棧結(jié)構(gòu)和Java內(nèi)存棧的抽象模型結(jié)構(gòu)本身比較相似,所以我們在學(xué)習(xí)過程就正常把這兩種結(jié)構(gòu)放在一起考慮了,而且二者除了概念上有一點(diǎn)點(diǎn)小的區(qū)別,理解成為一種結(jié)構(gòu)對于初學(xué)者也未嘗不可,所以實(shí)際上也可以覺得二者沒有太大的本質(zhì)區(qū)別。但是在學(xué)習(xí)的時(shí)候最好分清楚內(nèi)存堆棧和Java內(nèi)存棧的一小點(diǎn)細(xì)微的差距,前者是物理概念和本身模型,后者是抽象概念和本身模型的一個(gè)共同體。而內(nèi)存堆棧更多的說法可以理解為一個(gè)內(nèi)存塊,因?yàn)閮?nèi)存塊可以通過索引和指針進(jìn)行數(shù)據(jù)結(jié)構(gòu)的組合,內(nèi)存棧就是內(nèi)存塊針對數(shù)據(jù)結(jié)構(gòu)的一種表示,而內(nèi)存堆則是內(nèi)存塊的另外一種數(shù)據(jù)結(jié)構(gòu)的表示,這樣理解更容易區(qū)分內(nèi)存棧內(nèi)存堆棧(內(nèi)存塊)的概念。
    • 棧幀:棧幀是內(nèi)存棧里面的最小單位,指的是內(nèi)存棧里面每一個(gè)最小內(nèi)存存儲(chǔ)單元,它針對內(nèi)存棧僅僅做了兩個(gè)操作:入棧和出棧,一般情況下:所說的堆棧幀棧幀倒是一個(gè)概念,所以在理解上記得加以區(qū)分
    • 內(nèi)存堆:這里的內(nèi)存堆和內(nèi)存棧是相對應(yīng)的,其實(shí)內(nèi)存堆里面的數(shù)據(jù)也是存儲(chǔ)在系統(tǒng)內(nèi)存堆棧里面的,只是它使用了另外一種方式來進(jìn)行堆里面內(nèi)存的管理,而本章題目要講到的就是Java語言本身的內(nèi)存堆和內(nèi)存棧,而這兩個(gè)概念都是抽象的概念模型,而且是相對的。
      棧幀:棧幀主要包括三個(gè)部分:局部變量、操作數(shù)棧幀(操作幀)幀數(shù)據(jù)(數(shù)據(jù)幀)。本地變量和操作數(shù)幀的大小取決于需要,這些大小是在編譯時(shí)就決定的,并且在每個(gè)方法的類文件數(shù)據(jù)中進(jìn)行分配,幀的數(shù)據(jù)大小則不一樣,它雖然也是在編譯時(shí)就決定的但是它的大小和本身代碼實(shí)現(xiàn)有關(guān)。當(dāng)JVM調(diào)用一個(gè)Java方法的時(shí)候,它會(huì)檢查類的數(shù)據(jù)來確定在本地變量和操作方法要求的棧大小,它計(jì)算該方法所需要的內(nèi)存大小,然后將這些數(shù)據(jù)分配好內(nèi)存空間壓入到內(nèi)存堆棧中。
      棧幀——局部變量局部變量是以Java棧幀組合成為的一個(gè)以零為基的數(shù)組,使用局部變量的時(shí)候使用的實(shí)際上是一個(gè)包含了0的一個(gè)基于索引的數(shù)組結(jié)構(gòu)。int類型、float、引用以及返回值都占據(jù)了一個(gè)數(shù)組中的局部變量的條目,而byte、shortchar則在存儲(chǔ)到局部變量的時(shí)候是先轉(zhuǎn)化成為int再進(jìn)行操作的,則longdouble則是在這樣一個(gè)數(shù)組里面使用了兩個(gè)元素的空間大小,在局部變量里面存儲(chǔ)基本數(shù)據(jù)類型的時(shí)候使用的就是這樣的結(jié)構(gòu)。舉個(gè)例子:
    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;
        }
    }
      棧幀——操作幀和局部變量一樣,操作幀也是一組有組織的數(shù)組的存儲(chǔ)結(jié)構(gòu),但是和局部變量不一樣的是這個(gè)不是通過數(shù)組的索引訪問的,而是直接進(jìn)行的入棧和出棧的操作,當(dāng)操作指令直接壓入了操作棧幀過后,從棧幀里面出來的數(shù)據(jù)會(huì)直接在出棧的時(shí)候被讀取使用。除了程序計(jì)數(shù)器以外,操作幀也是可以直接被指令訪問到的,JVM里面沒有寄存器。處理操作幀的時(shí)候Java虛擬機(jī)是基于內(nèi)存棧的而不是基于寄存器的,因?yàn)樗诓僮鬟^程是直接對內(nèi)存棧進(jìn)行操作而不是針對寄存器進(jìn)行操作。而JVM內(nèi)部的指令也可以來源于其他地方比如緊接著操作符以及操作數(shù)的字節(jié)碼流或者直接從常量池里面進(jìn)行操作。JVM指令其實(shí)真正在操作過程的焦點(diǎn)是集中在內(nèi)存棧棧幀的操作幀上的。JVM指令將操作幀作為一個(gè)工作空間,有許多指令都是從操作幀里面出棧讀取的,對指令進(jìn)行操作過后將操作幀的計(jì)算結(jié)果重新壓入內(nèi)存堆棧內(nèi)。比如iadd指令將兩個(gè)整數(shù)壓入到操作幀里面,然后將兩個(gè)操作數(shù)進(jìn)行相加,相加的時(shí)候從內(nèi)存棧里面讀取兩個(gè)操作數(shù)的值,然后進(jìn)行運(yùn)算,最后將運(yùn)算結(jié)果重新存入到內(nèi)存堆棧里面。舉個(gè)簡單的例子:
    begin
    iload_0 //將整數(shù)類型的局部變量0壓入到內(nèi)存棧里面
    iload_1 //將整數(shù)類型的局部變量1壓入到內(nèi)存棧里面
    iadd     //將兩個(gè)變量出棧讀取,然后進(jìn)行相加操作,將結(jié)果重新壓入棧中
    istore_2 //將最終輸出結(jié)果放在另外一個(gè)局部變量里面
    end
      綜上所述,就是整個(gè)計(jì)算過程針對內(nèi)存的一些操作內(nèi)容,而整體的結(jié)構(gòu)可以用下圖來描述:
      棧幀——數(shù)據(jù)幀:除了局部變量和操作幀以外,Java棧幀還包括了數(shù)據(jù)幀,用于支持常量池、普通的方法返回以及異常拋出等,這些數(shù)據(jù)都是存儲(chǔ)在Java內(nèi)存棧幀的數(shù)據(jù)幀中的。很多JVM的指令集實(shí)際上使用的都是常量池里面的一些條目,一些指令,只是把int、long、float、double或者String從常量池里面壓入到Java棧幀的操作幀上邊,一些指令使用常量池來管理類或者數(shù)組的實(shí)例化操作、字段的訪問控制、或者方法的調(diào)用,其他的指令就用來決定常量池條目中記錄的某一特定對象是否某一類或者常量池項(xiàng)中指定的接口。常量池會(huì)判斷類型、字段、方法、類、接口、類字段以及引用是如何在JVM進(jìn)行符號(hào)化描述,而這個(gè)過程由JVM本身進(jìn)行對應(yīng)的判斷。這里就可以理解JVM如何來判斷我們通常說的:“原始變量存儲(chǔ)在內(nèi)存棧上,而引用的對象存儲(chǔ)在內(nèi)存堆上邊。”除了常量池判斷幀數(shù)據(jù)符號(hào)化描述特性以外,這些數(shù)據(jù)幀必須在JVM正常執(zhí)行或者異常執(zhí)行過程輔助它進(jìn)行處理操作。如果一個(gè)方法是正常結(jié)束的,JVM必須恢復(fù)棧幀調(diào)用方法的數(shù)據(jù)幀,而且必須設(shè)置PC寄存器指向調(diào)用方法后邊等待的指令完成該調(diào)用方法的位置。如果該方法存在返回值,JVM也必須將這個(gè)值壓入到操作幀里面以提供給需要這些數(shù)據(jù)的方法進(jìn)行調(diào)用。不僅僅如此,數(shù)據(jù)幀也必須提供一個(gè)方法調(diào)用的異常表,當(dāng)JVM在方法中拋出異常而非正常結(jié)束的時(shí)候,該異常表就用來存放異常信息。
      3)內(nèi)存堆(Heap):
      當(dāng)一個(gè)Java應(yīng)用程序在運(yùn)行的時(shí)候在程序中創(chuàng)建一個(gè)對象或者一個(gè)數(shù)組的時(shí)候,JVM會(huì)針對該對象和數(shù)組分配一個(gè)新的內(nèi)存堆空間。但是在JVM實(shí)例內(nèi)部,只存在一個(gè)內(nèi)存堆實(shí)例,所有的依賴該JVM的Java應(yīng)用程序都需要共享該堆實(shí)例,而Java應(yīng)用程序本身在運(yùn)行的時(shí)候它自己包含了一個(gè)由JVM虛擬機(jī)實(shí)例分配的自己的堆空間,而在應(yīng)用程序啟動(dòng)的時(shí)候,任何一個(gè)Java應(yīng)用程序都會(huì)得到JVM分配的堆空間,而且針對每一個(gè)Java應(yīng)用程序,這些運(yùn)行Java應(yīng)用程序的堆空間都是相互獨(dú)立的。這里所提及到的共享堆實(shí)例是指JVM在初始化運(yùn)行的時(shí)候整體堆空間只有一個(gè),這個(gè)是Java語言平臺(tái)直接從操作系統(tǒng)上能夠拿到的整體堆空間,所以的依賴該JVM的程序都可以得到這些內(nèi)存空間,但是針對每一個(gè)獨(dú)立的Java應(yīng)用程序而言,這些堆空間是相互獨(dú)立的,每一個(gè)Java應(yīng)用程序在運(yùn)行最初都是依靠JVM來進(jìn)行堆空間的分配的。即使是兩個(gè)相同的Java應(yīng)用程序,一旦在運(yùn)行的時(shí)候處于不同的操作系統(tǒng)進(jìn)程(一般為java.exe)中,它們各自分配的堆空間都是獨(dú)立的,不能相互訪問,只是兩個(gè)Java應(yīng)用進(jìn)程初始化拿到的堆空間來自JVM的分配,而JVM是從最初的內(nèi)存堆實(shí)例里面分配出來的。在同一個(gè)Java應(yīng)用程序里面如果出現(xiàn)了不同的線程,則是可以共享每一個(gè)Java應(yīng)用程序拿到的內(nèi)存堆空間的,這也是為什么在開發(fā)多線程程序的時(shí)候,針對同一個(gè)Java應(yīng)用程序必須考慮線程安全問題,因?yàn)樵谝粋€(gè)Java進(jìn)程里面所有的線程是可以共享這個(gè)進(jìn)程拿到的堆空間的數(shù)據(jù)的。但是Java內(nèi)存有一個(gè)特性,就是JVM擁有針對新的對象分配內(nèi)存的指令,但是它卻不包含釋放該內(nèi)存空間指令,當(dāng)然開發(fā)過程可以在Java源代碼中顯示釋放內(nèi)存或者說在JVM字節(jié)碼中進(jìn)行顯示的內(nèi)存釋放,但是JVM僅僅只是檢測堆空間中是否有引用不可達(dá)(不可以引用)的對象,然后將接下來的操作交給垃圾回收器來處理。
      對象表示:
      JVM規(guī)范里面并沒有提及到Java對象如何在堆空間中表示和描述,對象表示可以理解為設(shè)計(jì)JVM的工程師在最初考慮到對象調(diào)用以及垃圾回收器針對對象的判斷而獨(dú)立的一種Java對象在內(nèi)存中的存儲(chǔ)結(jié)構(gòu),該結(jié)構(gòu)是由設(shè)計(jì)最初考慮的。針對一個(gè)創(chuàng)建的類實(shí)例而言,它內(nèi)部定義的實(shí)例變量以及它的超類以及一些相關(guān)的核心數(shù)據(jù),是必須通過一定的途徑進(jìn)行該對象內(nèi)部存儲(chǔ)以及表示的。當(dāng)開發(fā)過程給定了一個(gè)對象引用的時(shí)候,JVM必須能夠通過這個(gè)引用快速從對象堆空間中去拿到該對象能夠訪問的數(shù)據(jù)內(nèi)容。也就是說,堆空間內(nèi)對象的存儲(chǔ)結(jié)構(gòu)必須為外圍對象引用提供一種可以訪問該對象以及控制該對象的接口使得引用能夠順利地調(diào)用該對象以及相關(guān)操作。因此,針對堆空間的對象,分配的內(nèi)存中往往也包含了一些指向方法區(qū)的指針,因?yàn)閺恼w存儲(chǔ)結(jié)構(gòu)上講,方法區(qū)似乎存儲(chǔ)了很多原子級(jí)別的內(nèi)容,包括方法區(qū)內(nèi)最原始最單一的一些變量:比如類字段、字段數(shù)據(jù)、類型數(shù)據(jù)等等。而JVM本身針對堆空間的管理存在兩種設(shè)計(jì)結(jié)構(gòu):
      1】設(shè)計(jì)一:
      堆空間的設(shè)計(jì)可以劃分為兩個(gè)部分:一個(gè)處理池和一個(gè)對象池,一個(gè)對象的引用可以拿到處理池的一個(gè)本地指針,而處理池主要分為兩個(gè)部分:一個(gè)指向?qū)ο蟪乩锩娴闹羔樢约耙粋€(gè)指向方法區(qū)的指針。這種結(jié)構(gòu)的優(yōu)勢在于JVM在處理對象的時(shí)候,更加能夠方便地組合堆碎片以使得所有的數(shù)據(jù)被更加方便地進(jìn)行調(diào)用。當(dāng)JVM需要將一個(gè)對象移動(dòng)到對象池的時(shí)候,它僅僅需要更新該對象的指針到一個(gè)新的對象池的內(nèi)存地址中就可以完成了,然后在處理池中針對該對象的內(nèi)部結(jié)構(gòu)進(jìn)行相對應(yīng)的處理工作。不過這樣的方法也會(huì)出現(xiàn)一個(gè)缺點(diǎn)就是在處理一個(gè)對象的時(shí)候針對對象的訪問需要提供兩個(gè)不同的指針,這一點(diǎn)可能不好理解,其實(shí)可以這樣講,真正在對象處理過程存在一個(gè)根據(jù)時(shí)間戳有區(qū)別的對象狀態(tài),而對象在移動(dòng)、更新以及創(chuàng)建的整個(gè)過程中,它的處理池里面總是包含了兩個(gè)指針,一個(gè)指針是指向對象內(nèi)容本身,一個(gè)指針是指向了方法區(qū),因?yàn)橐粋€(gè)完整的對外的對象是依靠這兩部分被引用指針引用到的,而我們開發(fā)過程是不能夠操作處理池的兩個(gè)指針的,只有引用指針我們可以通過外圍編程拿到。如果Java是按照這種設(shè)計(jì)進(jìn)行對象存儲(chǔ),這里的引用指針就是平時(shí)提及到的“Java的引用”,只是JVM在引用指針還做了一定的封裝,這種封裝的規(guī)則是JVM本身設(shè)計(jì)的時(shí)候做的,它就通過這種結(jié)構(gòu)在外圍進(jìn)行一次封裝,比如Java引用不具備直接操作內(nèi)存地址的能力就是該封裝的一種限制規(guī)則。這種設(shè)計(jì)的結(jié)構(gòu)圖如下:
      2】設(shè)計(jì)二:
      另外一種堆空間設(shè)計(jì)就是使用對象引用拿到的本地指針,將該指針直接指向綁定好的對象的實(shí)例數(shù)據(jù),這些數(shù)據(jù)里面僅僅包含了一個(gè)指向方法區(qū)原子級(jí)別的數(shù)據(jù)去拿到該實(shí)例相關(guān)數(shù)據(jù),這種情況下只需要引用一個(gè)指針來訪問對象實(shí)例數(shù)據(jù),但是這樣的情況使得對象的移動(dòng)以及對象的數(shù)據(jù)更新變得更加復(fù)雜。當(dāng)JVM需要移動(dòng)這些數(shù)據(jù)以及進(jìn)行堆內(nèi)存碎片的整理的時(shí)候,就必須直接更新該對象所有運(yùn)行時(shí)的數(shù)據(jù)區(qū),這種情況可以用下圖進(jìn)行表示:
      JVM需要從一個(gè)對象引用來獲得該引用能夠引用的對象數(shù)據(jù)存在多個(gè)原因,當(dāng)一個(gè)程序試圖將一個(gè)對象的引用轉(zhuǎn)換成為另外一個(gè)類型的時(shí)候,JVM就會(huì)檢查兩個(gè)引用指向的對象是否存在父子類關(guān)系,并且檢查兩個(gè)引用引用到的對象是否能夠進(jìn)行類型轉(zhuǎn)換,而且所有這種類型的轉(zhuǎn)換必須執(zhí)行同樣的一個(gè)操作:instanceof操作,在上邊兩種情況下,JVM都必須要去分析引用指向的對象內(nèi)部的數(shù)據(jù)。當(dāng)一個(gè)程序調(diào)用了一個(gè)實(shí)例方法的時(shí)候,JVM就必須進(jìn)行動(dòng)態(tài)綁定操作,它必須選擇調(diào)用方法的引用類型,是一個(gè)基于類的方法調(diào)用還是一個(gè)基于對象的方法調(diào)用,要做到這一點(diǎn),它又要獲取該對象的唯一引用才可以。不管對象的實(shí)現(xiàn)是使用什么方式來進(jìn)行對象描述,都是在針對內(nèi)存中關(guān)于該對象的方法表進(jìn)行操作,因?yàn)槭褂眠@樣的方式加快了實(shí)例針對方法的調(diào)用,而且在JVM內(nèi)部實(shí)現(xiàn)的時(shí)候這樣的機(jī)制使得其運(yùn)行表現(xiàn)比較良好,所以方法表的設(shè)計(jì)在JVM整體結(jié)構(gòu)中發(fā)揮了極其重要的作用。關(guān)于方法表的存在與否,在JVM規(guī)范里面沒有嚴(yán)格說明,也有可能真正在實(shí)現(xiàn)過程只是一個(gè)抽象概念,物理層它根本不存在,針對放發(fā)表實(shí)現(xiàn)對于一個(gè)創(chuàng)建的實(shí)例而言,它本身具有不太高的內(nèi)存需要求,如果該實(shí)現(xiàn)里面使用了方法表,則對象的方法表應(yīng)該是可以很快被外圍引用訪問到的。
      有一種辦法就是通過對象引用連接到方法表的時(shí)候,如下圖:
      該圖表明,在每個(gè)指針指向一個(gè)對象的時(shí)候,實(shí)際上是使用的一個(gè)特殊的數(shù)據(jù)結(jié)構(gòu),這些特殊的結(jié)構(gòu)包括幾個(gè)部分:
    • 一個(gè)指向該對象類所有數(shù)據(jù)的指針
    • 該對象的方法表
      實(shí)際上從圖中可以看出,方法表就是一個(gè)指針數(shù)組,它的每一個(gè)元素包含了一個(gè)指針,針對每個(gè)對象的方法都可以直接通過該指針在方法區(qū)中找到匹配的數(shù)據(jù)進(jìn)行相關(guān)調(diào)用,而這些方法表需要包括的內(nèi)容如下:
    • 方法內(nèi)存堆棧段空間中操作棧的大小以及局部變量
    • 方法字節(jié)碼
    • 一個(gè)方法的異常表
      這些信息使得JVM足夠針對該方法進(jìn)行調(diào)用,在調(diào)用過程,這種結(jié)構(gòu)也能夠方便子類對象的方法直接通過指針引用到父類的一些方法定義,也就是說指針在內(nèi)存空間之內(nèi)通過JVM本身的調(diào)用使得父類的一些方法表也可以同樣的方式被調(diào)用,當(dāng)然這種調(diào)用過程避免不了兩個(gè)對象之間的類型檢查,但是這樣的方式就使得繼承的實(shí)現(xiàn)變得更加簡單,而且方法表提供的這些數(shù)據(jù)足夠引用對對象進(jìn)行帶有任何OO特征的對象操作。
      另外一種數(shù)據(jù)在上邊的途中沒有顯示出來,也是從邏輯上講內(nèi)存堆中的對象的真實(shí)數(shù)據(jù)結(jié)構(gòu)——對象的。這一點(diǎn)可能需要關(guān)聯(lián)到JMM模型中講的進(jìn)行理解。JVM中的每一個(gè)對象都是和一個(gè)鎖(互斥)相關(guān)聯(lián)的,這種結(jié)構(gòu)使得該對象可以很容易支持多線程訪問,而且該對象的對象鎖一次只能被一個(gè)線程訪問。當(dāng)一個(gè)線程在運(yùn)行的時(shí)候具有某個(gè)對象的鎖的時(shí)候,僅僅只有這個(gè)線程可以訪問該對象的實(shí)例變量,其他線程如果需要訪問該實(shí)例的實(shí)例變量就必須等待這個(gè)線程將它占有的對象鎖釋放過后才能夠正常訪問,如果一個(gè)線程請求了一個(gè)被其他線程占有的對象鎖,這個(gè)請求線程也必須等到該鎖被釋放過后才能夠拿到這個(gè)對象的對象鎖。一旦這個(gè)線程擁有了一個(gè)對象鎖過后,它自己可以多次向同一個(gè)鎖發(fā)送對象的鎖請求,但是如果它要使得被該線程鎖住的對象可以被其他鎖訪問到的話就需要同樣的釋放鎖的次數(shù),比如線程A請求了對象B的對象鎖三次,那么A將會(huì)一直占有B對象的對象鎖,直到它將該對象鎖釋放了三次。
      很多對象也可能在整個(gè)生命周期都沒有被對象鎖鎖住過,在這樣的情況下對象鎖相關(guān)的數(shù)據(jù)是不需要對象內(nèi)部實(shí)現(xiàn)的,除非有線程向該對象請求了對象鎖,否則這個(gè)對象就沒有該對象鎖的存儲(chǔ)結(jié)構(gòu)。所以上邊的實(shí)現(xiàn)圖可以知道,很多實(shí)現(xiàn)不包括指向?qū)ο箧i的“鎖數(shù)據(jù)”,鎖數(shù)據(jù)的實(shí)現(xiàn)必須要等待某個(gè)線程向該對象發(fā)送了對象鎖請求過后,而且是在第一次鎖請求過后才會(huì)被實(shí)現(xiàn)。這個(gè)結(jié)構(gòu)中,JVM卻能夠間接地通過一些辦法針對對象的鎖進(jìn)行管理,比如把對象鎖放在基于對象地址的搜索樹上邊。實(shí)現(xiàn)了鎖結(jié)構(gòu)的對象中,每一個(gè)Java對象邏輯上都在內(nèi)存中成為了一個(gè)等待集,這樣就使得所有的線程在鎖結(jié)構(gòu)里面針對對象內(nèi)部數(shù)據(jù)可以獨(dú)立操作,等待集就使得每個(gè)線程能夠獨(dú)立于其他線程去完成一個(gè)共同的設(shè)計(jì)目標(biāo)以及程序執(zhí)行的最終結(jié)果,這樣就使得多線程的線程獨(dú)享數(shù)據(jù)以及線程共享數(shù)據(jù)機(jī)制很容易實(shí)現(xiàn)。
      不僅僅如此,針對內(nèi)存堆對象還必須存在一個(gè)對象的鏡像,該鏡像的主要目的是提供給垃圾回收器進(jìn)行監(jiān)控操作,垃圾回收器是通過對象的狀態(tài)來判斷該對象是否被應(yīng)用,同樣它需要針對堆內(nèi)的對象進(jìn)行監(jiān)控。而當(dāng)監(jiān)控過程垃圾回收器收到對象回收的事件觸發(fā)的時(shí)候,雖然使用了不同的垃圾回收算法,不論使用什么算法都需要通過獨(dú)有的機(jī)制來判斷對象目前處于哪種狀態(tài),然后根據(jù)對象狀態(tài)進(jìn)行操作。開發(fā)過程程序員往往不會(huì)去仔細(xì)分析當(dāng)一個(gè)對象引用設(shè)置成為null了過后虛擬機(jī)內(nèi)部的操作,但實(shí)際上Java里面的引用往往不像我們想像中那么簡單,Java引用中的虛引用、弱引用就是使得Java引用在顯示提交可回收狀態(tài)的情況下對內(nèi)存堆中的對象進(jìn)行的反向監(jiān)控,這些引用可以監(jiān)視到垃圾回收器回收該對象的過程。垃圾回收器本身的實(shí)現(xiàn)也是需要內(nèi)存堆中的對象能夠提供相對應(yīng)的數(shù)據(jù)的。其實(shí)這個(gè)位置到底JVM里面是否使用了完整的Java對象的鏡像還是使用的一個(gè)鏡像索引我沒有去仔細(xì)分析過,總之是在堆結(jié)構(gòu)里面存在著堆內(nèi)對象的一個(gè)類似拷貝的鏡像機(jī)制,使得垃圾回收器能夠順利回收不再被引用的對象。
      4)內(nèi)存棧和內(nèi)存堆的實(shí)現(xiàn)原理探測【該部分為不確定概念】:
      實(shí)際上不論是內(nèi)存棧結(jié)構(gòu)、方法區(qū)還是內(nèi)存堆結(jié)構(gòu),歸根到底使用的是操作系統(tǒng)的內(nèi)存,操作系統(tǒng)的內(nèi)存結(jié)構(gòu)可以理解為內(nèi)存塊,常用的抽象方式就是一個(gè)內(nèi)存堆棧,而JVM在OS上邊安裝了過后,就在啟動(dòng)Java程序的時(shí)候按照配置文件里面的內(nèi)容向操作系統(tǒng)申請內(nèi)存空間,該內(nèi)存空間會(huì)按照J(rèn)VM內(nèi)部的方法提供相應(yīng)的結(jié)構(gòu)調(diào)整。
      內(nèi)存棧應(yīng)該是很容易理解的結(jié)構(gòu)實(shí)現(xiàn),一般情況下,內(nèi)存棧是保持連續(xù)的,但是不絕對,內(nèi)存棧申請到的地址實(shí)際上很多情況下都是連續(xù)的,而每個(gè)地址的最小單位是按照計(jì)算機(jī)位來算的,該計(jì)算機(jī)位里面只有兩種狀態(tài)1和0,而內(nèi)存棧的使用過程就是典型的類似C++里面的普通指針結(jié)構(gòu)的使用過程,直接針對指針進(jìn)行++或者--操作就修改了該指針針對內(nèi)存的偏移量,而這些偏移量就使得該指針可以調(diào)用不同的內(nèi)存棧中的數(shù)據(jù)。至于針對內(nèi)存棧發(fā)送的指令就是常見的計(jì)算機(jī)指令,而這些指令就使得該指針針對內(nèi)存棧的棧幀進(jìn)行指令發(fā)送,比如發(fā)送操作指令、變量讀取等等,直接就使得內(nèi)存棧的調(diào)用變得更加簡單,而且棧幀在接受了該數(shù)據(jù)過后就知道到底針對棧幀內(nèi)部的哪一個(gè)部分進(jìn)行調(diào)用,是操作幀、數(shù)據(jù)幀還是局部變量。
      內(nèi)存堆實(shí)際上在操作系統(tǒng)里面使用了雙向鏈表的數(shù)據(jù)結(jié)構(gòu),雙向鏈表的結(jié)構(gòu)使得即使內(nèi)存堆不具有連續(xù)性,每一個(gè)堆空間里面的鏈表也可以進(jìn)入下一個(gè)堆空間,而操作系統(tǒng)本身在整理內(nèi)存堆的時(shí)候會(huì)做一些簡單的操作,然后通過每一個(gè)內(nèi)存堆的雙向鏈表就使得內(nèi)存堆更加方便。而且堆空間不需要有序,甚至說有序不影響堆空間的存儲(chǔ)結(jié)構(gòu),因?yàn)樗鼩w根到底是在內(nèi)存塊上邊進(jìn)行實(shí)現(xiàn)的,內(nèi)存塊本身是一個(gè)堆棧結(jié)構(gòu),只是該內(nèi)存堆棧里面的塊如何分配不由JVM決定,是由操作系統(tǒng)已經(jīng)最開始分配好了,也就是最小存儲(chǔ)單位。然后JVM拿到從操作系統(tǒng)申請的堆空間過后,先進(jìn)行初始化操作,然后就可以直接使用了。
      常見的對程序有影響的內(nèi)存問題主要是兩種:溢出和內(nèi)存泄漏,上邊已經(jīng)講過了內(nèi)存泄漏,其實(shí)從內(nèi)存的結(jié)構(gòu)分析,泄漏這種情況很難甚至說不可能發(fā)生在棧空間里面,其主要原因是??臻g本身很難出現(xiàn)懸停的內(nèi)存,因?yàn)闂?臻g的存儲(chǔ)結(jié)構(gòu)有可能是內(nèi)存的一個(gè)地址數(shù)組,所以在訪問??臻g的時(shí)候使用的都是索引或者下標(biāo)或者就是最原始的出棧和入棧的操作,這些操作使得棧里面很難出現(xiàn)像堆空間一樣的內(nèi)存懸停(也就是引用懸掛問題。堆空間懸停的內(nèi)存是因?yàn)闂V写娣诺囊玫淖兓?,其?shí)引用可以理解為從棧到堆的一個(gè)指針,當(dāng)該指針發(fā)生變化的時(shí)候,堆內(nèi)存碎片就有可能產(chǎn)生,而這種情況下在原始語言里面就經(jīng)常發(fā)生內(nèi)存泄漏的情況,因?yàn)檫@些懸停的堆空間在系統(tǒng)里面是不能夠被任何本地指針引用到,就使得這些對象在未被回收的時(shí)候脫離了可操作區(qū)域并且占用了系統(tǒng)資源。
      棧溢出問題一直都是計(jì)算機(jī)領(lǐng)域里面的一個(gè)安全性問題,這里不做深入討論,說多了就偏離主題了,而內(nèi)存泄漏是程序員最容易理解的內(nèi)存問題,還有一個(gè)問題來自于我一個(gè)黑客朋友就是:堆溢出現(xiàn)象,這種現(xiàn)象可能更加復(fù)雜。
      其實(shí)Java里面的內(nèi)存結(jié)構(gòu),最初看來就是堆和棧的結(jié)合,實(shí)際上可以這樣理解,實(shí)際上對象的實(shí)際內(nèi)容才存在對象池里面,而有關(guān)對象的其他東西有可能會(huì)存儲(chǔ)于方法區(qū),而平時(shí)使用的時(shí)候的引用是存在內(nèi)存棧上的,這樣就更加容易理解它內(nèi)部的結(jié)構(gòu),不僅僅如此,有時(shí)候還需要考慮到Java里面的一些字段和屬性到底是對象域的還是類域的,這個(gè)也是一個(gè)比較復(fù)雜的問題。
      二者的區(qū)別簡單總結(jié)一下:
    • 管理方式:JVM自己可以針對內(nèi)存棧進(jìn)行管理操作,而且該內(nèi)存空間的釋放是編譯器就可以操作的內(nèi)容,而堆空間在Java中JVM本身執(zhí)行引擎不會(huì)對其進(jìn)行釋放操作,而是讓垃圾回收器進(jìn)行自動(dòng)回收
    • 空間大?。?/strong>一般情況下??臻g相對于堆空間而言比較小,這是由棧空間里面存儲(chǔ)的數(shù)據(jù)以及本身需要的數(shù)據(jù)特性決定的,而堆空間在JVM堆實(shí)例進(jìn)行分配的時(shí)候一般大小都比較大,因?yàn)槎芽臻g在一個(gè)Java程序中需要存儲(chǔ)太多的Java對象數(shù)據(jù)
    • 碎片相關(guān):針對堆空間而言,即使垃圾回收器能夠進(jìn)行自動(dòng)堆內(nèi)存回收,但是堆空間的活動(dòng)量相對棧空間而言比較大,很有可能存在長期的堆空間分配和釋放操作,而且垃圾回收器不是實(shí)時(shí)的,它有可能使得堆空間的內(nèi)存碎片主鍵累積起來。針對??臻g而言,因?yàn)樗旧砭褪且粋€(gè)堆棧的數(shù)據(jù)結(jié)構(gòu),它的操作都是一一對應(yīng)的,而且每一個(gè)最小單位的結(jié)構(gòu)棧幀和堆空間內(nèi)復(fù)雜的內(nèi)存結(jié)構(gòu)不一樣,所以它一般在使用過程很少出現(xiàn)內(nèi)存碎片。
    • 分配方式:一般情況下,棧空間有兩種分配方式:靜態(tài)分配和動(dòng)態(tài)分配,靜態(tài)分配是本身由編譯器分配好了,而動(dòng)態(tài)分配可能根據(jù)情況有所不同,而堆空間卻是完全的動(dòng)態(tài)分配的,是一個(gè)運(yùn)行時(shí)級(jí)別的內(nèi)存分配。而??臻g分配的內(nèi)存不需要我們考慮釋放問題,而堆空間即使在有垃圾回收器的前提下還是要考慮其釋放問題。
    • 效率:因?yàn)閮?nèi)存塊本身的排列就是一個(gè)典型的堆棧結(jié)構(gòu),所以??臻g的效率自然比起堆空間要高很多,而且計(jì)算機(jī)底層內(nèi)存空間本身就使用了最基礎(chǔ)的堆棧結(jié)構(gòu)使得棧空間和底層結(jié)構(gòu)更加符合,它的操作也變得簡單就是最簡單的兩個(gè)指令:入棧和出棧;??臻g針對堆空間而言的弱點(diǎn)是靈活程度不夠,特別是在動(dòng)態(tài)管理的時(shí)候。而堆空間最大的優(yōu)勢在于動(dòng)態(tài)分配,因?yàn)樗谟?jì)算機(jī)底層實(shí)現(xiàn)可能是一個(gè)雙向鏈表結(jié)構(gòu),所以它在管理的時(shí)候操作比棧空間復(fù)雜很多,自然它的靈活度就高了,但是這樣的設(shè)計(jì)也使得堆空間的效率不如棧空間,而且低很多。

    3.本機(jī)內(nèi)存[部分內(nèi)容來源于IBM開發(fā)中心]
      Java堆空間是在編寫Java程序中被我們使用得最頻繁的內(nèi)存空間,平時(shí)開發(fā)過程,開發(fā)人員一定遇到過OutOfMemoryError,這種結(jié)果有可能來源于Java堆空間的內(nèi)存泄漏,也可能是因?yàn)槎训?strong>大小不夠而導(dǎo)致的,有時(shí)候這些錯(cuò)誤是可以依靠開發(fā)人員修復(fù)的,但是隨著Java程序需要處理越來越多的并發(fā)程序,可能有些錯(cuò)誤就不是那么容易處理了。有些時(shí)候即使Java堆空間沒有滿也可能拋出錯(cuò)誤,這種情況下需要了解的就是JRE(Java Runtime Environment)內(nèi)部到底發(fā)生了什么。Java本身的運(yùn)行宿主環(huán)境并不是操作系統(tǒng),而Java虛擬機(jī),Java虛擬機(jī)本身是用C編寫的本機(jī)程序,自然它會(huì)調(diào)用到本機(jī)資源,最常見的就是針對本機(jī)內(nèi)存的調(diào)用。本機(jī)內(nèi)存是可以用于運(yùn)行時(shí)進(jìn)程的,它和Java應(yīng)用程序使用的Java堆內(nèi)存不一樣,每一種虛擬化資源都必須存儲(chǔ)在本機(jī)內(nèi)存里面,包括虛擬機(jī)本身運(yùn)行的數(shù)據(jù),這樣也意味著主機(jī)的硬件和操作系統(tǒng)在本機(jī)內(nèi)存的限制將直接影響到Java應(yīng)用程序的性能
      i.Java運(yùn)行時(shí)如何使用本機(jī)內(nèi)存:
      1)堆空間和垃圾回收
      Java運(yùn)行時(shí)是一個(gè)操作系統(tǒng)進(jìn)程(Windows下一般為java.exe),該環(huán)境提供的功能會(huì)受一些位置的用戶代碼驅(qū)動(dòng),這雖然提高了運(yùn)行時(shí)在處理資源的靈活性,但是無法預(yù)測每種情況下運(yùn)行時(shí)環(huán)境需要何種資源,這一點(diǎn)Java堆空間講解中已經(jīng)提到過了。在Java命令行可以使用-Xmx-Xms來控制堆空間初始配置,mx表示堆空間的最大大小,ms表示初始化大小,這也是上提到的啟動(dòng)Java的配置文件可以配置的內(nèi)容。盡管邏輯內(nèi)存堆可以根據(jù)堆上的對象數(shù)量和在GC上花費(fèi)的時(shí)間增加或者減少,但是使用本機(jī)內(nèi)存的大小是保持不變的,而且由-Xms的值指定,大部分GC算法都是依賴被分配的連續(xù)內(nèi)存塊的堆空間,因此不能在堆需要擴(kuò)大的時(shí)候分配更多本機(jī)內(nèi)存,所有的堆內(nèi)存必須保留下來,請注意這里說的不是Java堆內(nèi)存空間本機(jī)內(nèi)存。
      本機(jī)內(nèi)存保留本機(jī)內(nèi)存分配不一樣,本機(jī)內(nèi)存被保留的時(shí)候,無法使用物理內(nèi)存或者其他存儲(chǔ)器作為備用內(nèi)存,盡管保留地址空間塊不會(huì)耗盡物理資源,但是會(huì)阻止內(nèi)存用于其他用途,由保留從未使用過的內(nèi)存導(dǎo)致的泄漏和泄漏分配的內(nèi)存造成的問題其嚴(yán)重程度差不多,但使用的堆區(qū)域縮小時(shí),一些垃圾回收器會(huì)回收堆空間的一部分內(nèi)容,從而減少物理內(nèi)存的使用。對于維護(hù)Java堆的內(nèi)存管理系統(tǒng),需要更多的本機(jī)內(nèi)存來維護(hù)它的狀態(tài),進(jìn)行垃圾收集的時(shí)候,必須分配數(shù)據(jù)結(jié)構(gòu)來跟蹤空閑存儲(chǔ)空間和進(jìn)度記錄,這些數(shù)據(jù)結(jié)構(gòu)的確切大小和性質(zhì)因?qū)崿F(xiàn)的不同而有所差異。
      2)JIT
      JIT編譯器在運(yùn)行時(shí)編譯Java字節(jié)碼來優(yōu)化本機(jī)可執(zhí)行代碼,這樣極大提高了Java運(yùn)行時(shí)的速度,并且支持Java應(yīng)用程序與本地代碼相當(dāng)?shù)乃俣冗\(yùn)行。字節(jié)碼編譯使用本機(jī)內(nèi)存,而且JIT編譯器的輸入(字節(jié)碼)和輸出(可執(zhí)行代碼)也必須存儲(chǔ)在本機(jī)內(nèi)存里面,包含了多個(gè)經(jīng)過JIT編譯的方法的Java程序會(huì)比一些小型應(yīng)用程序使用更多的本機(jī)內(nèi)存。
      3)類和類加載器
      Java 應(yīng)用程序由一些類組成,這些類定義對象結(jié)構(gòu)和方法邏輯。Java 應(yīng)用程序也使用 Java 運(yùn)行時(shí)類庫(比如 java.lang.String中的類,也可以使用第三方庫。這些類需要存儲(chǔ)在內(nèi)存中以備使用。存儲(chǔ)類的方式取決于具體實(shí)現(xiàn)。Sun JDK 使用永久生成(permanent generation,PermGen)堆區(qū)域,從最基本的層面來看,使用更多的類將需要使用更多內(nèi)存。(這可能意味著您的本機(jī)內(nèi)存使用量會(huì)增加,或者您必須明確地重新設(shè)置 PermGen 或共享類緩存等區(qū)域的大小,以裝入所有類)。記住,不僅您的應(yīng)用程序需要加載到內(nèi)存中,框架、應(yīng)用服務(wù)器、第三方庫以及包含類的 Java 運(yùn)行時(shí)也會(huì)按需加載并占用空間。Java 運(yùn)行時(shí)可以卸載類來回收空間,但是只有在非常嚴(yán)酷的條件下才會(huì)這樣做,不能卸載單個(gè)類,而是卸載類加載器,隨其加載的所有類都會(huì)被卸載。只有在以下情況下才能卸載類加載器
    • Java 堆不包含對表示該類加載器的 java.lang.ClassLoader 對象的引用。
    • Java 堆不包含對表示類加載器加載的類的任何 java.lang.Class 對象的引用。
    • 在 Java 堆上,該類加載器加載的任何類的所有對象都不再存活(被引用)。

      需要注意的是,Java 運(yùn)行時(shí)為所有 Java 應(yīng)用程序創(chuàng)建的 3 個(gè)默認(rèn)類加載器 bootstrapextension 和 application 都不可能滿足這些條件,因此,任何系統(tǒng)類(比如 java.lang.String)或通過應(yīng)用程序類加載器加載的任何應(yīng)用程序類都不能在運(yùn)行時(shí)釋放。即使類加載器適合進(jìn)行收集,運(yùn)行時(shí)也只會(huì)將收集類加載器作為 GC 周期的一部分。一些實(shí)現(xiàn)只會(huì)在某些 GC 周期中卸載類加載器,也可能在運(yùn)行時(shí)生成類,而不去釋放它。許多 Java EE 應(yīng)用程序使用 JavaServer Pages (JSP) 技術(shù)來生成 Web 頁面。使用 JSP 會(huì)為執(zhí)行的每個(gè) .jsp 頁面生成一個(gè)類,并且這些類會(huì)在加載它們的類加載器的整個(gè)生存期中一直存在 —— 這個(gè)生存期通常是 Web 應(yīng)用程序的生存期。另一種生成類的常見方法是使用 Java 反射。反射的工作方式因 Java 實(shí)現(xiàn)的不同而不同,當(dāng)使用 java.lang.reflect API 時(shí),Java 運(yùn)行時(shí)必須將一個(gè)反射對象(比如 java.lang.reflect.Field)的方法連接到被反射到的對象或類。這可以通過使用 Java 本機(jī)接口(Java Native Interface,JNI訪問器來完成,這種方法需要的設(shè)置很少,但是速度緩慢,也可以在運(yùn)行時(shí)為您想要反射到的每種對象類型動(dòng)態(tài)構(gòu)建一個(gè)類。后一種方法在設(shè)置上更慢,但運(yùn)行速度更快,非常適合于經(jīng)常反射到一個(gè)特定類的應(yīng)用程序。Java 運(yùn)行時(shí)在最初幾次反射到一個(gè)類時(shí)使用 JNI 方法,但當(dāng)使用了若干次 JNI 方法之后,訪問器會(huì)膨脹為字節(jié)碼訪問器,這涉及到構(gòu)建類并通過新的類加載器進(jìn)行加載。執(zhí)行多次反射可能導(dǎo)致創(chuàng)建了許多訪問器類和類加載器,保持對反射對象的引用會(huì)導(dǎo)致這些類一直存活,并繼續(xù)占用空間,因?yàn)閯?chuàng)建字節(jié)碼訪問器非常緩慢,所以 Java 運(yùn)行時(shí)可以緩存這些訪問器以備以后使用,一些應(yīng)用程序和框架還會(huì)緩存反射對象,這進(jìn)一步增加了它們的本機(jī)內(nèi)存占用。

      4)JNI
      JNI支持本機(jī)代碼調(diào)用Java方法,反之亦然,Java運(yùn)行時(shí)本身極大依賴于JNI代碼來實(shí)現(xiàn)類庫功能,比如文件和網(wǎng)絡(luò)I/O,JNI應(yīng)用程序可以通過三種方式增加Java運(yùn)行時(shí)對本機(jī)內(nèi)存的使用:
    • JNI應(yīng)用程序的本機(jī)代碼被編譯到共享庫中,或編譯為加載到進(jìn)程地址空間中的可執(zhí)行文件,大型本機(jī)應(yīng)用程序可能僅僅加載就會(huì)占用大量進(jìn)程地址空間
    • 本機(jī)代碼必須與Java運(yùn)行時(shí)共享地址空間,任何本機(jī)代碼分配本機(jī)代碼執(zhí)行內(nèi)存映射都會(huì)耗用Java運(yùn)行時(shí)內(nèi)存
    • 某些JNI函數(shù)可能在它們的常規(guī)操作中使用本機(jī)內(nèi)存,GetTypeArrayElementsGetTypeArrayRegion函數(shù)可以將Java堆復(fù)制到本機(jī)內(nèi)存緩沖區(qū)中,提供給本地代碼使用,是否復(fù)制數(shù)據(jù)依賴于運(yùn)行時(shí)實(shí)現(xiàn),通過這種方式訪問大量Java堆數(shù)據(jù)就可能使用大量的本機(jī)內(nèi)存堆空間
      5)NIO
      JDK 1.4開始添加了新的I/O類,引入了一種基于通道和緩沖區(qū)執(zhí)行I/O的新方式,就像Java堆上的內(nèi)存支持I/O緩沖區(qū)一樣,NIO添加了對直接ByteBuffer的支持,ByteBuffer受本機(jī)內(nèi)存而不是Java堆的支持,直接ByteBuffer可以直接傳遞到本機(jī)操作系統(tǒng)庫函數(shù),以執(zhí)行I/O,這種情況雖然提高了Java程序在I/O的執(zhí)行效率,但是會(huì)對本機(jī)內(nèi)存進(jìn)行直接的內(nèi)存開銷。ByteBuffer直接操作和非直接操作的區(qū)別如下:
      對于在何處存儲(chǔ)直接 ByteBuffer 數(shù)據(jù),很容易產(chǎn)生混淆。應(yīng)用程序仍然在 Java 堆上使用一個(gè)對象來編排 I/O 操作,但持有該數(shù)據(jù)的緩沖區(qū)將保存在本機(jī)內(nèi)存中,Java 堆對象僅包含對本機(jī)堆緩沖區(qū)的引用。非直接 ByteBuffer 將其數(shù)據(jù)保存在 Java 堆上的 byte[] 數(shù)組中。直接ByteBuffer對象會(huì)自動(dòng)清理本機(jī)緩沖區(qū),但這個(gè)過程只能作為Java堆GC的一部分執(zhí)行,它不會(huì)自動(dòng)影響施加在本機(jī)上的壓力。GC僅在Java堆被填滿,以至于無法為堆分配請求提供服務(wù)的時(shí)候,或者在Java應(yīng)用程序中顯示請求它發(fā)生。
      6)線程:
      應(yīng)用程序中的每個(gè)線程都需要內(nèi)存來存儲(chǔ)器堆棧(用于在調(diào)用函數(shù)時(shí)持有局部變量并維護(hù)狀態(tài)的內(nèi)存區(qū)域)。每個(gè) Java 線程都需要堆??臻g來運(yùn)行。根據(jù)實(shí)現(xiàn)的不同,Java 線程可以分為本機(jī)線程和 Java 堆棧。除了堆??臻g,每個(gè)線程還需要為線程本地存儲(chǔ)(thread-local storage)內(nèi)部數(shù)據(jù)結(jié)構(gòu)提供一些本機(jī)內(nèi)存。盡管每個(gè)線程使用的內(nèi)存量非常小,但對于擁有數(shù)百個(gè)線程的應(yīng)用程序來說,線程堆棧的總內(nèi)存使用量可能非常大。如果運(yùn)行的應(yīng)用程序的線程數(shù)量比可用于處理它們的處理器數(shù)量多,效率通常很低,并且可能導(dǎo)致糟糕的性能和更高的內(nèi)存占用。
      ii.本機(jī)內(nèi)存耗盡:
      Java運(yùn)行時(shí)善于以不同的方式來處理Java堆空間的耗盡本機(jī)堆空間的耗盡,但是這兩種情形具有類似癥狀,當(dāng)Java堆空間耗盡的時(shí)候,Java應(yīng)用程序很難正常運(yùn)行,因?yàn)镴ava應(yīng)用程序必須通過分配對象來完成工作,只要Java堆被填滿,就會(huì)出現(xiàn)糟糕的GC性能,并且拋出OutOfMemoryError。相反,一旦 Java 運(yùn)行時(shí)開始運(yùn)行并且應(yīng)用程序處于穩(wěn)定狀態(tài),它可以在本機(jī)堆完全耗盡之后繼續(xù)正常運(yùn)行,不一定會(huì)發(fā)生奇怪的行為,因?yàn)樾枰峙浔緳C(jī)內(nèi)存的操作比需要分配 Java 堆的操作少得多。盡管需要本機(jī)內(nèi)存的操作因 JVM 實(shí)現(xiàn)不同而異,但也有一些操作很常見:啟動(dòng)線程、加載類以及執(zhí)行某種類型的網(wǎng)絡(luò)文件 I/O本機(jī)內(nèi)存不足行為與 Java 堆內(nèi)存不足行為也不太一樣,因?yàn)闊o法對本機(jī)堆分配進(jìn)行控制,盡管所有 Java 堆分配都在 Java 內(nèi)存管理系統(tǒng)控制之下,但任何本機(jī)代碼(無論其位于 JVM、Java 類庫還是應(yīng)用程序代碼中)都可能執(zhí)行本機(jī)內(nèi)存分配,而且會(huì)失敗。嘗試進(jìn)行分配的代碼然后會(huì)處理這種情況,無論設(shè)計(jì)人員的意圖是什么:它可能通過 JNI 接口拋出一個(gè) OutOfMemoryError,在屏幕上輸出一條消息,發(fā)生無提示失敗并在稍后再試一次,或者執(zhí)行其他操作。
      iii.例子:
      這篇文章一致都在講概念,這里既然提到了ByteBuffer,先提供一個(gè)簡單的例子演示該類的使用:
      ——[$]使用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的方式讀取文件系統(tǒng)上的文件,這段程序的輸入就為:
    Hello World
      ——[$]獲取ByteBuffer上的字節(jié)轉(zhuǎn)換為Byte數(shù)組——
    package org.susan.java.io;

    import java.nio.ByteBuffer;

    public class ByteBufferToByteArray {
        public static void main(String args[]) throws Exception{
            // 從byte數(shù)組創(chuàng)建ByteBuffer
            byte[] bytes = new byte[10];
            ByteBuffer buffer = ByteBuffer.wrap(bytes);

            // 在position和limit,也就是ByteBuffer緩沖區(qū)的首尾之間讀取字節(jié)
            bytes = new byte[buffer.remaining()];
            buffer.get(bytes, 0, bytes.length);

            // 讀取所有ByteBuffer內(nèi)的字節(jié)
            buffer.clear();
            bytes = new byte[buffer.capacity()];
            buffer.get(bytes, 0, bytes.length);
        }
    }
      上邊代碼就是從ByteBuffer到byte數(shù)組轉(zhuǎn)換過程,有了這個(gè)過程在開發(fā)過程中可能更加方便,ByteBuffer的詳細(xì)講解我保留到IO部分,這里僅僅是涉及到了一些,所以提供兩段實(shí)例代碼。
      iv.共享內(nèi)存:
      在Java語言里面,沒有共享內(nèi)存的概念,但是在某些引用中,共享內(nèi)存卻很受用,例如Java語言的分布式系統(tǒng),存著大量的Java分布式共享對象,很多時(shí)候需要查詢這些對象的狀態(tài),以查看系統(tǒng)是否運(yùn)行正?;蛘吡私膺@些對象目前的一些統(tǒng)計(jì)數(shù)據(jù)和狀態(tài)。如果使用的是網(wǎng)絡(luò)通信的方式,顯然會(huì)增加應(yīng)用的額外開銷,也增加了不必要的應(yīng)用編程,如果是共享內(nèi)存方式,則可以直接通過共享內(nèi)存查看到所需要的對象的數(shù)據(jù)和統(tǒng)計(jì)數(shù)據(jù),從而減少一些不必要的麻煩。
      1)共享內(nèi)存特點(diǎn):
    • 可以被多個(gè)進(jìn)程打開訪問
    • 讀寫操作的進(jìn)程在執(zhí)行讀寫操作的時(shí)候其他進(jìn)程不能進(jìn)行寫操作
    • 多個(gè)進(jìn)程可以交替對某一個(gè)共享內(nèi)存執(zhí)行寫操作
    • 一個(gè)進(jìn)程執(zhí)行了內(nèi)存寫操作過后,不影響其他進(jìn)程對該內(nèi)存的訪問,同時(shí)其他進(jìn)程對更新后的內(nèi)存具有可見性
    • 在進(jìn)程執(zhí)行寫操作時(shí)如果異常退出,對其他進(jìn)程的寫操作禁止自動(dòng)解除
    • 相對共享文件,數(shù)據(jù)訪問的方便性和效率  
      2)出現(xiàn)情況:
    • 獨(dú)占的寫操作,相應(yīng)有獨(dú)占的寫操作等待隊(duì)列。獨(dú)占的寫操作本身不會(huì)發(fā)生數(shù)據(jù)的一致性問題;
    • 共享的寫操作,相應(yīng)有共享的寫操作等待隊(duì)列。共享的寫操作則要注意防止發(fā)生數(shù)據(jù)的一致性問題;
    • 獨(dú)占的讀操作,相應(yīng)有共享的讀操作等待隊(duì)列;
    • 共享的讀操作,相應(yīng)有共享的讀操作等待隊(duì)列;
      3)Java中共享內(nèi)存的實(shí)現(xiàn):
      JDK 1.4里面的MappedByteBuffer為開發(fā)人員在Java中實(shí)現(xiàn)共享內(nèi)存提供了良好的方法,該緩沖區(qū)實(shí)際上是一個(gè)磁盤文件的內(nèi)存映象,二者的變化會(huì)保持同步,即內(nèi)存數(shù)據(jù)發(fā)生變化過后會(huì)立即反應(yīng)到磁盤文件中,這樣會(huì)有效地保證共享內(nèi)存的實(shí)現(xiàn),將共享文件和磁盤文件簡歷聯(lián)系的是文件通道類:FileChannel,該類的加入是JDK為了統(tǒng)一外圍設(shè)備的訪問方法,并且加強(qiáng)了多線程對同一文件進(jìn)行存取的安全性,這里可以使用它來建立共享內(nèi)存用,它建立了共享內(nèi)存和磁盤文件之間的一個(gè)通道。打開一個(gè)文件可使用RandomAccessFile類的getChannel方法,該方法直接返回一個(gè)文件通道,該文件通道由于對應(yīng)的文件設(shè)為隨機(jī)存取,一方面可以進(jìn)行讀寫兩種操作,另外一個(gè)方面使用它不會(huì)破壞映象文件的內(nèi)容。這里,如果使用FileOutputStream和FileInputStream則不能理想地實(shí)現(xiàn)共享內(nèi)存的要求,因?yàn)檫@兩個(gè)類同時(shí)實(shí)現(xiàn)自由讀寫很困難。
      下邊代碼段實(shí)現(xiàn)了上邊提及的共享內(nèi)存功能
    // 獲得一個(gè)只讀的隨機(jī)存取文件對象
    RandomAccessFile RAFile = new RandomAccessFile(filename,"r");
    // 獲得相應(yīng)的文件通道
    FileChannel fc = RAFile.getChannel();
    // 取得文件的實(shí)際大小
    int size = (int)fc.size();

    // 獲得共享內(nèi)存緩沖區(qū),該共享內(nèi)存只讀 
    MappedByteBuffer mapBuf = fc.map(FileChannel.MAP_RO,0,size);

    // 獲得一個(gè)可讀寫的隨機(jī)存取文件對象 
    RAFile = new RandomAccessFile(filename,"rw");

    // 獲得相應(yīng)的文件通道 
    fc = RAFile.getChannel();

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

    // 獲得共享內(nèi)存緩沖區(qū),該共享內(nèi)存可讀寫 
    mapBuf = fc.map(FileChannel.MAP_RW,0,size);

    // 獲取頭部消息:存取權(quán)限 

    mode = mapBuf.getInt(); 
      如果多個(gè)應(yīng)用映象使用同一文件名的共享內(nèi)存,則意味著這多個(gè)應(yīng)用共享了同一內(nèi)存數(shù)據(jù),這些應(yīng)用對于文件可以具有同等存取權(quán)限,一個(gè)應(yīng)用對數(shù)據(jù)的刷新會(huì)更新到多個(gè)應(yīng)用中。為了防止多個(gè)應(yīng)用同時(shí)對共享內(nèi)存進(jìn)行寫操作,可以在該共享內(nèi)存的頭部信息加入寫操作標(biāo)記,該共享文件的頭部基本信息至少有:
    • 共享內(nèi)存長度
    • 共享內(nèi)存目前的存取模式
      共享文件的頭部信息是私有信息,多個(gè)應(yīng)用可以對同一個(gè)共享內(nèi)存執(zhí)行寫操作,執(zhí)行寫操作和結(jié)束寫操作的時(shí)候,可以使用如下方法:
    public boolean startWrite()
    {
        if(mode == 0// 這里mode代表共享內(nèi)存的存取模式,為0代表可寫
        {
            mode = 1; // 意味著別的應(yīng)用不可寫
            mapBuf.flip();
            mapBuf.putInt(mode);    //寫入共享內(nèi)存的頭部信息
            return true;
        }
        else{
            return false//表明已經(jīng)有應(yīng)用在寫該共享內(nèi)存了,本應(yīng)用不能夠針對共享內(nèi)存再做寫操作
        }
    }

    public boolean stopWrite()
    {
        mode = 0// 釋放寫權(quán)限
        mapBuf.flip();
        mapBuf.putInt(mode);    //寫入共享內(nèi)存頭部信息
        return true;
    }
      【*:上邊提供了對共享內(nèi)存執(zhí)行寫操作過程的兩個(gè)方法,這兩個(gè)方法其實(shí)理解起來很簡單,真正需要思考的是一個(gè)針對存取模式的設(shè)置,其實(shí)這種機(jī)制和最前面提到的內(nèi)存的鎖模式有點(diǎn)類似,一旦當(dāng)mode(存取模式)設(shè)置稱為可寫的時(shí)候,startWrite才能返回true,不僅僅如此,某個(gè)應(yīng)用程序在向共享內(nèi)存寫入數(shù)據(jù)的時(shí)候還會(huì)修改其存取模式,因?yàn)槿绻恍薷牡脑捑蜁?huì)導(dǎo)致其他應(yīng)用同樣針對該內(nèi)存是可寫的,這樣就使得共享內(nèi)存的實(shí)現(xiàn)變得混亂,而在停止寫操作stopWrite的時(shí)候,需要將mode設(shè)置稱為1,也就是上邊注釋段提到的釋放寫權(quán)限?!?/span>
      關(guān)于鎖的知識(shí)這里簡單做個(gè)補(bǔ)充【*:上邊代碼的這種模式可以理解為一種簡單的鎖模式】:一般情況下,計(jì)算機(jī)編程中會(huì)經(jīng)常遇到鎖模式,在整個(gè)鎖模式過程中可以將鎖分為兩類(這里只是輔助理解,不是嚴(yán)格的鎖分類)——共享鎖排他鎖(也稱為獨(dú)占鎖),鎖的定位是定位于針對所有與計(jì)算機(jī)有關(guān)的資源比如內(nèi)存、文件、存儲(chǔ)空間等,針對這些資源都可能出現(xiàn)鎖模式。在上邊堆和棧一節(jié)講到了Java對象鎖,其實(shí)不僅僅是對象,只要是計(jì)算機(jī)中會(huì)出現(xiàn)寫入和讀取共同操作的資源,都有可能出現(xiàn)鎖模式。
      共享鎖——當(dāng)應(yīng)用程序獲得了資源的共享鎖的時(shí)候,那么應(yīng)用程序就可以直接訪問該資源,資源的共享鎖可以被多個(gè)應(yīng)用程序拿到,在Java里面線程之間有時(shí)候也存在對象的共享鎖,但是有一個(gè)很明顯的特征,也就是內(nèi)存共享鎖只能讀取數(shù)據(jù),不能夠?qū)懭霐?shù)據(jù),不論是什么資源,當(dāng)應(yīng)用程序僅僅只能拿到該資源的共享鎖的時(shí)候,是不能夠針對該資源進(jìn)行寫操作的。
      獨(dú)占鎖——當(dāng)應(yīng)用程序獲得了資源的獨(dú)占鎖的時(shí)候,應(yīng)用程序訪問該資源在共享鎖上邊多了一個(gè)權(quán)限就是寫權(quán)限,針對資源本身而言,一個(gè)資源只有一把獨(dú)占鎖,也就是說一個(gè)資源只能同時(shí)被一個(gè)應(yīng)用或者一個(gè)執(zhí)行代碼程序允許寫操作,Java線程中的對象寫操作也是這個(gè)道理,若某個(gè)應(yīng)用拿到了獨(dú)占鎖的時(shí)候,不僅僅可以讀取資源里面的數(shù)據(jù),而且可以向該資源進(jìn)行數(shù)據(jù)寫操作。
      數(shù)據(jù)一致性——當(dāng)資源同時(shí)被應(yīng)用進(jìn)行讀寫訪問的時(shí)候,有可能會(huì)出現(xiàn)數(shù)據(jù)一致性問題,比如A應(yīng)用拿到了資源R1的獨(dú)占鎖,B應(yīng)用拿到了資源R1的共享鎖,A在針對R1進(jìn)行寫操作,而兩個(gè)應(yīng)用的操作——A的寫操作和B的讀操作出現(xiàn)了一個(gè)時(shí)間差,s1的時(shí)候B讀取了R1的資源,s2的時(shí)候A寫入了數(shù)據(jù)修改了R1的資源,s3的時(shí)候B又進(jìn)行了第二次讀,而兩次讀取相隔時(shí)間比較短暫而且初衷沒有考慮到A在B的讀取過程修改了資源,這種情況下針對鎖模式就需要考慮到數(shù)據(jù)一致性問題。獨(dú)占鎖的排他性在這里的意思是該鎖只能被一個(gè)應(yīng)用獲取,獲取過程只能由這個(gè)應(yīng)用寫入數(shù)據(jù)到資源內(nèi)部,除非它釋放該鎖,否則其他拿不到鎖的應(yīng)用是無法對資源進(jìn)行寫入操作的。
      按照上邊的思路去理解代碼里面實(shí)現(xiàn)共享內(nèi)存的過程就更加容易理解了。
      如果執(zhí)行寫操作的應(yīng)用異常中止,那么映像文件的共享內(nèi)存將不再能執(zhí)行寫操作。為了在應(yīng)用異常中止后,寫操作禁止標(biāo)志自動(dòng)消除,必須讓運(yùn)行的應(yīng)用獲知退出的應(yīng)用。在多線程應(yīng)用中,可以用同步方法獲得這樣的效果,但是在多進(jìn)程中,同步是不起作用的。方法可以采用的多種技巧,這里只是描述一可能的實(shí)現(xiàn):采用文件鎖的方式。寫共享內(nèi)存應(yīng)用在獲得對一個(gè)共享內(nèi)存寫權(quán)限的時(shí)候,除了判斷頭部信息的寫權(quán)限標(biāo)志外,還要判斷一個(gè)臨時(shí)的鎖文件是否可以得到,如果可以得到,則即使頭部信息的寫權(quán)限標(biāo)志為1(上述),也可以啟動(dòng)寫權(quán)限,其實(shí)這已經(jīng)表明寫權(quán)限獲得的應(yīng)用已經(jīng)異常退出,這段代碼如下:
    // 打開一個(gè)臨時(shí)文件,注意統(tǒng)一共享內(nèi)存,該文件名必須相同,可以在共享文件名后邊添加“.lock”后綴
    RandomAccessFile files = new RandomAccessFile("memory.lock","rw");
    // 獲取文件通道
    FileChannel lockFileChannel = files.getChannel();
    // 獲取文件的獨(dú)占鎖,該方法不產(chǎn)生任何阻塞直接返回
    FileLock fileLock = lockFileChannel.tryLock();
    // 如果為空表示已經(jīng)有應(yīng)用占有了
    if( fileLock == null ){
        // ...不可寫
    }else{
        // ...可以執(zhí)行寫操作
    }
      4)共享內(nèi)存的應(yīng)用:
      在Java中,共享內(nèi)存一般有兩種應(yīng)用:
      [1]永久對象配置——在java服務(wù)器應(yīng)用中,用戶可能會(huì)在運(yùn)行過程中配置一些參數(shù),而這些參數(shù)需要永久 有效,當(dāng)服務(wù)器應(yīng)用重新啟動(dòng)后,這些配置參數(shù)仍然可以對應(yīng)用起作用。這就可以用到該文 中的共享內(nèi)存。該共享內(nèi)存中保存了服務(wù)器的運(yùn)行參數(shù)和一些對象運(yùn)行特性??梢栽趹?yīng)用啟動(dòng)時(shí)讀入以啟用以前配置的參數(shù)。
      [2]查詢共享數(shù)據(jù)——個(gè)應(yīng)用(例 sys.java)是系統(tǒng)的服務(wù)進(jìn)程,其系統(tǒng)的運(yùn)行狀態(tài)記錄在共享內(nèi)存中,其中運(yùn)行狀態(tài)可能是不斷變化的。為了隨時(shí)了解系統(tǒng)的運(yùn)行狀態(tài),啟動(dòng)另一個(gè)應(yīng)用(例 mon.java),該應(yīng)用查詢該共享內(nèi)存,匯報(bào)系統(tǒng)的運(yùn)行狀態(tài)。
      v.小節(jié):
      提供本機(jī)內(nèi)存以及共享內(nèi)存的知識(shí),主要是為了讓讀者能夠更順利地理解JVM內(nèi)部內(nèi)存模型的物理原理,包括JVM如何和操作系統(tǒng)在內(nèi)存這個(gè)級(jí)別進(jìn)行交互,理解了這些內(nèi)容就讓讀者對Java內(nèi)存模型的認(rèn)識(shí)會(huì)更加深入,而且不容易遺忘。其實(shí)Java的內(nèi)存模型遠(yuǎn)不及我們想象中那么簡單,而且其結(jié)構(gòu)極端復(fù)雜,看過《Inside JVM》的朋友應(yīng)該就知道,結(jié)合JVM指令集去寫點(diǎn)小代碼測試.class文件的里層結(jié)構(gòu)也不失為一種好玩的學(xué)習(xí)方法。
      
    4.防止內(nèi)存泄漏
      Java中會(huì)有內(nèi)存泄漏,聽起來似乎是很不正常的,因?yàn)镴ava提供了垃圾回收器針對內(nèi)存進(jìn)行自動(dòng)回收,但是Java還是會(huì)出現(xiàn)內(nèi)存泄漏的。
      i.什么是Java中的內(nèi)存泄漏:
      在Java語言中,內(nèi)存泄漏就是存在一些被分配的對象,這些對象有兩個(gè)特點(diǎn):這些對象可達(dá),即在對象內(nèi)存的有向圖中存在通路可以與其相連;其次,這些對象是無用的,即程序以后不會(huì)再使用這些對象了。如果對象滿足這兩個(gè)條件,該對象就可以判定為Java中的內(nèi)存泄漏,這些對象不會(huì)被GC回收,然而它卻占用內(nèi)存,這就是Java語言中的內(nèi)存泄漏。Java中的內(nèi)存泄漏和C++中的內(nèi)存泄漏還存在一定的區(qū)別,在C++里面,內(nèi)存泄漏的范圍更大一些,有些對象被分配了內(nèi)存空間,但是卻不可達(dá),由于C++中沒有GC,這些內(nèi)存將會(huì)永遠(yuǎn)收不回來,在Java中這些不可達(dá)對象則是被GC負(fù)責(zé)回收的,因此程序員不需要考慮這一部分的內(nèi)存泄漏。二者的圖如下:
      因此按照上邊的分析,Java語言中也是存在內(nèi)存泄漏的,但是其內(nèi)存泄漏范圍比C++要小很多,因?yàn)镴ava里面有個(gè)特殊程序回收所有的不可達(dá)對象:垃圾回收器。對于程序員來說,GC基本是透明的,不可見的。雖然,我們只有幾個(gè)函數(shù)可以訪問GC,例如運(yùn)行GC的函數(shù)System.gc(),但是根據(jù)Java語言規(guī)范定義,該函數(shù)不保證JVM的垃圾收集器一定會(huì)執(zhí)行。因?yàn)椋煌腏VM實(shí)現(xiàn)者可能使用不同的算法管理GC。通常,GC的線程的優(yōu)先級(jí)別較低,JVM調(diào)用GC的策略也有很多種,有的是內(nèi)存使用到達(dá)一定程度時(shí),GC才開始工作,也有定時(shí)執(zhí)行的,有的是平緩執(zhí)行GC,有的是中斷式執(zhí)行GC。但通常來說,我們不需要關(guān)心這些。除非在一些特定的場合,GC的執(zhí)行影響應(yīng)用程序的性能,例如對于基于Web的實(shí)時(shí)系統(tǒng),如網(wǎng)絡(luò)游戲等,用戶不希望GC突然中斷應(yīng)用程序執(zhí)行而進(jìn)行垃圾回收,那么我們需要調(diào)整GC的參數(shù),讓GC能夠通過平緩的方式釋放內(nèi)存,例如將垃圾回收分解為一系列的小步驟執(zhí)行,Sun提供的HotSpot JVM就支持這一特性。
      舉個(gè)例子:
      ——[$]內(nèi)存泄漏的例子——
    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;
            }
        }
    }
      從上邊這個(gè)例子可以看到,循環(huán)申請了String對象,并且將申請的對象放入了一個(gè)Vector中,如果僅僅是釋放對象本身,因?yàn)閂ector仍然引用了該對象,所以這個(gè)對象對CG來說是不可回收的,因此如果對象加入到Vector后,還必須從Vector刪除才能夠回收,最簡單的方式是將Vector引用設(shè)置成null。實(shí)際上這些對象已經(jīng)沒有用了,但是還是被代碼里面的引用引用到了,這種情況GC拿它就沒有了任何辦法,這樣就可以導(dǎo)致了內(nèi)存泄漏。
      【*:Java語言因?yàn)樘峁┝死厥掌鳎绽碚f是不會(huì)出現(xiàn)內(nèi)存泄漏的,Java里面導(dǎo)致內(nèi)存泄漏的主要原因就是,先前申請了內(nèi)存空間而忘記了釋放。如果程序中存在對無用對象的引用,這些對象就會(huì)駐留在內(nèi)存中消耗內(nèi)存,因?yàn)闊o法讓GC判斷這些對象是否可達(dá)。如果存在對象的引用,這個(gè)對象就被定義為“有效的活動(dòng)狀態(tài)”,同時(shí)不會(huì)被釋放,要確定對象所占內(nèi)存被回收,必須要確認(rèn)該對象不再被使用。典型的做法就是把對象數(shù)據(jù)成員設(shè)置成為null或者中集合中移除,當(dāng)局部變量不需要的情況則不需要顯示聲明為null?!?/span>
      ii.常見的Java內(nèi)存泄漏
      1)全局集合:
      在大型應(yīng)用程序中存在各種各樣的全局?jǐn)?shù)據(jù)倉庫是很普遍的,比如一個(gè)JNDI樹或者一個(gè)Session table(會(huì)話表),在這些情況下,必須注意管理存儲(chǔ)庫的大小,必須有某種機(jī)制從存儲(chǔ)庫中移除不再需要的數(shù)據(jù)。
      [$]解決:
      [1]常用的解決方法是周期運(yùn)作清除作業(yè),該作業(yè)會(huì)驗(yàn)證倉庫中的數(shù)據(jù)然后清楚一切不需要的數(shù)據(jù)
      [2]另外一種方式是反向鏈接計(jì)數(shù),集合負(fù)責(zé)統(tǒng)計(jì)集合中每個(gè)入口的反向鏈接數(shù)據(jù),這要求反向鏈接告訴集合合適會(huì)退出入口,當(dāng)反向鏈接數(shù)目為零的時(shí)候,該元素就可以移除了。
      2)緩存:
      緩存一種用來快速查找已經(jīng)執(zhí)行過的操作結(jié)果的數(shù)據(jù)結(jié)構(gòu)。因此,如果一個(gè)操作執(zhí)行需要比較多的資源并會(huì)多次被使用,通常做法是把常用的輸入數(shù)據(jù)的操作結(jié)果進(jìn)行緩存,以便在下次調(diào)用該操作時(shí)使用緩存的數(shù)據(jù)。緩存通常都是以動(dòng)態(tài)方式實(shí)現(xiàn)的,如果緩存設(shè)置不正確而大量使用緩存的話則會(huì)出現(xiàn)內(nèi)存溢出的后果,因此需要將所使用的內(nèi)存容量與檢索數(shù)據(jù)的速度加以平衡。
      [$]解決:
      [1]常用的解決途徑是使用java.lang.ref.SoftReference類堅(jiān)持將對象放入緩存,這個(gè)方法可以保證當(dāng)虛擬機(jī)用完內(nèi)存或者需要更多堆的時(shí)候,可以釋放這些對象的引用。
      3)類加載器:
      Java類裝載器的使用為內(nèi)存泄漏提供了許多可乘之機(jī)。一般來說類裝載器都具有復(fù)雜結(jié)構(gòu),因?yàn)轭愌b載器不僅僅是只與"常規(guī)"對象引用有關(guān),同時(shí)也和對象內(nèi)部的引用有關(guān)。比如數(shù)據(jù)變量方法各種類。這意味著只要存在對數(shù)據(jù)變量,方法,各種類和對象的類裝載器,那么類裝載器將駐留在JVM中。既然類裝載器可以同很多的類關(guān)聯(lián),同時(shí)也可以和靜態(tài)數(shù)據(jù)變量關(guān)聯(lián),那么相當(dāng)多的內(nèi)存就可能發(fā)生泄漏。
      iii.Java引用摘錄自前邊的《Java引用總結(jié)》】
      Java中的對象引用主要有以下幾種類型:
      1)強(qiáng)可及對象(strongly reachable):
      可以通過強(qiáng)引用訪問的對象,一般來說,我們平時(shí)寫代碼的方式都是使用的強(qiáng)引用對象,比如下邊的代碼段:
      StringBuilder builder= new StringBuilder();
      上邊代碼部分引用obj這個(gè)引用將引用內(nèi)存堆中的一個(gè)對象,這種情況下,只要obj的引用存在,垃圾回收器就永遠(yuǎn)不會(huì)釋放該對象的存儲(chǔ)空間。這種對象我們又成為強(qiáng)引用(Strong references,這種強(qiáng)引用方式就是Java語言的原生的Java引用,我們幾乎每天編程的時(shí)候都用到。上邊代碼JVM存儲(chǔ)了一個(gè)StringBuilder類型的對象的強(qiáng)引用在變量builder呢。強(qiáng)引用和GC的交互是這樣的,如果一個(gè)對象通過強(qiáng)引用可達(dá)或者通過強(qiáng)引用鏈可達(dá)的話這種對象就成為強(qiáng)可及對象,這種情況下的對象垃圾回收器不予理睬。如果我們開發(fā)過程不需要垃圾回器回收該對象,就直接將該對象賦為強(qiáng)引用,也是普通的編程方法。
      2)軟可及對象(softly reachable):
      不通過強(qiáng)引用訪問的對象,即不是強(qiáng)可及對象,但是可以通過軟引用訪問的對象就成為軟可及對象,軟可及對象就需要使用類SoftReferencejava.lang.ref.SoftReference)。此種類型的引用主要用于內(nèi)存比較敏感的高速緩存,而且此種引用還是具有較強(qiáng)的引用功能,當(dāng)內(nèi)存不夠的時(shí)候GC會(huì)回收這類內(nèi)存,因此如果內(nèi)存充足的時(shí)候,這種引用通常不會(huì)被回收的。不僅僅如此,這種引用對象在JVM里面保證在拋出OutOfMemory異常之前,設(shè)置成為null。通俗地講,這種類型的引用保證在JVM內(nèi)存不足的時(shí)候全部被清除,但是有個(gè)關(guān)鍵在于:垃圾收集器在運(yùn)行時(shí)是否釋放軟可及對象是不確定的,而且使用垃圾回收算法并不能保證一次性尋找到所有的軟可及對象。當(dāng)垃圾回收器每次運(yùn)行的時(shí)候都可以隨意釋放不是強(qiáng)可及對象占用的內(nèi)存,如果垃圾回收器找到了軟可及對象過后,可能會(huì)進(jìn)行以下操作:
    • 將SoftReference對象的referent域設(shè)置成為null,從而使該對象不再引用heap對象。
    • SoftReference引用過的內(nèi)存堆上的對象一律被生命為finalizable。
    • 當(dāng)內(nèi)存堆上的對象finalize()方法被運(yùn)行而且該對象占用的內(nèi)存被釋放,SoftReference對象就會(huì)被添加到它的ReferenceQueue,前提條件是ReferenceQueue本身是存在的。
      既然Java里面存在這樣的對象,那么我們在編寫代碼的時(shí)候如何創(chuàng)建這樣的對象呢?創(chuàng)建步驟如下:
      先創(chuàng)建一個(gè)對象,并使用普通引用方式【強(qiáng)引用】,然后再創(chuàng)建一個(gè)SoftReference來引用該對象,最后將普通引用設(shè)置為null,通過這樣的方式,這個(gè)對象就僅僅保留了一個(gè)SoftReference引用,同時(shí)這種情況我們所創(chuàng)建的對象就是SoftReference對象。一般情況下,我們可以使用該引用來完成Cache功能,就是前邊說的用于高速緩存,保證最大限度使用內(nèi)存而不會(huì)引起內(nèi)存泄漏的情況。下邊的代碼段:
      public static void main(String args[])
      {
        //創(chuàng)建一個(gè)強(qiáng)可及對象
        A a = new A();
        //創(chuàng)建這個(gè)對象的軟引用SoftReference
        SoftReference sr = new SoftReference(a);
        //將強(qiáng)引用設(shè)置為空,以遍垃圾回收器回收強(qiáng)引用
        a = null;
        //下次使用該對象的操作
        if( sr != null ){
          a = (A)sr.get();
        }else{
          //這種情況就是由于內(nèi)存過低,已經(jīng)將軟引用釋放了,因此需要重新裝載一次
          a = new A();
          sr = new SoftReference(a);
        }
      }
      軟引用技術(shù)使得Java系統(tǒng)可以更好地管理內(nèi)存,保持系統(tǒng)穩(wěn)定,防止內(nèi)存泄漏,避免系統(tǒng)崩潰,因此在處理一些內(nèi)存占用大而且生命周期長使用不頻繁的對象可以使用該技術(shù)。
      3)弱可及對象(weakly reachable):
      不是強(qiáng)可及對象同樣也不是軟可及對象,僅僅通過弱引用WeakReferencejava.lang.ref.WeakReference)訪問的對象,這種對象的用途在于規(guī)范化映射(canonicalized mapping,對于生存周期相對比較長而且重新創(chuàng)建的時(shí)候開銷少的對象,弱引用也比較有用,和軟引用對象不同的是,垃圾回收器如果碰到了弱可及對象,將釋放WeakReference對象的內(nèi)存,但是垃圾回收器需要運(yùn)行很多次才能夠找到弱可及對象。弱引用對象在使用的時(shí)候,可以配合ReferenceQueue類使用,如果弱引用被回收,JVM就會(huì)把這個(gè)弱引用加入到相關(guān)的引用隊(duì)列中去。最簡單的弱引用方法如以下代碼:
      WeakReference weakWidget = new WeakReference(classA);
      在上邊代碼里面,當(dāng)我們使用weakWidget.get()來獲取classA的時(shí)候,由于弱引用本身是無法阻止垃圾回收的,所以我們也許會(huì)拿到一個(gè)null為返回。【*:這里提供一個(gè)小技巧,如果我們希望取得某個(gè)對象的信息,但是又不影響該對象的垃圾回收過程,我們就可以使用WeakReference來記住該對象,一般我們在開發(fā)調(diào)試器和優(yōu)化器的時(shí)候使用這個(gè)是很好的一個(gè)手段。】
      如果上邊的代碼部分,我們通過weakWidget.get()返回的是null就證明該對象已經(jīng)被垃圾回收器回收了,而這種情況下弱引用對象就失去了使用價(jià)值,GC就會(huì)定義為需要進(jìn)行清除工作。這種情況下弱引用無法引用任何對象,所以在JVM里面就成為了一個(gè)死引用,這就是為什么我們有時(shí)候需要通過ReferenceQueue類來配合使用的原因,使用了ReferenceQueue過后,就使得我們更加容易監(jiān)視該引用的對象,如果我們通過一ReferenceQueue類來構(gòu)造一個(gè)弱引用,當(dāng)弱引用的對象已經(jīng)被回收的時(shí)候,系統(tǒng)將自動(dòng)使用對象引用隊(duì)列來代替對象引用,而且我們可以通過ReferenceQueue類的運(yùn)行來決定是否真正要從垃圾回收器里面將該死引用(Dead Reference)清除
      弱引用代碼段:
      //創(chuàng)建普通引用對象
      MyObject object = new MyObject();
      //創(chuàng)建一個(gè)引用隊(duì)列
      ReferenceQueue rq = new ReferenceQueue();
      //使用引用隊(duì)列創(chuàng)建MyObject的弱引用
      WeakReference wr = new WeakReference(object,rq);
      這里提供兩個(gè)實(shí)在的場景來描述弱引用的相關(guān)用法:
      [1]你想給對象附加一些信息,于是你用一個(gè) Hashtable 把對象和附加信息關(guān)聯(lián)起來。你不停的把對象和附加信息放入 Hashtable 中,但是當(dāng)對象用完的時(shí)候,你不得不把對象再從 Hashtable 中移除,否則它占用的內(nèi)存變不會(huì)釋放。萬一你忘記了,那么沒有從 Hashtable 中移除的對象也可以算作是內(nèi)存泄漏。理想的狀況應(yīng)該是當(dāng)對象用完時(shí),Hashtable 中的對象會(huì)自動(dòng)被垃圾收集器回收,不然你就是在做垃圾回收的工作。
      [2]你想實(shí)現(xiàn)一個(gè)圖片緩存,因?yàn)榧虞d圖片的開銷比較大。你將圖片對象的引用放入這個(gè)緩存,以便以后能夠重新使用這個(gè)對象。但是你必須決定緩存中的哪些圖片不再需要了,從而將引用從緩存中移除。不管你使用什么管理緩存的算法,你實(shí)際上都在處理垃圾收集的工作,更簡單的辦法(除非你有特殊的需求,這也應(yīng)該是最好的辦法)是讓垃圾收集器來處理,由它來決定回收哪個(gè)對象。 
      當(dāng)Java回收器遇到了弱引用的時(shí)候有可能會(huì)執(zhí)行以下操作:
    • 將WeakReference對象的referent域設(shè)置成為null,從而使該對象不再引用heap對象。
    • WeakReference引用過的內(nèi)存堆上的對象一律被生命為finalizable。
    • 當(dāng)內(nèi)存堆上的對象finalize()方法被運(yùn)行而且該對象占用的內(nèi)存被釋放,WeakReference對象就會(huì)被添加到它的ReferenceQueue,前提條件是ReferenceQueue本身是存在的。
      4)清除:
      當(dāng)引用對象的referent域設(shè)置為null,并且引用類在內(nèi)存堆中引用的對象聲明為可結(jié)束的時(shí)候,該對象就可以清除,清除不做過多的講述
      5)虛可及對象(phantomly reachable):
      不是強(qiáng)可及對象,也不是軟可及對象,同樣不是弱可及對象,之所以把虛可及對象放到最后來講,主要也是因?yàn)樗奶厥庑裕袝r(shí)候我們又稱之為“幽靈對象”,已經(jīng)結(jié)束的,可以通過虛引用來訪問該對象。我們使用類PhantomReferencejava.lang.ref.PhantomReference)來訪問,這個(gè)類只能用于跟蹤被引用對象進(jìn)行的收集,同樣的,可以用于執(zhí)行per-mortern清除操作。PhantomReference必須與ReferenceQueue類一起使用。需要使用ReferenceQueue是因?yàn)樗軌虺洚?dāng)通知機(jī)制,當(dāng)垃圾收集器確定了某個(gè)對象是虛可及對象的時(shí)候,PhantomReference對象就被放在了它的ReferenceQueue上,這就是一個(gè)通知,表明PhantomReference引用的對象已經(jīng)結(jié)束,可以收集了,一般情況下我們剛好在對象內(nèi)存在回收之前采取該行為。這種引用不同于弱引用和軟引用,這種方式通過get()獲取到的對象總是返回null,僅僅當(dāng)這些對象在ReferenceQueue隊(duì)列里面的時(shí)候,我們可以知道它所引用的哪些對對象是死引用(Dead Reference)。而這種引用和弱引用的區(qū)別在于:
      弱引用(WeakReference是在對象不可達(dá)的時(shí)候盡快進(jìn)入ReferenceQueue隊(duì)列的,在finalization方法執(zhí)行和垃圾回收之前是確實(shí)會(huì)發(fā)生的,理論上這類對象是不正確的對象,但是WeakReference對象可以繼續(xù)保持Dead狀態(tài),
      虛引用(PhantomReference是在對象確實(shí)已經(jīng)從物理內(nèi)存中移除過后才進(jìn)入的ReferenceQueue隊(duì)列,而且get()方法會(huì)一直返回null
      當(dāng)垃圾回收器遇到了虛引用的時(shí)候?qū)⒂锌赡軋?zhí)行以下操作:
    • PhantomReference引用過的heap對象聲明為finalizable;
    • 虛引用在堆對象釋放之前就添加到了它的ReferenceQueue里面,這種情況使得我們可以在堆對象被回收之前采取操作*:再次提醒,PhantomReference對象必須經(jīng)過關(guān)聯(lián)的ReferenceQueue來創(chuàng)建,就是說必須ReferenceQueue類配合操作
      看似沒有用處的虛引用,有什么用途呢?
    • 首先,我們可以通過虛引用知道對象究竟什么時(shí)候真正從內(nèi)存里面移除的,而且這也是唯一的途徑。
    • 虛引用避過了finalize()方法,因?yàn)閷τ诖朔椒ǖ膱?zhí)行而言,虛引用真正引用到的對象是異常對象,若在該方法內(nèi)要使用對象只能重建。一般情況垃圾回收器會(huì)輪詢兩次,一次標(biāo)記為finalization,第二次進(jìn)行真實(shí)的回收,而往往標(biāo)記工作不能實(shí)時(shí)進(jìn)行,或者垃圾回收其會(huì)等待一個(gè)對象去標(biāo)記finalization。這種情況很有可能引起MemoryOut,而使用虛引用這種情況就會(huì)完全避免。因?yàn)樘撘迷谝脤ο蟮倪^程不會(huì)去使得這個(gè)對象由Dead復(fù)活,而且這種對象是可以在回收周期進(jìn)行回收的。
      在JVM內(nèi)部,虛引用比起使用finalize()方法更加安全一點(diǎn)而且更加有效。而finaliaze()方法回收在虛擬機(jī)里面實(shí)現(xiàn)起來相對簡單,而且也可以處理大部分工作,所以我們?nèi)匀皇褂眠@種方式來進(jìn)行對象回收的掃尾操作,但是有了虛引用過后我們可以選擇是否手動(dòng)操作該對象使得程序更加高效完美。
      iv.防止內(nèi)存泄漏[來自IBM開發(fā)中心]:
      1)使用軟引用阻止泄漏:
      [1]在Java語言中有一種形式的內(nèi)存泄漏稱為對象游離(Object Loitering):
      ——[$]對象游離——
    // 注意,這段代碼屬于概念說明代碼,實(shí)際應(yīng)用中不要模仿
    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);
            // 計(jì)算該文件的值然后返回該對象
        }
    }
      上邊的代碼是類LeakyChecksum用來說明對象游離的概念,里面有一個(gè)getFileChecksum()方法用來計(jì)算文件內(nèi)容校驗(yàn)和,getFileCheckSum方法將文件內(nèi)容讀取到緩沖區(qū)中計(jì)算校驗(yàn)和,更加直觀的實(shí)現(xiàn)就是簡單地將緩沖區(qū)作為getFileChecksum中的本地變量分配,但是上邊這個(gè)版本比這種版本更加“聰明”,不是將緩沖區(qū)緩沖在實(shí)例中字段中減少內(nèi)存churn。該“優(yōu)化”通常不帶來預(yù)期的好處,對象分配比很多人期望的更加便宜。(還要注意,將緩沖區(qū)從本地變量提升到實(shí)例變量,使得類若不帶有附加的同步,就不再是線程安全的了。直觀的實(shí)現(xiàn)不需要將 getFileChecksum() 聲明為 synchronized,并且會(huì)在同時(shí)調(diào)用時(shí)提供更好的可伸縮性。)
      這個(gè)類存在很多的問題,但是我們著重來看內(nèi)存泄漏。緩存緩沖區(qū)的決定很可能是根據(jù)這樣的假設(shè)得出的,即該類將在一個(gè)程序中被調(diào)用許多次,因此它應(yīng)該更加有效,以重用緩沖區(qū)而不是重新分配它。但是結(jié)果是,緩沖區(qū)永遠(yuǎn)不會(huì)被釋放,因?yàn)樗鼘Τ绦騺碚f總是可及的(除非LeakyChecksum對象被垃圾收集了)。更壞的是,它可以增長,卻不可以縮小,所以 LeakyChecksum 將永久保持一個(gè)與所處理的最大文件一樣大小的緩沖區(qū)。退一萬步說,這也會(huì)給垃圾收集器帶來壓力,并且要求更頻繁的收集;為計(jì)算未來的校驗(yàn)和而保持一個(gè)大型緩沖區(qū)并不是可用內(nèi)存的最有效利用。LeakyChecksum 中問題的原因是,緩沖區(qū)對于 getFileChecksum() 操作來說邏輯上是本地的,但是它的生命周期已經(jīng)被人為延長了,因?yàn)閷⑺嵘搅藢?shí)例字段。因此,該類必須自己管理緩沖區(qū)的生命周期,而不是讓 JVM 來管理。
      這里可以提供一種策略就是使用Java里面的軟引用:
      弱引用如何可以給應(yīng)用程序提供當(dāng)對象被程序使用時(shí)另一種到達(dá)該對象的方法,但是不會(huì)延長對象的生命周期。Reference 的另一個(gè)子類——軟引用——可滿足一個(gè)不同卻相關(guān)的目的。其中弱引用允許應(yīng)用程序創(chuàng)建不妨礙垃圾收集的引用,軟引用允許應(yīng)用程序通過將一些對象指定為 “expendable” 而利用垃圾收集器的幫助。盡管垃圾收集器在找出哪些內(nèi)存在由應(yīng)用程序使用哪些沒在使用方面做得很好,但是確定可用內(nèi)存的最適當(dāng)使用還是取決于應(yīng)用程序。如果應(yīng)用程序做出了不好的決定,使得對象被保持,那么性能會(huì)受到影響,因?yàn)槔占鞅仨毟有燎诘毓ぷ鳎苑乐箲?yīng)用程序消耗掉所有內(nèi)存。高速緩存是一種常見的性能優(yōu)化,允許應(yīng)用程序重用以前的計(jì)算結(jié)果,而不是重新進(jìn)行計(jì)算。高速緩存是 CPU 利用和內(nèi)存使用之間的一種折衷,這種折衷理想的平衡狀態(tài)取決于有多少內(nèi)存可用。若高速緩存太少,則所要求的性能優(yōu)勢無法達(dá)到;若太多,則性能會(huì)受到影響,因?yàn)樘嗟膬?nèi)存被用于高速緩存上,導(dǎo)致其他用途沒有足夠的可用內(nèi)存。因?yàn)槔占鞅葢?yīng)用程序更適合決定內(nèi)存需求,所以應(yīng)該利用垃圾收集器在做這些決定方面的幫助,這就是件引用所要做的。如果一個(gè)對象惟一剩下的引用是弱引用或軟引用,那么該對象是軟可及的(softly reachable。垃圾收集器并不像其收集弱可及的對象一樣盡量地收集軟可及的對象,相反,它只在真正 “需要” 內(nèi)存時(shí)才收集軟可及的對象。軟引用對于垃圾收集器來說是這樣一種方式,即 “只要內(nèi)存不太緊張,我就會(huì)保留該對象。但是如果內(nèi)存變得真正緊張了,我就會(huì)去收集并處理這個(gè)對象。” 垃圾收集器在可以拋出OutOfMemoryError 之前需要清除所有的軟引用。通過使用一個(gè)軟引用來管理高速緩存的緩沖區(qū),可以解決 LeakyChecksum中的問題,如上邊代碼所示?,F(xiàn)在,只要不是特別需要內(nèi)存,緩沖區(qū)就會(huì)被保留,但是在需要時(shí),也可被垃圾收集器回收:
      ——[$]使用軟引用修復(fù)上邊代碼段——
    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);
        }
    }
      一種廉價(jià)緩存:
      CachingChecksum使用一個(gè)軟引用來緩存單個(gè)對象,并讓 JVM 處理從緩存中取走對象時(shí)的細(xì)節(jié)。類似地,軟引用也經(jīng)常用于 GUI 應(yīng)用程序中,用于緩存位圖圖形。是否可使用軟引用的關(guān)鍵在于,應(yīng)用程序是否可從大量緩存的數(shù)據(jù)恢復(fù)。如果需要緩存不止一個(gè)對象,您可以使用一個(gè) Map,但是可以選擇如何使用軟引用。您可以將緩存作為 Map<K, SoftReference<V>> 或SoftReference<Map<K,V>> 管理。后一種選項(xiàng)通常更好一些,因?yàn)樗o垃圾收集器帶來的工作更少,并且允許在特別需要內(nèi)存時(shí)以較少的工作回收整個(gè)緩存。弱引用有時(shí)會(huì)錯(cuò)誤地用于取代軟引用,用于構(gòu)建緩存,但是這會(huì)導(dǎo)致差的緩存性能。在實(shí)踐中,弱引用將在對象變得弱可及之后被很快地清除掉——通常是在緩存的對象再次用到之前——因?yàn)樾〉睦占\(yùn)行得很頻繁。對于在性能上非常依賴高速緩存的應(yīng)用程序來說,軟引用是一個(gè)不管用的手段,它確實(shí)不能取代能夠提供靈活終止期、復(fù)制事務(wù)型高速緩存的復(fù)雜的高速緩存框架。但是作為一種 “廉價(jià)(cheap and dirty” 的高速緩存機(jī)制,它對于降低價(jià)格是很有吸引力的。正如弱引用一樣,軟引用也可創(chuàng)建為具有一個(gè)相關(guān)的引用隊(duì)列,引用在被垃圾收集器清除時(shí)進(jìn)入隊(duì)列。引用隊(duì)列對于軟引用來說,沒有對弱引用那么有用,但是它們可以用于發(fā)出管理警報(bào),說明應(yīng)用程序開始缺少內(nèi)存。
      2)垃圾回收對引用的處理:
      弱引用和軟引用都擴(kuò)展了抽象的 Reference 類虛引用(phantom references),引用對象被垃圾收集器特殊地看待。垃圾收集器在跟蹤堆期間遇到一個(gè) Reference 時(shí),不會(huì)標(biāo)記或跟蹤該引用對象,而是在已知活躍的 Reference 對象的隊(duì)列上放置一個(gè) Reference。在跟蹤之后,垃圾收集器就識(shí)別軟可及的對象——這些對象上除了軟引用外,沒有任何強(qiáng)引用。垃圾收集器然后根據(jù)當(dāng)前收集所回收的內(nèi)存總量和其他策略考慮因素,判斷軟引用此時(shí)是否需要被清除。將被清除的軟引用如果具有相應(yīng)的引用隊(duì)列,就會(huì)進(jìn)入隊(duì)列。其余的軟可及對象(沒有清除的對象)然后被看作一個(gè)根集(root set),堆跟蹤繼續(xù)使用這些新的根,以便通過活躍的軟引用而可及的對象能夠被標(biāo)記處理軟引用之后,弱可及對象的集合被識(shí)別 —— 這樣的對象上不存在強(qiáng)引用或軟引用。這些對象被清除和加入隊(duì)列。所有 Reference 類型在加入隊(duì)列之前被清除,所以處理事后檢查(post-mortem)清除的線程永遠(yuǎn)不會(huì)具有 referent 對象的訪問權(quán),而只具有Reference 對象的訪問權(quán)。因此,當(dāng) References 與引用隊(duì)列一起使用時(shí),通常需要細(xì)分適當(dāng)?shù)囊妙愋?,并將它直接用于您的設(shè)計(jì)中(與 WeakHashMap 一樣,它的 Map.Entry 擴(kuò)展了 WeakReference)或者存儲(chǔ)對需要清除的實(shí)體的引用。
      3)使用弱引用堵住內(nèi)存泄漏:
      [1]全局Map造成的內(nèi)存泄漏:
      無意識(shí)對象保留最常見的原因是使用 Map 將元數(shù)據(jù)與臨時(shí)對象(transient object相關(guān)聯(lián)。假定一個(gè)對象具有中等生命周期,比分配它的那個(gè)方法調(diào)用的生命周期長,但是比應(yīng)用程序的生命周期短,如客戶機(jī)的套接字連接。需要將一些元數(shù)據(jù)與這個(gè)套接字關(guān)聯(lián),如生成連接的用戶的標(biāo)識(shí)。在創(chuàng)建 Socket 時(shí)是不知道這些信息的,并且不能將數(shù)據(jù)添加到 Socket 對象上,因?yàn)椴荒芸刂?nbsp;Socket 類或者它的子類。這時(shí),典型的方法就是在一個(gè)全局 Map 中存儲(chǔ)這些信息:
    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);
      這種方法的問題是元數(shù)據(jù)的生命周期需要與套接字的生命周期掛鉤,但是除非準(zhǔn)確地知道什么時(shí)候程序不再需要這個(gè)套接字,并記住從 Map 中刪除相應(yīng)的映射,否則,Socket 和 User 對象將會(huì)永遠(yuǎn)留在 Map 中,遠(yuǎn)遠(yuǎn)超過響應(yīng)了請求和關(guān)閉套接字的時(shí)間。這會(huì)阻止 Socket 和User 對象被垃圾收集,即使應(yīng)用程序不會(huì)再使用它們。這些對象留下來不受控制,很容易造成程序在長時(shí)間運(yùn)行后內(nèi)存爆滿。除了最簡單的情況,在幾乎所有情況下找出什么時(shí)候 Socket 不再被程序使用是一件很煩人和容易出錯(cuò)的任務(wù),需要人工對內(nèi)存進(jìn)行管理。
      [2]弱引用內(nèi)存泄漏代碼:
      程序有內(nèi)存泄漏的第一個(gè)跡象通常是它拋出一個(gè) OutOfMemoryError,或者因?yàn)轭l繁的垃圾收集而表現(xiàn)出糟糕的性能。幸運(yùn)的是,垃圾收集可以提供能夠用來診斷內(nèi)存泄漏的大量信息。如果以 -verbose:gc 或者 -Xloggc 選項(xiàng)調(diào)用 JVM,那么每次 GC 運(yùn)行時(shí)在控制臺(tái)上或者日志文件中會(huì)打印出一個(gè)診斷信息,包括它所花費(fèi)的時(shí)間、當(dāng)前堆使用情況以及恢復(fù)了多少內(nèi)存。記錄 GC 使用情況并不具有干擾性,因此如果需要分析內(nèi)存問題或者調(diào)優(yōu)垃圾收集器,在生產(chǎn)環(huán)境中默認(rèn)啟用 GC 日志是值得的。有工具可以利用 GC 日志輸出并以圖形方式將它顯示出來,JTune 就是這樣的一種工具。觀察 GC 之后堆大小的圖,可以看到程序內(nèi)存使用的趨勢。對于大多數(shù)程序來說,可以將內(nèi)存使用分為兩部分:baseline 使用和 current load 使用。對于服務(wù)器應(yīng)用程序,baseline 使用就是應(yīng)用程序在沒有任何負(fù)荷、但是已經(jīng)準(zhǔn)備好接受請求時(shí)的內(nèi)存使用,current load 使用是在處理請求過程中使用的、但是在請求處理完成后會(huì)釋放的內(nèi)存。只要負(fù)荷大體上是恒定的,應(yīng)用程序通常會(huì)很快達(dá)到一個(gè)穩(wěn)定的內(nèi)存使用水平。如果在應(yīng)用程序已經(jīng)完成了其初始化并且負(fù)荷沒有增加的情況下,內(nèi)存使用持續(xù)增加,那么程序就可能在處理前面的請求時(shí)保留了生成的對象。
    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]使用弱引用堵住內(nèi)存泄漏:
      SocketManager 的問題是 Socket-User 映射的生命周期應(yīng)當(dāng)與 Socket 的生命周期相匹配,但是語言沒有提供任何容易的方法實(shí)施這項(xiàng)規(guī)則。這使得程序不得不使用人工內(nèi)存管理的老技術(shù)。幸運(yùn)的是,從 JDK 1.2 開始,垃圾收集器提供了一種聲明這種對象生命周期依賴性的方法,這樣垃圾收集器就可以幫助我們防止這種內(nèi)存泄漏——利用弱引用。弱引用是對一個(gè)對象(稱為 referent的引用的持有者。使用弱引用后,可以維持對 referent 的引用,而不會(huì)阻止它被垃圾收集。當(dāng)垃圾收集器跟蹤堆的時(shí)候,如果對一個(gè)對象的引用只有弱引用,那么這個(gè) referent 就會(huì)成為垃圾收集的候選對象,就像沒有任何剩余的引用一樣,而且所有剩余的弱引用都被清除。(只有弱引用的對象稱為弱可及(weakly reachableWeakReference 的 referent 是在構(gòu)造時(shí)設(shè)置的,在沒有被清除之前,可以用 get() 獲取它的值。如果弱引用被清除了(不管是 referent 已經(jīng)被垃圾收集了,還是有人調(diào)用了 WeakReference.clear(),get() 會(huì)返回 null。相應(yīng)地,在使用其結(jié)果之前,應(yīng)當(dāng)總是檢查get() 是否返回一個(gè)非 null 值,因?yàn)?referent 最終總是會(huì)被垃圾收集的。用一個(gè)普通的(強(qiáng))引用拷貝一個(gè)對象引用時(shí),限制 referent 的生命周期至少與被拷貝的引用的生命周期一樣長。如果不小心,那么它可能就與程序的生命周期一樣——如果將一個(gè)對象放入一個(gè)全局集合中的話。另一方面,在創(chuàng)建對一個(gè)對象的弱引用時(shí),完全沒有擴(kuò)展 referent 的生命周期,只是在對象仍然存活的時(shí)候,保持另一種到達(dá)它的方法。弱引用對于構(gòu)造弱集合最有用,如那些在應(yīng)用程序的其余部分使用對象期間存儲(chǔ)關(guān)于這些對象的元數(shù)據(jù)的集合——這就是 SocketManager 類所要做的工作。因?yàn)檫@是弱引用最常見的用法,WeakHashMap 也被添加到 JDK 1.2 的類庫中,它對鍵(而不是對值)使用弱引用。如果在一個(gè)普通 HashMap 中用一個(gè)對象作為鍵,那么這個(gè)對象在映射從 Map 中刪除之前不能被回收,WeakHashMap 使您可以用一個(gè)對象作為 Map 鍵,同時(shí)不會(huì)阻止這個(gè)對象被垃圾收集。下邊的代碼給出了 WeakHashMap 的 get() 方法的一種可能實(shí)現(xiàn),它展示了弱引用的使用:
    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;
        }
    }
      調(diào)用 WeakReference.get() 時(shí),它返回一個(gè)對 referent 的強(qiáng)引用如果它仍然存活的話),因此不需要擔(dān)心映射在 while 循環(huán)體中消失,因?yàn)閺?qiáng)引用會(huì)防止它被垃圾收集。WeakHashMap 的實(shí)現(xiàn)展示了弱引用的一種常見用法——一些內(nèi)部對象擴(kuò)展 WeakReference。其原因在下面一節(jié)討論引用隊(duì)列時(shí)會(huì)得到解釋。在向 WeakHashMap 中添加映射時(shí),請記住映射可能會(huì)在以后“脫離”,因?yàn)殒I被垃圾收集了。在這種情況下,get() 返回 null,這使得測試 get() 的返回值是否為 null 變得比平時(shí)更重要了。
      [4]使用WeakHashMap堵住泄漏
      在 SocketManager 中防止泄漏很容易,只要用 WeakHashMap 代替 HashMap 就行了,如下邊代碼所示。(如果 SocketManager 需要線程安全,那么可以用 Collections.synchronizedMap() 包裝 WeakHashMap。當(dāng)映射的生命周期必須與鍵的生命周期聯(lián)系在一起時(shí),可以使用這種方法。不過,應(yīng)當(dāng)小心不濫用這種技術(shù),大多數(shù)時(shí)候還是應(yīng)當(dāng)使用普通的 HashMap 作為 Map 的實(shí)現(xiàn)。
    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);
        }
    }
      引用隊(duì)列:
      WeakHashMap 用弱引用承載映射鍵,這使得應(yīng)用程序不再使用鍵對象時(shí)它們可以被垃圾收集,get() 實(shí)現(xiàn)可以根據(jù) WeakReference.get() 是否返回 null 來區(qū)分死的映射和活的映射。但是這只是防止 Map 的內(nèi)存消耗在應(yīng)用程序的生命周期中不斷增加所需要做的工作的一半,還需要做一些工作以便在鍵對象被收集后從 Map 中刪除死項(xiàng)。否則,Map 會(huì)充滿對應(yīng)于死鍵的項(xiàng)。雖然這對于應(yīng)用程序是不可見的,但是它仍然會(huì)造成應(yīng)用程序耗盡內(nèi)存,因?yàn)榧词规I被收集了,Map.Entry 和值對象也不會(huì)被收集??梢酝ㄟ^周期性地掃描 Map,對每一個(gè)弱引用調(diào)用 get(),并在 get() 返回 null 時(shí)刪除那個(gè)映射而消除死映射。但是如果 Map 有許多活的項(xiàng),那么這種方法的效率很低。如果有一種方法可以在弱引用的 referent 被垃圾收集時(shí)發(fā)出通知就好了,這就是引用隊(duì)列的作用。引用隊(duì)列是垃圾收集器向應(yīng)用程序返回關(guān)于對象生命周期的信息的主要方法。弱引用有兩個(gè)構(gòu)造函數(shù):一個(gè)只取 referent 作為參數(shù),另一個(gè)還取引用隊(duì)列作為參數(shù)。如果用關(guān)聯(lián)的引用隊(duì)列創(chuàng)建弱引用,在 referent 成為 GC 候選對象時(shí),這個(gè)引用對象(不是referent)就在引用清除后加入 到引用隊(duì)列中。之后,應(yīng)用程序從引用隊(duì)列提取引用并了解到它的 referent 已被收集,因此可以進(jìn)行相應(yīng)的清理活動(dòng),如去掉已不在弱集合中的對象的項(xiàng)。(引用隊(duì)列提供了與 BlockingQueue 同樣的出列模式 ——polled、timed blocking 和 untimed blocking。)WeakHashMap 有一個(gè)名為 expungeStaleEntries() 的私有方法,大多數(shù) Map 操作中會(huì)調(diào)用它,它去掉引用隊(duì)列中所有失效的引用,并刪除關(guān)聯(lián)的映射。
      4)關(guān)于Java中引用思考:
      先觀察一個(gè)列表:
    級(jí)別 回收時(shí)間 用途 生存時(shí)間
    強(qiáng)引用 從來不會(huì)被回收 對象的一般狀態(tài) JVM停止運(yùn)行時(shí)終止
    軟引用 在內(nèi)存不足時(shí) 在客戶端移除對象引用過后,除非再次激活,否則就放在內(nèi)存敏感的緩存中 內(nèi)存不足時(shí)終止
    弱引用 在垃圾回收時(shí),也就是客戶端已經(jīng)移除了強(qiáng)引用,但是這種情況下內(nèi)存還是客戶端引用可達(dá)的 阻止自動(dòng)刪除不需要用的對象 GC運(yùn)行后終止
    虛引用[幽靈引用] 對象死亡之前,就是進(jìn)行finalize()方法調(diào)用附近 特殊的清除過程 不定,當(dāng)finalize()函數(shù)運(yùn)行過后再回收,有可能之前就已經(jīng)被回收了。
      可以這樣理解:
      SoftReference:假定垃圾回收器確定在某一時(shí)間點(diǎn)某個(gè)對象是軟可到達(dá)對象。這時(shí),它可以選擇自動(dòng)清除針對該對象的所有軟引用,以及通過強(qiáng)引用鏈,從其可以到達(dá)該對象的針對任何其他軟可到達(dá)對象的所有軟引用。在同一時(shí)間或晚些時(shí)候,它會(huì)將那些已經(jīng)向引用隊(duì)列注冊的新清除的軟引用加入隊(duì)列。 軟可到達(dá)對象的所有軟引用都要保證在虛擬機(jī)拋出 OutOfMemoryError 之前已經(jīng)被清除。否則,清除軟引用的時(shí)間或者清除不同對象的一組此類引用的順序?qū)⒉皇苋魏渭s束。然而,虛擬機(jī)實(shí)現(xiàn)不鼓勵(lì)清除最近訪問或使用過的軟引用。 此類的直接實(shí)例可用于實(shí)現(xiàn)簡單緩存;該類或其派生的子類還可用于更大型的數(shù)據(jù)結(jié)構(gòu),以實(shí)現(xiàn)更復(fù)雜的緩存。只要軟引用的指示對象是強(qiáng)可到達(dá)對象,即正在實(shí)際使用的對象,就不會(huì)清除軟引用。例如,通過保持最近使用的項(xiàng)的強(qiáng)指示對象,并由垃圾回收器決定是否放棄剩余的項(xiàng),復(fù)雜的緩存可以防止放棄最近使用的項(xiàng)。一般來說,WeakReference我們用來防止內(nèi)存泄漏,保證內(nèi)存對象被VM回收。
      WeakReference:弱引用對象,它們并不禁止其指示對象變得可終結(jié),并被終結(jié),然后被回收。弱引用最常用于實(shí)現(xiàn)規(guī)范化的映射。假定垃圾回收器確定在某一時(shí)間點(diǎn)上某個(gè)對象是弱可到達(dá)對象。這時(shí),它將自動(dòng)清除針對此對象的所有弱引用,以及通過強(qiáng)引用鏈和軟引用,可以從其到達(dá)該對象的針對任何其他弱可到達(dá)對象的所有弱引用。同時(shí)它將聲明所有以前的弱可到達(dá)對象為可終結(jié)的。在同一時(shí)間或晚些時(shí)候,它將那些已經(jīng)向引用隊(duì)列注冊的新清除的弱引用加入隊(duì)列。 SoftReference多用作來實(shí)現(xiàn)cache機(jī)制,保證cache的有效性。
      PhantomReference:虛引用對象,在回收器確定其指示對象可另外回收之后,被加入隊(duì)列。虛引用最常見的用法是以某種可能比使用 Java 終結(jié)機(jī)制更靈活的方式來指派 pre-mortem 清除操作。如果垃圾回收器確定在某一特定時(shí)間點(diǎn)上虛引用的指示對象是虛可到達(dá)對象,那么在那時(shí)或者在以后的某一時(shí)間,它會(huì)將該引用加入隊(duì)列。為了確保可回收的對象仍然保持原狀,虛引用的指示對象不能被檢索:虛引用的 get 方法總是返回 null。與軟引用和弱引用不同,虛引用在加入隊(duì)列時(shí)并沒有通過垃圾回收器自動(dòng)清除。通過虛引用可到達(dá)的對象將仍然保持原狀,直到所有這類引用都被清除,或者它們都變得不可到達(dá)。
      以下是不確定概念
      【*:Java引用的深入部分一直都是討論得比較多的話題,上邊大部分為摘錄整理,這里再談?wù)勎覀€(gè)人的一些看法。從整個(gè)JVM框架結(jié)構(gòu)來看,Java的引用垃圾回收器形成了針對Java內(nèi)存堆的一個(gè)對象的“閉包管理集”,其中在基本代碼里面常用的就是強(qiáng)引用,強(qiáng)引用主要使用目的是就是編程的正常邏輯,這是所有的開發(fā)人員最容易理解的,而弱引用和軟引用的作用是比較耐人尋味的。按照引用強(qiáng)弱,其排序可以為:強(qiáng)引用——軟引用——弱引用——虛引用,為什么這樣寫呢,實(shí)際上針對垃圾回收器而言,強(qiáng)引用是它絕對不會(huì)隨便去動(dòng)的區(qū)域,因?yàn)樵趦?nèi)存堆里面的對象,只有當(dāng)前對象不是強(qiáng)引用的時(shí)候,該對象才會(huì)進(jìn)入垃圾回收器目標(biāo)區(qū)域
      軟引用又可以理解為“內(nèi)存應(yīng)急引用”,也就是說它和GC是完整地配合操作的,為了防止內(nèi)存泄漏,當(dāng)GC在回收過程出現(xiàn)內(nèi)存不足的時(shí)候,軟引用會(huì)被優(yōu)先回收,從垃圾回收算法上講,軟引用在設(shè)計(jì)的時(shí)候是很容易被垃圾回收器發(fā)現(xiàn)的。為什么軟引用是處理告訴緩存的優(yōu)先選擇的,主要有兩個(gè)原因:第一,它對內(nèi)存非常敏感,從抽象意義上講,我們甚至可以任何它和內(nèi)存的變化緊緊綁定到一起操作的,因?yàn)閮?nèi)存一旦不足的時(shí)候,它會(huì)優(yōu)先向垃圾回收器報(bào)警以提示內(nèi)存不足;第二,它會(huì)盡量保證系統(tǒng)在OutOfMemoryError之前將對象直接設(shè)置成為不可達(dá),以保證不會(huì)出現(xiàn)內(nèi)存溢出的情況;所以使用軟引用來處理Java引用里面的高速緩存是很不錯(cuò)的選擇。其實(shí)軟引用不僅僅和內(nèi)存敏感,實(shí)際上和垃圾回收器的交互也是敏感的,這點(diǎn)可以這樣理解,因?yàn)楫?dāng)內(nèi)存不足的時(shí)候,軟引用會(huì)報(bào)警,而這種報(bào)警會(huì)提示垃圾回收器針對目前的一些內(nèi)存進(jìn)行清除操作,而在有軟引用存在的內(nèi)存堆里面,垃圾回收器會(huì)第一時(shí)間反應(yīng),否則就會(huì)MemoryOut了。按照我們正常的思維來考慮,垃圾回收器針對我們調(diào)用System.gc()的時(shí)候,是不會(huì)輕易理睬的,因?yàn)閮H僅是收到了來自強(qiáng)引用層代碼的請求,至于它是否回收還得看JVM內(nèi)部環(huán)境的條件是否滿足,但是如果是軟引用的方式去申請垃圾回收器會(huì)優(yōu)先反應(yīng),只是我們在開發(fā)過程不能控制軟引用對垃圾回收器發(fā)送垃圾回收申請,而JVM規(guī)范里面也指出了軟引用不會(huì)輕易發(fā)送申請到垃圾回收器。這里還需要解釋的一點(diǎn)的是軟引用發(fā)送申請不是說軟引用像我們調(diào)用System.gc()這樣直接申請垃圾回收,而是說軟引用會(huì)設(shè)置對象引用為null,而垃圾回收器針對該引用的這種做法也會(huì)優(yōu)先響應(yīng),我們可以理解為是軟引用對象在向垃圾回收器發(fā)送申請。反應(yīng)快并不代表垃圾回收器會(huì)實(shí)時(shí)反應(yīng),還是會(huì)在尋找軟引用引用到的對象的時(shí)候遵循一定的回收規(guī)則,反應(yīng)快在這里的解釋是相對強(qiáng)引用設(shè)置對象為null,當(dāng)軟引用設(shè)置對象為null的時(shí)候,該對象的被收集的優(yōu)先級(jí)比較
      弱引用是一種比軟引用相對復(fù)雜的引用,其實(shí)弱引用和軟引用都是Java程序可以控制的,也就是說可以通過代碼直接使得引用針對弱可及對象以及軟可及對象是可引用的,軟引用和弱引用引用的對象實(shí)際上通過一定的代碼操作是可重新激活的,只是一般不會(huì)做這樣的操作,這樣的用法違背了最初的設(shè)計(jì)。弱引用和軟引用在垃圾回收器的目標(biāo)范圍有一點(diǎn)點(diǎn)不同的就是,使用垃圾回收算法是很難找到弱引用的,也就是說弱引用用來監(jiān)控垃圾回收的整個(gè)流程也是一種很好的選擇,它不會(huì)影響垃圾回收的正常流程,這樣就可以規(guī)范化整個(gè)對象從設(shè)置為null了過后的一個(gè)生命周期的代碼監(jiān)控。而且因?yàn)槿跻檬欠翊嬖趯厥照麄€(gè)流程都不會(huì)造成影響,可以這樣認(rèn)為,垃圾回收器找得到弱引用,該引用的對象就會(huì)被回收,如果找不到弱引用,一旦等到GC完成了垃圾回收過后,弱引用引用的對象占用的內(nèi)存也會(huì)自動(dòng)釋放,這就是軟引用在垃圾回收過后的自動(dòng)終止。
      最后談?wù)?strong>虛引用,虛引用應(yīng)該是JVM里面最厲害的一種引用,它的厲害在于它可以在對象的內(nèi)存物理內(nèi)存中清除掉了過后再引用該對象,也就是說當(dāng)虛引用引用到對象的時(shí)候,這個(gè)對象實(shí)際已經(jīng)從物理內(nèi)存堆清除掉了,如果我們不用手動(dòng)對對象死亡或者瀕臨死亡進(jìn)行處理的話,JVM會(huì)默認(rèn)調(diào)用finalize函數(shù),但是虛引用存在于該函數(shù)附近的生命周期內(nèi),所以可以手動(dòng)對對象的這個(gè)范圍的周期進(jìn)行監(jiān)控。它之所以稱為“幽靈引用”就是因?yàn)樵搶ο蟮奈锢韮?nèi)存已經(jīng)不存在的,我個(gè)人覺得JVM保存了一個(gè)對象狀態(tài)的鏡像索引,而這個(gè)鏡像索引里面包含了對象在這個(gè)生命周期需要的所有內(nèi)容,這里的所需要就是這個(gè)生命周期內(nèi)需要的對象數(shù)據(jù)內(nèi)容,也就是對象死亡和瀕臨死亡之前finalize函數(shù)附近,至于強(qiáng)引用所需要的其他對象附加內(nèi)容是不需要在這個(gè)鏡像里面包含的,所以即使物理內(nèi)存不存在,還是可以通過虛引用監(jiān)控到該對象的,只是這種情況是否可以讓對象重新激活為強(qiáng)引用我就不敢說了。因?yàn)樘撘迷谝脤ο蟮倪^程不會(huì)去使得這個(gè)對象由Dead復(fù)活,而且這種對象是可以在回收周期進(jìn)行回收的。

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

    只有注冊用戶登錄后才能發(fā)表評(píng)論。


    網(wǎng)站導(dǎo)航:
     
    主站蜘蛛池模板: 免费AA片少妇人AA片直播| 亚洲91精品麻豆国产系列在线| gogo免费在线观看| 免费女人18毛片a级毛片视频| 亚洲一本到无码av中文字幕| 成人片黄网站A毛片免费| 亚洲免费观看网站| 97性无码区免费| 亚洲成a人片在线观| 麻豆视频免费观看| 亚洲成人午夜电影| 精品久久久久成人码免费动漫| 亚洲国产日韩在线人成下载| 99久久久国产精品免费无卡顿| 亚洲AV一二三区成人影片| 成年美女黄网站18禁免费| 亚洲中文字幕久久精品无码VA| 夜夜嘿视频免费看| 国产亚洲欧美日韩亚洲中文色| 免费国内精品久久久久影院| 免费人妻精品一区二区三区| 亚洲日本在线观看视频| 中文字幕不卡免费高清视频| 国产V亚洲V天堂无码| 99re免费视频| 亚洲人成在久久综合网站| 成人免费无码大片A毛片抽搐色欲| 亚洲成AV人片在WWW| 又黄又爽的视频免费看| 一日本道a高清免费播放| 亚洲精品无码Av人在线观看国产 | 特级毛片A级毛片免费播放| 亚洲成?v人片天堂网无码| 国产免费牲交视频免费播放 | 亚洲免费综合色在线视频| 国产免费人成视频在线观看| 国产精品九九久久免费视频| 亚洲爆乳精品无码一区二区三区| 97在线视频免费播放| 亚洲视频在线观看2018| 国产日产成人免费视频在线观看|