《深入理解JVM虛擬機》垃圾回收部分 讀書筆記

自動內存管理機制

Java內存區域與內存溢出異常

運行時數據區域

程序計數器

“程序計數器(Program Counter Register)是一塊較小的內存空間,它可以看作是當前線程所執行的字節碼的行號指示器”
“如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是Native方法,這個計數器值則爲空(Undefined)“

Java虛擬機棧

“虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀(Stack Frame[1])用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息”
“每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程”
異常
“如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常”
“如果虛擬機棧可以動態擴展(當前大部分的Java虛擬機都可動態擴展,只不過Java虛擬機規範中也允許固定長度的虛擬機棧),如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常”

本地方法中棧

“虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的Native方法服務”
“與虛擬機棧一樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常”

java堆

“Java堆是被所有線程共享的一塊內存區域”
“所有的對象實例以及數組都要在堆上分配,但是隨着JIT編譯器的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有的對象都分配在堆上也漸漸變得不是那麼“絕對”了。”

內存回收

“Java堆中還可以細分爲:新生代和老年代;再細緻一點的有Eden空間、From Survivor空間、To Survivor空間等”

內存分配

“線程共享的Java堆中可能劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer,TLAB)”

方法區

“方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然Java虛擬機規範把方法區描述爲堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是與Java堆區分開來”

運行時常量池

“運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。”

直接內存

HotSpot虛擬機對象探究

對象的創建

如何分配內存

  • 空閒列表(Free List)
    如果Java堆中的內存並不是規整的,已使用的內存和空閒的內存相互交錯,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄
  • 指針碰撞(Bump the Pointer)
    對象所需內存的大小在類加載完成後便可完全確定,爲對象分配空間的任務等同於把一塊確定大小的內存從Java堆中劃分出來。假設Java堆中內存是絕對規整的,所有用過的內存都放在一邊,空閒的內存放在另一邊,中間放着一個指針作爲分界點的指示器,那所分配內存就僅僅是把那個指針向空閒空間那邊挪動一段與對象大小相等的距離,這種分配方式稱爲“指針碰撞”(Bump the Pointer)

如何解決線程安全問題

  • 一種是對分配內存空間的動作進行同步處理——實際上虛擬機採用CAS配上失敗重試的方式保證更新操作的原子性
  • 另一種是把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存,稱爲本地線程分配緩衝(Thread Local Allocation Buffer,TLAB)。哪個線程要分配內存,就在哪個線程的TLAB上分配,只有TLAB用完並分配新的TLAB時,才需要同步鎖定。虛擬機是否使用TLAB,可以通過-XX:+/-UseTLAB參數來設定。

注意:內存分配完成後,虛擬機需要將分配到的內存空間都初始化爲零值(不包括對象頭),如果使用TLAB,這一工作過程也可以提前至TLAB分配時進行。這一步操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值

對象的內存佈局

對象頭(Header)

1.png

虛擬機對象頭 Mark Word

實例數據(Instance Data)

實例數據部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。這部分的存儲順序會受到虛擬機分配策略參數(FieldsAllocationStyle)和字段在Java源碼中定義順序的影響

對其填充(Padding)

對齊填充並不是必然存在的,也沒有特別的含義,它僅僅起着佔位符的作用.由於HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說,就是對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(1倍或者2倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。

對象的訪問定位

使用句柄

2.png

Java堆中將會劃分出一塊內存來作爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息

直接指針

Java堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,而reference中存儲的直接就是對象地址
3.png

垃圾收集器與內存分配策略

對象已死?

引用計數法(並未採用)

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器爲0的對象就是不可能再被使用的
缺陷:遇到互相引用的對象時,無法通知GC收集器回收

可達性分析算法

通過一系列的稱爲"GC Roots"的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的

1.png

作爲GC Roots的對象

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  • 方法區中類靜態屬性引用的對象
  • 方法區中常量引用的對象。
  • 本地方法棧中JNI(即一般說的Native方法)引用的對象。

再談引用

  • 當內存空間還足夠時,則能保留在內存之中;如果內存空間在進行垃圾收集後還是非常緊張,則可以拋棄這些對象。很多系統的緩存功能都符合這樣的應用場景
  • 強引用就是指在程序代碼之中普遍存在的,類似"Object obj=new Object()"這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象
  • 軟引用是用來描述一些還有用但並非必需的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的內存,纔會拋出內存溢出異常。在JDK 1.2之後,提供了SoftReference類來實現軟引用。(如緩存?)
  • 弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象
  • 虛引用也稱爲幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知
    通過此可以監聽對象是否被GC?

生存還是死亡

  • 如果對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視爲“沒有必要執行”。
    如果這個對象被判定爲有必要執行finalize()方法,那麼這個對象將會放置在一個叫做F-Queue的隊列之中,並在稍後由一個由虛擬機自動建立的、低優先級的Finalizer線程去執行它。這裏所謂的“執行”是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束,這樣做的原因是,如果一個對象在finalize()方法中執行緩慢,或者發生了死循環(更極端的情況),將很可能會導致F-Queue隊列中其他對象永久處於等待,甚至導致整個內存回收系統崩潰。finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模的標記,如果對象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移除出“即將回收”的集合;如果對象這時候還沒有逃脫,那基本上它就真的被回收了。從代碼清單3-2中我們可以看到一個對象的finalize()被執行,但是它仍然可以存活
  • 任何一個對象的finalize()方法都只會被系統自動調用一次,如果對象面臨下一次回收,它的finalize()方法不會被再次執行
  • 它的運行代價高昂,不確定性大,無法保證各個對象的調用順序

回收方法區

永久代的垃圾收集主要回收兩部分內容:廢棄常量和無用的類

常量

就是沒有任何String對象引用常量池中的"abc"常量,也沒有其他地方引用了這個字面量,如果這時發生內存回收,而且必要的話,這個"abc"常量就會被系統清理出常量池。常量池中的其他類(接口)、方法、字段的符號引用也與此類似

  • 該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例
  • 加載該類的ClassLoader已經被回收
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

垃圾回收算法

標記清除法(Mark-Sweep)

“首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象”

不足

  • “一個是效率問題,標記和清除兩個過程的效率都不高”
  • “一個是空間問題,標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作”

複製算法(Copying)

“它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉”
不足
“這種算法的代價是將內存縮小爲了原來的一半,未免太高了一點”

標記整理算法(Mark-Compact)

“標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存

分代收集算法

“根據對象存活週期的不同將內存劃分爲幾塊”(新生代和老年代)

  • “在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集”
  • “老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”算法來進行回收”

HotSpot算法實現

枚舉根節點

此操作必須要在整個執行系統"暫停"狀態下執行,不可以出現分析過程中對象引用關係還在不斷變化的情況,該點不滿足的話分析結果準確性就無法得到保證

STW(Stop The World)

“是導致GC進行時必須停頓所有Java執行線程(Sun將這件事情稱爲"Stop The World")的其中一個重要原因,即使是在號稱(幾乎)不會發生停頓的CMS收集器中,枚舉根節點時也是必須要停頓的”

引用對象

“虛擬機應當是有辦法直接得知哪些地方存放着對象引用。在HotSpot的實現中,是使用一組稱爲OopMap的數據結構來達到這個目的的,在類加載完成的時候,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用”

安全點(SafePoint)

  • "特定位置"在OopMap中記錄了信息
  • “選定基本上是以程序“是否具有讓程序長時間執行的特徵”爲標準進行選定的”
  • “如何在GC發生時讓所有線程(這裏不包括執行JNI調用的線程)都“跑”到最近的安全點上再停頓下來”
    – 搶斷式中斷(Preemptive Suspension)
    “不需要線程的執行代碼主動去配合,在GC發生時,首先把所有線程全部中斷,如果發現有線程中斷的地方不在安全點上,就恢復線程,讓它“跑”到安全點上”
    – 主動式中斷(Voluntary Suspension)
    主動式中斷的思想是當GC需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就自己中斷掛起

安全區域(Sage Region)

Safepoint機制保證了程序執行時,在不太長的時間內就會遇到可進入GC的Safepoint。但是,程序“不執行”的時候呢?所謂的程序不執行就是沒有分配CPU時間,典型的例子就是線程處於Sleep狀態或者Blocked狀態,這時候線程無法響應JVM的中斷請求,“走”到安全的地方去中斷掛起,JVM也顯然不太可能等待線程重新被分配CPU時間。對於這種情況,就需要安全區域(Safe Region)來解決。
安全區域是指在一段代碼片段之中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的。我們也可以把Safe Region看做是被擴展了的Safepoint

垃圾收集器

2.png

Serial收集器

  • 單線程的收集器
  • 簡單而高效
  • Serial收集器對於運行在Client模式下的虛擬機來說是一個很好的選擇
  • 過程
    3.png

ParNew收集器

  • Serial收集器的多線程版本
  • 運行在Server模式下的虛擬機中首選的新生代收集器
  • ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至由於存在線程交互的開銷,該收集器在通過超線程技術實現的兩個CPU的環境中都不能百分之百地保證可以超越Serial收集器。當然,隨着可以使用的CPU的數量的增加,它對於GC時系統資源的有效利用還是很有好處的。它默認開啓的收集線程數與CPU的數量相同,在CPU非常多(譬如32個,現在CPU動輒就4核加超線程,服務器超過32個邏輯CPU的情況越來越多了)的環境下,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數
  • 過程
    4.png

Parallel Scavenge收集器

  • 一個新生代收集器,它也是使用複製算法的收集器,又是並行的多線程收集器(吞吐量優先”)
  • 參數
    • -XX:MaxGCPauseMillis
      “制最大垃圾收集停頓時間”
    • -XX:GCTimeRation
      設置吞吐量大小
    • -XX:UseAdaptiveSizePolicy
      “GC自適應的調節策略(GC Ergonomics)
      “個開關參數,當這個參數打開之後,就不需要手工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數了,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量

SerialOld收集器

  • “使用“標記-整理”算法”
  • “Serial收集器的老年代版本,它同樣是一個單線程收集器”
  • “主要意義也是在於給Client模式下的虛擬機使用”
  • 用於Server
    • 一種用途是在JDK,1.5以及之前的版本中與Parallel Scavenge收集器搭配使用
    • 另一種用途就是作爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用
  • 過程
    5.png

Parallel Old收集器

  • “使用多線程和“標記-整理”算法”
  • Parallel Old是Parallel Scavenge收集器的老年代版本
  • 參數
    6.png

CMS收集器(Concurrent MarkSweep)

  • 一種以獲取最短回收停頓時間爲目標的收集器

  • CMS收集器是基於“標記—清除”算法實現的

    • 初始標記(CMS initial mark)
    • 併發標記(CMS concurrent mark)
    • 重新標記(CMS remark)
    • 併發清除(CMS concurrent sweep)
      速度比較
      初始<重新<併發標記|清除
  • 過程
    7.png

缺點

  • CMS收集器對CPU資源非常敏感
  • CMS收集器無法處理浮動垃圾(Floating Garbage),可能出現"Concurrent Mode Failure"失敗而導致另一次Full GC的產生
  • 收集結束時會有大量空間碎片產生

G1收集器(Garbage First)

特點

  • 並行與併發
  • 分代收集
  • 空間整合
  • 可預測的停頓

在G1之前的其他收集器進行收集的範圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的內存佈局就與其他收集器有很大差別,它將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合

它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的來由)

步驟

  • 初始標記(Initial Marking)
  • 併發標記(Concurrent Marking)
  • 最終標記(Final Marking)
  • 篩選回收(Live Data Counting and Evacuation)
  • 過程
    8.png

內存分配與回收策略

分代回收名稱

  • 新生代GC(Minor)
    “指發生在新生代的垃圾收集動作,因爲Java對象大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快”
  • 老年代GC(Major/Full)
    “指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略裏就有直接進行Major GC的策略選擇過程)。Major GC的速度一般會比Minor GC慢10倍以上。”

對象優先在Eden分配

“對象在新生代Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC”

大對象直接進入老年代

所謂的大對象是指,需要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組(筆者列出的例子中的byte[]數組就是典型的大對象)

調節參數
-XX:PretenureSizeThreshold
大於這個設置值的對象直接在老年代分配。這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的內存複製(複習一下:新生代採用複製算法收集內存)

長期存活的對象進入老年代

對象在Eden出生並且經過一次MinorGC,如果能被移動到survivor中,則年齡爲1,此後每經歷過一次MinorGC年齡加一,默認爲15則會進入老年代
調節參數
-XX:MaxTenuringThreshold=n
n爲多少次MinorGC

動態對象年齡判斷

如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡

空間分配擔保

發生MinorGC前,虛擬機會檢查老年代可分配的連續區域是否大於整個Eden中所有對象大小,如果滿足則MinorGC是安全的,否則則看HandlePromotionFailure是否允許失敗,如果允許“那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試着進行一次Minor GC,儘管這次Minor GC是有風險的;如果小於,或者HandlePromotionFailure設置不允許冒險,那這時也要改爲進行一次Full GC”

“JDK 6 Update 24之後的規則變爲只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,否則將進行Full GC”

虛擬機性能監控工具與故障處理工具

JDK命令行工具

jps:虛擬機進程管理工具

“可以列出正在運行的虛擬機進程,並顯示虛擬機執行主類(Main Class,main()函數所在的類)名稱以及這些進程的本地虛擬機唯一ID(Local Virtual Machine Identifier,LVMID)”
jps[options][hostid]
參數

  • -p
    只輸出LVMID省略Main類名
  • -m
    輸出虛擬機進程啓動時傳遞給main()函數的參數
  • -l
    輸出主類的全名,如果進程執行的是jar包,則輸出jar的路徑
  • -v
    輸出啓動時的jvm參數

jstat:虛擬機統計信息監視工具

監視虛擬機各種運行狀態信息的命令行工具。它可以顯示本地或者遠程[1]虛擬機進程中的類裝載、內存、垃圾收集、JIT編譯等運行數據,在沒有GUI圖形界面,只提供了純文本控制檯環境的服務器上,它將是運行期定位虛擬機性能問題的首選工具

jstat[option vmid[interval[s|ms][count]]]
9.png

jinfo:Java配置信息工具

實時地查看和調整虛擬機各項參數

jinfo[option]pid
jinfo-flag CMSInitiatingOccupancyFraction 1444

jmap:Java內存映像工具

用於生成堆轉儲快照(一般稱爲heapdump或dump文件)

jmap[option]vmid
10.png

jhat:虛擬機堆轉儲快照分析工具

與jmap搭配使用,來分析jmap生成的堆轉儲快照

jstack: java堆棧跟蹤工具

生成虛擬機當前時刻的線程快照(一般稱爲threaddump或者javacore文件)
jstack[option]vmid

  • -F
    當正常輸出不被響應時,強制輸出線程堆棧

  • -l
    出堆棧外,顯示關於鎖的附加信息

  • -m
    如果調用本地方法的話,顯示c/c++的堆棧

HSDIS:JIT生成代碼反彙編

JDK的可視化工具

JConsole:Java監視與管理控制檯

基於JMX的可視化監視、管理工具

VisualVM:多合一故障處理工具

VisualVM(All-in-One Java Troubleshooting Tool)是到目前爲止隨JDK發佈的功能最強大的運行監視和故障處理程序,並且可以預見在未來一段時間內都是官方主力發展的虛擬機故障處理工具。官方在VisualVM的軟件說明中寫上了"All-in-One"的描述字樣,預示着它除了運行監視、故障處理外,還提供了很多其他方面的功能

調優案例與奇淫技巧

一分多合理利用資源

建立5個32位JDK的邏輯集羣,每個進程按2GB內存計算(其中堆固定爲1.5GB),佔用了10GB內存。另外建立一個Apache服務作爲前端均衡代理訪問門戶。考慮到用戶對響應速度比較關心,並且文檔服務的主要壓力集中在磁盤和內存訪問,CPU資源敏感度較低,因此改爲CMS收集器進行垃圾回收。部署方式調整後,服務再沒有出現長時間停頓,速度比硬件升級前有較大提升

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章