目錄
一、JVM記憶體模型
記憶體劃分
物件創建
常量池
二、類加載
類加載程序
類加載生命周期
加載
驗證
準備
決議
初始化
創建物件
類加載器
類加載器加載類的大致步驟
雙親委派模式
如何破壞雙親委派模式
三、垃圾回識訓制
什么是垃圾回收
MinorGC和MajorGC
垃圾判斷演算法
參考計數器法
根搜索演算法
垃圾回收演算法
標記-清除
復制演算法
標記-整理(標記-壓縮)
分代演算法
新生代如何進入老年代
如何觸發FullGC
記憶體溢位和記憶體泄漏
四、垃圾收集器
Serial垃圾收集器
ParNew垃圾收集器
Parallel Scavenge 垃圾收集器
Serial Old垃圾收集器
Parallel Old垃圾回收器
CMS收集器
一、JVM記憶體模型
記憶體劃分
JVM記憶體共分為堆、虛擬機堆疊,方法區,本地方法堆疊、程式計數器(暫存器),
- 堆:被所有執行緒共享的一塊記憶體區域,在虛擬機啟動的時候創建,用于存放物件實體,
- 虛擬機堆疊:是執行緒私有的,每個方法在執行的時候都會創建一個堆疊幀,堆疊幀存盤了區域變數,運算元堆疊,動態鏈接,方法回傳地址,
-
區域變數表:
區域變數表主要存放了編譯器可知的各種資料型別(boolean、byte、char、short、int、float、long、double)、物件參考(reference型別,它不同于物件本身,可能是一個指向物件起始地址的參考指標,也可能是指向一個代表物件的句柄或其他與此物件相關的位置)和returnAddress型別,區域變數表所需的記憶體空間在編譯期確定,當進入一個方法時,方法在堆疊幀中所需要分配的區域變數控制元件是完全確定的,不可動態改變大小,
-
運算元堆疊:
后進先出LIFO,最大深度由編譯期確定,堆疊幀剛建立使,運算元堆疊為空,執行方法操作時,運算元堆疊用于存放JVM從區域變數表復制的常量或者變數,提供提取,及結果入堆疊,也用于存放呼叫方法需要的引數及接受方法回傳的結果,
運算元堆疊可以存放一個jvm中定義的任意資料型別的值,
在任意時刻,運算元堆疊都一個固定的堆疊深度,基本型別除了long、double占用兩個深度,其它占用一個深度
-
動態連接:
每個堆疊幀都包含一個指向運行時常量池中該堆疊幀所屬方法的參考,持有這個參考是為了支持方法呼叫程序中的動態連接,Class檔案的常量池中存在有大量的符號參考,位元組碼中的方法呼叫指令就以常量池中指向方法的符號參考為引數,這些符號參考,一部分會在類加載階段或第一次使用的時候轉化為直接參考(如final、static域等),稱為靜態決議,另一部分將在每一次的運行期間轉化為直接參考,這部分稱為動態連接,
-
方法回傳地址:
當一個方法被執行后,有兩種方式退出該方法:執行引擎遇到了任意一個方法回傳的位元組碼指令(lreturn、freturn、dreturn以及areturn)或遇到了例外,并且該例外沒有在方法體內得到處理,無論采用何種退出方式,在方法退出之后,都需要回傳到方法被呼叫的位置,程式才能繼續執行,方法回傳時可能需要在堆疊幀中保存一些資訊,用來幫助恢復它的上層方法的執行狀態,一般來說,方法正常退出時,呼叫者的PC計數器的值就可以作為回傳地址,堆疊幀中很可能保存了這個計數器值,而方法例外退出時,回傳地址是要通過例外處理器來確定的,堆疊幀中一般不會保存這部分資訊,
方法退出的程序實際上等同于把當前堆疊幀出堆疊,因此退出時可能執行的操作有:恢復上層方法的區域變數表和運算元堆疊,如果有回傳值,則把它壓入呼叫者堆疊幀的運算元堆疊中,調整PC計數器的值以指向方法呼叫指令后面的一條指令,
-
方法區:執行緒共享的一塊記憶體區域,用于存盤已經被虛擬機加載的類資訊,常量,靜態變數等,
-
本地方法堆疊:執行緒私有的,與虛擬機堆疊類似,主要為虛擬機使用到的Native方法服務,
-
程式計數器:執行緒私有的,程式計數器指當前正在執行的位元組碼的行號,如果是Native方法,則為空,
物件創建
1、類加載檢查: 虛擬機遇到一條 new 指令時,首先會去檢查這個指令的引數是否能在常量池中定位到這個類的符號參考,并且檢查這個符號參考代表的類是否已被加載過、決議和初始化過,如果沒有,那必須先執行相應的類加載程序,
2、分配記憶體: 在類加載檢查通過后,接下來虛擬機將為新生物件分配記憶體,物件所需的記憶體大小在類加載完成后便可確定,為物件分配空間的任務等同于把一塊確定大小的記憶體從 Java 堆中劃分出來,分配方式有 “指標碰撞” 和 “空閑串列” 兩種,選擇那種分配方式由 Java 堆是否規整決定,而Java堆是否規整又由所采用的垃圾收集器是否帶有壓縮整理功能決定,
記憶體分配的兩種方式:
選擇以上兩種方式中的哪一種,取決于 Java 堆記憶體是否規整,而 Java 堆記憶體是否規整,取決于 GC 收集器的演算法是"標記-清除",還是"標記-整理"(也稱作"標記-壓縮"),值得注意的是,復制演算法記憶體也是規整的,

3、初始化零值: 記憶體分配完成后,虛擬機需要將分配到的記憶體空間都初始化為零值(不包括物件頭),這一步操作保證了物件的實體欄位在 Java 代碼中可以不賦初始值就直接使用,程式能訪問到這些欄位的資料型別所對應的零值,
4、設定物件頭: 初始化零值完成之后,虛擬機要對物件進行必要的設定,例如這個物件是那個類的實體、如何才能找到類的元資料資訊、哈希值、 gc分代年齡 、鎖狀態標志、 執行緒持有的鎖, 這些資訊存放在物件頭中, 另外,根據虛擬機當前運行狀態的不同,如是否啟用偏向鎖等,物件頭會有不同的設定方式,
5、執行 init 方法: 在上面作業都完成之后,從虛擬機的視角來看,一個新的物件已經產生了,但從 Java 程式的視角來看,物件創建才剛開始,<init> 方法還沒有執行,所有的欄位都還為零,所以一般來說,執行 new 指令之后會接著執行 <init> 方法,把物件按照程式員的意愿進行初始化,這樣一個真正可用的物件才算完全產生出來,
常量池
Java 基本型別的包裝類的大部分都實作了常量池技術,即Byte,Short,Integer,Long,Character,Boolean;這5種包裝類默認創建了數值[-128,127]的相應型別的快取資料,但是超出此范圍仍然會去創建新的物件,
兩種浮點數型別的包裝類 Float,Double 并沒有實作常量池技術,String也實作了常量池,比如:
public static void main(String[] args) {
String a="123";
String b="123";
String c=new String("123");
System.out.println(a==b); //true
System.out.println(a.equals(c));//true
}
因為a和b都是從常量池內取值,所以這倆個值相等,那a和c不應該回傳true啊,因為這倆物件在堆中的參考地址一定不同啊,這個時候需要一個新的知識點,==和equal的區別
基礎資料型別(Byte,Short,Integer,Long,Character,Boolean),== 與 equal 都是作用于比較物件內容(堆)是否相同,
參考物件型別, == 與 equal 都是作用于比較物件記憶體地址(堆疊)是否相同,
那既然是這樣a與c更應該是false,所有的類都繼承Object類,如果不重寫equals(),默認執行的是Object的equals()方法
public boolean equals(Object obj) {
return (this == obj);
}
String 類重寫了equals()方法,所以a,c是true,
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
對于基本資料型別,(Byte,Short,Integer,Long,Character,Boolean)這5種包裝類默認創建了數值[-128,127]的相應型別的快取資料,但是超出此范圍仍然會去創建新的物件,而Float和Double則沒有,所以創建物件后的參考地址必然不同,
public static void main(String[] args) {
Integer a=300;
Integer b=300;
Integer c=30;
Integer d=30;
System.out.println(a==b);//false
System.out.println(c==d);//true
}
總結:相同內容的物件地址不一定相同,但相同地址的物件內容一定相同,
二、類加載
類加載程序
當程式主動使用某個類時,如果該類還沒有被加載到記憶體,則JVM會通過加載、連接、初始化來對這個類進行初始化,
類加載生命周期

加載
加載,是指Java虛擬機查找位元組流(查找.class檔案),并且根據位元組流創建java.lang.Class物件的程序,這個程序,將類的.class檔案中的二進制資料讀入記憶體,放在運行時區域的方法區內,然后在堆中創建java.lang.Class物件,用來封裝類在方法區的資料結構,
類加載階段:
(1)Java虛擬機將.class檔案讀入記憶體,并為之創建一個Class物件,
(2)任何類被使用時系統都會為其創建一個且僅有一個Class物件,
(3)這個Class物件描述了這個類創建出來的物件的所有資訊,比如有哪些構造方法,都有哪些成員方法,都有哪些成員變數等,
驗證
驗證階段的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機的要求,并且不會危害虛擬機自身的安全,整體來看,驗證階段大致分為4個驗證動作,
1、檔案格式驗證
第一階段是驗證位元組流是否符合Class檔案格式的規范,并且能被當前版本的虛擬機處理,比如是否以魔數開頭,(為了方便虛擬機識別一個檔案是否是class型別的檔案,SUN公司規定每個class檔案都必須以一個word(四個位元組)作為開始,這個數字就是魔數,主、次版本號是否在當前虛擬機處理范圍內;常量池的常量資料型別是否被支持,
2、元資料驗證
元資料驗證是對位元組碼描述資訊進行語意分析,以保證其描述的資訊符合Java語言規范的要求,這個階段可能的驗證點:
a.是否有父類;
b.是否繼承了不被允許繼承的類;
c.如果該類不是抽象類,是否實作了其父類或介面要求實作的所有方法;
3、位元組碼驗證
位元組碼驗證的主要目的是通過資料流和控制流分析,確定程式語意的合法性和邏輯性,該階段將對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的事情,這個階段可能的驗證點:
a.保證任何時候運算元堆疊的資料型別與指令代碼序列的一致性;
b.跳轉指令不會跳轉到方法體以外的位元組碼指令上;
4、符號參考驗證
符號參考驗證的主要目的是保證決議動作能正常執行,如果無法通過符號參考驗證,則會拋出例外,這個階段可能的驗證點:
a.符號參考的類、欄位、方法的訪問性(public、private等)是否可被當前類訪問;
b.指定類是否存在符合方法的欄位描述符;
準備
為靜態變數分配記憶體,并將其初始化為默認值,
注意:
public static int value = 1;在準備階段的初始值是 0而不是1,而把value賦值的putstatic指令將在初始化階段才會被執行,
特殊情況:
public static final int value = 1;//此時準備value賦值為1,
決議
決議階段是虛擬機將常量池內的符號參考替換成直接參考的程序,直接參考是直接指向目標的指標,相對偏移量或是一個能間接定位到目標的句柄,
初始化
當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化,為初始化變數賦值,執行類構造器等,
創建物件
- new關鍵字創建 Class a=new A();此方法會呼叫建構式,
- 通過反射的物體類.newInstance(), Class.forName("com.xiaojie.entity.User") 全限定類名,此方法會呼叫無參建構式,
- constructor.newInstance(); 此方法會呼叫建構式,
- clone()克隆方法,此方法不會呼叫建構式,淺克隆是指拷貝物件時僅僅拷貝物件本身(包括物件中的基本變數),而不拷貝物件包含的參考指向的物件,深克隆不僅拷貝物件本身,而且拷貝物件包含的參考指向的所有物件,
- 使用反序列化,此方法可以進行深克隆,也不會呼叫建構式,
package com.xiaojie.entity;
/**
* @Description:
* @author: xiaojie
* @date: 2021.09.22
*/
public class User implements Cloneable{
private Long id;
private String name;
private Integer age;
public void setId(Long id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
public String getName() {
return name;
}
public Integer getAge() {
return age;
}
public Long getId() {
return id;
}
public User(Long id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
System.out.println("我是有參建構式,,,,,,");
}
public User() {
System.out.println("我是無參的建構式,,,,,");
}
@Override
public Object clone() throws CloneNotSupportedException {
//如果要進行深克隆,需要在此處重寫clone()方法,對參考型物件進行克隆
return super.clone();
}
}
public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, CloneNotSupportedException, IOException {
//1、new 關鍵字
User user=new User();//呼叫無參建構式
//2、通過反射的物體類.newInstance(), Class.forName("com.xiaojie.entity.User") 全限定類名
User user1 = User.class.newInstance();//呼叫無參建構式
Class<?> aClass = Class.forName("com.xiaojie.entity.User");
User user2 = (User) aClass.newInstance();//呼叫無參建構式
//3、constructor.newInstance();
Constructor<User> constructor = User.class.getConstructor();
User user3 = constructor.newInstance(); //呼叫無參建構式
Constructor<User> constructor1 =User.class.getConstructor(Long.class,String.class,Integer.class);
User user5 = constructor1.newInstance(1L, "tom", 18);//呼叫有參建構式
//4、使用clone方法 不會呼叫構造器
User user4= (User) user5.clone();
System.out.println(user4);//com.xiaojie.entity.User@f6f4d33
System.out.println(user5);//com.xiaojie.entity.User@23fc625e 可見復制后的物件并不相等,但是物件的屬性值是一樣的,
System.out.println(user5.getName()==user4.getName()); //true
//淺克隆是指拷貝物件時僅僅拷貝物件本身(包括物件中的基本變數),而不拷貝物件包含的參考指向的物件,
//深克隆不僅拷貝物件本身,而且拷貝物件包含的參考指向的所有物件,
//5、使反序列化,反序列化可以進行深克隆 不會呼叫構造器
ObjectInputStream in = new ObjectInputStream(new FileInputStream(""));
User user6 = (User) in.readObject();
}
類加載器
類加載器負責加載所有的類,其為所有被載入記憶體中的類生成一個java.lang.Class實體物件,一旦一個類被加載到JVM中,同一個類就不會被再次載入了,
JVM預定義的有三種類加載器
根類加載器(Bootstrap ClassLoader):或者叫啟動類加載器,它用來加載 Java 的核心類,是用原生代碼來實作的,并不繼承自 java.lang.ClassLoader(負責加載$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++實作,不是ClassLoader子類),由于引導類加載器涉及到虛擬機本地實作細節,開發者無法直接獲取到啟動類加載器的參考,所以不允許直接通過參考進行操作,
擴展類加載器(Extension ClassLoader):它負責加載JRE的擴展目錄,lib/ext或者由java.ext.dirs系統屬性指定的目錄中的JAR包的類,由Java語言實作,父類加載器為null,
系統類加載器(Application ClassLoader):被稱為系統(也稱為應用程式)類加載器,它負責在JVM啟動時加載來自Java命令的-classpath選項、java.class.path系統屬性,或者CLASSPATH換將變數所指定的JAR包和類路徑,程式可以通過ClassLoader的靜態方法getSystemClassLoader()來獲取系統類加載器,如果沒有特別指定,則用戶自定義的類加載器都以此類加載器作為父加載器,由Java語言實作,父類加載器為ExtClassLoader,
類加載器加載類的大致步驟
JVM的類加載機制主要有如下3種
全盤負責:所謂全盤負責,就是當一個類加載器負責加載某個Class時,該Class所依賴和參考其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入,
雙親委派:所謂的雙親委派,則是先讓父類加載器試圖加載該Class,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類,通俗的講,就是某個特定的類加載器在接到加載類的請求時,首先將加載任務委托給父加載器,依次遞回,如果父加載器可以完成類加載任務,就成功回傳;只有父加載器無法完成此加載任務時,才自己去加載,
快取機制:快取機制將會保證所有加載過的Class都會被快取,當程式中需要使用某個Class時,類加載器先從快取區中搜尋該Class,只有當快取區中不存在該Class物件時,系統才會讀取該類對應的二進制資料,并將其轉換成Class物件,存入緩沖區中,這就是為很么修改了Class后,必須重新啟動JVM,程式所做的修改才會生效的原因,

- 在加載之前會判斷快取區是否存在該類物件,如果存在則直接回傳相應的物件,
- 如果不存在,則判斷該類加載器是否有父類加載器,或者自己是一個父類加載器,根加載器,
- 如果有父類加載器,則委托父類加載器去加載(如果父類有父類依次遞回),如果父類加載器沒有找到該類,則自己去加載該類,加載成功回傳,加載失敗,拋出ClassNotFoundExcepton的例外,
- 如果是根類加載器則利用根類加載器加載對應的物件,加載成功回傳,加載失敗,拋出ClassNotFoundExcepton的例外,
雙親委派模式
雙親委派機制,其作業原理的是,如果一個類加載器收到了類加載請求,它并不會自己先去加載,而是把這個請求委托給父類的加載器去執行,如果父類加載器還存在其父類加載器,則進一步向上委托,依次遞回,請求最終將到達頂層的啟動類加載器,如果父類加載器可以完成類加載任務,就成功回傳,倘若父類加載器無法完成此加載任務,子加載器才會嘗試自己去加載,這就是雙親委派模式,這就是雙親委派模式,
雙親委派模式的好處:采用雙親委派模式的是好處是Java類隨著它的類加載器一起具備了一種帶有優先級的層次關系,通過這種層級關可以避免類的重復加載,當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次,再一個是考慮到安全因素,java核心api中定義型別不會被隨意替換,假設通過網路傳遞一個名為java.lang.Integer的類,通過雙親委托模式傳遞到啟動類加載器,而啟動類加載器在核心Java API發現這個名字的類,發現該類已被加載,并不會重新加載網路傳遞的過來的java.lang.Integer,而直接回傳已加載過的Integer.class,這樣便可以防止核心API庫被隨意篡改,
如何破壞雙親委派模式
雙親委派代碼
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 檢查該類是否已經加載過,如果加載過就直接回傳
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//沒加載過,呼叫父類加載器去加載,遞回呼叫
c = parent.loadClass(name, false);
} else {
//沒有父類就啟動啟動類去加載,這是個native方法
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//如果沒有找到拋出例外
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//如果還沒找到,則自己去加載該類
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
破壞雙親委派有兩種方式
1、自定義類加載器,重寫findClass();
package com.xiaojie.classloader;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
/**
* @Description: 自定義類加載器
* 使用場景
* (1)加密:Java代碼可以輕易的被反編譯,如果你需要把自己的代碼進行加密以防止反編譯,
* 可以先將編譯后的代碼用某種加密演算法加密,類加密后就不能再用Java的ClassLoader去加載類了,
* 這時就需要自定義ClassLoader在加載類的時候先解密類,然后再加載,
*
* (2)從非標準的來源加載代碼:如果你的位元組碼是放在資料庫、甚至是在云端,
* 就可以自定義類加載器,從指定的來源加載類,
*
* (3)以上兩種情況在實際中的綜合運用:比如你的應用需要通過網路來傳輸 Java 類的位元組碼,
* 為了安全性,這些位元組碼經過了加密處理,這個時候你就需要自定義類加載器來從某個網路地址上讀取
* 加密后的位元組代碼,接著進行解密和驗證,最后定義出在Java虛擬機中運行的類,
* @author: xiaojie
* @date: 2021.09.23
*/
public class MyClassLoader extends ClassLoader {
public MyClassLoader() {
}
public MyClassLoader(ClassLoader parent) {
super(parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
File file = new File("D:/People.class");
try {
byte[] bytes = getClassBytes(file);
//defineClass方法可以把二進制流位元組組成的檔案轉換為一個java.lang.Class
Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
return c;
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
private byte[] getClassBytes(File file) throws Exception {
// 這里要讀入.class的位元組,因此要使用位元組流
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer by = ByteBuffer.allocate(1024);
while (true) {
int i = fc.read(by);
if (i == 0 || i == -1)
break;
by.flip();
wbc.write(by);
by.clear();
}
fis.close();
return baos.toByteArray();
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
MyClassLoader mcl = new MyClassLoader();
Class<?> clazz = Class.forName("People", true, mcl);
Object obj = clazz.newInstance();
System.out.println(obj);
System.out.println("使用的類加載器是:" + obj.getClass().getClassLoader());
}
}
1、
2、使用執行緒背景關系類加載器,典型案例如JDBC連接,通過應用程式類加載器加載, ClassLoader loader = Thread.currentThread().getContextClassLoader();
三、垃圾回識訓制
什么是垃圾回收
垃圾回收(Garbage Collection,GC),顧名思義就是釋放垃圾占用的空間,防止記憶體泄露,有效的使用可以使用的記憶體,對記憶體堆中不可達的物件進行清除和回收,垃圾回收是自動進行回收的,不能人為控制,程式員唯一能做的就是通過呼叫System.gc() 方法來"建議"執行垃圾收集器,但其是否可以執行,什么時候執行卻都是不可知的,
MinorGC和MajorGC
新生代 GC(Minor GC):指發生在新生代的垃圾收集動作,因為 Java 物件大多都具備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快,
老年代 GC(Major GC / Full GC):指發生在老年代的 GC,出現了 Major GC,經常會伴隨至少一次的 Minor GC(但非絕對的,在 ParallelScavenge 收集器的收集策略里就有直接進行 Major GC 的策略選擇程序) ,MajorGC 的速度一般會比 Minor GC 慢 10倍以上,
垃圾判斷演算法
參考計數器法
參考計數法就是給物件中添加一個參考計數器,每當有一個地方參考它時,計數器值加1;當參考失效時,計數器值減1,任何時刻計數器值為0的物件就是不可能再被使用的,但是這種方法不能判斷物件相互參考的這種情況,
根搜索演算法
根搜索演算法的基本思路就是通過一系列名為”GC Roots”的物件作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為參考鏈(Reference Chain),當一個物件到GC Roots沒有任何參考鏈相連時,則證明此物件是不可達的,
GC ROOTS主要回收的區域
(1). 虛擬機堆疊(堆疊幀中的區域變數區,也叫做區域變數表)中參考的物件,
(2). 方法區中的類靜態屬性參考的物件,
(3). 方法區中常量參考的物件,
(4). 本地方法堆疊中JNI(Native方法)參考的物件,
垃圾回收演算法
標記-清除
標記-清除包含兩部分,標記和清除,一部分標記出可達的物件(有的人認為是標記不可達的物件),然后清除掉不可達的物件,
這種演算法的缺點是容易產生不連續的空間碎片,而且標記和清除的效率都不是很高,這種演算法適合老年代的物件回收,
復制演算法
記憶體會被分為兩部分From區和To區,每次只是使用from區,to區則空閑著,當from區記憶體不夠了,開始執行GC操作,這個時候,會把from區存活的物件拷貝到to區,然后直接把from區進行記憶體清理,
這種演算法的雖然避免了標記-清除碎片化的問題,但是如果回收物件較多較大需要花費更長的時間,而且總會有一部分空間是空閑的,浪費記憶體空間,這種演算法適用于新生代的物件 ,
標記-整理(標記-壓縮)
標記整理和標記清除演算法比較相同,也經過標記階段,然后把可達物件移動到一端,對不可達的物件進行洗掉,
這種演算法也解決了空間碎片化的問題,但是移動物件,需要修改物件的參考地址,而且標記,整理效率也不高,這種演算法適合老年代的物件回收,
分代演算法
這種演算法,根據物件的存活周期的不同將記憶體劃分成幾塊,新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集演算法,新生代物件朝生夕死,物件數量多,只要重點掃描這個區域,那么就可以大大提高垃圾收集的效率,另外老年代物件存盤久,無需經常掃描老年代,避免掃描導致的開銷,
新生代使用復制演算法,因為新生代中的物件一般都是朝生夕死的,存活物件的數量并不多,這樣使用復制演算法進行拷貝時效率比較高,jvm將堆記憶體劃分為新生代與老年代,又將新生代劃分為Eden與2塊Survivor Space,然后在Eden –>Survivor Space 以及From Survivor Space 與To Survivor Space 之間實行復制演算法,

堆空間中新生代和老年代的默認比例是1:2(可以通過引數 –XX:NewRatio)來設定,在新生代中Eden:From:To=8:1:1 (通過引數 –XX:SurvivorRatio )來設定,
復制演算法的程序
- 當Eden區滿的時候,會觸發第一次MinorGC,把還活著的物件拷貝到Survivor From區,這個時候存活的物件就1歲了,當eden區再次執行MinorGC,就會掃描Eden和From區,把存活的物件復制到To區,然后清空Eden和From區,
- 當Eden區再次滿了之后,再次觸發MinorGC,就會掃描Eden和To(新的From區)區,然后將存活的物件復制到From區(新的To區),然后清空Eden和To區,
- 這樣依次往復,在From和To區之間復制來復制去,每熬過一次MinorGC的物件就長大一歲,當物件年滿15歲之后,依然存活,則會進入老年代,
- 可以通過引數設定-XX:MaxTenuringThreshold=15 默認也是15次
注意:這種情況不考慮,破格直接進入老年代的情況,
老年代使用標記清除或者標記整理
老年代中因為物件存活率高、沒有額外空間對它進行分配擔保,就必須“標記-清除-壓縮”演算法進行回收,
新生代如何進入老年代
- 創建大物件直接進入老年代 -XX:PretenureSizeThreshold=1M 只對Serial及ParNew收集器管用,
- 新生代采用的是復制收集演算法,S0和S1始終只是用其中一塊記憶體區,當出現MinorGC后大部分物件仍然存活的話,就需要老年代進行空間分配擔保,把survior區無法容納的物件直接晉升到老年代,
- 長期存活的物件>15歲
- 當 Survivor 空間中相同年齡(比如10)所有物件的大小總和大于 Survivor 空間的一半,年齡大于或等于該年齡(10)的物件就可以直接進入老年代,而不需要達到MaxTenuringThreshold的分代年齡,
如何觸發FullGC
- System.gc()方法的呼叫(大多數的情況下都會進行fullGC,但不能百分之百保證),
- 當老年代沒有足夠空間存放物件時(認為達到92%這個數值僅供參考),會觸發一次FullGC,
- 空間分配擔保時,如果剩余空間不足以盛放新生代的物件,這時要進行一次FullGC
- 如果元空間區域的記憶體達到了所設定的閾值-XX:MetaspaceSize=,觸發FullGC,
記憶體溢位和記憶體泄漏
記憶體溢位(out of memory),是指程式在申請記憶體時,沒有足夠的記憶體空間供其使用,出現out of memory;比如系統就分給你10M的空間,你要存放20M的東西,這樣就會導致記憶體溢位,
產生原因:
1.記憶體中加載的資料量過于龐大,如一次從資料庫取出過多資料;
2.集合類中有對物件的參考,使用完后未清空,使得JVM不能回收;
3.代碼中存在死回圈或回圈產生過多重復的物件物體;
4.使用的第三方軟體中的BUG;
5.啟動引數記憶體值設定的過小
記憶體溢位解決方式就是增大jvm的記憶體
-Xmx3550m -Xms3550m 設定最大記憶體和初始化記憶體,兩者盡量一致,避免每次垃圾回收完成后JVM重新分配記憶體,
記憶體泄露(memory leak),是指程式在申請記憶體后,無法釋放已申請的記憶體空間,比如一個物件占用了10M的空間,但是它使用完了,一直不釋放,如果一次記憶體泄漏可以容忍,但是有很多的記憶體泄漏,不管有多少的記憶體遲早會被占用光,而導致的后果就是,記憶體溢位,
產生原因:
記憶體泄露的本質原因是因為代碼問題
- 不使用的物件不能被垃圾回識訓制回收,
- 使用完的資源記得關閉,比如io,資料庫等close(),
四、垃圾收集器
垃圾回收器有Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1、ZGC(jdk11之后),按照新生代和老年代來分,負責新生代的主要是 Serial、ParNew、Parallel Scavenge,負責老年代回收的是Serial Old、Parallel Old、CMS,而G1回收器可以對整個堆進行垃圾回收,

Serial垃圾收集器
特點:單執行緒、簡單高效(與其他收集器的單執行緒相比),對于限定單個CPU的環境來說,Serial收集器由于沒有執行緒互動的開銷,專心做垃圾收集自然可以獲得最高的單執行緒手機效率,收集器進行垃圾回收時,必須暫停其他所有的作業執行緒,直到它結束(Stop The World),使用新生代復制演算法,
應用場景:適用于Client模式下的虛擬機,
運行示意圖

ParNew垃圾收集器
和Serial完全一致,除了在收集器使用多執行緒外,
特點:多執行緒、ParNew收集器默認開啟的收集執行緒數與CPU的數量相同,在CPU非常多的環境中,可以使用-XX:ParallelGCThreads引數來限制垃圾收集的執行緒數,
和Serial收集器一樣存在Stop The World問題
應用場景:ParNew收集器是許多運行在Server模式下的虛擬機中首選的新生代收集器,因為它是除了Serial收集器外,唯一一個能與CMS收集器配合作業的,

Parallel Scavenge 垃圾收集器
Parallel Scavenge收集器是一個更關注吞吐量的收集器,與parnew 類似,
特點:屬于新生代收集器也是采用復制演算法的收集器,又是并行的多執行緒收集器(與ParNew收集器類似),
該收集器的目標是達到一個可控制的吞吐量,還有一個值得關注的點是:GC自適應調節策略(與ParNew收集器最重要的一個區別)
GC自適應調節策略:Parallel Scavenge收集器可設定-XX:+UseAdptiveSizePolicy引數,當開關打開時不需要手動指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRation)、晉升老年代的物件年齡(-XX:PretenureSizeThreshold)等,虛擬機會根據系統的運行狀況收集性能監控資訊,動態設定這些引數以提供最優的停頓時間和最高的吞吐量,這種調節方式稱為GC的自適應調節策略,
Parallel Scavenge收集器使用兩個引數控制吞吐量:
- XX:MaxGCPauseMillis 控制最大的垃圾收集停頓時間
- XX:GCRatio 直接設定吞吐量的大小,
Serial Old垃圾收集器
Serial Old是Serial收集器的老年代版本,
特點:同樣是單執行緒收集器,采用標記-整理演算法,
應用場景:主要也是使用在Client模式下的虛擬機中,也可在Server模式下使用,
Parallel Old垃圾回收器
是Parallel Scavenge收集器的老年代版本,
特點:多執行緒,采用標記-整理演算法,
應用場景:注重高吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge+Parallel Old 收集器,
CMS收集器

注意:“標記”是指將存活的物件和要回收的物件都給標記出來,而“清除”是指清除掉將要回收的物件,
其中,初始標記、重新標記這兩個步驟仍然需要“Stop The World”,
初始標記只是標記一下GC Roots能直接關聯到的物件,速度很快,
并發標記階段 :并不會阻礙用戶執行緒正常執行任務,與用戶執行緒并發執行進行標記,
重新標記階段則是為了修正并發標記期間因用戶程式繼續動作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比并發標記的時間短,
并發清除:對標記的物件進行清除回收,
CMS收集器的缺點:
- 對CPU資源非常敏感,
- 無法處理浮動垃圾,可能出現Concurrent Model Failure失敗而導致另一次Full GC的產生,
- 因為采用標記-清除演算法所以會存在空間碎片的問題,導致大物件無法分配空間,不得不提前觸發一次Full GC,
未完待續
性能調優,實戰線上問題排查
,,,,,
參考:
https://blog.csdn.net/qzqanzc/article/details/81008598
JVM記憶體模型_哦絕影-CSDN博客_記憶體模型
jvm之java類加載機制和類加載器(ClassLoader)的詳解_翻過一座座山-CSDN博客_類加載器
自定義類加載器 - twoheads - 博客園
https://www.cnblogs.com/chenpt/p/9803298.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/339306.html
標籤:其他
