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