實(shí)戰(zhàn)案例分享:根據(jù) JVM crash 日志定位和分析問題
點(diǎn)擊上方藍(lán)色字體,選擇“設(shè)為星標(biāo)”

1. JVM crash了
下面是一份crash report, 下面是截取了crash report的部分,用于分析:
# Problematic frame:
# V [libjvm.so+0x5bbf05] instanceKlass::oop_follow_contents(ParCompactionManager*, oopDesc*)+0x2c5
Stack 信息:
Stack: [0x00007fa9482b3000,0x00007fa9483b4000], sp=0x00007fa9483b2a10, free space=1022k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
V [libjvm.so+0x5bbf05] instanceKlass::oop_follow_contents(ParCompactionManager*, oopDesc*)+0x2c5
V [libjvm.so+0x87504c] ParCompactionManager::follow_marking_stacks()+0x1ec
V [libjvm.so+0x85c138] MarkFromRootsTask::do_it(GCTaskManager*, unsigned int)+0x78
V [libjvm.so+0x55813f] GCTaskThread::run()+0x12f
V [libjvm.so+0x821ca8] java_start(Thread*)+0x108
看到里面的棧信息是GCTaskThread線程,初步判斷在執(zhí)行GC的時(shí)候發(fā)生了crash,代碼段在0x5bbf05,函數(shù)是instanceKlass::oop_follow_content。
InstanceKlass 就是我們常說的class對象,因?yàn)槭窃贕C的時(shí)候出現(xiàn)問題,具體的代碼段通常是在GC部分并不能容易的判斷發(fā)生了什么,而我們更需要知道的是GC的時(shí)候在處理哪個(gè)對象出了問題
2. GC 的參數(shù)
JVM在GC的控制參數(shù)中,有一個(gè)GC前進(jìn)行校驗(yàn)的參數(shù),在校驗(yàn)過程中當(dāng)發(fā)生地址異常的化會(huì)打印出異常的地址,并且讓JVM crash,因?yàn)檫@個(gè)參數(shù)每一次GC都要檢查,包括新生代的GC,影響一定的性能,并不適合在產(chǎn)品環(huán)境中使用,但對發(fā)現(xiàn)GC中的對象問題,卻非常有幫助。
-XX:+VerifyBeforeGC -XX:+VerifyAfterGC
產(chǎn)品的日志打印出了異常的對象地址:
Failed: 0x000000079ac5fe30 -> 0x0000000410bc55c0
3. SA 工具之CLHSDB
知道錯(cuò)誤的對象地址,需要分析core dump知道哪個(gè)對象出了問題,在Linux上通常會(huì)用GDB,但是這并不適合分析我們初學(xué)者,尤其是我們并不是非常清楚對象的結(jié)構(gòu)和布局,我們需要利用JMV提供的SA工具 JVM提供的HSDB工具是一款非常好的工具,通過工具能查看和分析運(yùn)行中的JVM的heap對象,當(dāng)然也可以常看core dump, 但問題是HSDB是有UI界面的,我們在linux系統(tǒng)中通常沒有UI界面,用過HSDB工具,可以發(fā)現(xiàn)當(dāng)我們啟動(dòng)命令控制臺(tái)的時(shí)候,實(shí)際上HSDB是把CLHSDB嵌入在了HSDB的圖形界面里,那我們可以使用CLHSDB來通過命令行的方式進(jìn)行dump分析。
3.1 如何啟動(dòng)CLHSDB
java -cp .:$JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.CLHSDB
Attach 一個(gè)core dump:
java -cp .:$JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.CLHSDB $JAVA_HOME/bin/java 99083
這里有幾個(gè)注意點(diǎn):
版本問題,如果產(chǎn)品上裝了多個(gè)JVM環(huán)境的化,注意core dump要和JVM的分析的版本一致
SA環(huán)境需要root權(quán)限
3.2 分析對象
在前面提到的日志中,錯(cuò)誤的對象地址是:Failed: 0x000000079ac5fe30 -> 0x0000000410bc55c0
先掃描一下0x000000079ac5fe30附近的地址的對象
可以看到0x000000079ac5fe30地址最近的對象的地址0x000000079ac5fe08這是一個(gè)MemberName對象,繼續(xù)查看地址0x000000079ac5fe30的內(nèi)容
查看一下地址0x0000000782178ab8的對象,就是一個(gè)method的對象

這樣我們就能構(gòu)建了地址的 0x000000079ac5fe30對象
地址0x000000079ac5fe30 是屬于0x000000079ac5fe08地址的對象的成員,也就是MemberName對象的成員
通過0x0000000782178ab8的地址分析,這是一個(gè)reinvokeTarget的method的地址
我們在來看MemberName的對象結(jié)構(gòu)
final class More ...MemberName implements Member, Cloneable {
73 private Class> clazz; // class in which the method is defined
74 private String name; // may be null if not yet materialized
75 private Object type; // may be null if not yet materialized
76 private int flags; // modifier bits; see reflect.Modifier
77 //@Injected JVM_Method* vmtarget;
78 //@Injected int vmindex;
79 private Object resolution; // if null, this guy is resolved
}
無論從0x0000000782178ab8的地址對象反向分析,還是從0x000000079ac5fe08地址位移分析,我們都可以很準(zhǔn)確的判定,0x000000079ac5fe30對應(yīng)的是vmtarget的對象。(在JVM里經(jīng)常會(huì)內(nèi)部修改一些類的內(nèi)部結(jié)構(gòu)用于記錄狀態(tài),但是又不能被Java應(yīng)用修改)
但是有點(diǎn)不對,剛才不是地址是 0x0000000410bc55c0,怎么現(xiàn)在變成了0x0000000782178ab8? 要知道這兩個(gè)地址為何不一樣,我們先要對應(yīng)代碼段,地址 0x0000000410bc55c0是怎么獲取到的?Crash report里會(huì)有堆棧信息 crash report就不貼了,最后調(diào)用的是VerifyFieldColsure:do_oop
class VerifyFieldClosure: public OopClosure {
protected:
template void do_oop_work(T* p) {
guarantee(Universe::heap()->is_in_closed_subset(p), "should be in heap");
oop obj = oopDesc::load_decode_heap_oop(p);
if (!obj->is_oop_or_null()) {
tty->print_cr("Failed: " PTR_FORMAT " -> " PTR_FORMAT, p, (address)obj);
Universe::print();
guarantee(false, "boom");
}
}
public:
virtual void do_oop(oop* p) { VerifyFieldClosure::do_oop_work(p); }
virtual void do_oop(narrowOop* p) { VerifyFieldClosure::do_oop_work(p); }
};
日志里打印的
Failed: 0x000000079ac5fe30 -> 0x0000000410bc55c0
就是這個(gè)函數(shù)打印出來的,在代碼里obj的地址很明顯的調(diào)用了函數(shù)load_decode_heap_oop(p)
inline oop oopDesc::load_decode_heap_oop_not_null(oop* p) { return *p; }
inline oop oopDesc::load_decode_heap_oop_not_null(narrowOop* p) {
return decode_heap_oop_not_null(*p);
}
在oop和narrowOop的情況下是不一樣的獲取地址方式
3. 指針的壓縮
在繼續(xù)分析下去之前,我們先要介紹oop, narrowOop的背景
在JVM 1.6后面為了節(jié)省heap的堆內(nèi)存會(huì)使用壓縮指針地址的設(shè)計(jì),因?yàn)閷ο蠼Y(jié)構(gòu)里指向別的對象是指針引用oop,這個(gè)地址是保存在Heap中的,保存Bit 64的地址太浪費(fèi)Heap空間,所以JVM里保存了一個(gè)以heap的基地址為基本地址,計(jì)算對象真實(shí)地址和基本地址差值并且通過位移(shift)來節(jié)省空間,該指針定義為narrow_oop而不同于常見的oop 一個(gè)小坑:雖然使用了narrow_oop,當(dāng)指定的heap的地址空間低于一個(gè)閥值的情況下會(huì)將narrow_oop的基地址和shift都設(shè)置為0,也就是不壓縮指針可以通過設(shè)置參數(shù):-XX:+PrintCompressedOopsMode 打印來判斷narrowoop的base和shift
0x0000000410bc55c0 是個(gè)無效地址,而0x0000000782178ab8卻是個(gè)有效地址,對應(yīng)的是method instance同時(shí)也能匹配上MemberName.vmtarget,我們可以認(rèn)為0x0000000782178ab8的地址是有效的,為何JVM通過decode地址是0x0000000410bc55c0確實(shí)個(gè)無效地址,非常有可能存在JVM并沒有把壓縮后的地址保存在vmtarget中,而是直接把真實(shí)的地址賦給了vmtarget,為了猜測是否有效,我們來看jvm的代碼
void java_lang_invoke_MemberName::adjust_vmtarget(oop mname, oop ref) {
mname->address_field_put(_vmtarget_offset, (address)ref);
}
4. MethodHandler
雖然我們找到了JVM crash問題的根因,但我們還需要繼續(xù)深入的找到誰才是罪魁禍?zhǔn)祝褪荍VM為何會(huì)調(diào)整vmtarget的值 分析誰調(diào)用了adjust_vmtarget函數(shù)即可
void MemberNameTable::adjust_method_entries(methodOop* old_methods, methodOop* new_methods,
int methods_length, bool *trace_name_printed) {
assert(SafepointSynchronize::is_at_safepoint(), "only called at safepoint");
- // search the MemberNameTable for uses of either obsolete or EMCP methods
+ // For each redefined method
for (int j = 0; j < methods_length; j++) {
methodOop old_method = old_methods[j];
methodOop new_method = new_methods[j];
- oop mem_name = find_member_name_by_method(old_method);
- if (mem_name != NULL) {
- java_lang_invoke_MemberName::adjust_vmtarget(mem_name, new_method);
-
- if (RC_TRACE_IN_RANGE(0x00100000, 0x00400000)) {
- if (!(*trace_name_printed)) {
- // RC_TRACE_MESG macro has an embedded ResourceMark
- RC_TRACE_MESG(("adjust: name=%s",
- Klass::cast(old_method->method_holder())->external_name()));
- *trace_name_printed = true;
- }
- // RC_TRACE macro has an embedded ResourceMark
- RC_TRACE(0x00400000, ("MemberName method update: %s(%s)",
- new_method->name()->as_C_string(),
- new_method->signature()->as_C_string()));
- }
很幸運(yùn),只有methodhandles.cpp調(diào)用,而函數(shù)adjust_method_entries,只在redefineclass的時(shí)候調(diào)用就是在instrument的時(shí)候,目前比較紅火的RASP技術(shù)的核心關(guān)鍵。
5. 如何修復(fù)?
既然問題出現(xiàn)在地址壓縮上,那么修復(fù)就變的非常簡單,只要壓縮地址后保存就可以了
mname->address_field_put(_vmtarget_offset, (address)ref);
改成
mname->obj_field_put(_vmtarget_offset, new_method);
如果你不想修改代碼?
一種方法比較簡單,就是instrument的時(shí)候不修改methodhandle的類就好
既然問題出在壓縮指針上,不壓縮不就沒問題了么?JVM提供了環(huán)境參數(shù)可以控制是否壓縮指針
-XX:+UseCompressedOops
這樣一個(gè)完成的通過JVM crash 日志和core dump進(jìn)行JVM的問題定位和分析結(jié)束了,希望能對你有所幫助。

版權(quán)聲明:
文章不錯(cuò)?點(diǎn)個(gè)【在看】吧!??




