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等
JVM白話地址
1.10JVM調優
所有執行緒共享資料區大小=新生代大小 + 年老代大小 + 持久代大小,持久代一般固定大小為 64m,所以 java 堆中增大年輕代后,將會減小年老代大小(因為老年代的清理是使用 fullgc,所以老年代過小的話反而是會增多 fullgc 的),此值對系統性能影響較大,Sun 官方推薦配置為 java 堆的 3/8,
調整最大堆記憶體和最小堆記憶體:通常會將這兩個引數配置成相同的值,其目的是為了能夠在 java 垃圾回識訓制清理完堆區后不需要重新分隔計算堆區的大小而浪費資源,
調整新生代和老年代的比值,
調整 Survivor 區和 Eden 區的比值,
設定年輕代和老年代的大小,
根據實際事情調整新生代和幸存代的大小,官方推薦新生代占 java 堆的 3/8,幸存代占新生代的 1/10,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/551274.html
標籤:其他
上一篇:Java的static修飾符
下一篇:返回列表
