JVM面試大總結

来源:https://www.cnblogs.com/tianClassmate/archive/2023/01/03/17022178.html
-Advertisement-
Play Games

JVM是運行在操作系統之上的,它與硬體沒有直接的交互。先說一下JVM的記憶體區域,當函數開始運行時,JVM拿到自己的記憶體將自己的記憶體區域進行了分割,分為五塊區域:線程共用的有堆、方法區,線程私有的有java棧、本地方法棧、程式計數器。 ...


一、彙總

JVM是運行在操作系統之上的,它與硬體沒有直接的交互。先說一下JVM的記憶體區域,當函數開始運行時,JVM拿到自己的記憶體將自己的記憶體區域進行了分割,分為五塊區域:線程共用的有堆、方法區,線程私有的有java棧、本地方法棧、程式計數器。

方法區是用來載入class文件的區域,靜態變數、常量、類元信息、運行時的常量池存放在在方法區中, 方法區在jdk1.7之前它又叫做永久代,但是jdk1.8之後改成元數據空間了;

new的對象都存放在堆中;

棧也叫棧記憶體,8種類型的基本變數、對象的引用變數、實例方法都是在函數的棧記憶體中分配,棧中的數據都是以棧幀的格式存在,每執行一個方法都會產生一個棧幀,保存到棧(後進先出)的頂部,頂部棧就是當前的方法,該方法執行完畢後會自動將此棧幀出棧。java棧隨著線程創建而產生,隨著線程的終結而銷毀,每個線程在開闢、運行的過程中會單獨創建這樣的一份記憶體,有多少個線程就可能有多少個棧區;

本地方法棧是存儲C++的native方法運行時候的棧區;程式計數器是指向當前程式運行的位置。

記憶體模型、類載入機制、GC是重點,性能調優部分更偏嚮應用,重點突出實踐能力,編譯器優化和執行模式部分偏向於理論基礎,重點掌握知識點。

image-20221123104653211

1、JMM如何保證原子性、一致性、可見性

在java中提供了兩個高級的位元組碼指令monitorenter和monitorexit,使用對應的關鍵字Synchronized來保證代碼塊內的操作是原子的。

2、環境變數理解

classpath是javac編譯器的一個環境變數。它的作用與import、package關鍵字有關。

package的所在位置,就是設置CLASSPATH當編譯器面對import packag這個語句時,它先會查找CLASSPATH所指定的目錄,並檢視子目錄java/util是否存在,然後找出名稱吻合的已編譯文件(.class文件)。如果沒有找到就會報錯!

二、分區和記憶體模型

記憶體模型叫做記憶體結構。 所謂模型是行為+數據 也就是JVM的記憶體結構佈局,加上記憶體的執行行為,棧中數據如何分配,堆中數據如何分配,堆棧數據運行時如何同步,加鎖狀態數據如何同步,也就是happen before那一套。

1、JVM的劃分及作用

image-20221208093850776

Java虛擬機主要分為以下幾個區:

(1)方法區

​ a. 有時候也成為永久代,在該區內很少發生垃圾回收,但是並不代表不發生GC,在這裡進行的GC主要是對方法區里的常量池和對類型的卸載

​ b. 方法區主要用來存儲已被虛擬機載入的類的信息、常量、靜態變數和即時編譯器編譯後的代碼等數據。

​ c. 該區域是被線程共用的。

​ d. 方法區里有一個運行時常量池,用於存放靜態編譯產生的字面量和符號引用。該常量池具有動態性,也就是說常量並不一定是編譯時確定,運行時生成的常量也會存在這個常量池中。

(2)虛擬機棧:

​ a. 虛擬機棧也就是我們平常所稱的棧記憶體,它為java方法服務,每個方法在執行的時候都會創建一個棧幀,用於存儲局部變數表、操作數棧、動態鏈接和方法出口等信息。

​ b. 虛擬機棧是線程私有的,它的生命周期與線程相同。

​ c. 局部變數表裡存儲的是基本數據類型、returnAddress類型(指向一條位元組碼指令的地址)和對象引用,這個對象引用有可能是指向對象起始地址的一個指針,也有可能是代表對象的句柄或者與對象相關聯的位置。局部變數所需的記憶體空間在編譯器間確定

​ d. 操作數棧的作用主要用來存儲運算結果以及運算的操作數,它不同於局部變數表通過索引來訪問,而是壓棧和出棧的方式

​ e. 每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調用過程中的動態連接.動態鏈接就是將常量池中的符號引用在運行期轉化為直接引用。

(3)本地方法棧
本地方法棧和虛擬機棧類似,只不過本地方法棧為Native方法服務。

(4)堆

java堆是所有線程所共用的一塊記憶體,在虛擬機啟動時創建,幾乎所有的對象實例都在這裡創建,因此該區域經常發生垃圾回收操作。

堆裡面分為新生代和老生代(java8 取消了永久代,採用了 Metaspace),新生代包 含 Eden+Survivor 區,survivor 區裡面分為 from 和 to 區,記憶體回收時,如果用的是複製演算法,從 from 複製到 to,當經過一次或者多次 GC 之後,存活下來的對象會被移動到老年區,當 JVM 記憶體不夠用的時候,會觸發 Full GC,清理 JVM 老年區當新生區滿了之後會觸發 YGC,先把存活的對象放到其中一個 Survice 區,然後進行垃圾清理。

因為如果僅僅清理需要刪除的對象,這樣會導致記憶體碎片,因此一般會把 Eden 進行完全的清理,然後整理記憶體。那麼下次 GC 的時候, 就會使用下一個 Survive,這樣迴圈使用。如果有特別大的對象,新生代放不下,就會使用老年代的擔保,直接放到老年代裡面。因為 JVM 認為,一般大對象的存活時間一般比較久遠。

(5)程式計數器

記憶體空間小,位元組碼解釋器工作時通過改變這個計數值可以選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理和線程恢復等功能都需要依賴這個計數器完成。該記憶體區域是唯一一個java虛擬機規範沒有規定任何OOM情況的區域。

2、 heap 和stack 有什麼區別

(1)申請方式

stack:由系統自動分配。例如,聲明在函數中一個局部變數 int b; 系統自動在棧中為 b 開闢空間

heap:需要程式員自己申請,並指明大小,在 c 中 malloc 函數,對於Java 需要手動 new Object()的形式開闢

(2)申請後系統的響應

stack:只要棧的剩餘空間大於所申請空間,系統將為程式提供記憶體,否則將報異常提示棧溢出。

heap:首先應該知道操作系統有一個記錄空閑記憶體地址的鏈表,當系統收到程式的申請時,會遍歷該鏈表,尋找第一個空間大於所申請空間的堆結點,然後將該結點從空閑結點鏈表中刪除,並將該結點的空間分配給程式。另外,由於找到的堆結點的大小不一定正好等於申請的大小,系統會自動的將多餘的那部分重新放入空閑鏈表中。

(3)申請大小的限制

stack:棧是向低地址擴展的數據結構,是一塊連續的記憶體的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在 WINDOWS 下,棧的大小是 2M(也有的說是 1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩餘空間時,將提示 overflow。因此,能從棧獲得的空間較小。

heap:堆是向高地址擴展的數據結構,是不連續的記憶體區域。這是由於系統是用鏈表來存儲的空閑記憶體地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限於電腦系統中有效的虛擬記憶體。由此可見, 堆獲得的空間比較靈活,也比較大。

(4)申請效率的比較

stack:由系統自動分配,速度較快。但程式員是無法控制的。

heap:由 new 分配的記憶體,一般速度比較慢,而且容易產生記憶體碎片,不過用起來最方便。

(5)heap和stack中的存儲內容

stack:在函數調用時,第一個進棧的是主函數中後的下一條指令(函數調用語句的下一條可執行語句)的地址, 然後是函數的各個參數,在大多數的 C 編譯器中,參數是由右往左入棧的,然後是函數中的局部變數。註意靜態變數是不入棧的。

當本次函數調用結束後,局部變數先出棧,然後是參數,最後棧頂指針指向最開始存的地址,也就是主函數中的下一條指令,程式由該點繼續運行。

heap:一般是在堆的頭部用一個位元組存放堆的大小。堆中的具體內容有程式員安排。

3、Jvm記憶體模型(重排序、記憶體屏障)

記憶體屏障:可以阻擋編譯器的優化,也可以阻擋處理器的優化

happens-before原則:

1:一個線程的A操作總是在B之前,那多線程的A操作肯定實在B之前。

2:monitor 再加鎖的情況下,持有鎖的肯定先執行。

3:volatile修飾的情況下,寫先於讀發生

4:線程啟動在一起之前 strat

5:線程死亡在一切之後 end

6:線程操作在一切線程中斷之前

7:一個對象構造函數的結束都該對象的finalizer的開始之前

8:傳遞性,如果A肯定在B之前,B肯定在C之前,那A肯定是在C之前。

主記憶體:所有線程共用的記憶體空間

工作記憶體:每個線程特有的記憶體空間

三、類載入過程

1、JVM的類載入過程

Java類載入需要經歷一下幾個過程:

(1)載入

載入時類載入的第一個過程,在這個階段,將完成一下三件事情:

​ a. 通過一個類的全限定名獲取該類的二進位流。

​ b. 將該二進位流中的靜態存儲結構轉化為方法去運行時數據結構。

​ c. 在記憶體中生成該類的Class對象,作為該類的數據訪問入口。

(2)驗證

驗證的目的是為了確保Class文件的位元組流中的信息不回危害到虛擬機.在該階段主要完成以下四種驗證:

​ a. 文件格式驗證:驗證位元組流是否符合Class文件的規範,如主次版本號是否在當前虛擬機範圍內,常量池中的常量是否有不被支持的類型.

​ b. 元數據驗證:對位元組碼描述的信息進行語義分析,如這個類是否有父類,是否集成了不被繼承的類等。

​ c. 位元組碼驗證:是整個驗證過程中最複雜的一個階段,通過驗證數據流和控制流的分析,確定程式語義是否正確,主要針對方法體的驗證。如:方法中的類型轉換是否正確,跳轉指令是否正確等。

​ d. 符號引用驗證:這個動作在後面的解析過程中發生,主要是為了確保解析動作能正確執行。

​ e. 準備

準備階段是為類的靜態變數分配記憶體並將其初始化為預設值,這些記憶體都將在方法區中進行分配。準備階段不分配類中的實例變數的記憶體,實例變數將會在對象實例化時隨著對象一起分配在Java堆中。

(3)解析

該階段主要完成符號引用到直接引用的轉換動作。解析動作並不一定在初始化動作完成之前,也有可能在初始化之後。

(4)初始化

初始化時類載入的最後一步,前面的類載入過程,除了在載入階段用戶應用程式可以通過自定義類載入器參與之外,其餘動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的Java程式代碼。

2、類載入器

類載入器 就是根據指定全限定名稱將class文件載入到JVM記憶體,轉為Class對象。

主要有一下四種類載入器:

(1)啟動類載入器(Bootstrap ClassLoader)用來載入java核心類庫,無法被java程式直接引用,由C++語言實現(針對HotSpot),負責將存放在<JAVA_HOME>\lib目錄或-Xbootclasspath參數指定的路徑中的類庫載入到記憶體中。。

(2)擴展類載入器(extensions class loader):它用來載入 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。該類載入器在此目錄裡面查找並載入 Java 類,負責載入<JAVA_HOME>\lib\ext目錄或java.ext.dirs系統變數指定的路徑中的所有類庫。。

(3)系統類載入器(system class loader)也叫應用類載入器:它根據 Java 應用的類路徑(CLASSPATH)來載入 Java 類。一般來說,Java 應用的類都是由它來完成載入的。可以通過 ClassLoader.getSystemClassLoader()來獲取它。負責載入用戶類路徑(classpath)上的指定類庫,我們可以直接使用這個類載入器。一般情況,如果我們沒有自定義類載入器預設就是用這個載入器。

(4)用戶自定義類載入器,通過繼承 java.lang.ClassLoader類的方式實現。

3、雙親委派機制

如果一個類載入器收到類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器完成。每個類載入器都是如此,只有當父載入器在自己的搜索範圍內找不到指定的類時(即ClassNotFoundException),子載入器才會嘗試自己去載入。

image-20221209092113983

為什麼需要雙親委派模型?

在這裡,先想一下,如果沒有雙親委派,那麼用戶是不是可以自己定義一個java.lang.Object的同名類,java.lang.String的同名類,並把它放到ClassPath中,那麼類之間的比較結果及類的唯一性將無法保證,因此,為什麼需要雙親委派模型?防止記憶體中出現多份同樣的位元組碼。

怎麼打破雙親委派模型?

打破雙親委派機制則不僅要繼承ClassLoader類,還要重寫loadClass和findClass方法。

四、垃圾回收演算法

1、java中垃圾收集的方法有哪些?

java中有四種垃圾回收演算法,分別是標記清除法、標記整理法、複製演算法、分代收集演算法;

①標記-清除

這是垃圾收集演算法中最基礎的,根據名字就可以知道,它的思想就是標記哪些要被回收的對象,然後統一回收。

第一步:利用可達性去遍歷記憶體,把存活對象和垃圾對象進行標記;

第二步:在遍歷一遍,將所有標記的對象回收掉;

這種方法很簡單,但是會有兩個主要問題:1.效率不高,標記和清除的效率都很低;2.會產生大量不連續的記憶體碎片,導致以後程式在分配較大的對象時,由於沒有充足的連續記憶體而提前觸發一次GC動作。

②複製演算法:

為瞭解決效率問題,複製演算法將可用記憶體按容量劃分為相等的兩部分,然後每次只使用其中的一塊,當一塊記憶體用完時,就將還存活的對象複製到第二塊記憶體上,然後一次性清楚完第一塊記憶體,再將第二塊上的對象複製到第一塊。

但是這種方式,記憶體的代價太高,每次基本上都要浪費一般的記憶體。

於是將該演算法進行了改進,記憶體區域不再是按照1:1去劃分,而是將記憶體劃分為8:1:1三部分,較大那份記憶體交Eden區,其餘是兩塊較小的記憶體區叫Survior區。每次都會優先使用Eden區,若Eden區滿,就將對象複製到第二塊記憶體區上,然後清除Eden區,如果此時存活的對象太多,以至於Survivor不夠時,會將這些對象通過分配擔保機制複製到老年代中。(java堆又分為新生代和老年代)

*③標記-整理:

第一步:利用可達性去遍歷記憶體,把存活對象和垃圾對象進行標記;

第二步:將所有的存活的對象向一段移動,將端邊界以外的對象都回收掉;

該演算法主要是為瞭解決標記-清除,產生大量記憶體碎片的問題;當對象存活率較高時,也解決了複製演算法的效率問題。它的不同之處就是在清除對象的時候現將可回收對象移動到一端,然後清除掉端邊界以外的對象,這樣就不會產生記憶體碎片了。

④分代收集

現在的虛擬機垃圾收集大多採用這種方式,它根據對象的生存周期,將堆分為新生代和老年代。在新生代中,由於對象生存期短,每次回收都會有大量對象死去,那麼這時就採用複製演算法。老年代里的對象存活率較高,沒有額外的空間進行分配擔保,所以可以使用標記-整理 或者 標記-清除。

記憶體利用率:標記整理演算法 > 標記清除演算法 > 複製演算法

記憶體連續性: 標記整理演算法 = 複製演算法 > 標記清除演算法

效率:

對象存活率不高:複製演算法 > 標記清楚演算法 > 標記整理演算法對象存活率高:標記清除演算法 > 複製演算法 > 標記整理演算法新生代對象存活率不高:選擇複製演算法

老年代對象存活率較高:選擇標記清除演算法 標記整理演算法

2、JVM如何判斷一個對象可以被回收

判斷一個對象是否存活有兩種方法:

(1)引用計數法

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

引用計數法有一個缺陷就是無法解決迴圈引用問題,也就是說當對象A引用對象B,對象B又引用者對象A,那麼此時A,B對象的引用計數器都不為零,也就造成無法完成垃圾回收,所以主流的虛擬機都沒有採用這種演算法。

(2)可達性演算法(引用鏈法)

該演算法的基本思路就是通過一些被稱為引用鏈(GC Roots)的對象作為起點,從這些節點開始向下搜索,搜索走過的路徑被稱為(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時(即從GC Roots節點到該節點不可達),則證明該對象是不可用的。

GCRoot對象有四種對象:

1、jvm棧中的引用對象

2、方法區中的引用靜態常量

3、方法區中的普通引用常量4、native方法中的引用對象

一個對象經過兩次標記為垃圾對象,該對象才會被判定為垃圾對象。

3、JVM記憶體分配與回收策略

記憶體分配:

(1)棧區:棧分為java虛擬機棧和本地方法棧

(2)堆區:堆被所有線程共用區域,在虛擬機啟動時創建,唯一目的存放對象實例。堆區是gc的主要區域,通常情況下分為兩個區塊年輕代和年老代。更細一點年輕代又分為Eden區,主要放新創建對象,From survivor 和 To survivor 保存gc後幸存下的對象,預設情況下各自占比 8:1:1。

(3)方法區:被所有線程共用區域,用於存放已被虛擬機載入的類信息,常量,靜態變數等數據。被Java虛擬機描述為堆的一個邏輯部分。習慣是也叫它永久代(permanment generation)

(4)程式計數器:當前線程所執行的行號指示器。通過改變計數器的值來確定下一條指令,比如迴圈,分支,跳轉,異常處理,線程恢復等都是依賴計數器來完成。線程私有的。

回收策略以及Minor GC和Major GC:

Minor GC是新生代GC,指的是發生在新生代的垃圾收集動作。由於java對象大都是朝生夕死的,所以Minor GC非常平凡,一般回收速度也比較i快。

Major GC/Full GC 是老年代GC,指的是發生在老年代的GC,出現Major GC一般經常會伴有Minor GC,Major GC的速度比Minor GC慢的多。

(1)對象優先在堆的Eden區分配。

(2)大對象直接進入老年代。

(3)長期存活的對象將直接進入老年代。

當Eden區沒有足夠的空間進行分配時,虛擬機會執行一次Minor GC.Minor GC通常發生在新生代的Eden區,在這個區的對象生存期短,往往發生GC的頻率較高,回收速度比較快;Full Gc/Major GC 發生在老年代,一般情況下,觸發老年代GC的時候不會觸發Minor GC,但是通過配置,可以在Full GC之前進行一次Minor GC這樣可以加快老年代的回收速度。

4、GC流程

java堆 = 新生代+老年代;

新生代 = Eden + Suivivor(S0 + S1),預設分配比例是8:1:1;

當Eden區空間滿了的時候,就會觸發一次Minor GC,以收集新生代的垃圾,存活下來的對象會被分配到Survivor區

大對象(需要大量連續記憶體空間的對象)會直接被分配到老年代

如果對象在Eden中出生,並且在經歷過一次Minor GC之後仍然存活,被分配到存活區的話,年齡+1,此後每經歷過一次Minor GC並且存活下來,年齡就+1,當年齡達到15的時候,會被晉升到老年代;

當老年代滿了,而無法容納更多對象的話,會觸發一次full gc;full gc存儲的是整個記憶體堆(包括年輕代和老年代);

Major GC是發生在老年代的GC,清理老年區,經常會伴隨至少一次minor gc;

5、垃圾收集器及各自的特點

垃圾收集器是JVM調優中最核心的一個知識點,我們常說的JVM調優其實都是根據對應的垃圾收集器特性而去做調整和優化。

垃圾收集器雖然看起來數量比較多,但其實總體邏輯都是因為我們硬體環境的升級而演化出來的產品,不同垃圾收集器的產生總體可以劃分為幾個階段。。

第一階段:單線程收集時代(Serial和Serial Old)

第二階段:多線程收集時代(Parallel Scanvenge 和Parallel Old)

第三階段:併發收集時代(ParNew和CMS)

第四階段:智能併發收集時代(G1)

下麵的圖一方面介紹了有哪些垃圾收集器,另外一方面也描述了每個垃圾收集器是負責哪個分代(新生代、老年的)的垃圾收集,還有一部分信息是告訴我們每個新生代的垃圾收集器可以與哪些老年代的搜集配合工作。

image-20221213090646764

常用組合:Serial+Serial Old, Parallel Scavenge+Parallel Old,ParNew+CMS,G1(不需要組合其他收集器)。

Serial 垃圾收集流程

Serial會開啟一個線程進行垃圾收集,在收集的整個過程都會暫停用戶線程(Stop the Word),直到垃圾收集完畢,如果把垃圾收集的過程當作打掃房間衛生,那麼Serial 的收集過程就是在你收集房間的時候,你首先會讓房間里的人都出去,然後你再安心打掃房間,直到你打掃完畢了才能讓外面的人進來,這樣就不用擔心你一邊打掃房間一邊還有人在房間里扔垃圾了。

註意:說到“暫停用戶線程”,這裡也是各種垃圾收集器的一個區分指標,後面的有些垃圾收集器收集的某些階段是不需要暫停用戶線程的。

收集器特點

收集區域: Serial (新生代),Serial Old(老年代)。

使用演算法: Serial (標記複製法),Serial Old(標記整理法)。

搜集方式: 單線程收集。

優勢: 記憶體資源占用少、單核CPU環境最佳選項。

劣勢: 整個搜集過程需要停頓用戶線程。多核CPU、記憶體富足的環境,資源優勢無法利用起來。

Parallel Scavenge工作流程

Parallel Scavenge 和Parallel Old的工作機制一樣,這裡以Parallel Scavenge為例,Parallel Old在收集過程中會開啟多個線程一起收集,整個過程都會暫停用戶線程,直到整個垃圾收集過程結束。和之前的Serial垃圾收集器一對比,同樣進行垃圾收集前都是先叫其他人都離開房間,但是不同的是serial只有一個人打掃房間,而這裡卻是有多個人一起打掃房間,所以從這一點看Parallel 系列的收集器要比之前的效率高上很多。

收集器特點

收集區域: Parallel Scavenge (新生代),Parallel Old(老年代)。

使用演算法: Parallel Scavenge (標記複製法),Parallel Old(標記整理法)。

搜集方式: 多線程。

優勢: 多線程收集,CPU多核環境下效率要比serial高。

劣勢: 整個搜集過程需要停頓用戶線程。

③ParNew收集器流程

ParNew收集流程和Parallel Scavenge一樣 ,同樣是先停止應用程式線程,再進行多線程同時收集,整個收集過程都會暫停用戶線程(Stop the Word),直到垃圾收集完畢。

ParNew的特點

收集區域: 新生代。

使用演算法: 標記複製法。

搜集方式: 多線程。

搭配收集器: CMS。

優勢: 多線程收集,CPU多核環境下效率要比serial高,新生代唯一一個能與CMS配合的收集器。

劣勢: 整個搜集過程需要停頓用戶線程。

CMS收集器

為了儘量減少用戶線程的停頓時間,CMS採用了一種全新的策略使得在垃圾回收過程中的某些階段用戶線程和垃圾回收線程可以一起工作,這樣就避免了因為長時間的垃圾回收而使用戶線程一直處於等待之中。

整個過程就像我們打掃房間的時候可以讓大家留在房間里工作,等我把房間的其他地方都打掃完,只剩大家工作的那部分區域的垃圾,這個時候再讓大家到房間外面去,我再把房間里那些剩下的地方清理乾凈就行了,這樣做的好處就是大家的工作時間變長了,在房間外等待的時間變短了。

CMS 也是按這個邏輯把整個垃圾收集的過程分成四個階段,分別是初始標記、併發標記、重新標記、併發清理四個階段,然後CMS會根據每個階段不同的特性來決定是否停頓用戶線程。

階段一:初始標記

初始標記的目的是先把所有GC Root直接引用的對象進行標記,因為需要避免在標記GC Root的過程還有程式在繼續產生GC Root對象,所以這個過程是需要需要停止用戶線程 ,因為這個過程只會標記GC Root的直接引用,並不會對整個GC Root的引用進行遍歷,所以這個過程速度也是所有階段中最快的。

階段二:併發標記

併發標記階段的工作就是把階段一標記好的GC Root對象進行深度的遍歷,找到所有與GC Root關聯的對象併進行標記,這個過程中是採用多線程的方式進行遍歷標記,對整個JVM 的GC Root進行遍歷的過程是垃圾收集過程中最耗時的一步,CMS為了考慮儘量不停頓用戶線程,所以這個階段是不停止用戶線程的,也就是說這個階段JVM會分配一些資源給用戶線程執行任務,通過這樣的方式減少用戶線程的停頓時間。

階段三:重新標記

因為在階段二的時候用戶線程同時也在運行,這個過程中又會產生新的垃圾,所以重新標記階段主要任務是把上一個階段中產生的新垃圾進行標記( 使用多線程標記),很顯然這個過程是對上一個階段用戶線程運行遺留的垃圾進行標記,所以數量 是非常少執行時間也是最短的,當然為了避免這個過程再次產生新的垃圾,所以重新標記的過程是會停頓用戶線程的。

階段四:併發清理

併發清理階段是對那些被標記為可回收的對象進行清理,在一般情況下併發清理階段是使用的標記清除法,因為這個過程不會牽扯到對象的地址變更,所以CMS在併發清理階段是不需要停止用戶線程的。也正因為併發清理階段用戶線程也可以同時運行,所以在用戶線程運行的過程中自然也會產生新的垃圾,這也就是導致CMS收集器會產生“浮動垃圾”的原因。

當然,在一種情況下併發清理階段CMS也會停頓用戶線程,這就和我們之前說過的CMS選用的垃圾回收演算法有關係,因為一般情況下使用的都是標記清除法,但是標記清除法的弊端就是在於會產生空間碎片,所以當空間碎片到達了一定程度時,此時CMS會使用標記整理法解決空間碎片的問題,不過因為標記整理法會將對象的位置進行挪動並更新對象的引用的指向地址,那麼這個過程中用戶線程同時運行的話會產生併發問題,所以當CMS進行碎片整理的時候必須得停止用戶線程。

CMS的特點

收集區域: 老年代。

使用演算法: 標記清除法+標記整理法。

搜集方式: 多線程。

搭配收集器: ParNew。

優勢: 多線程收集,收集過程不停止用戶線程,所以用戶請求停頓時間短。

CMS遺留的問題

CMS收集器開闢了一條垃圾收集的新思路,不過這麼好的垃圾收集器卻一直沒有被Hospot虛擬機納入到預設的垃圾收集器,到Jdk8使用的預設收集器都還是 Parallel scavenge 和 Parallel old,這其中非常重要的原因就是CMS遺留了幾個比較頭疼的問題。

1、浮動垃圾

在併發清理階段因為需垃圾收集線程是和用戶線程同時執行任務的,這個時候用戶線程運行時產生的垃圾是無法在當前階段進行回收的,所以這段時間用戶線程產生的新垃圾只能遺留到下一次收集,這些在垃圾收集過程中新產生的垃圾我們稱為浮動垃圾。

3、空間碎片整理造成卡頓

CMS在平常情況下會使用標記清除法進行回收,只有在老年代的空間碎片達到一定程度,這個時候就會使用標記整理法對記憶體的空間碎片進行整理,因為標記整理的過程需要移動對象的位置,所以這個過程只能Stop the word,這個時候記憶體越大那麼這個收集時間就越長,造成這種卡頓現象。

4、可能導致系統長時間的假死。

因為在併發清除階段會有新的對象產生,在有擔保機制的情況下,當新生代垃圾清理的時候存活的對象大多,導致Survior區無法容納全部的對象,這時就會觸發擔保機制,這裡存活的對象裡面會有一部分會直接進入老年代,所以在每次GC的時候老年代需要預留一部分記憶體出來,所以通常CMS 在老年代占用了大概百分之七八十的時候就進行FullGC。

不過這段時間的產生對象的總體大小是未知的,如果新生代存活的對象非常多,這些擔保的對象轉移到老年代的時候可能導致老年代預留的空間也不足以容納,那麼此時CMS不得不進行一次Stop the word 的Full GC ,因為此時堆空間已經完全占滿,這個時候已經無法使用併發的清理方式進行收集了,所以此時只能停止用戶線程來專心進行垃圾收集,而這時候老年代收集器不得不從CMS切換成Serial old垃圾收集器來進行垃圾收集 。

至於這裡為什麼要使用單線程的Serial old,而不選擇多線程的Parallel Old,那是因為CMS的新生代收集器是ParNew,而ParNew只能與CMS和Serial Old配合),所以這也是個無奈的選擇。而切換成Serial old來進行垃圾收集的時候就有問題了,Serial old收集器是單線程的,它只適用於記憶體大小在幾十到上百M的大小,而往往我們現在的記憶體大小都是幾G到幾十G,所以這種情況下整個垃圾收集的時間可能會特別特別長,有時候可能達到幾個小時甚至好幾天的都有可能。

G1收集器

CMS開創了垃圾收集器的一個新時代,它實現了垃圾收集和用戶線程同時執行,達到垃圾收集的過程不停止用戶線程的目標,這個思路作為後面的收集器提供了一個很好的典範。時代向前優化不止,除了需要解決了CMS遺留了的幾個問題外,硬體資源的升級換代,可用的記憶體資源越來越多一直是促進垃圾收集器發展的一個核心驅動力,可使用的記憶體資源變多對於軟體來說這當然是個好事,不過對於垃圾收集器來說就變得越來越麻煩了,隨著發展我們發現傳統垃圾收集器的收集方式已經不適用於這種大記憶體的垃圾收集了。

不管是Serial系列、Parallel系列、CMS系列,它們都是基於把記憶體進行物理分區的形式把JVM記憶體分成老年代、新生代、永久代或MetaSpace,這種分區模式下進行垃圾收集時必須對某個區域進行整體性的收集(比如整個新生代、整個老年代收集或者整個堆), 原來的記憶體空間都不是很大,一般就是幾G到幾十G,但現在的硬體資源發展可用的記憶體達到幾百G甚至上T的程度,那麼JVM中的某一個分代區域就可能會有幾十上百G的大小,那麼如果這時候採用傳統模式下的物理分區的收集的話,每次垃圾掃描記憶體區域變大了、那麼需要的清理時間自然就會變得更加長了;換做打掃衛生來說,原來你只需要打掃幾個小辦公室就行了,但是隨著公司業務發展整棟樓是都是你公司了,這個時候你需要打掃公司衛生的時間無疑也會變得特別長。

所以問題出現了,那麼自然就有人會來解決的,G1就是在這種環境下誕生的,G1首先吸取了CMS優良的思路,還是使用併發收集的模式,但是更重要的是G1摒棄了原來的物理分區,而是把整個記憶體分成若幹個大小的Region區域,然後由不同的Region在邏輯上來組合成各個分代,這樣做的好處是G1進行垃圾回收的時候就可以用Region作為單位來進行更細粒度的回收了,每次回收可以只針對某一個或多個Region來進行回收。

G1最核心的分區基本單位Region ,G1沒有像之前一樣把堆記憶體劃分為固定連續的幾塊區域,而是完全捨棄了進行記憶體上的物理分區,而是把堆記憶體拆分成了大小為1M-32M的Region塊,然後以Region為單位自由的組合成新生代、老年代、Eden區、survior區、大對象區(Humonggous Region),隨著垃圾回收和對象分配每個Region也不會一直固定屬於哪個分代,我們可以認為Region可以隨時扮演任何一個分代區域的記憶體。

G1的回收流程和CMS邏輯大致相同,分別進行初始標記、併發標記、重新標記、篩選清除,區別在最後一個階段G1不會直接進行清除,而是會根據設置的停頓時間進行智能的篩選和局部的回收。

階段一:初始標記

初始標記額目的是先把所有GC Root直接引用的對象進行標記,因為需要避免在標記GC Root的過程還有程式在繼續產生GC Root對象,所以這個過程是需要停止用戶線程 ,因為這個過程並不會對整個GC Root的引用進行遍歷,所以這個過程速度是非常快的。

階段二:併發標記

併發標記階段的工作就是把階第一段標記好的GC Root對象進行深度的遍歷,找到所有與GC Root關聯的對象併進行標記,這個過程中是採用多線程的方式進行遍歷標記,對整個JVM 的GC Root進行遍歷的過程是垃圾收集過程中最耗時的一步,為了儘量不停頓用戶線程,所以這個階段GC線程會和用戶線程同時運行,通過這樣的方式減少用戶線程的停頓時間。

階段三:最終標記

因為在上個階段用戶線程同時也在運行,用戶線程運行的過程中又會產生新的垃圾,所以重新標記階段主要任務是把上一個階段中產生的新垃圾進行標記( 使用多線程標記),很顯然這個過程是對上一個階段用戶線程運行遺留的垃圾進行標記,所以數量是非常少執行時間也是非常短的,當然為了避免這個過程再次產生新的垃圾,所以重新標記的過程是會停頓用戶線程的。

階段四:篩選回收

把存活的對象複製到空閑Region區域,再根據Collect Set記錄的可回收Region信息進行篩選,計算Region回收成本,根據用戶設定的停頓時間值制定回收計劃,根據回收計劃篩選合適的Region區域進行回收。

回收演算法:從局部來說G1是使用的標記複製法,把存活對象從一個Region複製到另外的Region,但從整個堆來說G1的邏輯又相當於是標記整理法,每次垃圾收集時會把存活的對象整理到其他對應區域的Region里,再把原來的Region標記為可回收區域記錄到CSet里,所以G1的每一次回收都是一次整理過程,所以也就不會產生空間碎片問題。

G1的特點

收集區域: 整個堆記憶體。

使用演算法: 標記複製法

搜集方式: 多線程。

搭配收集器: 無需其他收集器搭配。

優勢: 停頓時間可控,吞吐量高,可根據具體場景選擇吞吐量有限還是停頓時間有限,不需要額外的收集器搭配。

劣勢: 因為需要維護的額外信息比較多,所以需要的記憶體空間也要大,6G以上的記憶體才能考慮使用G1收集器。

⑥總結

從現在往回看,我們會發現每個垃圾收集器都是一個時代的產物。

第一階段:在單核CPU,記憶體資源稀缺的時代使用的是Serial和Serial Old收集器,對於單核CPU,記憶體只有幾十M的場景Serial的效率是非常高的。

第二階段:進入多核CPU時代後出現了Parallel Scavenge和Parallel Old收集器,利用多線程並行收集極大的提高了垃圾收集的效率,所以在多核CPU場景,記憶體在幾百M到幾G的場景Parallel Scavenge和Parallel Old是適用的。

第三階段:隨著記憶體的變大,垃圾收集的過程時間變得越來越長了,BS系統的發展也逐漸開始重視用戶體驗了,所以就出現了CMS以減少用戶線程停頓時間為目的的收集器,CMS通過併發收集減少了用戶線程的停頓時間,在多核CPU,並且記憶體空間幾G到幾十G的空間、並且註重用戶體驗的CMS垃圾收集器是適用的。

第四階段:CMS遺留了一些比較致命的問題,所以就有了G1,G1不再對記憶體進行物理上的分代,而只是進行邏輯上的分區,通過各種機制讓垃圾收集變得更智能和可控了,多核CPU,並且記憶體在10G到上百G的場景G1比較適合。

6、強引用、軟應用、弱引用、虛引用的區別?

①強引用:
強引用是我們使用最廣泛的引用,如果一個對象具有強引用,那麼垃圾回收期絕對不會回收它,當記憶體空間不足時,垃圾回收器寧願拋出OutOfMemoryError,也不會回收具有強引用的對象;我們可以通過顯示的將強引用對象置為null,讓gc認為該對象不存在引用,從而來回收它;

②軟引用:
軟應用是用來描述一些有用但不是必須的對象,在java中用SoftReference來表示,當一個對象只有軟應用時,只有當記憶體不足時,才會回收它;
軟引用可以和引用隊列聯合使用,如果軟引用所引用的對象被垃圾回收器所回收了,虛擬機會把這個軟引用加入到與之對應的引用隊列中;

③弱引用:
弱引用是用來描述一些可有可無的對象,在java中用WeakReference來表示,在垃圾回收時,一旦發現一個對象只具有軟引用的時候,無論當前記憶體空間是否充足,都會回收掉該對象;
弱引用可以和引用隊列聯合使用,如果弱引用所引用的對象被垃圾回收了,虛擬機會將該對象的引用加入到與之關聯的引用隊列中;

④虛引用:
虛引用就是一種可有可無的引用,無法用來表示對象的生命周期,任何時候都可能被回收,虛引用主要使用來跟蹤對象被垃圾回收的活動,虛引用和軟引用與弱引用的區別在於:虛引用必須和引用隊列聯合使用;在進行垃圾回收的時候,如果發現一個對象只有虛引用,那麼就會將這個對象的引用加入到與之關聯的引用隊列中,程式可以通過發現一個引用隊列中是否已經加入了虛引用,來瞭解被引用的對象是否需要被進行垃圾回收。

五、性能調優

常用JVM參數

-Xmn:調整新生代大小

-Xms:調整堆初始大小,預設記憶體的1/64

-Xmx:調整堆的最大可擴展大小,預設是1/4

-XX:+PrintGCDetails 輸出詳細的GC處理日誌,查看堆的詳細信息。

設置JVM參數

命令:java -Xms20m -Xmx50m xx.class

image-20221208091214424

1、用過哪些調優的參數?用過jmap等條用工具麽?

1)堆棧配置相關

-Xms 設置初始堆的大小

-Xmx 設置最大堆的大小

-Xmn 設置年輕代大小,相當於同時配置-XX:NewSize和-XX:MaxNewSize為一樣的值

-Xss 每個線程的堆棧大小

-XX:NewSize 設置年輕代大小(for 1.3/1.4)

-XX:MaxNewSize 年輕代最大值(for 1.3/1.4)

-XX:NewRatio 年輕代與年老代的比值(除去持久代)

-XX:SurvivorRatio Eden區與Survivor區的的比值

-XX:PretenureSizeThreshold 當創建的對象超過指定大小時,直接把對象分配在老年代。

-XX:MaxTenuringThreshold設定對象在Survivor複製的最大年齡閾值,超過閾值轉移到老年代

2)垃圾收集器相關

-XX:+UseParallelGC:選擇垃圾收集器為並行收集器。

-XX:ParallelGCThreads=20: 配置並行收集器的線程數

-XX:+UseConcMarkSweepGC:設置年老代為併發收集。

-XX:CMSFullGCsBeforeCompaction=5 由於併發收集器不對記憶體空間進行壓縮、整理,所以運行一段時間以後會產生“碎片”,使得運行效率降低。此值設置運行5次GC以後對記憶體空間進行壓縮、 整理。

-XX:+UseCMSCompactAtFullCollection:打開對年老代的壓縮。可能會影響性能,但是可以消除 碎片

3)輔助信息相關

-XX:+PrintGCDetails 列印GC詳細信息

-XX:+HeapDumpOnOutOfMemoryError讓JVM在發生記憶體溢出的時候自動生成記憶體快照,排查問題用

-XX:+DisableExplicitGC禁止系統System.gc(),防止手動誤觸發FGC造成問題.

-XX:+PrintTLAB 查看TLAB空間的使用情況

2、調優工具

常用調優工具分為兩類,jdk自帶監控工具:jps、jstat、jmap、jconsole和jvisualvm,第三方有:MAT(Memory Analyzer Tool)、GChisto。

jconsole,Java Monitoring and Management Console是從java5開始,在JDK中自帶的java監控和管理控制台,用於對JVM中記憶體,線程和類等的監控jvisualvm,jdk自帶全能工具,可以分析記憶體快照、線程快照;監控記憶體變化、GC變化等。MAT,Memory Analyzer Tool,一個基於Eclipse的記憶體分析工具,是一個快速、功能豐富的Java heap分析工具,它可以幫助我們查找記憶體泄漏和減少記憶體消耗

GChisto,一款專業分析gc日誌的工具

3、那些JVM性能調優

首先需要註意的是在對JVM記憶體調優的時候不能只看操作系統級別Java進程所占用的記憶體,這個數值不能準確的反應堆記憶體的真實占用情況,因為GC過後這個值是不會變化的,因此記憶體調優的時候要更多地使用JDK提供的記憶體查看工具,比如JConsole和Java VisualVM。

對JVM記憶體的系統級的調優主要的目的是減少GC的頻率和Full GC的次數,過多的GC和Full GC是會占用很多的系統資源(主要是CPU),影響系統的吞吐量。特別要關註Full GC,因為它會對整個堆進行整理,導致Full GC一般由於以下幾種情況:

舊生代空間不足

調優時儘量讓對象在新生代GC時被回收、讓對象在新生代多存活一段時間和不要創建過大的對象及數組避免直接在舊生代創建對象

Pemanet Generation空間不足

增大Perm Gen空間,避免太多靜態對象

統計得到的GC後晉升到舊生代的平均大小大於舊生代剩餘空間

控制好新生代和舊生代的比例

System.gc()被顯示調用

垃圾回收不要手動觸發,儘量依靠JVM自身的機制

調優手段主要是通過控制堆記憶體的各個部分的比例和GC策略來實現,下麵來看看各部分比例不良設置會導致什麼後果

1). 新生代設置過小

一是新生代GC次數非常頻繁,增大系統消耗;二是導致大對象直接進入舊生代,占據了舊生代剩餘空間,誘發Full GC

2). 新生代設置過大

一是新生代設置過大會導致舊生代過小(堆總量一定),從而誘發Full GC;二是新生代GC耗時大幅度增加

一般說來新生代占整個堆1/3比較合適

3). Survivor設置過小

導致對象從eden直接到達舊生代,降低了在新生代的存活時間

4). Survivor設置過大

導致eden過小,增加了GC頻率

另外,通過-XX:MaxTenuringThreshold=n來控制新生代存活時間,儘量讓對象在新生代被回收

由記憶體管理和垃圾回收可知新生代和舊生代都有多種GC策略和組合搭配,選擇這些策略對於我們這些開發人員是個難題,JVM提供兩種較為簡單的GC策略的設置方式

1). 吞吐量優先

JVM以吞吐量為指標,自行選擇相應的GC策略及控制新生代與舊生代的大小比例,來達到吞吐量指標。這個值可由-XX:GCTimeRatio=n來設置

2). 暫停時間優先

JVM以暫停時間為指標,自行選擇相應的GC策略及控制新生代與舊生代的大小比例,儘量保證每次GC造成的應用停止時間都在指定的數值範圍內完成。這個值可由-XX:MaxGCPauseRatio=n來設置


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 家居網購項目實現013 以下皆為部分代碼,詳見 https://github.com/liyuelian/furniture_mall.git 32.功能30-會員不能登錄後臺管理 32.1需求分析/圖解 管理員admin登錄後,可以訪問所有頁面 會員登錄後,不能訪問後臺管理相關頁面,其他頁面可以訪 ...
  • 大家好,我是車轍,我的掘金小冊《SkyWalking:應用監控和鏈路跟蹤》已經上線啦,這是我的第一本電子書,歡迎大家訂閱。 整整好是9月的最後一天下午,能按耐住衝動的是少之又少,至於原因嘛你懂的。趕高鐵的準備趕高鐵,沒趕高鐵的也假裝趕高鐵。特別是開發同學,腦門上就差貼張紙條:別打擾我。 現在離跑路時 ...
  • 大多數開發者可能都用過 Postman,根據其官網的介紹:Postman 是一個用於構建和使用 API 的 API 平臺,簡化了 API 生命周期的每個步驟,提供更便捷的團隊協作,因此可以更快地創建更好的 API。這裡的 API,除了我們常用的 HTTP API 之外,還包括 Websocket(B ...
  • 一、前言 redis在我們企業級開發中是很常見的,但是單個redis不能保證我們的穩定使用,所以我們要建立一個集群。 redis有兩種高可用的方案: High availability with Redis Sentinel(哨兵) Scaling with Redis Cluster(分片集群) ...
  • 有問必答 最近有好多讀者私信我,為什麼選擇GoFrame做電商項目的開發? 原因很簡單: 因為我司是用GoFrame做電商業務開發的,而且我司同事基本都是PHP轉Go的。GoFrame可以說是非常適合PHPer轉Gopher的開發框架。 在入職我司之前,我有使用Gin和go-micro框架,目前也正 ...
  • JavaSE:基礎語法 註釋 Java中的註釋有三種: 單行註釋:只能註釋當前行,以//開始,直到行結束 ​ //輸出HelloWorld! 多行註釋:註釋一段文字,以/ * 開始以 * / 結束! ​ /* 這是我們Java程式的主入口, main方法也是程式的主線程。 */ 文檔註釋:用於生產A ...
  • JZ78 把二叉樹列印成多行 題目 給定一個節點數為 n 二叉樹,要求從上到下按層列印二叉樹的 val 值,同一層結點從左至右輸出,每一層輸出一行, 將輸出的結果存放到一個二維數組中返回。 例如:給定的二叉樹是{1,2,3,#,#,4,5} [ [1], [2,3], [4,5] ] 方法 非遞歸層 ...
  • 1. 協議的作用 TCP/IP 中消息傳輸基於流的方式,沒有邊界 協議的目的就是劃定消息的邊界,制定通信雙方要共同遵守的通信規則 2. Redis 協議 如果我們要向 Redis 伺服器發送一條 set name Nyima 的指令,需要遵守如下協議 // 該指令一共有3部分,每條指令之後都要添加回 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...