JDK 、JRE、JVM
- JDK( Java Development Kit ) Java開發工具包
- JRE+ 開發命令工具(運行java.exe、編譯javac.exe、javaw.exe)
- JRE( Java Runtime Environment )Java運行環境
- JVM + Java核心類庫(lang、io、util)
- JVM( Java Virtual Mechinal )Java虛擬機
- 實現跨平臺的核心(一次編寫,到處運行)
- 編譯與解釋
- 計算機只認識低級語言(機器語言、匯編語言),而不認識高級語言(Java、C、Python)
- 編譯:通過編譯器,將高級語言編譯為低級語言,在Java語言中,編譯又分為前端編譯和后端編譯。
- 解釋:通過解釋器直接執行,不需要編譯成機器語言。(HotSpot引入了JIT技術)
- 編譯階段:
- .java文件 編譯 成.class javac 前端編譯 編譯期
- .class文件 翻譯成 機器指令 JVM 后端編譯 運行期
- Java編譯性還是解析性?編譯型+解釋型
- 反編譯 .class ->.java :jd-gui ;javap(簡易字節碼)、jad(jad xxx.class)7、cfr(語法長)
對象創建流程
- 類加載檢查
當虛擬機接收到一條new指令( ?)時,先去檢查這個指令的參數,是否能在常量池中定位到,這個類的符號引用,并且檢查這個符號引用代表的類是否已被加載、解析和初始化。如果沒有,則執行類加載過程。 - 分配內存
類加載檢查通過后,虛擬機將為新生對象分配內存,相當于將一塊同等大小的內存從Java堆中劃分出來。- 對象一定會被分配到堆上嗎?:如果JIT的逃逸分析后該對象沒有逃逸,那么可能優化到棧上分配。
- 簡單理解為:如果對象沒有被棧外引用,則直接創建到棧上,減少對象在堆上的分配和回收的開銷,提高程序的性能。
- JIT優化:https://blog.csdn.net/qq_39939541/article/details/131778650
- 對象一定會被分配到堆上嗎?:如果JIT的逃逸分析后該對象沒有逃逸,那么可能優化到棧上分配。
- 初始化
內存分配完成后,虛擬機需要將分配到的內存空間都初始化為零值,保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。- 零值: 基本數據類型會賦值為默認值(0 0l false),引用數據為賦值為null
- 設置對象頭
初始化零值之后,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。- 對象組成:對象頭、實例數據、對齊填充
- 執行 <init>
從虛擬機的角度已經創建完成一個新的對象,但從Java程序代碼來看,對象創建才剛剛開始,需要執行<init>方法,按照程序中設定的初始化去操作初始化,這時候一個真正的程序對象就生成了
<init>() :收集類中所有實例變量的賦值動作、實例代碼塊和構造函數合并產生的
內存分配方式
-
指針碰撞:默認 內存規整
- 指針加法:新對象分配內存,指針向“未用”區域移動,移動大小為新對象的占用內存大小
- 回收算法:標記復制,標記整理
-
空閑列表: 內存不規整
- 列表: 已使用和未使用的內存在堆中相互交錯,記錄那些內存塊可用,分配的時候從空余的內存塊中分配一塊和新對象的占用內存大小的給新對象
- 回收算法:標記清除
-
并發情況:CAS+TLAB
由于堆是全局共享的,如果使用指針碰撞的方式,當多線程分配對象時,指針會成為熱點資源,效率變低
首先在TLAB分配,當對象大于TLAB可用內存時候,再使用CAS+失敗重試策略
- TLAB 本地線程分配緩沖
- 每個線程預先劃分一塊區域,每個線程只需要在自己的區域內進行內存分配即可,不需要爭搶熱點指針,如果分配的區域內存使用完了,在重新申請即可。
- 僅在分配時候是獨占的,讀取依然是共享的
- 默認開啟,缺省 Eden 的1%:-XX:UserTLAB;-XX:TLABWasteTarget’Percent
TLAB問題:
- 空間浪費-內存孔隙:線程C,剩余空間1格,分配新對象需要2格
- 直接在堆分配:后續仍然大于1格,后續需要分配的大多數對象都需要在堆內存直接分配
- 廢棄TLAB,使用新的TLAB:頻繁廢棄TLAB,頻繁申請TLAB,并發問題
- JVM選擇策略:最大浪費空間 refill_waste 運行時不斷調整,使系統的運行狀態達到最優
- 最大浪費空間:允許浪費多少空間,不允許就是要保留,使用堆分配;允許浪費,就新建一個TLAB。一旦使用最大浪費空間,說明了原有的TLAB已經放不下對象了。
- 當請求對象大于refill_waste時,會選擇在堆中分配
- 反之,會廢棄當前TLAB,新建TLAB來分配新對象
- refill_waste:1.5格;新對象:2格
- 浪費后,仍然會產生孔隙;當發生 GC 的時候,TLAB 沒有用完,沒有分配的內存也會成為碎片
- CAS+重試
- 第一個線程搶占到了分配空間,第二個線程沒有搶占到就重試搶占后面一塊內存空間,保證更新操作的原子性對分配空間同步處理
- 誰搶上誰用,搶不上的繼續搶,保持同步
對象組成
mark world:
-
對象頭
- mark world:存儲對象自身的運行時數據
- hashcode哈希碼、GC分代年齡15(1111)
- 輕量級鎖指針、重量級鎖指針、GC標記、偏向鎖線程ID、偏向鎖時間戳
- 指針類型:指向對象的類元數據地址\Class對象,即對象代表哪個類(加載)
- 記錄數組長度:如果是數組對象,則對象頭中還有一部分用來記錄數組長度,該數據在32位和64位JVM中長度都是32bit
- mark world:存儲對象自身的運行時數據
-
實例數據
- 用來存儲對象真正的有效信息(包括父類繼承下來的和自己定義的)
- 在Java代碼中能看到的屬性和他們的值
-
對齊填充字節
- JVM要求Java的對象占的內存大小應該是8bit的倍數,所以后面有幾個字節用于把對象的大小補齊至8bit的倍數,沒有特別的功能
- https://blog.csdn.net/qq_35843514/article/details/119393207
一個空的Object對象占16個字節
- 在64位系統中,指針壓縮(前提)
- 開啟:markword占8個字節+類元指針占4個字節+padding填充4個字節=16個字節;
- 不開啟:markword占8個字節+類元指針占8個字節=16個字節;
- 啟用指針壓縮
- -XX:+UseCompressedOops(1.6默認開啟),禁止指針壓縮:-XX:-UseCompressedOops
- 如果不開啟這個指針壓縮,都是用8個字節來存儲這些對象的內存地址,這些信息放到堆里面,無形的就會增大很多空間,導致堆的壓力很大。很容易觸發gc
對象創建方式
- new創建新對象
- new Student()
- 通過反射機制
- Student.class.newInstance()\Student.class.getConstructor().newInstance();
- String str = (String)Class.forName(“java.lang.String”).newInstance();
- 采用clone機制
//1.實現Cloneable接口并復寫Object的clone方法
public class MyClass implements Cloneable {private int value;public MyClass(int value) {this.value = value;}public int getValue() {return value;}@Overridepublic MyClass clone() throws CloneNotSupportedException {return (MyClass) super.clone();}
}
//2.new MyClass().clone()
public class Main {public static void main(String[] args) throws CloneNotSupportedException {MyClass obj1 = new MyClass(10);MyClass obj2 = obj1.clone();System.out.println(obj1.getValue()); // 輸出:10System.out.println(obj2.getValue()); // 輸出:10}
}
- 通過反序列化機制
- 調用 ObjectInputStream 類的 readObject() 方法
序列化:指把 Java 對象轉換為字節序列的過程;
反序列化:指把字節序列恢復為 Java 對象的過程;
- 調用 ObjectInputStream 類的 readObject() 方法
類加載機制
類加載過程
類加載子系統在運行時第一次遇到一個class文件時就去加載、鏈接、初始化class文件
- 加載 Loading : 將類的.class文件加載到JVM中
類加載器 通過“包名 + 類名”,找到Class字節碼文件,創建一個java.lang.Class對象的實例來表示這個類,用,在方法區中存儲類的元數據,Class對象作為程序中每個類的數據訪問入口。 - 鏈接 Linking
- 驗證 Verify : 確保class文件字節流中的信息符合虛擬機規范,有沒有安全隱患
主要體現:文件格式、元數據、字節碼、符號引用 - 準備 Prepare :為類的“靜態變量”分配內存,并設置默認值/零值,初始化階段會顯式賦值(將值直接賦上)
不包含final修飾的,因為final修飾的在編譯時分配內存賦默認值了
例子:public static int value=123;
在準備階段之后賦為零值(int為0)-分配空間;在初始化階段才會真正賦為123-真正賦值
例子:public static final int value=123;
在準備階段直接賦為123(基本類型以及字符串常量,對象還是在初始化階段賦值) - 解析 Resolve:將常量池中的符號引用->直接引用
本類中如果用到了其他類,需要找到對應類,即將常量池中的符號引用->直接引用
主要體現:字段、接口、方法
- 驗證 Verify : 確保class文件字節流中的信息符合虛擬機規范,有沒有安全隱患
- 初始化 Initilization : 執行類構造器方法()的過程,主要完成“靜態變量賦值”以及“靜態代碼塊執行”
- 產生方式:類存在 static修飾的變量 或者靜態代碼塊 時,編譯器自動生成
- 什么時候會觸發類的初始化? 觸發時機:主動調用 懶加載
- 創建類的實例 new
- Class.forName(“”) 反射類
- 首次訪問這個類的靜態變量或靜態方法時
- 子類初始化,如果父類還沒初始化,會引發父類初始化;子類訪問父類的靜態變量,只會觸發父類的初始化;
- JVM啟動時會自動加載一些基礎類,例如java.lang.Object類和java.lang.Class類等
- 類的生命周期:
- 加載:
- 使用:
- 卸載:Java自帶的類是不會被回收掉的,只有自定義類加載器一些場景的類會被回收掉,如tomcat,SPI,JSP等臨時類,是存活不久的,所以需要來不斷回收
- 該類所有的實例都已被GC回收
- 該類的ClassLoader已經被GC回收
- 類對應的Class對象沒有被引用
類加載機制
雙親委派機制\父委派模型
當一個類加載器收到了類加載的請求的時候,他不會直接去加載指定的類,而是把這個請求委托給自己的父加載器去加載。只有父加載器無法加載這個類的時候,才會由子類加載器來負責類的加載
類加載器
通過一個類全限定名稱來獲取其二進制文件(.class)流的工具,被稱為類加載器。
或者說:找.class文件的工具,用于“加載”到JVM中
- Java語言系統中支持以下4種類加載器:各類加載器是組合關系來復用父加載器的代碼,不是繼承關系
- 啟動類加載器 Bootstrap ClassLoader JAVA_HOME\lib rt.jar resources.jar
用于加載 Java 的核心類,由底層的 C++ 實現,并不是一個 Java 類,無法被 Java 程序直接引用 - 擴展類加載器 Extention ClassLoader JAVA_HOME\lib\ext
用來加載java的擴展庫,開發者可以直接使用這個類加載器 - 應用\系統類加載器 Application ClassLoader 用戶路徑(classpath)上的類庫
用于加載程序員自己編寫的類。系統默認的類加載器 - 用戶自定義類加載器 User ClassLoader
自定義類加載器時,需要繼承 java.lang.ClassLoader 類 或 URLClassLoader,并至少重寫其中的findClass(Stringname)方法,若想打破雙親委托機制,需要重寫loadClass()方法
- 啟動類加載器 Bootstrap ClassLoader JAVA_HOME\lib rt.jar resources.jar
public class MyClassLoader extends ClassLoader{@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {return super.findClass(name);}@Overrideprotected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {return super.loadClass(name, resolve);}
}
雙親委派流程
- 先檢查類是否已經被加載過,如果已經被加載,就不需要加載 緩存
- 若沒有加載,則調用父加載器的 loadClass() 方法進行加載
- 若父加載器為空,則默認使用啟動類加載器作為父加載器
- 如果父類加載失敗,拋出 ClassNotFoundException 異常后,調用自己的 findClass() 方法進行加載
ClassLoader 方法:
- loadClass():如果父類加載器加載失敗,則會調用自己的findClass方法完成加載,默認的雙親委派機制在此方法中實現,保證了雙親委派規則。(類加載過程是線程安全的)
- findClass():根據名稱或位置加載 .class 字節碼
- definclass():把 .class 字節碼轉化為 Class 對象
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// 1. 檢查該類是否已經加載Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {//private final ClassLoader parent;// 2. 若沒有加載,則調用父加載器的 loadClass() 方法進行加載c = parent.loadClass(name, false);} else {// 3.若父加載器為空,則默認使用啟動類加載器作為父加載器BootstrapClassLoaderc = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {//父類無法完成加載請求,拋出 ClassNotFoundException 異常}if (c == null) {long t1 = System.nanoTime();// 4. 父類無法完成加載請求,拋出異常后,再調用自己的 findClass() 進行加載c = findClass(name);// 5. 記錄耗時sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}
}
補充:ClassNotFoundException和NoClassDefFoundError區別
- ClassNotFoundException:運行期異常exception
在類加載階段嘗試加載類的過程中,找不到類的定義時觸發
原因:Class.forName(“”)、類加載器loadClass、findSystemClass(),路徑沒有找到指定名稱的類,拋出該異常 - NoClassDefFoundError:編譯期通過,運行時報錯error
表示運行時嘗試加載一個類的定義時,雖然找到了類文件,但在加載、解析或鏈接類的過程中發生了問題
原因:依賴問題或類定義文件(.class文件)損壞導致的
依賴問題-interface模塊:不放邏輯,模塊中間調用接口,轉換類(邏輯類,異常類)
A、B類,B類引用A,后續A被刪除 - NoSuchMethodError:編譯期通過,運行時報錯error
表示方法找不到
原因:jar包沖突
雙親委派機制作用
- 避免類的重復加載
當父加載器已經加載過某一個類時,子加載器就不會再重新加載這個類 - 保護程序安全,防止核心API被隨意篡改
-
沙箱安全機制
在classpath下,要加載一個 java.lang.Integer 類的請求,通過雙親委派機制委派的啟動類加載器,發現存在Integer類直接返回,不會重新加載傳遞的過來的Integer類,只會加載JAVA_HOME中的jar包里面的類,可以防止核心API被隨意篡改。
-
打破雙親委派的情況下,可以替換java. 包的類嗎?不可以
=>無法替換 java. 包的類,即使打破雙親委派,依然需要調用父類中的 defineClass()方法 來把字節流轉換為一個JVM識別的 Class 對象,而 defineClass()方法 通過 preDefineClass()方法 限制類全限定名不能以 java. 開頭。 -
如何判斷JVM中類和其他類是不是同一個類?
取決于類加載器:每一個類加載器,都擁有一個獨立的類名稱空間
比較兩個類是否“相等”,只有在同一個類加載器下才有比較意義。即使這兩個類來源于同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等
-
//將字節流轉換成jvm可識別的java類protected final Class<?> defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)throws ClassFormatError{protectionDomain = preDefineClass(name, protectionDomain);//檢查類全限定名是否有效String source = defineClassSourceLocation(protectionDomain);Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);//調用本地方法,執行字節流轉JVM類的邏輯。postDefineClass(c, protectionDomain);return c;}
//檢查類名的有效性
private ProtectionDomain preDefineClass(String name,ProtectionDomain pd){if (!checkName(name))throw new NoClassDefFoundError("IllegalName: " + name);if ((name != null) && name.startsWith("java.")) { //禁止替換以java.開頭的類文件throw new SecurityException("Prohibited package name: " +name.substring(0, name.lastIndexOf('.')));}if (pd == null) {pd = defaultDomain;}if (name != null) checkCerts(name, pd.getCodeSource());return pd;}
打破雙親委派
不委派、向下委派:
某些情況下父類加載器需要委托子類加載器去加載class文件,受到加載范圍的限制,父類加載器無法加載到需要的文件
https://blog.csdn.net/laodanqiu/article/details/138817475
- 打破雙親委派方式
- 重寫 loadClass() 方法
- 利用線程上下文加載器
- 例子:JDBC注冊數據源驅動 4.0之后
=>對于DriverManager類由jdk提供,位于rt.jar,被啟動類加載器加載,而mysql的驅動jar包是有應用類加載器加載,當啟動類加載器加載完DriverManager類后,又將DriverManager委派給應用程序類加載器去加載mysql的驅動jar包,這里需要啟動類加載器來委托子類來加載Driver實現,從而破壞了雙親委派
// 1.加載數據訪問驅動
Class.forName("com.mysql.cj.jdbc.Driver");
//2.連接到數據"庫"上去
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/test?characterEncoding=GBK", "root", "");
//省略 Class.forName() 注冊驅動這一步,在JDBC4.0后,支持SPI的形式注冊Driver數據源
//當我們導入mysql-connector-java依賴jar包后,會生成 META-INF/services/java.sql.Driver ,java.sql.Driver中內容是“com.mysql.cj.jdbc.Drive”
public class DriverManager {static {loadInitialDrivers();println("JDBC DriverManager initialized");}private static void loadInitialDrivers() {//省略代碼//使用ServiceLoader.load()加載配置文件中指定的實現//Driver.class => java.sql.Driver -> 具體實現com.mysql.cj.jdbc.DriveServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();try{while(driversIterator.hasNext()) {driversIterator.next();}} catch(Throwable t) {// Do nothing}//省略代碼}
}
//獲取"線程上下文類加載器",即應用類加載器
public static <S> ServiceLoader<S> load(Class<S> service) {//獲取"線程上下文類加載器",類似于 ThreadLocal 將變量傳遞到整個線程的生命周期//這個值如果沒有特定設置,一般默認使用的是應用程序類加載器ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);
}
//在DriverManager的靜態代碼塊要執行的loadInitialDrivers(),中 driversIterator.next();
//具體實現:在ServiceLoader中的next()方法,
public S next() {if (acc == null) {return nextService();} else {PrivilegedAction<S> action = new PrivilegedAction<S>() {//會返回一個nextService(); public S run() { return nextService(); }};return AccessController.doPrivileged(action, acc);}
}
private S nextService() {if (!hasNextService())throw new NoSuchElementException();String cn = nextName;nextName = null;Class<?> c = null;try {//重點在于這里://cn是剛才通過spi獲取的Driver具體實現類:com.mysql.cj.jdbc.Drive//loader是剛才獲取的應用類加載器c = Class.forName(cn, false, loader);} catch (ClassNotFoundException x) {fail(service,"Provider " + cn + " not found");}if (!service.isAssignableFrom(c)) {fail(service,"Provider " + cn + " not a subtype");}try {S p = service.cast(c.newInstance());providers.put(cn, p);return p;} catch (Throwable x) {fail(service,"Provider " + cn + " could not be instantiated",x);}throw new Error(); // This cannot happen
}
運行時數據區
內存結構
問題:共享狀態?作用?存儲內容?擴容問題?版本變化?
(1)線程獨占
-
虛擬機棧
用于存儲棧幀,當方法被調用時會創建一個棧幀入棧
存儲:局部變量表(存儲本地局部變量)、操作數棧(進行運算)、幀數據(方法返回以及異常派發)
在深度溢出或擴展失敗的時,會分別拋出 StackOverflowError 和 OutOfMemoryError 異常
-
本地方法棧
存儲native本地方法信息,在Execution Engine執行時加載本地方法庫
HotSpot VM將本地方法棧和虛擬機棧合并了,本地方法棧也會在虛擬機棧的深度溢出或擴展失敗的時候會分別拋出StackOverflowError 和 OutOfMemoryError 異常。 -
程序計數器 /pc寄存器
存儲當前執行的指令的地址,執行后指向下一條指令地址
如果正在執行的是一個Natvie(本地方法),那么這個計數器的值則為空
(2)線程共享
- 堆
存儲所有對象及其實例變量和數組的信息,是垃圾回收的主要區域 OutOfMemoryError - 方法區
存儲與類相關的一切信息
存儲:運行時常量池、字段數據、方法數據、方法代碼,是堆的邏輯組成部分,垃圾回收是可選的- 具體實現:
1.7及之前實現是永久代,JDK1.8及之后是“元數據區”元空間
JDK6->JDK7:static變量、字符串常量池移到堆里
永久代,垃圾回收不容易被觸發,尤其像字符串對象很可能是隨時變成垃圾 - 元空間與永久代最大的區別在于:空間
元空間并不在虛擬機中,而是使用本地內存\堆外內存,元空間的內存使用量受限于操作系統對本地內存的限制,更加靈活,有效地避免了永久代的內存溢出問題,并且可以減少垃圾回收的壓力
=》OOM:Metaspace - 方法區與堆:類是模板,對象是實體
類的結構信息(包括實例變量的定義)存儲在方法區,而實例變量的具體值存儲在堆內存中
這種分離使得類的結構信息在程序運行時是共享的,而實例變量的具體值則是每個對象獨有的。
- 具體實現:
堆和棧 區別
- 功能
堆:對象存儲單位,代表著數據,所在區域不連續,會有碎片
棧:方法運行時單位,代表著邏輯,所在區域連續,沒有碎片 - 共享性
堆:線程共享
棧:線程獨占 - 大小及分配方式
堆:程序員自己申請 大,速度比較慢,易產生內存碎片
棧:系統自動分配 小,速度較快,程序員是無法控制的 - 異常
堆:堆空間不足 java.lang.OutOfMemoryError
棧:棧溢出 java.lang.StackOverFlowError;棧擴展失敗 OutOfMemoryError
(1)虛擬機中的堆一定是線程共享的嗎
不一定 ,為了保證對象的內存分配過程中的線程安全性,引入了TLAB
TLAB 在內存分配上是線程獨占的,在讀取數據上是線程共享的
常量池
常量池為了避免頻繁的創建和銷毀影響系統性能,實現了對象共享。
例如字符串常量池,在編譯階段就把所有字符串放到一個常量池中,節省內存空間,節省運行時間。
- Class文件常量池\類常量池 Constant Pool Table
Class文件常量池:Class文件中的資源倉庫,在編譯后產生,每個Class字節碼文件都有一個Class文件常量池,JVM類加載.class,將類的元數據置于方法區中(在運行期加載到方法區中去)
加載:1.class文件信息Class對象加載到方法區;2.Class文件常量池會隨著加載到運行常量池中
Class文件:描述信息(類的版本、字段、方法、接口等)+常量池(存放編譯期生成的各種字面量和符號引用)- 字面量:雙引號字符串和常量:文本字符串,被聲明為final的常量值 private int value = 1;
- 符號引用:類和接口的全限定名、字段的名稱和描述符、方法的名稱和描述符 com.a.Test、value、main()
動態鏈接:符號引用只有到運行時被加載到內存后,這些符號才有對應的內存地址信息。這些常量池一旦被裝入內存就變成運行時常量池,對應的符號引用在程序加載或運行時,會被轉變為被加載到內存區域的代碼的直接引用
//.class字節碼文件內容:
cafe babe 0000 0033 0010 0a00 0300 0d07
魔數 次版本號 主版本號 常量池計數器 常量池 方法...
查看字節碼文件:javap -verbose Test.class ,其中constant pool table代表class常量池
魔數(用來確定一個文件能否被JVM接受)、版本號、常量池、類、父類、和接口數組、字段、方法等信息
- 運行時常量池 Runtime Constant Pool
運行時常量池:每個已加載的類都會有一個對應的運行時常量池,用于存儲常量、符號引用(包括對應的直接引用)和一些編譯期已知的常量數據- 運行時常量池具有動態性:Class文件常量池+運行時產生的常量(1.7前字符常量池)
- 編譯期的常量池的內容可以進入運行時常量池(ldc) 懶加載
- 運行時產生的常量也可以放入池中String.intern()
- JDK 1.7,雖然兩者位置不同,但是根據虛擬機規范,字符串常量,需要放在運行時常量池中
- 字符串池就是運行時常量池的一個邏輯子區域。即字符串池是運行時常量池的分池
- 運行時常量池具有動態性:Class文件常量池+運行時產生的常量(1.7前字符常量池)
String s = "黃河之水天上來";
編譯期-class常量池:字面量 "黃河之水天上來"
jvm-運行期->運行時常量池 "黃河之水天上來" 不是對象,只是字面量-常量
--->
1.程序執行到這一行,根據字面量去 字符常量池 中,查找對應字面量的“引用” 懶加載
ldc(JavaThread* thread, bool wide)) :將int、float或String型常量從常量池推送至棧頂
2.沒有,在 字符常量池 創建String對象,并返回引用;
版本變化: 運行時常量池在方法區中,1.7及之前實現是永久代,JDK1.8及之后是“元數據區”元空間
- 字符串常量池 String Pool
字符串常量池:來保存字符串的常量池,
在HotSpot虛擬機中,實現為StringTable,底層是一個c++的hashtable <字面量,引用>
String Pool中存的是引用值而不是具體的實例對象,具體的實例對象是在堆中開辟的一塊空間存放的
版本變化: JDK6->JDK7:static變量、字符串常量池移到堆里
垃圾回收:字符串常量池本身不會被GC,如果一直不回收處于總是"只進不出"的狀態,很可能會導致內存泄露
Full GC時,對StringTable<>進行可達性分析,引用對象不可達,移除這個引用,并且銷毀執行的String對象
組成部分:ldc 、String.intern()
intern()
1.JDK1.6:如果存在,則返回常量池中的引用;不存在,復制一個副本放到常量池
2.JDK1.7&JDK1.7+:如果存在,則返回字符串常量池中的引用;不存在,將在堆上的地址引用復制到字符串常量池// 語句1
String s3 = new String("a") + new String("b");// 語句2
s3.intern();// 語句3
String s4 = "ab";// 語句4
System.out.println(s3 == s4);問題1:語句4結果?
如果是JDK1.6及以前的版本,結果就是false;而如果是JDK1.7開始的版本,結果就是true
問題2:語句1有幾個對象?
"a" 、"b"、 new String("a")、new String("b")、
new StringBuilder、new StringBuilder("a").append("b").toString()
- 基本類型包裝類對象常量池
除了Float和Double,其他類型都存在常量池,當然這些常量池是有緩存范圍 - 享元模式
對應值在緩存范圍內,可以使用對象池,超出范圍需要創建對象
垃圾回收
判斷對象是否存活
-
引用計數法
給對象添加引用計數器,有引用就+1,引用失效就-1。任何時刻,引用為0,即判斷對象死亡
優點:實現簡單,效率高
缺點:在主流的Java虛擬機中不被使用,無法解決對象相互循環引用的問題
循環引用:a引用b,b又引用a,但是ab沒有被其他引用,應該被回收(對象) -
可達性分析算法
從根引用(GCRoots)進行“引用鏈”遍歷掃描,如果可達則對象存活,如果不可達則對象已成為垃圾
缺點:STW時間長(解決三色標記法);內存消耗(需要存儲大量的對象數量和引用關系)
如果要使用可達性分析來判斷是否可以回收,需要在一個一致性快照中進行STW- GC Roots 當前一定不能回收的對象
- 虛擬機棧中引用的對象:正在運行的線程\方法,不能回收
- 本地方法棧中引用的對象:本地方法native
- 方法區中類靜態屬性引用的對象:static 對象,隨著類的存在而存在
- 方法區中的常量引用的對象: static final ; public static final A ACONSTANT = new A()
- 對象不可達,一定會被垃圾收集器回收嗎: finalize() “自我拯救一次” 官方不推薦使用
- 第一次標記和篩選
- 標記:通過可達性分析算法,將不可達對象標記為白色,篩選:是否要執行 finalize() 方法
- 篩選:對象沒有覆蓋 finalize() 方法 或者 finalize() 方法已經被虛擬機調用過
- 第二次標記
- 如果對象有必要執行finalize() 方法 (finalize() 方法被覆蓋并且沒有執行過),將對象放到 F-Queue 的隊列中排隊,稍后由一條虛擬機自動建立的、低優先級的 Finalizer線程 去觸發方法,稍后GC將對F-Queue中的對象進行第二次小規模標記。如果在隊列中的對象連接上GC Roots引用鏈,那么在第二次標記時,將其移除出“即將回收”的集合。如果仍然沒有關聯,則進行第二次標記,才會對該對象進行回收
- 如果對象有必要執行finalize() 方法 (finalize() 方法被覆蓋并且沒有執行過),將對象放到 F-Queue 的隊列中排隊,稍后由一條虛擬機自動建立的、低優先級的 Finalizer線程 去觸發方法,稍后GC將對F-Queue中的對象進行第二次小規模標記。如果在隊列中的對象連接上GC Roots引用鏈,那么在第二次標記時,將其移除出“即將回收”的集合。如果仍然沒有關聯,則進行第二次標記,才會對該對象進行回收
- 第一次標記和篩選
- GC Roots 當前一定不能回收的對象
-
三色標記法
屬于可達性分析的一種,可以大大的降低STW的時長
- STW、 safe point 、Safe Region
- STW Stop-The-World 全局停頓
- 執行任何垃圾收集算法時,Java應用程序的其他所有線程都被掛起,且不能徹底避免的,只能盡量降低STW的時長(所有Java代碼停止,native代碼可以執行,但不能與JVM交互)
- safe point 安全點
- 當線程執行到這個位置的時候,可以被認為處于“安全狀態”,如果有需要,可以在這里暫停,當JVM需要對線程進行掛起的時候,會等到安全點在執行
- 線程掛起場景: STW、獲取Dump、死鎖檢測
- Safe Region 安全區域
- 用于處理無法立即響應到達安全點請求的線程
- 例如:長時間的計算操作,是不會與GC操作沖突,不會改變對象的引用關系,這種代碼區域就可以稱之為安全區域。
- 當線程運行到安全的代碼\安全區域時,JVM認為線程雖然沒在安全點,但是因為處于安全區域,也可以進行正常的GC操作,當這段代碼執行完了,要退出安全區域的時候,就需要檢查一下,自己能不能退出去,比如看看GC是否在運行。
- https://blog.csdn.net/WZH577/article/details/109782827
- STW Stop-The-World 全局停頓
三色標記法
可以減少JVM在GC過程中的STW時長。 CMS、G1等主要使用的標記算法
(1)引用計數法、可達性分析算法問題
循環引用;SWT時間過長
(2)三色標記法將對象分為三種狀態:白色、灰色和黑色。
白色:未標記
灰色:已標記,引用對象(相當于)未標記完
黑色:已標記,引用對象已標記完
(3)標記過程的三個階段
- 初始標記 STW
遍歷所有GC Roots,將 GC Roots 和 GC Roots的下一級的對象標記為灰色
只會掃描被直接或者間接引用的對象,而不會掃描整個堆,因此這個過程其實很快 - 并發標記 沒有STW
遍歷GC Roots的下游,從灰色對象開始遍歷整個對象圖,將被引用的對象標記為灰色,并將已經遍歷過的對象標記為黑色,重復此步驟直到灰色對象集合為空。
在并發標記階段采用多線程,用戶線程與標記線程并發執行,沒有STW,降低GC停頓時長,但應用程序線程可能會修改對象圖,因此垃圾回收器需要使用寫屏障技術來保證并發標記的正確性。耗時最久 - 重新標記 STW
再標記一次,標記在并發標記階段中被修改的對象以及未被遍歷到的對象。
STW原因:重新遍歷灰色對象,如果不停頓,用戶線程還是繼續執行,那么這個GC永遠可能也做不完了 - 垃圾回收器會執行清除操作,清除未被標記對象
垃圾回收器會將所有未被標記的對象標記為白色
(4)三色標記算法的漏洞:并發標記過程,會導致出現多標,漏標問題
- 多標:多余標記,應該回收的對象讓它存活了,會產生浮動垃圾
- 可以容忍,下次垃圾回收周期會把它們清除掉
- 可以容忍,下次垃圾回收周期會把它們清除掉
在并發標記階段,D->E,E為灰色,如果存在用戶線程執行D.E = null,則D->E引用鏈斷開,但由于E已經被置為灰色,導致存活,進而導致F、G存活,多標問題
- 漏標:遺漏/忘記標記,應該存活的對象,被回收了,導致新引用被標記不可達
- 嚴重問題,一個存活對象被回收掉
- 嚴重問題,一個存活對象被回收掉
在并發標記階段,D->E,E為灰色,如果存在用戶線程執行D.G->G,E.G->null ,此時E為灰色,G仍然為白色,即使E->G存在引用鏈,但是它不會被掃描和被標記為灰-黑色,標記環節結束時,會把對象G當做垃圾清除掉
- 破壞漏標發生的充要條件
- 黑色對象D新增了對白色對象G的引用 (滿足條件1):增量更新
- 至少有一個黑色對象被標記后,又存在一個白色對象的引用
- 灰色對象E指向白色對象G的引用被斷開了 (滿足條件2): 原始快照
- 所有的灰色對象在引用掃描完成之前刪除了對白色對象的引用
- 黑色對象D新增了對白色對象G的引用 (滿足條件1):增量更新
- 增量更新:實時記錄變化,確保每一次變化都會被重新檢查
- 解決方案:
- 如果有黑色對象被標記后,又存在一個白色對象的引用,記錄黑色對象的引用,在重新標記階段以黑色引用為根,重新掃描
- 缺點:浪費時間(重新掃描黑色鏈,會浪費時間,但漏標情況較少,可以接受)
- 回收器: CMS垃圾收集器使用增量更新方案
- 原始快照:基于GC開始時的狀態做決策,忽略之后的變化,確保GC的穩定性和一致性
- 解決方案:
- 如果灰色對象在掃描完成前刪除了對白色對象的引用,在灰色對象取消引用之前,先將灰色對象引用的白色對象記錄下來,在重新標記階段以白色對象為根,重新掃描
- 缺點:
- 多標問題(D->G正常;斷開);需要更多內存
- 回收器: G1垃圾收集器使用原始快照方案
(5)讀寫屏障
可以理解成就是在寫操作前后插入一段代碼,用于記錄一些信息、保存某些數據等,概念類似于AOP
- 增量更新:針對新增引用的情況下,就是在賦值操作之前添加一個寫屏障,在寫屏障中記錄新增的引用(將黑色存為灰色,在重新標記階段,重新遍歷鏈路)
- 原始快照:針對引用減少的情況下,就是在賦值操作(置空)執行之前添加一個寫屏障,在寫屏障中記錄被置空的對象引用
跨代引用
(1)跨代引用問題:
跨代引用:指在Java堆內存的不同代之間存在引用關系,導致對象在不同代之間的引用
比如:新生代到老年代的引用,老年代到新生代的引用等
假如我們進行YoungGC,從GC Roots進行可達性分析,如果一個對象被老年代對象引用,為了判斷新生代中某個對象是否存活,需要額外遍歷整個老年代來確保可達性分析的正確性,反過來也是一樣。這種方案成本太高
注:不僅新生代、老年代存在跨代引用,G1的rigen之間也存在跨代引用,即所有涉及到部分區域收集的收集器都面臨這樣的問題。
簡單思想:標記一下有沒有額外的引用,于是定義了一個全局的數據結構——Remembered Set
(2)Remembered Set 記憶集
用于記錄從非收集區域指向收集區域的指針集合的抽象思想,卡表是記憶集的一種實現方式
例如:在分代GC中,通常只能單獨收集的只有Yong gen,那記憶集記錄的就是Old gen指向Young gen的跨代指針,那就不需要遍歷整個老年代了,只需掃描Remembered Set中的條目,從而減少了掃描的開銷
(3)Card Table 卡表
HotSpot虛擬機中,卡表最簡單的形式可以只是一個字節數組,采用空間換時間思想,不需要掃描整個老年代空間
HotSpot:
CARD_TABLE[this.address>>9]=0;
字節數組每一個元素都對應一塊固定大小的內存塊,稱為“卡頁” Card Page
一個卡頁大小為2(9)(512字節),從CARD_TABLE[0]開始對應第一塊。一個卡頁通常包含不止一個對象,只要其中有一個\多個對象存在跨代引用指針,那就將對應卡表的數組元素的值標識為1,使這個元素變臟(Dirty),沒有則標識為0
在垃圾收集發生時,只要篩選出卡表中變臟的元素(包含跨代指針),把他們加入GC Roots中一并掃描
注意:新生代對象引用老年代對象時,老年代對象所在的區域不會被標記為臟頁,即只有老年代引用新生代才會處理卡頁
(4)Logging Write Barrier 寫屏障
寫屏障是為了維護卡表:例如他們何時變臟、誰把他們變臟等->只要引用字段賦完值進行記錄到卡表中
垃圾回收算法
對象無法存活,判定為垃圾,如何回收?垃圾回收算法
標記清除法、復制算法、標記整理法、分代收集算法
- 標記-清除算法: 老年代(CMS,為了降低STW的時長 )
- 標記階段:利用可達性分析算法遍歷內存,標記所有的活動對象
- 清除階段:再遍歷一遍內存,將未被標記的對象清除
優點:速度快,因為不需要移動和復制對象
缺點:導致空間不連續,產生比較多的內存碎片,可能導致后續沒有連續空間,需要進行一次GC
- 復制算法:年輕代
- 首先將內存劃分成兩個區域,每次只使用一塊
- 當快滿的時候,將標記出來的存活的對象復制到另一塊內存區域中,然后對整個之前的空間進行垃圾回收,將未復制的垃圾對象清理掉。
優點:
內存空間是連續的,不會產生內存碎片
不需要標記,直接對存活對象(對象的指針是否被引用)進行復制;只處理存活對象,不處理垃圾對象
缺點:浪費了一半的內存,復制對象會造成性能和時間上的消耗
- 標記-整理算法:老年代
- 標記階段:利用可達性分析算法遍歷內存,標記所有的活動對象
- 整理階段:移動所有存活對象,且按照內存地址次序依次排列,然后將末端內存地址以后的內存全部回收
特點:適用于存活對象多,垃圾少的情況;需要整理的過程,無空間碎片產生;
優點:不會產生內存碎片;不會浪費內存空間
缺點:太耗時間(性能低)
- 分代收集算法:總綱
根據內存對象的存活周期不同,把Java堆分為新生代和老年代
通過將不同時期的對象存儲在不同的內存池中,就可以節省寶貴的時間和空間,從而改善系統的性能。
- 新生代 Young: 占總空間的 1/3
- Eden 8 : Survivor From 1: Survivor To 1
- 復制算法:有大量對象死去和少量對象存活,付出少量存活對象的復制成本就可以完成收集
- 老年代:Old :占總空間的 2/3
- 標記清理算法、標記整理算法:對象的存活率極高,沒有額外的空間對他進行分配擔保
- 為什么要分代?為什么年輕代要分3份?
- 分代:老年代,長期存活,空間大;年輕代:朝生夕死,不確定是否存活
- 年輕代要分3份:朝生夕死,復制算法
(1)Java代碼如何調用垃圾回收
通過使用系統類的方法:System.gc();
通過使用運行時類方法:Runtime.getRuntime().gc();
這兩個方法用來提示 JVM 要進行垃圾回收。但是,立即開始還是延遲進行垃圾回收是取決于 JVM 的。
垃圾回收過程
垃圾回收對象轉化流程
- 對象創建先分配在Eden區
- 對象最開始分配在 Eden區 ,如果Eden區沒有足夠的空間時,觸發 yonggc
- 把yonggc后仍然存活的對象移動到 Survivor From區,Eden區 清空,然后再次分配新對象到 Eden區
- 如果再觸發gc,采用復制算法,把 Eden區存活的和 Survivor From區存活的對象轉移到另一塊空著的Survivor To區
- 每移動\gc一次,對象年齡+1,對象年齡到達15次進入老年代
- 大對象直接進入老年代
大對象是指需要大量連續內存空間的對象,比如:字符串、數組
這樣做的目的是避免在Eden區和兩個Survivor區之間發生大量的內存拷貝 - 長期存活的對象進入老年代
虛擬機為每個對象定義了一個年齡計數器,如果對象經過了1次Minor GC那么對象會進入Survivor區,之后每經過一次Minor GC那么對象的年齡加1,直到達到閾值(15)對象進入老年區。 - 動態年齡分配
如果在Survivor空間中小于等于某個年齡的所有對象大小的總和大于Survivor空間的一半時,那么就把大于等于這個年齡的對象都晉升到老年代。
從年齡小的對象開始,不斷地累加對象的大小,當年齡達到N時,剛好達到TargetSurvivorRatio這個閾值,那么就把所有年齡大于等于N的對象全部晉升到老年代去
- 對象太多:都達到一半Survivor,數量太多,每次yonggc都需要移動很多對象
- 從N年齡后的對象,大概率已經經過了多次回收,希望那些可能是長期存活的對象,盡早進入老年代
- 空間分配擔保
每次進行Yong GC之前,會進行空間分配擔保。
對象什么時候進入老年代
- 15次
在對象頭中,分代年齡占4bit,即2(4)-1 (1111)
設置年齡: -XX:MaxTenuringThreshold - 大對象
設置閾值:-XX:PretenureSizeThreshold - 動態年齡分配
- 空間擔保機制
空間分配擔保原則
(1)問題
如果Survivor區域的空間不夠,就要分配給老年代。但是,老年代也是可能空間不足的。
所以,在這個過程中就需要做一次空間分配擔保(CMS),來保證對象能夠進入老年代
(2)空間分配擔保機制
- 在進行Minor GC之前,JVM首先會檢查【老年代最大連續空閑空間】是否大于【當前新生代所有對象占用的總空間】
- 如果大于,那么說明此次的Minor GC是安全的,可以放心的進行Minor GC
- 如果小于,則JVM會去查看HandlePromotionFailure參數的值是否為true(表示是否允許擔保失敗,1.7就不在支持了,直接到第5步)
- 如果允許擔保失敗,則此時JVM會去檢查【老年代最大連續空閑空間】是否大于【歷次晉升到老年代的對象的平均大小】
- 如果不允許擔保失敗,則此時就會進行一次Full GC 以騰出老年代更多的空間
- 如果小于,則JVM此時會進行一次Full GC,以便于騰出更多的老年代空間
- 如果大于,則JVM會冒險進行一次Minor GC
1、存活對象<survivor,存活對象進入survivor區中
2、存活對象>survivor,存活對象<老年代可用空間,直接進入老年代
3、存活對象>survivor,存活對象>老年代可用空間,就觸發了 Full GC
如果 Full GC后,老年代還是沒有足夠的空間,此時就會發生OOM內存溢出了
Yong GC 、 Old Gc 、Full GC
Yong GC:主要用于對新生代垃圾回收 Minor GC
- Eden區滿了之后就會觸發minor GC清除年輕代中的垃圾
- Parallel Scavenge垃圾回收器,Full GC 前先執行一下 Yong GC
Old GC:主要用于對老年代垃圾回收 Major GC
- 老年代內存不足:當老年代空間不足以存放新的晉升對象或存活對象時,就會觸發Major GC
- 老年代使用率達到閾值:一些JVM實現中,當老年代的內存使用率達到一定閾值時,也可能觸發Major GC
Full GC:全堆回收:新生代、老年代、永久代
- 老年代內存不足:當老年代空間不足以存放晉升對象時,可能觸發Full GC
- 空間擔保失敗
- 永久代空間不夠或者是超過了臨界值,會觸發完全垃圾回收
- 執行System.gc()、jmap -dump 等命令
垃圾回收器
1)常見的垃圾回收器
(1)串行垃圾回收器:Serial, Serial Old
(2)并行垃圾回收器:Parallel Scavenge,Parallel Old,ParNew
(3)并發標記清除垃圾回收器:CMS
(4)G1垃圾回收器(JDK 7中推出,JDK 9中設置為默認)
(5)ZGC垃圾回收器(JDK 11 推出,JDK 17默認)
新生代收集器有Serial、ParNew、Parallel Scavenge
老年代收集器有Serial Old、Parallel Old、CMS
整堆收集器有G1、ZGC
(2)默認垃圾收集器
JDK1.8:Parallel Scavenge(新生代)+Parallel Old(老年代)
JDK1.9:G1
CMS與G1
CMS 并發標記清除垃圾回收器
以獲取最短回收停頓時間為目標的收集器(追求低停頓),它在垃圾收集時使得用戶線程和 GC 線程并發執行,因此在垃圾收集過程中用戶也不會感到明顯的卡頓。
“并發” “低停頓”
(1)回收過程
- 初始標記:
標記 GC Root 開始的下級(注:僅下一級)對象,這個過程會 STW,但是跟 GC Root 直接關聯的下級對象不會很多,因此這個過程其實很快。 - 并發標記:
gcroot的下游
根據上一步的結果,繼續向下標識所有關聯的對象,直到這條鏈上的最盡頭。這個過程是多線程的,雖然耗時理論上會比較長,但是其它工作線程并不會阻塞,沒有STW。 - 重新標記:
顧名思義,就是要再標記一次。為啥還要再標記一次?因為第 2 步并沒有阻塞其它工作線程,其它線程在標識過程中,很有可能會產生新的垃圾。 - 并發清理:
GC線程清除不可達的對象,并回收它們占用的內存空間。這個階段與應用線程并發執行,不需要STW。
(2)CMS的問題是什么 - 并發回收導致CPU資源緊張:
并發階段,雖然不會導致用戶線程停頓,但因為占用一部分線程而導致應用程序變慢,降低程序總吞吐量
CMS默認啟動的回收線程數是:(CPU核數 + 3)/ 4,當CPU核數不足四個時,CMS對用戶程序的影響就可能變得很大。 - 無法處理浮動垃圾:
在并發清理節點,用戶線程執行也會產生垃圾,但是這部分垃圾是在標記之后,只有等到下一次GC時,才能被清理掉,這部分垃圾叫浮動垃圾。 - 并發失敗
CMS是和業務線程并發運行的,在執行CMS的過程中有業務對象需要在老年代直接分配(大對象),但是老年代沒有足夠的空間來分配,所以導致concurrent mode failure, 一旦出現此錯誤時,便會 STW 切換到SerialOld收集方式,這樣一來停頓時間就很長了
默認情況下:+XX:CMSInitiatingOccupancyFraction 老年代使用 92%空間,會觸發 CMS 垃圾回收 - 內存碎片整理
CMS使用“標記-清理”會產生大量的空間碎片,會給大對象分配帶來麻煩,會出現老年代還有很多剩余空間,但無法找到足夠大的連續空間來分配當前對象,而不得不提前觸發一次 Full GC 的情況。
解決:在 Full GC 時開啟內存碎片的合并整理過程:-XX:UseCMSCompactAtFullCollection 默認開啟0
由于這個內存整理必須移動存活對象,是無法并發的,雖然空間碎片沒有了但是停頓時間變長了
G1 Garbage First
https://www.jianshu.com/p/477a0f2b2164
G1 收集器不采用傳統的新生代和老年代物理隔離的布局方式,僅在邏輯上劃分新生代和老年代,將整個堆內存劃分為2048個大小相等的獨立內存塊Region,每個Region是邏輯連續的一段內存,具體大小根據堆的實際大小而定,整體被控制在 1M - 32M 之間,且為2的N次冪(1M、2M、4M、8M、16M和32M),并使用不同的Region來表示新生代和老年代,G1不再要求相同類型的 Region 在物理內存上相鄰,而是通過Region的動態分配方式實現邏輯上的連續。
G1收集器通過跟蹤Region中的垃圾堆積情況,每次根據設置的垃圾回收時間,回收優先級最高的區域,避免整個新生代或整個老年代的垃圾回收,使得stop the world的時間更短、更可控,同時在有限的時間內可以獲得最高的回收效率。
通過區域劃分和優先級區域回收機制,確保G1收集器可以在有限時間獲得最高的垃圾收集效率。
G1設計初衷就是替換 CMS,成為一種全功能收集器。G1 在JDK9 之后成為服務端模式下的默認垃圾回收器,取代了 Parallel Scavenge 加 Parallel Old 的默認組合,而 CMS 被聲明為不推薦使用的垃圾回收器。
(1)分區Region:
G1 垃圾收集器將堆內存劃分為若干個 Region,每個 Region 分區只能是一種角色 E S O H,空白區域代表的是未分配的內存。
H:存放巨型對象,對象的大小超過Region容量的50%以上,對于其他回收器這個對象默認會被分配在老年代,但可能是個短期存活的巨型對象,可能導老年代頻繁GC,G1劃分了一個H區專門存放巨型對象,如果尋找不到連續的H區的話,就會 Full GC
(2)Remembered Set RSet
為了避免整堆掃描,為每個分區各自分配了一個 RSet,記錄了其它 Region 對當前 Region 的引用情況。
當回收某個Region時,只需掃描它的 RSet 就可以找到外部引用,來確定引用本分區內的對象是否存活,
進而確定本分區內的對象存活情況
注意:只記錄老年代到新生代的引用,且不是同一分區的
如果引用源在本分區,不需要記錄; G1 每次 GC 時,所有的新生代都會被掃描,引用源是年輕代的對象也不需要記錄,只需要記錄老年代到新生代之間的引用
(3)Card Table:
RSet是一個概念模型。實際上,Card Table 是 RS 的一種實現方式。類似于跨代引用那里的介紹
G1對內存的使用以分區(Region)為單位,而對象的分配則以卡片(Card)為單位。
(4)回收過程
- 初始標記(會STW)
標記 GC Roots 根及其根下一級,個階段需要停頓線程,但耗時很短 - 并發標記
標記 GC Roots 引用鏈,耗時較長,但可與用戶程序并發執行 - 最終標記(會STW)
對用戶線程做短暫的暫停,處理并發階段結束后仍有引用變動的對象 - 篩選回收(會STW)
對各個Region的回收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃,然后把決定回收的那一部分Region的存活對象復制到空的Region中,再清理掉整個舊Region的全部空間。
這里的操作涉及存活對象的移動,必須暫停用戶線程,由多條回收器線程并行完成的
CMS 與 G1 區別 - 使用范圍及回收算法
CMS:老年代;標記-清除算法,容易產生內存碎片
G1:整堆;標記-復制算法回收年輕代、標記-整理算法回收老年代,沒有內存空間碎片 - STW時間
CMS:以最小的停頓時間為目標,無法預測停頓時間
G1:可預測垃圾回收的停頓時間(建立可預測的停頓時間模型) - 垃圾回收過程
CMS:初始標記,并發標記,重新標記,并發清理
G1:初始標記,并發標記,最終標記,篩選回收