savepoint, gc roots
-
對象是否已死?
- 引用計數(shù)
- 解決不了循環(huán)引用問題
- 可達性分析(從 gc roots搜索引用路徑)
- 虛擬機棧(棧幀中的本地變量表)中的-引用對象
- 本地方法棧中-JNI(native方法)引用的對象
- 方法區(qū)中-類靜態(tài)屬性引用的對象
- 方法區(qū)中-常量引用的對象
- 引用(上面可達性分析都涉及到了引用)
- strong reference
- soft reference
- 將要發(fā)生內(nèi)存溢出之前,將把這些對象列進回收范圍之中進行第二次回收,如果這次回收還沒有足夠內(nèi)存則拋出OOM
- weak reference
- 當(dāng)gc工作時 無論當(dāng)前內(nèi)存是否夠用 都會回收掉只有弱引用的對象
- phantom reference
- 為一個對象設(shè)置一個虛引用唯一目的是在這個對象被收集時受到一個系統(tǒng)通知
- 生存還是死亡
- 當(dāng)可達性分析后,判斷一個對象不可達或可達只是存在phantom,weak或soft在回收隊列中時,則第一次標(biāo)記并賽選
- 賽選條件是:是否有必要執(zhí)行finalize方法
- 沒有必要:對象沒有覆蓋finalize方法和finalize方法已經(jīng)被JVM調(diào)用過了
- 賽選條件是:是否有必要執(zhí)行finalize方法
- 有必要執(zhí)行finalize方法的放在f-queue隊列當(dāng)中,jvm自動創(chuàng)建且低優(yōu)先級的Finalizer線程執(zhí)行(這里是觸發(fā)不保證等它運行結(jié)束)
- finalize方法是對象逃離死亡的最后一次機會
- 忘掉finalize這個方法的存在
- 當(dāng)可達性分析后,判斷一個對象不可達或可達只是存在phantom,weak或soft在回收隊列中時,則第一次標(biāo)記并賽選
- 回收方法區(qū)
- 廢棄常量和無用的類
- 無用的類(-Xnoclassgc,-verbose:class,-XX:+TraceClassLoading,-XX:+TraceClassUnloading)
- 該類的所有實例都已經(jīng)回收
- 加載該類的classloader已經(jīng)被回收
- 該類對應(yīng)的java.lang.Class對象沒有被引用
- 引用計數(shù)
-
gc roots
- gc roots tracing
- 時間不能耗太多
- 不需要一個不漏的檢查完所有的執(zhí)行上下文和全局引用位置
- OopMap的數(shù)據(jù)結(jié)構(gòu)實現(xiàn) 直接從這里得知信息
- stop the world
- 時間不能耗太多
- savepoint
- 當(dāng)GC需要中斷線程的時候,不直接對線程操作,而是簡單設(shè)置一個標(biāo)志,各個線程執(zhí)行時主動去輪詢這個標(biāo)志,標(biāo)志為真時主動掛起
- saveregion
- 對應(yīng)沒有分到cpu的線程既: sleep, blocked等,無法響應(yīng)中斷
- 安全區(qū)域指:一段代碼片段中 引用關(guān)系不會發(fā)生變化 這個區(qū)域任何地方執(zhí)行GC都是安全的
- 在線程執(zhí)行到了safe region中的代碼時 標(biāo)識自己進入了safe region 當(dāng)在這段時間里JVM發(fā)起GC 就不用管標(biāo)識自己為safe region狀態(tài)的線程了
- 在線程要離開safe region時 它要檢查系統(tǒng)是否已經(jīng)完成了根節(jié)點枚舉或整個GC過程 如果完成就繼續(xù)執(zhí)行 否則等待收到可以離開safe region的信號位置
- gc roots tracing
-
savepoint
- 一直都知道,當(dāng)發(fā)生GC時,正在執(zhí)行Java code的線程必須全部停下來,才可以進行垃圾回收,這就是熟悉的STW(stop the world),但是STW的背后實現(xiàn)原理,比如這些線程如何暫停、又如何恢復(fù)?就比較疑惑了, 然而這一切的一切,都涉及到一個概念safepoint,openjdk的實現(xiàn)位于openjdk/hotspot/src/share/vm/runtime/safepoint.cpp
- 什么是safepoint
- safepoint可以用在不同地方,比如GC、Deoptimization,在Hotspot VM中,GC safepoint比較常見,需要一個數(shù)據(jù)結(jié)構(gòu)記錄每個線程的調(diào)用棧、寄存器等一些重要的數(shù)據(jù)
- 從線程角度看,safepoint可以理解成是在代碼執(zhí)行過程中的一些特殊位置,當(dāng)線程執(zhí)行到這些位置的時候,說明虛擬機當(dāng)前的狀態(tài)是安全的,如果有需要,可以在這個位置暫停,
- 比如發(fā)生GC時,需要暫停所以活動線程,但是線程在這個時刻,還沒有執(zhí)行到一個安全點,所以該線程應(yīng)該繼續(xù)執(zhí)行,到達下一個安全點的時候暫停,等待GC結(jié)束。
- 什么地方可以放safepoint
- 理論上,在解釋器的每條字節(jié)碼的邊界都可以放一個safepoint,不過掛在safepoint的調(diào)試符號信息要占用內(nèi)存空間,如果每條機器碼后面都加safepoint的話,需要保存大量的運行時數(shù)據(jù),所以要盡量少放置safepoint,在safepoint會生成polling代碼詢問VM是否要“進入safepoint”,polling操作也是有開銷的
- 通過JIT編譯的代碼里,會在所有方法的返回之前,以及所有非counted loop的循環(huán)(無界循環(huán))回跳之前放置一個safepoint,為了防止發(fā)生GC需要STW時,該線程一直不能暫停。另外,JIT編譯器在生成機器碼的同時會為每個safepoint生成一些“調(diào)試符號信息”,為GC生成的符號信息是OopMap,指出棧上和寄存器里哪里有GC管理的指針
-
線程如何被掛起
- 如果觸發(fā)GC動作,VM thread會在VMThread::loop()方法中調(diào)用SafepointSynchronize::begin()方法,最終使所有的線程都進入到safepoint
// Roll all threads forward to a safepoint and suspend them all void SafepointSynchronize::begin() { Thread* myThread = Thread::current(); assert(myThread->is_VM_thread(), "Only VM thread may execute a safepoint"); if (PrintSafepointStatistics || PrintSafepointStatisticsTimeout > 0) { _safepoint_begin_time = os::javaTimeNanos(); _ts_of_current_safepoint = tty->time_stamp().seconds(); } ... }
- 在safepoint實現(xiàn)中,有這樣一段注釋,Java threads可以有多種不同的狀態(tài),所以掛起的機制也不同,一共列舉了5中情況:
- 1.執(zhí)行java code
- 在執(zhí)行字節(jié)碼時會檢查safepoint狀態(tài),因為在begin方法中會調(diào)用Interpreter::notice_safepoints()方法,通知解釋器更新dispatch table
- 2.執(zhí)行native code
- 如果VM thread發(fā)現(xiàn)一個Java thread正在執(zhí)行native code,并不會等待該Java thread阻塞,不過當(dāng)該Java thread從native code返回時,必須檢查safepoint狀態(tài),看是否需要進行阻塞
- 3.執(zhí)行compiled code
- 如果想進入safepoint,則設(shè)置polling page不可讀,當(dāng)Java thread發(fā)現(xiàn)該內(nèi)存頁不可讀時,最終會被阻塞掛起。在SafepointSynchronize::begin()方法中,通過 os::make_polling_page_unreadable()方法設(shè)置polling page為不可讀
- 4.線程處于Block狀態(tài)
- 即使線程已經(jīng)滿足了block condition,也要等到safepoint operation完成,如GC操作,才能返回
- 5.線程正在轉(zhuǎn)換狀態(tài)
- 會去檢查safepoint狀態(tài),如果需要阻塞,就把自己掛起
- 最終實現(xiàn)
- 當(dāng)線程訪問到被保護的內(nèi)存地址時,會觸發(fā)一個SIGSEGV信號,進而觸發(fā)JVM的signal handler來阻塞這個線程,The GC thread can protect some memory to which all threads in the process can write (using the mprotect system call) so they no longer can. Upon accessing this temporarily forbidden memory, a signal handler kicks in。再看看底層是如何處理這個SIGSEGV信號,實現(xiàn)位于hotspot/src/os_cpu/linux_x86/vm/os_linux_x86.cpp 執(zhí)行os::block_on_serialize_page_trap()把當(dāng)前線程阻塞掛起
- 1.執(zhí)行java code
- 線程如何恢復(fù)
- 有了begin方法,自然有對應(yīng)的end方法,在SafepointSynchronize::end()中,會最終喚醒所有掛起等待的線程,大概實現(xiàn)如下:
- 1.重新設(shè)置pooling page為可讀
- 2.設(shè)置解釋器為ignore_safepoints
- 3.喚醒所有掛起等待的線程
- 如果觸發(fā)GC動作,VM thread會在VMThread::loop()方法中調(diào)用SafepointSynchronize::begin()方法,最終使所有的線程都進入到safepoint
-
對JVM性能有什么影響
- 通過設(shè)置JVM參數(shù) -XX:+PrintGCApplicationStoppedTime, 可以打出系統(tǒng)停止的時間
- 一個大概率的原因是當(dāng)發(fā)生GC時,有線程遲遲進入不到safepoint進行阻塞,導(dǎo)致其他已經(jīng)停止的線程也一直等待,VM Thread也在等待所有的Java線程掛起才能開始GC,這里需要分析業(yè)務(wù)代碼中是否存在有界的大循環(huán)邏輯,可能在JIT優(yōu)化時,這些循環(huán)操作沒有插入safepoint檢查
-
References