1.1堆空間結構 Java 的自動記憶體管理主要是針對對象記憶體的回收和對象記憶體的分配。同時,Java 自動記憶體管理最核心的功能是 堆 記憶體中對象的分配與回收。Java 堆是垃圾收集器管理的主要區域,因此也被稱作 GC 堆。Eden 區、兩個 Survivor 區 S0 和 S1 都屬於新生代,中間一層 ...
1.1堆空間結構
Java 的自動記憶體管理主要是針對對象記憶體的回收和對象記憶體的分配。同時,Java 自動記憶體管理最核心的功能是 堆 記憶體中對象的分配與回收。Java 堆是垃圾收集器管理的主要區域,因此也被稱作 GC 堆。Eden 區、兩個 Survivor 區 S0 和 S1 都屬於新生代,中間一層屬於老年代,最下麵一層屬於永久代。
1.2記憶體分配和回收機制
當 Eden 區沒有足夠空間進行分配時,虛擬機將發起一次 Minor GC。GC 期間虛擬機又發現 allocation1 無法存入 Survivor 空間,所以只好通過 分配擔保機制 把新生代的對象提前轉移到老年代中去。執行 Minor GC 後,後面分配的對象如果能夠存在 Eden 區的話,還是會在 Eden 區分配記憶體。
大對象直接進入老年代,大對象就是需要大量連續記憶體空間的對象。大對象直接進入老年代主要是為了避免為大對象分配記憶體時由於分配擔保機制帶來的複製而降低效率。
長期存活的對象將進入老年代,大部分情況,對象都會首先在 Eden 區域分配。如果對象在 Eden 出生並經過第一次 Minor GC 後仍然能夠存活,並且能被 Survivor 容納的話,將被移動到 Survivor 空間(s0 或者 s1)中,並將對象年齡設為 1。對象在 Survivor 中每熬過一次 MinorGC,年齡就增加 1 歲,當它的年齡增加到一定程度(預設為 15 歲),就會被晉升到老年代中。
回收機制:
部分收集 (Partial GC):
- 新生代收集(Minor GC / Young GC):只對新生代進行垃圾收集;
- 老年代收集(Major GC / Old GC):只對老年代進行垃圾收集。需要註意的是 Major GC 在有的語境中也用於指代整堆收集;
- 混合收集(Mixed GC):對整個新生代和部分老年代進行垃圾收集。
整堆收集 (Full GC):收集整個 Java 堆和方法區。
GC 調優策略中很重要的一條經驗總結是這樣說的:
將新對象預留在新生代,由於 Full GC 的成本遠高於 Minor GC,因此儘可能將對象分配在新生代是明智的做法,實際項目中根據 GC 日誌分析新生代空間大小分配是否合理,適當通過“-Xmn”命令調節新生代大小,最大限度降低新對象直接進入老年代的情況。
1.3死亡對象判斷方法
引用計數法:給對象中添加一個引用計數器:有一個地方引用它,計數器就加 1;當引用失效,計數器就減 1;這個方法實現簡單,效率高,但是目前主流的虛擬機中並沒有選擇這個演算法來管理記憶體,其最主要的原因是它很難解決對象之間相互迴圈引用的問題。
可達性分析演算法:通過一系列的稱為 “GC Roots” 的對象作為起點,從這些節點開始向下搜索,節點所走過的路徑稱為引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連的話,則證明此對象是不可用的,需要被回收。
對象可以被回收,就代表一定會被回收嗎?
即使在可達性分析法中不可達的對象,也並非是“非死不可”的,這時候它們暫時處於“緩刑階段”,要真正宣告一個對象死亡,至少要經歷兩次標記過程;可達性分析法中不可達的對象被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行 finalize 方法。當對象沒有覆蓋 finalize 方法,或 finalize 方法已經被虛擬機調用過時,虛擬機將這兩種情況視為沒有必要執行。被判定為需要執行的對象將會被放在一個隊列中進行第二次標記,除非這個對象與引用鏈上的任何一個對象建立關聯,否則就會被真的回收。
1.強引用(StrongReference)
一個對象具有強引用,那就類似於必不可少的生活用品,垃圾回收器絕不會回收它。當記憶體空間不足,Java 虛擬機寧願拋出 OutOfMemoryError 錯誤,使程式異常終止,也不會回收強引用的對象。
2.軟引用(SoftReference)
如果一個對象只具有軟引用,那就類似於可有可無的生活用品。如果記憶體空間足夠,垃圾回收器就不會回收它,如果記憶體空間不足了,就會回收這些對象的記憶體。
3.弱引用(WeakReference)
如果一個對象只具有弱引用,那就類似於可有可無的生活用品。弱引用與軟引用的區別在於:只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的記憶體區域的過程中,一旦發現了只具有弱引用的對象,不管當前記憶體空間足夠與否,都會回收它的記憶體。
4.虛引用(PhantomReference)
與其他幾種引用都不同,虛引用並不會決定對象的生命周期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。虛引用主要用來跟蹤對象被垃圾回收的活動。
虛引用與軟引用和弱引用的一個區別在於: 虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的記憶體之前,把這個虛引用加入到與之關聯的引用隊列中。程式可以通過判斷引用隊列中是否已經加入了虛引用,來瞭解被引用的對象是否將要被垃圾回收。程式如果發現某個虛引用已經被加入到引用隊列,那麼就可以在所引用的對象的記憶體被回收之前採取必要的行動。
運行時常量池主要回收的是廢棄的常量。 字元串常量池中存在字元串 "abc",如果當前沒有任何 String 對象引用該字元串常量的話,就說明常量 "abc" 就是廢棄常量,如果這時發生記憶體回收的話而且有必要的話,"abc" 就會被系統清理出常量池了。
方法區主要回收的是無用的類。而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下麵 3 個條件才能算是 “無用的類” ,滿足可以回收。
- 該類所有的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
- 載入該類的 ClassLoader 已經被回收。
- 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
1.4垃圾收集演算法
標記-清除演算法:該演算法分為“標記”和“清除”階段:首先標記出所有不需要回收的對象,在標記完成後統一回收掉所有沒有被標記的對象。這種垃圾收集演算法會帶來兩個明顯的問題:效率問題、空間問題(標記清除後會產生大量不連續的碎片)
標記-複製演算法:將記憶體分為大小相同的兩塊,每次使用其中的一塊。當這一塊的記憶體使用完後,就將還存活的對象複製到另一塊去,然後再把使用的空間一次清理掉。這樣就使每次的記憶體回收都是對記憶體區間的一半進行回收。
標記-整理演算法:根據老年代的特點提出的一種標記演算法,標記過程仍然與“標記-清除”演算法一樣,但後續步驟不是直接對可回收對象回收,而是讓所有存活的對象向一端移動,然後直接清理掉端邊界以外的記憶體。
分代收集演算法:當前虛擬機的垃圾收集都採用分代收集演算法,根據對象存活周期的不同將記憶體分為幾塊。一般將 java 堆分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集演算法。
比如在新生代中,每次收集都會有大量對象死去,所以可以選擇”標記-複製“演算法,只需要付出少量對象的複製成本就可以完成每次垃圾收集。而老年代的對象存活幾率是比較高的,而且沒有額外的空間對它進行分配擔保,所以我們必須選擇“標記-清除”或“標記-整理”演算法進行垃圾收集。
1.5垃圾收集器
Serial (串列)收集器:最基本、歷史最悠久的垃圾收集器了,它的 “單線程” 的意義不僅僅意味著它只會使用一條垃圾收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集工作的時候必須暫停其他所有的工作線程,直到它收集結束。它簡單而高效(與其他收集器的單線程相比)。Serial 收集器由於沒有線程交互的開銷,自然可以獲得很高的單線程收集效率。Serial 收集器對於運行在 Client 模式下的虛擬機來說是個不錯的選擇。
ParNew 收集器:Serial 收集器的多線程版本,除了使用多線程進行垃圾收集外,其餘行為和 Serial 收集器完全一樣。新
Parallel Scavenge 收集器:收集器關註點是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的關註點更多的是用戶線程的停頓時間。所謂吞吐量就是 CPU 中用於運行用戶代碼的時間與 CPU 總消耗時間的比值。
上面都是新生代採用標記-複製演算法,老年代採用標記-整理演算法。
Serial Old 收集器:Serial 收集器的老年代版本,它同樣是一個單線程收集器。它主要有兩大用途:一種用途是在 JDK1.5 以及以前的版本中與 Parallel Scavenge 收集器搭配使用,另一種用途是作為 CMS 收集器的後備方案。
Parallel Old 收集器:Parallel Scavenge 收集器的老年代版本。使用多線程和“標記-整理”演算法。在註重吞吐量以及 CPU 資源的場合,都可以優先考慮 Parallel Scavenge 收集器和 Parallel Old 收集器。
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。它非常符合在註重用戶體驗的應用上使用。是 HotSpot 虛擬機第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作。CMS 收集器是一種 “標記-清除”演算法實現的,主要優點:併發收集、低停頓。但是它有下麵三個明顯的缺點:
- 對 CPU 資源敏感;
- 無法處理浮動垃圾;
- 它使用的回收演算法-“標記-清除”演算法會導致收集結束時會有大量空間碎片產生。
G1 是一款面向伺服器的垃圾收集器,主要針對配備多顆處理器及大容量記憶體的機器. 以極高概率滿足 GC 停頓時間要求的同時,還具備高吞吐量性能特征.
- 並行與併發:G1 能充分利用 CPU、多核環境下的硬體優勢,使用多個 CPU來縮短 Stop-The-World 停頓時間。部分其他收集器原本需要停頓 Java 線程執行的 GC 動作,G1 收集器仍然可以通過併發的方式讓 java 程式繼續執行。
- 分代收集:雖然 G1 可以不需要其他收集器配合就能獨立管理整個 GC 堆,但是還是保留了分代的概念。
- 空間整合:與 CMS 的“標記-清除”演算法不同,G1 從整體來看是基於“標記-整理”演算法實現的收集器;從局部上來看是基於“標記-複製”演算法實現的。
- 可預測的停頓:這是 G1 相對於 CMS 的另一個大優勢,降低停頓時間是 G1 和 CMS 共同的關註點,但 G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為 M 毫秒的時間片段內。
G1 收集器在後臺維護了一個優先列表,每次根據允許的收集時間,優先選擇回收價值最大的 Region(這也就是它的名字 Garbage-First 的由來)
到 jdk8 為止,預設的垃圾收集器是 Parallel Scavenge 和 Parallel Old。從 jdk9 開始,G1 收集器成為預設的垃圾收集器
1.6類文件結構
JVM 可以理解的代碼就叫做位元組碼(即擴展名為 .class 的文件),它不面向任何特定的處理器,只面向虛擬機。Java 語言通過位元組碼的方式,在一定程度上解決了傳統解釋型語言執行效率低的問題,同時又保留瞭解釋型語言可移植的特點。所以 Java 程式運行時比較高效,而且,由於位元組碼並不針對一種特定的機器,因此,Java 程式無須重新編譯便可在多種不同操作系統的電腦上運行。
常量池計數器是從 1 開始計數的,將第 0 項常量空出來是有特殊考慮的,索引值為 0 代表“不引用任何一個常量池項”常量池主要存放兩大常量:字面量和符號引用。
1.7類載入過程
載入:
- 通過全類名獲取定義此類的二進位位元組流
- 將位元組流所代表的靜態存儲結構轉換為方法區的運行時數據結構
- 在記憶體中生成一個代表該類的 Class 對象,作為方法區這些數據的訪問入口
驗證:
準備:正式為類變數分配記憶體並設置類變數初始值的階段,這些記憶體都將在方法區中分配。有以下幾點需要註意:
- 這時候進行記憶體分配的僅包括類變數( 靜態變數,被 static 關鍵字修飾的變數),而不包括實例變數。實例變數會在對象實例化時隨著對象一塊分配在 Java 堆中。
- 這裡所設置的初始值"通常情況"下是數據類型預設的零值(如 0、0L、null、false 等)而不是 111(初始化階段才會賦值)。
解析:虛擬機將常量池內的符號引用替換為直接引用的過程,也就是得到類或者欄位、方法在記憶體中的指針或者偏移量。解析動作主要針對類或介面、欄位、類方法、介面方法、方法類型、方法句柄和調用限定符 7 類符號引用進行。
初始化:初始化階段是執行初始化方法,是類載入的最後一步,這一步 JVM 才開始真正執行類中定義的 Java 程式代碼(位元組碼)。<clinit> ()方法是編譯之後自動生成的。
卸載:即該類的 Class 對象被 GC。卸載類需要滿足 3 個要求:
- 該類的所有的實例對象都已被 GC,也就是說堆不存在該類的實例對象。
- 該類沒有在其他任何地方被引用
- 該類的類載入器的實例已被 GC
1.8類初始化過程
1.首先,初始化父類中的靜態成員變數和靜態代碼塊,按照在程式中出現的順序初始化;
2.然後,初始化子類中的靜態成員變數和靜態代碼塊,按照在程式中出現的順序初始化;
3.其次,初始化父類的普通成員變數和代碼塊,再執行父類的構造方法;
4.最後,初始化子類的普通成員變數和代碼塊,再執行子類的構造方法;
1.9類載入器
JVM 中內置了三個重要的 ClassLoader,除了 BootstrapClassLoader 其他類載入器均由 Java 實現且全部繼承自java.lang.ClassLoader:
- BootstrapClassLoader(啟動類載入器) :最頂層的載入類,由 C++實現,負責載入 %JAVA_HOME%/lib目錄下的 jar 包和類或者被 -Xbootclasspath參數指定的路徑中的所有類。
- ExtensionClassLoader(擴展類載入器) :主要負責載入 %JRE_HOME%/lib/ext 目錄下的 jar 包和類,或被 java.ext.dirs 系統變數所指定的路徑下的 jar 包。
- AppClassLoader(應用程式類載入器) :面向我們用戶的載入器,負責載入當前應用 classpath 下的所有 jar 包和類。
每一個類都有一個對應它的類載入器。系統中的 ClassLoader 在協同工作的時候會預設使用 雙親委派模型 。即在類載入的時候,系統會首先判斷當前類是否被載入過。已經被載入的類會直接返回,否則才會嘗試載入。載入的時候,首先會把該請求委派給父類載入器的 loadClass() 處理,因此所有的請求最終都應該傳送到頂層的啟動類載入器 BootstrapClassLoader 中。當父類載入器無法處理時,才由自己來處理。當父類載入器為 null 時,會使用啟動類載入器 BootstrapClassLoader 作為父類載入器。
雙親委派模型的好處:雙親委派模型保證了 Java 程式的穩定運行,可以避免類的重覆載入(JVM 區分不同類的方式不僅僅根據類名,相同的類文件被不同的類載入器載入產生的是兩個不同的類),也保證了 Java 的核心 API 不被篡改。
如果我們不想用雙親委派模型怎麼辦?
自定義載入器的話,需要繼承 ClassLoader 。如果我們不想打破雙親委派模型,就重寫 ClassLoader 類中的 findClass() 方法即可,無法被父類載入器載入的類最終會通過這個方法被載入。但是,如果想打破雙親委派模型則需要重寫 loadClass() 方法.打破雙親委派機制的場景有很多:JDBC、JNDI、Tomcat等
1.10JVM調優
所有線程共用數據區大小=新生代大小 + 年老代大小 + 持久代大小。持久代一般固定大小為 64m。所以 java 堆中增大年輕代後,將會減小年老代大小(因為老年代的清理是使用 fullgc,所以老年代過小的話反而是會增多 fullgc 的)。此值對系統性能影響較大,Sun 官方推薦配置為 java 堆的 3/8。
調整最大堆記憶體和最小堆記憶體:通常會將這兩個參數配置成相同的值,其目的是為了能夠在 java 垃圾回收機制清理完堆區後不需要重新分隔計算堆區的大小而浪費資源。
調整新生代和老年代的比值。
調整 Survivor 區和 Eden 區的比值。
設置年輕代和老年代的大小。
根據實際事情調整新生代和幸存代的大小,官方推薦新生代占 java 堆的 3/8,幸存代占新生代的 1/10。