本來準備把內存模型單獨放到某一篇文章的某個章節裏面講解,後來查閱了國外不少文檔才發現其實JVM內存模型的內容還蠻多的,因此直接做爲一個章節的基礎知識來說解,可能該章節概念的東西比較多。一個開發Java的開發者,一旦瞭解了JVM內存模型就可以更加深刻地瞭解該語言的語言特性,可能這個章節更多的是概念,沒有太多代碼實例,因此但願讀者諒解,有什麼筆誤來Email告知:silentbalanceyh@126.com,本文儘可能涵蓋全部Java語言能夠碰到的和內存相關的內容,一樣也會提到一些和內存相關的計算機語言的一些知識,爲草案。由於平時開發的時候沒有特殊狀況不會進行內存管理,因此有可能有筆誤的地方比較多,我用的是Windows平臺,因此本文涉及到的與操做系統相關的只是僅僅侷限於Windows平臺。不只僅如此,這一個章節牽涉到的多線程和另一些內容並無講到,這裏主要是結合JVM內部特性把本章節做爲核心的概念性章節來說解,這樣方便初學者深刻以及完全理解Java語言)
本文章節:java
在系統開發過程,常常會遇到這幾個基本概念,不管是網絡通信、對象之間的消息通信仍是Web開發人員經常使用的Http請求都會遇到這樣幾個概念,常常有人提到Ajax是異步通信方式,那麼究竟怎樣的方式是這樣的概念描述呢?
可見性就是在多核或者多線程運行過程當中內存的一種共享模式,在JMM模型裏面,經過併發線程修改變量值的時候,必須將線程變量同步回主存事後,其餘線程纔可能訪問到。
【*:簡單講,內存的可見性使內存資源能夠共享,當一個線程執行的時候它所佔有的內存,若是它佔有的內存資源是可見的,那麼這時候其餘線程在必定規則內是能夠訪問該內存資源的,這種規則是由JMM內部定義的,這種狀況下內存的該特性稱爲其可見性。】
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本身進行內存管理的一種優點。
若是在該模型內部使用了一致的同步性的時候,這些屬性中的每個屬性都遵循比較簡單的原則:和全部同步的內存塊同樣,每一個同步塊以內的任何變化都具有了原子性以及可見性,和其餘同步方法以及同步塊遵循一樣一致的原則,並且在這樣的一個模型內,每一個同步塊不能使用同一個鎖,在整個程序的調用過程是按照編寫的程序指定指令運行的。即便某一個同步塊內的處理可能會失效,可是該問題
不會影響到其餘線程的同步問題,也不會引發連環失效。
簡單講:當程序運行的時候使用了一致的同步性的時候,每一個同步塊有一個獨立的空間以及獨立的同步控制器和鎖機制,而後對外按照JVM的執行指令進行數據的讀寫操做。這種狀況使得使用內存的過程變得很是嚴謹!
JVM線程必須依靠自身來維持對象的可見性以及對象自身應該提供相對應的操做而實現整個內存操做的三個特性,而不是僅僅依靠特定的修改對象狀態的線程來完成如此複雜的一個流程。
JMM最初設計的時候存在必定的缺陷,這種缺陷雖然現有的JVM平臺已經修復,可是這裏不得不說起,也是爲了讀者更加了解JMM的設計思路,這一個小節的概念可能會牽涉到不少更加深刻的知識,若是讀者不能讀懂沒有關係先看了文章後邊的章節再返回來看也能夠。
學過Java的朋友都應該知道Java中的不可變對象,這一點在本文最後講解String類的時候也會說起,而JMM最初設計的時候,這個問題一直都存在,就是:不可變對象彷佛能夠改變它們的值(這種對象的不可變指經過使用final關鍵字來獲得保證),(
Publis Service Reminder:讓一個對象的全部字段都爲final並不必定使得這個對象不可變——
全部類型還必須是原始類型而不能是對象的引用。而不可變對象被認爲不要求同步的。可是,由於在將內存寫方面的更改從一個線程傳播到另一個線程的時候存在潛在的延遲,這樣就使得有可能存在一種競態條件,即容許一個線程首先看到不可變對象的一個值,一段時間以後看到的是一個不一樣的值。這種狀況之前怎麼發生的呢?在
JDK 1.4中的String實現裏,這兒基本有三個重要的決定性字段:
對字符數組的引用、長度和描述字符串的開始數組的偏移量。String就是以這樣的方式在JDK 1.4中實現的,而不是隻有字符數組,所以字符數組能夠在多個String和StringBuffer對象之間共享,而不須要在每次建立一個String的時候都拷貝到一個新的字符數組裏。假設有下邊的代碼:
另外一個主要領域是與volatile字段的內存操做從新排序有關,這個領域中現有的JMM引發了一些比較混亂的結果。現有的JMM代表易失性的讀和寫是
直接和主存打交道的,這樣避免了把值存儲到
寄存器或者繞過處理器
特定的緩存,這使得多個線程通常能看見一個給定變量最新的值。但是,結果是這種volatile定義並無最初想象中那樣如願以償,而且致使了volatile的重大混亂。爲了在缺少同步的狀況下提供較好的性能,編譯器、運行時和緩存一般是容許進行內存的從新排序操做的,只要當前執行的線程分辨不出它們的區別。(這就是
within-thread as-if-serial semantics[線程內彷佛是串行]的解釋)可是,易失性的讀和寫是徹底跨線程安排的,編譯器或緩存不能在彼此之間從新排序易失性的讀和寫。遺憾的是,經過參考普通變量的讀寫,JMM容許易失性的讀和寫被重排序,這樣覺得着開發人員不能使用易失性標誌做爲操做已經完成的標誌。好比:
這裏的思想是使用易失性變量initialized擔任守衛來代表一套別的操做已經完成了,這是一個很好的思想,可是不能在JMM下工做,由於舊的JMM容許非易失性的寫(好比寫到configOptions字段,以及寫到由configOptions引用Map的字段中)與易失性的寫一塊兒從新排序,所以另一個線程可能會看到initialized爲true,可是對於configOptions字段或它所引用的對象尚未一個一致的或者說當前的針對內存的視圖變量,volatile的舊語義只承諾在讀和寫的變量的可見性,而不承諾其餘變量,雖然這種方法更加有效的實現,可是結果會和咱們設計之初截然不同。
與Big-Endian相對的就是Little-Endian的存儲方式,一樣按照8位爲一個存儲單位上邊的數據0x0A0B0C0D存儲格式爲:
能夠看到LSB的值存儲的0x0D,也就是數據的最低位是從內存的低地址開始存儲的,它的高位是
從右到左的順序逐漸增長內存分配空間進行存儲的,若是按照十六位爲存儲單位存儲格式爲:
從上圖能夠看到最低的16位的存儲單位裏面存儲的值爲0x0C0D,接着纔是0x0A0B,這樣就能夠看到按照數據從高位到低位在內存中存儲的時候是從右到左進行遞增存儲的,實際上能夠從寫內存的順序來理解,實際上數據存儲在內存中無非在使用的時候是
寫內存和
讀內存,針對LSB的方式最好的書面解釋就是向左增長來看待,若是真正在進行內存讀寫的時候使用這樣的順序,其意義就體現出來了:
按照這種讀寫格式,0x0D存儲在最低內存地址,而從右往左的增加就能夠看到LSB存儲的數據爲0x0D,和初衷吻合,則十六位的存儲就能夠按照下邊的格式來解釋:
實際上從上邊的存儲還會考慮到另一個問題,若是按照這種方式從右往左的方式進行存儲,若是是遇到Unicode文字就和從左到右的語言顯示方式相反。好比一個單詞「XRAY」,使用Little-Endian的方式存儲格式爲:
使用這種方式進行內存讀寫的時候就會發現計算機語言和語言自己的順序會有衝突,這種衝突主要是以使用語言的人的習慣有關,而書面化的語言從左到右就能夠知道其衝突是不可避免的。咱們通常使用語言的閱讀方式都是從左到右,而低端存儲(Little-Endian)的這種內存讀寫的方式使得咱們最終從計算機裏面讀取字符須要進行倒序,並且考慮另一個問題,
若是是針對中文而言,一個字符是兩個字節,就會出現總體順序和每個位的順序會進行兩次倒序操做,這種方式真正在製做處理器的時候也存在一種計算上的衝突,而針對使用文字從左到右進行閱讀的國家而言,從右到左的方式(Big-Endian)則會有這樣的文字衝突,另一方面,儘管有不少國家使用語言是從右到左,可是僅僅和Big-Endian的方式存在衝突,這些國家畢竟佔少數,因此能夠理解的是,爲何
主流的系統都是使用的Little-Endian的方式
【*:這裏不解釋Middle-Endian的方式以及Mixed-Endian的方式】
LSB:在計算機中,最低有效位是一個二進制給予單位的整數,
位的位置肯定了該數據是一個偶數仍是奇數,LSB有時被稱爲最右位。在使用具體位二進制數以內,常見的存儲方式就是每一位存儲1或者0的方式,從0向上到1每一比特逢二進一的存儲方式。
LSB的這種特性用來指定單位位,而不是位的數字,而這種方式也有可能產生必定的混亂。
——以上是關於Big-Endian和Little-Endian的簡單講解——
JVM虛擬機將搜索和使用類型的一些信息也存儲在方法區中以方便應用程序加載讀取該數據。設計者在設計過程也考慮到要方便JVM進行Java應用程序的快速執行,而這種取捨主要是爲了程序在運行過程當中內存不足的狀況可以經過必定的取捨去彌補內存不足的狀況。在JVM內部,
全部的線程共享
相同的
方法區,所以,訪問方法區的數據結構
必須是線程安全的,若是兩個線程都試圖去調用去找一個名爲Lava的類,好比Lava尚未被加載,
只有一個線程能夠加載該類而另外的線程只可以等待。方法區的大小在分配過程當中是不固定的,隨着Java應用程序的運行,JVM能夠調整其大小,須要注意一點,方法區的內存
不須要是連續的,由於方法區內存
能夠分配在
內存堆中,即便是虛擬機JVM實例對象本身所在的內存堆也是可行的,而在實現過程是容許程序員自身來指定方法區的初始化大小的。
一樣的,由於Java自己的自動內存管理,方法區也會被垃圾回收的,Java程序能夠經過類擴展動態加載器對象,類能夠成爲「未引用」向垃圾回收器進行申請,若是一個類是「未引用」的,則該類就可能被卸載,
而方法區針對具體的語言特性有幾種信息是存儲在方法區內的:
【類型信息】:
- 類型的徹底限定名(java.lang.String格式)
- 類型的徹底限定名的直接父類的徹底限定名(除非這個父類的類型是一個接口或者java.lang.Object)
- 不論類型是一個類或者接口
- 類型的修飾符(例如public、abstract、final)
- 任何一個直接超類接口的徹底限定名的列表
在JVM和類文件名的內部,類型名通常都是徹底限定名(java.lang.String)格式,在Java源文件裏面,徹底限定名必須加入包前綴,而不是咱們在開發過程寫的簡單類名,而在方法上,只要是符合Java語言規範的類的徹底限定名均可以,而JVM可能直接進行解析,好比:(java.lang.String)在JVM內部名稱爲java/lang/String,這就是咱們在異常捕捉的時候常常看到的ClassNotFoundException的異常裏面類信息的名稱格式。
除此以外,還必須爲每一種加載過的類型在JVM內進行存儲,下邊的信息不存儲在方法區內,下邊的章節會一一說明
- 類型常量池
- 字段信息
- 方法信息
- 全部定義在Class內部的(靜態)變量信息,除開常量
- 一個ClassLoader的引用
- Class的引用
【常量池】
針對類型加載的類型信息,JVM將這些存儲在常量池裏,常量池是一個根據類型定義的常量的有序常量集,包括字面量(String、Integer、Float常量)以及符號引用(類型、字段、方法),整個長量池會被JVM的一個索引引用,如同數組裏面的元素集合按照索引訪問同樣,JVM針對這些常量池裏面存儲的信息也是按照索引方式進行。實際上長量池在Java程序的
動態連接過程起到了一個相當重要的做用。
【字段信息】
針對字段的類型信息,下邊的信息是存儲在方法區裏面的:
- 字段名
- 字段類型
- 字段修飾符(public,private,protected,static,final,volatile,transient)
【方法信息】
針對方法信息,下邊信息存儲在方法區上:
- 方法名
- 方法的返回類型(包括void)
- 方法參數的類型、數目以及順序
- 方法修飾符(public,private,protected,static,final,synchronized,native,abstract)
針對非本地方法,還有些附加方法信息須要存儲在方法區內:
- 方法字節碼
- 方法中局部變量區的大小、方法棧幀
- 異常表
【類變量】
類變量在一個類的多個實例之間共享,這些變量直接和類相關,而不是和類的實例相關,(定義過程簡單理解爲類裏面定義的static類型的變量),針對類變量,其邏輯部分就是存儲在方法區內的。在JVM使用這些類以前,JVM先要在方法區裏面爲定義的
non-final變量分配內存空間;常量(定義爲final)則在JVM內部則不是以一樣的方式來進行存儲的,儘管針對常量而言,一個final的類變量是擁有它本身的常量池,做爲常量池裏面的存儲某部分,
類常量是存儲在方法區內的,而其邏輯部分則不是按照上邊的類變量的方式來進行內存分配的。雖然non-final類變量是做爲這些類型聲明中存儲數據的某一部分,final變量存儲爲任何使用它類型的一部分的數據格式進行簡單存儲。
【ClassLoader引用】
對於每種類型的加載,JVM必須檢測其類型是否符合了JVM的語言規範,對於經過類加載器加載的對象類型,JVM必須存儲對類的引用,而這些針對類加載器的引用是做爲了方法區裏面的類型數據部分進行存儲的。
【類Class的引用】
JVM在加載了任何一個類型事後會
建立一個java.lang.Class的實例,虛擬機必須經過必定的途徑來引用該類型對應的一個Class的實例,而且將其存儲在方法區內
【方法表】
爲了提升訪問效率,必須仔細的設計存儲在方法區中的數據信息結構。除了以上討論的結構,jvm的實現者還添加一些其餘的數據結構,如
方法表【下邊會說明】。
2)內存棧(Stack):
當一個新線程啓動的時候,JVM會爲Java線程建立每一個線程的
獨立內存棧,如前所言Java的內存棧是由棧幀構成,棧幀自己處於
遊離
狀態,在JVM裏面,棧幀的操做只有兩種:
出棧和
入棧。正在被線程執行的方法通常稱爲當前線程方法,而該方法的棧幀就稱爲當前幀,而在該方法內定義的類稱爲當前類,常量池也稱爲當前常量池。當執行一個方法如此的時候,JVM保留當前類和當前常量池的
跟蹤,當虛擬機遇到了存儲在棧幀中的數據上的操做指令的時候,它就執行當前幀的操做。當一個線程調用某個Java方法時,虛擬機建立而且將一個新幀壓入到內存堆棧中,而這個壓入到內存棧中的幀成爲
當前棧幀,當該方法執行的時候,JVM使用內存棧來存儲參數、局部變量、中間計算結果以及其餘相關數據。方法在執行過程有可能由於兩種方式而結束:若是一個方法返回完成就屬於方法執行的正常結束,若是在這個過程拋出異常而結束,能夠稱爲非正常結束,不管是正常結束仍是異常結束,JVM都會彈出或者丟棄該棧幀,則上一幀的方法就成爲了當前幀。
在JVM中,Java線程的棧數據是屬於某個線程獨有的,其餘的線程不可以修改或者經過其餘方式來訪問該線程的棧幀,正由於如此這種狀況不用擔憂多線程同步訪問Java的局部變量,當一個線程調用某個方法的時候,方法的局部變量是在方法內部進行的Java棧幀的存儲,只有當前線程能夠訪問該局部變量,而其餘線程不能隨便訪問該內存棧裏面存儲的數據。內存棧內的棧幀數據和方法區以及內存堆同樣,Java棧的
棧幀
不須要分配在連續的堆棧內,或者說它們多是在堆,或者二者組合分配,實際數據用於表示Java堆棧和棧幀結構是JVM自己的設計結構決定的,並且在編程過程能夠容許程序員指定一個用於Java堆棧的初始大小以及最大、最小尺寸。
【概念區分】
- 內存棧:這裏的內存棧和物理結構內存堆棧有點點區別,是內存裏面數據存儲的一種抽象數據結構。從操做系統上講,在程序執行過程對內存的使用自己經常使用的數據結構就是內存堆棧,而這裏的內存堆棧指代的就是JVM在使用內存過程整個內存的存儲結構,多指內存的物理結構,而Java內存棧不是指代的一個物理結構,更多的時候指代的是一個抽象結構,就是符合JVM語言規範的內存棧的一個抽象結構。由於物理內存堆棧結構和Java內存棧的抽象模型結構自己比較類似,因此咱們在學習過程就正常把這兩種結構放在一塊兒考慮了,並且兩者除了概念上有一點點小的區別,理解成爲一種結構對於初學者也何嘗不可,因此實際上也能夠以爲兩者沒有太大的本質區別。可是在學習的時候最好分清楚內存堆棧和Java內存棧的一小點細微的差距,前者是物理概念和自己模型,後者是抽象概念和自己模型的一個共同體。而內存堆棧更多的說法能夠理解爲一個內存塊,由於內存塊能夠經過索引和指針進行數據結構的組合,內存棧就是內存塊針對數據結構的一種表示,而內存堆則是內存塊的另一種數據結構的表示,這樣理解更容易區份內存棧和內存堆棧(內存塊)的概念。
- 棧幀:棧幀是內存棧裏面的最小單位,指的是內存棧裏面每個最小內存存儲單元,它針對內存棧僅僅作了兩個操做:入棧和出棧,通常狀況下:所說的堆棧幀和棧幀卻是一個概念,因此在理解上記得加以區分
- 內存堆:這裏的內存堆和內存棧是相對應的,其實內存堆裏面的數據也是存儲在系統內存堆棧裏面的,只是它使用了另一種方式來進行堆裏面內存的管理,而本章題目要講到的就是Java語言自己的內存堆和內存棧,而這兩個概念都是抽象的概念模型,並且是相對的。
棧幀:棧幀主要包括三個部分:
局部變量、
操做數棧幀(操做幀)和
幀數據(數據幀)。本地變量和操做數幀的大小取決於須要,這些大小是在編譯時就決定的,而且在每一個方法的類文件數據中進行分配,幀的數據大小則不同,它雖然也是在編譯時就決定的可是它的大小和自己代碼實現有關。當JVM調用一個Java方法的時候,它會檢查類的數據來肯定在本地變量和操做方法要求的棧大小,它計算該方法所須要的內存大小,而後將這些數據分配好內存空間壓入到內存堆棧中。
棧幀——局部變量:局部變量是以Java棧幀組合成爲的一個
以零爲基的數組,使用局部變量的時候使用的其實是一個包含了0的一個基於索引的
數組結構。int類型、float、引用以及返回值都佔據了一個數組中的局部變量的條目,而byte、short、char則在存儲到局部變量的時候是先
轉化成爲int再進行操做的,則long和double則是在這樣一個數組裏面使用了
兩個元素的空間大小,在局部變量裏面存儲基本數據類型的時候使用的就是這樣的結構。舉個例子:
class Example3a{
public static int runClassMethod(int i,long l,float f,double d,Object o,byte b)
{
return 0;
}
public int runInstanceMethod(char c,double d,short s,boolean b)
{
return 0;
}
}
棧幀——操做幀:和局部變量同樣,操做幀也是一組有組織的數組的存儲結構,可是和局部變量不同的是這個不是經過
數組的索引訪問的,而是直接進行的
入棧和出棧的操做,當操做指令直接壓入了操做棧幀事後,從棧幀裏面出來的數據會直接在出棧的時候被
讀取和
使用。除了
程序計數器之外,操做幀也是能夠直接被指令訪問到的,JVM裏面
沒有寄存器。處理操做幀的時候Java虛擬機是基於內存棧的而不是基於寄存器的,由於它在操做過程是直接對內存棧進行操做而不是針對寄存器進行操做。而JVM內部的指令也能夠來源於其餘地方好比緊接着操做符以及操做數的字節碼流或者直接從常量池裏面進行操做。JVM指令其實真正在操做過程的焦點是集中在內存棧棧幀的操做幀上的。JVM指令將操做幀做爲一個工做空間,有許多指令都是從操做幀裏面出棧讀取的,對指令進行操做事後將操做幀的計算結果從新壓入內存堆棧內。好比iadd指令將兩個整數壓入到操做幀裏面,而後將兩個操做數進行相加,相加的時候從內存棧裏面讀取兩個操做數的值,而後進行運算,最後將運算結果從新存入到內存堆棧裏面。舉個簡單的例子:
begin
iload_0 //將整數類型的局部變量0壓入到內存棧裏面
iload_1 //將整數類型的局部變量1壓入到內存棧裏面
iadd //將兩個變量出棧讀取,而後進行相加操做,將結果從新壓入棧中
istore_2 //將最終輸出結果放在另一個局部變量裏面
end
綜上所述,就是整個計算過程針對內存的一些操做內容,而總體的結構能夠用下圖來描述:
棧幀——數據幀:除了局部變量和操做幀之外,Java棧幀還包括了數據幀,用於支持常量池、普通的方法返回以及異常拋出等,這些數據都是存儲在Java內存棧幀的數據幀中的。不少JVM的指令集實際上使用的都是常量池裏面的一些條目,一些指令,只是把int、long、float、double或者String從常量池裏面壓入到Java棧幀的操做幀上邊,一些指令使用常量池來管理類或者數組的實例化操做、字段的訪問控制、或者方法的調用,其餘的指令就用來決定常量池條目中記錄的某一特定對象是否某一類或者常量池項中指定的接口。常量池會判斷類型、字段、方法、類、接口、類字段以及引用是如何在JVM進行符號化描述,而這個過程由JVM自己進行對應的判斷。這裏就能夠理解JVM如何來判斷咱們一般說的:「原始變量存儲在內存棧上,而引用的對象存儲在內存堆上邊。」除了常量池判斷幀數據符號化描述特性之外,這些數據幀必須在JVM正常執行或者異常執行過程輔助它進行處理操做。若是一個方法是正常結束的,JVM必須恢復棧幀調用方法的數據幀,並且必須設置PC寄存器指向調用方法後邊等待的指令完成該調用方法的位置。若是該方法存在返回值,JVM也必須將這個值壓入到操做幀裏面以提供給須要這些數據的方法進行調用。不只僅如此,數據幀也必須提供一個方法調用的
異常表,當JVM在方法中拋出異常而
非正常結束的時候,該異常表就用來存放異常信息。
3)內存堆(Heap):
當一個Java應用程序在運行的時候在程序中建立一個對象或者一個數組的時候,JVM會針對該對象和數組分配一個新的內存堆空間。可是在JVM實例內部,只存在
一個內存堆實例,全部的依賴該JVM的Java應用程序都須要共享該堆實例,而Java應用程序自己在運行的時候它本身包含了一個由JVM虛擬機實例分配的
本身的堆空間,而在應用程序啓動的時候,任何一個Java應用程序都會獲得
JVM分配的堆空間,並且針對每個Java應用程序,這些運行Java應用程序的堆空間都是相互獨立的。這裏所說起到的共享堆實例是指JVM在初始化運行的時候
總體堆空間只有一個,這個是Java語言平臺直接從操做系統上可以拿到的總體堆空間,因此的依賴該JVM的程序均可以獲得這些內存空間,可是針對每個
獨立的Java應用程序而言,這些堆空間是
相互獨立的,每個Java應用程序在運行最初都是依靠JVM來進行堆空間的分配的。即便是兩個相同的Java應用程序,一旦在運行的時候處於
不一樣的操做系統進程(通常爲java.exe)中,它們各自分配的堆空間都是獨立的,不能相互訪問,只是兩個
Java應用進程初始化拿到的堆空間來自JVM的分配,而JVM是從最初的內存堆實例裏面分配出來的。在同一個Java應用程序裏面若是出現了不一樣的
線程,則是能夠
共享每個Java應用程序拿到的內存堆空間的,這也是爲何在開發多線程程序的時候,針對同一個Java應用程序必須
考慮線程安全問題,由於在一個Java進程裏面全部的線程是能夠共享這個進程拿到的堆空間的數據的。可是Java
內存堆有一個特性,就是JVM擁有針對新的對象分配內存的指令,可是它卻不包含
釋放該內存空間的
指令,固然開發過程能夠在Java源代碼中顯示釋放內存或者說在JVM字節碼中進行顯示的內存釋放,可是JVM僅僅只是檢測堆空間中是否有引用不可達(不能夠引用)的對象,而後將接下來的操做交給垃圾回收器來處理。
對象表示:
JVM規範裏面並
沒有說起到Java對象如何在堆空間中
表示和描述,對象表示能夠理解爲設計JVM的工程師在最初考慮到對象調用以及垃圾回收器針對對象的判斷而獨立的一種Java
對象在內存中的
存儲結構,該結構是由設計最初考慮的。針對一個建立的類實例而言,它內部定義的實例變量以及它的超類以及一些相關的核心數據,是必須經過必定的途徑進行該對象內部存儲以及表示的。當開發過程給定了一個對象引用的時候,JVM必須可以經過這個引用快速從對象堆空間中去拿到該對象可以訪問的數據內容。也就是說,堆空間內對象的存儲結構必須爲外圍對象引用提供一種能夠訪問該對象以及控制該對象的接口使得引用可以順利地調用該對象以及相關操做。所以,針對堆空間的對象,分配的內存中每每也包含了一些指向方法區的指針,由於從總體存儲結構上講,
方法區
彷佛存儲了不少原子級別的內容,包括方法區內最原始最單一的一些變量:好比
類字段、字段數據、類型數據等等。而JVM自己針對堆空間的管理存在兩種設計結構:
【1】設計一:
堆空間的設計能夠劃分爲兩個部分:一個
處理池和一個
對象池,一個對象的引用能夠拿處處理池的一個本地指針,而處理池主要分爲兩個部分:
一個指向對象池裏面的指針以及一個指向方法區的指針。這種結構的優點在於JVM在處理對象的時候,更加可以方便地組合堆碎片以使得全部的數據被更加方便地進行調用。當JVM須要將一個對象移動到對象池的時候,它僅僅須要更新該對象的指針到一個新的對象池的內存地址中就能夠完成了,而後在處理池中針對該對象的內部結構進行相對應的處理工做。不過這樣的方法也會出現一個缺點就是在處理一個對象的時候針對對象的訪問
須要提供兩個不一樣的指針,這一點可能很差理解,其實能夠這樣講,真正在對象處理過程存在一個
根據時間戳有區別的對象狀態,而對象在移動、更新以及建立的整個過程當中,它的處理池裏面老是包含了兩個指針,一個指針是指向
對象內容自己,一個指針是指向了
方法區,由於一個完整的對外的對象是依靠這兩部分被引用指針引用到的,而咱們開發過程是
不能夠操做處理池的兩個指針的,只有引用指針咱們能夠經過外圍編程拿到。若是Java是按照這種設計進行對象存儲,這裏的引用指針就是平時說起到的「Java的引用」,只是JVM在引用指針還作了
必定的封裝,這種封裝的規則是JVM自己設計的時候作的,它就經過這種結構在外圍進行一次封裝,好比Java引用不具有直接操做內存地址的能力就是該封裝的一種
限制規則。這種設計的結構圖以下:
【2】設計二:
另一種堆空間設計就是使用對象引用拿到的本地指針,將該指針直接指向綁定好的對象的實例數據,這些數據裏面僅僅包含了一個指向方法區原子級別的數據去拿到該實例相關數據,這種狀況下只須要引用一個指針來訪問對象實例數據,可是這樣的狀況使得
對象的移動以及對象的數據更新變得更加複雜。當JVM須要移動這些數據以及進行堆內存碎片的整理的時候,就必須直接更新該對象全部運行時的數據區,這種狀況能夠用下圖進行表示:
JVM須要從一個對象引用來得到該引用可以引用的對象數據存在多個緣由,當一個程序試圖將一個對象的引用轉換成爲另一個類型的時候,JVM就會檢查兩個引用指向的對象是否存在
父子類關係,而且檢查兩個引用引用到的對象是否可以進行
類型轉換,並且全部這種類型的轉換必須執行一樣的一個操做:instanceof操做,在上邊兩種狀況下,JVM都必需要去分析引用指向的對象內部的數據。當一個程序調用了一個實例方法的時候,JVM就必須進行動態綁定操做,它必須選擇調用方法的引用類型,是一個基於類的方法調用仍是一個基於對象的方法調用,要作到這一點,它又要獲取該對象的
惟一引用才能夠。無論對象的實現是使用什麼方式來進行對象描述,都是在針對內存中關於該對象的方法表進行操做,由於使用這樣的方式加快了實例針對方法的調用,並且在JVM內部實現的時候這樣的機制使得其運行表現比較良好,因此
方法表的設計在JVM總體結構中發揮了極其重要的做用。關於方法表的存在與否,在JVM規範裏面沒有嚴格說明,也有可能真正在實現過程只是一個
抽象概念,
物理層它根本不存在,針對放發表實現對於一個建立的實例而言,它自己具備不過高的內存須要求,若是該實現裏面使用了方法表,則對象的方法表應該是能夠很快被外圍引用訪問到的。
有一種辦法就是經過對象引用鏈接到方法表的時候,以下圖:
該圖代表,在每一個指針指向一個對象的時候,其實是使用的一個特殊的數據結構,這些特殊的結構包括幾個部分:
實際上從圖中能夠看出,方法表就是一個指針數組,它的每個元素包含了一個指針,針對每一個對象的方法均可以直接經過該指針在方法區中找到匹配的數據進行相關調用,而這些方法表須要包括的內容以下:
- 方法內存堆棧段空間中操做棧的大小以及局部變量
- 方法字節碼
- 一個方法的異常表
這些信息使得JVM足夠針對該方法進行調用,在調用過程,這種結構也可以方便子類對象的方法直接經過指針引用到父類的一些方法定義,也就是說指針在內存空間以內經過JVM自己的調用使得父類的一些方法表也能夠一樣的方式被調用,固然這種調用過程避免不了兩個對象之間的類型檢查,可是這樣的方式就使得繼承的實現變得更加簡單,並且方法表提供的這些數據足夠引用對對象進行帶有任何OO特徵的對象操做。
另一種數據在上邊的途中沒有顯示出來,也是從邏輯上講內存堆中的對象的真實數據結構——
對象的鎖。這一點可能須要關聯到JMM模型中講的進行理解。JVM中的每個對象都是和一個鎖(互斥)相關聯的,這種結構使得該對象能夠很容易支持多線程訪問,並且該對象的對象鎖一次只能被
一個線程訪問。當一個線程在運行的時候具備某個對象的鎖的時候,僅僅只有這個線程能夠訪問該對象的實例變量,其餘線程若是須要訪問該實例的實例變量就必須等待這個線程將它佔有的對象鎖釋放事後纔可以正常訪問,若是一個線程請求了一個被其餘線程佔有的對象鎖,這個請求線程也必須等到該鎖被釋放事後纔可以拿到這個對象的對象鎖。一旦這個線程擁有了一個對象鎖事後,它本身能夠
屢次向同一個鎖發送對象的
鎖請求,可是若是它要使得被該線程鎖住的對象能夠被其餘鎖訪問到的話就須要一樣的釋放鎖的次數,好比
線程A請求了
對象B的對象鎖
三次,那麼
A將會一直佔有
B對象的
對象鎖,直到它將該對象鎖釋放了三次。
不少對象也可能在整個生命週期都沒有被對象鎖鎖住過,在這樣的狀況下對象鎖相關的數據是
不須要對象內部實現的,
除非有線程向該對象請求了對象鎖,不然這個對象就沒有該對象鎖的存儲結構。因此上邊的實現圖能夠知道,不少實現不包括指向對象鎖的
「鎖數據」,鎖數據的實現必需要等待某個線程向該對象發送了對象鎖請求事後,並且是在
第一次鎖請求事後纔會被
實現。這個結構中,JVM卻可以間接地經過一些辦法針對對象的鎖進行管理,好比把對象鎖放在基於對象地址的搜索樹上邊。實現了鎖結構的對象中,每個Java對象邏輯上都在內存中成爲了一個等待集,這樣就使得全部的線程在鎖結構裏面針對對象內部數據能夠獨立操做,等待集就使得每一個線程可以獨立於其餘線程去完成一個共同的設計目標以及程序執行的最終結果,這樣就使得多線程的線程獨享數據以及線程共享數據機制很容易實現。
不只僅如此,針對內存堆對象還必須存在一個
對象的鏡像,該鏡像的主要目的是
提供給垃圾回收器進行監控操做,垃圾回收器是經過對象的狀態來判斷該對象是否被應用,一樣它須要針對堆內的對象進行監控。而當監控過程垃圾回收器收到對象回收的事件觸發的時候,雖然使用了不一樣的垃圾回收算法,不論使用什麼算法都須要經過獨有的機制來
判斷對象目前處於哪一種狀態,而後根據對象狀態進行操做。開發過程程序員每每不會去仔細分析當一個對象引用設置成爲null了事後虛擬機內部的操做,但實際上Java裏面的引用每每不像咱們想像中那麼簡單,Java引用中的虛引用、弱引用就是使得Java引用在顯示提交可回收狀態的狀況下對內存堆中的對象進行的反向監控,這些引用能夠
監視到垃圾回收器回收該對象的過程。垃圾回收器自己的實現也是須要內存堆中的對象可以提供相對應的數據的。其實這個位置到底JVM裏面是否使用了完整的Java對象的鏡像仍是使用的一個鏡像索引我沒有去仔細分析過,總之是在堆結構裏面存在着堆內對象的一個相似拷貝的
鏡像機制,使得垃圾回收器可以順利回收再也不被引用的對象。
4)內存棧和內存堆的實現原理探測【該部分爲不肯定概念】:
實際上不管是內存棧結構、方法區仍是內存堆結構,歸根到底使用的是操做系統的內存,操做系統的內存結構能夠理解爲
內存塊,經常使用的抽象方式就是一個內存堆棧,而JVM在OS上邊安裝了事後,就在啓動Java程序的時候按照
配置文件裏面的內容向操做系統申請內存空間,該內存空間會按照JVM內部的方法提供相應的結構調整。
內存棧應該是很容易理解的結構實現,
通常狀況下,內存棧是
保持連續的,可是
不絕對,內存棧申請到的地址實際上不少狀況下都是連續的,而每一個地址的最小單位是按照計算機位來算的,該計算機位裏面只有兩種狀態1和0,而內存棧的使用過程就是典型的相似C++裏面的普通指針結構的使用過程,直接針對指針進行++或者--操做就修改了該指針針對內存的偏移量,而這些偏移量就使得該指針能夠調用不一樣的內存棧中的數據。至於針對內存棧發送的指令就是常見的計算機指令,而這些指令就使得該指針針
對內存棧的棧幀進行指令發送,好比發送
操做指令、變量讀取等等,直接就使得內存棧的調用變得更加簡單,並且棧幀在接受了該數據事後就知道到底針對棧幀內部的哪個部分進行調用,是操做幀、數據幀仍是局部變量。
內存堆實際上在操做系統裏面使用了雙向鏈表的數據結構,雙向鏈表的結構使得即便內存堆不具備連續性,每個堆空間裏面的鏈表也能夠進入下一個堆空間,而操做系統自己在整理內存堆的時候會作一些簡單的操做,而後經過每個內存堆的雙向鏈表就使得內存堆更加方便。並且堆空間不須要有序,甚至說
有序不影響堆空間的存儲結構,由於它歸根究竟是在內存塊上邊進行實現的,內存塊自己是一個堆棧結構,只是該內存堆棧裏面的塊如何分配不禁JVM決定,是由操做系統已經最開始分配好了,也就是最小存儲單位。而後JVM拿到從操做系統申請的堆空間事後,先進行初始化操做,而後就能夠直接使用了。
常見的對程序有影響的內存問題主要是兩種:
溢出和內存泄漏,上邊已經講過了內存泄漏,其實從內存的結構分析,泄漏這種狀況很難甚至說不可能
發生在棧空間裏面,其主要緣由是棧空間自己很難出現懸停的內存,由於棧空間的存儲結構有多是內存的一個地址數組,因此在訪問棧空間的時候使用的都是索引或者下標或者就是最原始的出棧和入棧的操做,這些操做使得棧裏面很難出現像堆空間同樣的
內存懸停(也就是引用懸掛)問題。堆空間懸停的內存是由於棧中存放的引用的變化,其實引用能夠理解爲從棧到堆的一個指針,當該指針發生變化的時候,堆內存碎片就有可能產生,而這種狀況下在原始語言裏面就常常發生內存泄漏的狀況,由於這些懸停的堆空間在系統裏面是不可以被任何本地指針引用到,就使得這些對象在未被回收的時候脫離了可操做區域而且佔用了系統資源。
棧溢出問題一直都是計算機領域裏面的一個安全性問題,這裏不作深刻討論,說多了就偏離主題了,而內存泄漏是程序員最容易理解的內存問題,還有一個問題來自於我一個黑客朋友就是:
堆溢出現象,這種現象可能更加複雜。
其實Java裏面的內存結構,最初看來就是堆和棧的結合,實際上能夠這樣理解,實際上對象的實際內容才存在對象池裏面,而有關對象的其餘東西有可能會存儲於方法區,而平時使用的時候的引用是存在內存棧上的,這樣就更加容易理解它內部的結構,不只僅如此,有時候還須要考慮到Java裏面的一些字段和屬性究竟是對象域的仍是類域的,這個也是一個比較複雜的問題。
兩者的區別簡單總結一下:
- 管理方式:JVM本身能夠針對內存棧進行管理操做,並且該內存空間的釋放是編譯器就能夠操做的內容,而堆空間在Java中JVM自己執行引擎不會對其進行釋放操做,而是讓垃圾回收器進行自動回收
- 空間大小:通常狀況下棧空間相對於堆空間而言比較小,這是由棧空間裏面存儲的數據以及自己須要的數據特性決定的,而堆空間在JVM堆實例進行分配的時候通常大小都比較大,由於堆空間在一個Java程序中須要存儲太多的Java對象數據
- 碎片相關:針對堆空間而言,即便垃圾回收器可以進行自動堆內存回收,可是堆空間的活動量相對棧空間而言比較大,頗有可能存在長期的堆空間分配和釋放操做,並且垃圾回收器不是實時的,它有可能使得堆空間的內存碎片主鍵累積起來。針對棧空間而言,由於它自己就是一個堆棧的數據結構,它的操做都是一一對應的,並且每個最小單位的結構棧幀和堆空間內複雜的內存結構不同,因此它通常在使用過程不多出現內存碎片。
- 分配方式:通常狀況下,棧空間有兩種分配方式:靜態分配和動態分配,靜態分配是自己由編譯器分配好了,而動態分配可能根據狀況有所不一樣,而堆空間倒是徹底的動態分配的,是一個運行時級別的內存分配。而棧空間分配的內存不須要咱們考慮釋放問題,而堆空間即便在有垃圾回收器的前提下仍是要考慮其釋放問題。
- 效率:由於內存塊自己的排列就是一個典型的堆棧結構,因此棧空間的效率天然比起堆空間要高不少,並且計算機底層內存空間自己就使用了最基礎的堆棧結構使得棧空間和底層結構更加符合,它的操做也變得簡單就是最簡單的兩個指令:入棧和出棧;棧空間針對堆空間而言的弱點是靈活程度不夠,特別是在動態管理的時候。而堆空間最大的優點在於動態分配,由於它在計算機底層實現多是一個雙向鏈表結構,因此它在管理的時候操做比棧空間複雜不少,天然它的靈活度就高了,可是這樣的設計也使得堆空間的效率不如棧空間,並且低不少。
3.本機內存[部份內容來源於IBM開發中心]
Java堆空間是在編寫Java程序中被咱們使用得
最頻繁的內存空間,平時開發過程,開發人員必定遇到過OutOfMemoryError,這種結果有可能來源於Java堆空間的
內存泄漏,也多是由於堆的
大小不夠而致使的,
有時候這些錯誤是能夠依靠開發人員
修復的,可是隨着Java程序須要處理愈來愈多的併發程序,可能有些錯誤就不是那麼容易處理了。有些時候即便Java
堆空間沒有滿也可能拋出錯誤,這種狀況下須要瞭解的就是JRE(Java Runtime Environment)內部到底發生了什麼。Java自己的運行宿主環境並
不是操做系統,而
是Java虛擬機,Java虛擬機自己是用C編寫的本機程序,天然它會調用到本機資源,最多見的就是針對
本機內存的調用。本機內存是能夠用於運行時進程的,它和Java應用程序使用的
Java堆內存
不同,每一種虛擬化資源都必須存儲在本機內存裏面,包括虛擬機自己運行的數據,這樣也意味着主機的硬件和操做系統在本機內存的限制將直接影響到Java應用程序的性能。
i.Java運行時如何使用本機內存:
1)堆空間和垃圾回收
Java運行時是一個操做系統進程(Windows下通常爲java.exe),該環境提供的功能會受一些位置的用戶代碼驅動,這雖然
提升了運行時在處理資源的
靈活性,可是沒法預測每種狀況下運行時環境須要何種資源,這一點Java堆空間講解中已經提到過了。在Java命令行能夠使用-Xmx和-Xms來控制堆空間初始配置,mx表示
堆空間的最大大小,ms表示
初始化大小,這也是上提到的啓動Java的配置文件
能夠配置的內容。儘管邏輯內存堆能夠根據堆上的對象數量和在GC上花費的時間增長或者減小,可是使用本機內存的大小是保持不變的,並且由-Xms的值指定,大部分GC算法都是依賴被分配的連續內存塊的堆空間,所以不能在堆須要擴大的時候
分配更多的
本機內存,全部的堆內存必須保留下來,請注意
這裏說的
不是Java堆內存空間是本機內存。
本機內存保留和
本機內存分配不同,本機內存被保留的時候,沒法使用物理內存或者其餘存儲器做爲備用內存,儘管保留地址空間塊不會耗盡物理資源,可是會阻止內存用於其餘用途,由保留從未使用過的內存致使的泄漏和泄漏分配的內存形成的問題其嚴重程度差很少,但使用的堆區域縮小時,一些垃圾回收器會回收堆空間的一部份內容,從而減小物理內存的使用。對於維護Java堆的內存管理系統,須要更多的
本機內存來維護它的狀態,進行垃圾收集的時候,必須分配數據結構來跟蹤空閒存儲空間和進度記錄,這些數據結構的確切大小和性質因實現的不一樣而有所差別。
2)JIT
JIT編譯器在
運行時編譯Java字節碼來優化本機可執行代碼,這樣極大提升了Java運行時的速度,而且支持Java應用程序與本地代碼至關的速度運行。字節碼編譯使用本機內存,並且JIT編譯器的
輸入(字節碼)和輸出(可執行代碼)也必須
存儲在本機內存裏面,包含了多個通過JIT編譯的方法的Java程序會比一些小型應用程序使用更多的本機內存。
3)類和類加載器
Java 應用程序由一些類組成,這些類定義對象結構和方法邏輯。Java 應用程序也使用 Java 運行時類庫(好比 java.lang.String)中的類,也能夠使用第三方庫。這些類須要存儲在內存中以備使用。存儲類的方式取決於具體實現。Sun JDK 使用
永久生成(permanent generation,PermGen)堆區域,從最基本的層面來看,使用更多的類將須要使用更多內存。(這可能意味着您的本機內存使用量會增長,或者您必須明確地從新設置 PermGen 或共享類緩存等區域的大小,以裝入全部類)。記住,不只您的應用程序須要加載到內存中,框架、應用服務器、第三方庫以及包含類的 Java 運行時也會
按需加載並佔用空間。Java 運行時能夠卸載類來回收空間,可是隻有在很是嚴酷的條件下才會這樣作,不能卸載單個類,而是卸載類加載器,隨其加載的全部類都會被卸載。只有在如下狀況下才能卸載類加載器
- Java 堆不包含對錶示該類加載器的 java.lang.ClassLoader 對象的引用。
- Java 堆不包含對錶示類加載器加載的類的任何 java.lang.Class 對象的引用。
- 在 Java 堆上,該類加載器加載的任何類的全部對象都再也不存活(被引用)。
須要注意的是,Java 運行時爲全部 Java 應用程序建立的 3 個默認類加載器( bootstrap、extension 和 application )都不可能知足這些條件,所以,任何系統類(好比 java.lang.String)或經過應用程序類加載器加載的任何應用程序類都不能在運行時釋放。即便類加載器適合進行收集,運行時也只會將收集類加載器做爲 GC 週期的一部分。一些實現只會在某些 GC 週期中卸載類加載器,也可能在運行時生成類,而不去釋放它。許多 Java EE 應用程序使用 JavaServer Pages (JSP) 技術來生成 Web 頁面。使用 JSP 會爲執行的每一個 .jsp 頁面生成一個類,而且這些類會在加載它們的類加載器的整個生存期中一直存在 —— 這個生存期一般是 Web 應用程序的生存期。另外一種生成類的常見方法是使用 Java 反射。反射的工做方式因 Java 實現的不一樣而不一樣,當使用 java.lang.reflect API 時,Java 運行時必須將一個反射對象(好比 java.lang.reflect.Field)的方法鏈接到被反射到的對象或類。這能夠經過使用 Java 本機接口(Java Native Interface,JNI)訪問器來完成,這種方法須要的設置不多,可是速度緩慢,也能夠在運行時爲您想要反射到的每種對象類型動態構建一個類。後一種方法在設置上更慢,但運行速度更快,很是適合於常常反射到一個特定類的應用程序。Java 運行時在最初幾回反射到一個類時使用 JNI 方法,但當使用了若干次 JNI 方法以後,訪問器會膨脹爲字節碼訪問器,這涉及到構建類並經過新的類加載器進行加載。執行屢次反射可能致使建立了許多訪問器類和類加載器,保持對反射對象的引用會致使這些類一直存活,並繼續佔用空間,由於建立字節碼訪問器很是緩慢,因此 Java 運行時能夠緩存這些訪問器以備之後使用,一些應用程序和框架還會緩存反射對象,這進一步增長了它們的本機內存佔用。程序員
4)JNI
JNI支持本機代碼調用Java方法,反之亦然,Java運行時自己極大依賴於JNI代碼來實現類庫功能,好比文件和網絡I/O,JNI應用程序能夠經過三種方式增長Java運行時對本機內存的使用:
- JNI應用程序的本機代碼被編譯到共享庫中,或編譯爲加載到進程地址空間中的可執行文件,大型本機應用程序可能僅僅加載就會佔用大量進程地址空間
- 本機代碼必須與Java運行時共享地址空間,任何本機代碼分配或本機代碼執行的內存映射都會耗用Java運行時內存
- 某些JNI函數可能在它們的常規操做中使用本機內存,GetTypeArrayElements和GetTypeArrayRegion函數能夠將Java堆複製到本機內存緩衝區中,提供給本地代碼使用,是否複製數據依賴於運行時實現,經過這種方式訪問大量Java堆數據就可能使用大量的本機內存堆空間
5)NIO
JDK 1.4開始添加了新的I/O類,引入了一種基於通道和緩衝區執行I/O的新方式,就像Java堆上的內存支持I/O緩衝區同樣,NIO添加了對直接ByteBuffer的支持,ByteBuffer受本機內存而不是Java堆的支持,直接ByteBuffer能夠直接傳遞到本機操做系統庫函數,以執行I/O,這種狀況雖然提升了Java程序在I/O的執行效率,可是會對本機內存進行直接的內存開銷。ByteBuffer直接操做和非直接操做的區別以下:
對於在何處存儲直接 ByteBuffer 數據,很容易產生混淆。應用程序仍然在 Java 堆上使用一個對象來編排 I/O 操做,但持有該數據的緩衝區將保存在本機內存中,Java 堆對象僅包含對本機堆緩衝區的引用。非直接 ByteBuffer 將其數據保存在 Java 堆上的 byte[] 數組中。直接ByteBuffer對象會自動清理本機緩衝區,但這個過程只能做爲Java堆GC的一部分執行,它不會自動影響施加在本機上的壓力。GC僅在Java堆被填滿,以致於沒法爲堆分配請求提供服務的時候,或者在Java應用程序中顯示請求它發生。
6)線程:
應用程序中的每一個線程都須要內存來存儲器堆棧(用於在調用函數時持有局部變量並維護狀態的內存區域)。每一個 Java 線程都須要堆棧空間來運行。根據實現的不一樣,Java 線程能夠分爲
本機線程和
Java 堆棧。除了堆棧空間,每一個線程還須要爲線程
本地存儲(thread-local storage)和內部數據結構提供一些本機內存。儘管每一個線程使用的內存量很是小,但對於擁有數百個線程的應用程序來講,線程堆棧的總內存使用量可能很是大。若是運行的應用程序的線程數量比可用於處理它們的處理器數量多,效率一般很低,而且可能致使糟糕的性能和更高的內存佔用。
ii.本機內存耗盡:
Java運行時善於以不一樣的方式來處理
Java堆空間的耗盡和
本機堆空間的耗盡,可是這兩種情形具備相似症狀,當Java堆空間耗盡的時候,Java應用程序很難正常運行,由於Java應用程序必須經過分配對象來完成工做,只要Java堆被填滿,就會出現糟糕的GC性能,而且拋出OutOfMemoryError。相反,一旦 Java 運行時開始運行而且應用程序處於穩定狀態,它能夠在本機堆徹底耗盡以後繼續正常運行,不必定會發生奇怪的行爲,由於須要分配本機內存的操做比須要分配 Java 堆的操做少得多。儘管須要本機內存的操做因 JVM 實現不一樣而異,但也有一些操做很常見:
啓動線程、
加載類以及
執行某種類型的網絡和文件 I/O。本機內存不足行爲與 Java 堆內存不足行爲也不太同樣,由於沒法對本機堆分配進行控制,儘管全部 Java 堆分配都在 Java 內存管理系統控制之下,但任何本機代碼
(不管其位於 JVM、Java 類庫仍是應用程序代碼中)均可能執行本機內存分配,並且會失敗。嘗試進行分配的代碼而後會處理這種狀況,不管設計人員的意圖是什麼:它可能經過 JNI 接口拋出一個 OutOfMemoryError,在屏幕上輸出一條消息,發生無提示失敗並在稍後再試一次,或者執行其餘操做。
iii.例子:
這篇文章一致都在講概念,這裏既然提到了ByteBuffer,先提供一個簡單的例子演示該類的使用:
——[$]使用NIO讀取txt文件——
package org.susan.java.io;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class ExplicitChannelRead {
public static void main(String args[]){
FileInputStream fileInputStream;
FileChannel fileChannel;
long fileSize;
ByteBuffer byteBuffer;
try{
fileInputStream = new FileInputStream("D://read.txt");
fileChannel = fileInputStream.getChannel();
fileSize = fileChannel.size();
byteBuffer = ByteBuffer.allocate((int)fileSize);
fileChannel.read(byteBuffer);
byteBuffer.rewind();
for( int i = 0; i < fileSize; i++ )
System.out.print((char)byteBuffer.get());
fileChannel.close();
fileInputStream.close();
}catch(IOException ex){
ex.printStackTrace();
}
}
}
在讀取文件的路徑放上該txt文件裏面寫入:Hello World,上邊這段代碼就是
使用NIO的方式讀取文件系統上的文件,這段程序的輸入就爲:
Hello World
——[$]獲取ByteBuffer上的字節轉換爲Byte數組——
package org.susan.java.io;
import java.nio.ByteBuffer;
public class ByteBufferToByteArray {
public static void main(String args[]) throws Exception{
// 從byte數組建立ByteBuffer
byte[] bytes = new byte[10];
ByteBuffer buffer = ByteBuffer.wrap(bytes);
// 在position和limit,也就是ByteBuffer緩衝區的首尾之間讀取字節
bytes = new byte[buffer.remaining()];
buffer.get(bytes, 0, bytes.length);
// 讀取全部ByteBuffer內的字節
buffer.clear();
bytes = new byte[buffer.capacity()];
buffer.get(bytes, 0, bytes.length);
}
}
上邊代碼就是從
ByteBuffer到byte數組的
轉換過程,有了這個過程在開發過程當中可能更加方便,ByteBuffer的詳細講解我保留到IO部分,這裏僅僅是涉及到了一些,因此提供兩段實例代碼。
iv.共享內存:
在Java語言裏面,
沒有共享內存的概念,可是在某些引用中,共享內存卻很受用,例如Java語言的分佈式系統,存着大量的Java分佈式共享對象,不少時候須要查詢這些對象的狀態,以查看系統是否運行正常或者瞭解這些對象目前的一些統計數據和狀態。若是使用的是網絡通訊的方式,顯然會增長應用的額外開銷,也增長了沒必要要的應用編程,若是是共享內存方式,則能夠直接經過共享內存查看到所須要的對象的數據和統計數據,從而減小一些沒必要要的麻煩。
1)共享內存特色:
- 能夠被多個進程打開訪問
- 讀寫操做的進程在執行讀寫操做的時候其餘進程不能進行寫操做
- 多個進程能夠交替對某一個共享內存執行寫操做
- 一個進程執行了內存寫操做事後,不影響其餘進程對該內存的訪問,同時其餘進程對更新後的內存具備可見性
- 在進程執行寫操做時若是異常退出,對其餘進程的寫操做禁止自動解除
- 相對共享文件,數據訪問的方便性和效率
2)出現狀況:
- 獨佔的寫操做,相應有獨佔的寫操做等待隊列。獨佔的寫操做自己不會發生數據的一致性問題;
- 共享的寫操做,相應有共享的寫操做等待隊列。共享的寫操做則要注意防止發生數據的一致性問題;
- 獨佔的讀操做,相應有共享的讀操做等待隊列;
- 共享的讀操做,相應有共享的讀操做等待隊列;
3)Java中共享內存的實現:
JDK 1.4裏面的MappedByteBuffer爲開發人員在Java中實現共享內存提供了良好的方法,該緩衝區其實是
一個磁盤文件的內存映象,兩者的變化會保持同步,即內存數據發生變化事後會當即反應到磁盤文件中,這樣會有效地保證共享內存的實現,將共享文件和磁盤文件簡歷聯繫的是文件通道類:FileChannel,該類的加入是JDK爲了統一外圍設備的訪問方法,而且增強了多線程對同一文件進行存取的安全性,這裏能夠使用它來創建共享內存用,它創建了共享內存和磁盤文件之間的一個通道。打開一個文件可以使用RandomAccessFile類的getChannel方法,該方法直接返回一個文件通道,該文件通道因爲對應的文件設爲隨機存取,一方面能夠進行讀寫兩種操做,另一個方面使用它不會破壞映象文件的內容。這裏,若是使用FileOutputStream和FileInputStream則不能理想地實現共享內存的要求,由於這兩個類同時實現自由讀寫很困難。
下邊代碼段實現了上邊說起的共享內存功能
// 得到一個只讀的隨機存取文件對象
RandomAccessFile RAFile = new RandomAccessFile(filename,"r");
// 得到相應的文件通道
FileChannel fc = RAFile.getChannel();
// 取得文件的實際大小
int size = (int)fc.size();
// 得到共享內存緩衝區,該共享內存只讀
MappedByteBuffer mapBuf = fc.map(FileChannel.MAP_RO,0,size);算法
// 得到一個可讀寫的隨機存取文件對象
RAFile = new RandomAccessFile(filename,"rw");編程
// 得到相應的文件通道
fc = RAFile.getChannel();bootstrap
// 取得文件的實際大小,以便映像到共享內存
size = (int)fc.size();數組
// 得到共享內存緩衝區,該共享內存可讀寫
mapBuf = fc.map(FileChannel.MAP_RW,0,size);緩存
// 獲取頭部消息:存取權限
mode = mapBuf.getInt();
若是多個應用映象使用同一文件名的共享內存,則意味着這多個應用共享了同一內存數據,這些應用對於文件能夠具備同等存取權限,一個應用對數據的刷新會更新到多個應用中。爲了防止多個應用同時對共享內存進行寫操做,能夠在該共享內存的頭部信息加入寫操做標記,該共享文件的頭部基本信息至少有:
共享文件的頭部信息是私有信息,多個應用能夠對同一個共享內存執行寫操做,執行寫操做和結束寫操做的時候,能夠使用以下方法:
public boolean startWrite()
{
if(mode == 0) // 這裏mode表明共享內存的存取模式,爲0表明可寫
{
mode = 1; // 意味着別的應用不可寫
mapBuf.flip();
mapBuf.putInt(mode); //寫入共享內存的頭部信息
return true;
}
else{
return false; //代表已經有應用在寫該共享內存了,本應用不可以針對共享內存再作寫操做
}
}
public boolean stopWrite()
{
mode = 0; // 釋放寫權限
mapBuf.flip();
mapBuf.putInt(mode); //寫入共享內存頭部信息
return true;
}
【*:上邊提供了對共享內存執行寫操做過程的兩個方法,這兩個方法其實理解起來很簡單,真正須要思考的是一個針對
存取模式的設置,其實這種機制和最前面提到的內存的
鎖模式有點相似,一旦當mode(存取模式)設置稱爲可寫的時候,startWrite才能返回true,不只僅如此,某個應用程序在向共享內存寫入數據的時候還會修改其存取模式,由於若是不修改的話就會致使其餘應用一樣針對該內存是可寫的,這樣就使得共享內存的實現變得混亂,而在中止寫操做stopWrite的時候,須要將mode設置稱爲1,也就是上邊註釋段提到的
釋放寫權限。】
關於鎖的知識這裏簡單作個補充【*:上邊代碼的這種模式能夠理解爲一種簡單的鎖模式】:通常狀況下,計算機編程中會常常遇到鎖模式,在整個鎖模式過程當中能夠將鎖分爲兩類(這裏只是輔助理解,不是嚴格的鎖分類)——
共享鎖和
排他鎖(也稱爲獨佔鎖),鎖的定位是定位於針對全部與計算機有關的資源好比內存、文件、存儲空間等,針對這些資源均可能出現鎖模式。在上邊堆和棧一節講到了Java對象鎖,其實不只僅是對象,只要是計算機中會出現
寫入和讀取共同操做的資源,都有可能出現鎖模式。
共享鎖——當應用程序得到了資源的共享鎖的時候,那麼應用程序就能夠直接訪問該資源,資源的共享鎖能夠被多個應用程序拿到,在Java裏面線程之間有時候也存在對象的共享鎖,可是有一個很明顯的特徵,也就是內存共享鎖
只能讀取數據,不可以寫入數據,不管是什麼資源,當應用程序僅僅只能拿到該資源的共享鎖的時候,是不可以針對該資源進行寫操做的。
獨佔鎖——當應用程序得到了資源的獨佔鎖的時候,應用程序訪問該資源在共享鎖上邊多了一個權限就是寫權限,針對資源自己而言,
一個資源只有一把獨佔鎖,也就是說一個資源只能同時被一個應用或者一個執行代碼程序容許寫操做,Java線程中的對象寫操做也是這個道理,若某個應用拿到了獨佔鎖的時候,不只僅能夠讀取資源裏面的數據,並且能夠向該資源進行數據寫操做。
數據一致性——當資源同時被應用進行讀寫訪問的時候,有可能會出現數據一致性問題,好比A應用拿到了資源R1的獨佔鎖,B應用拿到了資源R1的共享鎖,A在針對R1進行寫操做,而兩個應用的操做——A的寫操做和B的讀操做出現了一個時間差,s1的時候B讀取了R1的資源,s2的時候A寫入了數據修改了R1的資源,s3的時候B又進行了第二次讀,而兩次讀取相隔時間比較短暫並且初衷沒有考慮到A在B的讀取過程修改了資源,這種狀況下針對鎖模式就須要考慮到數據一致性問題。獨佔鎖的排他性在這裏的意思是該鎖只能被一個應用獲取,獲取過程只能由這個應用寫入數據到資源內部,除非它釋放該鎖,不然其餘拿不到鎖的應用是沒法對資源進行寫入操做的。
按照上邊的思路去理解代碼裏面實現共享內存的過程就更加容易理解了。
若是執行寫操做的應用異常停止,那麼映像文件的共享內存將再也不能執行寫操做。爲了在應用異常停止後,寫操做禁止標誌自動消除,必須讓運行的應用獲知退出的應用。在多線程應用中,能夠用同步方法得到這樣的效果,可是在多進程中,同步是不起做用的。方法能夠採用的多種技巧,這裏只是描述一可能的實現:採用文件鎖的方式。寫共享內存應用在得到對一個共享內存寫權限的時候,除了判斷
頭部信息的寫權限標誌外,還要判斷一個臨時的鎖文件是否能夠獲得,若是能夠獲得,則即便頭部信息的寫權限標誌爲1(上述),也能夠
啓動寫權限,其實這已經代表寫權限得到的應用已經異常退出,這段代碼以下:
// 打開一個臨時文件,注意統一共享內存,該文件名必須相同,能夠在共享文件名後邊添加「.lock」後綴
RandomAccessFile files = new RandomAccessFile("memory.lock","rw");
// 獲取文件通道
FileChannel lockFileChannel = files.getChannel();
// 獲取文件的獨佔鎖,該方法不產生任何阻塞直接返回
FileLock fileLock = lockFileChannel.tryLock();
// 若是爲空表示已經有應用佔有了
if( fileLock == null ){
// ...不可寫
}else{
// ...能夠執行寫操做
}
4)共享內存的應用:
在Java中,共享內存通常有兩種應用:
[1]永久對象配置——在java服務器應用中,用戶可能會在運行過程當中配置一些參數,而這些參數須要永久 有效,當服務器應用從新啓動後,這些配置參數仍然能夠對應用起做用。這就能夠用到該文 中的共享內存。該共享內存中保存了服務器的運行參數和一些對象運行特性。能夠在應用啓動時讀入以啓用之前配置的參數。
[2]查詢共享數據——一個應用(例 sys.java)是系統的服務進程,其系統的運行狀態記錄在共享內存中,其中運行狀態多是不斷變化的。爲了隨時瞭解系統的運行狀態,啓動另外一個應用(例 mon.java),該應用查詢該共享內存,彙報系統的運行狀態。
v.小節:
提供本機內存以及共享內存的知識,主要是爲了讓讀者可以更順利地理解JVM內部內存模型的物理原理,包括JVM如何和操做系統在內存這個級別進行交互,理解了這些內容就讓讀者對Java內存模型的認識會更加深刻,並且不容易遺忘。其實Java的內存模型遠不及咱們想象中那麼簡單,並且其結構極端複雜,看過《Inside JVM》的朋友應該就知道,結合JVM指令集去寫點小代碼測試.class文件的裏層結構也不失爲一種好玩的學習方法。
4.防止內存泄漏
Java中會有內存泄漏,聽起來彷佛是很不正常的,由於Java提供了垃圾回收器針對內存進行自動回收,可是Java仍是會出現內存泄漏的。
i.什麼是Java中的內存泄漏:
在Java語言中,
內存泄漏就是存在一些被分配的對象,這些對象有兩個特色:
這些對象可達,即在對象內存的有向圖中存在通路能夠與其相連;其次,這些對象是無用的,即程序之後不會再使用這些對象了。若是對象知足這兩個條件,該對象就能夠斷定爲Java中的內存泄漏,這些對象不會被GC回收,然而它卻佔用內存,這就是
Java語言中的內存泄漏。Java中的內存泄漏和C++中的內存泄漏還存在必定的區別,在C++裏面,內存泄漏的範圍更大一些,有些對象被分配了內存空間,可是卻不可達,因爲C++中沒有GC,這些內存將會永遠收不回來,在Java中這些不可達對象則是被GC負責回收的,所以程序員不須要考慮這一部分的內存泄漏。兩者的圖以下:
所以按照上邊的分析,Java語言中也是
存在內存泄漏的,可是其內存泄漏範圍比C++要小不少,由於Java裏面有個特殊程序回收全部的不可達對象:
垃圾回收器。對於程序員來講,GC基本是透明的,不可見的。雖然,咱們只有幾個函數能夠訪問GC,例如運行GC的函數System.gc(),可是根據Java語言規範定義,該函數
不保證JVM的垃圾收集器必定會執行。由於,不一樣的JVM實現者可能使用不一樣的算法管理GC。一般,GC的線程的優先級別較低,JVM調用GC的策略也有不少種,有的是內存使用到達必定程度時,GC纔開始工做,也有
定時執行的,有的是
平緩執行GC,有的是
中斷式執行GC。但一般來講,咱們不須要關心這些。除非在一些特定的場合,GC的執行影響應用程序的性能,例如對於基於Web的實時系統,如網絡遊戲等,用戶不但願GC忽然中斷應用程序執行而進行垃圾回收,那麼咱們須要調整GC的參數,讓GC可以經過平緩的方式釋放內存,例如將垃圾回收分解爲一系列的小步驟執行,Sun提供的HotSpot JVM就支持這一特性。
舉個例子:
——[$]內存泄漏的例子——
package org.susan.java.collection;
import java.util.Vector;
public class VectorMemoryLeak {
public static void main(String args[]){
Vector<String> vector = new Vector<String>();
for( int i = 0; i < 1000; i++ ){
String tempString = new String();
vector.add(tempString);
tempString = null;
}
}
}
從上邊這個例子能夠看到,循環申請了String對象,而且將申請的對象放入了一個Vector中,若是僅僅是釋放對象自己,由於Vector仍然引用了該對象,因此這個對象對CG來講是不可回收的,所以若是對象加入到Vector後,還必須從Vector刪除纔可以回收,最簡單的方式是將
Vector引用設置成null。實際上這些對象已經沒有用了,可是仍是被代碼裏面的引用引用到了,這種狀況GC拿它就沒有了任何辦法,這樣就能夠致使了內存泄漏。
【*:Java語言由於提供了垃圾回收器,照理說是不會出現內存泄漏的,Java裏面致使內存泄漏的主要緣由就是,先前申請了內存空間而忘記了釋放。若是程序中存在對無用對象的引用,這些對象就會駐留在內存中消耗內存,由於沒法讓GC判斷這些對象是否可達。若是存在對象的引用,這個對象就被定義爲「有效的活動狀態」,同時不會被釋放,要肯定對象所佔內存被回收,必需要確認該對象再也不被使用。典型的作法就是把對象數據成員設置成爲null或者中集合中移除,當局部變量不須要的狀況則不須要顯示聲明爲null。】
ii.常見的Java內存泄漏
1)全局集合:
在大型應用程序中存在各類各樣的全局數據倉庫是很廣泛的,好比一個JNDI樹或者一個Session table(會話表),在這些狀況下,必須注意管理
存儲庫的大小,必須有某種機制從存儲庫中
移除再也不須要的數據。
[$]解決:
[1]經常使用的解決方法是週期運做清除做業,該做業會驗證倉庫中的數據而後清楚一切不須要的數據
[2]另一種方式是
反向連接計數,集合負責統計集合中每一個入口的反向連接數據,這要求反向連接告訴集合合適會退出入口,當反向連接數目爲零的時候,該元素就能夠移除了。
2)緩存:
緩存一種用來快速查找已經執行過的操做結果的數據結構。所以,若是一個操做執行須要比較多的資源並會屢次被使用,一般作法是把經常使用的輸入數據的操做結果進行緩存,以便在下次調用該操做時使用緩存的數據。緩存一般都是以動態方式實現的,若是緩存設置不正確而大量使用緩存的話則會出現內存溢出的後果,所以須要將所使用的內存容量與檢索數據的速度加以平衡。
[$]解決:
[1]經常使用的解決途徑是使用java.lang.ref.SoftReference類堅持將對象放入緩存,這個方法能夠保證當虛擬機用完內存或者須要更多堆的時候,能夠釋放這些對象的引用。
3)類加載器:
Java類裝載器的使用爲內存泄漏提供了許多可乘之機。通常來講類裝載器都具備複雜結構,由於類裝載器不只僅是隻與"常規"對象引用有關,同時也和對象內部的引用有關。好比
數據變量,
方法和
各類類。這意味着只要存在對數據變量,方法,各類類和對象的類裝載器,那麼類裝載器將駐留在JVM中。既然類裝載器能夠同不少的類關聯,同時也能夠和靜態數據變量關聯,那麼至關多的內存就可能發生泄漏。
iii.Java引用【摘錄自前邊的《Java引用總結》】:
Java中的對象引用主要有如下幾種類型:
1)強可及對象(strongly reachable):
能夠經過強引用訪問的對象,通常來講,咱們平時寫代碼的方式都是使用的
強引用對象,好比下邊的代碼段:
StringBuilder builder= new StringBuilder();
上邊代碼部分引用obj這個引用將引用內存堆中的一個對象,這種狀況下,只要obj的引用存在,垃圾回收器就永遠不會釋放該對象的存儲空間。這種對象咱們又成爲
強引用(Strong references),這種強引用方式就是Java語言的原生的Java引用,咱們幾乎天天編程的時候都用到。上邊代碼JVM存儲了一個StringBuilder類型的對象的強引用在變量builder呢。強引用和GC的交互是這樣的,若是一個對象經過強引用可達或者經過強引用鏈可達的話這種對象就成爲強可及對象,這種狀況下的對象垃圾回收器不予理睬。若是咱們開發過程不須要垃圾回器回收該對象,就直接將該對象賦爲強引用,也是普通的編程方法。
2)軟可及對象(softly reachable):
不經過強引用訪問的對象,即不是強可及對象,可是能夠經過
軟引用訪問的對象就成爲
軟可及對象,軟可及對象就須要使用類SoftReference(java.lang.ref.SoftReference)。此種類型的引用主要用於
內存比較敏感的高速緩存,並且此種引用仍是具備較強的引用功能,當內存不夠的時候GC會回收這類內存,所以若是
內存充足的時候,這種引用一般不會被回收的。不只僅如此,這種引用對象在JVM裏面
保證在拋出OutOfMemory異常以前,設置成爲null。通俗地講,這種類型的引用保證在JVM內存不足的時候所有被清除,可是有個關鍵在於:垃圾收集器在運行時是否釋放軟可及對象是不肯定的,並且使用垃圾回收算法並不能保證一次性尋找到全部的軟可及對象。當垃圾回收器每次運行的時候均可以隨意釋放不是強可及對象佔用的內存,若是垃圾回收器找到了軟可及對象事後,可能會進行如下操做:
- 將SoftReference對象的referent域設置成爲null,從而使該對象再也不引用heap對象。
- SoftReference引用過的內存堆上的對象一概被生命爲finalizable。
- 當內存堆上的對象finalize()方法被運行並且該對象佔用的內存被釋放,SoftReference對象就會被添加到它的ReferenceQueue,前提條件是ReferenceQueue自己是存在的。
既然Java裏面存在這樣的對象,那麼咱們在編寫代碼的時候如何建立這樣的對象呢?建立步驟以下:
先建立一個對象,並使用普通引用方式
【強引用】,而後再
建立一個SoftReference來引用該對象,最後將普通引用
設置爲null,經過這樣的方式,這個對象就僅僅保留了一個SoftReference引用,同時這種狀況咱們所建立的對象就是SoftReference對象。通常狀況下,咱們能夠使用該引用來完成Cache功能,就是前邊說的用於高速緩存,保證最大限度使用內存而不會引發內存泄漏的狀況。下邊的代碼段:
public static void main(String args[])
{
//建立一個強可及對象
A a = new A();
//建立這個對象的軟引用SoftReference
SoftReference sr = new SoftReference(a);
//將強引用設置爲空,以遍垃圾回收器回收強引用
a = null;
//下次使用該對象的操做
if( sr != null ){
a = (A)sr.get();
}else{
//這種狀況就是因爲內存太低,已經將軟引用釋放了,所以須要從新裝載一次
a = new A();
sr = new SoftReference(a);
}
}
軟引用技術使得Java系統能夠更好地管理內存,保持系統穩定,防止內存泄漏,避免系統崩潰,所以在處理一些內存佔用大並且生命週期長使用不頻繁的對象能夠使用該技術。
3)弱可及對象(weakly reachable):
不是強可及對象一樣也不是軟可及對象,僅僅經過弱引用WeakReference(java.lang.ref.WeakReference)訪問的對象,這種對象的用途在於
規範化映射(canonicalized mapping),對於生存週期相對比較長並且從新建立的時候開銷少的對象,弱引用也比較有用,和軟引用對象不一樣的是,垃圾回收器若是碰到了弱可及對象,將釋放WeakReference對象的內存,可是垃圾回收器須要運行不少次纔可以
找到
弱可及對象。弱引用對象在使用的時候,能夠配合ReferenceQueue類使用,若是弱引用被回收,JVM就會把這個弱引用加入到相關的引用隊列中去。最簡單的弱引用方法如如下代碼:
WeakReference weakWidget = new WeakReference(classA);
在上邊代碼裏面,當咱們使用weakWidget.get()來獲取classA的時候,因爲弱引用自己是沒法阻止垃圾回收的,因此咱們也許會拿到一個
null爲返回。【*:這裏提供一個小技巧,若是咱們但願取得某個對象的信息,可是又不影響該對象的垃圾回收過程,咱們就能夠使用WeakReference來記住該對象,通常咱們在開發調試器和優化器的時候使用這個是很好的一個手段。】
若是上邊的代碼部分,咱們經過weakWidget.get()返回的是null就證實該對象已經被垃圾回收器回收了,而這種狀況下弱引用對象就失去了使用價值,GC就會定義爲須要進行清除工做。這種狀況下弱引用沒法引用任何對象,因此在JVM裏面就成爲了一個
死引用,這就是爲何咱們有時候須要經過ReferenceQueue類來配合使用的緣由,使用了ReferenceQueue事後,就使得咱們更加容易監視該引用的對象,若是咱們經過一ReferenceQueue類來構造一個弱引用,當弱引用的對象已經被回收的時候,系統將自動使用對象引用隊列來代替對象引用,並且咱們能夠經過ReferenceQueue類的運行來決定是否真正要從垃圾回收器裏面將該
死引用(Dead Reference)清除。
弱引用代碼段:
//建立普通引用對象
MyObject object = new MyObject();
//建立一個引用隊列
ReferenceQueue rq = new ReferenceQueue();
//使用引用隊列建立MyObject的弱引用
WeakReference wr = new WeakReference(object,rq);
這裏提供兩個實在的場景來描述弱引用的相關用法:
[1]你想給對象附加一些信息,因而你用一個 Hashtable 把對象和附加信息關聯起來。你不停的把對象和附加信息放入 Hashtable 中,可是當對象用完的時候,你不得不把對象再從 Hashtable 中移除,不然它佔用的內存變不會釋放。萬一你忘記了,那麼沒有從 Hashtable 中移除的對象也能夠算做是內存泄漏。理想的情況應該是當對象用完時,Hashtable 中的對象會自動被垃圾收集器回收,否則你就是在作垃圾回收的工做。
[2]你想實現一個圖片緩存,由於加載圖片的開銷比較大。你將圖片對象的引用放入這個緩存,以便之後可以從新使用這個對象。可是你必須決定緩存中的哪些圖片再也不須要了,從而將引用從緩存中移除。無論你使用什麼管理緩存的算法,你實際上都在處理垃圾收集的工做,更簡單的辦法(除非你有特殊的需求,這也應該是最好的辦法)是讓垃圾收集器來處理,由它來決定回收哪一個對象。
當Java回收器遇到了弱引用的時候有可能會執行如下操做:
- 將WeakReference對象的referent域設置成爲null,從而使該對象再也不引用heap對象。
- WeakReference引用過的內存堆上的對象一概被生命爲finalizable。
- 當內存堆上的對象finalize()方法被運行並且該對象佔用的內存被釋放,WeakReference對象就會被添加到它的ReferenceQueue,前提條件是ReferenceQueue自己是存在的。
4)清除:
當引用對象的referent域設置爲null,而且引用類在內存堆中引用的對象聲明爲可結束的時候,該對象就能夠清除,清除不作過多的講述
5)虛可及對象(phantomly reachable):
不是
強可及對象,也不是
軟可及對象,一樣不是
弱可及對象,之因此把虛可及對象放到最後來說,主要也是由於它的特殊性,有時候咱們又稱之爲
「幽靈對象」,已經結束的,能夠經過虛引用來訪問該對象。咱們使用類PhantomReference(java.lang.ref.PhantomReference)來訪問,這個類只能用於跟蹤被引用對象進行的收集,一樣的,能夠用於執行per-mortern清除操做。PhantomReference必須與ReferenceQueue類一塊兒使用。須要使用ReferenceQueue是由於它可以充當通知機制,當垃圾收集器肯定了某個對象是虛可及對象的時候,PhantomReference對象就被放在了它的ReferenceQueue上,這就是一個通知,代表PhantomReference引用的對象已經結束,能夠收集了,通常狀況下咱們恰好在對象內存在回收以前採起該行爲。這種引用不一樣於弱引用和軟引用,這種方式經過get()獲取到的對象老是返回null,僅僅當這些對象在ReferenceQueue隊列裏面的時候,咱們能夠知道它所引用的哪些對對象是死引用(Dead Reference)。而這種引用和弱引用的區別在於:
弱引用(WeakReference)是在對象不可達的時候儘快進入ReferenceQueue隊列的,在finalization方法執行和垃圾回收以前是確實會發生的,理論上這類對象是不正確的對象,可是WeakReference對象能夠繼續保持Dead狀態,
虛引用(PhantomReference)是在對象確實已經從物理內存中移除事後才進入的ReferenceQueue隊列,並且get()方法會一直返回null
當垃圾回收器遇到了虛引用的時候將有可能執行如下操做:
- PhantomReference引用過的heap對象聲明爲finalizable;
- 虛引用在堆對象釋放以前就添加到了它的ReferenceQueue裏面,這種狀況使得咱們能夠在堆對象被回收以前採起操做【*:再次提醒,PhantomReference對象必須通過關聯的ReferenceQueue來建立,就是說必須和ReferenceQueue類配合操做】
看似沒有用處的虛引用,有什麼用途呢?
- 首先,咱們能夠經過虛引用知道對象究竟何時真正從內存裏面移除的,並且這也是惟一的途徑。
- 虛引用避過了finalize()方法,由於對於此方法的執行而言,虛引用真正引用到的對象是異常對象,若在該方法內要使用對象只能重建。通常狀況垃圾回收器會輪詢兩次,一次標記爲finalization,第二次進行真實的回收,而每每標記工做不能實時進行,或者垃圾回收其會等待一個對象去標記finalization。這種狀況頗有可能引發MemoryOut,而使用虛引用這種狀況就會徹底避免。由於虛引用在引用對象的過程不會去使得這個對象由Dead復活,並且這種對象是能夠在回收週期進行回收的。
在JVM內部,虛引用比起使用finalize()方法更加安全一點並且更加有效。而finaliaze()方法回收在虛擬機裏面實現起來相對簡單,並且也能夠處理大部分工做,因此咱們仍然使用這種方式來進行對象回收的掃尾操做,可是有了虛引用事後咱們能夠選擇是否手動操做該對象使得程序更加高效完美。
iv.防止內存泄漏[來自IBM開發中心]:
1)使用軟引用阻止泄漏:
[1]在Java語言中有一種形式的內存泄漏稱爲對象遊離(Object Loitering):
——[$]對象遊離——
// 注意,這段代碼屬於概念說明代碼,實際應用中不要模仿
public class LeakyChecksum{
private byte[] byteArray;
public synchronized int getFileCheckSum(String filename)
{
int len = getFileSize(filename);
if( byteArray == null || byteArray.length < len )
byteArray = new byte[len];
readFileContents(filename,byteArray);
// 計算該文件的值而後返回該對象
}
}
上邊的代碼是類LeakyChecksum用來講明對象遊離的概念,裏面有一個getFileChecksum()方法用來計算文件內容
校驗和,getFileCheckSum方法將文件內容讀取到緩衝區中計算校驗和,更加直觀的實現就是簡單地將緩衝區做爲getFileChecksum中的本地變量分配,可是上邊這個版本比這種版本更加「聰明」,不是將緩衝區緩衝在實例中字段中減小內存churn。該
「優化」一般不帶來預期的好處,對象分配比不少人指望的更加便宜。(還要注意,將緩衝區從本地變量提高到實例變量,使得類若不帶有附加的同步,就再也不是線程安全的了。直觀的實現不須要將 getFileChecksum() 聲明爲 synchronized,而且會在同時調用時提供更好的可伸縮性。)
這個類存在不少的問題,可是咱們着重來看內存泄漏。緩存緩衝區的決定極可能是根據這樣的假設得出的,即該類將在一個程序中被調用許屢次,所以它應該更加有效,以重用緩衝區而不是從新分配它。可是結果是,緩衝區永遠不會被釋放,由於它對程序來講老是可及的(除非LeakyChecksum對象被垃圾收集了)。更壞的是,它能夠增加,卻不能夠縮小,因此 LeakyChecksum 將永久保持一個與所處理的最大文件同樣大小的緩衝區。退一萬步說,這也會給垃圾收集器帶來壓力,而且要求更頻繁的收集;爲計算將來的校驗和而保持一個大型緩衝區並非可用內存的最有效利用。LeakyChecksum 中問題的緣由是,緩衝區對於 getFileChecksum() 操做來講邏輯上是本地的,可是它的生命週期已經被人爲延長了,由於將它提高到了實例字段。所以,該類必須本身管理緩衝區的生命週期,而不是讓 JVM 來管理。
這裏能夠提供一種策略就是使用Java裏面的軟引用:
弱引用如何能夠給應用程序提供當對象被程序使用時另外一種到達該對象的方法,可是不會延長對象的生命週期。Reference 的另外一個子類——軟引用——可知足一個不一樣卻相關的目的。其中
弱引用容許應用程序建立不妨礙垃圾收集的引用,
軟引用容許應用程序經過將一些對象指定爲 「expendable」 而利用垃圾收集器的幫助。儘管垃圾收集器在找出哪些內存在由應用程序使用哪些沒在使用方面作得很好,可是肯定可用內存的最適當使用仍是取決於應用程序。若是應用程序作出了很差的決定,使得對象被保持,那麼性能會受到影響,由於垃圾收集器必須更加辛勤地工做,以防止應用程序消耗掉全部內存。
高速緩存是一種常見的性能優化,容許應用程序重用之前的計算結果,而不是從新進行計算。高速緩存是 CPU 利用和內存使用之間的一種折衷,這種折衷理想的平衡狀態取決於有多少內存可用。若高速緩存太少,則所要求的性能優點沒法達到;若太多,則性能會受到影響,由於太多的內存被用於高速緩存上,致使其餘用途沒有足夠的可用內存。由於垃圾收集器比應用程序更適合決定內存需求,因此應該利用垃圾收集器在作這些決定方面的幫助,這就是件引用所要作的。若是一個對象唯一剩下的引用是
弱引用或軟引用,那麼該對象是
軟可及的(softly reachable)。垃圾收集器並不像其收集弱可及的對象同樣儘可能地收集軟可及的對象,相反,它只在真正
「須要」 內存時才收集軟可及的對象。軟引用對於垃圾收集器來講是這樣一種方式,即 「只要內存不太緊張,我就會保留該對象。可是若是內存變得真正緊張了,我就會去收集並處理這個對象。」 垃圾收集器在能夠拋出OutOfMemoryError 以前須要清除全部的軟引用。經過使用一個軟引用來管理高速緩存的緩衝區,能夠解決 LeakyChecksum中的問題,如上邊代碼所示。如今,只要不是特別須要內存,
緩衝區就會被
保留,可是在須要時,也可被垃圾收集器回收:
——[$]使用軟引用修復上邊代碼段——
public class CachingChecksum
{
private SoftReference<byte[]> bufferRef;
public synchronized int getFileChecksum(String filename)
{
int len = getFileSize(filename);
byte[] byteArray = bufferRef.get();
if( byteArray == null || byteArray.length < len )
{
byteArray = new byte[len];
bufferRef.set(byteArray);
}
readFileContents(filename,byteArray);
}
}
一種廉價緩存:
CachingChecksum使用一個
軟引用來緩存單個對象,並讓 JVM 處理從緩存中取走對象時的細節。相似地,軟引用也常常用於 GUI 應用程序中,用於
緩存位圖圖形。是否可以使用軟引用的關鍵在於,應用程序是否可從大量緩存的數據恢復。若是須要緩存不止一個對象,您能夠使用一個 Map,可是能夠選擇如何使用軟引用。您能夠將緩存做爲 Map<K, SoftReference<V>> 或SoftReference<Map<K,V>> 管理。後一種選項一般更好一些,由於它給垃圾收集器帶來的工做更少,而且容許在特別須要內存時以較少的工做回收整個緩存。弱引用有時會錯誤地用於取代軟引用,用於構建緩存,可是這會致使差的緩存性能。在實踐中,弱引用將在對象變得弱可及以後被很快地清除掉——一般是在緩存的對象再次用到以前——由於小的垃圾收集運行得很頻繁。對於在性能上很是依賴高速緩存的應用程序來講,
軟引用是一個
無論用的手段,它確實不能取代可以提供
靈活終止期、
複製和
事務型高速緩存的複雜的
高速緩存框架。可是做爲一種 「
廉價(cheap and dirty)」 的高速緩存機制,它對於下降價格是頗有吸引力的。正如弱引用同樣,軟引用也可建立爲具備一個相關的引用隊列,引用在被垃圾收集器清除時進入隊列。引用隊列對於軟引用來講,沒有對弱引用那麼有用,可是它們能夠用於發出
管理警報,說明應用程序開始
缺乏內存。
2)垃圾回收對引用的處理:
弱引用和軟引用都擴展了抽象的 Reference 類
虛引用(
phantom references
),引用對象被垃圾收集器特殊地看待。垃圾收集器在跟蹤堆期間遇到一個 Reference 時,不會標記或跟蹤該引用對象,而是在已知活躍的 Reference 對象的隊列上放置一個 Reference。在跟蹤以後,垃圾收集器就識別軟可及的對象——這些對象上除了軟引用外,沒有任何強引用。垃圾收集器而後根據當前收集所回收的內存總量和其餘策略考慮因素,判斷軟引用此時是否須要被清除。將被清除的軟引用若是具備相應的引用隊列,就會進入隊列。其他的軟可及對象
(沒有清除的對象)而後被看做一個
根集(root set),堆跟蹤繼續使用這些新的根,以便經過活躍的軟引用而可及的對象可以被標記。處理軟引用以後,弱可及對象的集合被識別 —— 這樣的對象上不存在強引用或軟引用。這些對象被清除和加入隊列。全部 Reference 類型在加入隊列
以前被清除,因此處理過後檢查(post-mortem)清除的線程永遠不會具備 referent 對象的訪問權,而只具備Reference 對象的訪問權。所以,當 References 與引用隊列一塊兒使用時,一般須要細分適當的引用類型,並將它直接用於您的設計中(與 WeakHashMap 同樣,它的 Map.Entry 擴展了 WeakReference)或者存儲對須要清除的實體的引用。
3)使用弱引用堵住內存泄漏:
[1]全局Map形成的內存泄漏:
無心識對象保留最多見的緣由是使用 Map 將元數據與
臨時對象(transient object)相關聯。假定一個對象具備中等生命週期,比分配它的那個方法調用的生命週期長,可是比應用程序的生命週期短,如客戶機的套接字鏈接。須要將一些元數據與這個套接字關聯,如生成鏈接的用戶的標識。在建立 Socket 時是不知道這些信息的,而且不能將數據添加到 Socket 對象上,由於不能控制 Socket 類或者它的子類。這時,典型的方法就是在一個全局 Map 中存儲這些信息:
public class SocketManager{
private Map<Socket,User> m = new HashMap<Socket,User>();
public void setUser(Socket s,User u)
{
m.put(s,u);
}
public User getUser(Socket s){
return m.get(s);
}
public void removeUser(Socket s){
m.remove(s);
}
}
SocketManager socketManager;
//...
socketManager.setUser(socket,user);
這種方法的問題是元數據的生命週期須要與套接字的生命週期掛鉤,可是除非準確地知道何時程序再也不須要這個套接字,並記住從 Map 中刪除相應的映射,不然,Socket 和 User 對象將會永遠留在 Map 中,遠遠超過響應了請求和關閉套接字的時間。這會阻止 Socket 和User 對象被垃圾收集,即便應用程序不會再使用它們。這些對象留下來不受控制,很容易形成程序在長時間運行後內存爆滿。除了最簡單的狀況,在幾乎全部狀況下找出何時 Socket 再也不被程序使用是一件很煩人和容易出錯的任務,須要人工對內存進行管理。
[2]弱引用內存泄漏代碼:
程序有內存泄漏的第一個跡象一般是它拋出一個 OutOfMemoryError,或者由於頻繁的垃圾收集而表現出糟糕的性能。幸運的是,垃圾收集能夠提供可以用來診斷內存泄漏的大量信息。若是以 -verbose:gc 或者 -Xloggc 選項調用 JVM,那麼每次 GC 運行時在控制檯上或者日誌文件中會打印出一個診斷信息,包括它所花費的時間、當前堆使用狀況以及恢復了多少內存。記錄 GC 使用狀況並不具備干擾性,所以若是須要分析內存問題或者調優垃圾收集器,在生產環境中默認啓用 GC 日誌是值得的。有工具能夠利用 GC 日誌輸出並以圖形方式將它顯示出來,
JTune 就是這樣的一種工具。觀察 GC 以後堆大小的圖,能夠看到程序內存使用的趨勢。對於大多數程序來講,能夠將內存使用分爲兩部分:baseline 使用和 current load 使用。對於服務器應用程序,baseline 使用就是應用程序在沒有任何負荷、可是已經準備好接受請求時的內存使用,current load 使用是在處理請求過程當中使用的、可是在請求處理完成後會釋放的內存。只要負荷大致上是恆定的,應用程序一般會很快達到一個穩定的內存使用水平。若是在應用程序已經完成了其初始化而且負荷沒有增長的狀況下,內存使用持續增長,那麼程序就可能在處理前面的請求時保留了生成的對象。
public class MapLeaker{
public ExecuteService exec = Executors.newFixedThreadPool(5);
public Map<Task,TaskStatus> taskStatus
= Collections.synchronizedMap(new HashMap<Task,TaskStatus>());
private Random random = new Random();
private enum TaskStatus {
NOT_STARTED,
STARTED,
FINISHED };
private class Task implements Runnable{
private int[] numbers = new int[random.nextInt(200)];
public void run()
{
int[] temp = new int[random.nextInt(10000)];
taskStatus.put(this,TaskStatus.
STARTED);
doSomework();
taskStatus.put(this,TaskStatus.
FINISHED);
}
}
public Task newTask()
{
Task t = new Task();
taskStatus.put(t,TaskStatus.
NOT_STARTED);
exec.execute(t);
return t;
}
}
[3]使用弱引用堵住內存泄漏:
SocketManager 的問題是 Socket-User 映射的生命週期應當與 Socket 的生命週期相匹配,可是語言沒有提供任何容易的方法實施這項規則。這使得程序不得不使用人工內存管理的老技術。幸運的是,從
JDK 1.2 開始,垃圾收集器提供了一種聲明這種對象生命週期依賴性的方法,這樣垃圾收集器就能夠幫助咱們防止這種內存泄漏——
利用弱引用。弱引用是對一個對象
(稱爲 referent)的引用的持有者。使用弱引用後,能夠維持對 referent 的引用,而不會阻止它被垃圾收集。當垃圾收集器跟蹤堆的時候,若是對一個對象的引用只有弱引用,那麼這個 referent 就會成爲垃圾收集的候選對象,就像沒有任何剩餘的引用同樣,並且全部剩餘的弱引用都被清除。
(只有弱引用的對象稱爲弱可及(weakly reachable))WeakReference 的 referent 是在構造時設置的,在沒有被清除以前,能夠用 get() 獲取它的值。若是弱引用被清除了
(無論是 referent 已經被垃圾收集了,仍是有人調用了 WeakReference.clear()),get() 會返回
null。相應地,在使用其結果以前,應當老是檢查get() 是否返回一個非
null 值,由於 referent 最終老是會被垃圾收集的。用一個普通的(強)引用拷貝一個對象引用時,限制 referent 的生命週期至少與被拷貝的引用的生命週期同樣長。若是不當心,那麼它可能就與程序的生命週期同樣——若是將一個對象放入一個全局集合中的話。另外一方面,在建立對一個對象的弱引用時,徹底沒有擴展 referent 的生命週期,只是在對象仍然存活的時候,保持另外一種到達它的方法。弱引用對於構造弱集合最有用,如那些在應用程序的其他部分使用對象期間存儲關於這些對象的元數據的集合——這就是 SocketManager 類所要作的工做。由於這是弱引用最多見的用法,WeakHashMap 也被添加到
JDK 1.2 的類庫中,它對鍵(而不是對值)使用弱引用。若是在一個普通 HashMap 中用一個對象做爲鍵,那麼這個對象在映射從 Map 中刪除以前不能被回收,WeakHashMap 使您能夠用一個對象做爲 Map 鍵,同時不會阻止這個對象被垃圾收集。下邊的代碼給出了 WeakHashMap 的 get() 方法的一種可能實現,它展現了弱引用的使用:
public class WeakHashMap<K,V> implements Map<K,V>
{
private static class Entry<K,V> extends WeakReference<K> implements Map.Entry<K,V>
{
private V value;
private final int hash;
private Entry<K,V> next;
// ...
}
public V get(Object key)
{
int hash = getHash(key);
Entry<K,V> e = getChain(hash);
while(e != null)
{
k eKey = e.get();
if( e.hash == hash && (key == eKey || key.equals(eKey)))
return e.value;
e = e.next;
}
return null;
}
}
調用 WeakReference.get() 時,
它返回一個對 referent 的強引用(若是它仍然存活的話),所以不須要擔憂映射在 while 循環體中消失,由於強引用會防止它被垃圾收集。WeakHashMap 的實現展現了弱引用的一種常見用法——一些內部對象擴展 WeakReference。其緣由在下面一節討論引用隊列時會獲得解釋。在向 WeakHashMap 中添加映射時,請記住映射可能會在之後「脫離」,由於鍵被垃圾收集了。在這種狀況下,get() 返回 null,這使得測試 get() 的返回值是否爲 null 變得比平時更重要了。
[4]使用WeakHashMap堵住泄漏
在 SocketManager 中防止泄漏很容易,只要用 WeakHashMap 代替 HashMap 就好了,以下邊代碼所示。
(若是 SocketManager 須要線程安全,那麼能夠用 Collections.synchronizedMap() 包裝 WeakHashMap)。當映射的生命週期必須與鍵的生命週期聯繫在一塊兒時,能夠使用這種方法。不過,應當當心不濫用這種技術,大多數時候仍是應當使用普通的 HashMap 做爲 Map 的實現。
public class SocketManager{
private Map<Socket,User> m = new WeakHashMap<Socket,User>();
public void setUser(Socket s, User s)
{
m.put(s,u);
}
public User getUser(Socket s)
{
return m.get(s);
}
}
引用隊列:
WeakHashMap 用弱引用承載映射鍵,這使得應用程序再也不使用鍵對象時它們能夠被垃圾收集,get() 實現能夠根據 WeakReference.get() 是否返回 null 來區分死的映射和活的映射。可是這只是防止 Map 的內存消耗在應用程序的生命週期中不斷增長所須要作的工做的一半,還須要作一些工做以便在鍵對象被收集後從 Map 中刪除死項。不然,Map 會充滿對應於死鍵的項。雖然這對於應用程序是不可見的,可是它仍然會形成應用程序耗盡內存,由於即便鍵被收集了,Map.Entry 和值對象也不會被收集。能夠經過週期性地掃描 Map,對每個弱引用調用 get(),並在 get() 返回 null 時刪除那個映射而消除死映射。可是若是 Map 有許多活的項,那麼這種方法的效率很低。若是有一種方法能夠在弱引用的 referent 被垃圾收集時發出通知就行了,這就是引用隊列的做用。引用隊列是垃圾收集器嚮應用程序返回關於對象生命週期的信息的主要方法。弱引用有兩個構造函數:一個只取 referent 做爲參數,另外一個還取引用隊列做爲參數。若是用關聯的
引用隊列建立弱引用,在 referent 成爲 GC 候選對象時,這個引用對象
(不是referent)就在引用清除後加入 到引用隊列中。以後,應用程序從引用隊列提取引用並瞭解到它的 referent 已被收集,所以能夠進行相應的清理活動,如去掉已不在弱集合中的對象的項。(引用隊列提供了與 BlockingQueue 一樣的出列模式 ——
polled、timed blocking 和 untimed blocking。)WeakHashMap 有一個名爲 expungeStaleEntries() 的私有方法,大多數 Map 操做中會調用它,它去掉引用隊列中全部失效的引用,並刪除關聯的映射。
4)關於Java中引用思考:
先觀察一個列表:
級別 |
回收時間 |
用途 |
生存時間 |
強引用 |
歷來不會被回收 |
對象的通常狀態 |
JVM中止運行時終止 |
軟引用 |
在內存不足時 |
在客戶端移除對象引用事後,除非再次激活,不然就放在內存敏感的緩存中 |
內存不足時終止 |
弱引用 |
在垃圾回收時,也就是客戶端已經移除了強引用,可是這種狀況下內存仍是客戶端引用可達的 |
阻止自動刪除不須要用的對象 |
GC運行後終止 |
虛引用[幽靈引用] |
對象死亡以前,就是進行finalize()方法調用附近 |
特殊的清除過程 |
不定,當finalize()函數運行事後再回收,有可能以前就已經被回收了。 |
能夠這樣理解:
SoftReference:假定垃圾回收器肯定在某一時間點某個對象是軟可到達對象。這時,它能夠選擇自動清除針對該對象的全部軟引用,以及經過強引用鏈,從其能夠到達該對象的針對任何其餘軟可到達對象的全部軟引用。在同一時間或晚些時候,它會將那些已經向引用隊列註冊的新清除的軟引用加入隊列。 軟可到達對象的全部軟引用都要保證在虛擬機拋出 OutOfMemoryError 以前已經被清除。不然,清除軟引用的時間或者清除不一樣對象的一組此類引用的順序將不受任何約束。然而,虛擬機實現不鼓勵清除最近訪問或使用過的軟引用。 此類的直接實例可用於實現簡單緩存;該類或其派生的子類還可用於更大型的數據結構,以實現更復雜的緩存。只要軟引用的指示對象是強可到達對象,即正在實際使用的對象,就不會清除軟引用。例如,經過保持最近使用的項的強指示對象,並由垃圾回收器決定是否放棄剩餘的項,複雜的緩存能夠防止放棄最近使用的項。通常來講,WeakReference咱們用來防止內存泄漏,保證內存對象被VM回收。
WeakReference:弱引用對象,它們並不由止其指示對象變得可終結,並被終結,而後被回收。弱引用最經常使用於實現
規範化的映射。假定垃圾回收器肯定在某一時間點上某個對象是弱可到達對象。這時,它將自動清除針對此對象的全部弱引用,以及經過
強引用鏈和軟引用,能夠從其到達該對象的針對任何其餘弱可到達對象的全部弱引用。同時它將聲明全部之前的弱可到達對象爲可終結的。在同一時間或晚些時候,它將那些已經向引用隊列註冊的新清除的弱引用加入隊列。 SoftReference多用做來實現cache機制,保證cache的有效性。
PhantomReference:虛引用對象,在回收器肯定其指示對象可另外回收以後,被加入隊列。虛引用最多見的用法是以某種可能比使用 Java 終結機制更靈活的方式來指派 pre-mortem 清除操做。若是垃圾回收器肯定在某一特定時間點上虛引用的指示對象是虛可到達對象,那麼在那時或者在之後的某一時間,它會將該引用加入隊列。爲了確保可回收的對象仍然保持原狀,虛引用的指示對象不能被檢索:虛引用的 get 方法老是返回 null。與軟引用和弱引用不一樣,虛引用在加入隊列時並無經過垃圾回收器
自動清除。經過虛引用可到達的對象將仍然保持原狀,直到全部這類引用都被清除,或者它們都變得
不可到達。
如下是不肯定概念
【*:Java引用的深刻部分一直都是討論得比較多的話題,上邊大部分爲摘錄整理,這裏再談談我我的的一些見解。從整個JVM框架結構來看,
Java的引用和
垃圾回收器造成了針對Java
內存堆的一個對象的
「閉包管理集」,其中在基本代碼裏面經常使用的就是強引用,強引用主要使用目的是就是編程的正常邏輯,這是全部的開發人員最容易理解的,而弱引用和軟引用的做用是比較回味無窮的。按照引用強弱,其排序能夠爲:
強引用——軟引用——弱引用——虛引用,爲何這樣寫呢,實際上針對垃圾回收器而言,強引用是它絕對不會隨便去動的區域,由於在內存堆裏面的對象,只有當前對象不是強引用的時候,該對象纔會進入
垃圾回收器的目標區域。
軟引用又能夠理解爲
「內存應急引用」,也就是說它和GC是完整地
配合操做的,爲了防止內存泄漏,當GC在回收過程出現
內存不足的時候,軟引用會被
優先回收,從垃圾回收算法上講,軟引用在設計的時候是
很容易被垃圾回收器發現的。爲何軟引用是處理告訴緩存的優先選擇的,主要有兩個緣由:第一,它對內存很是敏感,從抽象意義上講,咱們甚至能夠任何它和內存的變化牢牢綁定到一塊兒操做的,由於內存一旦不足的時候,它會優先向垃圾回收器
報警以提示
內存不足;第二,它會盡可能保證系統在OutOfMemoryError以前將對象直接設置成爲不可達,以保證不會出現內存溢出的狀況;因此使用軟引用來處理Java引用裏面的高速緩存是很不錯的選擇。其實軟引用
不只僅和內存敏感,實際上和垃圾回收器的交互也是
敏感的,這點能夠這樣理解,由於當內存不足的時候,軟引用會
報警,而這種報警會提示垃圾回收器針對目前的一些內存進行
清除操做,而在有軟引用存在的內存堆裏面,垃圾回收器會
第一時間反應,不然就會MemoryOut了。按照咱們正常的思惟來考慮,
垃圾回收器針對咱們調用System.gc()的時候,是不會輕易理睬的,由於僅僅是收到了來自強引用層代碼的請求,至於它是否回收還得看JVM內部
環境的條件是否知足,可是若是是軟引用的方式去申請垃圾回收器會
優先反應,只是咱們在開發過程不能控制軟引用對垃圾回收器發送垃圾回收申請,而JVM規範裏面也指出了軟引用不會
輕易發送申請到垃圾回收器。這裏還須要解釋的一點的是軟引用
發送申請
不是說軟引用像咱們調用System.gc()這樣直接申請垃圾回收,而是說
軟引用會設置對象引用爲
null,而垃圾回收器針對該引用的這種作法也會
優先響應,咱們能夠理解爲是軟引用對象在向垃圾回收器發送申請。反應快並不表明垃圾回收器會實時反應,仍是會在尋找軟引用引用到的對象的時候遵循必定的
回收規則,反應快在這裏的解釋是相對強引用設置對象爲null,當軟引用設置對象爲null的時候,該對象的被收集的
優先級比較高。
弱引用是一種比軟引用相對複雜的引用,其實
弱引用和軟引用都是Java程序能夠控制的,也就是說能夠經過代碼
直接使得引用針對
弱可及對象以及
軟可及對象是可引用的,軟引用和弱引用引用的對象實際上經過必定的代碼操做是可
從新激活的,只是通常不會作這樣的操做,這樣的用法
違背了最初的設計。弱引用和軟引用在垃圾回收器的目標範圍有一點點不一樣的就是,使用垃圾回收算法是很難找到弱引用的,也就是說弱引用用來監控垃圾回收的整個流程也是一種很好的選擇,它
不會影響垃圾回收的
正常流程,這樣就能夠規範化整個對象從設置爲null了事後的一個生命週期的代碼監控。並且由於弱引用是否存在對垃圾回收整個流程都不會形成影響,能夠這樣認爲,垃圾回收器
找獲得弱引用,該引用的對象就會被回收,若是
找不到弱引用,一旦等到GC完成了垃圾回收事後,弱引用引用的對象佔用的內存也會自動釋放,這就是軟引用在垃圾回收事後的自動終止。
最後談談
虛引用,虛引用應該是JVM裏面最厲害的一種引用,它的厲害在於它能夠在
對象的內存從
物理內存中清除掉了事後再引用該對象,也就是說當虛引用引用到對象的時候,這個對象實際已經從
物理內存堆中
清除掉了,若是咱們不用手動對
對象死亡或者
瀕臨死亡進行處理的話,JVM會默認調用finalize函數,可是虛引用存在於該函數附近的
生命週期內,因此能夠手動對對象的這個範圍的週期進行
監控。它之因此稱爲
「幽靈引用」就是由於該對象的物理內存已經不存在的,我我的以爲JVM保存了一個對象狀態的
鏡像索引,而這個鏡像索引裏面包含了對象在這個生命週期須要的全部內容,這裏的所須要就是
這個生命週期內須要的對象數據內容,也就是
對象死亡和瀕臨死亡以前finalize函數附近,至於強引用所須要的其餘對象附加內容是不須要在這個鏡像裏面包含的,因此即便物理內存不存在,仍是能夠經過
虛引用監控到該對象的,只是這種狀況是否可讓對象從新激活爲強引用我就不敢說了。由於虛引用在引用對象的過程不會去使得這個對象由Dead復活,並且這種對象是能夠在回收週期進行回收的。
】
5.總結:
本章節主要涵蓋了Java裏面比較底層的一個章節,主要是以JVM內存模型爲基礎包括JVM針對內存的線程模型的探討以及針對Java裏面內存堆和棧的詳細分析。特別感謝白遠方同窗提供的彙編方面關於操做系統以及內存發展的資料提供。
參考:IBM開發中心文檔,《Inside JVM》
本文的講解可能比較概念化,但願全部讀者可以仔細品味,Java與對象相關的底層內從這裏面都說起到了,主要是方便初學者和深刻者可以更加理解Java虛擬機處理Java對象的一個流程以及底層的相關原理,也方便查詢和參考,可能會有不完善的地方,若是有什麼概念錯誤,請來Email告知,謝謝:
silentbalanceyh@126.com。並且由於這一個章節的內容概念不少,整理思考和撰寫花了太長時間,抱歉!