我對JVM的認知總結[筆記]

JVM是可運行Java代碼的假想計算機 ,包括一套字節碼指令集、一組寄存器、一個棧、
一個垃圾回收,堆 和 一個存儲方法域。
JVM 是運行在操作系統之上的,它與硬件沒有直接
的交互

JVM的運行過程

JAVA源文件->編譯器->class字節碼->JVM->機器碼

java虛擬機的多平臺運行

每一種平臺的解釋器是不同的,但是實現的虛擬機是相同的,這也就是 Java 爲什麼能夠
跨平臺的原因了 ,當一個程序從開始運行,這時虛擬機就開始實例化了,多個程序啓動就會
存在多個虛擬機實例。程序退出或者關閉,則虛擬機實例消亡,多個虛擬機實例之間數據不
能共享

線程是一個什麼東東

線程指程序執行過程中的一個線程實體。JVM 允許一個應用併發執行多個線程。
Hotspot JVM 中的 Java 線程與原生操作系統線程有直接的映射關係。當線程本地存儲、緩
衝區分配、同步對象、棧、程序計數器等準備好以後,就會創建一個操作系統原生線程

Java 線程結束,原生線程隨之被回收。操作系統負責調度所有線程,並把它們分配到任何可
用的 CPU 上。當原生線程初始化完畢,就會調用 Java 線程的 run() 方法。當線程結束時,會釋放原生線程和 Java 線程的所有資源。

java 類加載過程?

加載->驗證->準備->解析->初始化->使用->卸載

  • 加載(加載時類加載的第一個過程,在這個階段,將完成一下三件事情):
  1. 通過一個類的全限定名獲取該類的二進制流。
  2. 將該二進制流中的靜態存儲結構轉化爲方法區運行時數據結構。
  3. 在內存中生成該類的 Class 對象,作爲該類的數據訪問入口。
  • 驗證(驗證的目的是爲了確保 Class 文件的字節流中的信息不會危害到虛擬機;在該階段主要完成以下四鍾驗證)
  1. 文件格式驗證:驗證字節流是否符合 Class 文件的規範,如主次版本號是否在當前虛擬
    機範圍內,常量池中的常量是否有不被支持的類型.
  2. 元數據驗證:對字節碼描述的信息進行語義分析,如這個類是否有父類,是否集成了不
    被繼承的類等。
  3. 字節碼驗證:是整個驗證過程中最複雜的一個階段,通過驗證數據流和控制流的分析,
    確定程序語義是否正確,主要針對方法體的驗證。如:方法中的類型轉換是否正確,跳轉
    指令是否正確等。
  4. 符號引用驗證:這個動作在後面的解析過程中發生,主要是爲了確保解析動作能正確執
    行。
  • 準備(準備階段是爲類的靜態變量(類變量)分配內存並將其初始化爲默認值(初始化階段進行賦相應的值),這些內存都將在方法區中進行分配。準備階段不分配類中的實例變量(就是普通的成員變量)的內存,實例變量將會在對象實例化時(初始化階段)隨着對象一起分配在 Java 堆中),如果是靜態常量則會在準備階段爲其分配內存,並賦用戶自定義的值;
  1. public static int value=123; //在準備階段 value 初始值爲 0 。在初始化階段纔會變爲 123 。
  2. public static final int number = 3;//直接在準備階段賦值爲3,因爲其不可變
  • 解析(該階段主要完成符號引用到直接引用的轉換動作。解析動作並不一定在初始化動作完成之前,也有可能在初始化之後。)[JVM 針對類或接口、字段、類方法、接口方法、方法類型方法句柄和調用點限定符 7 類引用進行解析。這個階段的主要任務是將其在常量池中的符號引用替換成直接其在內存中的直接引用]
  • 初始化(初始化時類加載的最後一步,前面的類加載過程,除了在加載階段用戶應用程序可以通過自定義類加載器參與之外,其餘動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的 Java 程序代碼。)

    遇到 new、getstatic、putstatic、invokestatic 這四條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:

    • 使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯器把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
    • 使用 java.lang.reflect 包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
    • 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
    • 當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
    • 當使用 JDK1.7 動態語言支持時,如果一個 java.lang.invoke.MethodHandle實例最後的解析結果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,並且這個方法句柄所對應的類沒有進行初始化,則需要先出觸發其初始化。
  • 使用(當 JVM 完成初始化階段之後,JVM 便開始從入口方法開始執行用戶的程序代碼)
  • 卸載(當用戶程序代碼執行完畢後,JVM 便開始銷燬創建的 Class 對象,最後負責運行的 JVM 也退出內存)

什麼是java垃圾回收機制?

在 java 中,程序員是不需要顯示的去釋放一個對象的內存的,而是由虛擬機自行執行。在
JVM 中,有一個垃圾回收線程,它是低優先級的,在正常情況下是不會執行的,只有在虛
擬機空閒或者當前堆內存不足時,纔會觸發執行,掃描那些沒有被任何引用的對象,並將
它們添加到要回收的集合中,進行回收;

JVM內存分哪幾個區,每個區的作用是什麼?

  • 方法區(永久代)[線程共享]:
  1. 有時候也成爲永久代,在該區內很少發生垃圾回收,但是並不代表不發生 GC,在這裏
    進行的 GC 主要是對方法區裏的常量池和對類型的卸載
  2. 方法區主要用來存儲已被虛擬機加載的類的信息、常量、靜態變量和即時編譯器編譯後的代碼等數據。
  3. 該區域是被線程共享的。
  4. 方法區裏有一個運行時常量池,用於存放靜態編譯產生的字面量和符號引用。該常量池
    具有動態性,也就是說常量並不一定是編譯時確定,運行時生成的常量也會存在這個常量
    池中。
  • 虛擬機棧[線程數據隔離]
  1. 虛擬機棧也就是我們平常所稱的棧內存,它爲 java 方法服務,每個方法在執行的時候都會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接和方法出口等信息。
  2. 虛擬機棧是線程私有的,它的生命週期與線程相同。
  3. 局部變量表裏存儲的是基本數據類型、returnAddress 類型(指向一條字節碼指令的地
    址)和對象引用,這個對象引用有可能是指向對象起始地址的一個指針,也有可能是代表
    對象的句柄或者與對象相關聯的位置。局部變量所需的內存空間在編譯器間確定
  4. 操作數棧的作用主要用來存儲運算結果以及運算的操作數,它不同於局部變量表通過索
    引來訪問,而是壓棧和出棧的方式
  5. 每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了
    支持方法調用過程中的動態連接.動態鏈接就是將常量池中的符號引用在運行期轉化爲直接
    引用。
  • 本地方法棧[線程數據隔離]
  1. 本地方法棧和虛擬機棧類似,只不過本地方法棧爲 Native 方法服務;
  • 堆[線程共享]
  1. java 堆是所有線程所共享的一塊內存,在虛擬機啓動時創建,幾乎所有的對象實例都在這
    裏創建,因此該區域經常發生垃圾回收操作。
  • 程序計數器
  1. 內存空間小,字節碼解釋器工作時通過改變這個計數值可以選取下一條需要執行的字節碼
    指令,分支、循環、跳轉、異常處理和線程恢復等功能都需要依賴這個計數器完成。該內存區域是唯一一個 java 虛擬機規範沒有規定任何 OOM 情況的區域。

Hotspot JVM 後臺運行的系統線程主要有下面幾個:

線程 描述
虛擬機線程 (VM thread)這個線程等待 JVM 到達安全點操作出現。這些操作必須要在獨立的線程裏執行,因爲當堆修改無法進行時,線程都需要 JVM 位於安全點。這些操作的類型有:stop-the-world 垃圾回收、線程棧 dump、線程暫停、線程偏向鎖(biased locking)解除。
GC 線程 這些線程支持 JVM 中不同的垃圾回收活動
編譯器線程 這些線程在運行時將字節碼動態編譯成本地平臺相關的機器碼。
信號分發線程 這個線程接收發送到 JVM 的信號並調用適當的 JVM 方法處理。

JVM的基礎總結

在這裏插入圖片描述

JVM的內存區域

在這裏插入圖片描述

  • JVM 內存區域主要分爲線程私有區域【程序計數器、虛擬機棧、本地方法區】、線程共享區
    域【JAVA 堆、方法區】、直接內存
  • 線程私有數據區域生命週期與線程相同, 依賴用戶線程的啓動/結束 而 創建/銷燬(在 Hotspot
    VM 內, 每個線程都與操作系統的本地線程直接映射, 因此這部分內存區域的存/否跟隨本地線程的
    生/死對應)
  • 線程共享區域隨虛擬機的啓動/關閉而創建/銷燬
  • 直接內存並不是 JVM 運行時數據區的一部分, 但也會被頻繁的使用: 在 JDK 1.4 引入的 NIO 提
    供了基於 Channel 與 Buffer 的 IO 方式, 它可以使用 Native 函數庫直接分配堆外內存, 然後使用
    DirectByteBuffer 對象作爲這塊內存的引用進行操作(詳見: Java I/O 擴展), 這樣就避免了在 Java
    堆和 Native 堆中來回複製數據, 因此在一些場景中可以顯著提高性能

JVM的模型

在這裏插入圖片描述

jvm堆運行時內存

在這裏插入圖片描述

  • 新生代

是用來存放新生的對象。一般佔據堆的1/3空間。由於頻繁創建對象,所以新生代會頻繁觸發
MinorGC 進行垃圾回收。新生代又分爲 Eden 區、ServivorFrom、ServivorTo 三個區

  1. Eden區

    Java新對象的出生地(如果新創建的對象佔用內存很大,則直接分配到老
    年代)。當Eden區內存不夠的時候就會觸發MinorGC,對新生代區進行
    一次垃圾回收

  2. ServivorFrom

    上一次 GC 的倖存者,作爲這一次 GC 的被掃描者

  3. ServivorTo

    保留了一次 MinorGC 過程中的倖存者(上一次 GC 的倖存者,作爲這一次 GC 的被掃描者)

  • 老年代

主要存放應用程序中生命週期長的內存對象

老年代的對象比較穩定,所以 MajorGC 不會頻繁執行。在進行 MajorGC 前一般都先進行
了一次 MinorGC,使得有新生代的對象晉身入老年代,導致空間不夠用時才觸發。當無法找到足
夠大的連續空間分配給新創建的較大對象時也會提前觸發一次 MajorGC 進行垃圾回收騰出空間

  • 永久代

指內存的永久保存區域,主要存放 Class 和 Meta(元數據)的信息,Class 在被加載的時候被
放入永久區域,它和和存放實例的區域不同,GC 不會在主程序運行期對永久區域進行清理。所以這
也導致了永久代的區域會隨着加載的 Class 的增多而脹滿,最終拋出 OOM 異常;

  1. 新特性1.8

    在Java8中,永久代已經被移除,被一個稱爲“元數據區”(元空間)的區域所取代。元空間
    的本質和永久代類似,元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用
    本地內存。因此,默認情況下,元空間的大小僅受本地內存限制。類的元數據放入 native
    memory, 字符串池和類的靜態變量放入 java 堆中,這樣可以加載多少類的元數據就不再由
    MaxPermSize 控制, 而由系統的實際可用空間來控制


  • 延伸
  1. MinorGC【 採用複製算法】(新生代內存不夠用時候發生 MGC 也叫 YGC)

    MinorGC 的過程(複製->清空->互換)
    from區與to區的角色互換(上一次 GC 的倖存者,作爲這一次 GC 的被掃描者)
    1 : eden 、 servicorFrom 複製到 ServicorTo,年齡+1
    首先,把 Eden和 ServivorFrom區域中存活的對象複製到 ServicorTo區域(如果有對象的年
    齡以及達到了老年的標準,則賦值到老年代區),同時把這些對象的年齡+1(如果 ServicorTo 不
    夠位置了就放到老年區);

    2 : 清空 eden 、 servicorFrom
    然後,清空 Eden 和 ServicorFrom 中的對象;

    3 : ServicorTo 和 ServicorFrom 互換
    最後,ServicorTo 和 ServicorFrom 互換,原 ServicorTo 成爲下一次 GC 時的 ServicorFrom區

  2. MajorGC【採用標記清除算法】(清理老年代)

    首先掃描一次所有老年代,標記出存活的對象,然後回收沒
    有標記的對象。MajorGC 的耗時比較長,因爲要掃描再回收。MajorGC 會產生內存碎片,爲了減
    少內存損耗,我們一般需要進行合併或者標記出來方便下次直接分配。當老年代也滿了裝不下的
    時候,就會拋出 OOM(Out of Memory)異常

  3. Full GC 是清理整個堆空間—包括年輕代和老年代(JVM 內存不夠的時候發生 FGC)


JVM垃圾回收機制的演變

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

如和判斷一個對象是否存活?(或者 GC 對象的判定方法)

  • 引用計數法

對象被引用就加一,引用失效就減一,當爲零時,也被稱爲死對象,將會被回收

所謂引用計數法就是給每一個對象設置一個引用計數器,每當有一個地方引用這個對象
時,就將計數器加一,引用失效時,計數器就減一。當一個對象的引用計數器爲零時,說
明此對象沒有被引用,也就是“死對象”,將會被垃圾回收.

引用計數法有一個缺陷就是無法解決循環引用問題,也就是說當對象 A 引用對象 B,對象
B 又引用者對象 A,那麼此時 A,B 對象的引用計數器都不爲零,也就造成無法完成垃圾回
收,所以主流的虛擬機都沒有采用這種算法。
  • 可達性算法(引用鏈法)[也就是上面提到的根搜索算法]

根據GCRoots的對象向下搜索,沒有任何引用鏈時,認爲死對象.

從一個被稱爲 GC Roots 的對象開始向下搜索,如果一個對象到 GC
Roots 沒有任何引用鏈相連時,則說明此對象不可用
在 java 中可以作爲 GC Roots 的對象有以下幾種:

  • 虛擬機棧中引用的對象
  • 方法區類靜態屬性引用的對象
  • 方法區常量池引用的對象
  • 本地方法棧 JNI 引用的對象

雖然這些算法可以判定一個對象是否能被回收,但是當滿足上述條件時,這個對象不一
定會被回收
。當一個對象不可達 GC Root 時,這個對象並
不會立馬被回收,而是出於一個死緩的階段,若要被真正的回收需要經歷兩次標記
如果對象在可達性分析中沒有與 GC Root 的引用鏈,那麼此時就會被第一次標記並且進行
一次篩選,篩選的條件是是否有必要執行 finalize()方法。當對象沒有覆蓋 finalize()方法
或者已被虛擬機調用過,那麼就認爲是沒必要的。
如果該對象有必要執行 finalize()方法,那麼這個對象將會放在一個稱爲 F-Queue 的對隊
列中,虛擬機會觸發一個 Finalize()線程去執行,此線程是低優先級的,並且虛擬機不會承
諾一直等待它運行完,這是因爲如果 finalize()執行緩慢或者發生了死鎖,那麼就會造成 F-
Queue 隊列一直等待,造成了內存回收系統的崩潰。GC 對處於 F-Queue 中的對象進行
第二次被標記,這時,該對象將被移除”即將回收”集合,等待回收。

GC垃圾收集器

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

JVM的內存模型演變

在這裏插入圖片描述

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