目錄
一、JVM概述
1、1為什么要學習JVM
1、2虛擬機
1、3JVM作用
1、4JVM整體組成部分?
二、JVM結(jié)構(gòu)--類加載器
2、1類加載子系統(tǒng)
2、2類加載過程
2、2、1加載
2、2、2鏈接
2、2、3初始化
2、3類加載器分類
2.3.1 引導類加載器(啟動類加載器 BootStrap ClassLoader)
2.3.2 擴展類加載器(Extension ClassLoader)
2.3.3 應(yīng)用程序類加載器(系統(tǒng)類加載器 Application ClassLoader)
2.4 雙親委派機制
2.5 如何打破雙親委派機制
三、JVM運行時數(shù)據(jù)區(qū)
3、1運行時數(shù)據(jù)區(qū)組成概述
3.1.1 程序計數(shù)器(Program Counter Register)
3.1.2Java 虛擬機棧(Java Virtual Machine Stacks)
3.1.3 本地方法棧(Native Method Stack)
3.1.4Java 堆(Java Heap)
3.1.5 方法區(qū)(Methed Area)
3.2.程序計數(shù)器(Program Counter Register)
3.3.Java 虛擬機棧(Java Virtual Machine Stacks)
3.4.本地方法棧(Native Method Stack)
3.5.Java 堆內(nèi)存
3.5.1Java堆內(nèi)存概述
3.5.2堆內(nèi)存區(qū)域劃分
3.5.3 為什么分區(qū)(代)?
3.5.4 對象創(chuàng)建內(nèi)存分配過程
3.5.5 新生區(qū)與老年區(qū)配置比例
?3.5.6 分代收集思想 Minor GC、Major GC、Full GC
3.5.7 堆空間的參數(shù)設(shè)置
3.5.8 字符串常量池
3.6方法區(qū)
3.6.1 方法區(qū)的基本理解
3.6.2 方法區(qū)大小設(shè)置
3.6.3?方法區(qū)的內(nèi)部結(jié)構(gòu)
3.6.4?方法區(qū)的垃圾回收
四、本地方法接口
?4.1 什么是本地方法
4.2 為什么要使用 Native Method
五、執(zhí)行引擎
5.1 概述
5.2 什么是解釋器?什么是 JIT 編譯器?
5.3 為什么 Java 是半編譯半解釋型語言?
六、垃圾回收
6.1 垃圾回收概述
6.1.1 概述
6.1.2 什么是垃圾?
6.1.3 為什么需要 GC?
6.1.4 早期垃圾回收
6.1.5Java 垃圾回收機制
6.2 垃圾回收相關(guān)算法
6.2.1 垃圾標記階段算法
6.2.2 垃圾回收階段算法
6.3 垃圾回收相關(guān)概念
6.3.1System.gc() 的理解
6.3.2 內(nèi)存溢出與內(nèi)存泄漏
6.3.3Stop the World
6.4 垃圾回收器
6.4.1 垃圾回收器概述
6.4.2 垃圾回收器分類
6.4.3GC 性能指標
6.4.4HotSpot 垃圾收集器
6. 4.5CMS 回收器
6.4.6G1(Garbage First)回收器
6.4.7 查看 JVM 垃圾回收器設(shè)置垃圾回收器
一、JVM概述
1、1為什么要學習JVM
中高程序員必備技能:
項目管理、性能調(diào)優(yōu)
1、2虛擬機
- 虛擬機(Virtual Machine),虛擬計算機。他是一款軟件,用來執(zhí)行一系列虛擬計算機指令。大體上,虛擬機可以分為系統(tǒng)虛擬機和程序虛擬機。
- VMware屬于系統(tǒng)虛擬機,完全對物理計算機的仿真,提供一個可運行完整操作系統(tǒng)的平臺。程序虛擬機的典型代表是java虛擬機,專門為執(zhí)行某個計算機程序而設(shè)計。
在java虛擬機中執(zhí)行的指令稱為java字節(jié)碼指令。 - Java虛擬機是一種執(zhí)行java字節(jié)碼文件的虛擬機,它擁有獨立的運行機制。
- Java技術(shù)的核心就是Java虛擬機,因為所有的java程序都要在java虛擬機內(nèi)部運行。
1、3JVM作用
? ? ? ?Java虛擬機負責裝載字節(jié)碼到其內(nèi)部,解釋/編譯為對應(yīng)平臺上的機器碼指令執(zhí)行,每一條java指令,java虛擬機中都有詳細定義,如怎么取操作數(shù),怎么處理操作數(shù),處理結(jié)果放在哪。
特點:
- 一次編譯到處運行;
- 自動內(nèi)存管理;
- 自動垃圾回收功能;
? ? ? ?現(xiàn)在的JVM不僅可以執(zhí)行java字節(jié)碼文件,還可以執(zhí)行其他語言編譯后的字節(jié)碼文件,是一個跨語言平臺。
1、4JVM整體組成部分?
- 類加載器(ClassLoader)
- 運行時數(shù)據(jù)區(qū)(Runtime Data Area)
- 執(zhí)行引擎(Execution Engine)
- 本地庫接口(Native Interface)
? ? ? ??程序在執(zhí)行之前先要把 java 代碼轉(zhuǎn)換成字節(jié)碼(class 文件),jvm 首先需要把字節(jié)碼通過一定的方式 類加載器(ClassLoader) 把文件加載到內(nèi)存中的運行時數(shù)據(jù)區(qū)(Runtime Data Area) ,而字節(jié)碼文件是 jvm 的一套指令集規(guī)范,并不能直接交個底層操作系統(tǒng)去執(zhí)行,因此需要特定的命令解析器 執(zhí)行引擎(Execution Engine) 將字節(jié)碼翻譯成底層系統(tǒng)指令再交由CPU 去執(zhí)行,而這個過程中需要調(diào)用其他語言的接口 本地庫接口(Native
Interface) 來實現(xiàn)整個程序的功能,這就是這 4 個主要組成部分的職責與功能。? ? ? ?而我們通常所說的 JVM 組成指的是 運行時數(shù)據(jù)區(qū)(Runtime Data? Area) ,因為通常需要程序員調(diào)試分析的區(qū)域就是“運行時數(shù)據(jù)區(qū)”,或者更具體的來說就是“運行時數(shù)據(jù)區(qū)”里面的 Heap(堆)模塊。
二、JVM結(jié)構(gòu)--類加載器
2、1類加載子系統(tǒng)
? ? ? ? 類加載子系統(tǒng)負責從文件系統(tǒng)或者網(wǎng)絡(luò)中加載class文件。classLoader只負責class文件的加載,至于它是否可以裕興,則由Execution Engine決定。
? ? ? ?加載的類信息存放于一塊稱為方法區(qū)的內(nèi)存空間。
? ? ? ? class file 存在于硬盤上,可以理解為設(shè)計師畫在紙上的模板,而最終這個模板在執(zhí)行的時候是要加載 JVM 當中來,根據(jù)這個模板實例化出 n 個實例.
? ? ? ? class file 加載到 JVM 中,被稱為 DNA 元數(shù)據(jù)模板. 此過程就要有一個運輸工具(類加載器 Class Loader),扮演一個快遞員的角色
2、2類加載過程
2、2、1加載
- ?通過類名(地址)獲取此類的二進制字節(jié)流。
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)(元空間)的運行時結(jié)構(gòu)。
- 在內(nèi)存中生成一個代表這個類的java.lang.class對象,作為這個類的各種數(shù)據(jù)的訪問入口。
2、2、2鏈接
- 驗證:檢驗被加載的類是否有正確的內(nèi)部結(jié)構(gòu),并和其他類協(xié)調(diào)一致;
? ? ? ? ? ?驗證文件格式是否一致: class 文件在文件開頭有特定的文件標識(字節(jié)碼文件都以 CA FE BA BE 標識開頭);主,次版本號是否在當前 java 虛擬機接收范圍內(nèi).
? ? ? ? ? ?元數(shù)據(jù)驗證:對字節(jié)碼描述的信息進行語義分析,以保證其描述的信息符合java 語言規(guī)范的要求,例如這個類是否有父類;是否繼承瀏覽不允許被繼承的類(final 修飾的類)..... -
準備:準備階段則負責為類的靜態(tài)屬性分配內(nèi)存,并設(shè)置默認初始值;
? ? ? ? ? ?不包含用 final 修飾的 static 常量,在編譯時進行初始化.
? ? ? ? ? ?例如: public static int value = 123;value 在準備階段后的初始值是 0,而不是 123. - 解析:將類的二進制數(shù)據(jù)中的符號引用替換成直接引用(符號引用是 Class 文件的邏輯符號,直接引用指向的方法區(qū)中某一個地址)
2、2、3初始化
- 通過 new 關(guān)鍵字創(chuàng)建對象
- 訪問類的靜態(tài)變量,包括讀取和更新
- 訪問類的靜態(tài)方法
- 對某個類進行反射操作
- 初始化子類會導致父類的的初始化
- 執(zhí)行該類的 main 函數(shù)
除了以上幾種主動使用,以下情況被動使用,不會加載類:
-
引用該類的靜態(tài)常量,注意是常量,不會導致初始化,但是也有意外,這里的常量是指已經(jīng)指定字面量的常量,對于那些需要一些計算才能得出結(jié)果的常量就會導致類加載,比如:
public final static int NUMBER = 5 ; //不會導致類初始化,被動使用
public final static int RANDOM = new Random().nextInt() ; //會導致類加載 -
構(gòu)造某個類的數(shù)組時不會導致該類的初始化,比如:
Student[] students = new Student[10] ;
類的初始化順序
對static修飾的變量或語句塊進行賦值
? ? ? ?如果同時包含多個靜態(tài)變量和靜態(tài)代碼塊,則按照自上而下的順序依次執(zhí)行。
? ? ? ?如果初始化一個類的時候,其父類尚未初始化,則優(yōu)先初始化其父類。
? ? ? ?順序是:父類 static –> 子類 static
public class ClassInit{
static{
num = 20;
}
static int num = 10;
public static void main (String[] args) {
//num從準備到初始化值變化過程 num=0 -> num=20 -> num=10
System.out.println(num);//10
}
}
public class ClassInit{
static int num = 10;
static{
num = 20;
}
public static void main (String[] args) {
//num從準備到初始化值變化過程 num=0 -> num=10 -> num=20
System.out.println(num);//20
}
}
2、3類加載器分類
? ? ? ?站在JVM的角度看,類加載器可以分為兩種:
- 引導類加載器(啟動類加載器 Bootstrap ClassLoader).
- 其他所有類加載器,這些類加載器由 java 語言實現(xiàn),獨立存在于虛擬機外部,并 且全部繼承自抽象類 java.lang.ClassLoader.
2.3.1 引導類加載器(啟動類加載器 BootStrap ClassLoader)
2.3.2 擴展類加載器(Extension ClassLoader)
2.3.3 應(yīng)用程序類加載器(系統(tǒng)類加載器 Application ClassLoader)
2.4 雙親委派機制
?工作原理:
- 如果一個類加載器收到了類加載請求,它并不會自己先去加載,而是把這個請 求委托給父類的加載器去執(zhí)行.
- 如果父類加載器還存在其父類加載器,則進一步向上委托,依次遞歸,請求最終 將到達頂層的啟動類加載器.
- 如果父類加載器可以完成類的加載任務(wù),就成功返回,倘若父類加載器無法完 成加載任務(wù),子加載器才會嘗試自己去加載,這就是雙親委派機制.
-
如果均加載失敗,就會拋出 ClassNotFoundException 異常。
?雙親委派優(yōu)點:
- 安全,可避免用戶自己編寫的類替換 Java 的核心類,如 java.lang.String.
- 避免類重復加載,當父親已經(jīng)加載了該類時,就沒有必要子 ClassLoader 再加載一次
2.5 如何打破雙親委派機制
三、JVM運行時數(shù)據(jù)區(qū)
3、1運行時數(shù)據(jù)區(qū)組成概述
3.1.1 程序計數(shù)器(Program Counter Register)
3.1.2Java 虛擬機棧(Java Virtual Machine Stacks)
3.1.3 本地方法棧(Native Method Stack)
3.1.4Java 堆(Java Heap)
3.1.5 方法區(qū)(Methed Area)
? ? ? ?如圖: 紅色的為多個線程共享,灰色的為單個線程私有的,即 線程間共享:堆,方法區(qū). 線程私有:程序計數(shù)器,棧,本地方法棧.
3.2.程序計數(shù)器(Program Counter Register)
- 它是一塊很小的內(nèi)存空間,幾乎可以忽略不計,也是運行速度最快的存儲區(qū)域.
- 在 JVM 規(guī)范中,每個線程都有它自己的程序計數(shù)器,是線程私有的,生命周期與線程生命周期保持一致.
- 程序計數(shù)器會存儲當前線程正在執(zhí)行的 Java 方法的 JVM 指令地址.
- 它是程序控制流的指示器,分支,循環(huán),跳轉(zhuǎn),異常處理,線程恢復等基礎(chǔ)功能都需要依賴這個計數(shù)器來完成.
- 它是唯一一個在java虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域
3.3.Java 虛擬機棧(Java Virtual Machine Stacks)
棧中會出現(xiàn)異常,當線程請求的棧深度大于虛擬機所允許的深度時 , 會出現(xiàn)StackOverflowError.
- JVM 直接對 java 棧的操作只有兩個,就是對棧幀的入棧和出棧,遵循先進后出/后進先出的原則.
- 在一條活動的線程中,一個時間點上,只會有一個活動棧.即只有當前在執(zhí)行的方法的棧幀(棧頂)是有效地,這個棧幀被稱為當前棧(Current Frame),與當前棧幀對應(yīng)的方法稱為當前方法(CurrentMethod),定義這個方法的類稱為當前類(Current Class).
- 執(zhí)行引擎運行的所有字節(jié)碼指令只針對當前棧幀進行操作.
- 如果在該方法中調(diào)用了其他方法,對應(yīng)的新的棧幀就會被創(chuàng)建出來,放在棧的頂端,成為新的當前棧幀.
?? ? ? ?不同線程中所包含的棧幀(方法)是不允許存在相互引用的,即不可能在一個棧中引用另一個線程的棧幀(方法).
? ? ? ?如果當前方法調(diào)用了其他方法,方法返回之際,當前棧幀會傳回此方法的執(zhí)行結(jié)果給前一個棧幀,接著虛擬機會丟棄當前棧幀,使得前一個棧幀重新成為當前棧幀.
? ? ? ?Java 方法有兩種返回的方式,一種是正常的函數(shù)返回,使用 return 指令,另一種是拋出異常.不管哪種方式,都會導致棧幀被彈出.
-
局部變量表(Local Variables)
? ? ? ?局部變量表是一組變量值存儲空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。對于基本數(shù)據(jù)類型的變量,則直接存儲它的值,對于引用類型的變量,則存的是指向?qū)ο蟮囊谩? -
操作數(shù)棧(Operand Stack)(或表達式棧)
? ? ? ?棧最典型的一個應(yīng)用就是用來對表達式求值。在一個線程執(zhí)行方法的過程中,實際上就是不斷執(zhí)行語句的過程,而歸根到底就是進行計算的過程。因此可以這么說,程序中的所有計算過程都是在借助于操作數(shù)棧來完成的。 -
動態(tài)鏈接(Dynamic Linking) (或指向運行時常量池的方法引用)
? ? ? ?因為在方法執(zhí)行的過程中有可能需要用到類中的常量,所以必須要有一個引用指向運行時常量。 -
方法返回地址(Retuen Address)(或方法正常退出或者異常退出的定義)
? ? ? ?當一個方法執(zhí)行完畢之后,要返回之前調(diào)用它的地方,因此在棧幀中必須保存一個方法返回地址。
3.4.本地方法棧(Native Method Stack)
- Java 虛擬機棧管理 java 方法的調(diào)用,而本地方法棧用于管理本地方法的調(diào)用.
- 本地方法棧也是線程私有的.
- 允許被實現(xiàn)成固定或者是可動態(tài)擴展的內(nèi)存大小.內(nèi)存溢出方面也是相同的.
- 如果線程請求分配的棧容量超過本地方法棧允許的最大容量拋出 StackOverflowError.?本地方法是用 C 語言寫的.
- 它的具體做法是在 Native Method Stack 中登記 native 方法,在 Execution Engine 執(zhí)行時加載本地方法庫.
3.5.Java 堆內(nèi)存
3.5.1Java堆內(nèi)存概述
- 一個 JVM 實例只存在一個堆內(nèi)存,堆也是 Java 內(nèi)存管理的核心區(qū)域.
- Java 堆區(qū)在 JVM 啟動時的時候即被創(chuàng)建,其空間大小也就確定了,是 JVM 管理的最大一塊內(nèi)存空間.
-
堆內(nèi)存的大小是可以調(diào)節(jié).
? ? ? ?例如: -Xms:10m(堆起始大小) -Xmx:30m(堆最大內(nèi)存大小)
? ? ? ?一般情況可以將起始值和最大值設(shè)置為一致,這樣會減少垃圾回收之后堆內(nèi)存重新分配大小的次數(shù),提高效率. - 《Java 虛擬機規(guī)范》規(guī)定,堆可以處于物理上不連續(xù)的內(nèi)存空間中,但邏輯上它應(yīng)該被視為連續(xù)的.
- 所有的線程共享 Java 堆,在這里還可以劃分線程私有的緩沖區(qū).
- 《Java 虛擬機規(guī)范》中對 Java 堆的描述是:所有的對象實例都應(yīng)當在運行時分配在堆上.
- 在方法結(jié)束后,堆中的對象不會馬上被移除,僅僅在垃圾收集的時候才會被移除.
- 堆是 GC(Garbage Collection,垃圾收集器)執(zhí)行垃圾回收的重點區(qū)域.
3.5.2堆內(nèi)存區(qū)域劃分

3.5.3 為什么分區(qū)(代)?
? ? ? ?將對象根據(jù)存活概率進行分類,對存活時間長的對象,放到固定區(qū),從而減少掃描垃圾時間及 GC 頻率。? ? ? ?針對分類進行不同的垃圾回收算法,對算法揚長避短。
3.5.4 對象創(chuàng)建內(nèi)存分配過程
- new 的新對象先放到伊甸園區(qū),此區(qū)大小有限制.
- 當伊甸園的空間填滿時,程序又需要創(chuàng)建對象時,JVM 的垃圾回收器將對伊甸園區(qū)進行垃圾回收(Minor GC),將伊甸園區(qū)中的不再被引用的對象進行銷毀.再加載新的對象放到伊甸園區(qū).
- 然后將伊甸園區(qū)中的剩余對象移動到幸存者 0 區(qū).
- 如果再次出發(fā)垃圾回收,此時上次幸存下來存放到幸存者 0 區(qū)的對象,如果沒有回收,就會被放到幸存者 1 區(qū),每次會保證有一個幸存者區(qū)是空的.
- 如果再次經(jīng)歷垃圾回收,此時會重新放回幸存者 0 區(qū),接著再去幸存者 1 區(qū).
-
什么時候去養(yǎng)老區(qū)呢?默認是 15 次,也可以設(shè)置參數(shù),最大值為 15
? ? ? ?-XX:MaxTenuringThreshold=<N>
? ? ? ?在對象頭中,它是由 4 位數(shù)據(jù)來對 GC 年齡進行保存的,所以最大值為 1111,即為15。所以在對象的 GC 年齡達到 15 時,就會從新生代轉(zhuǎn)到老年代。 - 在老年區(qū),相對悠閑,當養(yǎng)老區(qū)內(nèi)存不足時,再次觸發(fā) Major GC,進行養(yǎng)老區(qū)的內(nèi)存清理.
- 若養(yǎng)老區(qū)執(zhí)行了 Major GC 之后發(fā)現(xiàn)依然無法進行對象保存,就會產(chǎn)生 OOM 異常. Java.lang.OutOfMemoryError:Java heap space
例如:
public static void main(String[] args) {
List<Integer> list = new ArrayList();
while(true){
list.add(new Random().nextInt());
}
}
3.5.5 新生區(qū)與老年區(qū)配置比例
- 默認**-XX:NewRatio**=2,表示新生代占 1,老年代占 2,新生代占整個堆的 1/3
- 可以修改**-XX:NewRatio**=4,表示新生代占 1,老年代占 4,新生代占整個堆的 1/5
-
當發(fā)現(xiàn)在整個項目中,生命周期長的對象偏多,那么就可以通過調(diào)整老年代的大小,來進行調(diào)優(yōu)
- 在 HotSpot 中,Eden 空間和另外兩個 survivor 空間缺省所占的比例是 8 : 1 :1,當然開發(fā)人員可以通過選項**-XX:SurvivorRatio**調(diào)整這個空間比例。比如-XX:SurvivorRatio=8,新生區(qū)的對象默認生命周期超過 15 ,就會去養(yǎng)老區(qū)養(yǎng)老
?3.5.6 分代收集思想 Minor GC、Major GC、Full GC
3.5.7 堆空間的參數(shù)設(shè)置
-XX:+PrintFlagsInitial
|
查看所有參數(shù)的默認初始值
|
-XX:+PrintFlagsFinal
|
查看所有參數(shù)的最終值(修改后的值)
|
-Xms
|
初始堆空間內(nèi)存(默認為物理內(nèi)存的 1/64)
|
-Xmx
|
最大堆空間內(nèi)存(默認為物理內(nèi)存的 1/4)
|
-Xmn
|
設(shè)置新生代的大小(初始值及最大值)
|
-XX:NewRatio
|
配置新生代與老年代在堆結(jié)構(gòu)的占比
|
-XX:SurvivorRatio
|
設(shè)置新生代中 Eden 和 S0/S1 空間比例
|
-XX:MaxTenuringTreshold
|
設(shè)置新生代垃圾的最大年齡
|
XX:+PrintGCDetails
|
輸出詳細的 GC 處理日志
|
3.5.8 字符串常量池
public static void main(String[] args) {
String temp = "world";
for (int i = 0; i < Integer.MAX_VALUE; i++) {
String str = temp + temp;
temp = str;
str.intern();//將字符串存儲到字符串常量池中
}
}
3.6方法區(qū)
3.6.1 方法區(qū)的基本理解
? ? ? ?方法區(qū),是一個被線程共享的內(nèi)存區(qū)域。其中主要存儲加載的類字節(jié)碼、class/method/field 等元數(shù)據(jù)、static final 常量、static 變量、即時編譯器編譯后的代碼等數(shù)據(jù)。另外,方法區(qū)包含了一個特殊的區(qū)域“運行時常量池”。

??? ? ?方法區(qū)在 JVM 啟動時被創(chuàng)建,并且它的實際的物理內(nèi)存空間中和 Java 堆區(qū)一樣都可以是不連續(xù)的.
? ? ? ?方法區(qū)的大小,跟堆空間一樣,可以選擇固定大小或者可擴展.
? ? ? ?方法區(qū)的大小決定了系統(tǒng)可以保存多少個類,如果系統(tǒng)定義了太多的類,導致方法區(qū)溢出, 虛擬機同樣會拋出內(nèi)存溢出的錯誤
? ? ? ?關(guān)閉 JVM 就會釋放這個區(qū)域的內(nèi)存.
?
3.6.2 方法區(qū)大小設(shè)置
Java 方法區(qū)的大小不必是固定的,JVM 可以根據(jù)應(yīng)用的需要動態(tài)調(diào)整.
- 元數(shù)據(jù)區(qū)大小可以使用參數(shù)-XX:MetaspaceSize 和 -XX:MaxMataspaceSize 指定,替代上述原有的兩個參數(shù).
- 默認值依賴于平臺,windows 下,-XXMetaspaceSize 是 21MB,
- -XX:MaxMetaspaceSize 的值是-1,級沒有限制.
- 這個-XX:MetaspaceSize 初始值是 21M 也稱為高水位線 一旦觸及就會觸發(fā) Full GC.
- 因此為了減少 FullGC 那么這個-XX:MetaspaceSize 可以設(shè)置一個較高的值
3.6.3?方法區(qū)的內(nèi)部結(jié)構(gòu)
?javap -v -p Demo.class > test.txt
?
3.6.4?方法區(qū)的垃圾回收
- 有些人認為方法區(qū)(如 Hotspot 虛擬機中的元空間或者永久代)是沒有垃圾收集行為的,其實不然。《Java 虛擬機規(guī)范》對方法區(qū)的約束是非常寬松的,提到過可以不要求虛擬機在方法區(qū)中實現(xiàn)垃圾收集。
- 一般來說這個區(qū)域的回收效果比較難令人滿意,尤其是類型的卸載,條件相當苛刻。但是這部分區(qū)域的回收有時又確實是必要的。
方法區(qū)的垃圾收集主要回收兩部分內(nèi)容:運行時常量池中廢棄的常量和不再使用的類型。
- 該類所有的實例都已經(jīng)被回收,也就是 Java 堆中不存在該類及其任何派生子類的實例。
- 加載該類的類加載器已經(jīng)被回收,這個條件除非是經(jīng)過精心設(shè)計的可替換類加載器的場景,如 OSGi、JSP 的重加載等,否則通常是很難達成的。
- 該類對應(yīng)的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
四、本地方法接口
?4.1 什么是本地方法
4.2 為什么要使用 Native Method
-
與 java 環(huán)境外交互。
? ? ? ?有時 java 應(yīng)用需要與 java 外面的環(huán)境交互,這是本地方法存在的主要原因。 你可以想想 java 需要與一些底層系統(tǒng),如某些硬件交換信息時的情況。本地方法正式這樣的一種交流機制:它為我們提供了一個非常簡潔的接口,而且我們無需去了解 java 應(yīng)用之外的繁瑣細節(jié)。 -
Sun 的解釋器是用 C 實現(xiàn)的,這使得它能像一些普通的 C 一樣與外部交互。
? ? ? ?jre大部分是用 java 實現(xiàn)的,它也通過一些本地方法與外界交互。例如:類 java.lang.Thread 的 setPriority()方法是用 Java 實現(xiàn)的,但是它實現(xiàn)調(diào)用的是該類里的本地方法 setPriority0()。
五、執(zhí)行引擎
5.1 概述
- 執(zhí)行引擎是 Java 虛擬機核心的組成部分之一。
- JVM 的主要任務(wù)是負責裝載字節(jié)碼到其內(nèi)部,但字節(jié)碼并不能夠直接運行在操作系統(tǒng)之上,因為字節(jié)碼指令并非等價于本地機器指令,它內(nèi)部包含的僅僅只是一些能夠被 JVM 所識別的字節(jié)碼指令、符號表,以及其他輔助信息。
- 那么,如果想要讓一個 Java 程序運行起來,執(zhí)行引擎(Execution Engine)的任務(wù)就是將字節(jié)碼指令解釋/編譯為對應(yīng)平臺上的本地機器指令才可以。簡單來說,JVM 中的執(zhí)行引擎充當了將高級語言翻譯為機器語言的譯者。
- 前端編譯:從 Java 程序員-字節(jié)碼文件的這個過程叫前端編譯.
- 執(zhí)行引擎這里有兩種行為:一種是解釋執(zhí)行,一種是編譯執(zhí)行(這里的是后端編譯)。
5.2 什么是解釋器?什么是 JIT 編譯器?
5.3 為什么 Java 是半編譯半解釋型語言?
- 當程序啟動后,解釋器可以馬上發(fā)揮作用,響應(yīng)速度快,省去編譯的時間,立即執(zhí)行。
- 編譯器要想發(fā)揮作用,把代碼編譯成本地代碼,需要一定的執(zhí)行時間,但編譯為本地代碼后,執(zhí)行效率高。就需要采用解釋器與即時編譯器并存的架構(gòu)來換取一個平衡點。
六、垃圾回收
6.1 垃圾回收概述
6.1.1 概述
- Java 和 C++語言的區(qū)別,就在于垃圾收集技術(shù)和內(nèi)存動態(tài)分配上,C++語言沒有垃圾收集技術(shù),需要程序員手動的收集。
- 垃圾收集,不是 Java 語言的伴生產(chǎn)物。早在 1960 年,第一門開始使用內(nèi)存動態(tài)分配和垃圾收集技術(shù)的 Lisp 語言誕生。
-
關(guān)于垃圾收集有三個經(jīng)典問題:
? ? ? ?哪些內(nèi)存需要回收?
? ? ? ?什么時候回收?
? ? ? ?如何回收? - 垃圾收集機制是 Java 的招牌能力,極大地提高了開發(fā)效率。如今,垃圾收集幾乎成為現(xiàn)代語言的標配,即使經(jīng)過如此長時間的發(fā)展,Java 的垃圾收集機制仍然在不斷的演進中,不同大小的設(shè)備、不同特征的應(yīng)用場景,對垃圾收集提出了新的挑戰(zhàn)。
6.1.2 什么是垃圾?
- 垃圾是指在運行程序中沒有任何引用指向的對象,這個對象就是需要被回收的垃圾。
- 如果不及時對內(nèi)存中的垃圾進行清理,那么,這些垃圾對象所占的內(nèi)存空間會一直保留到應(yīng)用程序結(jié)束,被保留的空間無法被其他對象使用。甚至可能導致內(nèi)存溢出。
6.1.3 為什么需要 GC?
- 對于高級語言來說,一個基本認知是如果不進行垃圾回收,內(nèi)存遲早都會被消耗完,因為不斷地分配內(nèi)存空間而不進行回收,就好像不停地生產(chǎn)生活垃圾而從來不打掃一樣。
- 除了釋放沒用的對象,垃圾回收也可以清除內(nèi)存里的記錄碎片。碎片整理將所占用的堆內(nèi)存移到堆的一端,以便 JVM 將整理出的內(nèi)存分配給新的對象。
6.1.4 早期垃圾回收
MibBridge *pBridge= new cmBaseGroupBridge();
//如果注冊失敗,使用 Delete 釋放該對象所占內(nèi)存區(qū)域
if(pBridge->Register(kDestroy)!=NO ERROR)
delete pBridge;
MibBridge *pBridge=new cmBaseGroupBridge();
pBridge->Register(kDestroy);
6.1.5Java 垃圾回收機制
?
6.2 垃圾回收相關(guān)算法
6.2.1 垃圾標記階段算法
- 在堆里存放著幾乎所有的 Java 對象實例,在 GC 執(zhí)行垃圾回收之前,首先需要區(qū)分出內(nèi)存中哪些是有用對象,哪些是垃圾對象。只有被標記為己經(jīng)是垃圾對象,GC 才會在執(zhí)行垃圾回收時,釋放掉其所占用的內(nèi)存空間,因此這個過程我們可以稱為垃圾標記階段。
- 那么在 JVM 中究竟是如何標記一個垃圾對象呢?簡單來說,當一個對象已經(jīng)不再被任何引用指向時,就可以宣判為垃圾對象。
- 判斷對象是否為垃圾對象一般有兩種方式:引用計數(shù)算法和可達性分析算法。
- 引用計數(shù)算法(Reference Counting)比較簡單,對每個對象保存一個整型的引用計數(shù)器屬性。用于記錄對象被引用的情況。
- 對于一個對象 A,只要有任何一個引用指向了對象 A,則對象 A 的引用計數(shù)器就加 1;當引用失效時,引用計數(shù)器就減 1。只要對象 A 的引用計數(shù)器的值為 0,即表示對象 A 不可能再被使用,可進行回收。
- 優(yōu)點:實現(xiàn)簡單,垃圾對象便于辨識;判定效率高,回收沒有延遲性。
-
缺點:
1.它需要單獨的字段存儲計數(shù)器,這樣的做法增加了存儲空間的開銷。
2.每次賦值都需要更新計數(shù)器,伴隨著加法和減法操作,這增加了時間開銷。
3.引用計數(shù)器有一個嚴重的問題,即無法處理循環(huán)引用的情況。這是一條致命缺陷,導致在.Java 的垃圾回收器中沒有使用這類算法。

?
- 相對于引用計數(shù)算法而言,可達性分析算法不僅同樣具備實現(xiàn)簡單和執(zhí)行高效等特點,更重要的是該算法可以有效地解決在引用計數(shù)算法中循環(huán)引用的問題,防止內(nèi)存泄漏的發(fā)生。
- 相較于引用計數(shù)算法,這里的可達性分析就是 Java、C#選擇的。這種類型的垃圾收集通常也叫作追蹤性垃圾收集(Tracing Garbage Collection)
?
?
-
虛擬機棧中引用的對象
比如:各個線程被調(diào)用的方法中使用到的參數(shù)、局部變量等。 -
方法區(qū)中類靜態(tài)屬性引用的對象
比如:Java 類的引用類型靜態(tài)變量 - 所有被同步鎖 synchronized 持有的對象
-
Java 虛擬機內(nèi)部的引用。
基 本 數(shù) 據(jù) 類 型 對 應(yīng) 的 Class 對 象 , 一 些 常 駐 的 異 常 對 象 ( 如 : NullPointerException、OutofMemoryError),系統(tǒng)類加載器。
protected void finalize() throws Throwable { }
- 在 finalize()時可能會導致對象復活。
- finalize()方法的執(zhí)行時間是沒有保障的,它完全由 GC 線程決定,極端情況下,若不發(fā)生 GC,則 finalize()方法將沒有執(zhí)行機會。
- 一個糟糕的 finalize()會嚴重影響 GC 的性能。比如 finalize 是個死循環(huán)。
6.2.1.5 生存還是死亡?
?
- 可觸及的:從根節(jié)點開始,可以到達這個對象。
- 可復活的:對象的所有引用都被釋放,但是對象有可能在 finalize()中復活。
- 不可觸及的:對象的 finalize()被調(diào)用,并且沒有復活,那么就會進入不可觸及狀態(tài)。
判定一個對象 objA 是否可回收,至少要經(jīng)歷兩次標記過程:
- 如果對象 objA 到 GC Roots 沒有引用鏈,則進行第一次標記。
- 進行篩選,判斷此對象是否有必要執(zhí)行 finalize()方法:
- 如果對象 objA 沒有重寫 finalize()方法,或者 finalize()方法已經(jīng)被虛擬機調(diào)用過,則虛擬機視為“沒有必要執(zhí)行”,objA 被判定為不可觸及的。
- 如果對象 objA 重寫了 finalize()方法,且還未執(zhí)行過,那么 objA 會被插入到隊列中,由一個虛擬機自動創(chuàng)建的、低優(yōu)先級的 Finalizer 線程觸發(fā)其finalize()方法執(zhí)行。
- finalize()方法是對象逃脫死亡的最后機會,稍后 GC 會對隊列中的對象進行第二次標記。如果 objA 在 finalize()方法中與引用鏈上的任何一個對象建立了聯(lián)系,那么在第二次標記時,objA 會被移出“即將回收”集合。之后,對象會再次出現(xiàn)沒有引用存在的情況。在這個情況下,finalize()方法不會被再次調(diào)用,對象會直接變成不可觸及的狀態(tài)。
6.2.2 垃圾回收階段算法
? ? ? ? 當成功區(qū)分出內(nèi)存中存活對象和死亡對象后,GC 接下來的任務(wù)就是執(zhí)行垃圾回收,釋放掉無用對象所占用的內(nèi)存空間,以便有足夠的可用內(nèi)存空間為新對象分配內(nèi)存。目前在 JVM 中比較常見的三種垃圾收集算法是:
- 標記-復制算法(Copying)
- 標記-清除算法(Mark-Sweep)
- 標記-壓縮算法(Mark-Compact)
6.2.2.1 標記-復制算法
? ? ? ? 它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。在垃 圾回收時將正在使用的內(nèi)存中的存活對象復制到未被使用的內(nèi)存塊中,之后清除 正在使用的內(nèi)存塊中的所有對象,交換兩個內(nèi)存的角色,最后完成垃圾回收。
?
復制算法的優(yōu)缺點
優(yōu)點
- 沒有標記和清除過程,實現(xiàn)簡單,運行高效
- 復制過去以后保證空間的連續(xù)性,不會出現(xiàn)“碎片”問題。
- 此算法的缺點也是很明顯的,就是需要兩倍的內(nèi)存空間。
- 對于 G1 這種分拆成為大量 region 的 GC,復制而不是移動,意味著 GC 需要維護 region 之間對象引用關(guān)系,不管是內(nèi)存占用或者時間開銷也不小.
復制算法的應(yīng)用場景
- 如果系統(tǒng)中的垃圾對象很多,復制算法需要復制的存活對象數(shù)量并不會太大,效率較高
- 老年代大量的對象存活,那么復制的對象將會有很多,效率會很低
- 在新生代,對常規(guī)應(yīng)用的垃圾回收,一次通??梢曰厥?70% - 99% 的內(nèi)存空間。回收性價比很高。所以現(xiàn)在的商業(yè)虛擬機都是用這種收集算法回收新生代。
?
6.2.2.2 標記-清除算法
執(zhí)行過程
當堆中的有效內(nèi)存空間被耗盡的時候,然后進行這項工作.
? ? ? ?清除:這里所謂的清除并不是真的置空,而是把需要清除的對象地址保存在空閑的地址列表里。下次有新對象需要加載時,判斷垃圾的位置空間是否夠,如果夠,就存放(也就是覆蓋原有的地址)
?
標記-清除算法的優(yōu)點:
? ? ? ?非?;A(chǔ)和常見的垃圾收集算法容易理解
標記-清除算法的缺點:
? ? ? ?標記清除算法的效率不算高
? ? ? ?這種方式清理出來的空閑內(nèi)存是不連續(xù)的,產(chǎn)生內(nèi)碎片。
?
6.2.2.3 標記-壓縮算法
背景
? ? ? ?復制算法的高效性是建立在存活對象少、垃圾對象多的前提下的。這種情況在新生代經(jīng)常發(fā)生,但是在老年代,更常見的情況是大部分對象都是存活對象。 如果依然使用復制算法,由于存活對象較多,復制的成本也將很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
? ? ? ?標記-清除算法的確可以應(yīng)用在老年代中,但是該算法不僅執(zhí)行效率低下,而且在執(zhí)行完內(nèi)存回收后還會產(chǎn)生內(nèi)存碎片,所以 JVM 的設(shè)計者需要在此基礎(chǔ)之上進行改進
?
標記壓縮算法執(zhí)行過程
第一階段和標記清除算法一樣,從根節(jié)點開始標記所有被引用對象
第二階段將所有的存活對象壓縮到內(nèi)存的一端,按順序排放。之后,清理邊界外所有的空間。
標記-壓縮算法與標記-清除算法的比較
? ? ? ?標記-壓縮算法的最終效果等同于標記-清除算法執(zhí)行完成后,再進行一次內(nèi)存碎片整理,因此,也可以把它稱為標記-清除-壓縮(Mark-Sweep-Compact)算法。
? ? ? ?二者的本質(zhì)差異在于標記-清除算法是一種非移動式的回收算法(空閑列表記錄位置),標記-壓縮是移動式的。是否移動回收后的存活對象是一項優(yōu)缺點并存的風險決策。
? ? ? ?可以看到,標記的存活對象將會被整理,按照內(nèi)存地址依次排列,而未被標記的內(nèi)存會被清理掉。如此一來,當我們需要給新對象分配內(nèi)存時JVM 只需要持有一個內(nèi)存的起始地址即可,這比維護一個空閑列表顯然少了許多開銷。
?
標記-壓縮算法的優(yōu)缺點
優(yōu)點
- 消除了標記-清除算法當中,內(nèi)存區(qū)域分散的缺點,我們需要給新對象分配內(nèi)存時,JVM 只需要持有一個內(nèi)存的起始地址即可。
- 消除了復制算法當中,內(nèi)存減半的高額代價。
- 從效率上來說,標記-壓縮算法要低于復制算法。
- 移動對象的同時,如果對象被其他對象引用,則還需要調(diào)整引用的地址
- 移動過程中,需要全程暫停用戶應(yīng)用程序。即:STW
6.2.2.4 垃圾回收算法小結(jié)
? ? ? ? 效率上來說,復制算法是當之無愧的老大,但是卻浪費了太多內(nèi)存。而為了盡量兼顧上面提到的三個指標,標記-壓縮算法相對來說更平滑一些,但是效率上不盡如人意,它比復制算法多了一個標記的階段,比標記-清除多了一個整理內(nèi)存的階段。
標記清除 | 標記整理 | 復制 | |
速率 | 中等 | 最慢 | 最快 |
空間開銷 | 少(會堆積碎片) | 少(無堆積碎片) | 通常需要活動對象的兩倍空間(無堆積碎片) |
移動對象 | 否 | 是 | 是 |
6.2.2.5 分代收集
為什么要使用分代收集
? ? ? ?前面所有這些算法中,并沒有一種算法可以完全替代其他算法,它們都具有自己獨特的優(yōu)勢和特點。分代收集應(yīng)運而生。
? ? ? ?分代收集,是基于這樣一個事實:不同的對象的生命周期是不一樣的。因此,不同生命周期的對象可以采取不同的收集方式,以便提高回收效率。一般是把 Java堆分為新生代和老年代,這樣就可以根據(jù)各個年代的特點使用不同的回收算法,以提高垃圾回收的效率。
? ? ? ?在 Java 程序運行的過程中,會產(chǎn)生大量的對象,其中有些對象是與業(yè)務(wù)信息相關(guān): 比如 Http 請求中的 Session 對象、線程、Socket 連接,這類對象跟業(yè)務(wù)直接掛鉤,因此生命周期比較長。? ? ? ? ?但是還有一些對象,主要是程序運行過程中生成的臨時變量,這些對象生命周期會比較短,比如:String 對象,由于其不變類的特性,系統(tǒng)會產(chǎn)生大量的這些對象,有些對象甚至只用一次即可回收。
? ? ? ?目前幾乎所有的 GC 都采用分代手機算法執(zhí)行垃圾回收的在 HotSpot 中,基于分代的概念,GC 所使用的內(nèi)存回收算法必須結(jié)合年輕代和老年代各自的特點。
年輕代(Young Gen)
年輕代特點:區(qū)域相對老年代較小,對象生命周期短、存活率低,回收頻繁。
? ? ? ?這種情況復制算法的回收整理,速度是最快的。復制算法的效率只和當前存活對象大小有關(guān),因此很適用于年輕代的回收。而復制算法內(nèi)存利用率不高的問題,通過 hotspot 中的兩個 survivor 的設(shè)計得到緩解。
老年代(Tenured Gen)
老年代特點:區(qū)域較大,對象生命周期長、存活率高,回收不及年輕代頻繁。
? ? ? ?這種情況存在大量存活率高的對象,復制算法明顯變得不合適。一般是由標記- 清除或者是標記-清除與標記-壓縮的混合實現(xiàn)。
- Mark 階段的開銷與存活對象的數(shù)量成正比。
- Sweep 階段的開銷與所管理區(qū)域的大小成正相關(guān)。
- Compact 階段的開銷與存活對象的數(shù)據(jù)成正比。
分代的思想被現(xiàn)有的虛擬機廣泛使用。幾乎所有的垃圾回收器都區(qū)分新生代和老年代。
6.3 垃圾回收相關(guān)概念
6.3.1System.gc() 的理解
? ? ? ?在默認情況下,通過 System.gc()者 Runtime.getRuntime().gc() 的調(diào)用,會顯式觸發(fā) Full GC,同時對老年代和新生代進行回收,嘗試釋放被丟棄對象占用的內(nèi)存。
? ? ? ?然而 System.gc()調(diào)用附帶一個免責聲明,無法保證對垃圾收集器的調(diào)用(不能確保立即生效)。
? ? ? ?JVM 實現(xiàn)者可以通過 System.gc() 調(diào)用來決定 JVM 的 GC 行為。而一般情況下,垃圾回收應(yīng)該是自動進行的,無須手動觸發(fā),否則就太過于麻煩了。在一些特殊情況下,我們可以在運行之間調(diào)用 System.gc()。
6.3.2 內(nèi)存溢出與內(nèi)存泄漏
內(nèi)存溢出
? ? ? ?內(nèi)存溢出相對于內(nèi)存泄漏來說,盡管更容易被理解,但是同樣的,內(nèi)存溢出也是引發(fā)程序崩潰的罪魁禍首之一。
? ? ? ?由于 GC 一直在發(fā)展,所有一般情況下,除非應(yīng)用程序占用的內(nèi)存增長速度非???,造成垃圾回收已經(jīng)跟不上內(nèi)存消耗的速度,否則不太容易出現(xiàn) OOM 的情況。
? ? ? ?大多數(shù)情況下,GC 會進行各種年齡段的垃圾回收,實在不行了就放大招,來一次獨占式的 Full GC 操作,這時候會回收大量的內(nèi)存,供應(yīng)用程序繼續(xù)使用。
? ? ? ?Javadoc 中對 OutofMemoryError 的解釋是,沒有空閑內(nèi)存,并且垃圾收集器也無法提供更多內(nèi)存。
內(nèi)存泄漏
? ? ? ?內(nèi)存泄漏也稱作“存儲滲漏”。嚴格來說,只有對象不會再被程序用到了,但是 GC 又不能回收他們的情況,才叫內(nèi)存泄漏。
? ? ? ?但實際情況很多時候一些不太好的實踐(或疏忽)會導致對象的生命周期變得很長甚至導致 OOM,也可以叫做寬泛意義上的“內(nèi)存泄漏”。
? ? ? ?盡管內(nèi)存泄漏并不會立刻引起程序崩潰,但是一旦發(fā)生內(nèi)存泄漏,程序中的可用內(nèi)存就會被逐步蠶食,直至耗盡所有內(nèi)存,最終出現(xiàn) OutofMemory 異常,導致程序崩潰。注意,這里的存儲空間并不是指物理內(nèi)存,而是指虛擬內(nèi)存大小,這個虛擬內(nèi)存大小取決于磁盤交換區(qū)設(shè)定的大小。
? ? ? ?注意,這里的存儲空間并不是指物理內(nèi)存,而是指虛擬內(nèi)存大小,這個虛擬內(nèi)存大小取決于磁盤交換區(qū)設(shè)定的大小。
常見例子
單例模式
? ? ? ?單例的生命周期和應(yīng)用程序是一樣長的,所以在單例程序中,如果持有對外部對象的引用的話,那么這個外部對象是不能被回收的,則會導致內(nèi)存泄漏的產(chǎn)生。
一些提供 close()的資源未關(guān)閉導致內(nèi)存泄漏
? ? ? ?數(shù)據(jù)庫連接 dataSourse.getConnection(),網(wǎng)絡(luò)連接 socket 和 io 連接必須手動 close,否則是不能被回收的。
6.3.3Stop the World
? ? ? ?Stop-the-World,簡稱 STW,指的是 GC 事件發(fā)生過程中,會產(chǎn)生應(yīng)用程序的停頓。停頓產(chǎn)生時整個應(yīng)用程序線程都會被暫停,沒有任何響應(yīng),有點像卡死的感覺,這個停頓稱為 STW。
可達性分析算法中枚舉根節(jié)點(GC Roots)會導致所有 Java 執(zhí)行線程停頓,為什么需要停頓所有 Java 執(zhí)行線程呢?
- 分析工作必須在一個能確保一致性的快照中進行
- 一致性指整個分析期間整個執(zhí)行系統(tǒng)看起來像被凍結(jié)在某個時間點上
- 如果出現(xiàn)分析過程中對象引用關(guān)系還在不斷變化,則分析結(jié)果的準確性無法保證,會出現(xiàn)漏標,錯標問題
- 被 STW 中斷的應(yīng)用程序線程會在完成 GC 之后恢復,頻繁中斷會讓用戶感覺像是網(wǎng)速不快造成電影卡帶一樣,所以我們需要減少 STW 的發(fā)生。
- 越優(yōu)秀,回收效率越來越高,盡可能地縮短了暫停時間。
? ? ? ?STW 是 JVM 在后臺自動發(fā)起和自動完成的。在用戶不可見的情況下,把用戶正常的工作線程全部停掉。
6.4 垃圾回收器
6.4.1 垃圾回收器概述
? ? ? ?如果說垃圾收集算法是內(nèi)存回收的方法論,那么收集器就是內(nèi)存回收的實踐者.
? ? ? ?垃圾收集器沒有在 java 虛擬機規(guī)范中進行過多的規(guī)定,可以由不同的廠商、不同版本的 JVM 來實現(xiàn)。
? ? ? ?由于 JDK 的版本處于高速迭代過程中,因此 Java 發(fā)展至今已經(jīng)衍生了眾多的垃圾回收器。從不同角度分析垃圾收集器,可以將 GC 分為不同的類型。
? ? ? ?實際使用時,可以根據(jù)實際的使用場景選擇不同的垃圾回收器,這也是 JVM 調(diào)優(yōu)的重要部
6.4.2 垃圾回收器分類
按線程數(shù)可以分為單線程(串行)垃圾回收器和多線程(并行)垃圾回收器
單線程垃圾回收器(Serial)
只有一個線程進行垃圾回收,使用于小型簡單的使用場景,垃圾回收時,其他用戶線程會暫停.

?
?按照工作模式分,可以分為獨占式和并發(fā)式垃圾回收器。
按工作的內(nèi)存區(qū)間分,又可分為年輕代垃圾回收器和老年代垃圾回收器。?
6.4.3GC 性能指標
- 垃圾收集開銷:垃圾收集所用時間與總運行時間的比例。
- 內(nèi)存占用:Java 堆區(qū)所占的內(nèi)存大小。
- 快速:一個對象從誕生到被回收所經(jīng)歷的時間。
?
6.4.4HotSpot 垃圾收集器
? ? ? ? 圖中展示了 7 種作用于不同分代的收集器,如果兩個收集器之間存在連線,則說明它們可以搭配使用。虛擬機所處的區(qū)域則表示它是屬于新生代還是老年代收集器。

?
6. 4.5CMS 回收器
CMS 概述
CMS(Concurrent Mark Sweep,并發(fā)標記清除)收集器是以獲取最短回收停頓時間為目標的收集器(追求低停頓),它在垃圾收集時使得用戶線程和 GC 線程并發(fā)執(zhí)行,因此在垃圾收集過程中用戶也不會感到明顯的卡頓。
垃圾回收過程
初始標記:
? ? ? ?Stop The World,僅使用一條初始標記線程對所有與 GC Roots 直接關(guān)聯(lián)的對象進行標記。
并發(fā)標記:
? ? ? ?垃圾回收線程,與用戶線程并發(fā)執(zhí)行。此過程進行可達性分析,標記出所有廢棄對象。
重新標記:
? ? ? ?Stop The World,使用多條標記線程并發(fā)執(zhí)行,將剛才并發(fā)標記過程中新出現(xiàn)的廢棄對象標記出來。
并發(fā)清除:
? ? ? ?只使用一條 GC 線程,與用戶線程并發(fā)執(zhí)行,清除剛才標記的對象。 這個過程非常耗時。
? ? ? ?并發(fā)標記與并發(fā)清除過程耗時最長,且可以與用戶線程一起工作,因此,總體上 說,CMS 收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)執(zhí)行的。
CMS 的優(yōu)點:
? ? ? ??可以作到并發(fā)收集
CMS 的弊端:
- CMS 是基于標記-清除算法來實現(xiàn)的,會產(chǎn)生內(nèi)存碎片。
- CMS 在并發(fā)階段,它雖然不會導致用戶線程停頓,但是會因為占用了一部分線程而導致應(yīng)用程序變慢,總吞吐量會降低。
- CMS 收集器無法處理浮動垃圾(floating garbage)。
?三色標記算法
? ? ? ?為了提高 JVM 垃圾回收的性能,從 CMS 垃圾收集器開始,引入了并發(fā)標記的概念。引入并發(fā)標記的過程就會帶來一個問題,在業(yè)務(wù)執(zhí)行的過程中,會對現(xiàn)有的引用關(guān)系鏈出現(xiàn)改變。
- 黑色:該對象已經(jīng)被標記過了,且該對象下的屬性也全部都被標記過了,例如GCRoots 對象。
- 灰色:對象已經(jīng)被垃圾收集器掃描過了,但是對象中還存在沒有掃描的引用(GC需要從此對象中去尋找垃圾);
- 白色:表示對象沒有被垃圾收集器訪問過,即表示不可達.
三色標記的過程:
為了解決并發(fā)的問題,引入中間狀態(tài)(灰色),當一個對象被標記的時候,會有下面幾個過程:
- 剛開始,確定為 GC Roots 的對象為黑色。
- 將 GC Roots 直接關(guān)聯(lián)的對象置為灰色。
- 遍歷灰色對象的所有引用,灰色對象本身置為黑色,其引用置為灰色。
- 重復步驟 3,直到?jīng)]有灰色對象為止。
- ?結(jié)束時,黑色對象存活,白色對象回收。
? ? ? ?這個過程正確執(zhí)行的前提是沒有其他線程改變對象間的引用關(guān)系,然而,并發(fā)標記的過程中,用戶線程仍在運行,因此就會產(chǎn)生漏標和錯標的情況。
漏標
假設(shè) GC 已經(jīng)在遍歷對象 B 了,而此時用戶線程執(zhí)行了 A.B=null 的操作,切斷了 A 到 B 的引用
?

?只要打破任一條件,就可以解決錯標的問題。
原始快照和增量更新
原始快照打破的是第一個條件:當灰色對象指向白色對象的引用被斷開時,就將這條引用關(guān)系記錄下來。當掃描結(jié)束后,再以這些灰色對象為根,重新掃描一次。
增量更新打破的是第二個條件:當黑色指向白色的引用被建立時,就將這個新的引用關(guān)系記錄下來,等掃描結(jié)束后,再以這些記錄中的黑色對象為根,重新掃描一次。相當于黑色對象一旦建立了指向白色對象的引用,就會變?yōu)榛疑珜ο蟆?
總結(jié)
? ? ? ?CMS 為了讓 GC 線程和用戶線程一起工作,回收的算法和過程比以前舊的收集器要復雜很多。究其原因,就是因為 GC 標記對象的同時,用戶線程還在修改對象的引用關(guān)系。因此 CMS 引入了三色算法,將對象標記為黑、灰、白三種顏色的對象,將用戶線程修改的引用關(guān)系記錄下來,以便在「重新標記」階段可以修正對象的引用。
? ? ? ?雖然 CMS 從來沒有被 JDK 當做默認的垃圾收集器,存在很多的缺點,但是它開啟了「GC 并發(fā)收集」的先河,為后面的收集器提供了思路。
6.4.6G1(Garbage First)回收器
既然我們已經(jīng)有了前面幾個強大的 GC,為什么還要發(fā)布 Garbage First(G1)GC?
? ? ? ? ?原因就在于應(yīng)用程序所應(yīng)對的業(yè)務(wù)越來越龐大、復雜,用戶越來越多,沒有GC 就不能保證應(yīng)用程序正常進行,而經(jīng)常造成 STW 的 GC 又跟不上實際的需求,所以才會不斷地嘗試對 GC 進行優(yōu)化。G1(Garbage-First)垃圾回收器是在 Java7 update 4 之后引入的一個新的垃圾回收器,是當今收集器技術(shù)發(fā)展的最前沿成果之一.
? ? ? ?與此同時,為了適應(yīng)現(xiàn)在不斷擴大的內(nèi)存和不斷增加的處理器數(shù)量,進一步降低暫停時間(pause time),同時兼顧良好的吞吐量。
? ? ? ?官方給 G1 設(shè)定的目標是在延遲可控的情況下獲得盡可能高的吞吐量,所以才擔當起“全功能收集器”的重任與期望。
G1 是一款面向服務(wù)端應(yīng)用的垃圾收集器。
為什么名字叫做 Garbage First(G1)呢?
? ? ? ?因為 G1 是一個并行回收器,它把堆內(nèi)存分割為很多不相關(guān)的區(qū)域(Region)(物理上不連續(xù)的邏輯上連續(xù)的)。使用不同的 Region 來表示 Eden、幸存者0 區(qū),幸存者 1 區(qū),老年代等。
? ? ? ?G1 GC 有計劃地避免在整個 Java 堆中進行全區(qū)域的垃圾收集。G1 跟蹤各個 Region 里面的垃圾堆積的價值大?。ɑ厥账@得的空間大小以及回收所需時間的經(jīng)驗值),在后臺維護一個優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先回收價值最大的 Region.
? ? ? ?由于這種方式的側(cè)重點在于回收垃圾最大量的區(qū)間(Region),所以我們給 G1 一個名字:垃圾優(yōu)先(Garbage First)。
? ? ? ?G1(Garbage-First)是一款面向服務(wù)端應(yīng)用的垃圾收集器,主要針對配備多核 CPU 及大容量內(nèi)存的機器,以極高概率滿足 GC 停頓時間的同時,還兼具高吞吐量的性能特征。
? ? ? ? 如下圖所示,G1 收集器收集器收集過程有初始標記、并發(fā)標記、最終標記、篩選回收,和 CMS 收集器前幾步的收集過程很相似:
- 初始標記:標記出 GC Roots 直接關(guān)聯(lián)的對象,這個階段速度較快,需要停止用戶線程,單線程執(zhí)行。
- 并發(fā)標記:從 GC Root 開始對堆中的對象進行可達新分析,找出存活對象,這個階段耗時較長,但可以和用戶線程并發(fā)執(zhí)行。
- 最終標記:修正在并發(fā)標記階段引用戶程序執(zhí)行而產(chǎn)生變動的標記記錄。
- 篩選回收:篩選回收階段會對各個 Region 的回收價值和成本進行排序,根據(jù)用戶所期望的 GC 停頓時間來指定回收計劃(用最少的時間來回收包含垃圾最多的區(qū)域.這就是 Garbage First 的由來——第一時間清理垃圾最多的區(qū)塊),這里為了提高回收效率,并沒有采用和用戶線程并發(fā)執(zhí)行的方式,而是停頓用戶線程。
適用場景:要求盡可能可控 GC 停頓時間;內(nèi)存占用較大的應(yīng)用。
6.4.7 查看 JVM 垃圾回收器設(shè)置垃圾回收器
打印默認垃圾回收器
-XX:+PrintCommandLineFlags -version
JDK 8 默認的垃圾回收器
年輕代使用 Parallel Scavenge GC
老年代使用 Parallel Old GC
打印垃圾回收詳細信息
-XX:+PrintGCDetails -version
設(shè)置默認垃圾回收器
Serial 回收器
-XX:+UseSerialGC 年輕代使用 Serial GC, 老年代使用 Serial Old GC
ParNew 回收器
-XX:+UseParNewGC 年輕代使用 ParNew GC,不影響老年代。
CMS 回收器文章來源:http://www.zghlxwxcb.cn/news/detail-770039.html
-XX:+UseConcMarkSweepGC 老年代使用 CMS GC。
# G1 回收器 文章來源地址http://www.zghlxwxcb.cn/news/detail-770039.html
-XX:+UseG1GC 手動指定使用 G1 收集器執(zhí)行內(nèi)存回收任務(wù)。
-XX:G1HeapRegionSize 設(shè)置每個 Region 的大小。
到了這里,關(guān)于JVM(Java虛擬機)-史上最全、最詳細JVM筆記的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!