Jvm虛擬機學習

一、組成及其作用

1、類加載器

虛擬機把描述類的數據從Class文件加載到內存,並且對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型;

1.1、類加載過程

  • 加載:根據查找路徑導入相應的class文件;
  • 驗證:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證,檢查加載的class文件的正確性;
  • 準備:給類中的靜態變量分配內存空間;
  • 解析:虛擬機將常量池中的符號引用替換成直接應用的過程,符號引用用一組符號來描述所引用的目標,在直接引用中直接指向內存中的地址;
  • 初始化:對靜態變量和靜態代碼塊執行初始化工作;

1.2、雙親委派模型

工作原理
如果一個類加載器收到了類加載請求,它並不會自己先去加載,而是把這個請求委託給父類的加載器去執行,如果父類加載器還存在其父類加載器,則進一步向上委託,依次遞歸,請求最終將到達頂層的啓動類加載器,如果父類加載器可以完成類加載任務,就成功返回,倘若父類加載器無法完成此加載任務,子加載器纔會嘗試自己去加載,這就是雙親委派模式;
優勢
避免類的重複加載;

1.3、類加載器分類

  • 啓動類加載器:加載Java的核心庫;
  • 擴展類加載器:加載Java的擴展庫;
  • 應用程序類加載器:加載Java應用的類;

2、運行時區域

2.1、 程序計數器

記錄當前正在執行的虛擬機字節碼的指令地址;
如果正在執行的是本地方法則爲空;

2.2、Java虛擬機棧

每個Java方法在執行的同時會創建一個棧幀用於存儲局部變量表、操作數棧、常量池引用等信息。從方法的調用直至完成的過程中,對應一個棧幀在Java虛擬機中入棧和出棧的過程;

該區域可能拋出以下異常:

  • 當線程請求的棧深度超過最大值,會拋出 StackOverflowError 異常;
  • 棧進行動態擴展時如果無法申請到足夠內存,會拋出 OutOfMemoryError 異常。

2.3、本地方法棧

本地方法棧與 Java 虛擬機棧類似,它們之間的區別只不過是本地方法棧爲本地方法服務。

2.4、堆

所有對象在這裏分配內存,是垃圾回收的主要區域(“GC堆”),被所有線程所共享;
從內存回收的角度,現在的垃圾收集器都是採用分代收集算法,主要是針對不同類型的對象採取不同的垃圾回收算法,可以將堆分爲兩塊:

  • 新生代(Young Generation)
  • 老年代(Old Generation)

其中新生代按照8:1:1的比例分爲Eden區、from Survivor、to Survivor三個區域,

堆不需要連續內存,並且可以動態增加其內存,增加失敗會拋出 OutOfMemoryError 異常,可以通過 -Xms和-Xmx這兩個虛擬機參數來指定一個程序的堆內存大小,第一個參數設定初始值,第二個參數設定最大值;

2.5、方法區

用於存放已被加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據;

在Java1.8開始,移除永久代,並且把方法區移至元空間,位於本地內存中,而不是虛擬機內存中;

2.6、運行時常量池

運行時常量池是方法區的一部分,Class 文件中的常量池(編譯器生成的字面量和符號引用)會在類加載後被放入這個區域。除了在編譯期生成的常量,還允許動態生成,例如 String 類的 intern()。

二、垃圾收集

如何判斷一個對象是否可被回收

1、引用計數法

爲對象添加一個引用計數器,當對象增加一個引用計數器加1,引用失效時計數器減1,引用計數爲0的對象可以被回收;
缺點:無法解決對象直接的循環引用問題;

2、可達性分析算法

以 GC Roots 爲起始點進行搜索,可達的對象都是存活的,不可達的對象可被回收。Java虛擬機中使用該算法來判斷對象是否可以回收,GCRoots一般包含以下內容:

  • 虛擬機棧中局部變量表中引用的對象;
  • 本地方法棧中 JNI 中引用的對象;
  • 靜態成員變量或者常量引用的對象;

3、一個對象有多個引用,如何判斷它的可達性

單弱多強

引用類型

1、強引用

被強引用關聯的對象不會被回收,使用new一個新對象的方式來創建強引用:

Object obj = new Object();

2、軟引用

被軟引用的對象只有在內存不夠的情況下才會被回收,使用SoftReference類來創建軟引用。

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;

3、弱引用

被弱引用關聯的對象一定會被回收,也就是說它只能存活到下一次垃圾回收發生之前。使用WeakReference 類來創建弱引用:

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;

4、虛引用

爲一個對象設置虛引用的唯一目的是能在這個對象被回收時收到一個系統通知;
使用PhantomReference來創建虛引用:

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj,null);
obj = null;

垃圾收集算法

1、標記-清除算法

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

不足:

  • 標記和清除過程的效率都不高;
  • 標記清除之後會產生大量不連續的內存碎片,而導致在分配較大對象時因爲無法得到足夠的連續內存而不得不提前觸發一次垃圾收集動作;

2、標記-整理算法

讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存;不會產生內存碎片;

不足:

  • 需要移動大量對象,處理效率比較低;

3、複製

將可用內存分爲大小相等的兩塊,每次只使用其中的一塊,當其中一塊內存用完之後,就將存活的對象複製到另外一塊上,然後再把已使用的內存空間一次性清理掉,使得每次都可以對整個半區進行內存回收

在商業虛擬機中並不需要按照1:1的比例進行劃分內存空間,將內存分爲一個較大的Eden和兩塊較小的Survior空間;當回收時,將Eden和Survivor中還存活的對象一次性複製到另外一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間;

HotSpot虛擬機默認Eden和Sruvivor的大小比例是8:1;

4、分代收集算法

現在的商業虛擬機採用分代收集算法,它根據對象存活週期將內存劃分爲幾塊,不同塊採用適當的收集算法。

一般將堆分爲新生代和老年代。

  • 新生代使用:複製算法(新生代中每次垃圾收集時都有大批對象死去,只有少量對象存活)
  • 老年代使用:標記 - 清除 或者 標記 - 整理 算法(老年代對象中因爲對象存活率高、沒有額外的空間進行分配擔保)

垃圾收集器

1、老年代回收器

CMS

CMS收集器是犧牲吞吐量爲代價來獲取最短停頓時間爲目標的垃圾回收器,
CMS收集器是基於“標記——清除”算法,整個過程分爲四個步驟:

  • 初始標記:僅僅標記一下GC Roots能直接關聯到的對象;
  • 併發標記:進行GC Roots Tracing,耗時最長不需要停頓;
  • 重新標記:爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄;
  • 併發清理:不需要停頓;

優點:併發收集、低停頓;
缺點:

  • 吞吐量低:低停頓時間是以犧牲吞吐量爲代價的,導致CPU利用率不高;
  • 無法處理浮動垃圾,可能出現 Concurrent Mode Failure。浮動垃圾是指併發清除階段由於用戶線程繼續運行而產生的垃圾,這部分垃圾只能到下一次 GC 時才能進行回收;
  • 標記-清除算法導致得到空間碎片,導致會給大對象的內存分配出現問題;

Serial old

作爲Serial 收集器的老年代版本,也是給 Client 場景下的虛擬機使用。如果用在 Server 場景下,它有兩大用途:

  • 在 JDK 1.5 以及之前版本(Parallel Old 誕生以前)中與 Parallel Scavenge 收集器搭配使用。
  • 作爲 CMS 收集器的後備預案,在併發收集發生 Concurrent Mode Failure 時使用。

Parallel old

Parallel Scavenge 收集器的老年代版本。

在注重吞吐量以及 CPU 資源敏感的場合,都可以優先考慮 Parallel Scavenge 加 Parallel Old 收集器。

2、新生代回收器

serial

單線程收集器,只會使用一個線程進行垃圾回收工作;

優點:簡單高效,沒有線程交互的開銷,擁有最高的單線程收集效率;

在內存不大的場景下,收集一兩百兆垃圾的停頓時間可以控制在一百多毫秒以內,只要不是太頻繁,這些停頓時間是可以接受的;

Parnew

Serial 收集器的多線程版本;

Server 場景下默認的新生代收集器,除了性能原因外,主要是因爲除了 Serial 收集器,只有它能與 CMS 收集器配合使用;

Parallel Scavenge

與 ParNew 一樣是多線程收集器
達到可控制的吞吐量,吞吐量是指CPU用於運行用戶程序的時間佔總時間的比值;

高吞吐量可以高效率的利用CPU時間,儘快完成程序的運算程序;

縮短停頓時間是以犧牲吞吐量和新生代空間來換取的:新生代空間變小,垃圾回收變得頻繁,導致吞吐量下降。

3、整堆回收器

G1

G1是一種兼顧吞吐量和停頓時間的GC實現,是JDK9以後的默認GC選項;一款面向服務端應用的垃圾收集器,在多 CPU 和大內存的場景下有很好的性能。

通過引入Region的概念,將原先的一整塊內存空間劃分成多個小空間,使得每個小空間可以單獨進行垃圾回收;

步驟分爲四步:

  • 初始標記:
  • 併發標記:
  • 最終標記:爲了修正在併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程的 Remembered Set Logs 裏面,最終標記階段需要把 Remembered Set Logs 的數據合併到 Remembered Set 中。這階段需要停頓線程,但是可並行執行。
  • 篩選回收:首先對於各個Region內中的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計劃;此階段也可以做到和用戶程序一起併發執行,但是因爲只回收一部分Region,時間時用戶可控制的,而且停頓用戶線程將大幅度提高收集效率;

空間整合:
從整體來看是基於"標記-整理"算法實現的收集器,從局部上來看是基於複製算法實現的,這意味着運行期間不會產生內存空間碎片;

最大的特點是引入了分區的思路,弱化了分化的概念;

每個分區被標記了E、S、O和H,H表示這些Region中存儲的是巨型對象,新建對象大小超過Region大小一半時,直接在歆的一個或者多個連續分區中分配,並標記爲H;

三、內存分配和回收策略

內存分配和回收策略

Minor GC 和 Full GC

  • Minor GC:回收新生代,因爲新生代對象存活時間很短,Minor GC就會頻繁執行,執行的速度一般也會比較快;
  • Full GC:回收老年代和新生代,老年代對象其存活時間長,因此Full GC很少執行,執行速度會比Minor GC慢很多;

內存分配策略

1、對象優先在Eden分配

大多數情況下,對象在新生代Eden上分配,當Eden空間不夠時,發起Minor GC

2、大對象直接進入老年代

大對象是指需要連續內存空間的對象,最典型的大對象是那種很長的字符串以及數組;

經常出現大對象會提前出發垃圾收集來獲取足夠的連續空間分配給大對象;

-XX:PretenureSizeThreshold,大於此值的對象直接在老年代分配,避免在Eden和Survivor之間的大量內存複製;

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

爲對象定義年齡計數器,對象在 Eden 出生並經過 Minor GC 依然存活,將移動到 Survivor 中,年齡就增加 1 歲,增加到一定年齡則移動到老年代中;

-XX:MaxTenuringThreshold 用來定義年齡的閾值;

4、動態對象年齡判定

虛擬機並不是永遠要求對象的年齡必須達到 MaxTenuringThreshold 才能晉升老年代,如果在 Survivor 中相同年齡所有對象大小的總和大於 Survivor 空間的一半,則年齡大於或等於該年齡的對象可以直接進入老年代,無需等到 MaxTenuringThreshold 中要求的年齡。

5、空間分配擔保

在發起Minor GC之前,虛擬機先檢查老年代中最大可用的連續空間是否大於新生代所有對象總空間,如果條件成立的話,那麼Minor GC可以確認是安全的;

Full GC的觸發條件

對於MinorGC,其觸發條件十分簡單,當Eden空間滿時,就將觸發一次Minor GC,而Full GC相對複雜,條件如下:

1、調用 System.gc()

只是建議虛擬機執行 FullGC,但是虛擬機不一定真正去執行。不建議使用這種方式,而是讓虛擬機管理內存。

2、老年代空間不足

老年代空間不租的常見問題是大對象直接進入老年代、長期存活的對象進入老年代等;

應當避免創建過大的對象和數組,除此之外還可以通過-Xmn虛擬機參數來調大新生代的大小,讓對象儘量在新生代中被回收掉,不進入老年代;還可以通過-XX:MaxTenuringThreshold調大對象進入老年代的年齡,讓對象在新生代中多存活一段時間;

3、空間分配擔保失敗

使用複製算法的 MinorGC需要老年代的內存空間作擔保,如果擔保失敗會執行一次 Full GC;

4、Concurrent Mode Failure

執行CMS GC的過程中同時有對象要放入老年代中,而此時的老年代空間不租(可能是GC過程中浮動垃圾過多導致暫時性地空間不足),便會報Concurrent Mode Failure錯誤,並且觸發Full GC;

內存泄漏

  • 程序動態分配了內存,但是在程序結束時,沒有進行及時釋放,導致那部分內存不可用;
  • 被分配地對象可達但是已經沒有作用,循環創建對象,各種連接沒有及時釋放;

可以通過一些性能檢測工具,如JProfiler等工具查找內存泄漏;

內存溢出

  • 虛擬機和本地方法棧溢出
  • 堆溢出
  • 方法區溢出

內存溢出和內存泄漏的區別

  • 內存泄漏導致內存溢出的原因之一;內存泄漏積累起來將導致內存溢出;
  • 內存泄漏可以通過完善代碼來避免;內存溢出可以通過調整配置來減少發生頻率,但無法徹底避免;

如何避免內存泄漏和溢出?

  • 儘早釋放無用對象的引用;
  • 採用臨時變量的時候,讓引用變量在退出活動域之後自動設置爲null,暗示垃圾收集器來收集該對象,防止發生內存泄漏;
  • 程序進行字符串處理時,應該避免使用string,而應使用StringBuffer,因爲每一個String對象都會獨立佔用內存一塊區域;

相關面試題

1、如何減少GC次數?

  • 儘量少用靜態變量;
  • 對象不用時最好顯式設置爲null;
  • 增大堆的最大值設置;
  • 儘量使用stringBuffer而不是String,減少不必要的中間對象
  • 經常使用的圖片可以使用軟引用類型;

2、對象在內存中的初始化過程

https://blog.csdn.net/WantFlyDaCheng/article/details/81808064

以Student s = new Student()爲例:

  1. 首先查看類的符號引用,看是否在常量池中,不在的話進行類加載的過程;
  2. 在棧內存爲s變量申請一個空間;
  3. 在堆內存中爲Student對象申請空間;
  4. 對類中的成員變量進行默認初始化
  5. 對類中的成員變量進行顯示初始化;
  6. 有構造代碼塊就先執行,沒有就省略;
  7. 執行構造方法,通過構造方法來對對象數據進行初始化;
  8. 堆內存中的數據初始化完畢之後,把內存值複製給s變量;

3、一般Java堆是如何實現的?

4、對象的強、軟、弱和虛引用

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