Java中JVM的雙親委派、內(nèi)存溢出、垃圾回收和調(diào)優(yōu)詳解
1.雙親委派
1.1 類加載過程
【加載】:加載是指將類的字節(jié)碼文件讀入內(nèi)存,并在內(nèi)存中創(chuàng)建一個(gè)Class對(duì)象,用來描述該類的結(jié)構(gòu)信息。類的字節(jié)碼可以來自本地磁盤、網(wǎng)絡(luò)等各種來源。
【連接】:連接是指對(duì)類的字節(jié)碼進(jìn)行驗(yàn)證、準(zhǔn)備和解析的過程。
- 驗(yàn)證:驗(yàn)證字節(jié)碼文件的正確性和安全性。例如,驗(yàn)證類的繼承關(guān)系是否合法、方法調(diào)用是否正確等。
- 準(zhǔn)備:為類的靜態(tài)變量分配內(nèi)存空間,并設(shè)置初始值。這些變量在該類的所有實(shí)例中共享。
- 解析:將類中的符號(hào)引用轉(zhuǎn)換為直接引用。符號(hào)引用是一種用來描述所引用的目標(biāo)的符號(hào),例如類、字段、方法等。直接引用是指指向具體內(nèi)存地址的指針。
【初始化】:初始化是類加載過程的最后一步,主要是對(duì)類的靜態(tài)變量賦值和執(zhí)行靜態(tài)代碼塊。
- 執(zhí)行靜態(tài)代碼塊和靜態(tài)變量初始化器。
- 調(diào)用父類的初始化方法。
- 執(zhí)行類中定義的初始化方法。
1.2 類加載器
類加載器:是Java虛擬機(jī)(JVM)的一個(gè)重要組成部分,它的主要作用是將類的字節(jié)碼加載到內(nèi)存中,并生成對(duì)應(yīng)的Class對(duì)象。
System.setProperty("java.system.class.loader", "com.example.MyClassLoader");
類加載器分類:
(1)第一種是 c++ 語言寫的 啟動(dòng)類加載器:Bootstrap ClassLoader。
(2)第二種是 java 語言寫的 其他各種類加載器??梢苑譃閿U(kuò)展類加載器, 應(yīng)用程序類加載器 以及各種各樣的自定義類加載器。自定義類加載器 一般是需要進(jìn)行動(dòng)態(tài)加載或者其他業(yè)務(wù)目的類加載器。
- 啟動(dòng)類加載器:它是JVM的一部分,主要負(fù)責(zé)加載Java的核心類庫,比如java.lang包中的類。
- 擴(kuò)展類加載器:它負(fù)責(zé)加載JVM擴(kuò)展目錄(java.ext.dirs)中的類庫,比如javax包中的類。
- 應(yīng)用程序類加載器:它負(fù)責(zé)加載應(yīng)用程序classpath目錄下的類庫,是程序中默認(rèn)的類加載器。
- 自定義類加載器:它是開發(fā)人員自己實(shí)現(xiàn)的類加載器,可以根據(jù)自己的需求實(shí)現(xiàn)不同的加載邏輯。
1.3 雙親委派機(jī)制
【類的唯一性】在一個(gè)虛擬機(jī)里,一個(gè)類只有一個(gè)對(duì)應(yīng)的類對(duì)象。 現(xiàn)在對(duì)類的唯一性的理解,應(yīng)該更為準(zhǔn)確地描述為: 在一個(gè)加載器下,一個(gè)類只有一個(gè)對(duì)應(yīng)的類對(duì)象。如果有多個(gè)類加載器都各自加載了同一個(gè)類,那么他們將得到不同的類對(duì)象。
【雙親委派機(jī)制】JVM中的雙親委派機(jī)制是指,當(dāng)一個(gè)類被加載時(shí),JVM會(huì)首先委托其父類加載器去加載該類,只有當(dāng)父類加載器無法加載該類時(shí),才會(huì)由子類加載器去加載。這樣可以保證Java虛擬機(jī)中的類不會(huì)出現(xiàn)同名的情況,也能保證Java核心類庫的安全性和穩(wěn)定性。
- 通過覆蓋ClassLoader的findClass()方法,在該方法中定義特殊的類搜索方式或者直接拋出ClassNotFoundException異常來繞過雙親委派機(jī)制。
- 自定義類加載器并打破雙親委派機(jī)制,可以使用URLClassLoader類或者繼承ClassLoader類并重寫loadClass()方法。
需要注意的是,在使用類加載器打破雙親委派機(jī)制時(shí),應(yīng)該保證加載的類與父加載器加載的類之間沒有依賴關(guān)系,否則可能會(huì)導(dǎo)致異?;蛘叱霈F(xiàn)未知的錯(cuò)誤。
(1)雙親委派機(jī)制的意義主要是保護(hù)一些基本類不受影響。比如常用的 String類, 其全限定名是 java.lang.String,只是 java.lang 這個(gè)包下的類在使用的時(shí)候,可以不用 import 而直接使用。像這種基本類按照雙親委派機(jī)制 都應(yīng)該從 rt.jar 里去獲取,而不應(yīng)該從自定義加載器里去獲取某個(gè)開發(fā)人員自己寫的 java.lang.String,畢竟開發(fā)人員自己寫的 java.lang.String 可能有很多 bug,通過這種方式,無論如何大家使用的都是 rt.jar 里的 java.lang.String 類了。
(2)避免類的重復(fù)加載。由于每個(gè)類加載器都有自己的命名空間,當(dāng)父類加載器已經(jīng)加載了一個(gè)類時(shí),子類加載器再去加載該類時(shí),就會(huì)導(dǎo)致重復(fù)加載,浪費(fèi)內(nèi)存空間。通過雙親委派機(jī)制,可以避免重復(fù)加載,提高JVM的運(yùn)行效率。
(3)確保類的一致性。由于父類加載器的優(yōu)先級(jí)高于子類加載器,所以子類加載器無法覆蓋父類加載器已經(jīng)加載的類。這樣可以保證不同的類加載器加載的同一個(gè)類是一致的,避免了由于不同版本的類加載器加載同一個(gè)類而導(dǎo)致的問題。
【注意】雙親委派機(jī)制對(duì)應(yīng)的英文是:Parents Delegation Model,這個(gè) Parents 后面有個(gè) s,所以就被翻譯成雙親了。而 parents 英文本身想表達(dá)的意思是上溯,父輩,祖先的意思,即爸爸,爺爺,祖爺爺~但是卻被翻譯成雙親,雙親在中文里強(qiáng)調(diào)的是父親和母親兩個(gè)。所以是翻譯不夠準(zhǔn)確,略微會(huì)引起一些歧義。
雖然雙親委派機(jī)制可以避免類的重復(fù)加載、提高JVM的運(yùn)行效率和系統(tǒng)的穩(wěn)定性,但在一些特定的場(chǎng)景下,也需要打破雙親委派機(jī)制,例如:
- 實(shí)現(xiàn)類隔離。在某些情況下,需要使用不同的類加載器加載同一個(gè)類的不同版本,或者需要使用不同的類加載器加載同一個(gè)類的不同實(shí)現(xiàn),這時(shí)就需要打破雙親委派機(jī)制,實(shí)現(xiàn)類的隔離。
- 自定義類加載器。在某些情況下,需要使用自定義的類加載器加載類,例如在OSGi等動(dòng)態(tài)模塊化框架中就需要使用自定義的類加載器。此時(shí)也需要打破雙親委派機(jī)制。
- 熱部署。在熱部署的場(chǎng)景下,需要?jiǎng)討B(tài)更新已經(jīng)加載的類,而雙親委派機(jī)制會(huì)阻止子類加載器重新加載已經(jīng)由父類加載器加載的類,因此需要打破雙親委派機(jī)制,實(shí)現(xiàn)熱部署功能。
1.4 自定義類加載器
為什么需要類加載器:每個(gè) Web 應(yīng)用各自都需要配置一個(gè)專有的類加載器。Tomcat會(huì)有多個(gè)不同的war包,需要實(shí)現(xiàn)類隔離。
- 創(chuàng)建一個(gè)類 CustomizedClassLoader ,繼承自 ClassLoader。
- 定義屬性 classesFolder 其值是當(dāng)前項(xiàng)目下的 classes_4_test 目錄。
- 重寫 loadClassData 方法,它會(huì)把傳進(jìn)去的全限定類名,匹配到文件。
- 重寫 findClass 方法,把這個(gè)字節(jié)數(shù)組通過調(diào)用 defineClass 方法,就轉(zhuǎn)換成 HOW2J 這個(gè)類對(duì)應(yīng)的 Class 對(duì)象了。
- 拿到這個(gè)類對(duì)象之后,通過反射機(jī)制,調(diào)用其 hello 方法,就能看到如圖所示的字符串:"Hello!"
import java.io.File; import java.lang.reflect.Method; import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.StrUtil; public class CustomizedClassLoader extends ClassLoader { private File classesFolder = new File(System.getProperty("user.dir"),"classes_4_test"); protected Class<?> findClass(String QualifiedName) throws ClassNotFoundException { byte[] data = loadClassData(QualifiedName); return defineClass(QualifiedName, data, 0, data.length); } private byte[] loadClassData(String fullQualifiedName) throws ClassNotFoundException { String fileName = StrUtil.replace(fullQualifiedName, ".", "/") + ".class"; File classFile = new File(classesFolder, fileName); if(!classFile.exists()) throw new ClassNotFoundException(fullQualifiedName); return FileUtil.readBytes(classFile); } public static void main(String[] args) throws Exception { CustomizedClassLoader loader = new CustomizedClassLoader(); Class<?> how2jClass = loader.loadClass("cn.ysy.diytomcat.Test"); Object o = how2jClass.newInstance(); Method m = how2jClass.getMethod("hello"); m.invoke(o); System.out.println(how2jClass.getClassLoader()); } }
2. 內(nèi)存溢出&內(nèi)存泄露
2.1 內(nèi)存模型
【基本定義】Java的內(nèi)存模型定義了Java程序在運(yùn)行時(shí)的內(nèi)存結(jié)構(gòu)以及多線程情況下,多個(gè)線程之間如何共享內(nèi)存。Java的內(nèi)存模型保證了線程安全性,避免了多線程訪問共享內(nèi)存時(shí)出現(xiàn)的數(shù)據(jù)競(jìng)爭(zhēng)、死鎖等問題。
【組成部分】Java內(nèi)存模型將內(nèi)存分為兩個(gè)部分:線程工作內(nèi)存和主內(nèi)存。線程工作內(nèi)存是線程獨(dú)有的內(nèi)存空間,用于存儲(chǔ)線程運(yùn)行時(shí)的局部變量等數(shù)據(jù),而主內(nèi)存是所有線程共享的內(nèi)存空間,用于存儲(chǔ)Java程序中定義的全局變量等數(shù)據(jù)。
【特性】Java內(nèi)存模型定義了一組規(guī)則,確保多個(gè)線程之間對(duì)共享內(nèi)存的訪問是正確的。其中包括:
- 可見性:當(dāng)一個(gè)線程修改了共享變量的值后,其他線程可以立即看到該變量的修改。
- 原子性:對(duì)共享變量的讀寫操作應(yīng)該被視為一個(gè)原子操作,不可被中斷。
- 有序性:線程之間的操作可能會(huì)被編譯器、處理器進(jìn)行指令重排等優(yōu)化,但是Java內(nèi)存模型保證了操作執(zhí)行的順序不會(huì)影響程序的正確性。
2.2 內(nèi)存溢出(OOM)
內(nèi)存溢出就是指程序運(yùn)行過程中申請(qǐng)的內(nèi)存大于系統(tǒng)能夠提供的內(nèi)存,導(dǎo)致無法申請(qǐng)到足夠的內(nèi)存,于是就發(fā)生了內(nèi)存溢出。
內(nèi)存溢出原因(案例,場(chǎng)景):
- 內(nèi)存中加載的數(shù)據(jù)量過于龐大,比如sql全表掃描 ;
- 集合類中有對(duì)對(duì)象的引用,使用完后未清空,使得JVM不能回收;
- 代碼中存在死循環(huán)或循環(huán)產(chǎn)生過多重復(fù)的對(duì)象實(shí)體;
- 啟動(dòng)參數(shù)內(nèi)存值設(shè)定的過小。
- 堆棧溢出
解決內(nèi)存溢出:
- 修改JVM啟動(dòng)參數(shù),直接增加內(nèi)存。
- 檢查錯(cuò)誤日志,查看“OutOfMemory”錯(cuò)誤前是否有其它異?;蝈e(cuò)誤。
- 對(duì)代碼進(jìn)行走查和分析,找出可能發(fā)生內(nèi)存溢出的位置。
- 使用內(nèi)存查看工具動(dòng)態(tài)查看內(nèi)存使用情況。
堆棧溢出:
- Java堆溢出 Java堆用于儲(chǔ)存對(duì)象實(shí)例,我們只要不斷地創(chuàng)建對(duì)象,并且保證GC Roots到對(duì)象之間有可達(dá)路徑來避免垃圾回收機(jī)制清除這些對(duì)象,那么隨著對(duì)象數(shù)量的增加,總?cè)萘坑|及最大堆的容量限制后就會(huì)產(chǎn)生內(nèi)存溢出異常。
- 虛擬機(jī)棧和本地方法棧溢出 HotSpot虛擬機(jī)中并不區(qū)分虛擬機(jī)棧和本地方法棧,如果虛擬機(jī)的棧內(nèi)存允許動(dòng)態(tài)擴(kuò)展,當(dāng)擴(kuò)展棧容量無法申請(qǐng)到足夠的內(nèi)存時(shí),將拋出OutOfMemoryError異常。
- 方法區(qū)和運(yùn)行時(shí)常量池溢出 方法區(qū)溢出也是一種常見的內(nèi)存溢出異常,在經(jīng)常運(yùn)行時(shí)生成大量動(dòng)態(tài)類的應(yīng)用場(chǎng)景里,就應(yīng)該特別關(guān)注這些類的回收狀況。
- 本地直接內(nèi)存溢出 直接內(nèi)存的容量大小可通過`-XX:MaxDirectMemorySize`參數(shù)來指定,如果不去指定,則默認(rèn)與Java堆最大值一致。如果直接通過反射獲取Unsafe實(shí)例進(jìn)行內(nèi)存分配,并超出了上述的限制時(shí),將會(huì)引發(fā)OOM異常。
2.3 內(nèi)存泄露
內(nèi)存泄漏是指不再使用的對(duì)象仍然被引用,導(dǎo)致垃圾收集器無法回收它們的內(nèi)存。由于不再使用的對(duì)象仍然無法清理,甚至這種情況可能會(huì)越積越多,最終導(dǎo)致致命的OutOfMemoryError。
內(nèi)存泄漏場(chǎng)景:
- 動(dòng)態(tài)分配內(nèi)存但沒有及時(shí)釋放:如果程序中使用了動(dòng)態(tài)內(nèi)存分配(例如使用 malloc 函數(shù)),但沒有及時(shí)釋放內(nèi)存(例如使用 free 函數(shù)),就會(huì)導(dǎo)致內(nèi)存泄漏。
- 循環(huán)引用:如果程序中存在循環(huán)引用(例如兩個(gè)對(duì)象互相引用),就會(huì)導(dǎo)致這些對(duì)象無法被垃圾回收器回收,最終導(dǎo)致內(nèi)存泄漏。
- 靜態(tài)變量未釋放:如果程序中存在靜態(tài)變量,且這些靜態(tài)變量在程序運(yùn)行過程中未被釋放,就會(huì)導(dǎo)致內(nèi)存泄漏。
- 沒有正確使用引用計(jì)數(shù):如果程序中使用了引用計(jì)數(shù),但沒有正確地使用引用計(jì)數(shù),就會(huì)導(dǎo)致內(nèi)存泄漏。
- 文件描述符未關(guān)閉:如果程序中使用了文件描述符(例如打開文件),但沒有正確關(guān)閉文件,就會(huì)導(dǎo)致內(nèi)存泄漏。
內(nèi)存泄露的原因:
- 長(zhǎng)生命周期的對(duì)象持有短生命周期對(duì)象的引用就很可能發(fā)生內(nèi)存泄露,盡管短生命周期對(duì)象已經(jīng)不再需要,但是因?yàn)殚L(zhǎng)生命周期對(duì)象持有它的引用而導(dǎo)致不能被回收。
- 不正確使用單例模式是引起內(nèi)存泄露的一個(gè)常見問題,單例對(duì)象在被初始化后將在JVM的整個(gè)生命周期中存在(以靜態(tài)變量的方式),如果單例對(duì)象持有外部對(duì)象的引用,那么這個(gè)外部對(duì)象將不能被jvm正?;厥眨瑢?dǎo)致內(nèi)存泄露 。
解決內(nèi)存泄漏:
- 啟用分析器 Java分析器是通過應(yīng)用程序監(jiān)視和診斷內(nèi)存泄漏的工具,它可以分析我們的應(yīng)用程序內(nèi)部發(fā)生的事情,例如如何分配內(nèi)存。使用分析器,我們可以比較不同的方法并找到可以最佳利用資源的方式。
- 啟用詳細(xì)垃圾收集日志 通過啟用詳細(xì)垃圾收集日志,我們可以跟蹤GC的詳細(xì)進(jìn)度。要啟用該功能,我們需要將以下內(nèi)容添加到JVM的配置當(dāng)中:`-verbose:gc`。通過這個(gè)參數(shù),我們可以看到GC內(nèi)部發(fā)生的細(xì)節(jié)。
- 使用引用對(duì)象我們還可以借助java.lang.ref包內(nèi)置的Java引用對(duì)象來規(guī)避問題,使用java.lang.ref包,而不是直接引用對(duì)象,即使用對(duì)象的特殊引用,使得它們可以輕松地被垃圾收集。
- Eclipse內(nèi)存泄漏警告 對(duì)于JDK1.5以及更高的版本中,Eclipse會(huì)在遇到明顯的內(nèi)存泄漏情況時(shí)顯示警告和錯(cuò)誤。因此,在Eclipse中開發(fā)時(shí),我們可以定期地訪問“問題”選項(xiàng)卡,并更加警惕內(nèi)存泄漏警告。
3.垃圾回收機(jī)制
3.1 傳統(tǒng)垃圾回收
垃圾回收(Garbage Collection,GC)就是釋放垃圾占用的內(nèi)存空間,對(duì)內(nèi)存堆中已經(jīng)死亡的或者長(zhǎng)時(shí)間沒有使用的對(duì)象進(jìn)行清除和回收,防止內(nèi)存泄露。
【垃圾判斷算法】
- 引用計(jì)數(shù)法:給每個(gè)對(duì)象添加一個(gè)計(jì)數(shù)器,當(dāng)有地方引用該對(duì)象時(shí)計(jì)數(shù)器加1,當(dāng)引用失效時(shí)計(jì)數(shù)器減1。用對(duì)象計(jì)數(shù)器是否為0來判斷對(duì)象是否可被回收。
- 可達(dá)性分析:通過GC ROOT的對(duì)象作為搜索起始點(diǎn),通過引用向下搜索,走過的路徑稱為引用鏈。通過對(duì)象是否到達(dá)引用鏈的路徑來判斷對(duì)象是否可被回收。
【垃圾回收算法】
- 標(biāo)記-清除算法:標(biāo)記清除算法(Mark-Sweep)是最基礎(chǔ)的一種垃圾回收算法,它分為2部分,先把內(nèi)存區(qū)域中的這些對(duì)象進(jìn)行標(biāo)記,哪些屬于可回收標(biāo)記出來,然后把這些垃圾拎出來清理掉。清理掉的垃圾就變成未使用的內(nèi)存區(qū)域,等待被再次使用。
- 標(biāo)記-復(fù)制算法:復(fù)制算法(Copying)是在標(biāo)記清除算法基礎(chǔ)上演化而來,解決標(biāo)記清除算法的內(nèi)存碎片問題。它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。
- 標(biāo)記-整理算法:標(biāo)記-整理算法標(biāo)記過程仍然與標(biāo)記-清除算法一樣,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng),再清理掉端邊界以外的內(nèi)存區(qū)域。
- 分代收集算法:分代收集算法分代收集算法嚴(yán)格來說并不是一種思想或理論,而是融合上述3種基礎(chǔ)的算法思想,而產(chǎn)生的針對(duì)不同情況所采用不同算法的一套組合拳,根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊。
【Full GC&Minor GC】
- Minor GC:回收新生代,因?yàn)樾律鷮?duì)象存活時(shí)間很短,因此
Minor GC
會(huì)頻繁執(zhí)行,執(zhí)行的速度一般也會(huì)比較快。 - Full GC:回收老年代和新生代,老年代的對(duì)象存活時(shí)間長(zhǎng),因此
Full GC
很少執(zhí)行,執(zhí)行速度會(huì)比Minor GC
慢很多。
【Full GC觸發(fā)條件】
- 調(diào)用 System.gc():只是建議虛擬機(jī)執(zhí)行 Full GC,但是虛擬機(jī)不一定真正去執(zhí)行。不建議使用這種方式,而是讓虛擬機(jī)管理內(nèi)存。
- 老年代空間不足:大對(duì)象直接進(jìn)入老年代、長(zhǎng)期存活的對(duì)象進(jìn)入老年代等。為了避免以上原因引起的 Full GC,應(yīng)當(dāng)盡量不要?jiǎng)?chuàng)建過大的對(duì)象以及數(shù)組、注意編碼規(guī)范避免內(nèi)存泄露。
- 空間分配擔(dān)保失敗:使用復(fù)制算法的 Minor GC 需要老年代的內(nèi)存空間作擔(dān)保,如果擔(dān)保失敗會(huì)執(zhí)行一次 Full GC。
- JDK 1.7 及以前的永久代空間不足:在 JDK 1.7 及以前,HotSpot 虛擬機(jī)中的方法區(qū)是用永久代實(shí)現(xiàn)的,永久代中存放的為一些 Class 的信息、常量、靜態(tài)變量等數(shù)據(jù)。當(dāng)系統(tǒng)中要加載的類、反射的類和調(diào)用的方法較多時(shí),永久代可能會(huì)被占滿,在未配置為采用 CMS GC 的情況下也會(huì)執(zhí)行 Full GC。
3.2 G1垃圾回收器
Garbage First(G1)收集器開創(chuàng)了收集器面向局部收集的設(shè)計(jì)思路和基于Region的內(nèi)存布局形式。其他收集器的垃圾收集的目標(biāo)范圍要么是整個(gè)新生代,要么就是整個(gè)老年代,再要么就是整個(gè)Java堆(Full GC)。而G1跳出了這個(gè)限制,G1垃圾回收器相較于其他垃圾回收器的一大特點(diǎn)就是它支持將Java堆內(nèi)存劃分為多個(gè)大小相等的區(qū)域,每個(gè)區(qū)域能夠獨(dú)立管理,并且可以根據(jù)應(yīng)用程序的實(shí)時(shí)需求動(dòng)態(tài)地選擇哪些區(qū)域進(jìn)行垃圾回收。
G1收集器的運(yùn)作過程大致可劃分為以下四個(gè)步驟:初始標(biāo)記、并發(fā)標(biāo)記、最終標(biāo)記、篩選回收。其中,初始標(biāo)記和最終標(biāo)記階段仍然需要停頓所有的線程,但是耗時(shí)很短。
- 初始標(biāo)記階段:首先,G1會(huì)標(biāo)記出從根對(duì)象直接可達(dá)的對(duì)象,這個(gè)過程是在年輕代暫停執(zhí)行的。這個(gè)階段結(jié)束后,G1就已經(jīng)確定了那些對(duì)象需要被回收。
- 并發(fā)標(biāo)記階段:這個(gè)階段是并發(fā)的,即應(yīng)用程序繼續(xù)執(zhí)行,垃圾回收器并行標(biāo)記那些不重要的對(duì)象,包括那些從根對(duì)象不可達(dá)但是與可達(dá)對(duì)象之間有引用關(guān)系的對(duì)象。
- 最終標(biāo)記階段:這個(gè)階段是在并發(fā)標(biāo)記階段完成后暫停執(zhí)行的,G1會(huì)重新掃描所有新添加的引用并標(biāo)記它們,防止被回收。
- 篩選階段:在這個(gè)階段中,G1會(huì)確定需要被回收的區(qū)域,并開始將這些區(qū)域的存活對(duì)象復(fù)制到空閑的區(qū)域中,同時(shí)清理掉這些區(qū)域的未使用對(duì)象。
- 清理階段:在這個(gè)階段中,G1已經(jīng)有一些可用的區(qū)域,在這個(gè)階段中將會(huì)對(duì)這些區(qū)域進(jìn)行整理,并且更新所有的引用地址,使得GC的過程能夠再次啟動(dòng)。
從JDK 9開始,G1成為了默認(rèn)的垃圾回收器,它是HotSpot在JVM上推出的垃圾回收器,旨在取代CMS。G1的主要特點(diǎn)包括:
- 分區(qū):G1將堆內(nèi)存劃分為多個(gè)大小相等的區(qū)域,每個(gè)區(qū)域的大小通常為1-32MB。
- 并行收集:G1采用多線程并行收集的方式,可以明顯縮短垃圾回收時(shí)間。
- 智能壓縮:G1生成一個(gè)可復(fù)制的新對(duì)象,而不是壓縮整個(gè)堆,這樣可以明顯降低垃圾回收的時(shí)間和開銷。
- 可預(yù)測(cè)的停頓時(shí)間:G1通過控制每個(gè)分區(qū)內(nèi)的垃圾回收時(shí)間,可以預(yù)測(cè)垃圾回收時(shí)間,并對(duì)應(yīng)用程序暫停時(shí)間進(jìn)行優(yōu)化。
3.3 CMS垃圾回收器
CMS收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器,是基于標(biāo)記清除算法實(shí)現(xiàn)的。它最主要的優(yōu)點(diǎn)是:并發(fā)收集、低停頓。它的運(yùn)作過程相對(duì)于前面幾種收集器來說要更復(fù)雜一些,整個(gè)過程分為四個(gè)步驟,包括:初始標(biāo)記、并發(fā)標(biāo)記、重新標(biāo)記、并發(fā)清除。
- 初始標(biāo)記僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,速度很快。
- 并發(fā)標(biāo)記階段就是從GC Roots的直接關(guān)聯(lián)對(duì)象開始遍歷整個(gè)對(duì)象圖的過程,這個(gè)過程耗時(shí)較長(zhǎng)但是不需要停頓用戶線程,可以與垃圾收集線程一起并發(fā)運(yùn)行。
- 重新標(biāo)記階段則是為了修正并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄,這個(gè)階段的停頓時(shí)間通常會(huì)比初始標(biāo)記階段稍長(zhǎng)一些,但也遠(yuǎn)比并發(fā)標(biāo)記階段的時(shí)間短。
- 并發(fā)清除階段,清理刪除掉標(biāo)記階段判斷的已經(jīng)死亡的對(duì)象,由于不需要移動(dòng)存活對(duì)象,所以這個(gè)階段也是可以與用戶線程同時(shí)并發(fā)的。
CMS收集器是HotSpot虛擬機(jī)追求低停頓的第一次成功嘗試,但是它還遠(yuǎn)達(dá)不到完美的程度,至少有以下三個(gè)明顯的缺點(diǎn):
- 并發(fā)階段,它雖然不會(huì)導(dǎo)致用戶線程停頓,卻因?yàn)檎加靡徊糠志€程而導(dǎo)致應(yīng)用程序變慢,降低總吞吐量。
- 它無法處理“浮動(dòng)垃圾”,有可能會(huì)出現(xiàn)“并發(fā)失敗”進(jìn)而導(dǎo)致另一次Full GC的發(fā)生。
- 它是一款基于標(biāo)記清除算法實(shí)現(xiàn)的收集器,這意味著收集結(jié)束時(shí)會(huì)有大量空間碎片產(chǎn)生。
G1與CMS的對(duì)比:G1從整體來看是基于標(biāo)記整理算法實(shí)現(xiàn)的收集器,但從局部上看又是基于標(biāo)記復(fù)制算法實(shí)現(xiàn)。無論如何,這兩種算法都意味著G1運(yùn)作期間不會(huì)產(chǎn)生內(nèi)存空間碎片,垃圾收集完成之后能提供規(guī)整的可用內(nèi)存。比起CMS,G1的弱項(xiàng)也可以列舉出不少。例如在用戶程序運(yùn)行過程中,G1無論是為了垃圾收集產(chǎn)生的內(nèi)存占用還是程序運(yùn)行時(shí)的額外執(zhí)行負(fù)載都要比CMS要高。
- 垃圾回收方式:G1使用基于區(qū)域(Region)的分代回收策略,而CMS則使用基于標(biāo)記-清除(Mark-Sweep)算法。G1將堆內(nèi)存劃分為多個(gè)大小相等的區(qū)域,并可通過動(dòng)態(tài)調(diào)整回收集的大小來控制回收時(shí)間;CMS則主要集中在老年代的增量式垃圾回收。
- 回收效果:G1的整體性能優(yōu)于CMS,特別是在大內(nèi)存、多核處理能力和更好的預(yù)測(cè)性上。G1可以更好地控制暫停時(shí)間,并減少因大型堆內(nèi)存垃圾回收引起的應(yīng)用程序暫停。CMS適用于小型到中型堆內(nèi)存垃圾回收,不能很好地?cái)U(kuò)展到大型堆內(nèi)存的情況。
- 對(duì)CPU和內(nèi)存的影響:G1會(huì)消耗更多的CPU資源,在回收過程中會(huì)產(chǎn)生一些額外的開銷,但可以避免長(zhǎng)時(shí)間的STW(Stop-The-World)現(xiàn)象。CMS在回收過程中也可能導(dǎo)致應(yīng)用程序暫停,但占用的CPU資源相對(duì)較少。
- 內(nèi)存分配方式:G1采用了隨機(jī)空間分配策略,而CMS則使用先進(jìn)先出空間分配策略。
- G1與CMS的選擇:目前在小內(nèi)存應(yīng)用上CMS的表現(xiàn)大概率仍然要會(huì)優(yōu)于G1,而在大內(nèi)存應(yīng)用上G1則大多能發(fā)揮其優(yōu)勢(shì),這個(gè)優(yōu)劣勢(shì)的Java堆容量平衡點(diǎn)通常在6GB至8GB之間。
3.4 ZGC垃圾回收
ZGC(Z Garbage Collector)是一個(gè)可伸縮性非常好的低延遲垃圾回收器,它在JDK 11中引入,旨在處理大型堆,適用于云部署和其他內(nèi)存密集型應(yīng)用程序。
ZGC的主要特點(diǎn)包括:
- 并發(fā)處理:ZGC在垃圾回收時(shí),不會(huì)像傳統(tǒng)的GC一樣暫停整個(gè)應(yīng)用程序,而是采用了并發(fā)處理的方式,讓應(yīng)用程序和垃圾回收器同時(shí)運(yùn)行。
- 基于Region的內(nèi)存管理:ZGC將內(nèi)存劃分為許多小的Region,每個(gè)Region都有自己的內(nèi)存池和垃圾回收器線程。這種方式可以使得ZGC只回收少量的內(nèi)存,從而減少GC停頓時(shí)間。
- 無需壓縮:ZGC不需要對(duì)內(nèi)存進(jìn)行壓縮,因?yàn)樗捎昧艘环N稱為“內(nèi)存重映射”的技術(shù),將不再使用的內(nèi)存區(qū)域映射到一個(gè)新的虛擬地址空間中,從而釋放出物理內(nèi)存。
ZGC具有高性能、低延遲、可預(yù)測(cè)和背壓感知等特點(diǎn),適用于多核CPU的應(yīng)用場(chǎng)景,它可以在不犧牲吞吐量的情況下,大大減少GC停頓時(shí)間,從而提高應(yīng)用程序的響應(yīng)速度和性能。
4.JVM調(diào)優(yōu)
4.1 JVM常用參數(shù)
- -Xmx:指定 Java 堆的最大內(nèi)存容量,例如 -Xmx2g 表示最大可用內(nèi)存為 2 GB。
- -Xms:指定 Java 堆的初始內(nèi)存容量,例如 -Xms512m 表示初始可用內(nèi)存為 512 MB。
- -Xmn:指定新生代的初始大小。
- -XX:MaxPermSize:指定非堆區(qū)的最大內(nèi)存容量,例如 -XX:MaxPermSize=256m 表示最大可用內(nèi)存為 256 MB。
- -XX:PermSize:指定非堆區(qū)的初始內(nèi)存容量,例如 -XX:PermSize=128m 表示初始可用內(nèi)存為 128 MB。
- -Xss:指定線程的堆棧大小,例如 -Xss1m 表示線程的堆棧大小為 1 MB。
- -XX:+UseParallelGC:使用并行垃圾回收器,提高回收效率。
- -XX:+UseConcMarkSweepGC:使用 CMS 垃圾回收器,在減少垃圾回收停頓時(shí)間方面表現(xiàn)出色。
- -XX:+PrintGCDetails:打印垃圾回收的詳細(xì)信息。
- -XX:+HeapDumpOnOutOfMemoryError:當(dāng)發(fā)生 OutOfMemoryError 錯(cuò)誤時(shí),自動(dòng)生成堆轉(zhuǎn)儲(chǔ)快照文件。
4.2 JVM調(diào)優(yōu)策略
- 定位問題:首先需要確定 JVM 調(diào)優(yōu)的目標(biāo)和調(diào)優(yōu)范圍,以及具體的性能問題,例如內(nèi)存使用過高、垃圾回收頻繁、響應(yīng)時(shí)間慢等。
- 收集數(shù)據(jù):通過 JVM 的監(jiān)控工具或第三方工具(例如 VisualVM、JConsole、JMC 等),收集 JVM 運(yùn)行時(shí)的各項(xiàng)指標(biāo)數(shù)據(jù),例如 CPU 使用率、內(nèi)存使用情況、垃圾回收信息等。
- 分析數(shù)據(jù):分析收集到的數(shù)據(jù),找出瓶頸所在,例如內(nèi)存泄漏、垃圾回收效率低等問題,確定需要調(diào)整的 JVM 參數(shù)。
- 調(diào)整 JVM 參數(shù):根據(jù)問題的根源,調(diào)整相應(yīng)的 JVM 參數(shù)。例如,如果堆內(nèi)存占用過高,可以增加堆內(nèi)存的大?。ㄍㄟ^ -Xmx 參數(shù))或者調(diào)整垃圾回收器的類型(通過 -XX:+UseConcMarkSweepGC 參數(shù)等)。
- 測(cè)試驗(yàn)證:在實(shí)際環(huán)境中測(cè)試優(yōu)化后的 JVM,驗(yàn)證是否能夠有效地解決問題,并進(jìn)行持續(xù)性能監(jiān)測(cè)和優(yōu)化。
到此這篇關(guān)于Java中JVM的雙親委派、內(nèi)存溢出、垃圾回收和調(diào)優(yōu)詳解的文章就介紹到這了,更多相關(guān)JVM的雙親委派、內(nèi)存溢出內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java實(shí)現(xiàn)對(duì)服務(wù)器的自動(dòng)巡檢郵件通知
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)對(duì)服務(wù)器的自動(dòng)巡檢郵件通知,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05Java基礎(chǔ)之?dāng)?shù)組的初始化過程
Java數(shù)組分為靜態(tài)和動(dòng)態(tài)初始化,靜態(tài)初始化中,程序員設(shè)定元素初始值,系統(tǒng)決定長(zhǎng)度;動(dòng)態(tài)初始化中,程序員設(shè)定長(zhǎng)度,系統(tǒng)提供初始值,數(shù)組初始化后長(zhǎng)度固定,存儲(chǔ)在堆內(nèi)存中,數(shù)組變量在棧內(nèi)存指向堆內(nèi)存數(shù)組對(duì)象,基本類型數(shù)組存儲(chǔ)數(shù)據(jù)值,引用類型數(shù)組存儲(chǔ)對(duì)象引用2024-10-10JAVA對(duì)象和字節(jié)數(shù)組互轉(zhuǎn)操作
這篇文章主要介紹了JAVA對(duì)象和字節(jié)數(shù)組互轉(zhuǎn)操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-08-08數(shù)組實(shí)現(xiàn)Java 自定義Queue隊(duì)列及應(yīng)用操作
這篇文章主要介紹了數(shù)組實(shí)現(xiàn)Java 自定義Queue隊(duì)列及應(yīng)用操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06springboot 項(xiàng)目容器啟動(dòng)后如何自動(dòng)執(zhí)行指定方法
這篇文章主要介紹了springboot 項(xiàng)目容器啟動(dòng)后如何自動(dòng)執(zhí)行指定方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11SpringBoot @Cacheable自定義KeyGenerator方式
這篇文章主要介紹了SpringBoot @Cacheable自定義KeyGenerator方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12SpringBoot中動(dòng)態(tài)更新@Value配置方式
這篇文章主要介紹了SpringBoot中動(dòng)態(tài)更新@Value配置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09SpringCloudAlibaba整合Feign實(shí)現(xiàn)遠(yuǎn)程HTTP調(diào)用的簡(jiǎn)單示例
這篇文章主要介紹了SpringCloudAlibaba 整合 Feign 實(shí)現(xiàn)遠(yuǎn)程 HTTP 調(diào)用,文章中使用的是OpenFeign,是Spring社區(qū)開發(fā)的組件,需要的朋友可以參考下2021-09-09如何批量測(cè)試Mybatis項(xiàng)目中的Sql是否正確詳解
這篇文章主要給大家介紹了關(guān)于如何批量測(cè)試Mybatis項(xiàng)目中Sql是否正確的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-12-12Log4j詳細(xì)使用教程_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了Log4j的使用教程,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08