Java JVM

前言

JVM是Java的重要組成部分,對于我這個Cpper轉Javaer也需要認真學習才對。

一、JVM內存結構

JVM內存空間
程序計數器
Java虛擬機棧
本地方法棧
方法區

在這里插入圖片描述
JDK 1.8 同 JDK 1.7 比,最大的差別就是:元數據區取代了永久代。元空間的本質和永久代類似,都是對 JVM 規范中方法區的實現。不過元空間與永久代之間最大的區別在于:元數據空間并不在虛擬機中,而是使用本地內存。

1、程序計數器(PC寄存器)

程序計數器:一塊較小的內存空間,是當前線程正在執行的那條字節碼指令的地址。若當前線程正在執行的是一個本地方法,那么此時程序計數器為Undefined

程序計數器的作用:

  • 字節碼解釋器通過改變程序計數器來依次讀取指令,從而實現代碼的流程控制。
  • 在多線程情況下,程序計數器記錄的是當前線程執行的位置,從而當線程切換回來時,就知道上次線程執行到哪了。

程序計數器的特點:

  • 是一塊較小的內存空間。
  • 線程私有,每條線程都有自己的程序計數器。
  • 生命周期:隨著線程的創建而創建,隨著線程的結束而銷毀。
  • 是唯一一個不會出現 OutOfMemoryError 的內存區域。

2、Java虛擬機棧

Java虛擬機棧:Java方法運行過程中的內存模型。
Java虛擬機棧會為每一個即將運行的Java方法創建一塊叫做“棧幀”的區域,用于存放該方法運行過程中的一些信息,如:

  • 局部變量表
  • 操作數棧
  • 動態鏈接
  • 方法返回地址

棧空間雖然不是無限的,但一般正常調用的情況下是不會出現問題的。不過,如果函數調用陷入無限循環的話,就會導致棧中被壓入太多棧幀而占用太多空間,導致棧空間過深。那么當線程請求棧的深度超過當前 Java 虛擬機棧的最大深度的時候,就拋出 StackOverFlowError 錯誤。
棧幀隨著方法調用而創建,隨著方法結束而銷毀。無論方法正常完成還是異常完成都算作方法結束。
虛擬機棧可能出現的兩種錯誤:

  • StackOverFlowError
  • OutOfMemoryError

3、本地方法棧(C棧)

本地方法棧則為虛擬機使用native方法服務。 在HotSpot虛擬機中和Java虛擬機合二為一。

棧幀變化過程:
本地方法被執行時,在本地方法棧也會創建一塊棧幀,用于存放該方法的局部變量、操作數棧、動態鏈接、方法出口信息等。
方法執行結束后,相應的棧幀也會出棧,并釋放內存空間。也會拋出StackOverFlowError和OutOfMemoryError異常。

如果Java虛擬機本身不支持Natvie方法,或是本身不依賴于傳統棧,那么也不提供本地方法棧。如果支持本地方法棧,那么這個棧一般會在線程創建的時候按線程分配。

4、堆(認真復習,這一塊好復雜)

此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例以及數組都在這里分配內存。
從 JDK 1.7 開始已經默認開啟逃逸分析,如果某些方法中的對象引用沒有被返回或者未被外面使用(也就是未逃逸出去),那么對象可以直接在棧上分配內存。
在這里插入圖片描述

堆的特點
  • 線程共享,整個Java虛擬機只有一個堆,所有的線程都訪問同一個堆。
  • 在虛擬機啟動時創建。
  • 是垃圾回收的主要場所。
  • 堆可分為新生代(Eden區:From SurviorTo Survior)、老年代。
  • Java虛擬機規范規定,堆可以處于物理上不連續的內存空間,但在邏輯上它應該被視為連續的。
  • 關于Surviror s0,s1區:復制之后有交換,誰空誰是to。

不同的區域存放不同的生命周期的對象,這樣可以根據不同的區域使用不同的垃圾回收算法,更具有針對性。
堆的大小既可以固定地也可以擴展,但對于主流的虛擬機,堆的大小是可擴展的,因為當線程請求分配內存,單堆已滿,且內存已無法再擴展時,就拋出OutOfMemoryError異常。

新生代與老年代
  • 老年代比新生代生命周期長。、
  • 新生代與老年代空間默認比例1:2:JVM調參數,XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整個堆的1/3。
  • HotSpot中,Eden空間和另外兩個Survivor空間缺省所占的比例是8:1:1
  • 幾乎所有的Java對象都是在Eden區被new出來的,Eden放不下的大對象,就直接進入老年代了。
對象分配過程
  • new的對象先放在Eden區,大小有限制;
  • 如果創建新對象時,Eden空間填滿了,就會觸發Minor GC,將Eden不再被其他對象引用的對象進行銷毀,再加載新的對象放到Eden區,特別注意的是Survivor區滿了是不是觸發Minor GC的,而是Eden空間填滿了,Minor GC才順便清理Survivor區,將Eden中剩余的對象移到Survivor0區
  • 再次觸發垃圾回收,此時上次Survivor下來的,放在Survivor0區的,如果沒有回收,就會放到Survivor1區
  • 再次經歷垃圾回收,又會將幸存者重新放回Survivor0區,依次類推
  • 默認是15次的循環,超過15次,則會將幸存者區幸存下來的轉去老年區,jvm參數設置次數:-XX:MaxTenuringThreshold=N進行設置
  • 頻繁在新生區收集,很少在老年區收集,幾乎不在永久區/元空間收集
Full GC/Major GC觸發條件

目前已出現的三種GC方式:Major GC、Minor GC、Full GC

  • 顯示調用System.gc(),老年代的空間不夠,方法區的空間不夠等都會觸發Full GC,同時對新生代和老年代回收,Full GC的STW的時間最長,應該要避免
  • 在出現Major GC之前,回顯觸發Minor GC,如果老年代的空間還是不夠就會觸發Major GC,STW的時間長度Minor GC
逃逸分析
標量替換
  • 標量不可在分解的量,java 的基本數據類型就是標量,標量的對立就是可以被進一步分解的量,而這種量稱之為聚合量。而在 JAVA 中對象就是可以被進一步分解的聚合量
  • 替換過程,通過逃逸分析確定該對象不會被外部訪問,并且對象可以被進一步分解時,JVM 不會創建該對象,而會將該對象成員變量分解若干個被這個方法使用的成員變量所代替。這些代替的成員變量在棧幀或寄存器上分配空間。
  • 使用逃逸分析,編譯器可以對代碼做如下優化:
    • 同步省略:如果一個對象被發現只能從一個線程被訪問到,那么對于這個對象的操作可以不考慮同步。
    • 將堆分配轉化為棧分配:如果一個對象在子程序中被分配,要使指向該對象的指針永遠不會逃逸,對象可能是棧分配的候選,而不是堆分配。
    • 分離對象或標量替換:有的對象可能不需要作為一個連續的內存結構存在也可以被訪問到,那么對象的部分(或全部)可以不存儲在內存,而是存儲在 CPU 寄存器中。
TLAB

TLAB的全稱是Thread Local Allocation Buffer,即線程本地分配緩存去,是屬于Eden區的,這是一個線程專用的內存分配區域,線程私有,默認開啟的(當然也不是絕對的,也要看哪種類型的虛擬機)
當然并不是所有的對象都可以在 TLAB 中分配內存成功,如果失敗了就會使用加鎖的機制來保持操作的原子性
-XX:+UseTLAB使用 TLAB,-XX:+TLABSize 設置 TLAB 大小

5、方法區

Java 虛擬機規范中定義方法區是堆的一個邏輯部分。

方法區
已經被虛擬機加載的類信息
常量
運行時常量池
靜態變量
即時編譯器編譯后的代碼
方法區的特點
  • 線程共享。方法區是堆的一個邏輯部分,因此和堆一樣,都是線程共享的。整個虛擬機中只有一個方法區。
  • 永久代。方法區中的信息一般需要長期存在,而且它又是堆的邏輯分區,因此用堆的劃分方法,把方法區稱為"永久代"。
  • 內存回收效率低。方法區中的信息一般需要長期存在,回收一遍之后可能只有少量信息無效。主要回收目標是:對常量池的回收;對類型的卸載。
  • Java虛擬機規范對方法區的要求比較寬松。和堆一樣,允許固定大小,也允許動態擴展,還允許不實現垃圾回收。
運行時常量池

常量就存放在運行時常量池內。
當類被Java虛擬機加載后,.class文件中的常量就存放在方法區的運行時常量池中。而且在運行期間,可以向常量池中添加新的常量。如String類的intern()方法就能在運行期間向常量池中添加字符串常量。
在這里插入圖片描述

6、直接內存(堆外內存)

操作直接內存

在NIO中引入了一種基于通道和緩沖的IO方式。它可以通過調用本地方法直接分配Java虛擬機之外的內存,然后通過一個存儲在堆中的DirectByteBuffer對象直接操作該內存,而無須先將外部內存中的數據復制到堆中再進行操作,從而提高了數據操作的效率。
直接內存的大小不受Java虛擬機控制,但既然是內存,當內存不足時就會拋出OutOfMemoryError異常。

直接內存與堆內存比較
  • 直接內存申請控件耗費更多的性能
  • 直接內存讀取IO的性能要優化普通的堆內存
  • 直接內存作用鏈:本地IO -> 直接內存 -> 本地IO
  • 堆內存作用鏈: 本地IO -> 直接內存 -> 非直接內存 -> 直接內存 -> 本地IO

在這里插入圖片描述

二、HotSpot 虛擬機對象探秘

對象的內存布局

內存布局
對象頭
哈希碼
GC分代年齡
鎖狀態標志
線程持有的鎖
偏向線程ID
偏向時間戳
實例數據
對齊填充

在這里插入圖片描述
對象頭可能包含類型指針,通過該指針能確定對象屬于哪個類。如果對象是一個數組,那么對象頭還會包括數組長度。
實例數據部分就是成員變量的值,其中包括父類成員變量和本類成員變量。
對齊填充用于確保對象的總長度為 8 字節的整數倍。

對象的創建過程

在這里插入圖片描述
類加載檢查:
虛擬機在解析.class文件時,若遇到一條 new 指令,首先它會去檢查常量池中是否有這個類的符號引用,并且檢查這個符號引用所代表的類是否已被加載、解析和初始化過。如果沒有,那么必須先執行相應的類加載過程。

為新生對象分配內存,分配堆中內存有兩種方式:

  • 指針碰撞
  • 空閑列表

初始化:
分配完內存后,為對象中的成員變量賦上初始值,設置對象頭信息,

對象的訪問方式

所有對象的存儲控件都是在堆中分配的,但是這個對象的引用卻是在堆棧中分配的。
也就是說在建立一個對象時需要兩個地方都分配內存,在堆中分配的內存實際建立這個對象,而在堆棧中分配的內存知識一個指向這個堆對象的指針(引用)而已。

對象的訪問方式
句柄訪問方式
直接指針訪問方式,HotSpot采用此類型

句柄訪問方式:
在這里插入圖片描述
直接指針訪問方式:
在這里插入圖片描述

三、垃圾收集策略與算法

垃圾收集主要是針對方法區進行;程序計數器、虛擬機棧和本地方法棧這三個區域屬于線程私有的,只存在于線程的生命周期內,線程結束之后也會消失,因此不需要對這三個區域進行垃圾回收。

判斷對象是否存活

若一個對象不被任何對象或變量引用,那么它就是無效對象,需要被回收。這個地方的實現類似于C++的指針指針,感興趣的同學可以去對比下。

對象頭維護一個counter計數器
優點
缺點
GC Roots不包括堆中對象所引用的對象來解決循環引用問題
判斷對象是否存活
引用計數法
實現簡單,判定效率高
難以解決對象循環引用問題,多線程場景需要執行同步操作
可達性分析法

可達性分析法
所有和 GC Roots 直接或間接關聯的對象都是有效對象,和 GC Roots 沒有關聯的對象就是無效對象。
GC Roots 是指:

  • Java 虛擬機棧(棧幀中的本地變量表)中引用的對象
  • 本地方法棧中引用的對象
  • 方法區中常量引用的對象
  • 方法區中類靜態屬性引用的對象

GC Roots 并不包括堆中對象所引用的對象,這樣就不會有循環引用的問題。

引用的種類

引用的種類
強引用
常見new申請的對象,只要存在GC不會回收被引用的對象
軟引用
JVM認為內存不足時,會嘗試去回收軟引用指向的對象
弱引用
無論內存是否充足,JVM進行垃圾回收時都會回收只被引用關聯的對象
虛引用
一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,在任何時候都可能被回收

回收堆中無效對象

對于可達分析中不可達的對象,也并不是沒有存活的可能。

判定finalize()是否有必要執行

在這里插入圖片描述
JVM會判斷此對象是否有必要執行finalize()方法,如果對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,那么視為"沒有必要執行"。那么對象基本上就針對被回收了。

如果對象被判定為有必要執行finalize()方法,那么對象會被放入一個F-Queue隊列中,虛擬機會以較低的優先級執行這些finalize()方法,但不會確保所有的finalize()方法都會執行結束,如果finalize()方法出現耗時操作,虛擬機就直接停止指向該方法,將對象清除。

對象重生或死亡

如果在執行finalize()方法時,將this賦給了某一個引用,那么該對象就重生了。如果沒有,那么就會被垃圾收集器清除。

任何一個對象的finalize()方法只會被系統自動調用一次,如果對象面臨下一次回收,它的finalize()方法不會被再次執行,想繼續在finalize()中自救就失效了。

回收方法區內存

方法區中存放生命周期較長的類信息、常量、靜態變量,每次垃圾收集只有少量的垃圾被清除。
方法區中主要清除兩種垃圾:

  • 廢棄常量:只要常量池中的常量不被任何變量或對象引用,那么這些常量就會被清除掉。
  • 無用的類:
    • 該類的所有對象都已經被清除
    • 加載該類的ClassLoader已經被回收
    • 該類的java.lang.Class對象沒有在任何地方被引用,無法再任何地方通過反射訪問該類的方法

    一個類被虛擬機加載進方法區,那么在堆中就會有一個代表該類的對象:java.lang.Class。這個對象在類被加載進方法區時創建,在方法區該類被刪除時清除。

垃圾收集算法

學習了如何判定無效對象、無用類、廢棄常量之后,剩余工作就是回收這些垃圾。
常見的垃圾算法有以下幾個:

垃圾回收算法
標記-清除算法
復制算法
標記-整理算法
分代收集算法
標記-清除算法

在這里插入圖片描述
標記的過程是:遍歷所有的 GC Roots,然后將所有 GC Roots 可達的對象標記為存活的對象
清除的過程將遍歷堆中所有的對象,將沒有標記的對象全部清除掉。與此同時,清除哪些被標記過對象的標記,以便下次的垃圾回收。

缺點:

  • 效率問題:標記和清除兩個過程的效率都不高;
  • 空間問題:標記清除之后會產生大量不連續的內存碎片,碎片太多可能導致以后需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
復制算法(新生代)

在這里插入圖片描述
為了解決效率問題,“復制”收集算法出現了。它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊內存用完,需要進行垃圾收集時,就將存活者的對象復制到另一塊上面,然后將第一塊內存全部清除。這種算法有優有劣:

  • 優點:不會有內存碎片問題
  • 缺點:內存縮小為原來的一半,浪費空間

為了解決空間利用率問題,可以將內存分為三塊:Eden、From Survivor、To Survivor,比例是8:1:1,每次使用Eden和其中一塊Survivor。回收時,將Eden和Survivor中還存活的對象一次性復制到另外一塊Survivor空間上,最后清理掉Eden和剛才使用的Survivor空間。這樣只有10%的內存被浪費。
但是我們無法保證每次回收都只有不多于10%的對象存活,當Survivor空間不夠,需要依賴其他內存(指老年代)進行分配擔保。

分配擔保

為對象分配內存空間時,如果Eden+Survivor中空閑區域無法裝下該對象,會觸發MinorGC進行垃圾收集。但如果Minor GC過后依然有超過10%的對象存活,這樣存活的對象直接通過分配擔保機制進入老年代,然后再講新對象存入Eden區。

標記-整理算法(老年代)

在這里插入圖片描述
標記:它的第一個階段與標記-清除算法是一模一樣的,均是GC Roots,然后將存活的對象標記。
整理:移動所有存活的對象,且按照內存地址次序依次排列,然后將末端內存地址以后的內存全部回收。因此,第二階段才成為整理階段。

這是一種老年代的垃圾收集算法。老年代的對象一般壽命比較長,因此每次垃圾回收會有大量對象存活,如果采用復制算法,每次需要復制存活的對象,效率很低。

分代收集算法

根據對象存活周期的不同,將內存劃分為幾塊。一般是把Java堆分為新生代和老年代,針對各個年代的特點采用最適當的收集算法。

  • 新生代:復制算法
  • 老年代:標記-清除算法、標記-整理算法

四、HotSpot垃圾收集器

HotSpot 虛擬機提供了多種垃圾收集器,每種收集器都有各自的特點:

單線程
多線程
多線程
單線程
多線程
多線程
垃圾收集器
新生代垃圾收集器
Serial垃圾收集器
ParNew垃圾收集器
Parallel Scavenge垃圾收集器
老年代垃圾收集器
Serial Old垃圾收集器
Parallel Old 垃圾收集器
CMS垃圾收集器
G1通用垃圾收集器

新生代垃圾收集器

Serial 垃圾收集器(單線程)

只開啟一條 GC 線程進行垃圾回收,并且在垃圾收集過程中停止一切用戶線程,即 Stop The World。Serial 垃圾收集器適合客戶端使用。

ParNew 垃圾收集器(多線程)

ParNew 是 Serial 的多線程版本。由多條 GC 線程并行地進行垃圾清理。但清理過程依然需要 Stop The World。
ParNew 追求“低停頓時間”,與 Serial 唯一區別就是使用了多線程進行垃圾收集,在多 CPU 環境下性能比 Serial 會有一定程度的提升;但線程切換需要額外的開銷,因此在單 CPU 環境中表現不如 Serial。

Parallel Scavenge 垃圾收集器(多線程)

Parallel Scavenge 和 ParNew 一樣,都是多線程、新生代垃圾收集器。但是兩者有巨大的不同點:

  • Parallel Scavenge:追求CPU吞吐量,能夠在較短時間內完成指定任務,因此適合沒有交互的后臺計算。
  • ParNew:追求降低用戶停頓時間,適合交互式應用。
    吞吐量 = 運行用戶代碼時間 +(運行用戶代碼時間 + 垃圾收集時間)

追求高吞吐量,可通過減少GC執行實際過程的時間,然而,僅僅偶爾運行GC意味著每當GC運行時將有許多工作要做,因為在此期間積累了堆中的對象數量很高。,單個GC需要花更多的時間來完成,從而導致更高的暫停時間。而考慮到低暫停時間,最好頻繁運行GC以便更快速完成,反過來有導致吞吐量下降。

  • 通過參數 -XX:GCTimeRadio 設置垃圾回收時間占總 CPU 時間的百分比。
  • 通道參數 -XX:MaxGCPauseMills設置垃圾處理過程最久停頓時間。
  • 通過命令 -XX:+UseAdaptiveSizePolicy 開啟自適應策略。我們只要設置好堆的大小和MaxGCPauseMillis或GCTimeRadio,收集器會自動調整新生代的大小、Eden和Survivor的比例、對象進入老年代和年齡,以最大程度上接近我們設置的MaxGCPauseMills或GCTimeRadio。

老年代垃圾收集器

Serial Old 垃圾收集器(單線程)

Serial Old 收集器是 Serial 的老年代版本,都是單線程收集器,只啟用一條 GC 線程,都適合客戶端應用。它們唯一的區別就是:Serial Old 工作在老年代,使用“標記-整理”算法;Serial 工作在新生代,使用“復制”算法。

Parallel Old 垃圾收集器(多線程)

Parallel Old 收集器是 Parallel Scavenge 的老年代版本,追求 CPU 吞吐量。

CMS垃圾收集器

CMS(Concurrent Mark Sweep,并發標記清除)收集器是以獲取最短回收停頓時間為目標的收集器(追求低停頓),它在垃圾收集時使得用戶線程和GC線程并發執行,因此在垃圾收集過程中用戶也不會感到明顯的卡頓。

  • 初始標記:Stop The World,僅使用一條初始標記線程對所有與GC Roots直接關聯的對象進行標記。
  • 并發標記:使用多條標記線程,與用戶線程并發執行。此過程進行可達性分析,標記出所有廢棄對象,速度很慢。
  • 重新標記:Stop The World,使用多條標記線程并發執行,將剛才并發標記過程中新出現的廢棄對象標記出來。
  • 并發清除:只使用一條GC線程,與用戶線程并發執行,清除剛才標記的對象。這個過程非常耗時。

并發標記與并發清除過程耗時最長,且可以與用戶一起工作,因此,總體來說,CMC收集器的內存回收過程是與用戶線程一起并發執行的。
在這里插入圖片描述
CMS的缺點:

  • 吞吐量低
  • 無法處理浮動垃圾
  • 使用“標記-清除”算法產生碎片空間,導致頻繁Full GC

對于產生碎片空間問題,可以通過開啟 -XX:+UseCMSCompactAtFullCollection,在每次 Full GC 完成后都會進行一次內存壓縮整理,將零散在各處的對象整理到一塊。
設置參數-XX:CMSFullGCsBeforeCompaction 告訴 CMS,經過了 N 次 Full GC 之后再進行一次內存整理。

G1通用垃圾收集器

G1是一款面向服務端應用的垃圾收集器,它沒有新生代和老年代的概念,而是將堆劃分為一塊塊獨立的Region。當要進行垃圾收集時,首先估計每個Region中垃圾的數量,每次都從垃圾回收價值最大的Region開始回收,因此可以獲得更大的回收效率。

從整體上看,G1 是基于“標記-整理”算法實現的收集器,從局部(兩個 Region 之間)上看是基于“復制”算法實現的,這意味著運行期間不會產生內存空間碎片。
每個 Region 都有一個 Remembered Set,用于記錄本區域中所有對象引用的對象所在的區域,進行可達性分析時,只要在 GC Roots 中再加上 Remembered Set 即可防止對整個堆內存進行遍歷。

如果不計算維護Remembered Set的操作,G1收集器的工作過程分為以下幾個步驟:

  • 初始標記:Stop The World,僅使用一條初始標記線程對所以后與GC Roots直接關聯的對象進行標記。
  • 并發標記:使用一條標記線程與用戶線程并發執行。此過程進行可達性分析,速度很慢。
  • 最終標記:Stop The World,使用多條標記線程并發執行。
  • 篩選回收:回收廢棄對象,此時也要Stop The World,并使用多條篩選回收線程并發執行。

五、內存分配與回收策略

對象的內存分配,就是堆上分配(也可能經過JIT編譯后備拆散為標量類型并間接在棧上分配),對象主要分配在新生代的Eden區上,少數情況下可能直接分配在老年代,分配規則不固定,取決于當前使用的垃圾收集器組合以及相關的參數配置。
以下為普遍的內存分配規則:

  • 對象優先在Eden分配
  • 大對象直接進入老年代:-XX:PretenureSizeThreshold 參數,令大于這個設置值的對象直接在老年代分配
  • 長期存活的對象將進入老年代: -XXMaxTenuringThreshold 設置新生代的最大年齡
  • 動態對象年齡判定
  • 空間分配擔保

可能會觸發JVM進行Full GC的情況

  • System.gc()方法的調用:此方法的調用時建議JVM進行Full GC,注意這只是建議而非一定,但在很多情況下它會觸發Full GC,從而增加Full GC的頻率。通常情況下我們只需要讓虛擬機自己去管理內存即可,我們可以通過 -XX:+ DisableExplicitGC 來禁止調用System.gc()
  • 老年代控件不足:老年代控件不足會觸發Full GC操作,若進行該操作后空間依然不足,則會拋出如下處理java.lang.OutOfMemoryError: Java heap space
  • 永久代空間不足:JVM規范中運行時數據區域中的方法區,在HotSpot虛擬機中已也稱為永久代,永久代可能會被占滿,會觸發 Full GC。如果經過 Full GC 仍然回收不了,那么 JVM 會拋出如下錯誤信息:java.lang.OutOfMemoryError: PermGen space
  • CMS GC時出現promotion failed concurrent mode failure promotion failed,就是上文所說的擔保失敗,而 concurrent mode failure 是在執行 CMS GC 的過程中同時有對象要放入老年代,而此時老年代空間不足造成的。
  • 統計得到的Minor GC晉升到舊生代的平均大小大于老年代的剩余空間。

六、JVM性能調優(后期還得看)

在高性能硬件上部署程序,目前主要有兩種方式:

  • 通過64位JDK來使用大內存;
  • 使用若干個32為虛擬機建立邏輯集群來利用硬件資源。

使用 64 位 JDK 管理大內存

++++

使用 32 位 JVM 建立邏輯集群

++++

調優案例分析與實戰

++++

七、類文件結構

JVM的“無關性”

談論JVM的無關性,主要有以下兩個:

  • 平臺無關性:任何操作系統都能運行Java代碼
  • 語言無關性:JVM能運行除Java以外的其他代碼

Class文件結構

Class文件是二進制文件,它的內容具有嚴格的規范,文件中沒有任何空格,全都是連續的0/1。
Class文件中的所有內容被分為兩種類型:無符號數、表。

  • 無符號數:無符號數表示Class文件中的值,這些值沒有任何類型,但有不同的長度。u1、u2、u4、u8分別代表1/2/4/8字節的無符號數。
  • 表由多個符號數或者其他表作為數據項構成的符合數據類型。
Class文件結構
魔數
版本信息
常量池
訪問標志
類索引/父類索引/接口索引集合
字符表集合
方法表集合
屬性表集合

魔數

Class 文件的頭 4 個字節稱為魔數,用來表示這個 Class 文件的類型。Class 文件的魔數是用 16 進制表示的“CAFE BABE”。

版本信息

緊接著魔數的 4 個字節是版本信息,5-6 字節表示次版本號,7-8 字節表示主版本號,它們表示當前 Class 文件中使用的是哪個版本的 JDK。
高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能運行以后版本的 Class 文件,即使文件格式并未發生任何變化,虛擬機也必須拒絕執行超過其版本號的 Class 文件。

常量池

版本信息之后就是常量池,常量池中存放兩種類型的常量:

  • 字面值常量:定義的字符串、被final修飾的值
  • 符號引用:類和接口的全限定名、字段的名字和描述符、方法的名字和描述符
常量池的特點
  • 常量池中常量數量不固定,因此常量池開頭放置一個u2類型的無符號數,用于存儲當前常量池的容量。
  • 常量池的每一項常量都是一個表,表開頭的第一位是一個u1類型的標志位(tag),代表當前這個常量屬于哪種常量類型。
訪問標志

在常量池結束之后,緊接著的兩個字節代表訪問標志, 這個標志用于識別一些類或者接口層次的訪問信息,包括:這個Class是類還是接口;是否定義為public類型;是否被abstract/final修飾。

類索引、父類索引、接口索引集合

類索引和父類索引都是一個 u2 類型的數據,而接口索引集合是一組 u2 類型的數據的集合,Class 文件中由這三項數據來確定類的繼承關系。類索引用于確定這個類的全限定名,父類索引用于確定這個類的父類的全限定名。

字段表集合

字段表集合存儲本類涉及到的成員變量,包括實例變量和類變量,但不包括方法中的局部變量。

方法表集合

方法表結構與屬性表類似。
volatile關鍵字和transient關鍵字不能修飾方法,所以方法表的訪問標志中沒有ACC_VOLATILE和ACC_TRANSIENT標志。
方法表的屬性表集合中有一張Code屬性表,用于存儲當前方法經編譯器編譯后的字節碼指令。

屬性表集合

每個屬性對應一張屬性表,屬性表的結構如下:

類型名稱數量
u2attribute_name_index1
u4attribute_length1
u1infoattribute_length

八、類加載的時機

類的生命周期

類從被加載到虛擬機內存開始,到卸載出內存位置,它的整個生命周期包括以下7個階段:

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

驗證、準備、解析3個階段統稱為連接
在這里插入圖片描述
加載、驗證、準備、初始化和卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始),而解析階段則不一定:它在某些情況下可以在初始化后再開始,這是為了支持Java語言的運行時綁定。

類加載過程中“初始化”開始的時機

Java虛擬機規范沒有強制約束類加載過程的第一階段(即:加載)什么時候開始,但對于“初始化”階段,有著嚴格的規定。有且僅有5種情況必須立即對類進行“初始化”:

  • 在遇到new、putstaitc、getstatic、invokestatic字節碼指令時,如果類尚未初始化,則需要先觸發其初始化。
  • 對類進行反射調用時,如果類還沒有初始化,則需要先觸發其初始化。
  • 初始化一個類時,如果其父類還沒有初始化,則需要先初始化父類。
  • 虛擬機啟動時,用于需要指定一個包含main()方法的主類,虛擬機會先初始化這個主類。
  • 當使用JDK1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結果為REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄所對應的類還沒初始化,則需要先觸發其初始化。

這5種場景中的行為稱為對一個類的進行主動引用,除此以外,其它所有引用類的方式都不會觸發初始化,稱為被動引用

接口的加載過程

接口加載過程與類加載過程稍有不同。

當一個類在初始化時,要求其父類全部都已經初始化過了,但是一個接口在初始化時,并不要求其父接口全部都完成了初始化,當真正用到父接口的時候才會初始化。

九、類加載的過程

類加載過程包括5個階段:加載、驗證、準備、解析和初始化。

加載

在加載階段,虛擬機需要完成3件事:

  • 通過類的全限定名獲取該類的二進制字符流。
  • 將二進制字節流所代表的靜態結構轉換為方法區的運行時數據結構。
  • 在內存中創建一個代表該類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口。
獲取二進制字符流

對于Class文件,虛擬機沒有指明要從哪里獲取、怎樣獲取,除了直接從編譯的.class文件中讀取,還有以下幾種方式:

  • 從 zip 包中讀取,如 jar、war 等;
  • 從網絡中獲取,如 Applet;
  • 通過動態代理技術生成代理類的二進制字符流;
  • 由JSP文件生成對應的Class類;
  • 從數據庫中讀取,如有些中間件服務器可以選擇把程序安裝到數據庫中來完成程序代碼在集群間的分發。
"非數組類"與"數組類"加載比較
  • 非數組類加載階段可以使用系統提供的引導類加載器,也可以由用戶自定義的類加載器完成,開發人員可以通過定義自己的類加載器控制字節流的獲取方式(如重寫一個類加載器的loadClass()方法)。
  • 數組類本身不通過類加載器創建,它是有Java虛擬機直接創建的,再由類加載器創建數組中的元素類。
注意事項
  • 虛擬機規范未規定 Class 對象的存儲位置,對于 HotSpot 虛擬機而言,Class 對象比較特殊,它雖然是對象,但存放在方法區中。
  • 加載階段與連接階段的部分內容交叉進行,加載階段尚未完成,連接階段可能已經開始了。但這兩個階段的開始時間仍然保持著固定的先后順序。

驗證

驗證階段確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。

準備

準備階段是正式為類變量(或稱“靜態成員變量”)分配內存并設置初始值的階段。這些變量(不包括實例變量)所使用的內存都在方法區中進行分配。

解析

解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。

初始化

類初始化階段是類加載過程的最后一步,是執行類構造器<clinit>()方法的過程。

<clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static {} 塊)中的語句合并產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的。

靜態語句塊中只能訪問定義在靜態語句塊之前的變量,定義在它之后的變量,在前面的靜態語句塊中可以賦值,但不能訪問。
<clinit>()方法不需要顯式調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行之前,父類的<clinit>()方法已經執行完畢。

由于父類的<clinit>()方法先執行,意味著父類中定義的靜態語句塊要優先與子類的變量賦值操作。

<clinit>() 方法不是必需的,如果一個類沒有靜態語句塊,也沒有對類變量的賦值操作,那么編譯器可以不為這個類生成<clinit>()方法。

接口中不能使用靜態代碼塊,但接口也需要通過<clinit>()方法為接口中定義的靜態成員變量顯式初始化。但接口與類不同,接口的<clinit>()方法不需要先執行父類的<clinit>()方法,只有當父接口中定義的變量使用的,父接口才會初始化。

虛擬機會保證一個類的<clinit>()方法在多線程環境中被正常加鎖、同步。如果多個線程同時去初始化一個類,那么只會有一個線程去執行這個類的<clinit>()方法。

十、類加載器

類與類加載器

判斷類是否"相等"

任意一個類,都由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性,每一個類加載器,都有一個獨立的類名稱空間。

因此,比較兩個類是否“相等”,只有在這個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個Class文件,被同一個虛擬機加載,只有加載它們的類加載器不同,那么這兩個類就必定不相等。

這里的“相等”,包括代表類的 Class 對象的 equals() 方法、isInstance() 方法的返回結果,也包括使用 instanceof 關鍵字做對象所屬關系判定等情況。

加載器種類
系統提供3種加載器
啟動類加載器
擴展類加載器
應用程序類加載器
  • 啟動類加載器(Bootstrap ClassLoader):負責將存放在<JAVA_HOME>\lib目錄中的,并且能被虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。
  • 擴展類加載器(Extension ClassLoader):負責加載<JAVA_HOME>\lib\ext目錄中的所有類庫,開發者可以直接使用擴展類加載器。
  • 應用程序類加載器(Application ClassLoader):由于這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為“系統類加載器”。它負責加載用戶類路徑(classpath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
    在這里插入圖片描述
    當然,如果有必要,還可以加入自己定義的類加載器。

雙親委派模型

什么是雙親委派模型

雙親委派模型是描述類加載器之間的層次關系。它要求除了頂層的啟動類加載器外,其余的類加載器都應當有自己的父類加載器。(父子關系一般不會以繼承的關系實現,而是以組合關系來復用父加載器的代碼)

工作過程

如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給輔流加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(找不到所需的類)時,子加載器才會嘗試自己去加載。

在java.lang.ClassLoader中的loadClass方法中實現該過程。

為什么使用雙親委派模型

像java.lang.Object這些存放在rt.jar中的類,無論使用哪個類加載器加載,最終都會委派給最頂端的啟動類加載器加載,從而給使得不同加載器加載的Object類都是同一個。

相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個成為java.lang.Object的類,并放在classpath下,那么系統將會出現多個不同的Object類,Java類型體系中最基礎的行為也就無法保證。

參考

  • JVM相關
  • JVM 底層原理最全知識總結
  • ?JVM相關知識體系詳解?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/90489.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/90489.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/90489.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

便捷刪除Android開發中XML中重復字符串資源的一個辦法

從android系統源碼中移植一些app到android studio開發的時候可能會遇到字符串重復的編譯報錯。一個辦法是把重復的刪除&#xff0c;只剩余一條即可。例如下面的編譯錯誤&#xff1a;Found item String/abc more than one time但是呢&#xff0c;xml中一般這種重復的很多很多&am…

免模型控制

文章目錄免模型控制Q-Learning 算法原理Sarsa 算法區別&#xff1a;免模型控制 免模型控制要解決的問題是&#xff0c;如何選擇動作以達到最高得分 Q-Learning 算法 原理 首先Q-Learning 確定了一個前提最優策略&#xff1a;π(s)arg?max?aQ(s,a)\pi(s) \arg\max_a Q(s,…

Vmware VSAN主機停機維護流程

當VSAN主機由于故障或進行擴容操作需要停機維護時&#xff0c;在關閉ESXi主機前和啟動ESXi主機后需要進行一些必要的檢查操作&#xff0c;以免對vSAN集群環境造成不可預知的風險&#xff0c;影響集群中的虛擬機運行。以下是vSAN集群中的ESXi主機停機維護的主要步驟。 1.確認受影…

中小企業安全落地:低成本漏洞管理與攻擊防御方案

中小企業普遍面臨 “預算有限、技術人員不足” 的困境&#xff0c;安全建設常陷入 “想做但做不起” 的尷尬。事實上&#xff0c;中小企業無需追求 “高大上” 的安全方案&#xff0c;通過 “開源工具 簡化流程 聚焦核心” 的思路&#xff0c;即可用低成本實現有效的漏洞管理…

面試150 搜索二維矩陣

思路1 直接遍歷搜尋&#xff0c;逐個判斷即可 class Solution:def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:mlen(matrix)nlen(matrix[0])for i in range(m):for j in range(n):if matrix[i][j]target:return Truereturn False思路2 Z字形搜索從矩…

npm init vite-app runoob-vue3-test2 ,npm init vue@latest,指令區別

這兩個命令都是用于創建 Vue.js 項目的腳手架命令&#xff0c;但它們在技術棧、配置方式和項目結構上有顯著區別&#xff1a;1. npm init vite-app runoob-vue3-test2技術棧&#xff1a;基于 Vite 構建工具使用 Vue 3 作為默認框架由 Vite 團隊維護特點&#xff1a;bash復制代碼…

WPF MVVM進階系列教程(二、數據驗證)

五一出去浪吹風著涼了&#xff0c;今天有點發燒&#x1f637; 手頭的工作放一放&#xff0c;更新一下博客吧。 什么是數據驗證(Validation) 數據驗證是指用于捕獲非法數值并拒絕這些非法數值的邏輯。 大多數采用用戶輸入的應用都需要有驗證邏輯&#xff0c;以確保用戶已輸入…

AI 音頻產品開發模板及流程(二)

AI 音頻產品開發模板及流程&#xff08;一&#xff09; 6. 同聲傳譯 實時翻譯&#xff0c;發言與翻譯幾乎同步&#xff0c;極大提升溝通效率。支持多語言互譯&#xff0c;適用于國際會議、商務洽談等多場景。自動斷句、轉寫和翻譯&#xff0c;減少人工干預&#xff0c;提升準…

kafka4.0集群部署

kafka4.0是最新版kafka&#xff0c;可在kafka官網下載&#xff0c;依賴的jdk版本要求在jdk17及jdk17以上tar -xzf kafka_2.13-4.0.0.tgzmv kafka_2.13-4.0.0 kafkacd kafka# 隨便一臺節點運行生成隨機uuid&#xff0c;后面每臺節點都要使用此uuidbin/kafka-storage.sh random-u…

【News】同為科技亮相首屆氣象經濟博覽會

7月18日&#xff0c;由中國氣象服務協會主辦的國內首個以“氣象經濟”為核心的國家級博覽會——首屆氣象經濟博覽會&#xff08;以下簡稱“博覽會”&#xff09;在合肥濱湖國際會展中心開幕。北京同為科技有限公司&#xff08;TOWE&#xff09;作為雷電防護領域的技術領導企業&…

數據結構 堆(2)---堆的實現

上篇文章我們詳細介紹了堆和樹的基本概念以及它們之間的關系&#xff0c;還要知道一般實現堆的方式是使用順序結構的數組進行存儲數據及實現。下來我們看看利用順序結構的數組如何實現對的內容:1.堆的實現關于堆的實現&#xff0c;也是三個文件&#xff0c;頭文件&#xff0c;實…

Arraylist與LinkedList區別

&#x1f4da; 歡迎來到我的Java八股文專欄&#xff01; &#x1f389;各位程序員小伙伴們好呀~ &#x1f44b; 我是雪碧聊技術&#xff0c;很高興能在CSDN與大家相遇&#xff01;?&#x1f680; 專欄介紹這個專欄將專注于分享Java面試中的經典"八股文"知識點 &…

Java實戰:基于Spring Cloud的電商微服務架構設計——從拆分到高可用的全流程解析

引言 2023年雙十一大促期間,某傳統電商平臺的單體應用再次“爆雷”:凌晨1點訂單量突破50萬單/分鐘時,用戶服務因數據庫連接池被訂單模塊占滿,導致登錄接口響應時間從200ms飆升至5秒,大量用戶流失。技術團隊緊急回滾后發現:這個運行了7年的單體應用,早已變成“代碼泥潭”…

STL學習(二、vector容器)

1.vector構造函數函數原型vector<int> v // 默認構造&#xff0c;size為0vector(const_iterator beg, const_iterator end) // 將v的[begin, end) 元素拷貝過來vector(n, elem) // 構造函數將n個elem拷貝到本身vector(const vector & v) // 拷貝構造2.vect…

深度學習-算子

概念&#xff1a;標識數字圖像中亮度變化明顯的點處理步驟1.濾波處理算子通常被稱為濾波器。2.增強確定各點sobel算子概念&#xff1a;主要用于獲得數字圖像的一階梯度&#xff0c;本質是梯度運算。Scharr算子Scharr算子 是一種用于邊緣檢測的梯度算子&#xff0c;它是Sobel算子…

全國產8通道250M AD FMC子卡

4片8路ADS42LB69標準FMC采集子卡自研成品ADC采集子卡和定制化設計ADC采集子卡&#xff0c;實測采集指標均與手冊標稱值一致。該板卡有全國產化和進口兩個版本&#xff0c;基于FMC標準設計&#xff0c;實現8路16bit/250MSPS ADC采集功能&#xff0c;遵循 VITA 57 標準&#xff0…

【牛客網C語言刷題合集】(三)

&#x1f31f;菜鳥主頁&#xff1a;晨非辰的主頁 &#x1f440;學習專欄&#xff1a;《C語言刷題集》 &#x1f4aa;學習階段&#xff1a;C語言方向初學者 ?名言欣賞&#xff1a;"任何足夠先進的bug都與魔法無異。" 前言&#xff1a;刷題博客主要記錄在學習編程語言…

Python之--字典

定義字典&#xff08;dict&#xff09;是一種無序、可變且可哈希的數據結構&#xff0c;字典是根據一個信息來查找另一個信息&#xff0c;它表示索引用的鍵和對應的值構成的成對關系。特點&#xff08;1&#xff09;字典與列表一樣&#xff0c;是Python里面的可變數據類型。&am…

【ARM】ARM微架構

1、 文檔目標對 ARM 微架構的概念有初步的了解。2、 問題場景在和客戶溝通和新同事交流時對于 ARM 架構和微架構二者有什么區別和聯系&#xff0c;做一個簡單的介紹。3、軟硬件環境1、軟件版本&#xff1a;不涉及2 、電腦環境&#xff1a;不涉及4、關于 ARM 架構和微架構架構不…

c++注意點(11)----設計模式(工廠方法)

創建型模式工廠方法模式是一種創建型設計模式&#xff0c; 其在父類中提供一個創建對象的方法&#xff0c; 允許子類決定實例化對象的類型。為什么需要工廠方法模式&#xff1f;看一個 “沒有工廠模式” 的痛點場景&#xff1a;假設你在開發一個游戲&#xff0c;最初只有 “戰士…