Java虛擬機理解-內存管理

運行時數據區域

jdk 1.8之前與之後的內存模型有差異,方法區有變化(https://cloud.tencent.com/developer/article/1470519)。

java的內存數據區域劃分:

  • 程序計數器
  • 虛擬機棧
  • 本地方法棧
  • 方法區

程序計數器(Program Counter Register)

理解爲當前線程所執行的字節碼的行號指示器,字節碼解釋器工作時通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常、線程恢復等基礎功能依賴於此。
每個線程獨立存儲,互不影響,生命週期與線程相同
java方法的計數器內容時正在執行的虛擬機字節碼的指令地址,Native方法則是空(Undefined),此區域無OOM。

虛擬機棧(Java Virtual Machine Stacks)

線程私有,生命週期與線程相同。
描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀(Stack Frame)用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每個方法從調用到執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。
常說的Java內存區分爲堆(Heap)棧(Stack)兩塊比較粗糙,實際的內存劃分複雜很多。常用的對象內存分配關係最密切的內存區域是這兩塊,其中的棧在這裏就是指虛擬機棧的局部變量表部分。
局部變量表存放了編譯期可知的各種基本數據類型、對象引用(reference類型,不等同於對象本身,可能是指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或者其他與此對象相關的位置)和returnAddress類型(指向了一條字節碼指令地址)
64位的long和double佔用2個局部變量空間(Slot),其他佔用一個。
這個區域有兩種異常:
如果線程請求的棧深度大於虛擬機允許的深度,將拋出StackOverflowError。
如果虛擬機可以動態擴展內存,當擴展時無法申請到足夠內存是,會拋出OutOfMemoryError。

本地方法棧(Native Method Stack)

與虛擬機棧的作用相似,虛擬機棧執行的是java方法,而本地方法棧執行的是Native方法。 Sun HotSpot將本地方法棧與虛擬機棧合二爲一。
拋出的異常也相同

Java堆(Java Heap)

被所有線程共享,在虛擬機啓動時創建。唯一的目的就是存放對象,幾乎所有的對象實例都在這裏分配內存(棧上分配,標量替換等優化技術會有影響)(Java虛擬機規範原文 The heap is the runtime data area from which memory for all class instances and arrays is allocated)。可以通過(-Xmx -Xms控制堆的擴展)
堆是GC管理的主要區域,現在的收集器基本都採用分待收集算法,所以堆還可以分爲:新生代和老年代;再細一點分爲Eden空間、From Survivor空間、To Survivor空間等。從內存分配的角度來看,線程共享的Java堆中可能劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer, TLAB)

方法區(Method Area)

被所有線程共享的內存區域,用於存儲被虛擬機加載的類信息、常量、靜態變量、即時編譯期編譯後的代碼等數據。Java虛擬機規範把方法區描述爲堆的一個邏輯部分,但是它別名是Non-Heap(非堆)。
很多人將方法區稱爲永久代(Permanent Generation),本質上兩者並不等價,僅僅因爲HotSpot設計團隊將GC分代收集擴展至方法區,或者說使用永久代來實現方法區而已。這樣HotSpot的GC可以像管理Java堆一樣管理這部分內存,省去寫專門代碼。其他虛擬機(JRockit,J9)是不存在永久代的概念。
但是這樣容易出現OOM,存在(-XX:MaxPermSize上限,其他的VM只要沒有觸及進程可用內存上限就不會有問題),因此有極少數方法可能會出現問題(String.intern()),這個區域的內存回收主要針對常量池的回收和對類型的卸載。

運行時常量池(Runtime Constant Pool)

是方法區的一部分,Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。
JVM對於Class文件每一個字節用於存儲哪種數據都必須符合規範,對於運行時常量池沒有任何細節要求。運行時常量池具有動態性,常量並非編譯時產生,運行時也可以產生新的常量,如String.intern()

直接內存(Direct Memory)

並非虛擬機運行時數據區的一部分,也不是虛擬機規範中定義的內存區域。
JDK1.4中加入了NIO(New Input/Output),引入了基於通道(Channel)與緩衝區(Buffer)的IO方式,使用Native函數庫直接分配堆外內存,然後通過存儲在Java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作。這部分是不受到Java堆大小的限制。

HotSpot虛擬機對象

對象的創建

普通對象的創建(不包括數組和Class對象),當虛擬機遇到new指令時,首先檢查這個指令的參數能否在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否被加載、解析和初始化過。如果沒有會先執行相應的類加載過程。
類加載檢查通過後,VM爲新生對象分配內存。對象所需的內存大小在加載後可以完全確定。若java分配和空閒內存區域是絕對規整的,那分配過程僅僅是將指針向空閒空間移動一個與對象大小相等的距離,這個稱之爲“指針碰撞(Bump the Pointer)”。如果不是規整的,那VM要維護一個列表記錄可用內存,分配時找到足夠大的內存空間分配同時更新列表,這個稱之爲“空閒列表(Free List)”。
Java堆是否規整是由GC是否帶有壓縮整理功能決定,所以Serial,ParNew等帶Compact過程的收集器使用的是指針碰撞,CMS這種基於Mark-Sweep算法的收集器採用空閒列表。
併發情況下的線程安全考慮有兩種方案,
一種是堆內存分配空間的動作進行同步處理,虛擬機會採用CAS(compare and swap)配上失敗重試的方式保證更新操作的原子性。【CAS 操作包含三個操作數 —— 內存位置(V)、預期原值(A)和新值(B)。 如果內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值 。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該 位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前 值。)】
一種是把內存分配的動作按照線程劃分在不同的空間之中進行,就是每個線程都事先分配一小塊內存區域稱爲本地線程分配緩衝(Thread Local Allocation Buffer,TLAB),只要在要分配新的TLAB才需要同步鎖定,VM是否使用TLAB,可以使用-XX:+/-UseTLAB參數設定
內存分配完成後,虛擬機會將分配到的內存空間都初始化爲零值,保證了字段再java代碼中可以不賦值直接使用。
接下來虛擬機設置對象頭(Object Header),如對象是哪個類的實例,如何找到類的元數據信息,對象的哈希值,對象GC分代年齡等信息。

對象的內存佈局

在HotSpot虛擬機中,對象在內存中存儲佈局分爲3塊區域:對象頭(Header),實例數據(Instance Data),對齊填充(Padding)。
對象頭包含兩部分信息,第一部分用於存儲對象自身的運行時數據,如HashCode,GC分代年齡,鎖狀態標誌,線程持有的鎖,偏向線程ID,偏向時間戳等,官方稱之爲“Mark Word”。
對象投的另外一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象時哪個類的實例。數組還會額外存儲一塊記錄數組長度的數據。
接下來的實例數據部分是對象真正存儲的有效信息,各種字段類型內容。無論是父類還是子類,這部分的存儲屬性會收到虛擬機分配策略參數(FieldsAllocationStyle)和字段在Java源碼中定義順序的影響。
第三部分對齊填充並不是必然存在的,也沒有特別含義,僅起着佔位符的作用。HotSpot VM自動內存管理系統要求對象起始地址必須是8字節的整數倍,因此對象實例部分沒有對齊時需要用填充來補齊。

對象的訪問定位

Java程序要通過棧上的reference數據來操作堆上的具體對象。目前主流的訪問對象的方式分爲使用句柄和直接指針兩種。

句柄訪問
Java堆中會劃分出一塊內存來作爲句柄池,reference中存儲的就是對象的句柄地址,句柄中包含了對象實例數據(存儲在堆中)和類型數據(存儲在方法區中)各自的具體地址信息。
好處是穩定,對象移動(如GC時移動)只會改變句柄中的實例數據指針。
直接指針訪問
在Java堆對象的佈局中放置訪問類型數據的相關信息。reference中存儲的直接是對象地址。速度快,因爲節省了一次指針定位的時間開銷。

Sun HotSpot使用的是直接指針訪問的方式。

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

程序計數器,虛擬機棧,本地方法棧3個區域是隨線程生命週期,棧中的棧幀隨着方法的進入退出執行出入棧。這幾個區域的內存分配和回收都具備確定性。
主要是Java堆和方法區是動態的。

對象存活算法

引用計數法(Reference Counting),添加引用計數器,引用加一失效減一,主流Java虛擬機沒有使用,因爲比較難解決對象相互循環引用。
可達性分析算法(Reachability Analysis),從“GC Roots”對象作爲起始點向下搜索,搜索路徑稱爲“引用鏈(Reference Chain)”,不可達則證明對象不可用,就可以回收。
GC Roots對象包括如下:
虛擬機棧(棧幀中的本地變量表)中引用的對象。
方法區中類靜態屬性引用的對象。
方法區中常量引用的對象。
本地方法棧中JNI引用的對象。

JDK1.2之後,Java對引用的概念進行了擴充,分爲

  • 強引用(Strong Reference)
    在程序代碼中普遍存在 Object o=new Object(),強引用存在永遠不會回收對象
  • 軟引用(Soft Reference)
    有用但並非必需的對象,在內存溢出異常前,會把這些對象列進回收範圍進行第二次回收。這次之後如果沒有足夠內存纔會拋出內存溢出。JDK1.2之後提供了SoftReference類實現軟引用
  • 弱引用(Weak Reference)
    強度比軟引用更弱,無論當前內存是否足夠都會回收。JDK1.2之後提供了WeakReference類實現軟引用
  • 虛引用(Phantom Reference)
    虛引用不會影響生存時間,也無法通過虛引用取得對象實例,僅用於在GC時收到一個系統通知。JDK1.2之後提供了PhantomReference類實現軟引用

可達性分析算法中不可達的對象,要經歷2次標記之後纔會真正死亡。可達性算法計算後第一次標記並且進行一次篩選,條件是次對象是否有必要執行finalize()方法。如果沒有該方法,或者虛擬機調用過該方法,則視爲無需執行。
如果被判定有必要執行,這個對象會被放在F-Queue隊列中,並且稍後在一個由虛擬機自動建立的,低優先級的Finalizer線程去執行。執行意味着觸發但並不會等待。對象如果與引用鏈上任何一個對象建立關聯,則第二次標記時將移出“即將回收”的集合;否則就回收。

方法區回收

Java虛擬機規範中說過可以不要求虛擬機在方法區實現垃圾回收,因爲性價比比較低,堆中的新生代垃圾回收一次一般可以回收70%-95%的空間,永久代遠低於此。
永久代垃圾回收主要包含兩部分:廢棄常量和無用的類。
如常量“abc”,當前系統中沒有任何String對象或者其他地方引用,則會被清理出常量池。
無用的類條件則苛刻很多:

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

在大量使用反射、動態代理、CGLib等ByteCode框架,動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載功能,防止永久代內存溢出。

垃圾回收算法

標記-清除算法(Mark-Sweep)分爲標記和清除兩個階段。這是最基礎的收集算法,因爲後續收集算法都是基於這種思路並且對其不足進行改進而來。
其主要不足有兩個:

  • 效率問題:標記和清除兩個過程效率都不高
  • 空間問題:標記清除後會產生大量不連續的內存碎片,可能會導致後續在內存分配較大對象時無法找到連續內存而提前觸發另一次垃圾收集動作。

複製算法(Copying),它將內存劃分爲大小相等的兩塊,每次只用其中的一塊。當這塊內存用完就將存活的對象複製到另外一塊上,然後把這塊內存直接清理掉。代價就是內存縮小爲原來的一半。
現在的商業虛擬機都採用本算法來收集新生代。IBM公司研究表明新生代中98%對象生命週期短暫,不需要劃分一半的內存,而是劃分爲一塊較大的Eden空間和兩塊較小的Survivor空間。每次使用Eden和一塊Survivor,回收時將Eden和Survivor中存活對象一次性複製到另外一塊Survivor。HotSpot默認Eden和Survivor的比例是8:1.當Survivor空間不夠時,需要依賴其他內存(老年代)進行分配擔保(Handle Promotion)。
複製算法在對象存活率較高時需要進行較多的複製操作,效率變低。而且如果不想浪費50%的空間,就需要額外的空間進行分配擔保(100%對象存貨),老年代一般不能直接用這種算法

標記-整理算法(Mark-Compact),與標記清除相似,但是第二步不是直接清理,而是讓所有存活的對象都向一段移動,然後直接清理掉邊界意外的內存。

分代收集算法(Generational Collection),根據對象存活週期的不同將內存劃分爲幾塊,一般是將Java堆分爲新生代和老年代,這樣可以根據各個年代採用適當的收集算法。

有關年輕代的JVM參數
1)-XX:NewSize和-XX:MaxNewSize
用於設置年輕代的大小,建議設爲整個堆大小的1/3或者1/4,兩個值設爲一樣大。
2)-XX:SurvivorRatio
用於設置Eden和其中一個Survivor的比值,這個值也比較重要。
3)-XX:+PrintTenuringDistribution
這個參數用於顯示每次Minor GC時Survivor區中各個年齡段的對象的大小。
4).-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold
用於設置晉升到老年代的對象年齡的最小值和最大值,每個對象在堅持過一次Minor GC之後,年齡就加1。

HotSpot算法實現

枚舉根節點

可以作爲GC ROOT的節點主要在全局性的引用(如常量或類靜態屬性)與執行上下文(棧幀中的本地變量表)中。可達性分析堆執行時間的敏感還體現在GC停頓上,分析工作必須在一個能確保一致性的快照中進行。一致性是指整個分析期間執行系統像是被凍結在某個時間點上,不可以發生任何變化。GC進行時必須停頓所有java執行線程(Sun稱之爲 Stop The World)的其中一個重要原因,CMS收集器中,枚舉根節點時也是必須要停頓的。
目前的主流Java虛擬機都是準確式GC,使用一組稱爲OopMap(Ordinary Object Pointer)的數據結構存儲哪些地方存放着對象引用。在JIT編譯過程中,會在特定的位置記錄下棧和寄存器中是哪些位置是引用,GC掃描時可以直接得知這些信息。

安全點(safepoint)

如果爲每一條指令生成OopMap,這些可能會需要大量的額外空間,成本較高。所以HotSpot是在特定位置記錄這些信息,這些位置稱爲安全點。安全點的選定基本上是以程序“是否具有讓程序長時間執行的特徵”,例如方法調用、循環跳轉、異常跳轉等。
如何在GC發生時讓所有的線程都在最近的安全點上停頓。這裏有兩種方案選擇:

  • 搶先式中斷(Preemptive Suspension)
    GC發生時首先中斷全部線程,發現有線程中斷的點不在安全點上就恢復讓其運行到安全點。現在幾乎沒有虛擬機使用類似的方式。
  • 主動式中斷(Voluntary Suspension)
    GC發生時設置標誌,各個線程執行時主動去輪詢這個標誌,爲真時中斷掛起。輪詢標誌與安全點所在的地點重合。

SafePoint一般出現在以下位置:

  • 循環體的結尾
  • 方法返回前
  • 調用方法的call之後
  • 拋出異常的位置

安全區域(Safe Region)

當線程處於Sleep或者Block時,線程無法響應JVM的中斷請求,這時候就需要安全區域。安全區域是指在一段代碼片段中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的。當線程進入SR中的代碼時,首先標誌自己進入了SR,當這段時間JVM發起GC時,不用管SR狀態的線程。線程要離開SR時,會檢查系統是否完成根節點枚舉(或者整個GC過程),完成就繼續執行否則就等待信號。

垃圾收集器

上圖展示了7種不同分代的收集器,如果中間有線,表示他們可以搭配使用。

Serial收集器

最基本,發展歷史最悠久的收集器。在JDK1.3.1之前時虛擬機新生代手機的唯一選擇。單線程收集器,Stop The World的執行者。虛擬機運行在Client模式下的默認新生代收集器,因爲它簡單而高效。在桌面場景中,新生代內存一般是幾十或者一兩百兆,停頓時間控制在幾十毫秒最多一百多毫秒。

ParNew收集器

ParNew就是Serial的多線程版本,它是Server模式下的虛擬機中首選的新生代收集器,因爲除了Serial,只有它能和CMS(Concurrent Mark Sweep)收集器配合工作。因爲JDK1.5用CMS收集老年代時,新生代只能選擇Serial或者ParNew中的一個。

  • 並行(Parallel):指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態
  • 併發(Concurrent):用戶線程與垃圾收集器同時執行(不一定是並行,可能交替執行)

Parallel Scavenge收集器

新生代收集器,複製算法,並行多線程收集器。
CMS等收集器的關注點是儘可能縮短垃圾收集時用戶線程的停頓時間,PS收集器目標是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)
停頓時間端適合與用戶交互的程序,高吞吐量可以高效利用CPU時間,儘快完成程序運算任務,適合後臺運算而不需要太多交互的任務。
Parallel Scavenge提供了兩個參數用於精確控制吞吐量,最大垃圾收集停頓時間 -XX:MaxGCPauseMillis,直接設置吞吐量大小-XX:GCTimeRatio。
還有一個參數-XX:+UseAdaptiveSizePolicy,打開之後,新生代大小、Eden和Survivor區比例,晉升老年代對象年齡等細節參數不需要設置。虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱之爲GC自適應的調節策略(GC Ergonomics)

Serial Old收集器

這是Serial收集器的老年代版本,這個收集器的主要意義也是在於給Client模式下的虛擬機使用。在Server模式下有兩大用途:一種是用在JDK1.5以及之前的版本與Parallel Scavenge收集器搭配使用,另一種就是作爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。

Parallel Old收集器

這是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。是在JDK1.6提供。注重吞吐量以及CPU資源敏感的場合,可以考慮Parallel Scavenge加Parallel Old收集器。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以捉去最短回收停頓時間爲目標的收集器。運作過程分爲4個步驟:

  • 初始標記(CMS initial mark)
  • 併發標記(CMS concurrent mark)
  • 重新標記(CMS remark)
  • 併發清除(CMS concurrent sweep)
    其中初始標記、重新標記兩個步驟仍然需要“Stop The World”,初始標記僅標記GC Roots能直接關聯的對象,速度很快。併發標記就是進行GC Roots Tracing的過程,重新標記階段則是爲了修正併發標記期間因爲用戶程序繼續運行而導致標記產生變動的那一部分對象的標記,這個階段停頓時間一般比初始標記階段稍長一些,但是比並發標記的時間短。
    CMS的優點:併發收集、低停頓
    3個明顯缺點:
  1. CMS收集器對CPU資源非常敏感。其實面向併發設計的程序對CPU資源都比較敏感,在併發階段雖然不會暫停用戶線程,但是會因爲佔用了一部分線程(CPU)資源導致應用程序變慢,總吞吐量會降低。CMS默認啓動回收線程是(CPU數量+3)/4,也就是CPU在4個以上時會佔用不少於25%的CPU資源並且隨着CPU數量增加而下降,當CPU不足4個是,影響可能就很大。
  2. CMS收集器無法處理浮動垃圾(Floating Garbage),可能出現Concurrent Mode Failure失敗導致另一次Full GC的產生。CMS併發清理階段用戶線程還在運行,還會由新的垃圾產生。這部分在標記過程後,CMS無法在當次收集中處理掉他們,要等到下一次GC時再清理,這些垃圾稱爲“浮動垃圾”。CMS在GC過程中用戶線程還要繼續運行,因此需要預留一部分內存供用戶使用,因此老年代不能等到完全填滿。
  3. 因爲Mark-Sweep算法本身可能會引發的空間碎片。

G1收集器

G1(Garbage-First)收集器是最前沿的成果之一。目標是替換CMS,有如下特點:

  • 並行與併發:G1能充分利用多CPU、多環境下的硬件優勢,縮短StopTheWorld停頓時間。
  • 分代收集:不需要其他收集器配合能夠獨立管理整個GC堆
  • 空間整合:整體看來是基於“標記-整理”實現,局部(2個Reginon之間)上來看是基於“複製”算法實現。這意味這G1運作期間不會產生內存空間碎片,收集後可以提供規整的可用內存。
  • 可預測的停頓:能夠建立可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片段內,消耗在垃圾收集上的時間不能超過N毫秒,這個幾乎是實時java(RTSJ,Real-Time Java Specification)的垃圾收集器特徵。

使用G1收集器時,Java堆內存被劃分爲多個大小相等的獨立區域(Region),雖然保留新生代和老年代的概念,但是新生代和老年代不再是物理隔離的了,他們是一部分Region(不需要連續)的集合。
G1能夠建立可預測的停頓時間模型,是因爲它可以有計劃的避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需要時間的經驗值),在後臺維護一個優先列表,根據允許的收集時間,有限回收價值最大的Region。
G1算法實現從2004年Sun實驗室發表第一篇G1論文,直到jdk7u4才移除了“Experimental”標識達到商用程度。

JDK9之後G1稱爲默認收集器,
JDK11開始引入ZGC,是一種可擴展的低延遲垃圾收集器,旨在實現以下目標:

  • 暫停時間不超過10毫秒
  • 暫停時間不會隨堆或實時設置大小而增加
  • 處理堆範圍從幾百M到幾T字節大小

JDK12開始引入Shenandoah GC,Shenandoah是一款concurrent及parallel的垃圾收集器;跟ZGC一樣也是面向low-pause-time的垃圾收集器,不過ZGC是基於colored pointers來實現,而Shenandoah GC是基於brooks pointers來實現。

內存分配與回收策略

大部分情況下,對象在新生代Eden區分配,當Eden沒有足夠空間,虛擬機發起一次Minor GC。之後仍然不足進行Full GC。
大對象直接進入老年代,所以代碼中出現大對象是要很慎重,特別是一堆生命週期很短的大對象。
長期存活的對象進入老年代。每熬過一次Minor GC,年齡增加1歲,默認15歲就會晉升到老年代。
動態對象年齡判定,如果Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象可以直接進入老年代,無需等待MaxTenuringThreshold中要求的年齡。
空間分配擔保,Minor GC之前虛擬機會先檢查老年代最大可用連續空間是否大於新生代所有對象總空間,如果成立可以確保Minor GC安全。如果不成立,檢查老年代最大連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於則嘗試進行Minor GC。如果虛擬機設置值不允許冒險,或者小於平均大小,則改爲一次Full GC。擔保失敗(Handle Promotion Failure)後,會發起一次Full GC。

Java工具

  • jps(JVM Process Status Tool)
    可以列出正在運行的虛擬機進程,並顯示虛擬機執行主類(MainClass,main()函數所在的類)以及本地虛擬機唯一ID(Local Virtual Machine Identifier, LVMID)
    [jira@iz2ze6589vyznj8lm7cbk7z ~]$ jps -lv
    6610 org.apache.catalina.startup.Bootstrap -Djava.util.logging.config.file=/home/jira/atlassian-jira-software-7.5.2-standalone/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Xms1384m -Xmx3768m -Djava.awt.headless=true -Datlassian.standalone=JIRA -Dorg.apache.jasper.runtime.BodyContentImpl.LIMIT_BUFFER=true -Dmail.mime.decodeparameters=true -Dorg.dom4j.factory=com.atlassian.core.xml.InterningDocumentFactory -XX:-OmitStackTraceInFastThrow -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Xloggc:/home/jira/atlassian-jira-software-7.5.2-standalone/logs/atlassian-jira-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause -Dcatalina.base=/home/jira/atlassian-jira-software-7.5.2-standalone -Dcatalina.home=/home/jira/atlassian-jira-software-7.5.2-standalone -Djava.io.tmpdir=/home/jira/atlassian-jira-software-7.5.2-standalone/temp
    25652 synchrony.core -Xss2048k -Xmx1g
    25431 org.apache.catalina.startup.Bootstrap -Djava.util.logging.config.file=/home/jira/atlassian-confluence-6.3.1/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Xms1256m -Xmx2048m -XX:PermSize=128m -XX:MaxPermSize=512m -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dconfluence.context.path= -Dorg.apache.tomcat.websocket.DEFAULT_BUFFER_SIZE=32768 -Dsynchrony.enable.xhr.fallback=true -Xms1024m -Xmx2024m -XX:+UseG1GC -Datlassian.plugins.enable.wait=300 -Djava.awt.headless=true -XX:G1ReservePercent=20 -Xloggc:/home/jira/atlassian-confluence-6.3.1/logs/gc-2019-10-27_01-10-04.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=2M -XX:-PrintGCDetails -XX:+PrintGCDateStamps -XX:-PrintTenuringDistribution -Xms1256m -Xmx2048m -XX:PermSize=128m -XX:MaxPermSize=512m -Djava.endorsed.dirs=/home/jira/atlassian-confluence-6.3.1/endorsed -Dcatalina.base=/home/jira/atlassian-confluence-6.3.1 -Dcatalina.home=/home/jira/atlassian-confluence-6.3.1 -Djava.io.tm
    5372 sun.tools.jps.Jps -Dapplication.home=/usr/java/jdk1.8.0_181-amd64 -Xms8m

  • jstat(JVM Statistics Monitoring Tool)虛擬機統計信息監視工具
    它可以顯示本地或者遠程虛擬機進程中的類裝載、內存、垃圾回收、JIT編譯等運行數據。
    [jira@iz2ze6589vyznj8lm7cbk7z ~]$ jstat -gcutil 6610
    S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
    69.10 0.00 52.43 92.57 90.47 83.80 5613 460.532 12 13.624 474.156
    這個是我們Jira服務器GC情況,E表示Eden區使用了52.43%,兩個Survivor區(S0(69.10%),S1(0%)),老年代O(表示Old)佔用了92.57%,jdk1.8之前永生代是P(Permanent)之後是M(Metaspace)90.47%,CCS(Compressed Class Space)83.8%。YGC(Young GC)5613次,YGCT總耗時460秒,FGC(Full GC)12次,FGCT耗時13秒,GCT總GC耗時474秒。

  • jinfo(Java infomation)
    java配置信息工具,可以實時地查看和調整虛擬機各項參數,jps -v可以查看顯式指定參數,jinfo -flag可以進行默認值查詢,jinfo -sysprops可以把虛擬機進程中System.getProperties()內容打印出來。

  • jmap(Memory Map for Java)
    用於生成堆轉儲快照(一般稱爲heapdump或者dump文件),還可以查詢finalize執行隊列,Java堆和永久代的詳細信息,如空間使用率,使用的收集器等。jmap -dump:format=b,file=文件名 [pid] 會生成一個bin文件

  • jhat(JVM Heap Analysis Tool)
    虛擬機堆轉儲快照分析工具,與jmap搭配使用,來分析jmap生成的堆轉儲快照。內置了一個http服務器,可以再完成之後啓動網站訪問。命令就是直接 jhat xxx.bin

  • jstack(Stack Trace for Java)
    Java堆棧跟蹤工具,用於生成虛擬機當前時刻的線程快照(一般稱爲threaddump或者javacore文件),線程快照就是虛擬機內每一條線程當前執行的方法堆棧信息集合。jstatck -l pid。後續的JDK1.5中,java.lang.Thread類新增了一個getAllStackTraces()方法,可以寫個頁面直接查詢。

  • HSDIS(JIT生成代碼反彙編)
    Sun官方推薦的HotSpot虛擬機JIT編譯代碼的反彙編插件。
    在主流商用虛擬機中,HotSpot和J9可以採用混合模式(解釋器與編譯器配搭使用),而JRockit內部沒有解釋器,採用純編譯模式。
    Java程序最初是通過解釋器進行解釋執行的,當虛擬機發現某個方法或代碼塊運行的特別頻繁時,就會把這些代碼認定爲“熱點代碼”(Hot Spot Code)。爲了提高熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成爲本地平臺相關的機器碼,並進行優化,而完成這個任務的編譯器稱爲及時編譯器(Just In Time Compiler,簡稱JIT)。
    解釋器與編譯器
    解釋器優勢:當程序需要迅速啓動和執行時,解釋器可以首先發揮作用,省去編譯的時間,立即執行。同時解釋器還可作爲編譯器激進優化時的一個“逃生門”,當激進優化假設不成立時可退回到解釋狀態繼續執行。
    編譯器優勢:編譯之後得到優化後的本地代碼,執行效率高,但優化耗時較長,且佔用內存資源較大。

JDK可視化工具

最強的兩個可視化工具:JConsole和Visual VM(All-in-One Java Troubleshooting Tool),這個是正式的JDK成員。
Visual VMI可以做到:
顯示虛擬機進程以及配置、環境信息(jps,jinfo)
監視應用程序的CPU、GC、堆、方法區以及線程信息(jstat,jstack)
dump以及分析堆轉儲快照(jmap,jhat)
方法及的程序運行性能分析,找出被調用最多、運行時間最長的方法。
離線程序快照:收集程序運行時配置、線程dump、內存dump等信息建立快照。

調優案例分析與實戰

高性能硬件程序部署

4CPU,16G內存,64位JDK1.5,-Xmx和-Xms固定在12G,但是網站長時間失去響應。
排查後發現是GC停頓,Server模式默認使用吞吐量優先,回收12GB,一次FullGC停頓高達14秒,讀取文檔序列號產生大對象直接進入老年代。因此出現每個十幾分鍾出現十幾秒的停頓。

堆外內存導致的溢出錯誤

普通PC機,內存2G,JVM分配了1.6G,GC正常,堆內存正常,頻繁OOM。因爲框架NIO操作使用到Direct Memory內存,這塊是比較難直接回收的,只有在FullGC時順便回收。

外部命令導致系統緩慢

每個用戶請求的處 理都需要執行一個外部shell腳本來獲得系統的一些信息。執行這個shell腳本是通過Java的 Runtime.getRuntime().exec()方法來調用的。這種調用方式可以達到目的,但是它在Java 虛擬機中是非常消耗資源的操作,即使外部命令本身能很快執行完畢,頻繁調用時創建進程 的開銷也非常可觀。Java虛擬機執行這個命令的過程是:首先克隆一個和當前虛擬機擁有一 樣環境變量的進程,再用這個新的進程去執行外部命令,最後再退出這個進程。如果頻繁執 行這個操作,系統的消耗會很大,不僅是CPU,內存負擔也很重。
用戶根據建議去掉這個Shell腳本執行的語句,改爲使用Java的API去獲取這些信息後, 系統很快恢復了正常。

JVM進程崩潰

BS系統,正常運行一段時間之後JVM自動關閉,留下一個hs_err_pdi###.log文件。由於異步啓用了Socket請求另外一個較慢的站點,超過虛擬機承受能力後導致崩潰。改爲消息隊列後正常。

不恰當的數據結構

在HashMap<Long,Long>結構中,只有Key和Value所存放 的兩個長整型數據是有效數據,共16B(2×8B)。這兩個長整型數據包裝成java.lang.Long對 象之後,就分別具有8B的MarkWord、8B的Klass指針,在加8B存儲數據的long值。在這兩個 Long對象組成Map.Entry之後,又多了16B的對象頭,然後一個8B的next字段和4B的int型的 hash字段,爲了對齊,還必須添加4B的空白填充,最後還有HashMap中對這個Entry的8B的引 用,這樣增加兩個長整型數字,實際耗費的內存爲 (Long(24B)×2)+Entry(32B)+HashMap Ref(8B)=88B,空間效率爲16B/88B=18%, 實在太低了。

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