《深刻理解JAVA虛擬機》筆記1


java程序運行時的內存空間,按照虛擬機規範有下面幾項:html

1)程序計數器  java

       指示下條命令執行地址。固然是線程私有,否則線程怎麼能並行的起來。 不重要,佔內存很小,忽略不計。c++

2)方法區git

      這個名字很讓我迷惑。這裏面裝的實際上是程序運行須要的類文件,常量,靜態變量等。做用容易明白。程序員

      程序運行時,執行代碼先得裝入內存,固然java好像是在第一次用到時才加載,這樣能夠避免裝入無用的類,節省內存。算法

      在HosSpot上,方法區現今和永久代是同一個區域。我就這麼理解了,雖然做者解釋說其實這二者根本不是同一個概念。編程

      概念暫時不大關心,垃圾回收是實實在在工做中用到的。bootstrap

3)本地方法棧canvas

      存儲Native本地方法中用到的局部變量表和操做數棧,動態連接,方法出口等。做用和(4)中無區別,並且在HotSpot上本地方法棧與java虛擬機棧是合二爲一的。數組

4Java虛擬機棧

      線程私有

      存儲java方法中用到的局部變量表和操做數棧,動態連接,方法出口等。

      在實際工做中會遇到棧內存溢出,StackOverflowError,不過以個人經驗,若是出現棧溢出,絕大多數都是死循環遞歸形成的。

5)堆

     堆在各線程見是共享的。

     這個是用的最多的。有好多人甚至覺得java運行的須要的內存就只有這個,果果-Xmx能夠指定堆可分配的最大值。-Xms指定堆最小分配。

     若是-Xms堆不夠用時,會自動擴展,當擴展到-Xmx的值,仍然不夠,就內存溢出了。OutOfMemoryError

     理論上全部的對象實例和數組都是在堆上分配的,可是JIT編譯器能夠聰明的作不少事情,也許有些對象的使用範圍是侷限在某些方法中的,這樣就能夠在棧上分配,

     畢竟棧上分配的內存管理要容易的多,用完即拋,不須要交給垃圾回收器來處理。

     固然,寫代碼的人最好有個譜,何時該局部變量,何時須要類變量,把本身寫的破代碼全交給編譯器去吭哧吭哧優化,一方面它不能徹底優化的完美,

     另外一方面也佔用cpu時間(哎,有感而發,破代碼見得太多了)

JVM內存詳解-閱讀筆記

 

 

 

 

 

 

JVM內存詳解

本機內存介紹

1.1 硬件限制

1.2 操做系統和虛擬內存

1.2.1 虛擬內存可增長可用內存空間, 可是可能引發性能問題

1.2.2 內核空間和用戶空間

內存耗盡時現象

Java 堆耗盡時,Java 應用程序很難正常運行,由於Java 應用程序必須經過分配對象來完成工做。只要Java 堆被填滿,就會出現糟糕的GC 性能並拋出表示Java 堆被填滿的OutOfMemoryError

相反,一旦Java運行時開始運行而且應用程序處於穩定狀態,它能夠在本機堆徹底耗盡以後繼續正常運行。不必定會發生奇怪的行爲,由於須要分配本機內存的操做比須要分配Java 堆的操做少得多。但一些常見操做仍是會報錯:啓動線程、加載類以及執行某種類型的網絡和文件I/O

如何佔用本機內存

3.1 java

Java 堆是分配了對象的內存區域。

堆的大小能夠在Java 命令行使用 -Xmx 和 -Xms 選項來控制(mx 表示堆的最大大小,ms 表示初始大小)。

Java堆佔用的本機內存爲-Xmx,未被使用的部分爲保留內存,保留內存不會分配給其餘程序使用;

對於維護Java 堆的內存管理系統,須要更多本機內存來維護它的狀態。例如行垃圾收集。

 

3.2 垃圾收集

3.3 即時(JIT)編譯

JIT 編譯器在運行時編譯Java 字節碼來優化本機可執行代碼。這極大地提升了Java 運行時的速度,而且支持Java 應用程序以與本機代碼至關的速度運行。

JIT 編譯器的輸入(字節碼)和輸出(可執行代碼)必須也存儲在本機內存中。

3.4 類和類加載器

存儲類的方式取決於具體實現。Sun JDK 使用永久生成(permanent generationPermGen)堆區域。Java 5 IBM 實現會爲每一個類加載器分配本機內存塊,並將類數據存儲在其中。

從最基本的層面來看,使用更多的類將須要使用更多內存。

Java運行時能夠卸載類來回收空間,可是隻有在很是嚴酷的條件下才會這樣作。不能卸載單個類,而是卸載類加載器,隨其加載的全部類都會被卸載。只有在如下狀況下才能卸載類加載器:

  Java 堆不包含對錶示該類加載器的 java.lang.ClassLoader 對象的引用。

  Java 堆不包含對錶示類加載器加載的類的任何 java.lang.Class 對象的引用。

  在Java 堆上,該類加載器加載的任何類的全部對象都再也不存活(被引用)。

須要注意的是,Java 運行時爲全部Java 應用程序建立的個默認類加載器(bootstrapextension 和 application )都不可能知足這些條件,所以,任何系統類(好比 java.lang.String)或經過應用程序類加載器加載的任何應用程序類都不能在運行時釋放。

3.5 JNI

JNI 應用程序可能經過種方式增長Java 運行時的本機內存佔用:

JNI 應用程序的本機代碼被編譯到共享庫中,或編譯爲加載到進程地址空間中的可執行文件。大型本機應用程序可能僅僅加載就會佔用大量進程地址空間。

本機代碼必須與Java 運行時共享地址空間。任何本機代碼分配或本機代碼執行的內存映射都會耗用Java 運行時的內存。

某些JNI 函數可能在它們的常規操做中使用本機內存。GetTypeArrayElements GetTypeArrayRegion 函數能夠將Java堆數據複製到本機內存緩衝區中,以供本機代碼使用。是否複製數據依賴於運行時實現。(IBM Developer Kit for Java 5.0 和更高版本會進行本機複製)。

經過這種方式訪問大量Java 堆數據可能會使用大量本機堆

3.6 NIO

直接bytebuffer會操做本機內存;

3.7 線程

線程的堆棧空間、線程本地存儲(thread-local storage)和內部數據結構會佔用本機內存。

堆棧大小因Java 實現和架構的不一樣而不一樣。一些實現支持爲Java 線程指定堆棧大小,其範圍一般在256KB 756KB 之間。

調試方法和技術

4.1 檢查java

JavaCore文件、heapdump文件;

 

4.2 檢查本機堆

Windows 提供的PerfMon 工具;

Linux 使用命令行工具(好比 pstop 和 pmap)可以顯示應用程序的本機內存佔用狀況。配合使用GCMV,進行長時間跟蹤後的分析。

因爲JVM 前期階段的本機內存增加而耗盡本機內存,以及內存使用隨負載增長而增長,這些都是嘗試在可用空間中作太多事情的例子。在這些場景中,您的選擇是:

  減小本機內存使用。縮小Java 堆大小是一個好的開端。

  限制本機內存使用。若是您的本機內存隨負載增長而增長,能夠採起某種方式限制負載或爲負載分配的資源。

  增長可用地址空間。這能夠經過如下方式實現:調優您的操做系統(例如,在Windows 上使用/3GB 開關增長用戶空間,或者在Linux 上使用龐大的內核空間),更換平臺(Linux 一般擁有比Windows 更多的用戶空間),或者 轉移到64 位操做系統。

 

 

4.3 是什麼在使用本機內存

根據您的Java 設置,將會使用多少本機內存。根據如下指南粗略估算一下:

Java 堆佔用的內存至少爲-Xmx 值。

每一個Java 線程須要堆棧空間。堆棧空間因實現不一樣而異,可是若是使用默認設置,每一個線程至多會佔用756KB 本機內存。

直接 ByteBuffer 至少會佔用提供給allocate() 例程的內存值。

UMDH 支持就地 調試Windows 上本機內存泄漏,在Linux 上,您可能須要進行一些傳統的調試,而不是依賴工具來解決問題。下面是一些建議的調試步驟:

提取測試案例。生成一個獨立環境,您須要可以在該環境中再現本機內存泄漏。這將使調試更加簡單。

儘量縮小測試案例。嘗試禁用函數來肯定是哪些代碼路徑致使了本機內存泄漏。若是您擁有本身的JNI 庫,能夠嘗試一次禁用一個來肯定是哪一個庫致使了內存泄漏。

縮小Java 堆大小。Java 堆多是進程的虛擬地址空間的最大使用者。經過減少Java 堆,能夠將更多空間提供給本機內存的其餘使用者。

關聯本機進程大小。一旦您得到了本機內存隨時間的使用狀況,能夠將其與應用程序工做負載和GC 數據比較。若是泄漏程度與負載級別成正比,則意味着泄漏是由每一個事務或操做路徑上的某個實體引發的。若是當進行垃圾收集時,本機進程大小顯著減少,這意味着您沒遇到內存泄漏,您擁有的是具備本機支持的對象組合(好比直接 ByteBuffer)。經過縮小Java 堆大小(從而迫使垃圾收集更頻繁地發生),或者在一個對象緩存中管理對象(而不是依賴於垃圾收集器來清理對象),您能夠減小本機支持對象持有的內存量。

消除限制

更改成64位,可是64位可能引入對象膨脹的問題;

必須關注物理內存是否知足Java程序使用,一旦物理內存不足,發生頻繁的與虛擬內存交換,會嚴重影響性能。

java中的內存溢出和內存泄漏

內存溢出:對於整個應用程序來講,JVM內存空間,已經沒有多餘的空間分配給新的對象。因此就發生內存溢出。

內存泄露:在應用的整個生命週期內,某個對象一直存在,且對象佔用的內存空間愈來愈大,最終致使JVM內存泄露,
好比:緩存的應用,若是不設置上限的話,緩存的容量可能會一直增加。
靜態集合引用,若是該集合存放了無數個對象,隨着時間的推移也有可能使容量無限制的增加,最終致使JVM內存泄露。

內存泄露,是應用程序中的某個對象長時間的存活,而且佔用空間不斷增加,最終致使內存泄露。
是對象分配後,長時間的容量增加。

內存溢出,是針對整個應用程序的全部對象的分配空間不足,會形成內存溢出。

 

內存泄漏

內存泄漏指因爲疏忽或錯誤形成程序未能釋放已經再也不使用的內存的狀況。內存泄漏並不是指內存在物理上的消失,而是應用程序分配某段內存後,因爲設計錯誤,失去了對該段內存的控制,於是形成了內存的浪費。內存泄漏與許多其餘問題有着類似的症狀,而且一般狀況下只能由那些能夠得到程序源代碼的程序員才能夠分析出來。然而,有很多人習慣於把任何不須要的內存使用的增長描述爲內存泄漏,即便嚴格意義上來講這是不許確的。
  通常咱們常說的內存泄漏是指堆內存的泄漏。堆內存是指程序從堆中分配的,大小任意的(內存塊的大小能夠在程序運行期決定),使用完後必須顯示釋放的內存。應用程序通常使用mallocreallocnew等函數從堆中分配到一塊內存,使用完後,程序必須負責相應的調用freedelete釋放該內存塊,不然,這塊內存就不能被再次使用,咱們就說這塊內存泄漏了。 
  內存泄漏能夠分爲4類: 
  1. 常發性內存泄漏。發生內存泄漏的代碼會被屢次執行到,每次被執行的時候都會致使一塊內存泄漏。 
  2. 偶發性內存泄漏。發生內存泄漏的代碼只有在某些特定環境或操做過程下才會發生。常發性和偶發性是相對的。對於特定的環境,偶發性的也許就變成了常發性的。因此測試環境和測試方法對檢測內存泄漏相當重要。 
  3. 一次性內存泄漏。發生內存泄漏的代碼只會被執行一次,或者因爲算法上的缺陷,致使總會有一塊僅且一塊內存發生泄漏。好比,在類的構造函數中分配內存,在析構函數中卻沒有釋放該內存,因此內存泄漏只會發生一次。 
  4. 隱式內存泄漏。程序在運行過程當中不停的分配內存,可是直到結束的時候才釋放內存。嚴格的說這裏並無發生內存泄漏,由於最終程序釋放了全部申請的內存。可是對於一個服務器程序,須要運行幾天,幾周甚至幾個月,不及時釋放內存也可能致使最終耗盡系統的全部內存。因此,咱們稱這類內存泄漏爲隱式內存泄漏。

 

簡單點: 內存泄漏就是忘記釋放使用完畢的內存,讓下次使用有必定風險。 
內存溢出就是必定的內存空間不能裝下全部的須要存放的數據,形成內存數據溢出。

 

 

主要從如下幾部分來講明,關於內存和內存泄露、溢出的概念,區份內存泄露和內存溢出;內存的區域劃分,瞭解GC回收機制;重點關注如何去監控和發現內存問題;此外分析出問題還要如何解決內存問題。

  下面就開始本篇的內容:

  第一部分 概念

  衆所周知,java中的內存由java虛擬機本身去管理的,他不像C++須要本身去釋放。籠統地去講,java的內存分配分爲兩個部分,一個是數據堆,一個是棧。程序在運行的時候通常分配數據堆,把局部的臨時的變量都放進去,生命週期和進程有關係。可是若是程序員聲明瞭static的變量,就直接在棧中運行的,進程銷燬了,不必定會銷燬static變量。

  另外爲了保證java內存不會溢出,java中有垃圾回收機制。 System.gc()即垃圾收集機制是指jvm用於釋放那些再也不使用的對象所佔用的內存。java語言並不要求jvmgc,也沒有規定gc如何工做。垃圾收集的目的在於清除再也不使用的對象。gc經過肯定對象是否被活動對象引用來肯定是否收集該對象。

  而其中,內存溢出就是你要求分配的java虛擬機內存超出了系統能給你的,系統不能知足需求,因而產生溢出。

  內存泄漏是指你向系統申請分配內存進行使用(new),但是使用完了之後卻不歸還(delete),結果你申請到的那塊內存你本身也不能再訪問,該塊已分配出來的內存也沒法再使用,隨着服務器內存的不斷消耗,而沒法使用的內存愈來愈多,系統也不能再次將它分配給須要的程序,產生泄露。一直下去,程序也逐漸無內存使用,就會溢出。

  第二部分 原理

  JAVA垃圾回收及對內存區劃分

  在Java虛擬機規範中,說起了以下幾種類型的內存空間:

  ◇ 棧內存(Stack):每一個線程私有的。

  ◇ 堆內存(Heap):全部線程公用的。

  ◇ 方法區(Method Area):有點像之前常說的「進程代碼段」,這裏面存放了每一個加載類的反射信息、類函數的代碼、編譯時常量等信息。

  ◇ 原生方法棧(Native Method Stack):主要用於JNI中的原生代碼,平時不多涉及。

  而Java的使用的是堆內存,java堆是一個運行時數據區,類的實例(對象)從中分配空間。Java虛擬機(JVM)的堆中儲存着正在運行的應用程序所創建的全部對象,「垃圾回收」也是主要是和堆內存(Heap)有關。

  垃圾回收的概念就是JAVA虛擬機(JVM)回收那些再也不被引用的對象內存的過程。通常咱們認爲正在被引用的對象狀態爲「alive,而沒有被應用或者取不到引用屬性的對象狀態爲「dead」。垃圾回收是一個釋放處於」dead」狀態的對象的內存的過程。而垃圾回收的規則和算法被動態的做用於應用運行當中,自動回收。

  JVM的垃圾回收器採用的是一種分代(generational )回收策略,用較高的頻率對年輕的對象(young generation)進行掃描和回收,這種叫作minor collection,而對老對象(old generation)的檢查回收頻率要低不少,稱爲major collection。這樣就不須要每次GC都將內存中全部對象都檢查一遍,這種策略有利於實時觀察和回收。

  (Sun JVM 1.3 有兩種最基本的內存收集方式:一種稱爲copyingscavenge,將全部仍然生存的對象搬到另一塊內存後,整塊內存就可回收。這種方法有效率,但須要有必定的空閒內存,拷貝也有開銷。這種方法用於minor collection。另一種稱爲mark-compact,將活着的對象標記出來,而後搬遷到一塊兒連成大塊的內存,其餘內存就能夠回收了。這種方法不須要佔用額外的空間,但速度相對慢一些。這種方法用於major collection. 

  一些對象被建立出來只是擁有短暫的生命週期,好比 iterators 和本地變量。另一些對象被建立是擁有很長的生命週期,好比持久化對象等。

  垃圾回收器的分代策略是把內存區劃分爲幾個代,而後爲每一個代分配一到多個內存區塊。當其中一個代用完了分配給他的內存後,JVM會在分配的內存區內執行一個局部的GC(也能夠叫minor collection)操做,爲了回收處於「dead」狀態的對象所佔用的內存。局部GC一般要比Full GC快不少。

JVM定義了兩個代,年輕代(yong generation)(有時稱爲「nursery」託兒所)和老年代(old generation)。年輕代包括 「Eden space(伊甸園)」和兩個「survivor spaces」。虛擬內存初始化的時候會把全部對象都分配到 Eden space,而且大部分對象也會在該區域被釋放。 當進行 minor GC的時候,VM會把剩下的沒有釋放的對象從Eden space移動到其中一個survivor spaces當中。此外,VM也會把那些長期存活在survivor spaces 裏的對象移動到 老生代的「tenured」 space中。當 tenured generation 被填滿後,就會產生Full GCFull GC會相對比較慢由於回收的內容包括了全部的 live狀態的對象。pemanet generation這個代包括了全部java虛擬機自身使用的相對比較穩定的數據對象,好比類和對象方法等。

  關於代的劃分,能夠從下圖中得到一個概況:

      第三部分 總結

  內存溢出主要是因爲代碼編寫時對某些方法、類應用不合理,或者沒有預估到臨時對象會佔用很大內存量,或者把過多的數據放入JVM緩存,或者性能壓力大致使消息堆積而佔用內存,以致於在性能測試時,生成龐大數量的臨時對象,GC時沒有作出有效回收甚至根本就不能回收,形成內存空間不足,內存溢出。

  若是編碼以前,對內存使用量進行預估,對放在內存中的數據進行評估,保證有用的信息儘快釋放,無用的信息可以被GC回收,這樣在必定程度上是能夠避免內存溢出問題的。

JNI內存泄露

 

jni 內存泄露

分類: JNI2011-09-27 03:10 1142人閱讀 評論(0) 收藏 舉報

jnireferencejava編程jvmstring

JNI 編程簡介

JNIJava Native Interface,是 native code 的編程接口。JNI 使 Java 代碼程序能夠與 native code 交互——在 Java 程序中調用 native code;在 native code 中嵌入 Java 虛擬機調用 Java 的代碼。

JNI 編程在軟件開發中運用普遍,其優點能夠歸結爲如下幾點:

1 利用 native code 的平臺相關性,在平臺相關的編程中彰顯優點。

2 對 native code 的代碼重用。

3 native code 底層操做,更加高效。

然而任何事物都具備兩面性,JNI 編程也一樣如此。程序員在使用 JNI 時應當認識到 JNI 編程中以下的幾點弊端,揚長避短,才能夠寫出更加完善、高性能的代碼:

4 從 Java 環境到 native code 的上下文切換耗時、低效。

5 JNI 編程,若是操做不當,可能引發 Java 虛擬機的崩潰。

6 JNI 編程,若是操做不當,可能引發內存泄漏。

 

回頁首

JAVA 中的內存泄漏

JAVA 編程中的內存泄漏,從泄漏的內存位置角度能夠分爲兩種:JVM 中 Java Heap 的內存泄漏;JVM 內存中 native memory 的內存泄漏。

Java Heap 的內存泄漏

Java 對象存儲在 JVM 進程空間中的 Java Heap 中,Java Heap 能夠在 JVM 運行過程當中動態變化。若是 Java 對象愈來愈多,佔據 Java Heap 的空間也愈來愈大,JVM 會在運行時擴充 Java Heap 的容量。若是 Java Heap 容量擴充到上限,而且在 GC 後仍然沒有足夠空間分配新的 Java 對象,便會拋出 out of memory 異常,致使 JVM 進程崩潰。

Java Heap 中 out of memory 異常的出現有兩種緣由——①程序過於龐大,導致過多 Java 對象的同時存在;②程序編寫的錯誤致使 Java Heap 內存泄漏。

多種緣由可能致使 Java Heap 內存泄漏。JNI 編程錯誤也可能致使 Java Heap 的內存泄漏。

JVM 中 native memory 的內存泄漏

從操做系統角度看,JVM 在運行時和其它進程沒有本質區別。在系統級別上,它們具備一樣的調度機制,一樣的內存分配方式,一樣的內存格局。

JVM 進程空間中,Java Heap 之外的內存空間稱爲 JVM 的 native memory。進程的不少資源都是存儲在 JVM 的 native memory 中,例如載入的代碼映像,線程的堆棧,線程的管理控制塊,JVM 的靜態數據、全局數據等等。也包括 JNI 程序中 native code 分配到的資源。

在 JVM 運行中,多數進程資源從 native memory 中動態分配。當愈來愈多的資源在 native memory 中分配,佔據愈來愈多 native memory 空間而且達到 native memory 上限時,JVM 會拋出異常,使 JVM 進程異常退出。而此時 Java Heap 每每尚未達到上限。

多種緣由可能致使 JVM 的 native memory 內存泄漏。例如 JVM 在運行中過多的線程被建立,而且在同時運行。JVM 爲線程分配的資源就可能耗盡 native memory 的容量。

JNI 編程錯誤也可能致使 native memory 的內存泄漏。對這個話題的討論是本文的重點。

 

回頁首

JNI 編程中明顯的內存泄漏

JNI 編程實現了 native code 和 Java 程序的交互,所以 JNI 代碼編程既遵循 native code 編程語言的編程規則,同時也遵照 JNI 編程的文檔規範。在內存管理方面,native code 編程語言自己的內存管理機制依然要遵循,同時也要考慮 JNI 編程的內存管理。

本章簡單歸納 JNI 編程中顯而易見的內存泄漏。從 native code 編程語言自身的內存管理,和 JNI 規範附加的內存管理兩方面進行闡述。

Native Code 自己的內存泄漏

JNI 編程首先是一門具體的編程語言,或者 語言,或者 C++,或者彙編,或者其它 native 的編程語言。每門編程語言環境都實現了自身的內存管理機制。所以,JNI 程序開發者要遵循 native 語言自己的內存管理機制,避免形成內存泄漏。以 語言爲例,當用 malloc() 在進程堆中動態分配內存時,JNI 程序在使用完後,應當調用 free() 將內存釋放。總之,全部在 native 語言編程中應當注意的內存泄漏規則,在 JNI 編程中依然適應。

Native 語言自己引入的內存泄漏會形成 native memory 的內存,嚴重狀況下會形成 native memory 的 out of memory

Global Reference 引入的內存泄漏

JNI 編程還要同時遵循 JNI 的規範標準,JVM 附加了 JNI 編程特有的內存管理機制。

JNI 中的 Local Reference 只在 native method 執行時存在,當 native method 執行完後自動失效。這種自動失效,使得對 Local Reference 的使用相對簡單,native method 執行完後,它們所引用的 Java 對象的 reference count 會相應減 1。不會形成 Java Heap 中 Java 對象的內存泄漏。

而 Global Reference 對 Java 對象的引用一直有效,所以它們引用的 Java 對象會一直存在 Java Heap 中。程序員在使用 Global Reference 時,須要仔細維護對 Global Reference 的使用。若是必定要使用 Global Reference,務必確保在不用的時候刪除。就像在 語言中,調用 malloc() 動態分配一塊內存以後,調用 free() 釋放同樣。不然,Global Reference 引用的 Java 對象將永遠停留在 Java Heap 中,形成 Java Heap 的內存泄漏。

 

回頁首

JNI 編程中潛在的內存泄漏——對 LocalReference 的深刻理解

Local Reference 在 native method 執行完成後,會自動被釋放,彷佛不會形成任何的內存泄漏。但這是錯誤的。對 Local Reference 的理解不夠,會形成潛在的內存泄漏。

本章重點闡述 Local Reference 使用不當可能引起的內存泄漏。引入兩個錯誤實例,也是 JNI 程序員容易忽視的錯誤;在此基礎上介紹 Local Reference 表,對比 native method 中的局部變量和 JNI Local Reference 的不一樣,使讀者深刻理解 JNI Local Reference 的實質;最後爲 JNI 程序員提出應該如何正確合理使用 JNI Local Reference,以免內存泄漏。

錯誤實例 1

在某些狀況下,咱們可能須要在 native method 裏面建立大量的 JNI Local Reference。這樣可能致使 native memory 的內存泄漏,若是在 native method 返回以前 native memory 已經被用光,就會致使 native memory 的 out of memory

在代碼清單 裏,咱們循環執行 count 次,JNI function NewStringUTF() 在每次循環中從 Java Heap 中建立一個 String 對象,str 是 Java Heap 傳給 JNI native method 的 Local Reference,每次循環中新建立的 String 對象覆蓋上次循環中 str 的內容。str 彷佛一直在引用到一個 String 對象。整個運行過程當中,咱們看似只建立一個 Local Reference

執行代碼清單 的程序,第一部分爲 Java 代碼,nativeMethod(int i) 中,輸入參數設定循環的次數。第二部分爲 JNI 代碼,用 語言實現了 nativeMethod(int i)


清單 1. Local Reference 引起內存泄漏

                

 Java 代碼部分

 class TestLocalReference { 

 private native void nativeMethod(int i); 

 public static void main(String args[]) { 

         TestLocalReference c = new TestLocalReference(); 

         //call the jni native method 

         c.nativeMethod(1000000); 

 }  

 static { 

 //load the jni library 

 System.loadLibrary("StaticMethodCall"); 

 } 

 } 

 

 JNI 代碼,nativeMethod(int i) 的 語言實現

 #include<stdio.h> 

 #include<jni.h> 

 #include"TestLocalReference.h"

 JNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod 

 (JNIEnv * env, jobject obj, jint count) 

 { 

 jint i = 0; 

 jstring str; 

 

 for(; i<count; i++) 

         str = (*env)->NewStringUTF(env, "0"); 

 } 

運行結果

 JVMCI161: FATAL ERROR in native method: Out of memory when expanding 

 local ref table beyond capacity 

 at TestLocalReference.nativeMethod(Native Method) 

 at TestLocalReference.main(TestLocalReference.java:9) 

 

 

運行結果證實,JVM 運行異常終止,緣由是建立了過多的 Local Reference,從而致使 out of memory。實際上,nativeMethod 在運行中建立了愈來愈多的 JNI Local Reference,而不是看似的始終只有一個。過多的 Local Reference,致使了 JNI 內部的 JNI Local Reference 表內存溢出。

錯誤實例 2

實例 是實例 的變種,Java 代碼未做修改,可是 nativeMethod(int i) 的 語言實現稍做修改。在 JNI 的 native method 中實現的 utility 函數中建立 Java 的 String 對象。utility 函數只創建一個 String 對象,返回給調用函數,可是 utility 函數對調用者的使用狀況是未知的,每一個函數均可能調用它,而且同一函數可能調用它屢次。在實例 中,nativeMethod 在循環中調用 count 次,utility 函數在建立一個 String 對象後即返回,而且會有一個退棧過程,彷佛所建立的 Local Reference 會在退棧時被刪除掉,因此應該不會有不少 Local Reference 被建立。實際運行結果並不是如此。


清單 2. Local Reference 引起內存泄漏

               

 Java 代碼部分參考實例 1,未作任何修改。

 

 JNI 代碼,nativeMethod(int i) 的 語言實現

 #include<stdio.h> 

 #include<jni.h> 

 #include"TestLocalReference.h"

 jstring CreateStringUTF(JNIEnv * env) 

 { 

 return (*env)->NewStringUTF(env, "0"); 

 } 

 JNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod 

 (JNIEnv * env, jobject obj, jint count) 

 { 

 jint i = 0; 

 for(; i<count; i++) 

 { 

         str = CreateStringUTF(env); 

 } 

 } 

運行結果

 JVMCI161: FATAL ERROR in native method: Out of memory when expanding local ref 

 table beyond  capacity 

 at TestLocalReference.nativeMethod(Native Method) 

 at TestLocalReference.main(TestLocalReference.java:9) 

 

 

運行結果證實,實例 的結果與實例 的徹底相同。過多的 Local Reference 被建立,仍然致使了 JNI 內部的 JNI Local Reference 表內存溢出。實際上,在 utility 函數 CreateStringUTF(JNIEnv * env)

執行完成後的退棧過程當中,建立的 Local Reference 並無像 native code 中的局部變量那樣被刪除,而是繼續在 Local Reference 表中存在,而且有效。Local Reference 和局部變量有着本質的區別。

Local Reference 深層解析

Java JNI 的文檔規範只描述了 JNI Local Reference 是什麼(存在的目的),以及應該怎麼使用 Local Reference(開放的接口規範)。可是對 Java 虛擬機中 JNI Local Reference 的實現並無約束,不一樣的 Java 虛擬機有不一樣的實現機制。這樣的好處是,不依賴於具體的 JVM 實現,有好的可移植性;而且開發簡單,規定了「應該怎麼作、怎麼用」。可是弊端是初級開發者每每看不到本質,「不知道爲何這樣作」。對 Local Reference 沒有深層的理解,就會在編程過程當中無心識的犯錯。

Local Reference 和 Local Reference 

理解 Local Reference 表的存在是理解 JNI Local Reference 的關鍵。

JNI Local Reference 的生命期是在 native method 的執行期(從 Java 程序切換到 native code 環境時開始建立,或者在 native method 執行時調用 JNI function 建立),在 native method 執行完畢切換回 Java 程序時,全部 JNI Local Reference 被刪除,生命期結束(調用 JNI function 能夠提早結束其生命期)。

實際上,每當線程從 Java 環境切換到 native code 上下文時(J2N),JVM 會分配一塊內存,建立一個 Local Reference 表,這個表用來存放本次 native method 執行中建立的全部的 Local Reference。每當在 native code 中引用到一個 Java 對象時,JVM 就會在這個表中建立一個 Local Reference。好比,實例 中咱們調用 NewStringUTF() 在 Java Heap 中建立一個 String 對象後,在 Local Reference 表中就會相應新增一個 Local Reference


圖 1. Local Reference 表、Local Reference 和 Java 對象的關係
 

圖 中:

⑴運行 native method 的線程的堆棧記錄着 Local Reference 表的內存位置(指針 p)。

⑵ Local Reference 表中存放 JNI Local Reference,實現 Local Reference 到 Java 對象的映射。

⑶ native method 代碼間接訪問 Java 對象(java obj1java obj2)。經過指針 定位相應的 Local Reference 的位置,而後經過相應的 Local Reference 映射到 Java 對象。

⑷當 native method 引用一個 Java 對象時,會在 Local Reference 表中建立一個新 Local Reference。在 Local Reference 結構中寫入內容,實現 Local Reference 到 Java 對象的映射。

⑸ native method 調用 DeleteLocalRef() 釋放某個 JNI Local Reference 時,首先經過指針 定位相應的 Local Reference 在 Local Ref 表中的位置,而後從 Local Ref 表中刪除該 Local Reference,也就取消了對相應 Java 對象的引用(Ref count 減 1)。

⑹當愈來愈多的 Local Reference 被建立,這些 Local Reference 會在 Local Ref 表中佔據愈來愈多內存。當 Local Reference 太多以致於 Local Ref 表的空間被用光,JVM 會拋出異常,從而致使 JVM 的崩潰。

Local Ref 不是 native code 的局部變量

不少人會誤將 JNI 中的 Local Reference 理解爲 Native Code 的局部變量。這是錯誤的。

Native Code 的局部變量和 Local Reference 是徹底不一樣的,區別能夠總結爲:

⑴局部變量存儲在線程堆棧中,而 Local Reference 存儲在 Local Ref 表中。

⑵局部變量在函數退棧後被刪除,而 Local Reference 在調用 DeleteLocalRef() 後纔會從 Local Ref 表中刪除,而且失效,或者在整個 Native Method 執行結束後被刪除。

⑶能夠在代碼中直接訪問局部變量,而 Local Reference 的內容沒法在代碼中直接訪問,必須經過 JNI function 間接訪問。JNI function 實現了對 Local Reference 的間接訪問,JNI function 的內部實現依賴於具體 JVM

代碼清單 中 str = (*env)->NewStringUTF(env, "0");

str 是 jstring 類型的局部變量。Local Ref 表中會新建立一個 Local Reference,引用到 NewStringUTF(env, "0") 在 Java Heap 中新建的 String 對象。如圖 所示:


圖 2. str 間接引用 string 對象
 

圖 中,str 是局部變量,在 native method 堆棧中。Local Ref3 是新建立的 Local Reference,在 Local Ref 表中,引用新建立的 String 對象。JNI 經過 str 和指針 間接定位 Local Ref3,但 和 Local Ref3 對 JNI 程序員不可見。

Local Reference 致使內存泄漏

在以上論述基礎上,咱們經過分析錯誤實例 和實例 2,來分析 Local Reference 可能致使的內存泄漏,加深對 Local Reference 的深層理解。

分析錯誤實例 1

局部變量 str 在每次循環中都被從新賦值,間接指向最新建立的 Local Reference,前面建立的 Local Reference 一直保留在 Local Ref 表中。

在實例 執行完第 次循環後,內存佈局如圖 3


圖 3. 執行 次循環後的內存佈局
 

繼續執行完第 i+1 次循環後,內存佈局發生變化,如圖 4


圖 4. 執行 i+1 次循環後的內存佈局
 

圖 中,局部變量 str 被賦新值,間接指向了 Local Ref i+1。在 native method 運行過程當中,咱們已經沒法釋放 Local Ref i 佔用的內存,以及 Local Ref i 所引用的第 個 string 對象所佔據的 Java Heap 內存。因此,native memory 中 Local Ref i 被泄漏,Java Heap 中建立的第 個 string 對象被泄漏了。

也就是說在循環中,前面建立的全部 個 Local Reference 都泄漏了 native memory 的內存,建立的全部 個 string 對象都泄漏了 Java Heap 的內存。

直到 native memory 執行完畢,返回到 Java 程序時(N2J),這些泄漏的內存纔會被釋放,可是 Local Reference 表所分配到的內存每每很小,在不少狀況下 N2J 以前可能已經引起嚴重內存泄漏,致使 Local Reference 表的內存耗盡,使 JVM 崩潰,例如錯誤實例 1

分析錯誤實例 2

實例 與實例 類似,雖然每次循環中調用工具函數 CreateStringUTF(env) 來建立對象,可是在 CreateStringUTF(env) 返回退棧過程當中,只是局部變量被刪除,而每次調用建立的 Local Reference 仍然存在 Local Ref 表中,而且有效引用到每一個新建立的 string 對象。str 局部變量在每次循環中被賦新值。

這樣的內存泄漏是潛在的,可是這樣的錯誤在 JNI 程序員編程過程當中卻常常出現。一般狀況,在觸發 out of memory 以前,native method 已經執行完畢,切換回 Java 環境,全部 Local Reference 被刪除,問題也就沒有顯露出來。可是某些狀況下就會引起 out of memory,致使實例 和實例 中的 JVM 崩潰。

控制 Local Reference 生命期

所以,在 JNI 編程時,正確控制 JNI Local Reference 的生命期。若是須要建立過多的 Local Reference,那麼在對被引用的 Java 對象操做結束後,須要調用 JNI function(如 DeleteLocalRef()),及時將 JNI Local Reference 從 Local Ref 表中刪除,以免潛在的內存泄漏。

 

回頁首

總結

本文闡述了 JNI 編程可能引起的內存泄漏,JNI 編程既可能引起 Java Heap 的內存泄漏,也可能引起 native memory 的內存泄漏,嚴重的狀況可能使 JVM 運行異常終止。JNI 軟件開發人員在編程中,應當考慮如下幾點,避免內存泄漏:

· native code 自己的內存管理機制依然要遵循。

· 使用 Global reference 時,當 native code 再也不須要訪問 Global reference 時,應當調用 JNI 函數 DeleteGlobalRef() 刪除 Global reference 和它引用的 Java 對象。Global reference 管理不當會致使 Java Heap 的內存泄漏。

· 透徹理解 Local reference,區分 Local reference 和 native code 的局部變量,避免混淆二者所引發的 native memory 的內存泄漏。

· 使用 Local reference 時,若是 Local reference 引用了大的 Java 對象,當再也不須要訪問 Local reference 時,應當調用 JNI 函數 DeleteLocalRef() 刪除 Local reference,從而也斷開對 Java 對象的引用。這樣能夠避免 Java Heap 的 out of memory

· 使用 Local reference 時,若是在 native method 執行期間會建立大量的 Local reference,當再也不須要訪問 Local reference 時,應當調用 JNI 函數 DeleteLocalRef() 刪除 Local referenceLocal reference 表空間有限,這樣能夠避免 Local reference 表的內存溢出,避免 native memory 的 out of memory

· 嚴格遵循 Java JNI 規範書中的使用規則。

Java內存機制詳解

Java 堆(每一個 Java 對象在其中分配)是您在編寫 Java 應用程序時使用最頻繁的內存區域。JVM 設計用於將咱們與主機的特性隔離,因此將內存看成堆來考慮再正常不過了。您必定遇到過 Java 堆 OutOfMemoryError , 它多是因爲對象泄漏形成的,也多是由於堆的大小不足以存儲全部數據,您也可能瞭解這些場景的一些調試技巧。可是隨着您的 Java 應用程序處理愈來愈多的數據和愈來愈多的併發負載,您可能就會遇到沒法使用常規技巧進行修復的 OutOfMemoryError 。 在一些場景中,即便 java 堆未滿,也會拋出錯誤。當這類場景發生時,您須要理解 Java 運行時環境(Java Runtime EnvironmentJRE)內部到底發生了什麼。

Java 應用程序在 Java 運行時的虛擬化環境中運行,可是運行時自己是使用 之類的語言編寫的本機程序,它也會耗用本機資源,包括本機內存 。本機內存是可用於運行時進程的內存,它與 Java 應用程序使用的 java 堆內存不一樣。每種虛擬化資源(包括 Java 堆和 Java 線程)都必須存儲在本機內存中,虛擬機在運行時使用的數據也是如此。這意味着主機的硬件和操做系統施加在本機內存上的限制會影響到 Java 應用程序的性能。

本系列文章共分兩篇,討論不一樣平臺上的相應話題。本文是其中一篇。在這兩篇文章中,您將瞭解什麼是本機內存,Java 運行時如何使用它,本機內存耗盡以後會發生什麼狀況,以及如何調試本機 OutOfMemoryError 。本文介紹 Windows 和 Linux 平臺上的這一主題,不會介紹任何特定的運行時實現。

 

本機內存簡介

硬件限制

本機進程遇到的許多限制都是由硬件形成的,而與操做系統沒有關係。每臺計算機都有一個處理器和一些隨機存取存儲器(RAM),後者也稱爲物理 內存。處理器將數據流解釋爲要執行的指令,它擁有一個或多個處理單元,用於執行整數和浮點運算以及更高級的計算。處理器具備許多寄存器 —— 常快速的內存元素,用做被執行的計算的工做存儲,寄存器大小決定了一次計算可以使用的最大數值。

處理器經過內存總線鏈接到物理內存。物理地址(處理器用於索引物理 RAM 的地址)的大小限制了能夠尋址的內存。例如,一個 16 位物理地址能夠尋址 0x0000 到 0xFFFF 的內存地址,這個地址範圍包括 2^16 = 65536 個唯一的內存位置。若是每一個地址引用一個存儲字節,那麼一個 16 位物理地址將容許處理器尋址 64KB 內存。

處理器被描述爲特定數量的數據位。這一般指的是寄存器大小,可是也存在例外,好比 32 位 390 指的是物理地址大小。對於桌面和服務器平臺,這個數字爲 3132 或 64;對於嵌入式設備和微處理器,這個數字可能小至 4。物理地址大小能夠與寄存器帶寬同樣大,也能夠比它大或小。若是在適當的操做系統上運行,大部分 64 位處理器能夠運行 32 位程序。

表 列出了一些流行的 Linux 和 Windows 架構,以及它們的寄存器和物理地址大小:


表 1. 一些流行處理器架構的寄存器和物理地址大小

架構

寄存器帶寬(位)

物理地址大小(位)

(現代)Intel® x86

32

32
36,具備 物理地址擴展(Pentium Pro 和更高型號)

x86 64

64

目前爲 48 位(之後將會增大)

PPC64

64

在 POWER 5 上爲 50 

390 31 

32

31

390 64 

64

64

 

操做系統和虛擬內存

若是您編寫無需操做系統,直接在處理器上運行的應用程序,您能夠使用處理器能夠尋址的全部內存(假設鏈接到了足夠的物理 RAM)。可是要使用多任務和硬件抽象等特性,幾乎全部人都會使用某種類型的操做系統來運行他們的程序。

在 Windows 和 Linux 等多任務操做系統中,有多個程序在使用系統資源。須要爲每一個程序分配物理內存區域來在其中運行。能夠設計這樣一個操做系統:每一個程序直接使用物理內存,並 且能夠可靠地僅使用分配給它的內存。一些嵌入式操做系統以這種方式工做,可是這在包含多個未通過集中測試的應用程序的環境中是不切實際的,由於任何程序都 可能破壞其餘程序或者操做系統自己的內存。

虛擬內存 容許多個進程共享物理內存,並且不會破壞彼此的數據。在具備虛擬內存的操做系統(好比 WindowsLinux 和許多其餘操做系統)中,每一個程序都擁有本身的虛擬地址空間 —— 一個邏輯地址區域,其大小由該系統上的地址大小規定(因此,桌面和服務器平臺的虛擬地址空間爲 3132 或 64 位)。進程的虛擬地址空間中的區域可被映射到物理內存、文件或任何其餘可尋址存儲。當數據未使用時,操做系統能夠在物理內存與一個交換區域 (Windows 上的頁面文件 或者 Linux 上的交換分區 )之間移動它,以實現對物理內存的最佳利用率。當一個程序 嘗試使用虛擬地址訪問內存時,操做系統連同片上硬件會將該虛擬地址映射到物理位置,這個位置能夠是物理 RAM、一個文件或頁面文件/交換分區。若是一個內存區域被移動到交換空間,那麼它將在被使用以前加載回物理內存中。圖 展現了虛擬內存如何將進程地址空間區域映射到共享資源:


圖 1. 虛擬內存將進程地址空間映射到物理資源 

程序的每一個實例以進程 的形式運行。在 Linux 和 Windows 上,進程是一個由受操做系統控制的資源(好比文件和套接字信息)、一個典型的虛擬地址空間(在某些架構上不止一個)和至少一個執行線程構成的集合。

虛擬地址空間大小可能比處理器的物理地址大小更小。32 位 Intel x86 最初擁有的 32 位物理地址僅容許處理器尋址 4GB 存儲空間。後來,添加了一種稱爲物理地址擴展(Physical Address ExtensionPAE)的特性,將物理地址大小擴大到了 36 位,容許安裝或尋址至多 64GB RAMPAE 容許操做系統將 32 位的 4GB 虛擬地址空間映射到一個較大的物理地址範圍,可是它不容許每一個進程擁有 64GB 虛擬地址空間。這意味着若是您將大於 4GB 的內存放入 32 位 Intel 服務器中,您將沒法將全部內存直接映射到一個單一進程中。

地址窗口擴展(Address Windowing Extension)特性容許 Windows 進程將其 32 位地址空間的一部分做爲滑動窗口映射到較大的內存區域中。Linux 使用相似的技術將內存區域映射到虛擬地址空間中。這意味着儘管您沒法直接引用大於 4GB 的內存,但您仍然能夠使用較大的內存區域。

內核空間和用戶空間

儘管每一個進程都有其本身的地址空間,但程序一般沒法使用全部這些空間。地址空間被劃分爲用戶空間 和內核空間 。 內核是主要的操做系統程序,包含用於鏈接計算機硬件、調度程序以及提供聯網和虛擬內存等服務的邏輯。

做爲計算機啓動序列的一部分,操做系統內核運行並初始化硬件。一旦內核配置了硬件及其本身的內部狀態,第一個用戶空間進程就會啓動。若是用戶 程序須要來自操做系統的服務,它能夠執行一種稱爲系統調用 的操做與內核程序交互,內核程序而後執行該請求。系統調用一般是讀取和寫入文件、聯網和啓動新進程等操做所必需的。

當執行系統調用時,內核須要訪問其本身的內存和調用進程的內存。由於正在執行當前線程的處理器被配置爲使用地址空間映射來爲當前進程映射虛擬 地址,因此大部分操做系統將每一個進程地址空間的一部分映射到一個通用的內核內存區域。被映射來供內核使用的地址空間部分稱爲內核空間,其他部分稱爲用戶空 間,可供用戶應用程序使用。

內核空間和用戶空間之間的平衡關係因操做系統的不一樣而不一樣,甚至在運行於不一樣硬件架構之上的同一操做系統的各個實例間也有所不一樣。這種平衡通 常是可配置的,可進行調整來爲用戶應用程序或內核提供更多空間。縮減內核區域可能致使一些問題,好比可以同時登陸的用戶數量限制或可以運行的進程數量限 制。更小的用戶空間意味着應用程序編程人員只能使用更少的內存空間。

默認狀況下,32 位 Windows 擁有 2GB 用戶空間和 2GB 內核空間。在一些 Windows 版本上,經過向啓動配置添加 /3GB 開關並使用 /LARGEADDRESSAWARE 開關從新連接應用程序,能夠將這種平衡調整爲 3GB 用戶空間和 1GB 內核空間。在 32 位 Linux 上,默認設置爲 3GB 用戶空間和 1GB 內核空間。一些 Linux 分發版提供了一個 hugemem 內核,支持 4GB 用戶空間。爲了實現這種配置,將進行系統調用時使用的地址空間分配給內核。經過這種方式增長用戶空間會減慢系統調用,由於每次進行系統調用時,操做系統必 須在地址空間之間複製數據並重置進程地址-空間映射。圖 展現了 32 位 Windows 的地址-空間佈局:


圖 2. 32 位 Windows 的地址-空間佈局 

圖 顯示了 32 位 Linux 的地址-空間配置:


圖 3. 32 位 Linux 的地址-空間佈局 

31 位 Linux 390 上還使用了一個獨立的內核地址空間,其中較小的 2GB 地址空間使對單個地址空間進行劃分不太合理,可是,390 架構能夠同時使用多個地址空間,並且不會下降性能。

進程空間必須包含程序須要的全部內容,包括程序自己和它使用的共享庫(在 Windows 上爲 DDL,在 Linux 上爲 .so 文件)。共享庫不只會佔據空間,使程序沒法在其中存儲數據,它們還會使地址空間碎片化,減小可做爲連續內存塊分配的內存。這對於在擁有 3GB 用戶空間的 Windows x86 上運行的程序尤其明顯。DLL 在構建時設置了首選的加載地址:當加載 DLL 時,它被映射處處於特定位置的地址空間,除非該位置已經被佔用,在這種狀況下,它會加載到別處。Windows NT 最初設計時設置了 2GB 可用用戶空間,這對於要構建來加載接近 2GB 區域的系統庫頗有用 —— 使大部分用戶區域均可供應用程序自由使用。當用戶區域擴展到 3GB 時,系統共享庫仍然加載接近 2GB 數據(約爲用戶空間的一半)。儘管整體用戶空間爲 3GB,可是不可能分配 3GB 大的內存塊,由於共享庫沒法加載這麼大的內存。

在 Windows 中使用 /3GB 開關,能夠將內核空間減小一半,也就是最初設計的大小。在一些情形下,可能耗盡 1GB 內核空間,使 I/O 變得緩慢,且沒法正常建立新的用戶會話。儘管 /3GB 開關可能對一些應用程序很是有用,但任何使用它的環境在部署以前都應該進行完全的負載測試。

本機內存泄漏或過分使用本機內存將致使不一樣的問題,具體取決於您是耗盡了地址空間仍是用完了物理內存。耗盡地址空間一般只會發生在 32 位進程上,由於最大 4GB 的內存很容易分配完。64 位進程具備數百或數千 GB 的用戶空間,即便您特地消耗空間也很難耗盡這麼大的空間。若是您確實耗盡了 Java 進程的地址空間,那麼 Java 運行時可能會出現一些陌生現象,本文稍後將詳細討論。當在進程地址空間比物理內存大的系統上運行時,內存泄漏或過分使用本機內存會迫使操做系統交換後備存 儲器來用做本機進程的虛擬地址空間。訪問通過交換的內存地址比讀取駐留(在物理內存中)的地址慢得多,由於操做系統必須從硬盤驅動器拉取數據。可能會分配 大量內存來用完全部物理內存和全部交換內存(頁面空間),在 Linux 上,這將觸發內核內存不足(OOM)結束程序,強制結束最消耗內存的進程。在 Windows 上,與地址空間被佔滿時同樣,內存分配將會失敗。

同時,若是嘗試使用比物理內存大的虛擬內存,顯然在進程因爲消耗內存太大而被結束以前就會遇到問題。系統將變得異常緩慢,由於它會將大部分時 間用於在內存與交換空間之間來回複製數據。當發生這種狀況時,計算機和獨立應用程序的性能將變得很是糟糕,從而使用戶意識到出現了問題。當 JVM 的 Java 堆被交換出來時,垃圾收集器的性能會變得很是差,應用程序可能被掛起。若是一臺機器上同時使用了多個 Java 運行時,那麼物理內存必須足夠分配給全部 Java 堆。

Java 運行時如何使用本機內存

Java 運行時是一個操做系統進程,它會受到我在上一節中列出的硬件和操做系統侷限性的限制。運行時環境提供的功能受一些未知的用戶代碼驅動,這使得沒法預測在每 種情形中運行時環境將須要何種資源。Java 應用程序在託管 Java 環境中執行的每一個操做都會潛在地影響提供該環境的運行時的需求。本節描述 Java 應用程序爲何和如何使用本機內存。

Java 堆和垃圾收集

Java 堆是分配了對象的內存區域。大多數 Java SE 實現都擁有一個邏輯堆,可是一些專家級 Java 運行時擁有多個堆,好比實現 Java 實時規範(Real Time Specification for JavaRTSJ)的運行時。一個物理堆可被劃分爲多個邏輯扇區,具體取決於用於管理堆內存的垃圾收集(GC)算法。這些扇區一般實現爲連續的本機內存 塊,這些內存塊受 Java 內存管理器(包含垃圾收集器)控制。

堆的大小能夠在 Java 命令行使用 -Xmx 和 -Xms 選項來控制(mx 表示堆的最大大小,ms 表示初始大小)。儘管邏輯堆(常常被使用的內存區域)能夠根據堆上的對象數量和在 GC 上花費的時間而增大和縮小,但使用的本機內存大小保持不變,並且由 -Xmx 值(最大堆大小)指定。大部分 GC 算法依賴於被分配爲連續的內存塊的堆,所以不能在堆須要擴大時分配更多本機內存。全部堆內存必須預先保留。

保留本機內存與分配本機內存不一樣。當本機內存被保留時,沒法使用物理內存或其餘存儲器做爲備用內存。儘管保留地址空間塊不會耗盡物理資源,但 會阻止內存被用於其餘用途。由保留從未使用的內存致使的泄漏與泄漏分配的內存同樣嚴重。

當使用的堆區域縮小時,一些垃圾收集器會回收堆的一部分(釋放堆的後備存儲空間),從而減小使用的物理內存。

對於維護 Java 堆的內存管理系統,須要更多本機內存來維護它的狀態。當進行垃圾收集時,必須分配數據結構來跟蹤空閒存儲空間和記錄進度。這些數據結構的確切大小和性質因 實現的不一樣而不一樣,但許多數據結構都與堆大小成正比。

即時 (JIT) 編譯器

JIT 編譯器在運行時編譯 Java 字節碼來優化本機可執行代碼。這極大地提升了 Java 運行時的速度,而且支持 Java 應用程序以與本機代碼至關的速度運行。

字節碼編譯使用本機內存(使用方式與 gcc 等靜態編譯器使用內存來運行同樣),但 JIT 編譯器的輸入(字節碼)和輸出(可執行代碼)必須也存儲在本機內存中。包含多個通過 JIT 編譯的方法的 Java 應用程序會使用比小型應用程序更多的本機內存。

類和類加載器

Java 應用程序由一些類組成,這些類定義對象結構和方法邏輯。Java 應用程序也使用 Java 運行時類庫(好比 java.lang.String ) 中的類,也能夠使用第三方庫。這些類須要存儲在內存中以備使用。

存儲類的方式取決於具體實現。Sun JDK 使用永久生成(permanent generationPermGen)堆區域。Java 5 的 IBM 實現會爲每一個類加載器分配本機內存塊,並將類數據存儲在其中。現代 Java 運行時擁有類共享等技術,這些技術可能須要將共享內存區域映射到地址空間。要理解這些分配機制如何影響您 Java 運行時的本機內存佔用,您須要查閱該實現的技術文檔。然而,一些廣泛的事實會影響全部實現。

從最基本的層面來看,使用更多的類將須要使用更多內存。(這可能意味着您的本機內存使用量會增長,或者您必須明確地從新設置 PermGen 或共享類緩存等區域的大小,以裝入全部類)。記住,不只您的應用程序須要加載到內存中,框架、應用服務器、第三方庫以及包含類的 Java 運行時也會按需加載並佔用空間。

Java 運行時能夠卸載類來回收空間,可是隻有在很是嚴酷的條件下才會這樣作。不能卸載單個類,而是卸載類加載器,隨其加載的全部類都會被卸載。只有在如下狀況下 才能卸載類加載器:

7 Java 堆不包含對錶示該類加載器的 java.lang.ClassLoader 對象的引用。

8 Java 堆不包含對錶示類加載器加載的類的任何 java.lang.Class 對象的引用。

9 在 Java 堆上,該類加載器加載的任何類的全部對象都再也不存活(被引用)。

須要注意的是,Java 運行時爲全部 Java 應用程序建立的 個默認類加載器( bootstrap 、extension 和 application )都不可能知足這些條件,所以,任何系統類(好比 java.lang.String ) 或經過應用程序類加載器加載的任何應用程序類都不能在運行時釋放。

即便類加載器適合進行收集,運行時也只會將收集類加載器做爲 GC 週期的一部分。一些實現只會在某些 GC 週期中卸載類加載器。

也可能在運行時生成類,而不用釋放它。許多 JEE 應用程序使用 JavaServer Pages (JSP) 技術來生成 Web 頁面。使用 JSP 會爲執行的每一個 .jsp 頁面生成一個類,而且這些類會在加載它們的類加載器的整個生存期中一直存在 —— 這個生存期一般是 Web 應用程序的生存期。

另外一種生成類的常見方法是使用 Java 反射。反射的工做方式因 Java 實現的不一樣而不一樣,但 Sun 和 IBM 實現都使用了這種方法,我立刻就會講到。

當使用 java.lang.reflect API 時,Java 運行時必須將一個反射對象(好比 java.lang.reflect.Field ) 的方法鏈接到被反射到的對象或類。這能夠經過使用 Java 本機接口(Java Native InterfaceJNI)訪問器來完成,這種方法須要的設置不多,可是速度緩慢。也能夠在運行時爲您想要反射到的每種對象類型動態構建一個類。後一種 方法在設置上更慢,但運行速度更快,很是適合於常常反射到一個特定類的應用程序。

Java 運行時在最初幾回反射到一個類時使用 JNI 方法,但當使用了若干次 JNI 方法以後,訪問器會膨脹爲字節碼訪問器,這涉及到構建類並經過新的類加載器進行加載。執行屢次反射可能致使建立了許多訪問器類和類加載器。保持對反射對象 的引用會致使這些類一直存活,並繼續佔用空間。由於建立字節碼訪問器很是緩慢,因此 Java 運行時能夠緩存這些訪問器以備之後使用。一些應用程序和框架還會緩存反射對象,這進一步增長了它們的本機內存佔用。

JNI

JNI 支持本機代碼(使用 和 C++ 等本機編譯語言編寫的應用程序)調用 Java 方法,反之亦然。Java 運行時自己極大地依賴於 JNI 代碼來實現類庫功能,好比文件和網絡 I/OJNI 應用程序可能經過 種方式增長 Java 運行時的本機內存佔用:

10 JNI 應用程序的本機代碼被編譯到共享庫中,或編譯爲加載到進程地址空間中的可執行文件。大型本機應用程序可能僅僅加載就會佔用大量進程地址空間。

11 本機代碼必須與 Java 運行時共享地址空間。任何本機代碼分配或本機代碼執行的內存映射都會耗用 Java 運行時的內存。

12 某些 JNI 函數可能在它們的常規操做中使用本機內存。GetType ArrayElements 和 GetType ArrayRegion 函數能夠將 Java 堆數據複製到本機內存緩衝區中,以供本機代碼使用。是否複製數據依賴於運行時實現。(IBM Developer Kit for Java 5.0 和更高版本會進行本機複製)。經過這種方式訪問大量 Java 堆數據可能會使用大量本機堆。

NIO

Java 1.4 中添加的新 I/O (NIO) 類引入了一種基於通道和緩衝區來執行 I/O 的新方式。就像 Java 堆上的內存支持 I/O 緩衝區同樣,NIO 添加了對直接 ByteBuffer 的支持(使用 java.nio.ByteBuffer.allocateDirect()方法進行分配), ByteBuffer 受本機內存而不是 Java 堆支持。直接 ByteBuffer 能夠直接傳遞到本機操做系統庫函數,以執行 I/O — 這使這些函數在一些場景中要快得多,由於它們能夠避免在 Java 堆與本機堆之間複製數據。

對於在何處存儲直接 ByteBuffer 數據,很容易產生混淆。應用程序仍然在 Java 堆上使用一個對象來編排 I/O 操做,但持有該數據的緩衝區將保存在本機內存中,Java 堆對象僅包含對本機堆緩衝區的引用。非直接ByteBuffer 將其數據保存在 Java 堆上的 byte[] 數組中。圖 展現了直接與非直接 ByteBuffer 對象之間的區別:


圖 4. 直接與非直接 java.nio.ByteBuffer 的內存拓撲結構 

直接 ByteBuffer 對象會自動清理本機緩衝區,但這個過程只能做爲 Java 堆 GC 的一部分來執行,所以它們不會自動響應施加在本機堆上的壓力。GC 僅在 Java 堆被填滿,以致於沒法爲堆分配請求提供服務時發生,或者在 Java 應用程序中顯式請求它發生(不建議採用這種方式,由於這可能致使性能問題)。

發生垃圾收集的情形多是,本機堆被填滿,而且一個或多個直接 ByteBuffers 適合於垃圾收集(而且能夠被釋放來騰出本機堆的空間),但 Java 堆幾乎老是空的,因此不會發生垃圾收集。

線程

應用程序中的每一個線程都須要內存來存儲器堆棧 (用於在調用函數時持有局部變量並維護狀態的內存區域)。每一個 Java 線程都須要堆棧空間來運行。根據實現的不一樣,Java 線程能夠分爲本機線程和 Java 堆棧。除了堆棧空間,每一個線程還須要爲線程本地存儲(thread-local storage)和內部數據結構提供一些本機內存。

堆棧大小因 Java 實現和架構的不一樣而不一樣。一些實現支持爲 Java 線程指定堆棧大小,其範圍一般在 256KB 到 756KB 之間。

儘管每一個線程使用的內存量很是小,但對於擁有數百個線程的應用程序來講,線程堆棧的總內存使用量可能很是大。若是運行的應用程序的線程數量比 可用於處理它們的處理器數量多,效率一般很低,而且可能致使糟糕的性能和更高的內存佔用。

 

 

本機內存耗盡會發生什麼?

Java 運行時善於以不一樣的方式來處理 Java 堆的耗盡與本機堆的耗盡,但這兩種情形具備相似的症狀。當 Java 堆耗盡時,Java 應用程序很難正常運行,由於 Java 應用程序必須經過分配對象來完成工做。只要 Java 堆被填滿,就會出現糟糕的 GC 性能並拋出表示 Java 堆被填滿的 OutOfMemoryError 

相反,一旦 Java 運行時開始運行而且應用程序處於穩定狀態,它能夠在本機堆徹底耗盡以後繼續正常運行。不必定會發生奇怪的行爲,由於須要分配本機內存的操做比須要分配 Java 堆的操做少得多。儘管須要本機內存的操做因 JVM 實現不一樣而異,但也有一些操做很常見:啓動線程、加載類以及執行某種類型的網絡和文件 I/O

本機內存不足行爲與 Java 堆內存不足行爲也不太同樣,由於沒法對本機堆分配進行單點控制。儘管全部 Java 堆分配都在 Java 內存管理系統控制之下,但任何本機代碼(不管其位於 JVMJava 類庫仍是應用程序代碼中)均可能執行本機內存分配,並且會失敗。嘗試進行分配的代碼而後會處理這種狀況,不管設計人員的意圖是什麼:它可能經過 JNI 接口拋出一個 OutOfMemoryError ,在屏幕上輸出一條消息,發生無提示失敗並在稍後再試一次,或者執行其餘操 做。

缺少可預測行爲意味着沒法肯定本機內存是否耗盡。相反,您須要使用來自操做系統和 Java 運行時的數據執行診斷。

 

 

本機內存耗盡示例

爲了幫助您瞭解本機內存耗盡如何影響您正使用的 Java 實現,本文的示例代碼中包含了一些 Java 程序,用於以不一樣方式觸發本機堆耗盡。這些示例使用經過 語言編寫的本機庫來消耗全部本機地址空間,而後嘗試執行一些使用本機內存的操做。提供的示例已通過編譯,編譯它們的指令包含在示例包的頂級目錄下的 README.html 文件中。

com.ibm.jtc.demos.NativeMemoryGlutton 類提供了 gobbleMemory() 方法,它在一個循環中調用 malloc ,直到幾乎全部本 機內存都已耗盡。完成任務以後,它經過如下方式輸出分配給標準錯誤的字節數:

Allocated 1953546736 bytes of native memory before running out

 

針對在 32 位 Windows 上運行的 Sun 和 IBM Java 運行時的每次演示,其輸出都已被捕獲。提供的二進制文件已在如下操做系統上進行了測試:

13 Linux x86

14 Linux PPC 32

15 Linux 390 31

16 Windows x86

使用如下 Sun Java 運行時版本捕獲輸出:

java version "1.5.0_11"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_11-b03)
Java HotSpot(TM) Client VM (build 1.5.0_11-b03, mixed mode)

 

使用的 IBM Java 運行時版本爲:

java version "1.5.0"
Java(TM) 2 Runtime Environment, Standard Edition (build pwi32devifx-20071025 (SR
6b))
IBM J9 VM (build 2.3, J2RE 1.5.0 IBM J9 2.3 Windows XP x86-32 j9vmwi3223-2007100
7 (JIT enabled)
J9VM - 20071004_14218_lHdSMR
JIT  - 20070820_1846ifx1_r8
GC   - 200708_10)
JCL  - 20071025

 

在耗盡本機內存時嘗試啓動線程

com.ibm.jtc.demos.StartingAThreadUnderNativeStarvation 類嘗試在耗盡進程地址空間時啓動一個線程。這是發現 Java 進程已耗盡內存的一種經常使用方式,由於許多應用程序都會在其整個生存期啓動線程。

當在 IBM Java 運行時上運行時,StartingAThreadUnderNativeStarvation 演示的輸出以下:

Allocated 1019394912 bytes of native memory before running out
JVMDUMP006I Processing Dump Event "systhrow", detail
"java/lang/OutOfMemoryError" - Please Wait.
JVMDUMP007I JVM Requesting Snap Dump using 'C:/Snap0001.20080323.182114.5172.trc'
JVMDUMP010I Snap Dump written to C:/Snap0001.20080323.182114.5172.trc
JVMDUMP007I JVM Requesting Heap Dump using 'C:/heapdump.20080323.182114.5172.phd'
JVMDUMP010I Heap Dump written to C:/heapdump.20080323.182114.5172.phd
JVMDUMP007I JVM Requesting Java Dump using 'C:/javacore.20080323.182114.5172.txt'
JVMDUMP010I Java Dump written to C:/javacore.20080323.182114.5172.txt
JVMDUMP013I Processed Dump Event "systhrow", detail "java/lang/OutOfMemoryError".
java.lang.OutOfMemoryError: ZIP006:OutOfMemoryError, ENOMEM error in ZipFile.open
   at java.util.zip.ZipFile.open(Native Method)
   at java.util.zip.ZipFile.<init>(ZipFile.java:238)
   at java.util.jar.JarFile.<init>(JarFile.java:169)
   at java.util.jar.JarFile.<init>(JarFile.java:107)
   at com.ibm.oti.vm.AbstractClassLoader.fillCache(AbstractClassLoader.java:69)
   at com.ibm.oti.vm.AbstractClassLoader.getResourceAsStream(AbstractClassLoader.java:113)
   at java.util.ResourceBundle$1.run(ResourceBundle.java:1101)
   at java.security.AccessController.doPrivileged(AccessController.java:197)
   at java.util.ResourceBundle.loadBundle(ResourceBundle.java:1097)
   at java.util.ResourceBundle.findBundle(ResourceBundle.java:942)
   at java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:779)
   at java.util.ResourceBundle.getBundle(ResourceBundle.java:716)
   at com.ibm.oti.vm.MsgHelp.setLocale(MsgHelp.java:103)
   at com.ibm.oti.util.Msg$1.run(Msg.java:44)
   at java.security.AccessController.doPrivileged(AccessController.java:197)
   at com.ibm.oti.util.Msg.<clinit>(Msg.java:41)
   at java.lang.J9VMInternals.initializeImpl(Native Method)
   at java.lang.J9VMInternals.initialize(J9VMInternals.java:194)
   at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:764)
   at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:758)
   at java.lang.Thread.uncaughtException(Thread.java:1315)
K0319java.lang.OutOfMemoryError: Failed to fork OS thread
   at java.lang.Thread.startImpl(Native Method)
   at java.lang.Thread.start(Thread.java:979)
   at com.ibm.jtc.demos.StartingAThreadUnderNativeStarvation.main(
StartingAThreadUnderNativeStarvation.java:22)

 

調用 java.lang.Thread.start() 來嘗試爲一個新的操做系統線程分配內存。此嘗試會失敗並拋出 OutOfMemoryError JVMDUMP 行通知用戶 Java 運行時已經生成了標準的 OutOfMemoryError 調試數據。

嘗試處理第一個 OutOfMemoryError 會致使第二個錯誤 —— :OutOfMemoryError, ENOMEM error in ZipFile.open 。當本機進程內存耗盡時一般會拋出多個 OutOfMemoryError Failed to fork OS thread 多是在耗盡本機內存時最多見的消息。

本文提供的示例會觸發一個 OutOfMemoryError 集羣,這比您在本身的應用程序中看到的狀況要嚴重得多。這必定程度上是由於幾乎全部本機內存都已被使用,與實際的應用程序不一樣,使用的內存不會在之後被釋 放。在實際應用程序中,當拋出 OutOfMemoryError 時,線程會關閉,而且可能會釋放一部分本機內存,以讓運行時處理錯誤。測試案例的這個細微特性還意味着,類庫的許多部分(好比安全系統)未被初始化,並且 它們的初始化受嘗試處理內存耗盡情形的運行時驅動。在實際應用程序中,您可能會看到顯示了不少錯誤,但您不太可能在一個位置看到全部這些錯誤。

在 Sun Java 運行時上執行相同的測試案例時,會生成如下控制檯輸出:

Allocated 1953546736 bytes of native memory before running out
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
   at java.lang.Thread.start0(Native Method)
   at java.lang.Thread.start(Thread.java:574)
   at com.ibm.jtc.demos.StartingAThreadUnderNativeStarvation.main(
StartingAThreadUnderNativeStarvation.java:22)

 

儘管堆棧軌跡和錯誤消息稍有不一樣,但其行爲在本質上是同樣的:本機分配失敗並拋出 java.lang.OutOfMemoryError 。 此場景中拋出的 OutOfMemoryError 與因爲 Java 堆耗盡而拋出的錯誤的唯一區別在於消息。

嘗試在本機內存耗盡時分配直接 ByteBuffer

com.ibm.jtc.demos.DirectByteBufferUnderNativeStarvation 類嘗試在地址空間耗盡時分配一個直接(也就是受本機支持的)java.nio.ByteBuffer 對象。當在 IBM Java 運行時上運行時,它生成如下輸出:

Allocated 1019481472 bytes of native memory before running out
JVMDUMP006I Processing Dump Event "uncaught", detail
"java/lang/OutOfMemoryError" - Please Wait.
JVMDUMP007I JVM Requesting Snap Dump using 'C:/Snap0001.20080324.100721.4232.trc'
JVMDUMP010I Snap Dump written to C:/Snap0001.20080324.100721.4232.trc
JVMDUMP007I JVM Requesting Heap Dump using 'C:/heapdump.20080324.100721.4232.phd'
JVMDUMP010I Heap Dump written to C:/heapdump.20080324.100721.4232.phd
JVMDUMP007I JVM Requesting Java Dump using 'C:/javacore.20080324.100721.4232.txt'
JVMDUMP010I Java Dump written to C:/javacore.20080324.100721.4232.txt
JVMDUMP013I Processed Dump Event "uncaught", detail "java/lang/OutOfMemoryError".
Exception in thread "main" java.lang.OutOfMemoryError:
Unable to allocate 1048576 bytes of direct memory after 5 retries
   at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:167)
   at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:303)
   at com.ibm.jtc.demos.DirectByteBufferUnderNativeStarvation.main(
   DirectByteBufferUnderNativeStarvation.java:29)
Caused by: java.lang.OutOfMemoryError
   at sun.misc.Unsafe.allocateMemory(Native Method)
   at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:154)
   ... 2 more

 

在此場景中,拋出了 OutOfMemoryError ,它會觸發默認的錯誤文檔。OutOfMemoryError 到達主線程堆棧的頂部,並在 stderr 上輸出。

當在 Sun Java 運行時上運行時,此測試案例生成如下控制檯輸出:

Allocated 1953546760 bytes of native memory before running out
Exception in thread "main" java.lang.OutOfMemoryError
   at sun.misc.Unsafe.allocateMemory(Native Method)
   at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:99)
   at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)
   at com.ibm.jtc.demos.DirectByteBufferUnderNativeStarvation.main(
DirectByteBufferUnderNativeStarvation.java:29)

 

調試方法和技術

查閱供應商文檔 
本文提供的指南是通常的調試原則,可用於理解本機內存耗盡場景。您的運行時供應商可能提供了本身的調試說明,供應商指望您按照這些說明與其 支持團隊聯繫。若是您要與運行時供應商(包括 IBM)合做解決問題,請始終檢查其調試和診斷文檔,查看在提交問題報告時應該執行哪些步驟。

 

當出現 java.lang.OutOfMemoryError 或看到有關內存不足的錯誤消息時,要作的第一件事是肯定哪一種類型的內存被耗盡。最簡單的方式是首先檢查 Java 堆是否被填滿。若是 Java 堆未致使 OutOfMemory 條件,那麼您應該分析本機堆使用狀況。

檢查 Java 

檢查堆使用狀況的方法因 Java 實現不一樣而異。在 Java 5 和 的 IBM 實現上,當拋出 OutOfMemoryError 時會生成一個 javacore 文件來告訴您。javacore 文件一般在 Java 進程的工做目錄中生成,以 javacore.日期 .時 間 .pid .txt 的形式命名。若是您在文本編輯器中打開該文件,能夠看到如下信息:

0SECTION       MEMINFO subcomponent dump routine
NULL           =================================
1STHEAPFREE    Bytes of Heap Space Free: 416760
1STHEAPALLOC   Bytes of Heap Space Allocated: 1344800

 

這部分信息顯示在生成 javacore 時有多少空閒的 Java 堆。注意,顯示的值爲十六進制格式。若是由於分配條件不知足而拋出了 OutOfMemoryError 異常,則 GC 軌跡部分會顯示以下信息:

1STGCHTYPE     GC History 
3STHSTTYPE     09:59:01:632262775 GMT j9mm.80 -   J9AllocateObject() returning NULL!
32 bytes requested for object of class 00147F80

 

J9AllocateObject() returning NULL! 意味着 Java 堆分配例程未成功完成,而且將拋出 OutOfMemoryError 

也可能因爲垃圾收集器運行太頻繁(意味着堆被填滿了而且 Java 應用程序的運行速度將很慢或中止運行)而拋出 OutOfMemoryError 。 在這種狀況下,您可能想要 Heap Space Free 值很是小,GC 軌跡將顯示如下消息之一:

1STGCHTYPE     GC History 
3STHSTTYPE     09:59:01:632262775 GMT j9mm.83 -     Forcing J9AllocateObject()
to fail due to excessive GC

 

1STGCHTYPE     GC History 
3STHSTTYPE     09:59:01:632262775 GMT j9mm.84 -     Forcing
J9AllocateIndexableObject() to fail due to excessive GC

 

當 Sun 實現耗盡 Java 堆內存時,它使用異常消息來顯示它耗盡的是 Java 堆:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

 

IBM 和 Sun 實現都擁有一個詳細的 GC 選項,用於在每一個 GC 週期生成顯示堆填充狀況的跟蹤數據。此信息可以使用工具(好比 IBM Monitoring and Diagnostic Tools for Java - Garbage Collection and Memory Visualizer (GCMV))來分析,以顯示 Java 堆是否在增加。

測量本機堆使用狀況

若是您肯定內存耗盡狀況不是由 Java 堆耗盡引發的,那麼下一步就是分析您的本機內存使用狀況。

Windows 提供的 PerfMon 工具可用於監控和記錄許多操做系統和進程指標,包括本機內存使用。它容許實時跟蹤計數器 ,或將其存儲在日誌文件中以供離線查看。使用 Private Bytes 計數器顯示整體地址空間使用狀況。若是顯示值接近於用戶空間的限制(前面已經討論過,介於 到 3GB 之間),您應該會看到本機內存耗盡狀況。

Linux 沒有相似於 PerfMon 的工具,可是它提供了幾個替代工具。命令行工具(好比 ps top 和 pmap )可以顯示應用程序的本機內存佔用狀況。儘管獲取進程內存使用狀況的實時快照很是有用,但經過記錄內存隨時間的 使用狀況,您可以更好地理解本機內存是如何被使用的。爲此,可以採起的一種方式是使用 GCMV

GCMV 最初編寫用於分析冗長的 GC 日誌,容許用戶在調優垃圾收集器時查看 Java 堆使用狀況和 GC 性能的變化。GCMV 後來進行了擴展,支持分析其餘數據源,包括 Linux 和 AIX 本機內存數據。GCMV 是做爲 IBM Support Assistant (ISA) 的插件發佈的。

要使用 GCMV 分析 Linux 本機內存配置文件,您首先必須使用腳本收集本機內存數據。GCMV 的 Linux 本機內存分析器經過根據時間戳隔行掃描的方式,讀取 Linux ps 命令的輸出。GCMV 提供了一個腳原本幫助以正確形式記錄收集數據。要找到該腳本:

· 下載並安裝 ISA Version 4(或更高版本),而後安裝 GCMV 工具插件。

· 啓動 ISA

· 從菜單欄單擊 Help >> Help Contents ,打開 ISA 幫助菜單。

· 在左側窗格的 Tool:IBM Monitoring and Diagnostic Tools for Java - Garbage Collection and Memory Visualizer >> Using the Garbage Collection and Memory Visualizer >> Supported Data Types >> Native memory >> Linux native memory 下找到 Linux 本機內存說明。

圖 顯示了該腳本在 ISA 幫助文件中的位置。若是您的幫助文件中沒有 GCMV Tool 條目,極可能是由於您沒有安裝 GCMV 插件。


圖 5. Linux 本機內存數據捕獲腳本在 ISA 幫助對話框中的位置 

GCMV 幫助文件中提供的腳本使用的 ps 命令僅適用於最新的 ps 版本。在一些舊的 Linux 分發版中,幫助文件中的命令將會生成錯誤信息。要查看您的 Linux 分發版上的行爲,能夠嘗試運行 ps -o pid,vsz=VSZ,rss=RSS。若是您的 ps 版本支持新的命令行參數語法,那麼獲得的輸出將相似於:

  PID    VSZ   RSS
 5826   3772  1960
 5675   2492   760

 

若是您的 ps 版本不支持新語法,獲得的輸出將相似於:

  PID VSZ,rss=RSS
 5826        3772
 5674        2488

 

若是您在一個較老的 ps 版本上運行,能夠修改本機內存腳本,將

ps -p $PID -o pid,vsz=VSZ,rss=RSS


行替換爲

ps -p $PID -o pid,vsz,rss

 

將幫助面板中的腳本複製到一個文件中(在本例中名爲 memscript.sh),找到您想要監控的 Java 進程的進程 ID (PID)(本例中爲 1234)並運行:

./memscript.sh 1234 > ps.out

 

這會把本機內存日誌寫入到 ps.out 中。要分析內存使用狀況:

· 在 ISA 中,從 Launch Activity 下拉菜單選擇 Analyze Problem 。

· 選擇接近 Analyze Problem 面板頂部的 Tools 標籤。

· 選擇 IBM Monitoring and Diagnostic Tools for Java - Garbage Collection and Memory Visualizer .

· 單擊接近工具面板底部的 Launch 按鈕。

· 單擊 Browse 按鈕並找到日誌文件。單擊 OK 啓動 GCMV

一旦您擁有了本機內存隨時間的使用狀況的配置文件,您須要肯定是存在本機內存泄漏,仍是在嘗試在可用空間中作太多事情。即便對於運行良好的 Java 應用程序,其本機內存佔用也不是從啓動開始就一成不變的。一些 Java 運行時系統(尤爲是 JIT 編譯器和類加載器)會不斷初始化,這會消耗本機內存。初始化增長的內存將高居不下,可是若是初始本機內存佔用接近於地址空間的限制,那麼僅這個前期階段就 足以致使本機內存耗盡。圖 給出了一個 Java 壓力測試示例中的 GCMV 本機內存使用狀況,其中突出顯示了前期階段。


圖 6. GCMV 的 Linux 本機內存使用示例,其中顯示了前期階段 

本機內存佔用也可能應工做負載不一樣而異。若是您的應用程序建立了較多進程來處理傳入的工做負載,或者根據應用於系統的負載量按比例分配本機存 儲(好比直接 ByteBuffer ),則可能因爲負載太高而耗盡本機內存。

因爲 JVM 前期階段的本機內存增加而耗盡本機內存,以及內存使用隨負載增長而增長,這些都是嘗試在可用空間中作太多事情的例子。在這些場景中,您的選擇是:

17 減小本機內存使用。 縮小 Java 堆大小是一個好的開端。

18 限制本機內存使用。 若是您的本機內存隨負載增長而增長,能夠採起某種方式限制負載或爲負載分配的資源。

19 增長可用地址空間。 這能夠經過如下方式實現:調優您的操做系統(例如,在 Windows 上使用 /3GB 開關增長用戶空間,或者在 Linux 上使用龐大的內核空間),更換平臺(Linux 一般擁有比 Windows 更多的用戶空間)。

一種實際的本機內存泄漏表現爲本機堆的持續增加,這些內存不會在移除負載或運行垃圾收集器時減小。內存泄漏程度因負載不一樣而不一樣,但泄漏的總 內存不會降低。泄漏的內存不可能被引用,所以它能夠被交換出去,並保持被交換出去的狀態。

當遇到內存泄漏時,您的選擇頗有限。您能夠增長用戶空間(這樣就會有更多的空間供泄漏),但這僅能延緩最終的內存耗盡。若是您擁有足夠的物理 內存和地址空間,而且會在進程地址空間耗盡以前重啓應用程序,那麼能夠容許地址空間繼續泄漏。

是什麼在使用本機內存?

一旦肯定本機內存被耗盡,下一個邏輯問題是:是什麼在使用這些內存?這個問題很難回答,由於在默認狀況下,Windows 和 Linux 不會存儲關於分配給特定內存塊的代碼路徑的信息。

當嘗試理解本機內存都到哪裏去了時,您的第一步是粗略估算一下,根據您的 Java 設置,將會使用多少本機內存。若是沒有對 JVM 工做機制的深刻知識,很可貴出精確的值,但您能夠根據如下指南粗略估算一下:

20 Java 堆佔用的內存至少爲 -Xmx 值。

21 每一個 Java 線程須要堆棧空間。堆棧空間因實現不一樣而異,可是若是使用默認設置,每一個線程至多會佔用 756KB 本機內存。

22 直接 ByteBuffer 至少會佔用提供給 allocate() 例程的內存值。

若是總數比您的最大用戶空間少得多,那麼您極可能不安全。Java 運行時中的許多其餘組件可能會分配大量內存,進而引發問題。可是,若是您的初步估算值與最大用戶空間很接近,則可能存在本機內存問題。若是您懷疑存在本機 內存泄漏,或者想要準確瞭解內存都到哪裏去了,使用一些工具將有所幫助。

Microsoft 提供了 UMDH(用戶模式轉儲堆)和 LeakDiag 工具來在 Windows 上調試本機內存增加。這兩個工具的機制相同:記錄特定內存區域被分配給了哪一個代碼路徑,並提供一種方式來定位所分配的內存不會在之後被釋放的代碼部分。我建 議您查閱文章 「Umdhtools.exe:如何使用 Umdh.exe 發現 Windows 上的內存泄漏」。在本文中,我將主要討論 UMDH 在分析存在泄漏的 JNI 應用程序時的輸出。

 

本文包含一個名爲 LeakyJNIApp 的 Java 應用程序,它循環調用一個 JNI 方法來泄漏本機內存。UMDH 命令獲取當前的本機堆的快照,以及分配每一個內存區域的代碼路徑的本機堆棧軌跡快照。經過獲取兩個快照,並使用 UMDH 來分析差別,您會獲得兩個快照之間的堆增加報告。

 

對於 LeakyJNIApp ,差別文件包含如下信息:

// _NT_SYMBOL_PATH set by default to C:/WINDOWS/symbols
//
// Each log entry has the following syntax:
//
// + BYTES_DELTA (NEW_BYTES - OLD_BYTES) NEW_COUNT allocs BackTrace TRACEID
// + COUNT_DELTA (NEW_COUNT - OLD_COUNT) BackTrace TRACEID allocations
//     ... stack trace ...
//
// where:
//
//     BYTES_DELTA - increase in bytes between before and after log
//     NEW_BYTES - bytes in after log
//     OLD_BYTES - bytes in before log
//     COUNT_DELTA - increase in allocations between before and after log
//     NEW_COUNT - number of allocations in after log
//     OLD_COUNT - number of allocations in before log
//     TRACEID - decimal index of the stack trace in the trace database
//         (can be used to search for allocation instances in the original
//         UMDH logs).
//

+  412192 ( 1031943 - 619751)    963 allocs     BackTrace00468

Total increase == 412192

 

重要的一行是 + 412192 ( 1031943 - 619751) 963 allocs BackTrace00468 。它顯示一個 backtrace 進行了 963 次分配,並且分配的內存都沒有釋放 — 總共使用了 412192 字節內存。經過查看一個快照文件,您能夠將BackTrace00468 與有意義的代碼路徑關聯起來。在第一個快照文件中搜索 BackTrace00468 ,能夠找到以下信息:

000000AD bytes in 0x1 allocations (@ 0x00000031 + 0x0000001F) by: BackTrace00468
        ntdll!RtlpNtMakeTemporaryKey+000074D0
        ntdll!RtlInitializeSListHead+00010D08
        ntdll!wcsncat+00000224
        leakyjniapp!Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod+000000D6

 

這顯示內存泄漏來自 Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod 函數中的 leakyjniapp.dll 模塊。

在編寫本文時,Linux 沒有相似於 UMDH 或 LeakDiag 的工具。但在 Linux 上仍然能夠採用許多方式來調試本機內存泄漏。Linux 上提供的許多內存調試器可分爲如下類別:

23 預處理器級別。 這些工具須要將一個頭文件編譯到被測試的源代碼中。能夠使用這些工具之一從新編譯您本身的 JNI 庫,以跟蹤您代碼中的本機內存泄漏。除非您擁有 Java 運行時自己的源代碼,不然這種方法沒法在 JVM 中發現內存泄漏(甚至很難在隨後將這類工具編譯到 JVM 等大型項目中,而且編譯很是耗時)。Dmalloc 就是這類工具的一個例子。

24 連接程序級別。 這些工具將被測試的二進制文件連接到一個調試庫。再一次,儘管這對個別 JNI 庫是可行的,但不推薦將其用於整個 Java 運行時,由於運行時供應商不太可能支持您運行修改的二進制文件。Ccmalloc 是這類工具的一個例子。

25 運行時連接程序級別。 這些工具使用 LD_PRELOAD 環境變量預先加載一個庫,這個庫將標準內存例程替換爲指定的版本。這些工具不須要從新編譯或從新連接源代碼,但其中許多工具與 Java 運行時不太兼容。Java 運行時是一個複雜的系統,能夠以很是規的方式使用內存和線程,這一般會干擾或破壞這類工具。您能夠試驗一下,看看是否有一些工具適用於您的場景。 NJAMD 是這類工具的一個例子。

26 基於模擬程序。 Valgrind memcheck 工具是這類內存調試器的唯一例子。它模擬底層處理器,與 Java 運行時模擬 JVM 的方式相似。能夠在 Valgrind 下運行 Java 應用程序,可是會有嚴重的性能影響(速度會減慢 10 到 30 倍),這意味着難以經過這種方式運行大型、複雜的 Java 應用程序。Valgrind 目前可在 Linux x86AMD64PPC 32 和 PPC 64 上使用。若是您使用 Valgrind,請在使用它以前嘗試使用最小的測試案例來將減輕性能問題(若是可能,最好移除整個 Java 運行時)。

對於可以容忍這種性能開銷的簡單場景,Valgrind memcheck 是最簡單且用戶友好的免費工具。它可以爲泄漏內存的代碼路徑提供完整的堆棧軌跡,提供方式與 Windows 上的 UMDH 相同。

LeakyJNIApp 很是簡單,可以在 Valgrind 下運行。當模擬的程序結束時,Valgrind memcheck 工具可以輸出泄漏的內存的彙總信息。默認狀況下,LeakyJNIApp 程序會一直運行,要使其在固定時期以後關閉,能夠將運行時間(以秒爲單位)做爲唯一的命令行參數進行傳遞。

一些 Java 運行時以很是規的方式使用線程堆棧和處理器寄存器,這可能使一些調試工具產生混淆,這些工具要求本機程序聽從寄存器使用和堆棧結構的標準約定。當使用 Valgrind 調試存在內存泄漏的 JNI 應用程序時,您能夠發現許多與內存使用相關的警告,而且一些線程堆棧看起來很奇怪,這是由 Java 運行時在內部構造其數據的方式所致使的,不用擔憂。

要使用 Valgrind memcheck 工具跟蹤 LeakyJNIApp , (在一行上)使用如下命令:

valgrind --trace-children=yes --leak-check=full
java -Djava.library.path=. com.ibm.jtc.demos.LeakyJNIApp 10

 

--trace-children=yes 選項使 Valgrind 跟蹤由 Java 啓動器啓動的任何進程。一些 Java 啓動器版本會從新執行其自己(它們從頭從新啓動其自己,再次設置環境變量來改變行爲)。若是您未指定 --trace-children , 您將不能跟蹤實際的 Java 運行時。

--leak-check=full 選項請求在代碼運行結束時輸出對泄漏的代碼區域的完整堆棧軌跡,而不僅是彙總內存的狀態。

當該命令運行時,Valgrind 輸出許多警告和錯誤(在此環境中,其中大部分都是無心義的),最後按泄漏的內存量升序輸出存在泄漏的調用堆棧。在 Linux x86 上,針對 LeakyJNIApp 的 Valgrind 輸出的彙總部分結尾以下:

==20494== 8,192 bytes in 8 blocks are possibly lost in loss record 36 of 45
==20494==    at 0x4024AB8: malloc (vg_replace_malloc.c:207)
==20494==    by 0x460E49D: Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod
(in /home/andhall/LeakyJNIApp/libleakyjniapp.so)
==20494==    by 0x535CF56: ???
==20494==    by 0x46423CB: gpProtectedRunCallInMethod
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x46441CF: signalProtectAndRunGlue
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x467E0D1: j9sig_protect
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9prt23.so)
==20494==    by 0x46425FD: gpProtectAndRun
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x4642A33: gpCheckCallin
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x464184C: callStaticVoidMethod
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x80499D3: main
(in /usr/local/ibm-java2-i386-50/jre/bin/java)
==20494==
==20494==
==20494== 65,536 (63,488 direct, 2,048 indirect) bytes in 62 blocks are definitely
lost in loss record 42 of 45
==20494==    at 0x4024AB8: malloc (vg_replace_malloc.c:207)
==20494==    by 0x460E49D: Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod
(in /home/andhall/LeakyJNIApp/libleakyjniapp.so)
==20494==    by 0x535CF56: ???
==20494==    by 0x46423CB: gpProtectedRunCallInMethod
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x46441CF: signalProtectAndRunGlue
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x467E0D1: j9sig_protect
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9prt23.so)
==20494==    by 0x46425FD: gpProtectAndRun
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x4642A33: gpCheckCallin
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x464184C: callStaticVoidMethod
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x80499D3: main
(in /usr/local/ibm-java2-i386-50/jre/bin/java)
==20494==
==20494== LEAK SUMMARY:
==20494==    definitely lost: 63,957 bytes in 69 blocks.
==20494==    indirectly lost: 2,168 bytes in 12 blocks.
==20494==      possibly lost: 8,600 bytes in 11 blocks.
==20494==    still reachable: 5,156,340 bytes in 980 blocks.
==20494==         suppressed: 0 bytes in 0 blocks.
==20494== Reachable blocks (those to which a pointer was found) are not shown.
==20494== To see them, rerun with: --leak-check=full --show-reachable=yes

 

堆棧的第二行顯示內存是由 com.ibm.jtc.demos.LeakyJNIApp.nativeMethod() 方法泄漏的。

也能夠使用一些專用調試應用程序來調試本機內存泄漏。隨着時間的推移,會有更多工具(包括開源和專用的)被開發出來,這對於研究當前技術的發 展示狀頗有幫助。

就目前而言,使用免費工具調試 Linux 上的本機內存泄漏比在 Windows 上完成相同的事情更具挑戰性。UMDH 支持就地 調試 Windows 上本機內存泄漏,在 Linux 上,您可能須要進行一些傳統的調試,而不是依賴工具來解決問題。下面是一些建議的調試步驟:

27 提取測試案例。 生成一個獨立環境,您須要可以在該環境中再現本機內存泄漏。這將使調試更加簡單。

28 儘量縮小測試案例。 嘗試禁用函數來肯定是哪些代碼路徑致使了本機內存泄漏。若是您擁有本身的 JNI 庫,能夠嘗試一次禁用一個來肯定是哪一個庫致使了內存泄漏。

29 縮小 Java 堆大小。 Java 堆多是進程的虛擬地址空間的最大使用者。經過減少 Java 堆,能夠將更多空間提供給本機內存的其餘使用者。

30 關聯本機進程大小。 一旦您得到了本機內存隨時間的使用狀況,能夠將其與應用程序工做負載和 GC 數據比較。若是泄漏程度與負載級別成正比,則意味着泄漏是由每一個事務或操做路徑上的某個實體引發的。若是當進行垃圾收集時,本機進程大小顯著減少,這意味 着您沒遇到內存泄漏,您擁有的是具備本機支持的對象組合(好比直接 ByteBuffer )。經過縮小 Java 堆大小(從而迫使垃圾收集更頻繁地發生),或者在一個對象緩存中管理對象(而不是依賴於垃圾收集器來清理對象),您能夠減小本機支持對象持有的內存量。

若是您肯定內存泄漏或增加來自於 Java 運行時自己,您可能須要聯繫運行時供應商來進一步調試。

消除限制:更改成 64 

使用 32 位 Java 運行時很容易遇到本機內存耗盡的狀況,由於地址空間相對較小。32 位操做系統提供的 到 4GB 用戶空間一般小於系統附帶的物理內存量,並且現代的數據密集型應用程序很容易耗盡可用空間。

若是 32 位地址空間不夠您的應用程序使用,您能夠經過移動到 64 位 Java 運行時來得到更多用戶空間。若是您運行的是 64 位操做系統,那麼 64 位 Java 運行時將可以知足海量 Java 堆的需求,還會減小與地址空間相關的問題。表 列出了 64 位操做系統上目前可用的用戶空間。


表 2. 64 位操做系統上的用戶空間大小

操做系統

默認用戶空間大小

Windows x86-64

8192GB

Windows Itanium

7152GB

Linux x86-64

500GB

Linux PPC64

1648GB

Linux 390 64

4EB

然而,移動到 64 位並非全部本機內存問題的通用解決方案,您仍然須要足夠的物理內存來持有全部數據。若是物理內存不夠 Java 運行時使用,運行時性能將變得很是糟,由於操做系統不得不在內存與交換空間之間來回複製 Java 運行時數據。出於相同緣由,移動到 64 位也不是內存泄漏永恆的解決方案,您只是提供了更多空間來供泄漏,這隻會延緩您不得不重啓應用程序的時間。

沒法在 64 位運行時中使用 32 位本機代碼。任何本機代碼(JNI 庫、JVM Tool Interface [JVMTI]JVM Profiling Interface [JVMPI] 以及 JVM Debug Interface [JVMDI] 代理)都必須編譯爲 64 位。64 位運行時的性能也可能比相同硬件上對應的 32 位運行時更慢。64 位運行時使用 64 位指針(本機地址引用),所以,64 位運行時上的 Java 對象會佔用比 32 位運行時上包含相同數據的對象更多的空間。更大的對象意味着要使用更大的堆來持有相同的數據量,同時保持相似的 GC 性能,這使操做系統和硬件緩存效率更低。使人驚訝的是,更大的 Java 堆並不必定意味着更長的 GC 暫停時間,由於堆上的活動數據量可能不會增長,而且一些 GC 算法在使用更大的堆時效率更高。

一些現代 Java 運行時包含減輕 64 位 「對象膨脹」 和改善性能的技術。這些功能在 64 位運行時上使用更短的引用。這在 IBM 實現中稱爲壓縮引用 ,而在 Sun 實現中稱爲壓縮 oop 。

對 Java 運行時性能的比較研究不屬於本文討論範圍,可是若是您正在考慮移動到 64 位,儘早測試應用程序以理解其執行原理會頗有幫助。因爲更改地址大小會影響到 Java 堆,因此您將須要在新架構上從新調優您的 GC 設置,而不是僅僅移植現有設置。

 

 

在設計和運行大型 Java 應用程序時,理解本機內存相當重要,可是這一點一般被忽略,由於它與複雜的硬件和操做系統細節密切相關,Java 運行時的目的正是幫助咱們規避這些細節。JRE 是一個本機進程,它必須在由這些紛繁複雜的細節定義的環境中工做。要從 Java 應用程序中得到最佳的性能,您必須理解應用程序如何影響 Java 運行時的本機內存使用。

耗盡本機內存與耗盡 Java 堆很類似,但它須要不一樣的工具集來調試和解決。修復本機內存問題的關鍵在於理解運行您的 Java 應用程序的硬件和操做系統施加的限制,並將其與操做系統工具知識結合起來,監控本機內存使用。經過這種方法,您將可以解決 Java 應用程序產生的一些很是棘手的問題。

Java 程序裏的內存泄漏 (個人收藏)

        譯序:Java 的內存泄漏,這不是一個新話題。Jim Patrick 的這篇文章早在 2001 年就寫出來了。但這並不意味着 Java 的內存泄漏是一個過期了的甚至不重要的話題。相反,Java 的內存泄漏應當是每個關心程序健壯性、高性能的程序員所必須瞭解的知識
        本文將揭示何時須要關注內存泄漏以及如何進行防止。
        摘要:Java 程序裏也存在內存泄漏?固然。和流行的見解相反,內存管理仍然是 Java 編程時應該考慮的事情。在這篇文章裏,你會了解到是什麼緣由致使了 Java 內存泄漏以及何時須要對這些泄漏進行關注。你也將會學到一個快速實用的課程以應對本身項目中的內存泄漏。夢痕文學網 http://www.menghen.net/
        Java 程序裏的內存泄漏是如何表現的
        大多數程序員都知道使用相似於 Java 的編程語言的好處之一就是他們無需再爲內存的分配和釋放所擔憂了。你只須要簡單地建立對象,當它們再也不爲程序所須要時 Java 會自行經過一個被稱爲垃圾收集的機制將其移除。這個過程意味着 Java 已經解決了困擾其餘編程語言的一個棘手的問題 -- 可怕的內存泄漏。果然是這樣的嗎?
        在進行深刻討論以前,讓咱們先回顧一下垃圾收集是如何進行實際工做的。垃圾收集器的工做就是找到程序再也不須要的對象並在當它們再也不被訪問或引用時將它們移除掉。垃圾收集器從貫穿整個程序生命週期的類這個根節點開始,掃描全部引用到的節點。在遍歷節點時,它跟蹤那些被活躍引用着的對象。那些再也不被引用的對象就知足了垃圾回收的條件。當這些對象被移除時被它們佔用的內存資源會交還給 Java 虛擬機(JVM)

        所以 Java 代碼的確不須要程序員負責內存管理的清理工做,它自行對再也不使用的對象進行垃圾收集。然而,須要記住的是,垃圾收集的關鍵在於一個對象在再也不被引用時才被統計爲再也不使用。下圖對這一律念進行了說明。

        上圖表示在一個 Java 程序執行時具備不一樣的生命週期的兩個類。類 首先被實例化,它存在的時間比較長,幾乎貫穿整個進程的生命週期。在某個時間點,類 被建立,類 添加了一個對這個新建類的引用。咱們假設類 是某個用於顯示並返回用戶指令的用戶界面部件。儘管類 再也不被使用,若是類 對類 的引用未被清除,類 將繼續存在並佔據內存空間,即便下一次垃圾收集被執行。
        何時須要注意內存泄漏?
        若是在你的程序執行一段時間以後遇到 java.lang.OutOfMemoryError 的話,內存泄漏無疑是最值得懷疑的。除了這種明顯的狀況以外,何時須要考慮內存泄漏?完美主義的程序員會回答說全部的內存泄漏都須要進行審查和更改。然而,在跳到這一結論以前還須要考慮其餘幾點因素,包括程序的生命週期以及內存泄漏的大小。
        考慮一下在一個程序的生命週期裏垃圾收集器可能從未執行的狀況。沒法保證何時 JVM 會調用垃圾收集 -- 即便程序顯式調用 System.gc()。一般狀況下,垃圾收集器不會自動運行,直到程序須要比目前可用內存還要多的內存。此時,JVM 會首先嚐試調用垃圾收集器以獲取更多可用內存。若是這個嘗試仍舊不可以釋放出足夠的資源,JVM 將會從操做系統獲取更多內存,直到達到所容許內存的最大值
        舉個例子來講,一個小型的 Java 應用程序,用來顯示一些簡單的配置修改的用戶界面元素,出現了內存泄漏。垃圾收集器可能在程序關閉以前都不會被調用到,由於 JVM 可能老是有足夠的內存來建立程序所須要的全部對象。所以,在這種狀況下,即使是一些已死對象在程序運行的時候仍舊佔據着內存,但這並不影響實際應用。
        若是開發中的 Java 代碼將以天天 24 小時運行在服務器上,這時內存泄漏將會比上面的那個配置工具程序要明顯的多了。即使是代碼中最小的內存泄漏,在持續運行的狀況下最終也將耗盡全部可用內存。
        相反的狀況下,即便一個程序只是短暫存活,卻分配了大量臨時對象(或者少許的佔用大量內存的對象),在這些對象再也不須要時沒有取消引用,這樣的 Java 代碼也會達到內存限制。
        最後一個值得注意的問題是,沒必要過於擔憂(Java 程序所形成的)內存泄漏。Java 內存泄漏不該該被認爲是像其餘語言中所發生的那樣危險,好比 C++ 的內存丟失將永遠不會返回給操做系統。Java 應用程序中,咱們把再也不須要的卻佔據着內存資源的對象都交給 JVM。因此在理論上來講,一旦 Java 程序和它的 JVM 關閉掉,全部分配的內存都將歸還給操做系統
        如何判定程序具備內存泄漏
        查看一個運行在 Windows NT 平臺上的 Java 程序是否具備內存泄漏,你能夠簡單地在程序運行的時候去觀察任務管理器中的內存設置。然而,在觀察一些運行中的 Java 程序以後,你會發現,它們跟本地應用程序相比使用更多內存。我開發過的一些 Java 項目會啓用 10 到 20 MB 的系統內存。與這個數字相比,本地的操做系統自帶的 Windows Explorer 程序使用到 5 MB
        另一個關於 Java 程序的內存使用要注意的是典型的運行在 IBM JDK1.1.8 JVM 上的程序彷佛在其運行時不斷吞噬了愈來愈多的系統內存。程序彷佛永遠不會返回一些內存給操做系統,直到一個很是大的物理內存分配給它。這會不會就是內存泄漏的跡象?
        要明白是怎麼回事,咱們須要熟悉 JVM 是如何將系統內存使用做本身的堆的。在運行 java.exe 時,你能夠使用一些特定的選項來控制垃圾收集的堆的啓動容量和最大容量(分別是 -ms 和 -mx)Sun 的 JDK 1.1.8 默認使用 1 MB 的啓動設置和 16  MB 的最大設置。IBM JDK 1.1.8 默認使用機器物理內存容量的一半做爲最大設置。這些內存設置對 JVM 發生內存溢出時的作法具備直接影響,這時 JVM 可能會繼續增加堆內存,而不是等待一個垃圾回收的結束
        所以爲了尋找並最終消除內存泄漏,咱們須要比任務監視程序更好的工具。當你想檢測內存泄漏的時候內存調試程序(參見下文的參考資料)能夠派上用場了。這些程序一般會給你關於堆內存裏對象的數量、每一個對象實例的個數以及對象使用中的內存等一些信息。此外,它們還會提供頗有用的視圖,這些視圖能夠顯示每一個對象的引用和引用者,以便你跟蹤內存漏洞的來源。
        接下來,我將展現如何使用 Sitraka Software 的 JProbe 調試工具來檢測和消除內存泄漏,但願會對你就如何部署這些工具併成功消除內存泄漏產生一些啓發。
        一個內存泄漏的例子
        這個示例主要展現了咱們部門開發的一個商業版應用的一個問題,這個問題在 JDK 1.1.8 上工做了幾個小時後被測試人員找出來。這個 Java 應用程序的相關代碼和包是由幾個不一樣團隊的程序員開發出來的。程序裏出現的內存泄漏的緣由,我懷疑,是由一些沒有真正理解其餘(團隊)開發的代碼的程序員所引發。討論中的 Java 代碼容許用戶沒必要去寫 Palm OS 本地代碼來建立 Palm 我的數碼助理應用。經過使用圖形界面,用戶能夠建立表單,使用控件對它們進行填充,而後鏈接控件事件來建立 Palm 應用程序。測試人員發現,這個 Java 應用最終發生了內存溢出——表單和控件的建立和刪除延時。開發人員並無發現這個問題存在,由於他們的機器(相對 Palm)擁有着更多的物理內存。
        爲了討論這個問題,我使用了 JProbe 來判定問題的存在。即便擁有 JProde 提供的強大工具和內存快照,調查仍然是一個繁瑣的、反覆的過程,它涉及先肯定內存泄漏的緣由,而後作出代碼更改並驗證其效果。

        JProbe 有幾個選項來控制在一次調試回話期間什麼樣的信息會被記錄。通過一些試驗後,我斷定獲取所需信息的最有效的方式是關掉性能數據收集,專一於捕獲的堆數據。JProbe 提供了一個叫作運行時堆摘要的視圖來顯示 Java 應用程序在一段時間內使用的堆內存的數量。它同時也提供了一個工具欄按鈕用來在須要時強制 JVM 執行垃圾收集 --在想要看一下一個類的給定實例再也不爲 Java 應用程序須要時是否會被垃圾收集,這個功能是頗有用的。下圖顯示了在一段時間內使用的堆存儲量。

        在堆使用狀況圖中,藍色部分表示已分配的堆空間量。我啓動 Java 程序以後它達到了一個穩定點,我強制垃圾收集器執行,這由綠線以前的藍色曲線的一個驟降表示(這條綠線表示一個檢查點被插入)。接下來,我先是添加然後刪掉了四個表單並再次調用垃圾收集器。檢查點以後的藍色曲線的水平線比檢查點以前的藍色曲線的水平線高的事實告訴咱們極可能出現了內存泄漏,由於該程序已經迴歸其只有一個簡單可見的表單的初始狀態。我檢查實例確認了泄漏。總之,結果代表 FormFrame (表單的主 UI )的數量在檢查點以後增長了四個。
        尋找緣由
        要想將測試人員提交的問題隔離出來,第一步就是提供一些簡單的、重複的測試用例。以上面那個例子爲例,我發現簡單地添加一個表單,刪除這個表單,而後強制垃圾收集器的結果是一些關聯到已經刪除掉的表單的實例仍然存活着。這種問題經過 JProbe實例摘要視圖來看是顯而易見的,視圖中統計了堆內存中每一個類的實例的個數。
        要定位垃圾收集器工做時具體實例的引用,我使用了 JProbe 的引用畫面,以下圖所示,來判定哪些類仍然在引用已被刪除掉的 FormFrame 類。這是調試這種問題的巧妙地方法之一,我經過它發現了不少不一樣的對象仍然在引用那些無用的對象。而經過試錯來查明到底是哪一個引用者真正形成這個問題的過程倒是至關耗時的。

        在這個案例中,根類(左上角紅色的那個)是出現問題的起源。右側用藍色突出的那個類就是追蹤到的 FormFrame 類。

        對於這個具體的例子,找到的罪魁禍首是一個包含一個靜態的哈希表的字體管理類。經過引用列表追蹤後,我發現根節點是一個靜態的哈希表,這個哈希表保存了每一個表單使用的字體。各類表單能夠被獨立地放大或縮小,因此哈希表包含了一個具備每一個指定的表單的全部字體的向量。當表單的縮放視圖改變時,帶有字體的向量被獲取並選擇合適的縮放因素來適應字體大小。
        這個字體管理器的問題是,在建立表單時,當代碼將字體向量放進哈希表時,卻沒有定義表單刪除時對向量的移除。所以,這個在整個應用程序的生命週期都存在的靜態的哈希表,卻歷來沒有移除指向每一個表單的鍵值。因此,全部的表單和其相關聯的類被遺留在了內存中。
        問題修正

        對於這個問題的簡單解決方案就是字體管理器增長一個方法,來容許哈希表的 remove() 方法會在用戶刪除表單時被調用到。增長的 removeKeyFromHashtables() 方法以下所示:

[java] view plaincopyprint?

 

31 public void removeKeyFromHashtables(GraphCanvas graph) {  

32   if (graph != null) {  

33     viewFontTable.remove(graph);     // remove key from hashtable  

34                                      // to prevent memory leak  

35   }  

36 }  

 

        而後,我在 FormFrame 類裏添加了對這個方法的一個調用。FormFrame 使用 Swing 的內部框架來實現表單 UI,所以對於字體管理器的調用被添加到當內部框架徹底關閉時所執行的方法,以下所示:

[java] view plaincopyprint?

 

37 /** 

38 * Invoked when a FormFrame is disposed. Clean out references to prevent  

39 * memory leaks. 

40 */  

41 public void internalFrameClosed(InternalFrameEvent e) {  

42   FontManager.get().removeKeyFromHashtables(canvas);  

43   canvas = null;  

44   setDesktopIcon(null);  

45 }  


        在我對代碼作出修改之後,我使用調試工具來確認在相同的測試用例被執行時刪除表單所關聯到的對象的數目。
        內存泄漏的防止
        能夠經過對一些常見問題的注意來防止內存泄漏。容器類,好比哈希表和向量,是找到引發內存泄漏的常見的地方。尤爲是當這些類被聲明爲靜態的並存活於應用程序的整個生命週期之中時。
        另外一個常見(致使內存泄漏的)問題是當你將一個類註冊爲事件監聽器,卻沒考慮到當這個類再也不須要時將其註銷。還有,指向其餘類的成員變量在恰當的時候要設置爲 null
        結束語
        尋找內存泄漏的緣由多是一個繁瑣的過程,尚未提到的一點是這將須要特殊的調試工具。然而,一旦你熟悉了追蹤對象引用的工具和模式,你將可以跟蹤內存泄漏。此外,你還會得到一些有價值的技能,不只能夠節省項目編程投入,並且在之後的項目中你將擁有找出能夠防止發生內存泄漏的編程作法的眼光

內存泄露收集整理(轉)

1、性能測試總結以內存泄露和內存溢出

2java垃圾回收和內存泄露的講解

3Java 內存泄露淺析

 

1、性能測試總結以內存泄露和內存溢出

http://lya041.blog.51cto.com/337966/668766

剛剛作完了一個項目的性能測試,「有幸」也遇到了內存泄露的案例,因此在此和你們分享一下。

主要從如下幾部分來講明,關於內存和內存泄露、溢出的概念,區份內存泄露和內存溢出;內存的區域劃分,瞭解GC回收機制;重點關注如何去監控和發現內存問題;此外分析出問題還要如何解決內存問題。

下面就開始本篇的內容:

第一部分 概念

衆所周知,java中的內存java虛擬機本身去管理的,他不想C++須要本身去釋放。籠統地去講,java的內存分配分爲兩個部分,一個是數據堆,一個是棧。程序在運行的時候通常分配數據堆,把局部的臨時的變量都放進去,生命週期和進程有關係。可是若是程序員聲明瞭static的變量,就直接在棧中運行的,進程銷燬了,不必定會銷燬static變量。

另外爲了保證java內存不會溢出,java中有垃圾回收機制。 System.gc()即垃圾收集機制是指jvm用於釋放那些再也不使用的對象所佔用的內存。java語言並不要求jvmgc,也沒有規定gc如何工做。垃圾收集的目的在於清除再也不使用的對象。gc經過肯定對象是否被活動對象引用來肯定是否收集該對象。

而其中,內存溢出就是你要求分配的java虛擬機內存超出了系統能給你的,系統不能知足需求,因而產生溢出。

內存泄漏是指你向系統申請分配內存進行使用(new),但是使用完了之後卻不歸還(delete),結果你申請到的那塊內存你本身也不能再訪問,該塊已分配出來的內存也沒法再使用,隨着服務器內存的不斷消耗,而沒法使用的內存愈來愈多,系統也不能再次將它分配給須要的程序,產生泄露。一直下去,程序也逐漸無內存使用,就會溢出。

第二部分 原理

JAVA垃圾回收及對內存區劃分

Java虛擬機規範中,說起了以下幾種類型的內存空間:

◇ 棧內存(Stack):每一個線程私有的。

◇ 堆內存(Heap):全部線程公用的。

◇ 方法區(Method Area):有點像之前常說的「進程代碼段」,這裏面存放了每一個加載類的反射信息、類函數的代碼、編譯時常量等信息。

◇ 原生方法棧(Native Method Stack):主要用於JNI中的原生代碼,平時不多涉及。

Java的使用的是堆內存,java堆是一個運行時數據區,類的實例(對象)從中分配空間。Java虛擬機(JVM)的堆中儲存着正在運行的應用程序所創建的全部對象,「垃圾回收」也是主要是和堆內存(Heap)有關。

垃圾回收的概念就是JAVA虛擬機(JVM)回收那些再也不被引用的對象內存的過程。通常咱們認爲正在被引用的對象狀態爲「alive,而沒有被應用或者取不到引用屬性的對象狀態爲「dead」。垃圾回收是一個釋放處於」dead」狀態的對象的內存的過程。而垃圾回收的規則和算法被動態的做用於應用運行當中,自動回收。

JVM的垃圾回收器採用的是一種分代(generational )回收策略,用較高的頻率對年輕的對象(young generation)進行掃描和回收,這種叫作minor collection,而對老對象(old generation)的檢查回收頻率要低不少,稱爲major collection。這樣就不須要每次GC都將內存中全部對象都檢查一遍,這種策略有利於實時觀察和回收。

Sun JVM 1.3 有兩種最基本的內存收集方式:一種稱爲copyingscavenge,將全部仍然生存的對象搬到另一塊內存後,整塊內存就可回收。這種方法有效率,但須要有必定的空閒內存,拷貝也有開銷。這種方法用於minor collection。另一種稱爲mark-compact,將活着的對象標記出來,而後搬遷到一塊兒連成大塊的內存,其餘內存就能夠回收了。這種方法不須要佔用額外的空間,但速度相對慢一些。這種方法用於major collection. 

一些對象被建立出來只是擁有短暫的生命週期,好比 iterators 和本地變量。

另一些對象被建立是擁有很長的生命週期,好比 高持久化對象等。

垃圾回收器的分代策略是把內存區劃分爲幾個代,而後爲每一個代分配一到多個內存區塊。當其中一個代用完了分配給他的內存後,JVM會在分配的內存區內執行一個局部的GC(也能夠叫minor collection)操做,爲了回收處於「dead」狀態的對象所佔用的內存。局部GC一般要不Full GC要快不少。

JVM定義了兩個代,年輕代(yong generation)(有時稱爲「nursery」託兒所)和老年代(old generation)。年輕代包括 「Eden space(伊甸園)」和兩個「survivor spaces」。虛擬內存初始化的時候會把全部對象都分配到 Eden space,而且大部分對象也會在該區域被釋放。 當進行  minor GC的時候,VM會把剩下的沒有釋放的對象從Eden space移動到其中一個survivor spaces當中。此外,VM也會把那些長期存活在survivor spaces 裏的對象移動到 老生代的「tenured」 space中。當 tenured generation 被填滿後,就會產生Full GCFull GC會相對比較慢由於回收的內容包括了全部的 live狀態的對象。pemanet generation這個代包括了全部java虛擬機自身使用的相對比較穩定的數據對象,好比類和對象方法等。

關於代的劃分,能夠從下圖中得到一個概況:

 

若是垃圾回收器影響了系統的性能,或者成爲系統的瓶頸,你能夠經過自定義各個代的大小來優化它的性能。使用JConsole,能夠方便的查看到當前應用所配置的垃圾回收器的各個參數。想要得到更詳細的參數,能夠參考如下調優介紹:

Tuning Garbage collection with the 5.0 HotSpot VM

http://java.sun.com/docs/hotspot/gc/index.html

最後,總結一下各區內存:

Eden Space (heap): 內存最初從這個線程池分配給大部分對象。

Survivor Space (heap):用於保存在eden space內存池中通過垃圾回收後沒有被回收的對象。

Tenured Generation (heap):用於保持已經在 survivor space內存池中存在了一段時間的對象。

Permanent Generation (non-heap): 保存虛擬機本身的靜態(refective)數據,例如類(class)和方法(method)對象。Java虛擬機共享這些類數據。這個區域被分割爲只讀的和只寫的,

Code Cache (non-heap):HotSpot Java虛擬機包括一個用於編譯和保存本地代碼(native code)的內存,叫作「代碼緩存區」(code cache

第三部分 監控(工具發現問題)

談到內存監控工具,JConsole是必需要介紹的,它是一個用JAVA寫的GUI程序,用來監控VM,並可監控遠程的VM,易用且功能強大。具體可監控JAVA內存、JAVA CPU使用率、線程執行狀況、加載類概況等,Jconsole須要在JVM參數中配置端口才能使用。

因爲是GUI程序,界面可視化,這裏就不作詳細介紹,

具體幫助支持文檔請參閱性能測試JConsole使用方法總結:

http://www.taobao.ali.com/chanpin/km/test/DocLib/性能測試輔助工具-JConsole的使用方法.aspx

或者參考SUN官網的技術文檔:

http://Java.sun.com/j2se/1.5.0/docs/guide/management/jconsole.html

http://Java.sun.com/javase/6/docs/technotes/tools/share/jconsole.html

在實際測試某一個項目時,內存出現泄露現象。起初在性能測試的1個小時中,並不明顯,而在穩定性測試的時候才發現,應用的HSF調用在通過幾個小時運行後,就出現性能明顯降低的狀況。在服務日誌中報大量HSF超時,但所調用系統沒有任何超時日誌,而且壓力應用的load都很低。通過查看日誌後,認爲應用可能存在內存泄漏。經過jconsole 以及 jmap 工具進行分析發現,確實存在內存泄漏問題,其中PS Old Gen最終達到佔用 100%的佔用。

 

從上圖能夠看到,雖然每次Full GCJVM內存會有部分回收,但回收並不完全,不可回收的內存對象會愈來愈多,這樣便會出現以上的一個趨勢。在Full GC沒法回收的對象愈來愈多時,最終已使用內存達到系統分配的內存最大值,系統最後無內存可分配,最終down機。

第四部分 分析

通過開發和架構師對應用的分析,查看此時內存隊列,看哪一個對象佔用數據最多,再利用jmap命令,對線程數據分析,以下所示:

num     #instances         #bytes  class name

———————————————-

1:       9248056        665860032  com.taobao.matrix.mc.domain.**

2:       9248031        295936992  com.taobao.matrix.**

3:       9248068        147969088  java.util.**

4:       1542111        37010664   java.util.Date

前三個instances不斷增長,指代的是同一個代碼邏輯,異步分發的問題,堵塞消息,回收屢次都沒法回收成功。致使內存溢出。

此外,對應用的性能單獨作了壓測,他的性能只能支撐到一半左右,故發送消息的TPS,應用確定沒法處理過來,致使消息堆積,而JAVA垃圾回收期認爲這些都是有用的對象,致使內存堆積,直至系統崩潰。

調優方法

因爲具體調優方法涉及到應用的配置信息,故在此暫不列出,能夠參考性能測試小組發佈的《性能測試調優寶典》

第四部分 總結

內存溢出主要是因爲代碼編寫時對某些方法、類應用不合理,或者沒有預估到臨時對象會佔用很大內存量,或者把過多的數據放入JVM緩存,或者性能壓力大致使消息堆積而佔用內存,以致於在性能測試時,生成龐大數量的臨時對象,GC時沒有作出有效回收甚至根本就不能回收,形成內存空間不足,內存溢出。

若是編碼以前,對內存使用量進行預估,對放在內存中的數據進行評估,保證有用的信息儘快釋放,無用的信息可以被GC回收,這樣在必定程度上是能夠避免內存溢出問題的。

 

2java垃圾回收和內存泄露的講解

http://lya041.blog.51cto.com/337966/665325

1.垃圾收集算法的核心思想

Java語言創建了垃圾收集機制,用以跟蹤正在使用的對象和發現並回收再也不使用(引用)的對象。該機制能夠有效防範動態內存分配中可能發生的兩個危險:因內存垃圾過多而引起的內存耗盡,以及不恰當的內存釋放所形成的內存非法引用。

垃圾收集算法的核心思想是:對虛擬機可用內存空間,即堆空間中的對象進行識別,若是對象正在被引用,那麼稱其爲存活 對象,反之,若是對象再也不被引用,則爲垃圾對象,能夠回收其佔據的空間,用於再分配。垃圾收集算法的選擇和垃圾收集系統參數的合理調節直接影響着系統性 能,所以須要開發人員作比較深刻的瞭解。

 

2.觸發主GCGarbage Collector)的條件

JVM進行次GC的頻率很高,但由於這種GC佔用時間極短,因此對系統產生的影響不大。更值得關注的是主GC的觸發條件,由於它對系統影響很明顯。總的來講,有兩個條件會觸發主GC:

①當應用程序空閒時,即沒有應用線程在運行時,GC會被調用。由於GC在優先級最低的線程中進行,因此當應用忙時,GC線程就不會被調用,但如下條件除外。

Java堆內存不足時,GC會被調用。當應用線程在運行,並在運行過程當中建立新對象,若這時內存空間不足,JVM 就會強制地調用GC線程,以便回收內存用於新的分配。若GC一次以後仍不能知足內存分配的要求,JVM會再進行兩次GC做進一步的嘗試,若仍沒法知足要 求,則  JVM將報「out of memory」的錯誤,Java應用將中止。

因爲是否進行主GCJVM根據系統環境決定,而系統環境在不斷的變化當中,因此主GC的運行具備不肯定性,沒法預計它什麼時候必然出現,但能夠肯定的是對一個長期運行的應用來講,其主GC是反覆進行的。

 

3.減小GC開銷的措施

根據上述GC的機制,程序的運行會直接影響系統環境的變化,從而影響GC的觸發。若不針對GC的特色進行設計和編碼,就會出現內存駐留等一系列負面影響。爲了不這些影響,基本的原則就是儘量地減小垃圾和減小GC過程當中的開銷。具體措施包括如下幾個方面:

(1)不要顯式調用System.gc()

此函數建議JVM進行主GC,雖然只是建議而非必定,但不少狀況下它會觸發主GC,從而增長主GC的頻率,也即增長了間歇性停頓的次數。

 

(2)儘可能減小臨時對象的使用

臨時對象在跳出函數調用後,會成爲垃圾,少用臨時變量就至關於減小了垃圾的產生,從而延長了出現上述第二個觸發條件出現的時間,減小了主GC的機會。

 

(3)對象不用時最好顯式置爲Null

通常而言,Null的對象都會被做爲垃圾處理,因此將不用的對象顯式地設爲Null,有利於GC收集器斷定垃圾,從而提升了GC的效率。

 

(4)儘可能使用StringBuffer,而不用String來累加字符串(詳見blog另外一篇文章JAVAStringStringBuffer

因爲String是固定長的字符串對象,累加String對象時,並不是在一個String對象中擴增,而是從新建立 新的String對象,Str5=Str1+Str2+Str3+Str4,這條語句執行過程當中會產生多個垃圾對象,由於對次做「+」操做時都必須建立 新的String對象,但這些過渡對象對系統來講是沒有實際意義的,只會增長更多的垃圾。避免這種狀況能夠改用StringBuffer來累加字符串,因 StringBuffer是可變長的,它在原有基礎上進行擴增,不會產生中間對象。

 

(5)能用基本類型如Int,Long,就不用Integer,Long對象

基本類型變量佔用的內存資源比相應對象佔用的少得多,若是沒有必要,最好使用基本變量。

 

(6)儘可能少用靜態對象變量

靜態變量屬於全局變量,不會被GC回收,它們會一直佔用內存。

 

(7)分散對象建立或刪除的時間

集中在短期內大量建立新對象,特別是大對象,會致使忽然須要大量內存,JVM在面臨這種狀況時,只能進行主GC, 以回收內存或整合內存碎片,從而增長主GC的頻率。集中刪除對象,道理也是同樣的。它使得忽然出現了大量的垃圾對象,空閒空間必然減小,從而大大增長了下 一次建立新對象時強制主GC的機會。

 

4gcfinalize方法

gc方法請求垃圾回收

使用System.gc()能夠無論JVM使用的是哪種垃圾回收的算法,均可以請求Java的垃圾回收。須要注意 的是,調用System.gc()也僅僅是一個請求。JVM接受這個消息後,並非當即作垃圾回收,而只是對幾個垃圾回收算法作了加權,使垃圾回收操做容 易發生,或提前發生,或回收較多而已。

 

finalize方法透視垃圾收集器的運行

JVM垃圾收集器收集一個對象以前 ,通常要求程序調用適當的方法釋放資源,但在沒有明確釋放資源的狀況下,Java提供了缺省機制來終止化該對象釋放資源,這個方法就是finalize()。它的原型爲:

protected void finalize() throws Throwable

finalize()方法返回以後,對象消失,垃圾收集開始執行。原型中的throws Throwable表示它能夠拋出任何類型的異常。

所以,當對象即將被銷燬時,有時須要作一些善後工做。能夠把這些操做寫在finalize()方法裏。

protected void finalize()

{

// finalization code here

}

⑶代碼示例

class Garbage

{

    int index;

    static int count;

    Garbage()

    {

        count++;

        System.out.println("object "+count+" construct");

        setID(count);

    }

    void setID(int id)

    {

        index=id;

    }

   

    protected void finalize()  //重寫finalize方法

    {

        System.out.println("object "+index+" is reclaimed");

    }

   

    public static void main(String[] args)

    {

        new Garbage();

        new Garbage();

        new Garbage();

        new Garbage();

        System.gc();  //請求運行垃圾收集器

    }

}

 

5Java 內存泄漏

因爲採用了垃圾回收機制,任何不可達對象(對象再也不被引用)均可以由垃圾收集線程回收。所以一般說的Java 內存泄漏實際上是指無心識的、非故意的對象引用,或者無心識的對象保持。無心識的對象引用是指代碼的開發人員原本已經對對象使用完畢,卻由於編碼的錯誤而意 外地保存了對該對象的引用(這個引用的存在並非編碼人員的主觀意願),從而使得該對象一直沒法被垃圾回收器回收掉,這種原本覺得能夠釋放掉的卻最終未能 被釋放的空間能夠認爲是被「泄漏了」。

考慮下面的程序,ObjStack類中,使用pushpop方法來管理堆棧中的對象。兩個方法中的索引 (index)用於指示堆棧中下一個可用位置。push方法存儲對新對象的引用並增長索引值,pop方法減少索引值並返回堆棧最上面的元素。在main 方法中,建立了容量爲64的棧,64次調用push方法向它添加對象,此時index的值爲64,隨後又32次調用pop方法,index的值變爲 32,出棧意味着在堆棧中的空間應該被收集。但事實上,pop方法只是減少了索引值,堆棧仍然保持着對那些對象的引用。故32個無用對象不會被GC回收形成了內存滲漏。

 

public class ObjStack {

 private Object[] stack;

 private int index;

 ObjStack(int indexcount) {

  stack new Object[indexcount];

  index 0;

 }

 

 public void push(Object obj) {

  stack[index] obj;

  index++;

  }

 public Object pop() {

  index--;

  returnstack[index];

 }

}

 

public class Pushpop {

 public static void main(String[] args) {

  int i = 0;

  Object tempobj;

  ObjStack stack1 new ObjStack(64);//new一個ObjStack對象,並調用有參構造函數。分配stack Obj數組的空間大小爲64,能夠存64個對象,從0開始存儲。

  while (i < 64)

  {

   tempobj new Object();//循環new Obj對象,把每次循環的對象一一存放在stack Obj數組中。

   stack1.push(tempobj);

   i++;

   System.out.println("" + i + "次進棧" + "/t");

  }

  while (i > 32)

  {

   tempobj = stack1.pop();//這裏形成了空間的浪費。

   //正確的pop方法可改爲以下所指示,當引用被返回後,堆棧刪除對他們的引用,所以垃圾收集器在之後能夠回收他們。

   /*    * public Object pop() {index - -;Object temp = stack [index];stack [index]=null;return temp;}

    */   i--;

   System.out.println("" + (64 - i) + "次出棧" + "/t");

  }

 }

}

 

如何消除內存泄漏

  雖然Java虛擬機(JVM)及其垃圾收集器(garbage collectorGC)負責管理大多數的內存任務,Java軟件程序中仍是有可能出現內存泄漏。實際上,這在大型項目中是一個常見的問題。避免內存泄 漏的第一步是要弄清楚它是如何發生的。本文介紹了編寫Java代碼的一些常見的內存泄漏陷阱,以及編寫不泄漏代碼的一些最佳實踐。一旦發生了內存泄漏,要 指出形成泄漏的代碼是很是困難的。所以本文還介紹了一種新工具,用來診斷泄漏並指出根本緣由。該工具的開銷很是小,所以能夠使用它來尋找處於生產中的系統 的內存泄漏。

垃圾收集器的做用

  雖然垃圾收集器處理了大多數內存管理問題,從而使編程人員的生活變得更輕鬆了,可是編程人員仍是可能犯錯而致使 出現內存問題。簡單地說,GC循環地跟蹤全部來自「根」對象(堆棧對象、靜態對象、JNI句柄指向的對象,諸如此類)的引用,並將全部它所能到達的對象標 記爲活動的。程序只能夠操縱這些對象;其餘的對象都被刪除了。由於GC使程序不可能到達已被刪除的對象,這麼作就是安全的。

  雖然內存管理能夠說是自動化的,可是這並不能使編程人員免受思考內存管理問題之苦。例如,分配(以及釋放)內存 總會有開銷,雖然這種開銷對編程人員來講是不可見的。建立了太多對象的程序將會比完成一樣的功能而建立的對象卻比較少的程序更慢一些(在其餘條件相同的情 況下)。

  並且,與本文更爲密切相關的是,若是忘記「釋放」先前分配的內存,就可能形成內存泄漏。若是程序保留對永遠再也不 使用的對象的引用,這些對象將會佔用並耗盡內存,這是由於自動化的垃圾收集器沒法證實這些對象將再也不使用。正如咱們先前所說的,若是存在一個對對象的引 用,對象就被定義爲活動的,所以不能刪除。爲了確保能回收對象佔用的內存,編程人員必須確保該對象不能到達。這一般是經過將對象字段設置爲null或者從 集合(collection)中移除對象而完成的。可是,注意,當局部變量再也不使用時,沒有必要將其顯式地設置爲null。對這些變量的引用將隨着方法的 退出而自動清除。

  歸納地說,這就是內存託管語言中的內存泄漏產生的主要緣由:保留下來卻永遠再也不使用的對象引用。

典型泄漏

  既然咱們知道了在Java中確實有可能發生內存泄漏,就讓咱們來看一些典型的內存泄漏及其緣由。

全局集合

  在大的應用程序中有某種全局的數據儲存庫是很常見的,例如一個JNDI樹或一個會話表。在這些狀況下,必須注意管理儲存庫的大小。必須有某種機制從儲存庫中移除再也不須要的數據。

  這可能有多種方法,可是最多見的一種是週期性運行的某種清除任務。該任務將驗證儲存庫中的數據,並移除任何再也不須要的數據。

  另外一種管理儲存庫的方法是使用反向連接(referrer)計數。而後集合負責統計集合中每一個入口的反向連接的數目。這要求反向連接告訴集合什麼時候會退出入口。當反向連接數目爲零時,該元素就能夠從集合中移除了。

緩存

  緩存是一種數據結構,用於快速查找已經執行的操做的結果。所以,若是一個操做執行起來很慢,對於經常使用的輸入數據,就能夠將操做的結果緩存,並在下次調用該操做時使用緩存的數據。

  緩存一般都是以動態方式實現的,其中新的結果是在執行時添加到緩存中的。典型的算法是:

檢查結果是否在緩存中,若是在,就返回結果。 

若是結果不在緩存中,就進行計算。 

將計算出來的結果添加到緩存中,以便之後對該操做的調用能夠使用。 

該算法的問題(或者說是潛在的內存泄漏)出在最後一步。若是調用該操做時有至關多的不一樣輸入,就將有至關多的結果存儲在緩存中。很明顯這不是正確的方法。

  爲了預防這種具備潛在破壞性的設計,程序必須確保對於緩存所使用的內存容量有一個上限。所以,更好的算法是:

檢查結果是否在緩存中,若是在,就返回結果。 

若是結果不在緩存中,就進行計算。 

若是緩存所佔的空間過大,就移除緩存最久的結果。 

將計算出來的結果添加到緩存中,以便之後對該操做的調用能夠使用。 

經過始終移除緩存最久的結果,咱們實際上進行了這樣的假設:在未來,比起緩存最久的數據,最近輸入的數據更有可能用到。這一般是一個不錯的假設。

  新算法將確保緩存的容量處於預約義的內存範圍以內。確切的範圍可能很難計算,由於緩存中的對象在不斷變化,並且它們的引用一應俱全。爲緩存設置正確的大小是一項很是複雜的任務,須要將所使用的內存容量與檢索數據的速度加以平衡。

  解決這個問題的另外一種方法是使用java.lang.ref.SoftReference類跟蹤緩存中的對象。這種方法保證這些引用可以被移除,若是虛擬機的內存用盡而須要更多堆的話。

ClassLoader

  Java ClassLoader結構的使用爲內存泄漏提供了許多可乘之機。正是該結構自己的複雜性使ClassLoader在內存泄漏方面存在如此多的問題。 ClassLoader的特別之處在於它不只涉及「常規」的對象引用,還涉及元對象引用,好比:字段、方法和類。這意味着只要有對字段、方法、類或 ClassLoader的對象的引用,ClassLoader就會駐留在JVM中。由於ClassLoader自己能夠關聯許多類及其靜態字段,因此就有 許多內存被泄漏了。

肯定泄漏的位置

  一般發生內存泄漏的第一個跡象是:在應用程序中出現了OutOfMemoryError。這一般發生在您最不肯 意它發生的生產環境中,此時幾乎不能進行調試。有多是由於測試環境運行應用程序的方式與生產系統不徹底相同,於是致使泄漏只出如今生產中。在這種狀況 下,須要使用一些開銷較低的工具來監控和查找內存泄漏。還須要可以無需重啓系統或修改代碼就能夠將這些工具鏈接到正在運行的系統上。可能最重要的是,當進 行分析時,須要可以斷開工具而保持系統不受干擾。

  雖然OutOfMemoryError一般都是內存泄漏的信號,可是也有可能應用程序確實正在使用這麼多的內 存;對於後者,或者必須增長JVM可用的堆的數量,或者對應用程序進行某種更改,使它使用較少的內存。可是,在許多狀況 下,OutOfMemoryError都是內存泄漏的信號。一種查明方法是不間斷地監控GC的活動,肯定內存使用量是否隨着時間增長。若是確實如此,就可 能發生了內存泄漏。

 

3Java 內存泄露淺析

Java使用有向圖的方式進行內存管理,

 

優勢:能夠消除引用循環的問題,管理內存精度高

 

缺點:效率低下(相比引用計數)。

 

什麼是Java中內存泄漏:

 

Java中,內存泄漏就是存在一些被分配的對象,這些對象有下面兩個特色:

 

<>:這些對象是可達的

<>:這些對象是無用的

 

這些對象不會被GC所回收,然而它卻佔用內存。

 


Java內存泄漏的方面:

 

靜態集合變量:HashMap, Hashtable,等,因爲這些集合不斷調用add()方法增長一些臨時對象,而沒有及時調用remove()方法移除臨時對象的引用,

 

致使一些無用的臨時對象不能被JVM回收,形成內存泄漏

 

單例對象:若是該對象持有另一個對象A的引用,那麼對象A不會被回收,當對象A是一個比較大的對象時,會形成嚴重的內存泄漏。

 

Web容器中的request, session, application對象:對於request,session,若是併發量較大,而每一個request, session都持有較多臨時對象的引用,

 

會致使服務器內存溢出。對於application對象和Web容器中一些具備很長生命週期的對象,長期持有一些臨時對象,也會形成內存泄漏。 

 

JDBC操做出現異常,沒有及時關閉鏈接(Connection)對象,會致使內存泄漏。

 

文件流操做出現異常,沒有及時關閉文件流(InputStream)對象,致使內存泄漏。

 

Http請求超時設置:若是超時設置的時間是無限長,那麼當一個Http請求的線程被卡住時,這個線程所佔有的資源會永遠不會釋放,致使內存泄漏。

zengdan

46 博客園

47 首頁

48 博問

49 閃存

50 新隨筆

51 聯繫

52 訂閱

53 管理

隨筆-20  文章-1  評論-0 

JMM

1.JMM簡介

i.內存模型概述

  Java平臺自動集成了線程以及多處理器技術,這種集成程度比Java之前誕生的計算機語言要厲害不少,該語言針對多種異構平臺的平臺獨立性而使用的多線程技術支持也是具備開拓性的一面,有時候在開發Java同步和線程安全要求很嚴格的程序時,每每容易混淆的一個概念就是內存模型。究竟什麼是內存模型?內存模型描述了程序中各個變量(實例域、靜態域和數組元素)之間的關係,以及在實際計算機系統中將變量存儲到內存和從內存中取出變量這樣的底層細節,對象最終是存儲在內存裏面的,這點沒有錯,可是編譯器、運行庫、處理器或者系統緩存能夠有特權在變量指定內存位置存儲或者取出變量的值。【JMM】(Java Memory Model的縮寫)容許編譯器和緩存以數據在處理器特定的緩存(或寄存器)和主存之間移動的次序擁有重要的特權,除非程序員使用了finalsynchronized明確請求了某些可見性的保證。

1)JSR133

  在Java語言規範裏面指出了JMM是一個比較開拓性的嘗試,這種嘗試視圖定義一個一致的、跨平臺的內存模型,可是它有一些比較細微並且很重要的缺點。其實Java語言裏面比較容易混淆的關鍵字主要是synchronizedvolatile,也由於這樣在開發過程當中每每開發者會忽略掉這些規則,這也使得編寫同步代碼比較困難。

JSR133自己的目的是爲了修復本來JMM的一些缺陷而提出的,其自己的制定目標有如下幾個:

54 保留目前JVM的安全保證,以進行類型的安全檢查

55 提供out-of-thin-air safety無中生有安全性,這樣「正確同步的」應該被正式並且直觀地定義

56 程序員要有信心開發多線程程序,固然沒有其餘辦法使得併發程序變得很容易開發,可是該規範的發佈主要目標是爲了減輕程序員理解內存模型中的一些細節負擔

57 提供大範圍的流行硬件體系結構上的高性能JVM實現,如今的處理器在它們的內存模型上有着很大的不一樣,JMM應該可以適合於實際的儘量多的體系結構而不以性能爲代價,這也是Java跨平臺型設計的基礎

58 提供一個同步的習慣用法,以容許發佈一個對象使他不用同步就可見,這種狀況又稱爲初始化安全(initialization safety的新的安全保證

59 對現有代碼應該只有最小限度的影響

2)同步、異步【這裏僅僅指概念上的理解,不牽涉到計算機底層基礎的一些操做】:

  在系統開發過程,常常會遇到這幾個基本概念,不管是網絡通信、對象之間的消息通信仍是Web開發人員經常使用的Http請求都會遇到這樣幾個概念,常常有人提到Ajax是異步通信方式,那麼究竟怎樣的方式是這樣的概念描述呢?

同步:同步就是在發出一個功能調用的時候,在沒有獲得響應以前,該調用就不返回,按照這樣的定義,其實大部分程序的執行都是同步調用的,通常狀況下,在描述同步和異步操做的時候,主要是指代須要其餘部件協做處理或者須要協做響應的一些任務處理。好比有一個線程A,在A執行的過程當中,可能須要B提供一些相關的執行數據,固然觸發B響應的就是AB發送一個請求或者說對B進行一個調用操做,若是A在執行該操做的時候是同步的方式,那麼A就會停留在這個位置等待B給一個響應消息,在B沒有任何響應消息回來的時候,A不能作其餘事情,只能等待,那麼這樣的狀況,A的操做就是一個同步的簡單說明。

異步:異步就是在發出一個功能調用的時候,不須要等待響應,繼續進行它該作的事情,一旦獲得響應了事後給予必定的處理,可是不影響正常的處理過程的一種方式。好比有一個線程A,在A執行的過程當中,一樣須要B提供一些相關數據或者操做,當AB發送一個請求或者對B進行調用操做事後,A不須要繼續等待,而是執行A本身應該作的事情,一旦B有了響應事後會通知AA接受到該異步請求的響應的時候會進行相關的處理,這種狀況下A的操做就是一個簡單的異步操做。

3)可見性、可排序性

  Java內存模型的兩個關鍵概念:可見性(Visibility可排序性(Ordering

  開發過多線程程序的程序員都明白,synchronized關鍵字強制實施一個線程之間的互斥鎖(相互排斥),該互斥鎖防止每次有多個線程進入一個給定監控器所保護的同步語句塊,也就是說在該狀況下,執行程序代碼所獨有的某些內存是獨佔模式其餘的線程是不能針對它執行過程所獨佔的內存進行訪問的,這種狀況稱爲該內存不可見。可是在該模型的同步模式中,還有另一個方面:JMM中指出了,JVM在處理該強制實施的時候能夠提供一些內存的可見規則,在該規則裏面,它確保當存在一個同步塊時,緩存被更新,當輸入一個同步塊時,緩存失效。所以在JVM內部提供給定監控器保護的同步塊之中,一個線程所寫入的值對於其他全部的執行由同一個監控器保護的同步塊線程來講是可見的,這就是一個簡單的可見性的描述。這種機器保證編譯器不會把指令從一個同步塊的內部移到外部,雖然有時候它會把指令由外部移動到內部。JMM在缺省狀況下不作這樣的保證——只要有多個線程訪問相同變量時必須使用同步。簡單總結:

  可見性就是在多核或者多線程運行過程當中內存的一種共享模式,在JMM模型裏面,經過併發線程修改變量值的時候,必須將線程變量同步回主存事後,其餘線程纔可能訪問到。

  【*:簡單講,內存的可見性使內存資源能夠共享,當一個線程執行的時候它所佔有的內存,若是它佔有的內存資源是可見的,那麼這時候其餘線程在必定規則內是能夠訪問該內存資源的,這種規則是由JMM內部定義的,這種狀況下內存的該特性稱爲其可見性。】

  可排序性提供了內存內部的訪問順序,在不一樣的程序針對不一樣的內存塊進行訪問的時候,其訪問不是無序的,好比有一個內存塊,AB須要訪問的時候,JMM會提供必定的內存分配策略有序地分配它們使用的內存,而在內存的調用過程也會變得有序地進行,內存的折中性質能夠簡單理解爲有序性。而在Java多線程程序裏面,JMM經過Java關鍵字volatile來保證內存的有序訪問。

ii.JMM結構:

1)簡單分析:

  Java語言規範中提到過,JVM中存在一個主存區(Main MemoryJava Heap MemoryJava全部變量都是存在主存中的,對於全部線程進行共享,而每一個線程又存在本身的工做內存(Working Memory,工做內存中保存的是主存中某些變量的拷貝,線程對全部變量的操做並不是發生在主存區,而是發生在工做內存中,而線程之間是不能直接相互訪問,變量在程序中的傳遞,是依賴主存來完成的。而在多核處理器下,大部分數據存儲在高速緩存中,若是高速緩存不通過內存的時候,也是不可見的一種表現。在Java程序中,內存自己是比較昂貴的資源,其實不只僅針對Java應用程序,對操做系統自己而言內存也屬於昂貴資源,Java程序在性能開銷過程當中有幾個比較典型的可控制的來源。synchronizedvolatile關鍵字提供的內存中模型的可見性保證程序使用一個特殊的、存儲關卡(memory barrier的指令,來刷新緩存,使緩存無效,刷新硬件的寫緩存而且延遲執行的傳遞過程,無疑該機制會對Java程序的性能產生必定的影響。

JMM的最初目的,就是爲了可以支持多線程程序設計的,每一個線程能夠認爲是和其餘線程不一樣的CPU上運行,或者對於多處理器的機器而言,該模型須要實現的就是使得每個線程就像運行在不一樣的機器、不一樣的CPU或者自己就不一樣的線程上同樣,這種狀況實際上在項目開發中是常見的。對於CPU自己而言,不能直接訪問其餘CPU的寄存器,模型必須經過某種定義規則來使得線程和線程在工做內存中進行相互調用而實現CPU自己對其餘CPU、或者說線程對其餘線程的內存中資源的訪問,而表現這種規則的運行環境通常爲運行該程序的運行宿主環境(操做系統、服務器、分佈式系統等),而程序自己表現就依賴於編寫該程序的語言特性,這裏也就是說用Java編寫的應用程序在內存管理中的實現就是遵循其部分原則,也就是前邊說起到的JMM定義了Java語言針對內存的一些的相關規則。然而,雖然設計之初是爲了可以更好支持多線程,可是該模型的應用和實現固然不侷限於多處理器,而在JVM編譯器編譯Java編寫的程序的時候以及運行期執行該程序的時候,對於CPU的系統而言,這種規則也是有效的,這就是是上邊提到的線程和線程之間的內存策略。JMM自己在描述過程沒有提過具體的內存地址以及在實現該策略中的實現方法是由JVM的哪個環節(編譯器、處理器、緩存控制器、其餘)提供的機制來實現的,甚至針對一個開發很是熟悉的程序員,也不必定可以瞭解它內部對於類、對象、方法以及相關內容的一些具體可見的物理結構。相反,JMM定義了一個線程與主存之間的抽象關係,其實從上邊的圖能夠知道,每個線程能夠抽象成爲一個工做內存(抽象的高速緩存和寄存器),其中存儲了Java的一些值,該模型保證了Java裏面的屬性、方法、字段存在必定的數學特性,按照該特性,該模型存儲了對應的一些內容,而且針對這些內容進行了必定的序列化以及存儲排序操做,這樣使得Java對象在工做內存裏面被JVM順利調用,(固然這是比較抽象的一種解釋)既然如此,大多數JMM的規則在實現的時候,必須使得主存和工做內存之間的通訊可以得以保證,並且不能違反內存模型自己的結構,這是語言在設計之處必須考慮到的針對內存的一種設計方法。這裏須要知道的一點是,這一切的操做在Java語言裏面都是依靠Java語言自身來操做的,由於Java針對開發人員而言,內存的管理在不須要手動操做的狀況下自己存在內存的管理策略,這也是Java本身進行內存管理的一種優點。

[1]原子性(Atomicity):

  這一點說明了該模型定義的規則針對原子級別的內容存在獨立的影響,對於模型設計最初,這些規則須要說明的僅僅是最簡單的讀取和存儲單元寫入的的一些操做,這種原子級別的包括——實例、靜態變量、數組元素,只是在該規則中不包括方法中的局部變量。

[2]可見性(Visibility):

  在該規則的約束下,定義了一個線程在哪一種狀況下能夠訪問另一個線程或者影響另一個線程,從JVM的操做上講包括了從另一個線程的可見區域讀取相關數據以及將數據寫入到另一個線程內。

[3]可排序性(Ordering):

  該規則將會約束任何一個違背了規則調用的線程在操做過程當中的一些順序,排序問題主要圍繞了讀取、寫入和賦值語句有關的序列。

  若是在該模型內部使用了一致的同步性的時候,這些屬性中的每個屬性都遵循比較簡單的原則:和全部同步的內存塊同樣,每一個同步塊以內的任何變化都具有了原子性以及可見性,和其餘同步方法以及同步塊遵循一樣一致的原則,並且在這樣的一個模型內,每一個同步塊不能使用同一個鎖,在整個程序的調用過程是按照編寫的程序指定指令運行的。即便某一個同步塊內的處理可能會失效,可是該問題不會影響到其餘線程的同步問題,也不會引發連環失效。簡單講:當程序運行的時候使用了一致的同步性的時候,每一個同步塊有一個獨立的空間以及獨立的同步控制器和鎖機制,而後對外按照JVM的執行指令進行數據的讀寫操做。這種狀況使得使用內存的過程變得很是嚴謹!

  若是不使用同步或者說使用同步不一致(這裏能夠理解爲異步,但不必定是異步操做),該程序執行的答案就會變得極其複雜。並且在這樣的狀況下,該內存模型處理的結果比起大多數程序員所指望的結果而言就變得十分脆弱,甚至比起JVM提供的實現都脆弱不少。由於這樣因此出現了Java針對該內存操做的最簡單的語言規範來進行必定的習慣限制,排除該狀況發生的作法在於:

  JVM線程必須依靠自身來維持對象的可見性以及對象自身應該提供相對應的操做而實現整個內存操做的三個特性,而不是僅僅依靠特定的修改對象狀態的線程來完成如此複雜的一個流程。

*:綜上所屬,JMMJVM內部實現的結構就變得相對複雜,固然通常的Java初學者能夠不用瞭解得這麼深刻。】

[4]三個特性的解析(針對JMM內部):

原子性(Atomicity):

  訪問存儲單元內的任何類型的字段的值以及對其更新操做的時候,除開long類型和double類型,其餘類型的字段是必需要保證其原子性的,這些字段也包括爲對象服務的引用。此外,該原子性規則擴展能夠延伸到基於longdouble的另外兩種類型volatile longvolatile doublevolatilejava關鍵字),沒有被volatile聲明的long類型以及double類型的字段值雖然不保證其JMM中的原子性,可是是被容許的。針對non-long/non-double的字段在表達式中使用的時候,JMM的原子性有這樣一種規則:若是你得到或者初始化該值或某一些值的時候,這些值是由其餘線程寫入,並且不是從兩個或者多個線程產生的數據在同一時間戳混合寫入的時候,該字段的原子性JVM內部是必須獲得保證的。也就是說JMM在定義JVM原子性的時候,只要在該規則不違反的條件下,JVM自己不去理睬該數據的值是來自於什麼線程,由於這樣使得Java語言在並行運算的設計的過程當中針對多線程的原子性設計變得極其簡單,並且即便開發人員沒有考慮到最終的程序也沒有太大的影響。再次解釋一下:這裏的原子性指的是原子級別的操做,好比最小的一塊內存的讀寫操做,能夠理解爲Java語言最終編譯事後最接近內存的最底層的操做單元,這種讀寫操做的數據單元不是變量的值,而是本機碼,也就是前邊在講《Java基礎知識》中提到的由運行器解釋的時候生成的Native Code

可見性(Visibility):

當一個線程須要修改另外線程的可見單元的時候必須遵循如下原則:

60 一個寫入線程釋放的同步鎖和緊隨其後進行讀取的讀線程的同步鎖是同一個
從本質上講,釋放鎖操做強迫它的隸屬線程【釋放鎖的線程】從工做內存中的寫入緩存裏面刷新(專業上講這裏不該該是刷新,能夠理解爲提供)數據(flush操做),而後獲取鎖操做使得另一個線程【得到鎖的線程】直接讀取前一個線程可訪問域(也就是可見區域)的字段的值。由於該鎖內部提供了一個同步方法或者同步塊,該同步內容具備線程排他性,這樣就使得上邊兩個操做只能針對單一線程在同步內容內部進行操做,這樣就使得全部操做該內容的單一線程具備該同步內容(加鎖的同步方法或者同步塊)內的線程排他性,這種狀況的交替也能夠理解爲具備「短暫記憶效應」。
這裏須要理解的是同步雙重含義:使用鎖機制容許基於高層同步協議進行處理操做,這是最基本的同步;同時系統內存(不少時候這裏是指基於機器指令的底層存儲關卡memory barrier,前邊提到過)在處理同步的時候可以跨線程操做,使得線程和線程之間的數據是同步的。這樣的機制也折射出一點,並行編程相對於順序編程而言,更加相似於分佈式編程。後一種同步能夠做爲JMM機制中的方法在一個線程中運行的效果展現,注意這裏不是多個線程運行的效果展現,由於它反應了該線程願意發送或者接受的雙重操做,而且使得它本身的可見區域能夠提供給其餘線程運行或者更新,從這個角度來看,使用鎖和消息傳遞能夠視爲相互之間的變量同步,由於相對其餘線程而言,它的操做針對其餘線程也是對等的。

61 一旦某個字段被申明爲volatile,在任何一個寫入線程在工做內存中刷新緩存的以前須要進行進一步的內存操做,也就是說針對這樣的字段進行當即刷新,能夠理解爲這種volatile不會出現通常變量的緩存操做,而讀取線程每次必須根據前一個線程的可見域裏面從新讀取該變量的值,而不是直接讀取。

62 當某個線程第一次去訪問某個對象的域的時候,它要麼初始化該對象的值,要麼從其餘寫入線程可見域裏面去讀取該對象的值;這裏結合上邊理解,在知足某種條件下,該線程對某對象域的值的讀取是直接讀取,有些時候卻須要從新讀取。
這裏須要當心一點的是,在併發編程裏面,很差的一個實踐就是使用一個合法引用去引用不徹底構造的對象,這種狀況在從其餘寫入線程可見域裏面進行數據讀取的時候發生頻率比較高。從編程角度上講,在構造函數裏面開啓一個新的線程是有必定的風險的,特別是該類是屬於一個可子類化的類的時候。Thread.start由調用線程啓動,而後由得到該啓動的線程釋放鎖具備相同的「短暫記憶效應」,若是一個實現了Runnable接口的超類在子類構造子執行以前調用了Thread(this).start()方法,那麼就可能使得該對象在線程方法run執行以前並沒有被徹底初始化,這樣就使得一個指向該對象的合法引用去引用了不徹底構造的一個對象。一樣的,若是建立一個新的線程T而且啓動該線程,而後再使用線程T來建立對象X,這種狀況就不能保證X對象裏面全部的屬性針對線程T都是可見的除非是在全部針對X對象的引用中進行同步處理,或者最好的方法是在T線程啓動以前建立對象X

63 若一個線程終止,全部的變量值都必須從工做內存中刷到主存,好比,若是一個同步線程由於另外一個使用Thread.join方法的線程而終止,那麼該線程的可見域針對那個線程而言其發生的改變以及產生的一些影響是須要保證可知道的。

  注意:若是在同一個線程裏面經過方法調用去傳一個對象的引用是絕對不會出現上邊說起到的可見性問題的。JMM保證全部上邊的規定以及關於內存可見性特性的描述——一個特殊的更新、一個特定字段的修改都是某個線程針對其餘線程的一個「可見性」的概念,最終它發生的場所在內存模型中Java線程和線程之間,至於這個發生時間能夠是一個任意長的時間,可是最終會發生,也就是說,Java內存模型中的可見性的特性主要是針對線程和線程之間使用內存的一種規則和約定,該約定由JMM定義。

  不只僅如此,該模型還容許不一樣步的狀況下可見性特性。好比針對一個線程提供一個對象或者字段訪問域的原始值進行操做,而針對另一個線程提供一個對象或者字段刷新事後的值進行操做。一樣也有可能針對一個線程讀取一個原始的值以及引用對象的對象內容,針對另一個線程讀取一個刷新事後的值或者刷新事後的引用。

  儘管如此,上邊的可見性特性分析的一些特徵在跨線程操做的時候是有可能失敗的,並且不可以避免這些故障發生。這是一個不爭的事實,使用同步多線程的代碼並不能絕對保證線程安全的行爲,只是容許某種規則對其操做進行必定的限制,可是在最新的JVM實現以及最新的Java平臺中,即便是多個處理器,經過一些工具進行可見性的測試發現實際上是不多發生故障的。跨線程共享CPU的共享緩存的使用,其缺陷就在於影響了編譯器的優化操做,這也體現了強有力的緩存一致性使得硬件的價值有所提高,由於它們之間的關係在線程與線程之間的複雜度變得更高。這種方式使得可見度的自由測試顯得更加不切實際,由於這些錯誤的發生極爲罕見,或者說在平臺上咱們開發過程當中根本碰不到。在並行程開發中,不使用同步致使失敗的緣由也不只僅是對可見度的不良把握致使的,致使其程序失敗的緣由是多方面的,包括緩存一致性、內存一致性問題等。

可排序性(Ordering):

  可排序規則在線程與線程之間主要有下邊兩點:

64 從操做線程的角度看來,若是全部的指令執行都是按照普通順序進行,那麼對於一個順序運行的程序而言,可排序性也是順序的

65 從其餘操做線程的角度看來,排序性如同在這個線程中運行在非同步方法中的一個「間諜」,因此任何事情都有可能發生。惟一有用的限制是同步方法和同步塊的相對排序,就像操做volatile字段同樣,老是保留下來使用

  【*:如何理解這裏「間諜」的意思,能夠這樣理解,排序規則在本線程裏面遵循了第一條法則,可是對其餘線程而言,某個線程自身的排序特性可能使得它不定地訪問執行線程的可見域,而使得該線程對自己在執行的線程產生必定的影響。舉個例子,A線程須要作三件事情分別是A1A2A3,而B是另一個線程具備操做B1B2,若是把參考定位到B線程,那麼對A線程而言,B的操做B1B2有可能隨時會訪問到A的可見區域,好比A有一個可見區域aA1就是把a修改稱爲1,可是B線程在A線程調用了A1事後,卻訪問了a而且使用B1或者B2操做使得a發生了改變,變成了2,那麼當A按照排序性進行A2操做讀取到a的值的時候,讀取到的是2而不是1,這樣就使得程序最初設計的時候A線程的初衷發生了改變,就是排序被打亂了,那麼B線程對A線程而言,其身份就是「間諜」,並且須要注意到一點,B線程的這些操做不會和A之間存在等待關係,那麼B線程的這些操做就是異步操做,因此針對執行線程A而言,B的身份就是「非同步方法中的‘間諜’。】

  一樣的,這僅僅是一個最低限度的保障性質,在任何給定的程序或者平臺,開發中有可能發現更加嚴格的排序,可是開發人員在設計程序的時候不能依賴這種排序,若是依賴它們會發現測試難度會成指數級遞增,並且在複合規定的時候會由於不一樣的特性使得JVM的實現由於不符合設計初衷而失敗。

注意:第一點在JLSJava Language Specification)的全部討論中也是被採用的,例如算數表達式通常狀況都是從上到下、從左到右的順序,可是這一點須要理解的是,從其餘操做線程的角度看來這一點又具備不肯定性,對線程內部而言,其內存模型自己是存在排序性的。【*:這裏討論的排序是最底層的內存裏面執行的時候的NativeCode的排序,不是說按照順序執行的Java代碼具備的有序性質,本文主要分析的是JVM的內存模型,因此但願讀者明白這裏指代的討論單元是內存區。】

iii.原始JMM缺陷:

  JMM最初設計的時候存在必定的缺陷,這種缺陷雖然現有的JVM平臺已經修復,可是這裏不得不說起,也是爲了讀者更加了解JMM的設計思路,這一個小節的概念可能會牽涉到不少更加深刻的知識,若是讀者不能讀懂沒有關係先看了文章後邊的章節再返回來看也能夠。

1)問題1:不可變對象不是不可變的

  學過Java的朋友都應該知道Java中的不可變對象,這一點在本文最後講解String類的時候也會說起,而JMM最初設計的時候,這個問題一直都存在,就是:不可變對象彷佛能夠改變它們的值(這種對象的不可變指經過使用final關鍵字來獲得保證),(Publis Service Reminder:讓一個對象的全部字段都爲final並不必定使得這個對象不可變——全部類型還必須是原始類型而不能是對象的引用。而不可變對象被認爲不要求同步的。可是,由於在將內存寫方面的更改從一個線程傳播到另一個線程的時候存在潛在的延遲,這樣就使得有可能存在一種競態條件,即容許一個線程首先看到不可變對象的一個值,一段時間以後看到的是一個不一樣的值。這種狀況之前怎麼發生的呢?在JDK 1.4中的String實現裏,這兒基本有三個重要的決定性字段:對字符數組的引用、長度和描述字符串的開始數組的偏移量String就是以這樣的方式在JDK 1.4中實現的,而不是隻有字符數組,所以字符數組能夠在多個StringStringBuffer對象之間共享,而不須要在每次建立一個String的時候都拷貝到一個新的字符數組裏。假設有下邊的代碼:

String s1 = "/usr/tmp";

String s2 = s1.substring(4); // "/tmp"

  這種狀況下,字符串s2將具備大小爲4長度和偏移量,可是它將和s1共享「/usr/tmp」裏面的同一字符數組,在String構造函數運行以前,Object的構造函數將用它們默認的值初始化全部的字段,包括決定性的長度和偏移字段。當String構造函數運行的時候,字符串長度和偏移量被設置成所須要的值。可是在舊的內存模型中,由於缺少同步,有可能另外一個線程會臨時地看到偏移量字段具備初始默認值0,然後又看到正確的值4,結果是s2的值從「/usr」變成了「/tmp」,這並非咱們真正的初衷,這個問題就是原始JMM的第一個缺陷所在,由於在原始JMM模型裏面這是合理並且合法的,JDK 1.4如下的版本都容許這樣作。

2)問題2:從新排序的易失性和非易失性存儲

  另外一個主要領域是與volatile字段的內存操做從新排序有關,這個領域中現有的JMM引發了一些比較混亂的結果。現有的JMM代表易失性的讀和寫是直接和主存打交道的,這樣避免了把值存儲到寄存器或者繞過處理器特定的緩存,這使得多個線程通常能看見一個給定變量最新的值。但是,結果是這種volatile定義並無最初想象中那樣如願以償,而且致使了volatile的重大混亂。爲了在缺少同步的狀況下提供較好的性能,編譯器、運行時和緩存一般是容許進行內存的從新排序操做的,只要當前執行的線程分辨不出它們的區別。(這就是within-thread as-if-serial semantics[線程內彷佛是串行]的解釋)可是,易失性的讀和寫是徹底跨線程安排的,編譯器或緩存不能在彼此之間從新排序易失性的讀和寫。遺憾的是,經過參考普通變量的讀寫,JMM容許易失性的讀和寫被重排序,這樣覺得着開發人員不能使用易失性標誌做爲操做已經完成的標誌。好比:

Map configOptions;

char[] configText;

volatile boolean initialized = false;

 

// 線程1

configOptions = new HashMap();

configText = readConfigFile(filename);

processConfigOptions(configText,configOptions);

initialized = true;

 

// 線程2

while(!initialized)

    sleep();

  這裏的思想是使用易失性變量initialized擔任守衛來代表一套別的操做已經完成了,這是一個很好的思想,可是不能在JMM下工做,由於舊的JMM容許非易失性的寫(好比寫到configOptions字段,以及寫到由configOptions引用Map的字段中)與易失性的寫一塊兒從新排序,所以另一個線程可能會看到initializedtrue,可是對於configOptions字段或它所引用的對象尚未一個一致的或者說當前的針對內存的視圖變量,volatile的舊語義只承諾在讀和寫的變量的可見性,而不承諾其餘變量,雖然這種方法更加有效的實現,可是結果會和咱們設計之初截然不同。

2.堆和棧

i.Java內存管理簡介:

  內存管理在Java語言中是JVM自動操做的,當JVM發現某些對象再也不須要的時候,就會對該對象佔用的內存進行重分配(釋放)操做,並且使得分配出來的內存可以提供給所須要的對象。在一些編程語言裏面,內存管理是一個程序的職責,可是書寫過C++的程序員很清楚,若是該程序須要本身來書寫頗有可能引發很嚴重的錯誤或者說不可預料的程序行爲,最終大部分開發時間都花在了調試這種程序以及修復相關錯誤上。通常狀況下在Java程序開發過程把手動內存管理稱爲顯示內存管理,而顯示內存管理常常發生的一個狀況就是引用懸掛——也就是說有可能在從新分配過程釋放掉了一個被某個對象引用正在使用的內存空間,釋放掉該空間事後,該引用就處於懸掛狀態。若是這個被懸掛引用指向的對象試圖進行原來對象(由於這個時候該對象有可能已經不存在了)進行操做的時候,因爲該對象自己的內存空間已經被手動釋放掉了,這個結果是不可預知的。顯示內存管理另一個常見的狀況是內存泄漏,當某些引用再也不引用該內存對象的時候,而該對象本來佔用的內存並無被釋放,這種狀況簡言爲內存泄漏。好比,若是針對某個鏈表進行了內存分配,而由於手動分配不當,僅僅讓引用指向了某個元素所處的內存空間,這樣就使得其餘鏈表中的元素不能再被引用並且使得這些元素所處的內存讓應用程序處於不可達狀態並且這些對象所佔有的內存也不可以被再使用,這個時候就發生了內存泄漏。而這種狀況一旦在程序中發生,就會一直消耗系統的可用內存直到可用內存耗盡,而針對計算機而言內存泄漏的嚴重程度大了會使得原本正常運行的程序直接由於內存不足而中斷,並非Java程序裏面出現Exception那麼輕量級。

  在之前的編程過程當中,手動內存管理帶了計算機程序不可避免的錯誤,並且這種錯誤對計算機程序是毀滅性的,因此內存管理就成爲了一個很重要的話題,可是針對大多數純面嚮對象語言而言,好比Java,提供了語言自己具備的內存特性:自動化內存管理,這種語言提供了一個程序垃圾回收器(Garbage Collector[GC],自動內存管理提供了一個抽象的接口以及更加可靠的代碼使得內存可以在程序裏面進行合理的分配。最多見的狀況就是垃圾回收器避免了懸掛引用的問題,由於一旦這些對象沒有被任何引用「可達」的時候,也就是這些對象在JVM的內存池裏面成爲了避免可引用對象,該垃圾回收器會直接回收掉這些對象佔用的內存,固然這些對象必須知足垃圾回收器回收的某些對象規則,而垃圾回收器在回收的時候會自動釋放掉這些內存。不只僅如此,垃圾回收器一樣會解決內存泄漏問題。

ii.詳解堆和棧[圖片以及部份內容來自《Inside JVM]

1)通用簡介

[編譯原理]學過編譯原理的人都明白,程序運行時有三種內存分配策略:靜態的、棧式的、堆式的

靜態存儲——是指在編譯時就可以肯定每一個數據目標在運行時的存儲空間需求,於是在編譯時就能夠給它們分配固定的內存空間。這種分配策略要求程序代碼中不容許有可變數據結構的存在,也不容許有嵌套或者遞歸的結構出現,由於它們都會致使編譯程序沒法計算準確的存儲空間。

棧式存儲——該分配可成爲動態存儲分配,是由一個相似於堆棧的運行棧來實現的,和靜態存儲的分配方式相反,在棧式存儲方案中,程序對數據區的需求在編譯時是徹底未知的,只有到了運行的時候才能知道,可是規定在運行中進入一個程序模塊的時候,必須知道該程序模塊所須要的數據區的大小才能分配其內存。和咱們在數據結構中所熟知的棧同樣,棧式存儲分配按照先進後出的原則進行分配。

堆式存儲——堆式存儲分配則專門負責在編譯時或運行時模塊入口處都沒法肯定存儲要求的數據結構的內存分配,好比可變長度串和對象實例,堆由大片的可利用塊或空閒塊組成,堆中的內存能夠按照任意順序分配和釋放。

[C++語言]對比C++語言裏面,程序佔用的內存分爲下邊幾個部分:

[1]棧區(Stack:由編譯器自動分配釋放,存放函數的參數值,局部變量的值等。其操做方式相似於數據結構中的棧。咱們在程序中定義的局部變量就是存放在棧裏,當局部變量的生命週期結束的時候,它所佔的內存會被自動釋放。

[2]堆區(Heap:通常由程序員分配和釋放,若程序員不釋放,程序結束時可能由OS回收。注意它與數據結構中的堆是兩回事,分配方式卻是相似於鏈表。咱們在程序中使用c++new或者c中的malloc申請的一塊內存,就是在heap上申請的,在使用完畢後,是須要咱們本身動手釋放的,不然就會產生「內存泄露」的問題。

[3]全局區(靜態區)(Static:全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域,未初始化的全局變量和未初始化的靜態變量在相鄰的另外一塊區域。程序結束後由系統釋放。

[4]文字常量區:常量字符串就是放在這裏的,程序結束後由系統釋放。在Java中對應有一個字符串常量池。

[5]程序代碼區:存放函數體的二進制代碼

2)JVM結構【堆、棧解析】:

  在Java虛擬機規範中,一個虛擬機實例的行爲主要描述爲:子系統內存區域數據類型指令,這些組件在描述了抽象的JVM內部的一個抽象結構。與其說這些組成部分的目的是進行JVM內部結構的一種支配,更多的是提供一種嚴格定義實現的外部行爲,該規範定義了這些抽象組成部分以及相互做用的任何Java虛擬機執行所須要的行爲。下圖描述了JVM內部的一個結構,其中主要包括主要的子系統、內存區域,如同之前在《Java基礎知識》中描述的:Java虛擬機有一個類加載器做爲JVM的子系統,類加載器針對Class進行檢測以鑑定徹底合格的類接口,而JVM內部也有一個執行引擎:

JVM運行一個程序的時候,它的內存須要用來存儲不少內容,包括字節碼、以及從類文件中提取出來的一些附加信息、以及程序中實例化的對象、方法參數、返回值、局部變量以及計算的中間結果JVM的內存組織須要在不一樣的運行時數據區進行以上的幾個操做,下邊針對上圖裏面出現的幾個運行時數據區進行詳細解析:一些運行時數據區共享了全部應用程序線程和其餘特有的單個線程,每一個JVM實例有一個方法區和一個內存堆,這些是共同在虛擬機內運行的線程。在Java程序裏面,每一個新的線程啓動事後,它就會被JVM在內部分配本身的PC寄存器[PC registers]程序計數器器)和Java堆棧Java stacks)。若該線程正在執行一個非本地Java方法,在PC寄存器的值指示下一條指令執行,該線程在Java內存棧中保存了非本地Java方法調用狀態,其狀態包括局部變量、被調用的參數、它的返回值、以及中間計算結果。而本地方法調用的狀態則是存儲在獨立的本地方法內存棧裏面(native method stacks),這種狀況下使得這些本地方法和其餘內存運行時數據區的內容儘量保證和其餘內存運行時數據區獨立,並且該方法的調用更靠近操做系統,這些方法執行的字節碼有可能根據操做系統環境的不一樣使得其編譯出來的本地字節碼的結構也有必定的差別。JVM中的內存棧是一個棧幀的組合,一個棧幀包含了某個Java方法調用的狀態,當某個線程調用方法的時候,JVM就會將一個新的幀壓入Java內存棧,當方法調用完成事後,JVM將會從內存棧中移除該棧幀。JVM裏面不存在一個能夠存放中間計算數據結果值的寄存器,其內部指令集使用Java棧空間來存儲中間計算的數據結果值,這種作法的設計是爲了保持Java虛擬機的指令集緊湊,使得與寄存器原理可以緊密結合而且進行操做。

1)方法區(Method Area

  在JVM實例中,對裝載的類型信息是存儲在一個邏輯方法內存區中,當Java虛擬機加載了一個類型的時候,它會跟着這個Class的類型去路徑裏面查找對應的Class文件,類加載器讀取類文件(線性二進制數據),而後將該文件傳遞給Java虛擬機,JVM從二進制數據中提取信息而且將這些信息存儲在方法區,而類中聲明(靜態)變量就是來自於方法區中存儲的信息。在JVM裏面用什麼樣的方式存儲該信息是由JVM設計的時候決定的,例如:當數據進入方法的時候,多類文件字節的存儲量以Big-Endian(第一次最重要的字節)的順序存儲,儘管如此,一個虛擬機能夠用任何方式針對這些數據進行存儲操做,若它存儲在一個Little-Endian處理器上,設計的時候就有可能將多文件字節的值按照Little-Endian順尋存儲。

——【$Big-EndianLittle-Endian】——

  程序存儲數據過程當中,若是數據是跨越多個字節對象就必須有一種約定:

66 它的地址是多少:對於跨越多個字節的對象,通常它所佔的字節都是連續的,它的地址等於它所佔字節最低地址,這種狀況鏈表可能存儲的僅僅是表頭

67 它的字節在內存中是如何組織的

  好比:int x,它的地址爲0x100,那麼它佔據了內存中的0x1000x1010x1020x103四個字節,因此通常狀況咱們以爲int4個字節。上邊只是內存組織的一種狀況,多字節對象在內存中的組織有兩種約定,還有一種狀況:若一個整數爲W位,它的表示以下:

  每一位表示爲:[Xw-1,Xw-2,...,X1,X0]

  它的最高有效字節MSBMost Significant Byte爲:[Xw-1,Xw-2,...,Xw-8]

最低有效字節LSBLeast Significant Byte爲:[X7,X6,...,X0]

  其他字節則位於LSBMSB之間

LSBMSB誰位於內存的最低地址,即表明了該對象的地址,這樣就引出了Big-EndianLittle-Endian的問題,若是LSBMSB前,LSB是最低地址,則該機器是小端,反之則是大端。DESDigital Equipment Corporation,如今是Compaq公司的一部分)和Intel機器(x86平臺)通常採用小端,IBMMotorolaPower PC)、Sun的機器通常採用大端。固然這種不能表明全部狀況,有的CPU既能工做於小端、又能夠工做於大端,好比ARMAlpha、摩托羅拉的PowerPC,這些狀況根據具體的處理器型號有所不一樣。可是大部分操做系統(WindowsFreeBSDLinux)通常都是Little Endian的,少部分系統(Mac OS)是Big Endian的,因此用什麼方式存儲還得依賴宿主操做系統環境。

 

由上圖能夠看到,映射訪問(「寫32位地址的0」)主要是由寄存器到內存、由內存到寄存器的一種數據映射方式,Big-Endian在上圖能夠看出的原子內存單位(Atomic Unit在系統內存中的增加方向爲從左到右,而Little-Endian的地址增加方向爲從右到左。舉個例子:

  若要存儲數據0x0A0B0C0D

Big-Endian

  以8爲一個存儲單位,其存儲的地址增加爲:

上圖中能夠看出MSB的值存儲了0x0A,這種狀況下數據的高位是從內存的低地址開始存儲的,而後從左到右開始增加,第二位0x0B就是存儲在第二位的,若是是按照16位爲一個存儲單位,其存儲方式又爲:

則能夠看到Big-Endian的映射地址方式爲:

 

MSB在計算機中,最高有效位(MSB)是指位值的存儲位置爲轉換爲二進制數據後的最大值,MSB有時候在Big-Endian的架構中稱爲最左最大數據位,這種狀況下再往左邊的內存位則不是數據位了,而是有效位數位置的最高符號位,不只僅如此,MSB也能夠對應一個二進制符號位的符號位補碼標記:「1」的含義爲負,「0」的含義爲正。最高位表明了「最重要字節」,也就是說當某些多字節數據擁有了最大值的時候它就是存儲的時候最高位數據的字節對應的內存位置:

Little-Endian

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

能夠看到LSB的值存儲的0x0D,也就是數據的最低位是從內存的低地址開始存儲的,它的高位是從右到左的順序逐漸增長內存分配空間進行存儲的,若是按照十六位爲存儲單位存儲格式爲:

從上圖能夠看到最低的16位的存儲單位裏面存儲的值爲0x0C0D,接着纔是0x0A0B,這樣就能夠看到按照數據從高位到低位在內存中存儲的時候是從右到左進行遞增存儲的,實際上能夠從寫內存的順序來理解,實際上數據存儲在內存中無非在使用的時候是寫內存讀內存,針對LSB的方式最好的書面解釋就是向左增長來看待,若是真正在進行內存讀寫的時候使用這樣的順序,其意義就體現出來了:

 

按照這種讀寫格式,0x0D存儲在最低內存地址,而從右往左的增加就能夠看到LSB存儲的數據爲0x0D,和初衷吻合,則十六位的存儲就能夠按照下邊的格式來解釋:

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

 

使用這種方式進行內存讀寫的時候就會發現計算機語言和語言自己的順序會有衝突,這種衝突主要是以使用語言的人的習慣有關,而書面化的語言從左到右就能夠知道其衝突是不可避免的。咱們通常使用語言的閱讀方式都是從左到右,而低端存儲(Little-Endian)的這種內存讀寫的方式使得咱們最終從計算機裏面讀取字符須要進行倒序,並且考慮另一個問題,若是是針對中文而言,一個字符是兩個字節,就會出現總體順序和每個位的順序會進行兩次倒序操做,這種方式真正在製做處理器的時候也存在一種計算上的衝突,而針對使用文字從左到右進行閱讀的國家而言,從右到左的方式(Big-Endian)則會有這樣的文字衝突,另一方面,儘管有不少國家使用語言是從右到左,可是僅僅和Big-Endian的方式存在衝突,這些國家畢竟佔少數,因此能夠理解的是,爲何主流的系統都是使用的Little-Endian的方式

*:這裏不解釋Middle-Endian的方式以及Mixed-Endian的方式】

LSB在計算機中,最低有效位是一個二進制給予單位的整數,位的位置肯定了該數據是一個偶數仍是奇數LSB有時被稱爲最右位。在使用具體位二進制數以內,常見的存儲方式就是每一位存儲1或者0的方式,從0向上到1每一比特逢二進一的存儲方式。LSB的這種特性用來指定單位位,而不是位的數字,而這種方式也有可能產生必定的混亂。

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

  JVM虛擬機將搜索和使用類型的一些信息也存儲在方法區中以方便應用程序加載讀取該數據。設計者在設計過程也考慮到要方便JVM進行Java應用程序的快速執行,而這種取捨主要是爲了程序在運行過程當中內存不足的狀況可以經過必定的取捨去彌補內存不足的狀況。在JVM內部,全部的線程共享相同方法區,所以,訪問方法區的數據結構必須是線程安全的,若是兩個線程都試圖去調用去找一個名爲Lava的類,好比Lava尚未被加載,只有一個線程能夠加載該類而另外的線程只可以等待。方法區的大小在分配過程當中是不固定的,隨着Java應用程序的運行,JVM能夠調整其大小,須要注意一點,方法區的內存不須要是連續的,由於方法區內存能夠分配內存堆中,即便是虛擬機JVM實例對象本身所在的內存堆也是可行的,而在實現過程是容許程序員自身來指定方法區的初始化大小的。

  一樣的,由於Java自己的自動內存管理,方法區也會被垃圾回收的,Java程序能夠經過類擴展動態加載器對象,類能夠成爲「未引用」向垃圾回收器進行申請,若是一個類是「未引用」的,則該類就可能被卸載,

  而方法區針對具體的語言特性有幾種信息是存儲在方法區內的:

【類型信息】

68 類型的徹底限定名(java.lang.String格式)

69 類型的徹底限定名的直接父類的徹底限定名(除非這個父類的類型是一個接口或者java.lang.Object

70 不論類型是一個類或者接口

71 類型的修飾符(例如publicabstractfinal

72 任何一個直接超類接口的徹底限定名的列表

  在JVM和類文件名的內部,類型名通常都是徹底限定名(java.lang.String)格式,在Java源文件裏面,徹底限定名必須加入包前綴,而不是咱們在開發過程寫的簡單類名,而在方法上,只要是符合Java語言規範的類的徹底限定名均可以,而JVM可能直接進行解析,好比:(java.lang.String)在JVM內部名稱爲java/lang/String,這就是咱們在異常捕捉的時候常常看到的ClassNotFoundException的異常裏面類信息的名稱格式。

  除此以外,還必須爲每一種加載過的類型在JVM內進行存儲,下邊的信息不存儲在方法區內,下邊的章節會一一說明

73 類型常量池

74 字段信息

75 方法信息

76 全部定義在Class內部的(靜態)變量信息,除開常量

77 一個ClassLoader的引用

78 Class的引用

【常量池】

  針對類型加載的類型信息,JVM將這些存儲在常量池裏,常量池是一個根據類型定義的常量的有序常量集,包括字面量(StringIntegerFloat常量)以及符號引用(類型、字段、方法),整個長量池會被JVM的一個索引引用,如同數組裏面的元素集合按照索引訪問同樣,JVM針對這些常量池裏面存儲的信息也是按照索引方式進行。實際上長量池在Java程序的動態連接過程起到了一個相當重要的做用。

【字段信息】

  針對字段的類型信息,下邊的信息是存儲在方法區裏面的:

79 字段名

80 字段類型

81 字段修飾符(public,private,protected,static,final,volatile,transient

【方法信息】

  針對方法信息,下邊信息存儲在方法區上:

82 方法名

83 方法的返回類型(包括void

84 方法參數的類型、數目以及順序

85 方法修飾符(public,private,protected,static,final,synchronized,native,abstract

  針對非本地方法,還有些附加方法信息須要存儲在方法區內:

86 方法字節碼

87 方法中局部變量區的大小、方法棧幀

88 異常表

【類變量】

  類變量在一個類的多個實例之間共享,這些變量直接和類相關,而不是和類的實例相關,(定義過程簡單理解爲類裏面定義的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堆棧的初始大小以及最大、最小尺寸。

【概念區分】

89 內存棧:這裏的內存棧和物理結構內存堆棧有點點區別,是內存裏面數據存儲的一種抽象數據結構。從操做系統上講,在程序執行過程對內存的使用自己經常使用的數據結構就是內存堆棧,而這裏的內存堆棧指代的就是JVM在使用內存過程整個內存的存儲結構,多指內存的物理結構,而Java內存棧不是指代的一個物理結構,更多的時候指代的是一個抽象結構,就是符合JVM語言規範的內存棧的一個抽象結構。由於物理內存堆棧結構和Java內存棧的抽象模型結構自己比較類似,因此咱們在學習過程就正常把這兩種結構放在一塊兒考慮了,並且兩者除了概念上有一點點小的區別,理解成爲一種結構對於初學者也何嘗不可,因此實際上也能夠以爲兩者沒有太大的本質區別。可是在學習的時候最好分清楚內存堆棧和Java內存棧的一小點細微的差距,前者是物理概念和自己模型,後者是抽象概念和自己模型的一個共同體。而內存堆棧更多的說法能夠理解爲一個內存塊,由於內存塊能夠經過索引和指針進行數據結構的組合,內存棧就是內存塊針對數據結構的一種表示,而內存堆則是內存塊的另一種數據結構的表示,這樣理解更容易區份內存棧和內存堆棧(內存塊)的概念。

90 棧幀:棧幀是內存棧裏面的最小單位,指的是內存棧裏面每個最小內存存儲單元,它針對內存棧僅僅作了兩個操做:入棧和出棧,通常狀況下:所說的堆棧幀棧幀卻是一個概念,因此在理解上記得加以區分

91 內存堆:這裏的內存堆和內存棧是相對應的,其實內存堆裏面的數據也是存儲在系統內存堆棧裏面的,只是它使用了另一種方式來進行堆裏面內存的管理,而本章題目要講到的就是Java語言自己的內存堆和內存棧,而這兩個概念都是抽象的概念模型,並且是相對的。

棧幀:棧幀主要包括三個部分:局部變量操做數棧幀(操做幀)幀數據(數據幀)。本地變量和操做數幀的大小取決於須要,這些大小是在編譯時就決定的,而且在每一個方法的類文件數據中進行分配,幀的數據大小則不同,它雖然也是在編譯時就決定的可是它的大小和自己代碼實現有關。當JVM調用一個Java方法的時候,它會檢查類的數據來肯定在本地變量和操做方法要求的棧大小,它計算該方法所須要的內存大小,而後將這些數據分配好內存空間壓入到內存堆棧中。

棧幀——局部變量:局部變量是以Java棧幀組合成爲的一個以零爲基的數組,使用局部變量的時候使用的其實是一個包含了0的一個基於索引的數組結構int類型、float、引用以及返回值都佔據了一個數組中的局部變量的條目,而byteshortchar則在存儲到局部變量的時候是先轉化成爲int再進行操做的,則longdouble則是在這樣一個數組裏面使用了兩個元素的空間大小,在局部變量裏面存儲基本數據類型的時候使用的就是這樣的結構。舉個例子:

class Example3a{

    public static int runClassMethod(int i,long l,float f,double d,Object o,byte b)

    {

        return 0;

    }

    public int runInstanceMethod(char c,double d,short s,boolean b)

    {

        return 0;

    }

}

棧幀——操做幀:和局部變量同樣,操做幀也是一組有組織的數組的存儲結構,可是和局部變量不同的是這個不是經過數組的索引訪問的,而是直接進行的入棧和出棧的操做,當操做指令直接壓入了操做棧幀事後,從棧幀裏面出來的數據會直接在出棧的時候被讀取使用。除了程序計數器之外,操做幀也是能夠直接被指令訪問到的,JVM裏面沒有寄存器。處理操做幀的時候Java虛擬機是基於內存棧的而不是基於寄存器的,由於它在操做過程是直接對內存棧進行操做而不是針對寄存器進行操做。而JVM內部的指令也能夠來源於其餘地方好比緊接着操做符以及操做數的字節碼流或者直接從常量池裏面進行操做。JVM指令其實真正在操做過程的焦點是集中在內存棧棧幀的操做幀上的。JVM指令將操做幀做爲一個工做空間,有許多指令都是從操做幀裏面出棧讀取的,對指令進行操做事後將操做幀的計算結果從新壓入內存堆棧內。好比iadd指令將兩個整數壓入到操做幀裏面,而後將兩個操做數進行相加,相加的時候從內存棧裏面讀取兩個操做數的值,而後進行運算,最後將運算結果從新存入到內存堆棧裏面。舉個簡單的例子:

begin

iload_0 //將整數類型的局部變量0壓入到內存棧裏面

iload_1 //將整數類型的局部變量1壓入到內存棧裏面

iadd     //將兩個變量出棧讀取,而後進行相加操做,將結果從新壓入棧中

istore_2 //將最終輸出結果放在另一個局部變量裏面

end

  綜上所述,就是整個計算過程針對內存的一些操做內容,而總體的結構能夠用下圖來描述:

棧幀——數據幀:除了局部變量和操做幀之外,Java棧幀還包括了數據幀,用於支持常量池、普通的方法返回以及異常拋出等,這些數據都是存儲在Java內存棧幀的數據幀中的。不少JVM的指令集實際上使用的都是常量池裏面的一些條目,一些指令,只是把intlongfloatdouble或者String從常量池裏面壓入到Java棧幀的操做幀上邊,一些指令使用常量池來管理類或者數組的實例化操做、字段的訪問控制、或者方法的調用,其餘的指令就用來決定常量池條目中記錄的某一特定對象是否某一類或者常量池項中指定的接口。常量池會判斷類型、字段、方法、類、接口、類字段以及引用是如何在JVM進行符號化描述,而這個過程由JVM自己進行對應的判斷。這裏就能夠理解JVM如何來判斷咱們一般說的:「原始變量存儲在內存棧上,而引用的對象存儲在內存堆上邊。」除了常量池判斷幀數據符號化描述特性之外,這些數據幀必須在JVM正常執行或者異常執行過程輔助它進行處理操做。若是一個方法是正常結束的,JVM必須恢復棧幀調用方法的數據幀,並且必須設置PC寄存器指向調用方法後邊等待的指令完成該調用方法的位置。若是該方法存在返回值,JVM也必須將這個值壓入到操做幀裏面以提供給須要這些數據的方法進行調用。不只僅如此,數據幀也必須提供一個方法調用的異常表,當JVM在方法中拋出異常而非正常結束的時候,該異常表就用來存放異常信息。

3)內存堆(Heap):

  當一個Java應用程序在運行的時候在程序中建立一個對象或者一個數組的時候,JVM會針對該對象和數組分配一個新的內存堆空間。可是在JVM實例內部,只存在一個內存堆實例,全部的依賴該JVMJava應用程序都須要共享該堆實例,而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規範裏面沒有嚴格說明,也有可能真正在實現過程只是一個抽象概念物理層它根本不存在,針對放發表實現對於一個建立的實例而言,它自己具備不過高的內存須要求,若是該實現裏面使用了方法表,則對象的方法表應該是能夠很快被外圍引用訪問到的。

  有一種辦法就是經過對象引用鏈接到方法表的時候,以下圖:

該圖代表,在每一個指針指向一個對象的時候,其實是使用的一個特殊的數據結構,這些特殊的結構包括幾個部分:

92 一個指向該對象類全部數據的指針

93 該對象的方法表

  實際上從圖中能夠看出,方法表就是一個指針數組,它的每個元素包含了一個指針,針對每一個對象的方法均可以直接經過該指針在方法區中找到匹配的數據進行相關調用,而這些方法表須要包括的內容以下:

94 方法內存堆棧段空間中操做棧的大小以及局部變量

95 方法字節碼

96 一個方法的異常表

  這些信息使得JVM足夠針對該方法進行調用,在調用過程,這種結構也可以方便子類對象的方法直接經過指針引用到父類的一些方法定義,也就是說指針在內存空間以內經過JVM自己的調用使得父類的一些方法表也能夠一樣的方式被調用,固然這種調用過程避免不了兩個對象之間的類型檢查,可是這樣的方式就使得繼承的實現變得更加簡單,並且方法表提供的這些數據足夠引用對對象進行帶有任何OO特徵的對象操做。

  另一種數據在上邊的途中沒有顯示出來,也是從邏輯上講內存堆中的對象的真實數據結構——對象的鎖。這一點可能須要關聯到JMM模型中講的進行理解。JVM中的每個對象都是和一個鎖(互斥)相關聯的,這種結構使得該對象能夠很容易支持多線程訪問,並且該對象的對象鎖一次只能被一個線程訪問。當一個線程在運行的時候具備某個對象的鎖的時候,僅僅只有這個線程能夠訪問該對象的實例變量,其餘線程若是須要訪問該實例的實例變量就必須等待這個線程將它佔有的對象鎖釋放事後纔可以正常訪問,若是一個線程請求了一個被其餘線程佔有的對象鎖,這個請求線程也必須等到該鎖被釋放事後纔可以拿到這個對象的對象鎖。一旦這個線程擁有了一個對象鎖事後,它本身能夠屢次向同一個鎖發送對象的鎖請求,可是若是它要使得被該線程鎖住的對象能夠被其餘鎖訪問到的話就須要一樣的釋放鎖的次數,好比線程A請求了對象B的對象鎖三次,那麼A將會一直佔有B對象的對象鎖,直到它將該對象鎖釋放了三次。

  不少對象也可能在整個生命週期都沒有被對象鎖鎖住過,在這樣的狀況下對象鎖相關的數據是不須要對象內部實現的,除非有線程向該對象請求了對象鎖,不然這個對象就沒有該對象鎖的存儲結構。因此上邊的實現圖能夠知道,不少實現不包括指向對象鎖的「鎖數據」,鎖數據的實現必需要等待某個線程向該對象發送了對象鎖請求事後,並且是在第一次鎖請求事後纔會被實現。這個結構中,JVM卻可以間接地經過一些辦法針對對象的鎖進行管理,好比把對象鎖放在基於對象地址的搜索樹上邊。實現了鎖結構的對象中,每個Java對象邏輯上都在內存中成爲了一個等待集,這樣就使得全部的線程在鎖結構裏面針對對象內部數據能夠獨立操做,等待集就使得每一個線程可以獨立於其餘線程去完成一個共同的設計目標以及程序執行的最終結果,這樣就使得多線程的線程獨享數據以及線程共享數據機制很容易實現。

  不只僅如此,針對內存堆對象還必須存在一個對象的鏡像,該鏡像的主要目的是提供給垃圾回收器進行監控操做,垃圾回收器是經過對象的狀態來判斷該對象是否被應用,一樣它須要針對堆內的對象進行監控。而當監控過程垃圾回收器收到對象回收的事件觸發的時候,雖然使用了不一樣的垃圾回收算法,不論使用什麼算法都須要經過獨有的機制來判斷對象目前處於哪一種狀態,而後根據對象狀態進行操做。開發過程程序員每每不會去仔細分析當一個對象引用設置成爲null了事後虛擬機內部的操做,但實際上Java裏面的引用每每不像咱們想像中那麼簡單,Java引用中的虛引用、弱引用就是使得Java引用在顯示提交可回收狀態的狀況下對內存堆中的對象進行的反向監控,這些引用能夠監視到垃圾回收器回收該對象的過程。垃圾回收器自己的實現也是須要內存堆中的對象可以提供相對應的數據的。其實這個位置到底JVM裏面是否使用了完整的Java對象的鏡像仍是使用的一個鏡像索引我沒有去仔細分析過,總之是在堆結構裏面存在着堆內對象的一個相似拷貝的鏡像機制,使得垃圾回收器可以順利回收再也不被引用的對象。

4)內存棧和內存堆的實現原理探測【該部分爲不肯定概念】:

  實際上不管是內存棧結構、方法區仍是內存堆結構,歸根到底使用的是操做系統的內存,操做系統的內存結構能夠理解爲內存塊,經常使用的抽象方式就是一個內存堆棧,而JVMOS上邊安裝了事後,就在啓動Java程序的時候按照配置文件裏面的內容向操做系統申請內存空間,該內存空間會按照JVM內部的方法提供相應的結構調整。

  內存棧應該是很容易理解的結構實現,通常狀況下,內存棧是保持連續的,可是不絕對,內存棧申請到的地址實際上不少狀況下都是連續的,而每一個地址的最小單位是按照計算機位來算的,該計算機位裏面只有兩種狀態10,而內存棧的使用過程就是典型的相似C++裏面的普通指針結構的使用過程,直接針對指針進行++或者--操做就修改了該指針針對內存的偏移量,而這些偏移量就使得該指針能夠調用不一樣的內存棧中的數據。至於針對內存棧發送的指令就是常見的計算機指令,而這些指令就使得該指針針對內存棧的棧幀進行指令發送,好比發送操做指令、變量讀取等等,直接就使得內存棧的調用變得更加簡單,並且棧幀在接受了該數據事後就知道到底針對棧幀內部的哪個部分進行調用,是操做幀、數據幀仍是局部變量。

  內存堆實際上在操做系統裏面使用了雙向鏈表的數據結構,雙向鏈表的結構使得即便內存堆不具備連續性,每個堆空間裏面的鏈表也能夠進入下一個堆空間,而操做系統自己在整理內存堆的時候會作一些簡單的操做,而後經過每個內存堆的雙向鏈表就使得內存堆更加方便。並且堆空間不須要有序,甚至說有序不影響堆空間的存儲結構,由於它歸根究竟是在內存塊上邊進行實現的,內存塊自己是一個堆棧結構,只是該內存堆棧裏面的塊如何分配不禁JVM決定,是由操做系統已經最開始分配好了,也就是最小存儲單位。而後JVM拿到從操做系統申請的堆空間事後,先進行初始化操做,而後就能夠直接使用了。

  常見的對程序有影響的內存問題主要是兩種:溢出和內存泄漏,上邊已經講過了內存泄漏,其實從內存的結構分析,泄漏這種狀況很難甚至說不可能發生在棧空間裏面,其主要緣由是棧空間自己很難出現懸停的內存,由於棧空間的存儲結構有多是內存的一個地址數組,因此在訪問棧空間的時候使用的都是索引或者下標或者就是最原始的出棧和入棧的操做,這些操做使得棧裏面很難出現像堆空間同樣的內存懸停(也就是引用懸掛)問題。堆空間懸停的內存是由於棧中存放的引用的變化,其實引用能夠理解爲從棧到堆的一個指針,當該指針發生變化的時候,堆內存碎片就有可能產生,而這種狀況下在原始語言裏面就常常發生內存泄漏的狀況,由於這些懸停的堆空間在系統裏面是不可以被任何本地指針引用到,就使得這些對象在未被回收的時候脫離了可操做區域而且佔用了系統資源。

  棧溢出問題一直都是計算機領域裏面的一個安全性問題,這裏不作深刻討論,說多了就偏離主題了,而內存泄漏是程序員最容易理解的內存問題,還有一個問題來自於我一個黑客朋友就是:堆溢出現象,這種現象可能更加複雜。

  其實Java裏面的內存結構,最初看來就是堆和棧的結合,實際上能夠這樣理解,實際上對象的實際內容才存在對象池裏面,而有關對象的其餘東西有可能會存儲於方法區,而平時使用的時候的引用是存在內存棧上的,這樣就更加容易理解它內部的結構,不只僅如此,有時候還須要考慮到Java裏面的一些字段和屬性究竟是對象域的仍是類域的,這個也是一個比較複雜的問題。

兩者的區別簡單總結一下:

97 管理方式:JVM本身能夠針對內存棧進行管理操做,並且該內存空間的釋放是編譯器就能夠操做的內容,而堆空間在JavaJVM自己執行引擎不會對其進行釋放操做,而是讓垃圾回收器進行自動回收

98 空間大小:通常狀況下棧空間相對於堆空間而言比較小,這是由棧空間裏面存儲的數據以及自己須要的數據特性決定的,而堆空間在JVM堆實例進行分配的時候通常大小都比較大,由於堆空間在一個Java程序中須要存儲太多的Java對象數據

99 碎片相關:針對堆空間而言,即便垃圾回收器可以進行自動堆內存回收,可是堆空間的活動量相對棧空間而言比較大,頗有可能存在長期的堆空間分配和釋放操做,並且垃圾回收器不是實時的,它有可能使得堆空間的內存碎片主鍵累積起來。針對棧空間而言,由於它自己就是一個堆棧的數據結構,它的操做都是一一對應的,並且每個最小單位的結構棧幀和堆空間內複雜的內存結構不同,因此它通常在使用過程不多出現內存碎片。

100 分配方式:通常狀況下,棧空間有兩種分配方式:靜態分配和動態分配,靜態分配是自己由編譯器分配好了,而動態分配可能根據狀況有所不一樣,而堆空間倒是徹底的動態分配的,是一個運行時級別的內存分配。而棧空間分配的內存不須要咱們考慮釋放問題,而堆空間即便在有垃圾回收器的前提下仍是要考慮其釋放問題。

101 效率:由於內存塊自己的排列就是一個典型的堆棧結構,因此棧空間的效率天然比起堆空間要高不少,並且計算機底層內存空間自己就使用了最基礎的堆棧結構使得棧空間和底層結構更加符合,它的操做也變得簡單就是最簡單的兩個指令:入棧和出棧;棧空間針對堆空間而言的弱點是靈活程度不夠,特別是在動態管理的時候。而堆空間最大的優點在於動態分配,由於它在計算機底層實現多是一個雙向鏈表結構,因此它在管理的時候操做比棧空間複雜不少,天然它的靈活度就高了,可是這樣的設計也使得堆空間的效率不如棧空間,並且低不少。

 

3.本機內存[部份內容來源於IBM開發中心]

  Java堆空間是在編寫Java程序中被咱們使用得最頻繁的內存空間,平時開發過程,開發人員必定遇到過OutOfMemoryError,這種結果有可能來源於Java堆空間的內存泄漏,也多是由於堆的大小不夠而致使的,有時候這些錯誤是能夠依靠開發人員修復的,可是隨着Java程序須要處理愈來愈多的併發程序,可能有些錯誤就不是那麼容易處理了。有些時候即便Java堆空間沒有滿也可能拋出錯誤,這種狀況下須要瞭解的就是JREJava 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 generationPermGen)堆區域,從最基本的層面來看,使用更多的類將須要使用更多內存。(這可能意味着您的本機內存使用量會增長,或者您必須明確地從新設置 PermGen 或共享類緩存等區域的大小,以裝入全部類)。記住,不只您的應用程序須要加載到內存中,框架、應用服務器、第三方庫以及包含類的 Java 運行時也會按需加載並佔用空間。Java 運行時能夠卸載類來回收空間,可是隻有在很是嚴酷的條件下才會這樣作,不能卸載單個類,而是卸載類加載器,隨其加載的全部類都會被卸載。只有在如下狀況下才能卸載類加載器

102 Java 堆不包含對錶示該類加載器的 java.lang.ClassLoader 對象的引用。

103 Java 堆不包含對錶示類加載器加載的類的任何 java.lang.Class 對象的引用。

104 在 Java 堆上,該類加載器加載的任何類的全部對象都再也不存活(被引用)。

  須要注意的是,Java 運行時爲全部 Java 應用程序建立的 個默認類加載器 bootstrapextension 和 application 都不可能知足這些條件,所以,任何系統類(好比 java.lang.String)或經過應用程序類加載器加載的任何應用程序類都不能在運行時釋放。即便類加載器適合進行收集,運行時也只會將收集類加載器做爲 GC 週期的一部分。一些實現只會在某些 GC 週期中卸載類加載器,也可能在運行時生成類,而不去釋放它。許多 Java EE 應用程序使用 JavaServer Pages (JSP) 技術來生成 Web 頁面。使用 JSP 會爲執行的每一個 .jsp 頁面生成一個類,而且這些類會在加載它們的類加載器的整個生存期中一直存在 —— 這個生存期一般是 Web 應用程序的生存期。另外一種生成類的常見方法是使用 Java 反射。反射的工做方式因 Java 實現的不一樣而不一樣,當使用 java.lang.reflect API 時,Java 運行時必須將一個反射對象(好比 java.lang.reflect.Field)的方法鏈接到被反射到的對象或類。這能夠經過使用 Java 本機接口Java Native InterfaceJNI訪問器來完成,這種方法須要的設置不多,可是速度緩慢,也能夠在運行時爲您想要反射到的每種對象類型動態構建一個類。後一種方法在設置上更慢,但運行速度更快,很是適合於常常反射到一個特定類的應用程序。Java 運行時在最初幾回反射到一個類時使用 JNI 方法,但當使用了若干次 JNI 方法以後,訪問器會膨脹爲字節碼訪問器,這涉及到構建類並經過新的類加載器進行加載。執行屢次反射可能致使建立了許多訪問器類和類加載器,保持對反射對象的引用會致使這些類一直存活,並繼續佔用空間,由於建立字節碼訪問器很是緩慢,因此 Java 運行時能夠緩存這些訪問器以備之後使用,一些應用程序和框架還會緩存反射對象,這進一步增長了它們的本機內存佔用。

4)JNI

  JNI支持本機代碼調用Java方法,反之亦然,Java運行時自己極大依賴於JNI代碼來實現類庫功能,好比文件和網絡I/OJNI應用程序能夠經過三種方式增長Java運行時對本機內存的使用:

105 JNI應用程序的本機代碼被編譯到共享庫中,或編譯爲加載到進程地址空間中的可執行文件,大型本機應用程序可能僅僅加載就會佔用大量進程地址空間

106 本機代碼必須與Java運行時共享地址空間,任何本機代碼分配本機代碼執行內存映射都會耗用Java運行時內存

107 某些JNI函數可能在它們的常規操做中使用本機內存,GetTypeArrayElementsGetTypeArrayRegion函數能夠將Java堆複製到本機內存緩衝區中,提供給本地代碼使用,是否複製數據依賴於運行時實現,經過這種方式訪問大量Java堆數據就可能使用大量的本機內存堆空間

5)NIO

  JDK 1.4開始添加了新的I/O類,引入了一種基於通道和緩衝區執行I/O的新方式,就像Java堆上的內存支持I/O緩衝區同樣,NIO添加了對直接ByteBuffer的支持,ByteBuffer受本機內存而不是Java堆的支持,直接ByteBuffer能夠直接傳遞到本機操做系統庫函數,以執行I/O,這種狀況雖然提升了Java程序在I/O的執行效率,可是會對本機內存進行直接的內存開銷。ByteBuffer直接操做和非直接操做的區別以下:

對於在何處存儲直接 ByteBuffer 數據,很容易產生混淆。應用程序仍然在 Java 堆上使用一個對象來編排 I/O 操做,但持有該數據的緩衝區將保存在本機內存中,Java 堆對象僅包含對本機堆緩衝區的引用。非直接 ByteBuffer 將其數據保存在 Java 堆上的 byte[] 數組中。直接ByteBuffer對象會自動清理本機緩衝區,但這個過程只能做爲JavaGC的一部分執行,它不會自動影響施加在本機上的壓力。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 內存管理系統控制之下,但任何本機代碼(不管其位於 JVMJava 類庫仍是應用程序代碼中)均可能執行本機內存分配,並且會失敗。嘗試進行分配的代碼而後會處理這種狀況,不管設計人員的意圖是什麼:它可能經過 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);

 

        // positionlimit,也就是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);

    }

}

  上邊代碼就是從ByteBufferbyte數組轉換過程,有了這個過程在開發過程當中可能更加方便,ByteBuffer的詳細講解我保留到IO部分,這裏僅僅是涉及到了一些,因此提供兩段實例代碼。

iv.共享內存:

  在Java語言裏面,沒有共享內存的概念,可是在某些引用中,共享內存卻很受用,例如Java語言的分佈式系統,存着大量的Java分佈式共享對象,不少時候須要查詢這些對象的狀態,以查看系統是否運行正常或者瞭解這些對象目前的一些統計數據和狀態。若是使用的是網絡通訊的方式,顯然會增長應用的額外開銷,也增長了沒必要要的應用編程,若是是共享內存方式,則能夠直接經過共享內存查看到所須要的對象的數據和統計數據,從而減小一些沒必要要的麻煩。

1)共享內存特色:

108 能夠被多個進程打開訪問

109 讀寫操做的進程在執行讀寫操做的時候其餘進程不能進行寫操做

110 多個進程能夠交替對某一個共享內存執行寫操做

111 一個進程執行了內存寫操做事後,不影響其餘進程對該內存的訪問,同時其餘進程對更新後的內存具備可見性

112 在進程執行寫操做時若是異常退出,對其餘進程的寫操做禁止自動解除

113 相對共享文件,數據訪問的方便性和效率  

2)出現狀況:

114 獨佔的寫操做,相應有獨佔的寫操做等待隊列。獨佔的寫操做自己不會發生數據的一致性問題;

115 共享的寫操做,相應有共享的寫操做等待隊列。共享的寫操做則要注意防止發生數據的一致性問題;

116 獨佔的讀操做,相應有共享的讀操做等待隊列;

117 共享的讀操做,相應有共享的讀操做等待隊列;

3)Java中共享內存的實現:

JDK 1.4裏面的MappedByteBuffer爲開發人員在Java中實現共享內存提供了良好的方法,該緩衝區其實是一個磁盤文件的內存映象,兩者的變化會保持同步,即內存數據發生變化事後會當即反應到磁盤文件中,這樣會有效地保證共享內存的實現,將共享文件和磁盤文件簡歷聯繫的是文件通道類:FileChannel,該類的加入是JDK爲了統一外圍設備的訪問方法,而且增強了多線程對同一文件進行存取的安全性,這裏能夠使用它來創建共享內存用,它創建了共享內存和磁盤文件之間的一個通道。打開一個文件可以使用RandomAccessFile類的getChannel方法,該方法直接返回一個文件通道,該文件通道因爲對應的文件設爲隨機存取,一方面能夠進行讀寫兩種操做,另一個方面使用它不會破壞映象文件的內容。這裏,若是使用FileOutputStreamFileInputStream則不能理想地實現共享內存的要求,由於這兩個類同時實現自由讀寫很困難。

  下邊代碼段實現了上邊說起的共享內存功能

// 得到一個只讀的隨機存取文件對象

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

// 得到相應的文件通道

FileChannel fc = RAFile.getChannel();

// 取得文件的實際大小

int size = (int)fc.size();

// 得到共享內存緩衝區,該共享內存只讀 
MappedByteBuffer mapBuf = fc.map(FileChannel.MAP_RO,0,size);

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

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

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

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

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

mode = mapBuf.getInt(); 

  若是多個應用映象使用同一文件名的共享內存,則意味着這多個應用共享了同一內存數據,這些應用對於文件能夠具備同等存取權限,一個應用對數據的刷新會更新到多個應用中。爲了防止多個應用同時對共享內存進行寫操做,能夠在該共享內存的頭部信息加入寫操做標記,該共享文件的頭部基本信息至少有:

118 共享內存長度

119 共享內存目前的存取模式

  共享文件的頭部信息是私有信息,多個應用能夠對同一個共享內存執行寫操做,執行寫操做和結束寫操做的時候,能夠使用以下方法:

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又進行了第二次讀,而兩次讀取相隔時間比較短暫並且初衷沒有考慮到AB的讀取過程修改了資源,這種狀況下針對鎖模式就須要考慮到數據一致性問題。獨佔鎖的排他性在這裏的意思是該鎖只能被一個應用獲取,獲取過程只能由這個應用寫入數據到資源內部,除非它釋放該鎖,不然其餘拿不到鎖的應用是沒法對資源進行寫入操做的。

  按照上邊的思路去理解代碼裏面實現共享內存的過程就更加容易理解了。

  若是執行寫操做的應用異常停止,那麼映像文件的共享內存將再也不能執行寫操做。爲了在應用異常停止後,寫操做禁止標誌自動消除,必須讓運行的應用獲知退出的應用。在多線程應用中,能夠用同步方法得到這樣的效果,可是在多進程中,同步是不起做用的。方法能夠採用的多種技巧,這裏只是描述一可能的實現:採用文件鎖的方式。寫共享內存應用在得到對一個共享內存寫權限的時候,除了判斷頭部信息的寫權限標誌外,還要判斷一個臨時的鎖文件是否能夠獲得,若是能夠獲得,則即便頭部信息的寫權限標誌爲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):

  不經過強引用訪問的對象,即不是強可及對象,可是能夠經過軟引用訪問的對象就成爲軟可及對象,軟可及對象就須要使用類SoftReferencejava.lang.ref.SoftReference)。此種類型的引用主要用於內存比較敏感的高速緩存,並且此種引用仍是具備較強的引用功能,當內存不夠的時候GC會回收這類內存,所以若是內存充足的時候,這種引用一般不會被回收的。不只僅如此,這種引用對象在JVM裏面保證在拋出OutOfMemory異常以前,設置成爲null。通俗地講,這種類型的引用保證在JVM內存不足的時候所有被清除,可是有個關鍵在於:垃圾收集器在運行時是否釋放軟可及對象是不肯定的,並且使用垃圾回收算法並不能保證一次性尋找到全部的軟可及對象。當垃圾回收器每次運行的時候均可以隨意釋放不是強可及對象佔用的內存,若是垃圾回收器找到了軟可及對象事後,可能會進行如下操做:

120 SoftReference對象的referent域設置成爲null,從而使該對象再也不引用heap對象。

121 SoftReference引用過的內存堆上的對象一概被生命爲finalizable

122 當內存堆上的對象finalize()方法被運行並且該對象佔用的內存被釋放,SoftReference對象就會被添加到它的ReferenceQueue,前提條件是ReferenceQueue自己是存在的。

  既然Java裏面存在這樣的對象,那麼咱們在編寫代碼的時候如何建立這樣的對象呢?建立步驟以下:

  先建立一個對象,並使用普通引用方式【強引用】,而後再建立一個SoftReference來引用該對象,最後將普通引用設置爲null,經過這樣的方式,這個對象就僅僅保留了一個SoftReference引用,同時這種狀況咱們所建立的對象就是SoftReference對象。通常狀況下,咱們能夠使用該引用來完成Cache功能,就是前邊說的用於高速緩存,保證最大限度使用內存而不會引發內存泄漏的狀況。下邊的代碼段:

  public static void main(String args[])

  {

    //建立一個強可及對象

    A a = new A();

    //建立這個對象的軟引用SoftReference

    SoftReference sr = new SoftReference(a);

    //將強引用設置爲空,以遍垃圾回收器回收強引用

    a = null;

    //下次使用該對象的操做

    if( sr != null ){

      a = (A)sr.get();

    }else{

      //這種狀況就是因爲內存太低,已經將軟引用釋放了,所以須要從新裝載一次

      a = new A();

      sr = new SoftReference(a);

    }

  }

  軟引用技術使得Java系統能夠更好地管理內存,保持系統穩定,防止內存泄漏,避免系統崩潰,所以在處理一些內存佔用大並且生命週期長使用不頻繁的對象能夠使用該技術。

3)弱可及對象(weakly reachable):

  不是強可及對象一樣也不是軟可及對象,僅僅經過弱引用WeakReferencejava.lang.ref.WeakReference)訪問的對象,這種對象的用途在於規範化映射(canonicalized mapping,對於生存週期相對比較長並且從新建立的時候開銷少的對象,弱引用也比較有用,和軟引用對象不一樣的是,垃圾回收器若是碰到了弱可及對象,將釋放WeakReference對象的內存,可是垃圾回收器須要運行不少次纔可以找到弱可及對象。弱引用對象在使用的時候,能夠配合ReferenceQueue類使用,若是弱引用被回收,JVM就會把這個弱引用加入到相關的引用隊列中去。最簡單的弱引用方法如如下代碼:

  WeakReference weakWidget = new WeakReference(classA);

  在上邊代碼裏面,當咱們使用weakWidget.get()來獲取classA的時候,因爲弱引用自己是沒法阻止垃圾回收的,因此咱們也許會拿到一個null爲返回。【*:這裏提供一個小技巧,若是咱們但願取得某個對象的信息,可是又不影響該對象的垃圾回收過程,咱們就能夠使用WeakReference來記住該對象,通常咱們在開發調試器和優化器的時候使用這個是很好的一個手段。】

  若是上邊的代碼部分,咱們經過weakWidget.get()返回的是null就證實該對象已經被垃圾回收器回收了,而這種狀況下弱引用對象就失去了使用價值,GC就會定義爲須要進行清除工做。這種狀況下弱引用沒法引用任何對象,因此在JVM裏面就成爲了一個死引用,這就是爲何咱們有時候須要經過ReferenceQueue類來配合使用的緣由,使用了ReferenceQueue事後,就使得咱們更加容易監視該引用的對象,若是咱們經過一ReferenceQueue類來構造一個弱引用,當弱引用的對象已經被回收的時候,系統將自動使用對象引用隊列來代替對象引用,並且咱們能夠經過ReferenceQueue類的運行來決定是否真正要從垃圾回收器裏面將該死引用(Dead Reference)清除

  弱引用代碼段:

  //建立普通引用對象

  MyObject object = new MyObject();

  //建立一個引用隊列

  ReferenceQueue rq = new ReferenceQueue();

  //使用引用隊列建立MyObject的弱引用

  WeakReference wr = new WeakReference(object,rq);

  這裏提供兩個實在的場景來描述弱引用的相關用法:

[1]你想給對象附加一些信息,因而你用一個 Hashtable 把對象和附加信息關聯起來。你不停的把對象和附加信息放入 Hashtable 中,可是當對象用完的時候,你不得不把對象再從 Hashtable 中移除,不然它佔用的內存變不會釋放。萬一你忘記了,那麼沒有從 Hashtable 中移除的對象也能夠算做是內存泄漏。理想的情況應該是當對象用完時,Hashtable 中的對象會自動被垃圾收集器回收,否則你就是在作垃圾回收的工做。

[2]你想實現一個圖片緩存,由於加載圖片的開銷比較大。你將圖片對象的引用放入這個緩存,以便之後可以從新使用這個對象。可是你必須決定緩存中的哪些圖片再也不須要了,從而將引用從緩存中移除。無論你使用什麼管理緩存的算法,你實際上都在處理垃圾收集的工做,更簡單的辦法(除非你有特殊的需求,這也應該是最好的辦法)是讓垃圾收集器來處理,由它來決定回收哪一個對象。 

  當Java回收器遇到了弱引用的時候有可能會執行如下操做:

123 WeakReference對象的referent域設置成爲null,從而使該對象再也不引用heap對象。

124 WeakReference引用過的內存堆上的對象一概被生命爲finalizable

125 當內存堆上的對象finalize()方法被運行並且該對象佔用的內存被釋放,WeakReference對象就會被添加到它的ReferenceQueue,前提條件是ReferenceQueue自己是存在的。

4)清除:

  當引用對象的referent域設置爲null,而且引用類在內存堆中引用的對象聲明爲可結束的時候,該對象就能夠清除,清除不作過多的講述

5)虛可及對象(phantomly reachable):

  不是強可及對象,也不是軟可及對象,一樣不是弱可及對象,之因此把虛可及對象放到最後來說,主要也是由於它的特殊性,有時候咱們又稱之爲「幽靈對象」,已經結束的,能夠經過虛引用來訪問該對象。咱們使用類PhantomReferencejava.lang.ref.PhantomReference)來訪問,這個類只能用於跟蹤被引用對象進行的收集,一樣的,能夠用於執行per-mortern清除操做。PhantomReference必須與ReferenceQueue類一塊兒使用。須要使用ReferenceQueue是由於它可以充當通知機制,當垃圾收集器肯定了某個對象是虛可及對象的時候,PhantomReference對象就被放在了它的ReferenceQueue上,這就是一個通知,代表PhantomReference引用的對象已經結束,能夠收集了,通常狀況下咱們恰好在對象內存在回收以前採起該行爲。這種引用不一樣於弱引用和軟引用,這種方式經過get()獲取到的對象老是返回null,僅僅當這些對象在ReferenceQueue隊列裏面的時候,咱們能夠知道它所引用的哪些對對象是死引用(Dead Reference)。而這種引用和弱引用的區別在於:

弱引用(WeakReference是在對象不可達的時候儘快進入ReferenceQueue隊列的,在finalization方法執行和垃圾回收以前是確實會發生的,理論上這類對象是不正確的對象,可是WeakReference對象能夠繼續保持Dead狀態,

虛引用(PhantomReference是在對象確實已經從物理內存中移除事後才進入的ReferenceQueue隊列,並且get()方法會一直返回null

  當垃圾回收器遇到了虛引用的時候將有可能執行如下操做:

126 PhantomReference引用過的heap對象聲明爲finalizable

127 虛引用在堆對象釋放以前就添加到了它的ReferenceQueue裏面,這種狀況使得咱們能夠在堆對象被回收以前採起操做*再次提醒,PhantomReference對象必須通過關聯的ReferenceQueue來建立,就是說必須ReferenceQueue類配合操做】

  看似沒有用處的虛引用,有什麼用途呢?

128 首先,咱們能夠經過虛引用知道對象究竟何時真正從內存裏面移除的,並且這也是惟一的途徑。

129 虛引用避過了finalize()方法,由於對於此方法的執行而言,虛引用真正引用到的對象是異常對象,若在該方法內要使用對象只能重建。通常狀況垃圾回收器會輪詢兩次,一次標記爲finalization,第二次進行真實的回收,而每每標記工做不能實時進行,或者垃圾回收其會等待一個對象去標記finalization。這種狀況頗有可能引發MemoryOut,而使用虛引用這種狀況就會徹底避免。由於虛引用在引用對象的過程不會去使得這個對象由Dead復活,並且這種對象是能夠在回收週期進行回收的。

  在JVM內部,虛引用比起使用finalize()方法更加安全一點並且更加有效。而finaliaze()方法回收在虛擬機裏面實現起來相對簡單,並且也能夠處理大部分工做,因此咱們仍然使用這種方式來進行對象回收的掃尾操做,可是有了虛引用事後咱們能夠選擇是否手動操做該對象使得程序更加高效完美。

iv.防止內存泄漏[來自IBM開發中心]

1)使用軟引用阻止泄漏:

[1]Java語言中有一種形式的內存泄漏稱爲對象遊離(Object Loitering):

——[$]對象遊離——

// 注意,這段代碼屬於概念說明代碼,實際應用中不要模仿

public class LeakyChecksum{

    private byte[] byteArray;

    public synchronized int getFileCheckSum(String filename)

    {

        int len = getFileSize(filename);

        if( byteArray == null || byteArray.length < len )

            byteArray = new byte[len];

        readFileContents(filename,byteArray);

        // 計算該文件的值而後返回該對象

    }

}

  上邊的代碼是類LeakyChecksum用來講明對象遊離的概念,裏面有一個getFileChecksum()方法用來計算文件內容校驗和getFileCheckSum方法將文件內容讀取到緩衝區中計算校驗和,更加直觀的實現就是簡單地將緩衝區做爲getFileChecksum中的本地變量分配,可是上邊這個版本比這種版本更加「聰明」,不是將緩衝區緩衝在實例中字段中減小內存churn。該「優化」一般不帶來預期的好處,對象分配比不少人指望的更加便宜。(還要注意,將緩衝區從本地變量提高到實例變量,使得類若不帶有附加的同步,就再也不是線程安全的了。直觀的實現不須要將 getFileChecksum() 聲明爲 synchronized,而且會在同時調用時提供更好的可伸縮性。)

  這個類存在不少的問題,可是咱們着重來看內存泄漏。緩存緩衝區的決定極可能是根據這樣的假設得出的,即該類將在一個程序中被調用許屢次,所以它應該更加有效,以重用緩衝區而不是從新分配它。可是結果是,緩衝區永遠不會被釋放,由於它對程序來講老是可及的(除非LeakyChecksum對象被垃圾收集了)。更壞的是,它能夠增加,卻不能夠縮小,因此 LeakyChecksum 將永久保持一個與所處理的最大文件同樣大小的緩衝區。退一萬步說,這也會給垃圾收集器帶來壓力,而且要求更頻繁的收集;爲計算將來的校驗和而保持一個大型緩衝區並非可用內存的最有效利用。LeakyChecksum 中問題的緣由是,緩衝區對於 getFileChecksum() 操做來講邏輯上是本地的,可是它的生命週期已經被人爲延長了,由於將它提高到了實例字段。所以,該類必須本身管理緩衝區的生命週期,而不是讓 JVM 來管理。

  這裏能夠提供一種策略就是使用Java裏面的軟引用:

  弱引用如何能夠給應用程序提供當對象被程序使用時另外一種到達該對象的方法,可是不會延長對象的生命週期。Reference 的另外一個子類——軟引用——可知足一個不一樣卻相關的目的。其中弱引用容許應用程序建立不妨礙垃圾收集的引用,軟引用容許應用程序經過將一些對象指定爲 「expendable」 而利用垃圾收集器的幫助。儘管垃圾收集器在找出哪些內存在由應用程序使用哪些沒在使用方面作得很好,可是肯定可用內存的最適當使用仍是取決於應用程序。若是應用程序作出了很差的決定,使得對象被保持,那麼性能會受到影響,由於垃圾收集器必須更加辛勤地工做,以防止應用程序消耗掉全部內存。高速緩存是一種常見的性能優化,容許應用程序重用之前的計算結果,而不是從新進行計算。高速緩存是 CPU 利用和內存使用之間的一種折衷,這種折衷理想的平衡狀態取決於有多少內存可用。若高速緩存太少,則所要求的性能優點沒法達到;若太多,則性能會受到影響,由於太多的內存被用於高速緩存上,致使其餘用途沒有足夠的可用內存。由於垃圾收集器比應用程序更適合決定內存需求,因此應該利用垃圾收集器在作這些決定方面的幫助,這就是件引用所要作的。若是一個對象唯一剩下的引用是弱引用或軟引用,那麼該對象是軟可及的(softly reachable。垃圾收集器並不像其收集弱可及的對象同樣儘可能地收集軟可及的對象,相反,它只在真正 「須要」 內存時才收集軟可及的對象。軟引用對於垃圾收集器來講是這樣一種方式,即 「只要內存不太緊張,我就會保留該對象。可是若是內存變得真正緊張了,我就會去收集並處理這個對象。」 垃圾收集器在能夠拋出OutOfMemoryError 以前須要清除全部的軟引用。經過使用一個軟引用來管理高速緩存的緩衝區,能夠解決 LeakyChecksum中的問題,如上邊代碼所示。如今,只要不是特別須要內存,緩衝區就會被保留,可是在須要時,也可被垃圾收集器回收:

——[$]使用軟引用修復上邊代碼段——

public class CachingChecksum

{

    private SoftReference<byte[]> bufferRef;

    public synchronized int getFileChecksum(String filename)

    {

        int len = getFileSize(filename);

        byte[] byteArray = bufferRef.get();

        if( byteArray == null || byteArray.length < len )

        {

            byteArray = new byte[len];

            bufferRef.set(byteArray);

        }

        readFileContents(filename,byteArray);

    }

}

  一種廉價緩存:

  CachingChecksum使用一個軟引用來緩存單個對象,並讓 JVM 處理從緩存中取走對象時的細節。相似地,軟引用也常常用於 GUI 應用程序中,用於緩存位圖圖形。是否可以使用軟引用的關鍵在於,應用程序是否可從大量緩存的數據恢復。若是須要緩存不止一個對象,您能夠使用一個 Map,可是能夠選擇如何使用軟引用。您能夠將緩存做爲 Map<K, SoftReference<V>> SoftReference<Map<K,V>> 管理。後一種選項一般更好一些,由於它給垃圾收集器帶來的工做更少,而且容許在特別須要內存時以較少的工做回收整個緩存。弱引用有時會錯誤地用於取代軟引用,用於構建緩存,可是這會致使差的緩存性能。在實踐中,弱引用將在對象變得弱可及以後被很快地清除掉——一般是在緩存的對象再次用到以前——由於小的垃圾收集運行得很頻繁。對於在性能上很是依賴高速緩存的應用程序來講,軟引用是一個無論用的手段,它確實不能取代可以提供靈活終止期複製事務型高速緩存的複雜的高速緩存框架。可是做爲一種 「廉價(cheap and dirty」 的高速緩存機制,它對於下降價格是頗有吸引力的。正如弱引用同樣,軟引用也可建立爲具備一個相關的引用隊列,引用在被垃圾收集器清除時進入隊列。引用隊列對於軟引用來講,沒有對弱引用那麼有用,可是它們能夠用於發出管理警報,說明應用程序開始缺乏內存

2)垃圾回收對引用的處理:

  弱引用和軟引用都擴展了抽象的 Reference 虛引用(phantom references),引用對象被垃圾收集器特殊地看待。垃圾收集器在跟蹤堆期間遇到一個 Reference 時,不會標記或跟蹤該引用對象,而是在已知活躍的 Reference 對象的隊列上放置一個 Reference。在跟蹤以後,垃圾收集器就識別軟可及的對象——這些對象上除了軟引用外,沒有任何強引用。垃圾收集器而後根據當前收集所回收的內存總量和其餘策略考慮因素,判斷軟引用此時是否須要被清除。將被清除的軟引用若是具備相應的引用隊列,就會進入隊列。其他的軟可及對象(沒有清除的對象)而後被看做一個根集(root set,堆跟蹤繼續使用這些新的根,以便經過活躍的軟引用而可及的對象可以被標記。處理軟引用以後,弱可及對象的集合被識別 —— 這樣的對象上不存在強引用或軟引用。這些對象被清除和加入隊列。全部 Reference 類型在加入隊列以前被清除,因此處理過後檢查(post-mortem)清除的線程永遠不會具備 referent 對象的訪問權,而只具備Reference 對象的訪問權。所以,當 References 與引用隊列一塊兒使用時,一般須要細分適當的引用類型,並將它直接用於您的設計中(與 WeakHashMap 同樣,它的 Map.Entry 擴展了 WeakReference)或者存儲對須要清除的實體的引用。

3)使用弱引用堵住內存泄漏:

[1]全局Map形成的內存泄漏:

  無心識對象保留最多見的緣由是使用 Map 將元數據與臨時對象(transient object相關聯。假定一個對象具備中等生命週期,比分配它的那個方法調用的生命週期長,可是比應用程序的生命週期短,如客戶機的套接字鏈接。須要將一些元數據與這個套接字關聯,如生成鏈接的用戶的標識。在建立 Socket 時是不知道這些信息的,而且不能將數據添加到 Socket 對象上,由於不能控制 Socket 類或者它的子類。這時,典型的方法就是在一個全局 Map 中存儲這些信息:

public class SocketManager{

    private Map<Socket,User> m = new HashMap<Socket,User>();

    public void setUser(Socket s,User u)

    {

        m.put(s,u);

    }

    public User getUser(Socket s){

        return m.get(s);

    }

    public void removeUser(Socket s){

        m.remove(s);

    }

}

 

SocketManager socketManager;

//...

socketManager.setUser(socket,user);

  這種方法的問題是元數據的生命週期須要與套接字的生命週期掛鉤,可是除非準確地知道何時程序再也不須要這個套接字,並記住從 Map 中刪除相應的映射,不然,Socket 和 User 對象將會永遠留在 Map 中,遠遠超過響應了請求和關閉套接字的時間。這會阻止 Socket User 對象被垃圾收集,即便應用程序不會再使用它們。這些對象留下來不受控制,很容易形成程序在長時間運行後內存爆滿。除了最簡單的狀況,在幾乎全部狀況下找出何時 Socket 再也不被程序使用是一件很煩人和容易出錯的任務,須要人工對內存進行管理。

[2]弱引用內存泄漏代碼:

  程序有內存泄漏的第一個跡象一般是它拋出一個 OutOfMemoryError,或者由於頻繁的垃圾收集而表現出糟糕的性能。幸運的是,垃圾收集能夠提供可以用來診斷內存泄漏的大量信息。若是以 -verbose:gc 或者 -Xloggc 選項調用 JVM,那麼每次 GC 運行時在控制檯上或者日誌文件中會打印出一個診斷信息,包括它所花費的時間、當前堆使用狀況以及恢復了多少內存。記錄 GC 使用狀況並不具備干擾性,所以若是須要分析內存問題或者調優垃圾收集器,在生產環境中默認啓用 GC 日誌是值得的。有工具能夠利用 GC 日誌輸出並以圖形方式將它顯示出來,JTune 就是這樣的一種工具。觀察 GC 以後堆大小的圖,能夠看到程序內存使用的趨勢。對於大多數程序來講,能夠將內存使用分爲兩部分:baseline 使用和 current load 使用。對於服務器應用程序,baseline 使用就是應用程序在沒有任何負荷、可是已經準備好接受請求時的內存使用,current load 使用是在處理請求過程當中使用的、可是在請求處理完成後會釋放的內存。只要負荷大致上是恆定的,應用程序一般會很快達到一個穩定的內存使用水平。若是在應用程序已經完成了其初始化而且負荷沒有增長的狀況下,內存使用持續增長,那麼程序就可能在處理前面的請求時保留了生成的對象。

public class MapLeaker{

    public ExecuteService exec = Executors.newFixedThreadPool(5);

    public Map<Task,TaskStatus> taskStatus

        = Collections.synchronizedMap(new HashMap<Task,TaskStatus>());

    private Random random = new Random();

    private enum TaskStatus { NOT_STARTEDSTARTEDFINISHED };

    private class Task implements Runnable{

        private int[] numbers = new int[random.nextInt(200)];

        public void run()

        {

            int[] temp = new int[random.nextInt(10000)];

            taskStatus.put(this,TaskStatus.STARTED);

            doSomework();

            taskStatus.put(this,TaskStatus.FINISHED);

        }

    }

    public Task newTask()

    {

        Task t = new Task();

        taskStatus.put(t,TaskStatus.NOT_STARTED);

        exec.execute(t);

        return t;

    }

}

[3]使用弱引用堵住內存泄漏:

  SocketManager 的問題是 Socket-User 映射的生命週期應當與 Socket 的生命週期相匹配,可是語言沒有提供任何容易的方法實施這項規則。這使得程序不得不使用人工內存管理的老技術。幸運的是,從 JDK 1.2 開始,垃圾收集器提供了一種聲明這種對象生命週期依賴性的方法,這樣垃圾收集器就能夠幫助咱們防止這種內存泄漏——利用弱引用。弱引用是對一個對象(稱爲 referent的引用的持有者。使用弱引用後,能夠維持對 referent 的引用,而不會阻止它被垃圾收集。當垃圾收集器跟蹤堆的時候,若是對一個對象的引用只有弱引用,那麼這個 referent 就會成爲垃圾收集的候選對象,就像沒有任何剩餘的引用同樣,並且全部剩餘的弱引用都被清除。(只有弱引用的對象稱爲弱可及(weakly reachable))WeakReference 的 referent 是在構造時設置的,在沒有被清除以前,能夠用 get() 獲取它的值。若是弱引用被清除了(無論是 referent 已經被垃圾收集了,仍是有人調用了 WeakReference.clear()get() 會返回 null。相應地,在使用其結果以前,應當老是檢查get() 是否返回一個非 null 值,由於 referent 最終老是會被垃圾收集的。用一個普通的(強)引用拷貝一個對象引用時,限制 referent 的生命週期至少與被拷貝的引用的生命週期同樣長。若是不當心,那麼它可能就與程序的生命週期同樣——若是將一個對象放入一個全局集合中的話。另外一方面,在建立對一個對象的弱引用時,徹底沒有擴展 referent 的生命週期,只是在對象仍然存活的時候,保持另外一種到達它的方法。弱引用對於構造弱集合最有用,如那些在應用程序的其他部分使用對象期間存儲關於這些對象的元數據的集合——這就是 SocketManager 類所要作的工做。由於這是弱引用最多見的用法,WeakHashMap 也被添加到 JDK 1.2 的類庫中,它對鍵(而不是對值)使用弱引用。若是在一個普通 HashMap 中用一個對象做爲鍵,那麼這個對象在映射從 Map 中刪除以前不能被回收,WeakHashMap 使您能夠用一個對象做爲 Map 鍵,同時不會阻止這個對象被垃圾收集。下邊的代碼給出了 WeakHashMap 的 get() 方法的一種可能實現,它展現了弱引用的使用:

public class WeakHashMap<K,V> implements Map<K,V>

{

    private static class Entry<K,V> extends WeakReference<K> implements Map.Entry<K,V>

    {

        private V value;

        private final int hash;

        private Entry<K,V> next;

        // ...

    }

 

    public V get(Object key)

    {

        int hash = getHash(key);

        Entry<K,V> e = getChain(hash);

        while(e != null)

        {

            k eKey = e.get();

            if( e.hash == hash && (key == eKey || key.equals(eKey)))

                return e.value;

            e = e.next;

        }

        return null;

    }

}

調用 WeakReference.get() 時,它返回一個對 referent 的強引用(若是它仍然存活的話),所以不須要擔憂映射在 while 循環體中消失,由於強引用會防止它被垃圾收集。WeakHashMap 的實現展現了弱引用的一種常見用法——一些內部對象擴展 WeakReference。其緣由在下面一節討論引用隊列時會獲得解釋。在向 WeakHashMap 中添加映射時,請記住映射可能會在之後「脫離」,由於鍵被垃圾收集了。在這種狀況下,get() 返回 null,這使得測試 get() 的返回值是否爲 null 變得比平時更重要了。

[4]使用WeakHashMap堵住泄漏

  在 SocketManager 中防止泄漏很容易,只要用 WeakHashMap 代替 HashMap 就好了,以下邊代碼所示。(若是 SocketManager 須要線程安全,那麼能夠用 Collections.synchronizedMap() 包裝 WeakHashMap。當映射的生命週期必須與鍵的生命週期聯繫在一塊兒時,能夠使用這種方法。不過,應當當心不濫用這種技術,大多數時候仍是應當使用普通的 HashMap 做爲 Map 的實現。

public class SocketManager{

    private Map<Socket,User> m = new WeakHashMap<Socket,User>();

    public void setUser(Socket s, User s)

    {

        m.put(s,u);

    }

    public User getUser(Socket s)

    {

        return m.get(s);

    }

}

引用隊列:

  WeakHashMap 用弱引用承載映射鍵,這使得應用程序再也不使用鍵對象時它們能夠被垃圾收集,get() 實現能夠根據 WeakReference.get() 是否返回 null 來區分死的映射和活的映射。可是這只是防止 Map 的內存消耗在應用程序的生命週期中不斷增長所須要作的工做的一半,還須要作一些工做以便在鍵對象被收集後從 Map 中刪除死項。不然,Map 會充滿對應於死鍵的項。雖然這對於應用程序是不可見的,可是它仍然會形成應用程序耗盡內存,由於即便鍵被收集了,Map.Entry 和值對象也不會被收集。能夠經過週期性地掃描 Map,對每個弱引用調用 get(),並在 get() 返回 null 時刪除那個映射而消除死映射。可是若是 Map 有許多活的項,那麼這種方法的效率很低。若是有一種方法能夠在弱引用的 referent 被垃圾收集時發出通知就行了,這就是引用隊列的做用。引用隊列是垃圾收集器嚮應用程序返回關於對象生命週期的信息的主要方法。弱引用有兩個構造函數:一個只取 referent 做爲參數,另外一個還取引用隊列做爲參數。若是用關聯的引用隊列建立弱引用,在 referent 成爲 GC 候選對象時,這個引用對象(不是referent就在引用清除後加入 到引用隊列中。以後,應用程序從引用隊列提取引用並瞭解到它的 referent 已被收集,所以能夠進行相應的清理活動,如去掉已不在弱集合中的對象的項。(引用隊列提供了與 BlockingQueue 一樣的出列模式 ——polledtimed 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復活,並且這種對象是能夠在回收週期進行回收的。

 

綠色通道: 好文要頂 關注我 收藏該文與我聯繫 

zengdan
關注 - 0
粉絲 - 2

+加關注

0

0

(請您對文章作出評價)

« 上一篇:JVM的運行原理以及JDK 7增長的新特性(二)
» 下一篇:JDK內置工具之一——JMap(java memory map)

posted @ 2013-07-12 20:52 zengdan 閱讀(38) 評論(0) 編輯 收藏

刷新評論刷新頁面返回頂部

發表評論

暱稱:

評論內容:

     

 

 註銷 訂閱評論

[使用Ctrl+Enter鍵快速提交]

博客園首頁博問新聞閃存程序員招聘知識庫

 

 

最新IT新聞:
· 初創公司的創始人該拿多少薪水?
· 歐洲出版巨頭認可懼怕Google
· 硅谷年輕人已然不愛大公司
· 從如今開始,Square 再也不是華爾街的寵兒
· 三星工廠起火致使智能電視報錯
» 更多新聞...

最新知識庫文章:

· MVC vs. MVP vs. MVVM
· 不是技術牛人,如何拿到國內IT巨頭的Offer
· 深刻淺出交換類排序算法
· Code Review談如何作技術
· Web開發常見的幾個漏洞解決方法

» 更多知識庫文章...

公告

暱稱:zengdan
園齡:211個月
粉絲:2
關注:0

關注成功

<

20144

>

 

30

31

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

1

2

3

4

5

6

7

8

9

10

相關文章
相關標籤/搜索