共有五個,線程共享的有 堆(Heap)、方法去(Method Area), 線程私有的有 虛擬機棧(VM Stack)、本地方法棧(Native Method Stack)和程序計數器。java
以Java堆爲例,探訪對象的建立、內存佈局和定位。算法
Java程序須要經過棧上的reference數據來操做堆上的 具體對象。 因爲reference類型在Java虛擬機規範中只 規定了一個指向對象的引用,並無定義這個引用應該 經過何種方式去定位、訪問堆中的對象的具體 位置, 因此對象訪問方式也是取決於虛擬機實現而定 的。 目前 主流的訪問方式有使用句柄和直接指針兩種。數組
垃圾收集(Garbage Collection,GC),最開始誕生於1960的Lisp.
程序計數器、虛擬機棧、本地方法棧隨線程而生,隨線程而滅,這三個區域不考慮垃圾回收。Java堆和方法區是主要進行垃圾回收的區域。安全
舉個 簡單 的 例子, 請看 代碼 清單 3- 1 中的 testGC() 方法: 對象 objA 和 objB 都有 字段 instance, 賦值 令 objA. instance= objB 及 objB. instance= objA, 除此以外, 這 兩個 對象 再無 任何 引用, 實際上 這 兩個 對象 已經 不可能 再被 訪問, 可是 它們 由於 互相 引用 着 對方, 致使 它們 的 引用 計數 都不 爲 0, 因而 引用 計數 算法 沒法 通知 GC 收集 器 回收 它們.數據結構
可達性分析算法(Reachability Analysis)這個算法的基本思路就是經過一系列的稱爲"GC Roots"的對象做爲起始 點, 從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈( Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用 圖論的話來講,就是從GCRoots到這個對象不可達)時,則證實此對象是不可用的。多線程
再談引用(引用的定義及分類) 在 JDK 1. 2 之前,Java中的引用的定義很傳統:若是 reference 類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用。
在JDK 1. 2以後, Java對引用的概念進行了擴充,將引用分爲強引用( Strong Reference)、軟引用( Soft Reference)、 弱引用( Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。併發
強引用:只要強引用還在永遠不會被垃圾回收,定義方式一般爲 A a = new A().框架
軟引用:描述一些還有用但非必要的屬性。在系統將要發生內存溢出 異常以前,將會把這些對象列進回收範圍之中進行第二次回收。若是 此次回收尚未足夠的內存, 纔會拋出內存溢出異常。jvm
軟引用:它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。當垃圾收集器工做時,不管當前內存是否足夠, 都會回收掉只被弱 引用關聯的對象。模塊化
虛引用:也成爲幽靈引用或者幻影引用,最弱。爲一個對象設置虛引用關聯的惟一目的就是能在這個對象 被收集器回收時收到一個系統通知。
要真正宣告一個對象死亡,至少要經歷兩次標記過程:若是對象在進行 可達性分析後發現沒有與GCRoots相鏈接的引用鏈,那它將會被第一次標記而且進行一次篩選, 篩選的條件是此對象是否有必要執行 finalize() 方法。 當對象沒覆蓋finalize() 方法, 或者finalize() 方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲「 沒有必要 執行」。 若是這個對象被斷定爲有必要執行 finalize()方法, 那麼 這個對象將會放置在一個叫作F-Queue的隊列之中,並在稍後由一個由虛擬機自動創建的、低優先級的Finalizer線程去執行它。
finalize()方法是對象逃脫死亡命運的最後一次機會。
Java 虛擬機規範中確實說過能夠不要求虛擬機在方法區實現垃圾收集, 並且在方法區中進行垃圾收集的「 性價比」通常比較低:在堆中,尤爲是 在新生代中, 常規應用進行一次垃圾收集通常能夠回收70%~95%的空間,而永久代的垃圾收集效率遠低於此。
永久代的垃圾收集主要回收兩部份內容:廢棄常量和無用的類。 回收 廢棄常量與回收Java堆中的對象很是相似。
如何判斷一個類是否無用:
最基礎的收集算法是「 標記- 清除」( Mark- Sweep)算法,如同它的 名字同樣, 算法分爲「 標記」 和「 清除」 兩個階段: 首先標記出 全部 須要回收的對象, 在標記完成後統一回收全部被標記的對象。
不足:它的主要不足有兩個: 一個是效率問題, 標記和清除兩個過程 的效率都不高;另外一個是空間 問題, 標記清除以後會產生大量 不連續 的內存碎片, 空間碎片太多可能會致使之後在程序運行過程當中須要 分配較大對象時, 沒法找到足夠的連續內存而不得不提早觸發另外一次 垃圾收集。
爲了解決效率問題, 一種稱爲「 複製」( Copying) 的收集算法出現 了, 它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。只是這種算法的代價是內存縮小爲了原來的一半,未免過高了一點。
如今的商業虛擬機都採用這種收集算法來回收新生代, IBM公司的專門研究代表,新生代中的對象98% 是「 朝 生 夕 死」 的, 因此並不須要按照 1: 1 的比例來劃分 內存 空間,而是將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。 當回收時,將Eden 和Survivor 中 還存活着的對象 一次性地複製到另一塊Survivor空間上,最後清理掉Eden和剛纔用過的 Survivor 空間。HotSpot虛擬機默認的Eden和Survivor的大小爲8:1
缺點:在對象存活率比較高時就要進行較多的複製操做,效率會變低。
(Mark-Compact)老年代通常採起該算法。
算法,標記過程仍然與「標記-清除」算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動, 而後直接 清理掉端邊界之外的內存。
當前商業虛擬機的垃圾收集都採用「 分 代 收集」( Generational Collection)算法,這種算法並無什麼新的思想,只是根據對象存活週期的不一樣將內存劃分爲幾塊。通常是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用「 標記— 清理」 或者「 標記— 整理」 算法來進行回收。
在HotSpot的實現中, 是使用一組稱OopMap的數據結構 來達到這個目的的, 在類加載完成的時候, HotSpot 就把對象內什麼偏移量上是什麼類型的數據計算出來, 在JIT編譯過程當中,也會在特定的位置記錄下棧和 寄存器 中哪些位置是引用。這樣, GC在掃描時就能夠直接得知 這些信息了。
在 OopMap的協助下,HotSpot能夠快速且準確地完成 GC Roots 枚舉,但一個很現實的問題隨之而來:可能致使 引用關係變化,或者說OopMap內容變化的指令很是多, 若是爲每一條指令都生成對應的OopMap,那將會須要大量的額外空間, 這樣GC的空間成本將會變得很高。
實際上,HotSpot也的確沒有爲每條指令都生成 OopMap, 前面已經 提到,只是 在「 特定 的 位置」 記錄了 這些信息, 這些位置稱爲安全點( Safepoint),即 程序執行時並不是在全部地方都能停頓下來開始 GC,只有 在到達安全點時才能暫停。
Safepoint機制保證了程序執行時, 在不太長的時間內 就會遇到可進入 GC的Safepoint。
安全區域(Safe-Region)是指在一段代碼片斷之中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的。咱們也能夠把Safe-Region看作是被擴展了的Safepoint。
這裏討論的收集器基於JDK1.7Update14以後的HotSpot虛擬機。
Serial 收集器是最基本、發展歷史最悠久的收集器,曾經是虛擬機新生代收集的惟一選擇。該收集器爲單線程收集器,它在工做時會暫停其它線程。「Stop the world」指的就是這種狀況。 優勢:簡單而高效( 與其餘收集器的單線程 比), 對於限定單個CPU的環境來講,Serial收集器因爲沒有線程交互的開銷,專心作垃圾收集天然能夠得到最高的單線程收集。
ParNew收集器其實就是Serial收集器的多線程版本, 除了使用多條線程進行垃圾收集以外,其他行爲包括 Serial收集器可用的全部控制參數、收集算法、Stop The World、對象分配規則、回收策略等都與Serial收集器徹底同樣, 在實現上,這兩種收集器也共用了至關多的代碼。
它是許多運行在Server模式下的虛擬機中首選的新生代 收集器,其中有一個與性能無關但很重要的緣由,緣由 是,除了Serial收集器外,目前只有它能與CMS收集器配合工做。
在 JDK 1. 5 時期, HotSpot 推 出了 一 款 在 強 交互 應用 中 幾乎 可 認爲 有 劃時代 意義 的 垃圾 收集 器—— CMS 收集 器( Concurrent Mark Sweep, 本節 稍後 將 詳細 介紹 這 款 收集 器), 這 款 收集 器 是 HotSpot 虛擬 機中 第一 款 真正 意義上 的 併發( Concurrent) 收集 器, 它 第一次 實現 了 讓 垃圾 收集 線程 與 用戶 線程( 基本上) 同時 工做, 用 前面 那個 例子 的 話來 說, 就是 作 到了 在 你的 媽媽 打掃 房間 的 時候 你 還能 一邊 往 地上 扔 紙屑。 不幸 的 是, CMS 做爲 老 年代 的 收集 器, 卻 沒法 與 JDK 1. 4. 0 中 已經 存在 的 新生代 收集 器 Parallel Scavenge 配合 工做[ 1], 因此 在 JDK 1. 5 中 使用 CMS 來 收集 老 年代 的 時候, 新生代 只能 選擇 ParNew 或者 Serial 收集 器 中的 一個。
Parallel Scavenge收集器是一個新生代收集器,它也是使用複製算法的收集器,又是並行的多線程收集器。
Parallel Scavenge收集器的特色是它的關注點與其餘收集器不一樣, CMS等收集器的關注點 是儘量地縮短垃圾收集時用戶線程的停頓時間,而 Parallel Scavenge 收集器的目標則是達到一個可控制的吞吐量(Throughput)。
Serial Old 是 Serial 收集 器 的 老年 代版本,它一樣是一個單線程收集器, 使用「 標記- 整理」 算法。 這個收集器 的 主要意義也是在於給Client 模式下的 虛擬機使用。若是在Serve 模式下,那麼 它主要還有兩大用途: 一種 用途 是在 JDK 1. 5 以及以前 的 版本中與 Parallel Scavenge收集器搭配使用[ 1], 另外一種 用途就是做爲CMS收集器的後備預案,在 併發收集發生Concurrent Mode Failure時 使用。
Parallel Old是Parallel Scavenge收集器 的老年代版本,使用多線程和「 標記- 整理」 算法。這個收集器是在JDK 1.6中 纔開始提供的。
CMS( Concurrent Mark Sweep)收集器 是 一種以獲取最短回收停頓時間爲目標的 收集器。 目前很大一部分的Java 應用 集中在互聯網站或者 B/ S 系統的服務 端上,這類應用尤爲重視服務的響應速度, 但願系統停頓時間最短, 以給用戶帶來 較好的體驗。
從名字(包含" Mark Sweep") 上就能夠 看出, CMS 收集器是基於「 標記— 清除」 算法實現的,它的運做過程相對於前面 幾種收集器來講更復雜一些, 整個過程 分爲 4 個 步驟, 包括:
CMS是一款優秀的收集器,它的主要優勢 在 名字上已經體現出來了: 併發收集、低 停頓,Sun公司的一些官方文檔中也稱之爲 併發低停頓收集器( Concurrent Low Pause Collector)。
缺點:
G1( Garbage- First 收集器是當今收集 器技術發展的最前沿成果之一,早在JDK 1. 7 剛剛確立項目目標, Sun 公司給出 的 JDK 1. 7 RoadMap 裏面,它就被視爲 JDK 1. 7 中HotSpot 虛擬機的一個重要進化 特徵。
G1 是一 款 面向 服務 端 應用 的 垃圾 收集 器。 HotSpot 開發 團隊 賦予 它的 使命 是( 在 比較 長期 的) 將來 能夠 替換 掉 JDK 1. 5 中 發佈 的 CMS 收集 器。 與其 他 GC 收集 器 相比, G1 具有 以下 特色。
虛擬機提供了- XX:+ PrintGCDetails 這個 收集器日誌參數, 告訴虛擬機在發生垃圾 收集行爲時打印內存回收日誌, 而且在 進程退出的時候輸出當前的內存各區域 分配狀況。 在實際應用中, 內存回收 日誌通常是打印到文件後經過日誌工具 進行分析, 不過本實驗的日誌並不 多,直接閱讀就能看得很清楚。
新生代GC( Minor GC): 指發生在 新生代的垃圾收集動做, 由於Java對象 大多都具有朝生夕滅的 特性, 因此 Minor GC 很是 頻繁,通常回收速度也比較快。
老年代GC( Major GC/ Full GC): 指發生在老年代的 GC,出現了Major GC, 常常會伴隨至少一次的Minor GC( 但非 絕對的,在 Parallel Scavenge 收集器 的收集策略裏就有直接進行 Major GC 的 策略 選擇 過程)。** Major GC的速度 通常 會 比 Minor GC慢10倍以上。**
所謂的大對象是指, 須要大量連續內存 空間的 Java 對象, 最典型的大對象 就是 那種很長的字符串以及數組。 大對象對 虛擬機的內存分配來講就是一個壞消息,常常出現大對象容易致使內存還有很多空 間時就提早觸發垃圾收集以獲取足夠的 連續 空間 來「 安置」 它們。
若是對象在Eden 出生並通過第一次 Minor GC 後仍然存活, 而且能被 Survivor 容納的 話,將被移動到 Survivor 空間中, 而且 對象年齡設爲1。 對象在 Survivor 區 中 每「 熬過」 一次 Minor GC,年齡就增長 1 歲, 當它的年齡增長到必定程度( 默認 爲 15 歲), 就將會被晉升到老年代 中。 對象晉升老年代的年齡閾值, 能夠經過參數- XX: MaxTenuringThreshold 設置。
若是在 Survivor 空間中相同年齡全部對象大小的總和大於Survivor 空間的一半,年齡 大於或等於該年齡的對象就能夠直接進入老年代, 無須等到 MaxTenuringThreshold 中 要求的年齡。
在發生Minor GC以前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代全部對象總空間,若是這個條件成立,那麼MinorGC能夠確保是安全的。若是不成立,則虛擬機會查看HandlePromotionFailure設置值是否容許擔保失敗。若是容許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,若是大於,將嘗試着進行一次Minor GC,儘管此次 Minor GC是有風險的; 若是小於,或者HandlePromotionFailure 設置不容許冒險, 那這時也要改成進行一次Full GC。
給一個系統定位問題的時候,知識、 經驗是關鍵基礎,數據是依據,工具 是運用知識處理數據的手段。 這裏說的數據包括: 運行日誌、 異常堆棧、 GC 日誌、 線程 快照( threaddump/ javacore 文件)、 堆 轉儲快照( heapdump/ hprof 文件) 等。 常用適當的虛擬機監控和 分析的工具能夠加快咱們分析數據、 定位解決問題的速度。