Java虛擬機(jī)GC的各種缺點(diǎn)匯總
Java通過(guò)垃圾收集器(Garbage Collection,簡(jiǎn)稱(chēng)GC)實(shí)現(xiàn)自動(dòng)內(nèi)存管理,這樣可有效減輕Java應(yīng)用開(kāi)發(fā)人員的負(fù)擔(dān),也避免了更多內(nèi)存泄露的風(fēng)險(xiǎn)。
如果你用過(guò)C++等需要手動(dòng)管理內(nèi)存的語(yǔ)言,那么你就會(huì)體會(huì)到GC帶來(lái)的便利,降低了語(yǔ)言使用的門(mén)檻。
不過(guò)在我們享受自動(dòng)內(nèi)存管理帶來(lái)的便利時(shí),也不得不關(guān)注它帶來(lái)的一些缺點(diǎn)。Java的垃圾收集器最被人詬病的可能就是STW了,不過(guò)除此之外,它還有一些缺點(diǎn),這一篇我們就列舉一下GC的幾大缺點(diǎn)。
1、停頓(SWT,stop-the-world)
在垃圾收集時(shí),垃圾收集周期要求所有的應(yīng)用程序線程停頓,這樣是為了避免在垃圾收集時(shí),應(yīng)用程序代碼破壞垃圾收集線程所掌握的堆狀態(tài)信息。
STW會(huì)讓所有業(yè)務(wù)線程暫停執(zhí)行,等待GC的標(biāo)記,即使是ZGC以及C4等相對(duì)先進(jìn)的垃圾收集器,仍然在根掃描等階段避免不了完全STW。這會(huì)降低整個(gè)業(yè)務(wù)的吞吐量,因?yàn)槔占⒉皇窃谧鰳I(yè)務(wù)相關(guān)的事情。STW也會(huì)讓增加時(shí)延,降低響應(yīng)速度。
如果你的應(yīng)用程序關(guān)注的是時(shí)延,那么看看JDK是否支持最新的垃圾收集器,如ZGC 這樣的就是主打低延遲的垃圾收集器;如果你的應(yīng)用程序只關(guān)注吞吐量,那就選擇Parallel GC,這個(gè)垃圾收集器雖然早就存在,但就吞吐量而言,仍然要比其它的收集器有一定的優(yōu)勢(shì)。
另外,整個(gè)虛擬機(jī)是一個(gè)系統(tǒng),而GC也是這個(gè)系統(tǒng)的一部分,并不是單獨(dú)運(yùn)行,需要和棧、編譯器以及線程等交互,線程安全點(diǎn)的檢查和寫(xiě)屏障等也會(huì)直接影響到程序的效率。
2、占用更多的內(nèi)存/內(nèi)存利用率低
最直接的空間浪費(fèi)就是To Survivor區(qū)了,目前許多GC都是采用分代垃圾收集,將整個(gè)堆劃分為年輕代和老年代,其中年輕代又被劃分為Eden、From Survivor和To Survivor區(qū)。年輕代多采用的復(fù)制算法不允許使用To Survivor區(qū),其大小通常是整個(gè)年輕代的1/10。
老年代的空間利用率不是太高,總要有一部分擔(dān)??臻g來(lái)保證年輕代GC的順利執(zhí)行。
為了實(shí)現(xiàn)單獨(dú)回收年輕代GC,需要將老年代的對(duì)象也做為根對(duì)象進(jìn)行掃描,為了加快老年代的掃描速度,需要卡表和偏移表等數(shù)據(jù)結(jié)構(gòu)進(jìn)行輔助,這些都需要空間,如卡表通常是512字節(jié)需要1個(gè)字節(jié)的卡表,那么一個(gè)2G大小的老年代需要約4MB的卡表,而G1的記憶集需要占用更多的內(nèi)存記錄代際之間的引用關(guān)系。
在為堆分配內(nèi)存空間時(shí),通常會(huì)調(diào)用mmap()申請(qǐng)和分配,不過(guò)Linux采用的是兩階段提交,也就是說(shuō)首先會(huì)申請(qǐng)到虛擬內(nèi)存空間,當(dāng)某個(gè)地址被訪問(wèn)時(shí)才會(huì)真正分配到物理空間。目前的JDK中可指定或不指定堆大小,當(dāng)不指定時(shí)可由GC自動(dòng)調(diào)整,不過(guò)好像大多數(shù)人在使用時(shí)仍然會(huì)為虛擬機(jī)指定堆大小參數(shù),甚至?xí)榱私档脱舆t配置AlwaysPreTouch等參數(shù),讓堆提前申請(qǐng)到所有的物理內(nèi)存,避免在程序運(yùn)行時(shí)動(dòng)態(tài)分配,影響效率。無(wú)論是手動(dòng)還是自動(dòng)調(diào)整的堆大小,一旦申請(qǐng)到了物理空間后就不會(huì)釋放,試想一下,如果在流量高峰時(shí),可能申請(qǐng)到了許多的物理內(nèi)存,而在流量低時(shí)內(nèi)存利用率可能非常低,不過(guò)阿里的JDK開(kāi)發(fā)過(guò)歸還物理內(nèi)存的特性。從JDK13起,ZGC新增內(nèi)存歸還特性(Uncommit Unused Memory),可將未使用的堆內(nèi)存歸還操作系統(tǒng),很適用于容器化場(chǎng)。這些措施有利于提高內(nèi)存使用率。
3、GC發(fā)生時(shí)間未知
當(dāng)GC發(fā)生時(shí)間未知時(shí),Java對(duì)象什么時(shí)候被回收就不確定,也就是Java的生命周期(存活時(shí)間)不確定。垃圾收集發(fā)生的時(shí)機(jī)沒(méi)有確定性,也不是以固定的頻率發(fā)生,這也會(huì)造成一些浮動(dòng)垃圾,也就是本來(lái)需要回收的對(duì)象還在占用空間,不能及時(shí)釋放也會(huì)影響到空間利用率。
我們這里探討一個(gè)與Java生成周期不確定導(dǎo)致Java的finalize特性變成雞肋的問(wèn)題。
如果要寫(xiě)C++,那么能將一個(gè)對(duì)象的生命周期范圍縮小在一個(gè)塊內(nèi),如下:
class ResoruceMark{ ResourceMark(){ // 在構(gòu)造函數(shù)中申請(qǐng)資源,如互斥鎖 } ~ResourceMark(){ // 在析構(gòu)函數(shù)中釋放資源 } }; // 在塊內(nèi)使用ResourceMark管理資源 { ResourceMark mark; // 申請(qǐng)到資源 ... // mark生命周期已經(jīng)結(jié)束,自動(dòng)調(diào)用構(gòu)造函數(shù)釋放資源 }
在Java虛擬機(jī)HotSpot中,有各種Mark字符串結(jié)尾的類(lèi),大多都是如上這樣的使用方式,如ResourceMark和HandleMark等。
Java的finalize()機(jī)制也嘗試提供自動(dòng)資源管理,可通過(guò)重寫(xiě)finalize()方法來(lái)釋放資源(類(lèi)似于C++的析構(gòu)函數(shù)),當(dāng)對(duì)象被回收時(shí),自動(dòng)調(diào)用這個(gè)finalize()方法釋放資源。
在HotSpot VM中,在GC進(jìn)行可達(dá)性分析的時(shí)候,如果當(dāng)前對(duì)象是finalize類(lèi)型的對(duì)象(重寫(xiě)了finalize()方法的對(duì)象),并且本身不可達(dá),則會(huì)被加入到一個(gè)ReferenceQueue類(lèi)型的隊(duì)列中。而系統(tǒng)在初始化的過(guò)程中,會(huì)啟動(dòng)一個(gè)FinalizerThread類(lèi)型的守護(hù)線程(線程名Finalizer),該線程會(huì)不斷消費(fèi)ReferenceQueue中的對(duì)象,并執(zhí)行其finalize()方法。對(duì)象在執(zhí)行finalize()方法后,只是斷開(kāi)了與Finalizer的關(guān)聯(lián),并不意味著會(huì)立即被回收,還是要等待下一次GC時(shí)才會(huì)被回收,而每個(gè)對(duì)象的finalize()方法都只會(huì)執(zhí)行一次,不會(huì)重復(fù)執(zhí)行。
它的問(wèn)題在于,這個(gè)finalize()方法非常依賴于GC回收動(dòng)作,GC運(yùn)行的時(shí)間是不確定的,所以finalize()方法什么時(shí)候被調(diào)用釋放其中的資源也是不確定的。假設(shè)需要回收的是文件句柄,如果這個(gè)finalze()遲遲不發(fā)生的話,那么這從某種意義上來(lái)說(shuō),也算是資源泄漏了,盡早有可以讓資源耗盡。所以它并不能安全地實(shí)現(xiàn)自動(dòng)資源管理。
finalize()在后序的版本中已標(biāo)記過(guò)時(shí),Java 官方明確建議避免使用(詳見(jiàn) JEP 421)
我們無(wú)法預(yù)知GC什么時(shí)候發(fā)生,這也會(huì)導(dǎo)致其它非預(yù)期的行為出現(xiàn),例如CMS垃圾收集器發(fā)生FullGC,這樣的FullGC收集效率低,STW時(shí)間長(zhǎng),如果此時(shí)有大量的Http請(qǐng)求,可能會(huì)在某個(gè)時(shí)刻有大量超時(shí)行為發(fā)生。
4、GC移動(dòng)對(duì)象
Java對(duì)象在GC后會(huì)被移動(dòng)到其它地方,所以在GC期間不允許操作Java對(duì)象,引用這個(gè)Java對(duì)象的地址在GC后也需要更新。
4.1、臨界區(qū)
之前寫(xiě)過(guò)一篇文章”GC垃圾收集時(shí),居然還有用戶線程在奔跑“,在GC發(fā)生期間,執(zhí)行本地native的線程還在運(yùn)行,不過(guò)這個(gè)線程可能會(huì)持有Java對(duì)象的間接引用,對(duì)對(duì)象的操作都需要通過(guò)JNI API來(lái)完成。
通過(guò)JNI API操作數(shù)組的方式是使用GetXXXArrayElements和ReleaseXXXArrayElements,不過(guò)這樣的操作非常影響效率,因?yàn)镚C會(huì)讓數(shù)組在內(nèi)存中的位置發(fā)生變化,以及直接將Java堆上的內(nèi)存地址交給用戶有些不安全,因此GetXXXArrayElements返回給用戶的是一個(gè)數(shù)組副本,而ReleaseXXXArrayElements則是將副本復(fù)制回Java堆中真實(shí)的數(shù)組里。
舉個(gè)例子如下:
JNIEXPORT void JNICALL Java_cn_hotspotvm_TestArray_mul( JNIEnv *env, jclass klass, jfloatArray mat1, jfloatArray mat2) { jboolean isCopyA, isCopyB; float *A = env->GetFloatArrayElements(mat1, &isCopyA); float *B = env->GetFloatArrayElements(mat2, &isCopyB); mult_SSE(A, B); // 第3個(gè)參數(shù)0表示將修改后的數(shù)據(jù)同步回 Java 數(shù)組,并釋放本地緩沖區(qū) env->ReleaseFloatArrayElements(mat1, A, 0); // 不將修改同步回 Java 數(shù)組,直接釋放緩沖區(qū)(適用于只讀操作) env->ReleaseFloatArrayElements(mat2, B, JNI_ABORT); }
其實(shí)在調(diào)用GetFloatArrayElements()時(shí)返回的是數(shù)組副本。
為了提高性能,我們可以使用臨界區(qū),在臨界區(qū)內(nèi)不允許發(fā)生GC,這樣就不用進(jìn)行數(shù)組副本的拷貝了,如下:
JNIEXPORT void JNICALL Java_cn_hotspotvm_TestArray_mul( JNIEnv *env, jclass klass, jfloatArray mat1, jfloatArray mat2) { jboolean isCopyA, isCopyB; float *A = static_cast<float*>(env->GetPrimitiveArrayCritical(mat1, &isCopyA)); float *B = static_cast<float*>(env->GetPrimitiveArrayCritical(mat2, &isCopyB)); mult_SSE(A, B); env->ReleasePrimitiveArrayCritical(mat1, A, 0); env->ReleasePrimitiveArrayCritical(mat2, B, JNI_ABORT); }
將GetFloatArrayElements和ReleaseFloatArrayElements換成GetPrimitiveArrayCritical和ReleasePrimitiveArrayCritical就行了。CriticalArray則是為了解決數(shù)組副本問(wèn)題,它是通過(guò)在GetPrimitiveArrayCritical和ReleasePrimitiveArrayCritical中創(chuàng)建一個(gè)阻止GC的臨界區(qū),得以將數(shù)組的真實(shí)數(shù)據(jù)直接暴露給用戶。
JNIEXPORT void JNICALL JavaCritical_cn_hotspotvm_TestArray_mul( jint length1, jfloat* mat1, jint length2, jfloat* mat2) { mult_SSE(mat1, mat2); }
CriticalNative是一種特殊的JNI函數(shù),整個(gè)函數(shù)都是一個(gè)臨界區(qū)(當(dāng)然,也包括跳過(guò)一些非關(guān)鍵的安全檢查),能夠以犧牲JVM整體穩(wěn)定性獲取最大的性能。 由于最初是被設(shè)計(jì)為JRE的加密模塊使用,考慮到現(xiàn)在的加密算法大多以塊為單位,換句話說(shuō)大多數(shù)情況下需要在JNI中頻繁傳遞小規(guī)模的數(shù)組,CriticalNative被專(zhuān)門(mén)設(shè)計(jì)對(duì)數(shù)組的傳遞進(jìn)行優(yōu)化。
JavaCritical函數(shù)相比較之前的版本,能更進(jìn)一步減少JNI調(diào)用開(kāi)銷(xiāo),這是由于它可以跳過(guò)一些"多余"的檢查,并進(jìn)入一個(gè)禁止JVM進(jìn)行垃圾回收的臨界區(qū),以此來(lái)獲得性能上的提升。
4.2、堆外內(nèi)存
許多的通信框架都會(huì)開(kāi)辟一塊堆外內(nèi)存來(lái)提高效率,如netty等。實(shí)際上,在網(wǎng)絡(luò)和磁盤(pán)IO過(guò)程中,如果數(shù)據(jù)是在Heap里的,最終也還是會(huì)拷貝一份到堆外,然后再進(jìn)行發(fā)送。原因在于,操作系統(tǒng)把內(nèi)存中的數(shù)據(jù)寫(xiě)入磁盤(pán)或網(wǎng)絡(luò)時(shí),要求數(shù)據(jù)所在的內(nèi)存區(qū)域不能變動(dòng),但是GC會(huì)對(duì)內(nèi)存進(jìn)行整理,導(dǎo)致數(shù)據(jù)內(nèi)存地址發(fā)生變化,所以只能先拷貝到堆外內(nèi)存(不受GC影響),然后把這個(gè)地址發(fā)給操作系統(tǒng)。
源代碼位置:openjdk/jdk/src/share/classes/sun/nio/ch/IOUtil.java static int read(FileDescriptor fd, ByteBuffer dst, long position, NativeDispatcher nd) throws IOException { if (dst.isReadOnly()) throw new IllegalArgumentException("Read-only buffer"); // 如果是在堆外內(nèi)存DirectBuffer時(shí),直接讀取內(nèi)容并返回就可以 if (dst instanceof DirectBuffer) return readIntoNativeBuffer(fd, dst, position, nd); // 申請(qǐng)一個(gè)臨時(shí)的DirectBuffer ByteBuffer bb = Util.getTemporaryDirectBuffer(dst.remaining()); try { // 將堆中的內(nèi)容拷貝到DirectBuffer int n = readIntoNativeBuffer(fd, bb, position, nd); bb.flip(); if (n > 0) dst.put(bb); return n; } finally { Util.offerFirstTemporaryDirectBuffer(bb); } }
在Java中有個(gè)DirectByteBuffer,DirectByteBuffer在創(chuàng)建的時(shí)候會(huì)通過(guò)Unsafe的native方法直接在Java堆外通過(guò)malloc分配一塊內(nèi)存,然后通過(guò)Unsafe的native方法來(lái)操作這塊內(nèi)存。對(duì)于需要頻繁操作的內(nèi)存,并且僅僅是臨時(shí)存在一會(huì)的,都建議使用堆外內(nèi)存,并且做成緩沖池,不斷循環(huán)利用這塊內(nèi)存。
舉個(gè)例子如下:
try (FileChannel channel = FileChannel.open(Paths.get("/tmp/data.txt"), StandardOpenOption.READ)) { // 直接緩沖區(qū) ByteBuffer buffer = ByteBuffer.allocateDirect(1024); while (channel.read(buffer) > 0) { buffer.flip(); // 處理數(shù)據(jù)... buffer.clear(); } } catch (IOException e) { e.printStackTrace(); }
調(diào)用FileChannel的open()方法會(huì)返回一個(gè)FileChannelmpl實(shí)例,這個(gè)實(shí)例的read()方法會(huì)調(diào)用IOUtil.read()方法,這個(gè)方法這是我們上面介紹的方法。
到此這篇關(guān)于Java虛擬機(jī)GC的各種缺點(diǎn)匯總的文章就介紹到這了,更多相關(guān)Java虛擬機(jī)GC內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring的嵌套事務(wù)(Propagation.NESTED)到底是個(gè)啥案例代碼講解
SavePoint是數(shù)據(jù)庫(kù)事務(wù)中的一個(gè)概念,?可以將整個(gè)事務(wù)切割為不同的小事務(wù),可以選擇將狀態(tài)回滾到某個(gè)小事務(wù)發(fā)生時(shí)的樣子,本文通過(guò)案例代碼講解Spring的嵌套事務(wù)(Propagation.NESTED)到底是個(gè)啥,感興趣的朋友跟隨小編一起看看吧2023-01-01prometheus監(jiān)控springboot應(yīng)用簡(jiǎn)單使用介紹詳解
這篇文章主要介紹了prometheus監(jiān)控springboot應(yīng)用簡(jiǎn)單使用介紹詳解,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-05-05Java利用沙箱支付實(shí)現(xiàn)電腦掃碼支付教程
當(dāng)我們制作的項(xiàng)目需要實(shí)現(xiàn)電腦掃碼支付功能時(shí),我們往往會(huì)采用沙箱支付來(lái)模擬實(shí)現(xiàn)。本文將主要介紹如何在Java中利用沙箱支付實(shí)現(xiàn)這一功能,需要的可以參考一下2022-01-01詳解Springboot集成sentinel實(shí)現(xiàn)接口限流入門(mén)
這篇文章主要介紹了詳解Springboot集成sentinel實(shí)現(xiàn)接口限流入門(mén),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11在JAVA?Web項(xiàng)目中動(dòng)態(tài)加載DLL/SO文件的方法
在JAVA?Web項(xiàng)目中,我們經(jīng)常需要調(diào)用一些第三方庫(kù)或者實(shí)現(xiàn)一些JAVA本身不支持的功能,這時(shí),我們可能會(huì)考慮使用JNI來(lái)調(diào)用DLL或SO文件,然而,因此,本文將介紹如何在JAVA?Web項(xiàng)目中動(dòng)態(tài)加載DLL/SO文件,需要的朋友可以參考下2024-12-12詳解SpringBoot 創(chuàng)建定時(shí)任務(wù)(配合數(shù)據(jù)庫(kù)動(dòng)態(tài)執(zhí)行)
本篇文章主要介紹了SpringBoot 創(chuàng)建定時(shí)任務(wù)(配合數(shù)據(jù)庫(kù)動(dòng)態(tài)執(zhí)行),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10IDEA中錯(cuò)誤:java: java.lang.NoSuchFieldError的問(wèn)題解決
本文主要介紹了IDEA中錯(cuò)誤:java: java.lang.NoSuchFieldError的問(wèn)題解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-04-04