一、概述
1.1 虛擬機介紹
- java虛擬機是一個可以執行java字節碼的虛擬機進程。java源文件被編譯成能被java虛擬機執行的字節碼文件(.class)
- 跨平臺的是java程序(包括字節碼文件),而不是JVM。JVM是用C/C++開發的,是編譯後的機器碼,不能跨平臺,不同平臺需要安裝不同版本的JVM
1.2 JVM組成部分
-
類加載器:在JVM啓動時或者類運行時將需要的class加載到JVM中
-
內存區:將內存劃分爲若干個區以模擬實際機器上的存儲、記錄和調度模塊。如實際機器上各種功能的寄存器或者PC指針的記錄器。
-
執行引擎:負責執行class文件中包含的字節碼指令,相當於實際機器上的CPU
-
本地方法調用:調用C或C++實現本地方法的代碼返回結果
- 線程私有的:
- 程序計數器
- 虛擬機棧
- 本地方法棧
- 線程共享的
- 堆
- 方法區
- 直接內存
- 線程私有的:
二、類加載器
-
從類被加載到虛擬機內存中開始,到卸除內存爲止
-
生命過程
- 加載
- 連接
- 驗證
- 準備
- 解析
- 初始化
- 使用
- 卸載
其中加載、驗證、準備、初始化、卸載這五個階段的過程是固定的,在類加載過程中必須按照這種順序按部就班的進行,而解析階段則不一定,可以在初始化以後進行,是爲了支持java語言的運行時綁定
2.1 加載
三件事情
-
通過一個類的全限定名獲取定義此類的二進制字節流
-
將這個字節流所代表的靜態存儲結構轉換爲方法區的運行時數據結構
-
在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據結構的訪問入口
加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,而且在java堆中也創建一個java.lang.Class類的對象,這樣便可以通過該對象訪問方法區中的這些數據
2.2 驗證
這一階段主要是爲了確保Class文件的字節流中包含的信息符合虛擬機的要求,並且不會危害虛擬機自身的安全。
四個校驗動作
- 文件格式驗證:驗證字節流是否符合Class文件格式的規範
- 元數據驗證:對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求
- 字節碼驗證:通過數據流和控制流分析。確定程序語義是合法的、符合邏輯的
- 符號引用驗證:確保解析動作能正確執行
2.3準備
是正式爲類變量分配內存並設置初始值的階段,這些變量所使用的內存都將在方法區分配
進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在java堆中
初始值通常情況下是數據類型默認的零值
2.4 解析
是將虛擬機常量池內的符號引用替換爲直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定附
符號引用:簡單的理解就是字符串,比如引用一個類,java/util.ArrayList 這就是一個符號引用,字符串引用的對象不一定被加載
直接引用:指針或者地址偏移量。引用對象一定在內存(已經加載)
2.5 初始化
類初始化時類加載的最後一步,處理加載階段,用戶可以通過自定義的類加載器參數,其他階段都完全由虛擬機主導和控制。到了初始化階段才真正執行Java代碼
類初始化的主要工作時爲了靜態變量賦程序設定的初值
static int a=100; 在準備階段a被賦默認值0,在初始化階段就會被賦值爲100
java虛擬機規範中嚴格規定了有且只有五種情況必須對類進行初始化:
- 使用new創建類的實例,或者使用getstatic、putstatic讀取或設置一個靜態字段的值(放入常量池中的常量除外),或者調用一個靜態方法的時候,對應類必須進行初始化。
- 通過java.lang.reflect包的方法對類進行反射調用的時候,要是類沒有進行過初始化,則要首先進行初始化
- 當初始化一個類的時候,如果發現其父類沒有進行過初始化,則首先觸發父類初始化
- 當虛擬機啓動時,用戶需要指定一個主類(包含main()方法的類),虛擬機會首先初始化這個類
- 使用jdk1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、RE_invokeStatic的方法句柄,並且這個方法句柄對應的類沒有進行初始化,則需要先觸發其初始化。
三、對象的創建過程
java中對象的創建就是在堆上分配內存空間的過程,此處說的對象創建僅限於new關鍵字創建的普通java對象,不包括數組對象的創建。
當虛擬機遇到一條含有new的指令時,會進行一系列對象創建的操作
3.1 檢查類是否被加載
- 檢查常量池中是否有即將要創建的這個對象所屬的類的符號引用;若常量池中沒有這個類的符號引用,說明這個類還沒有被定義。拋出ClassNotFoundException
- 進而檢查這個符號引用所代表的類是否已經被JVM加載,若該類還沒有被加載,就找該類的class文件,並加載進方法區;若該類已經被JVm加載,則準備爲對象分配內存
3.2 爲對象分配內存
- 根據方法區中該類的信息確定該類所需的內存大小;一個對象所需的內存大小是在這個對象所屬類被定義完就能確定的。且一個類所生產的所有對象的內存大小是一樣的。JVM在一個類被加載進方法區的時候就知道該類生產的每一個對象所需的內存大小
- 從堆中華根一塊對於大小的內存給新的對象
分配對中內存有兩種方式
- 指針碰撞
- 如果JVM的垃圾收集器採用複製算法或標記-整理算法,那麼堆中空閒內存是完整的區域,並且空閒內存和已使用內存之間由一個指針標記
- 空閒列表
- 如果JVM的垃圾回收機制採用標記-清除算法,則需要一章空閒列表來記錄空閒區域
PS:多線程併發時會出現正在給對象A分配內存,還沒來得及修改指針,對象B又用這個指針分配內存
- 採用同步的方法:使用CAS來保證操作的原子性
- 每個線程分配內存都在自己的空間內進行,即是每個線程都在堆中預先分配一小塊內存,稱爲本地線程分配緩衝,
3.3 爲分配的內存空間初始化零值
對象的內存分配完成後,還需要將對象的內存空間都初始化爲零值,這樣能報賬對象即時沒有賦初值也可以直接使用
3.4 爲對象進行其他設置
設置對象頭中的信息
所屬類、類的元數據信息、對象的hashcode、GC分代年齡等信息
3.5 執行init方法
調用對象的構造函數進行初始化
順序:先初始化父類的靜態代碼—>初始化子類的靜態代碼–>初始化父類的非靜態代碼—>初始化父類構造函數—>初始化子類非靜態代碼—>初始化子類構造函數
四、對象的內存佈局
4.1 對象頭(markword)
- 第一部分用於存儲對象自身運行時數據,如哈希碼、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳、對象分代年齡。
- 第二部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是那個類的實例
- 如果是一個java數組,那麼在對象頭中還必須有一塊用於記錄數組長度的數據
4.2 實例數據(Instance Data)
- 是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容
- 分配策略:相同寬度的字段總是放在一起
- 這部分的存儲順序會受到虛擬機分配此類參數和字段在java源碼中定義順序的影響
4.3 對其填充(Padding)
4.4 預估對象大小
五、對象訪問
對象的訪問方式由虛擬機決定,java虛擬機提供兩種主流方式
- 句柄訪問對象
- 直接指針訪問對象
5.1 句柄訪問
java堆劃出一塊內存作爲句柄池,引用中存儲對象的句柄地址,句柄中包含對象實例數據、類型數據的地址信息
- 優點:引用中存儲的是穩定的句柄地址,在對象被移動時,只需要改變句柄中實例數據的指針,不需要改動引用ref本身
5.2直接指針
與句柄訪問不同的是,ref中直接存儲的就是對象的實例數據,但是類型數據跟句柄訪問方式一樣。
- 優點:速度快,相對於句柄訪問少了一次指針定位的開銷時間
六、JVM內存區域
6.1 虛擬機棧
描述的是方法執行時的內存模型,是線程私有的,聲明週期與線程相同,每個方法被執行的同時會創建棧幀,主要保存執行方法時的局部變量表、操作數棧、動態連接和方法返回地址等信息,方法執行時入棧,方法執行完成出棧。
- 出現異常
- 線程請求的棧深度大於虛擬機允許的棧深度,將拋出StackOverflowError
- 虛擬機棧空間可以動態擴展,當動態擴展是無法申請到足夠的空間時,拋出OutOfMemory異常
6.2 本地方法棧
與虛擬機棧功能類似,這塊區域也不需要進行GC
- 區別:
- 虛擬機棧爲虛擬機執行Java方法時訪問
- 本地方法棧爲虛擬機執行本地方法時提供服務
6.3 程序計數器
- 是一塊很小的內存空間,它是線程私有的,可以認作爲當前線程的行號指示器
- 主要作用是記錄線程運行時的狀態,方便線程被喚醒時能從上一次被掛起時的狀態繼續執行
- 程序計數器是唯一一個在java虛擬機規範中沒有規定任何OOM情況的區域,所以這塊區域不需要進行GC
6.4 本地內存
- 線程共享區域,Java 8 中,本地內存,也就是我們通常說的堆內外存,包括元空間和方法區
- 主要存儲類的信息,常量,靜態變量,及時編譯器編譯後代碼等,這部分是在堆中實現的。
- 如果動態生成類(將類信息放入永久代)或大量執行String.intern(將字段串放入永久代中的常量區),很容易造成OOM。
- 所以在Java 8 中就把方法區的實現移動到了本地內存中的元空間中,這樣方法區不受JVM的控制,也不會進行GC,因此也提升了性能,也就不存在由於永久代的限制大小而導致的OOM異常,也方便在元空間中統一管理
6.5堆
- 對象實例和數組都是在堆上分配的,GC也主要對這兩類數據進行回收
- java虛擬機規範對這塊的描述是:所有對象實例及數組都要在堆上分配內存。
- 堆細分
- 新生代
- 老年代
七、對象存活判斷
- 引用計數
- 可達性分析
7.1 引用計數
每個對象都有一個引用計數屬性,新增一個引用時計數加一,引用釋放時計數減一,計數爲0時可以回收。此方法簡單,無法解決對象互相循環引用的問題
7.2 可達性分析
從GC Roots開始向下搜索,搜索所走過的路徑稱爲引用鏈。當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的
GC Roots 對象:
虛擬機棧(棧幀中的本地變量表)中引用的對象
方法區中的類靜態屬性引用的對象
方法區中常量引用的對象
本地方法棧中JNI(Native方法)中引用的對象
如何判斷無用的類
該類所有實例都被回收(Java堆中沒有該類的對象)
加載該類的ClassLoader已經被回收
該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何對方利用反射訪問該類
7.3 finalize
- finalize()方法,是在釋放該對象內存前由GC調用
- 通常建議在這個方法中釋放該對象持有的資源,例如持有的堆外內存、遠程服務長連接。一般情形下不建議重寫該方法。對於一個對象,該方法有且僅會被調用一次
7.4 對象引用類型
-
強引用:必不可少
- 垃圾回收器不會回收,當內存空間不足時,Java會拋出OutOfMemoryError,不會靠隨意回收具有強引用的對象來解決內存不足的問題
-
軟引用:可有可無
- 內存不足時,就會回收這些對象的內存,只要垃圾回收器沒有回收就可以被程序使用,軟引用可以用來實現內存敏捷的高速緩存
-
弱引用:
- 具有更短的生命週期,在垃圾回收器線程掃描時,一旦發現只具有弱引用的對象,不管內存足夠與否,都會進行回收
-
虛引用
- 不會決定對象的生命週期,如果一個對象僅持有虛引用,那麼它就跟沒有任何引用一樣,在任何時候都可能被垃圾回收。
利用弱引用和軟引用解決OOM問題,通過軟引用實現java對象的高速緩存。
八、垃圾回收算法
8.1 標記-清除
-
過程:
- 標記階段:通過根節點,標記所有從根節點開始的可達對象。因此,爲被標記的對象及時垃圾對象
- 清除階段:清除所有未被標記的對象
-
缺點:
- 效率:標記和清除兩個過程效率都不高
- 空間:標記清除後產生大量不連續的內存空間,導致空間碎片太多
8.2 標記-整理
類似與標記-清除,只是標記完對象後,讓所有存活的對象都向一端移動,然後清理掉邊界以外的內存
-
優點:
- 解決了內存碎片問題
- 沒有內存碎片後,對象創建內存分配也更快速了(可以使用TLAB進行分配)
-
缺點:效率問題
8.3 複製算法
將可用內存劃分爲大小相等的兩塊,每次只使用其中一塊,當一塊內存用完後,就將存活的對象複製到另一塊上,然後再把使用過的內存空間一次清理掉。只需要移動堆頂指針,按順序分配內存
- 優點:效率高沒有內存碎片
- 缺點:
- 浪費一半空間
- 複製收集算法在對象存活率較高時由於較多的複製操作,導致效率變低
8.4 分代算法
根據對象的存活週期的不同將內存劃分爲幾塊,一般是把java堆分爲新生代和老年代。然後根據各個年代的特點採用適當的收集算法
- 新生代:大批死去,少數存活使用複製算法
- 老年代:存活率較高,沒有額外空間對它進行分配擔保,就必須使用標記清理或者標記整理
九、安全點
9.1 安全點
一些特定的位置:當線程運行到這些位置時,線程的一些狀態可以被確定,比如記錄OopMap的狀態,從而確定GC Root的信息,使JVM可以安全的進行一些操作
特定位置:
- 循環的末尾(防止大循環的時候一直不進入安全點,而其他線程在等待)
- 方法返回前
- 調用方法的Call
- 拋出異常的位置
9.2 安全區域
安全點完美解決了如何進入GC的問題,當程序長時間不執行的時候就需要安全區域
安全區域是指一段代碼中,引用關係不會發生變化,在這個區域任何地方GC都是安全的,安全區域可以看做是安全點的一個擴展。線程執行到安全區域的代碼時,首先標識自己進入了安全區域,這樣GC時就不用管進入安全區域的線層了,線層要離開安全區域時就檢查JVM是否完成了GC Roots枚舉,如果完成就繼續執行,如果沒有完成就等待直到收到可以安全離開的信號。
十、JVM垃圾回收機制
收集器 | 串行、並行or併發 | 新生代/老年代 | 算法 | 目標 | 適用場景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 複製算法 | 響應速度優先 | 單CPU環境下的Client模式 |
Serial Old | 串行 | 老年代 | 標記-整理 | 響應速度優先 | 單CPU環境下的Client模式、CMS的後備預案 |
ParNew | 並行 | 新生代 | 複製算法 | 響應速度優先 | 多CPU環境時在Server模式下與CMS配合 |
Parallel Scavenge | 並行 | 新生代 | 複製算法 | 吞吐量優先 | 在後臺運算而不需要太多交互的任務 |
Parallel Old | 並行 | 老年代 | 標記-整理 | 吞吐量優先 | 在後臺運算而不需要太多交互的任務 |
CMS | 併發 | 老年代 | 標記-清除 | 響應速度優先 | 集中在互聯網站或B/S系統 服務端上的Java應用 |
G1 | 併發 | both | 標記-整理+複製算法 | 響應速度優先 | 面向服務端應用,將來替換CMS |
ZGC | 併發 | both | 標記-整理+複製算法 | 響應速度優先 | 面向服務端應用,將來替換CMS |
10.1 Serial(新生代)
- 最基本的單線程垃圾收集器,使用一個CPU或一條收集線程去執行垃圾收集工作
- 工作時會Stop The World,暫停所有用戶線程,造成卡頓,適合運行在Client模式下的虛擬機
- 用作新生代收集器,複製算法
10.2 ParNew(新生代)
- 使用了多線程去垃圾收集
- 除了Serial只有它可以和CMS搭配使用的收集器
- 用作新生代收集器,複製算法
10.3 Parallel Scavenge(新生代)
用作新生代收集器,複製算法。關注高吞吐量,可以高效地利用CPU時間,儘快完成程序的運行任務,主要適合在後臺運算而不需要太多交互的任務。
參數:
- MaxGCPauseMills:控制最大垃圾收集停頓時間
- GCTimeRatio:直接設置吞吐量大小的
10.4 Serial Old(老年代)
- serial收集器的老年代版本、單線程,標記-整理算法
- 一般用於Client模式的虛擬機
- 當虛擬機是Server模式時,有2個用途:一種用途是在JDK 1.5以及之前的版本中與Parallel Scavenge收集器搭配使用 ,另一種用途就是作爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。
10.5 Parallel Old(老年代)
- Parallel Scavenge收集器的老年代版本,使用多線程和 標記-整理 算法。在JDK 1.6中開始提供。 在注重吞吐量的場合,配合Parallel Scavenge收集器使用。
10.6 CMS(老年代)
- 一種以獲取最短回收停頓時間爲目標的收集器。適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗
- 基於標記-清除算法。適用作爲老年代收集器
- 過程:
- 初始標記:只是標記一下GC Roots能直接關聯到的對象,速度很快。會暫停
- 併發標記:進行GC Roots Tracing(可達性分析)的過程
- 重寫標記:會Stop The -World。爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般比初始標記階段稍長些,但遠比並發標記的時間短。
- 併發清除:回收內存
- 耗時最長的併發標記和併發清除過程收集器線程都可以與用戶線程一起工作,所以是併發執行的
- 缺點:
- 併發階段,雖然不會導致用戶線程暫停,但是會佔用一部分資源(CPU線程),導致應用變慢,吞吐量降低。默認啓動收集線程數是(CPU數量+3)/4。即當CPU在4個以上時,併發回收時垃圾收集線程不少於25%的CPU資源,並且隨着CPU數量的增加而下降。但是當CPU不足4個(譬如2個)時,CMS對用戶程序的影響就可能變得很大。
- 無法清除浮動垃圾。併發清除階段,用戶線程還在運行,還會產生新垃圾。這些垃圾不會再此次GC中被標記,只能等到下次GC被回收
- 標記-清除算法會產生大量不連續內存,導致分配大內存時內存不夠,提前觸發Full GC
10.7 G1
-
在JDK 1.7提供的先見垃圾收集器
-
即使用與新生代,也適用於老年代
-
空間整合:使用標記-整理算法,不產生碎片空間
-
整個java堆被分成多個大小相等的塊。新生代和老年代不再是物理隔離而是一部分region塊組成的集合
-
默認把堆平均分爲2048個region,最小1M最大32M,必須是2的冪次方,可以通過-xx:G1HeapRegionSize參數指定
region
- E:eden區,新生代
- S:survivor區,新生代
- O:old區,老年代
- H:humongous區,用來放大對象,當對象新建大小超過region大小一半時,直接在新的一個或多個連續region中分配
-
可預測的停頓時間:估算每個region內的垃圾可回收空間以及回收需要的時間,記錄在一個優先列表中。收集時,優先回收價值最大的region,而不是整個堆進行全區域回收。這樣提高了回收效率
-
young GC:新生代eden區沒有足夠可用空間時觸發。存活對象移到survivor區,或晉升old區
-
mixed GC:當old區對象很多時,老年代對象空間佔堆總空間的比值達到閾值會觸發,它除了回收年輕代,也回收部分老年代
- 回收步驟
- 初始標記:只是標記一下GC Roots能直接關聯到的對象,並且修改TAMS的值,讓下一階段用戶程序併發運行時,能在正確可用的Region中創建新對象。這階段需要停頓線程(STW),但耗時很短,共用YGC的停頓,所以一般伴隨着YGC發生。
- 併發標記:進行可達性分析,找出存活對象,耗時長,但可與用戶線程併發執行。
- 最終標記:修正併發標記階段用戶線程運行導致的變動記錄。會STW,但可以並行執行,時間不會很長。
- 篩選回收:根據每個region的回收價值和回收成本排序,根據用戶配置的GC停頓時間開始回收。
- 回收步驟
-
當對象分配過快,mixed GC來不及回收,G1會退化,觸發Full GC,它使用單線程的Serial收集器來回收,整個過程STW,要儘量避免這種情況。
-
當內存很少的時候(存活對象佔用大量空間),沒有足夠空間來複制對象,會導致回收失敗。這時會保留被移動過的對象和沒移動的對象,只調整引用。失敗發生後,收集器認爲存活對象被移動了,有足夠空間讓應用程序使用,於是用戶線程繼續工作,等待下一次觸發GC。如果內存不夠,就會觸發Full GC。
10.8 ZGC
- ZGC是一個併發、基於區域、增量式壓縮的收集器,STW階段只會在根對象掃描階段發送,這樣GC暫停時間不會隨着堆和存活對象的數量而增加
- 處理階段
- 標記
- 重定位/壓縮
- 重新分配集的選擇
- 引用處理
- 弱引用的清理
- 字符串常量池和符號表的清理
- 類卸載
- 着色指針:
- ZGC利用指針的64位中的幾位表示Finalizable、Remapped、Marked1、Marked0(ZGC僅支持64位平臺),以標記該指向內存的存儲狀態。 相當於在對象的指針上標註了對象的信息。注意,這裏的指針相當於Java術語當中的引用。 在這個被指向的內存發生變化的時候(內存在Compact被移動時),顏色就會發生變化。
- 讀屏障
- 由於着色指針的存在,在程序運行時訪問對象的時候,可以輕易知道對象在內存的存儲狀態(通過指針訪問對象),
若請求讀的內存在被着色了,那麼則會觸發讀屏障。讀屏障會更新指針再返回結果,此過程有一定的耗費,從而達到與用戶線程併發的效果。
- 由於着色指針的存在,在程序運行時訪問對象的時候,可以輕易知道對象在內存的存儲狀態(通過指針訪問對象),
- 與標記對象的傳統算法相比,ZGC在指針上做標記,在訪問指針時加入Load Barrier(讀屏障),比如當對象正被GC移動,指針上的顏色就會不對,這個屏障就會先把指針更新爲有效地址再返回,也就是,永遠只有單個對象讀取時有概率被減速,而不存在爲了保持應用與GC一致而粗暴整體的Stop The World。
清理
- 字符串常量池和符號表的清理
- 類卸載
- 着色指針:
- ZGC利用指針的64位中的幾位表示Finalizable、Remapped、Marked1、Marked0(ZGC僅支持64位平臺),以標記該指向內存的存儲狀態。 相當於在對象的指針上標註了對象的信息。注意,這裏的指針相當於Java術語當中的引用。 在這個被指向的內存發生變化的時候(內存在Compact被移動時),顏色就會發生變化。
- 讀屏障
- 由於着色指針的存在,在程序運行時訪問對象的時候,可以輕易知道對象在內存的存儲狀態(通過指針訪問對象),
若請求讀的內存在被着色了,那麼則會觸發讀屏障。讀屏障會更新指針再返回結果,此過程有一定的耗費,從而達到與用戶線程併發的效果。
- 由於着色指針的存在,在程序運行時訪問對象的時候,可以輕易知道對象在內存的存儲狀態(通過指針訪問對象),
- 與標記對象的傳統算法相比,ZGC在指針上做標記,在訪問指針時加入Load Barrier(讀屏障),比如當對象正被GC移動,指針上的顏色就會不對,這個屏障就會先把指針更新爲有效地址再返回,也就是,永遠只有單個對象讀取時有概率被減速,而不存在爲了保持應用與GC一致而粗暴整體的Stop The World。