Java虛擬機(jī)GC的各種缺點(diǎn)匯總
Java通過垃圾收集器(Garbage Collection,簡(jiǎn)稱GC)實(shí)現(xiàn)自動(dòng)內(nèi)存管理,這樣可有效減輕Java應(yīng)用開發(fā)人員的負(fù)擔(dān),也避免了更多內(nèi)存泄露的風(fēng)險(xiǎn)。
如果你用過C++等需要手動(dòng)管理內(nèi)存的語(yǔ)言,那么你就會(huì)體會(huì)到GC帶來(lái)的便利,降低了語(yǔ)言使用的門檻。
不過在我們享受自動(dòng)內(nèi)存管理帶來(lái)的便利時(shí),也不得不關(guān)注它帶來(lái)的一些缺點(diǎn)。Java的垃圾收集器最被人詬病的可能就是STW了,不過除此之外,它還有一些缺點(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)的檢查和寫屏障等也會(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)和分配,不過Linux采用的是兩階段提交,也就是說首先會(huì)申請(qǐng)到虛擬內(nèi)存空間,當(dāng)某個(gè)地址被訪問時(shí)才會(huì)真正分配到物理空間。目前的JDK中可指定或不指定堆大小,當(dāng)不指定時(shí)可由GC自動(dòng)調(diào)整,不過好像大多數(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)存利用率可能非常低,不過阿里的JDK開發(fā)過歸還物理內(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ī)沒有確定性,也不是以固定的頻率發(fā)生,這也會(huì)造成一些浮動(dòng)垃圾,也就是本來(lái)需要回收的對(duì)象還在占用空間,不能及時(shí)釋放也會(huì)影響到空間利用率。
我們這里探討一個(gè)與Java生成周期不確定導(dǎo)致Java的finalize特性變成雞肋的問題。
如果要寫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é)尾的類,大多都是如上這樣的使用方式,如ResourceMark和HandleMark等。
Java的finalize()機(jī)制也嘗試提供自動(dòng)資源管理,可通過重寫finalize()方法來(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類型的對(duì)象(重寫了finalize()方法的對(duì)象),并且本身不可達(dá),則會(huì)被加入到一個(gè)ReferenceQueue類型的隊(duì)列中。而系統(tǒng)在初始化的過程中,會(huì)啟動(dòng)一個(gè)FinalizerThread類型的守護(hù)線程(線程名Finalizer),該線程會(huì)不斷消費(fèi)ReferenceQueue中的對(duì)象,并執(zhí)行其finalize()方法。對(duì)象在執(zhí)行finalize()方法后,只是斷開了與Finalizer的關(guān)聯(lián),并不意味著會(huì)立即被回收,還是要等待下一次GC時(shí)才會(huì)被回收,而每個(gè)對(duì)象的finalize()方法都只會(huì)執(zhí)行一次,不會(huì)重復(fù)執(zhí)行。
它的問題在于,這個(gè)finalize()方法非常依賴于GC回收動(dòng)作,GC運(yùn)行的時(shí)間是不確定的,所以finalize()方法什么時(shí)候被調(diào)用釋放其中的資源也是不確定的。假設(shè)需要回收的是文件句柄,如果這個(gè)finalze()遲遲不發(fā)生的話,那么這從某種意義上來(lái)說,也算是資源泄漏了,盡早有可以讓資源耗盡。所以它并不能安全地實(shí)現(xiàn)自動(dòng)資源管理。
finalize()在后序的版本中已標(biāo)記過時(shí),Java 官方明確建議避免使用(詳見 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ū)
之前寫過一篇文章”GC垃圾收集時(shí),居然還有用戶線程在奔跑“,在GC發(fā)生期間,執(zhí)行本地native的線程還在運(yùn)行,不過這個(gè)線程可能會(huì)持有Java對(duì)象的間接引用,對(duì)對(duì)象的操作都需要通過JNI API來(lái)完成。
通過JNI API操作數(shù)組的方式是使用GetXXXArrayElements和ReleaseXXXArrayElements,不過這樣的操作非常影響效率,因?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ù)組副本問題,它是通過在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ān)鍵的安全檢查),能夠以犧牲JVM整體穩(wěn)定性獲取最大的性能。 由于最初是被設(shè)計(jì)為JRE的加密模塊使用,考慮到現(xiàn)在的加密算法大多以塊為單位,換句話說大多數(shù)情況下需要在JNI中頻繁傳遞小規(guī)模的數(shù)組,CriticalNative被專門設(shè)計(jì)對(duì)數(shù)組的傳遞進(jìn)行優(yōu)化。
JavaCritical函數(shù)相比較之前的版本,能更進(jìn)一步減少JNI調(diào)用開銷,這是由于它可以跳過一些"多余"的檢查,并進(jìn)入一個(gè)禁止JVM進(jìn)行垃圾回收的臨界區(qū),以此來(lái)獲得性能上的提升。
4.2、堆外內(nèi)存
許多的通信框架都會(huì)開辟一塊堆外內(nèi)存來(lái)提高效率,如netty等。實(shí)際上,在網(wǎng)絡(luò)和磁盤IO過程中,如果數(shù)據(jù)是在Heap里的,最終也還是會(huì)拷貝一份到堆外,然后再進(jìn)行發(fā)送。原因在于,操作系統(tǒng)把內(nèi)存中的數(shù)據(jù)寫入磁盤或網(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ì)通過Unsafe的native方法直接在Java堆外通過malloc分配一塊內(nèi)存,然后通過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)文章
線程池調(diào)用kafka發(fā)送消息產(chǎn)生的內(nèi)存泄漏問題排查解決
這篇文章主要為大家介紹了線程池調(diào)用kafka發(fā)送消息產(chǎn)生的內(nèi)存泄漏問題排查解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
Java Set接口及常用實(shí)現(xiàn)類總結(jié)
Collection的另一個(gè)子接口就是Set,他并沒有我們List常用,并且自身也沒有一些額外的方法,全是繼承自Collection中的,因此我們還是簡(jiǎn)單總結(jié)一下,包括他的常用實(shí)現(xiàn)類HashSet、LinkedHashSet、TreeSet的總結(jié)2023-01-01
解析Java和Eclipse中加載本地庫(kù)(.dll文件)的詳細(xì)說明
本篇文章是對(duì)Java和Eclipse中加載本地庫(kù)(.dll文件)進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05
IDEA連接MySQL提示serverTimezone的問題及解決方法
很多朋友私聊小編,使用IDEA軟件連接MySQL數(shù)據(jù)庫(kù)時(shí)總是提示Server returns invalid timezone. Go to 'Advanced' tab and set 'serverTimezone' property manually.的錯(cuò)誤,小編就不一一回復(fù)大家了,下面小編把我的解決方法分享到腳本之家平臺(tái),需要的朋友參考下吧2021-05-05
SpringCloudAlibaba微服務(wù)調(diào)用組件OpenFeign的方法
Feign是Netflix開發(fā)的聲明式、模板化的HTTP客戶端,其靈感來(lái)自Retrofit、JAXRS-2.0以及WebSocket,Feign可幫助我們更加便捷、優(yōu)雅地調(diào)用HTTP API,這篇文章主要介紹了SpringCloudAlibaba微服務(wù)調(diào)用組件OpenFeign,需要的朋友可以參考下2024-07-07
springboot向elk寫日志實(shí)現(xiàn)過程
這篇文章主要介紹了springboot向elk寫日志實(shí)現(xiàn)過程,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-10-10
spring和quartz整合,并簡(jiǎn)單調(diào)用(實(shí)例講解)
下面小編就為大家?guī)?lái)一篇spring和quartz整合,并簡(jiǎn)單調(diào)用(實(shí)例講解)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2017-07-07

