首页
学习
活动
专区
圈层
工具
发布
50 篇文章
1
混部之殇-论云原生资源隔离技术之CPU隔离(一)
2
腾讯TencentOS 十年云原生的迭代演进之路
5
一次内核hung task分析
7
NFSv4客户端hung住的BUG,您解决了吗?
8
nfs不同版本的挂载与解析
9
(好文重发)朴英敏:用crash工具分析Linux内核死锁的一次实战
10
内核问题解决方法记录
11
blocked for more than 120 seconds
12
记一次Linux主机内存脏数据引发的NameNode故障
13
​[linux][memory]cgroup回收内存对虚拟机的影响分析
14
docker cgroup 技术之memory(首篇)
15
[linux][memory] 内存回收
16
Linux内核理解 Memory barrier(内存屏障)
17
Linux内核27-优化和内存屏障
18
谢宝友:深入理解 Linux RCU 从硬件说起之内存屏障
19
谢宝友:深入理解 RCU 之概念
20
聊聊 Linux 上软件实现的“交换机” - Bridge!
21
谈谈 Linux 假死现象
22
宋宝华: 数据库为什么有可能喜欢Linux AIO(异步I/O)?
23
深入理解Linux内核之脏页跟踪
24
Iowait的成因、对系统影响及对策
25
打通IO栈:一次编译服务器性能优化实战
26
浅谈Linux dirty data配置
27
write文件一个字节后何时发起写磁盘IO?
28
深入理解 Linux的 I/O 系统
29
深入理解Linux 的Page Cache
30
深入理解Linux文件系统之文件系统挂载(上)
31
深入理解Linux文件系统之文件系统挂载(下)
32
【线上故障】通过系统日志分析和定位
33
实战案例分享:根据 JVM crash 日志定位和分析问题
34
Linux系统安全 | Linux日志分析和管理
35
如何快速处理线上故障
36
面试-线上故障如何排查
37
Linux内核Crash分析
38
内核timer crash debug思路
39
一次解决Linux内核内存泄漏实战全过程
40
Linux Kernel模块内存泄露的一种查找思路
41
linux系统奔溃之vmcore:kdump 的亲密战友 crash
42
crash浅析tasklist_lock与进程释放
43
Linux OOM机制分析
44
cgroup oom引发Pod重建问题分析
45
workqueue相关数据结构在内核crash分析中的实战应用
46
Linux设备驱动workqueue(工作队列)案例实现
47
Linux内核中的软中断、tasklet和工作队列具体解释
48
扒开 Linux 中断的底裤之 workqueue
49
Linux系统驱动之GIC驱动程序对中断的处理流程
50
Linux系统驱动之链式中断控制器驱动程序编写

实战案例分享:根据 JVM crash 日志定位和分析问题

1. JVM crash了

下面是一份crash report, 下面是截取了crash report的部分,用于分析:

代码语言:javascript
复制
# 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线程,初步判断在执行GC的时候发生了crash,代码段在0x5bbf05,函数是instanceKlass::oop_follow_content。
  • InstanceKlass 就是我们常说的class对象,因为是在GC的时候出现问题,具体的代码段通常是在GC部分并不能容易的判断发生了什么,而我们更需要知道的是GC的时候在处理哪个对象出了问题

2. GC 的参数

JVM在GC的控制参数中,有一个GC前进行校验的参数,在校验过程中当发生地址异常的化会打印出异常的地址,并且让JVM crash,因为这个参数每一次GC都要检查,包括新生代的GC,影响一定的性能,并不适合在产品环境中使用,但对发现GC中的对象问题,却非常有帮助。

代码语言:javascript
复制
-XX:+VerifyBeforeGC -XX:+VerifyAfterGC

产品的日志打印出了异常的对象地址:

代码语言:javascript
复制
Failed: 0x000000079ac5fe30 -> 0x0000000410bc55c0

3. SA 工具之CLHSDB

知道错误的对象地址,需要分析core dump知道哪个对象出了问题,在Linux上通常会用GDB,但是这并不适合分析我们初学者,尤其是我们并不是非常清楚对象的结构和布局,我们需要利用JMV提供的SA工具 JVM提供的HSDB工具是一款非常好的工具,通过工具能查看和分析运行中的JVM的heap对象,当然也可以常看core dump, 但问题是HSDB是有UI界面的,我们在linux系统中通常没有UI界面,用过HSDB工具,可以发现当我们启动命令控制台的时候,实际上HSDB是把CLHSDB嵌入在了HSDB的图形界面里,那我们可以使用CLHSDB来通过命令行的方式进行dump分析。

3.1 如何启动CLHSDB

代码语言:javascript
复制
java -cp .:$JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.CLHSDB

Attach 一个core dump:

代码语言:javascript
复制
java -cp .:$JAVA_HOME/lib/sa-jdi.jar sun.jvm.hotspot.CLHSDB $JAVA_HOME/bin/java 99083

这里有几个注意点:

  • 版本问题,如果产品上装了多个JVM环境的化,注意core dump要和JVM的分析的版本一致
  • SA环境需要root权限

3.2 分析对象

在前面提到的日志中,错误的对象地址是:Failed: 0x000000079ac5fe30 -> 0x0000000410bc55c0

先扫描一下0x000000079ac5fe30附近的地址的对象

可以看到0x000000079ac5fe30地址最近的对象的地址0x000000079ac5fe08这是一个MemberName对象,继续查看地址0x000000079ac5fe30的内容

查看一下地址0x0000000782178ab8的对象,就是一个method的对象

这样我们就能构建了地址的 0x000000079ac5fe30对象

  • 地址0x000000079ac5fe30 是属于0x000000079ac5fe08地址的对象的成员,也就是MemberName对象的成员
  • 通过0x0000000782178ab8的地址分析,这是一个reinvokeTarget的method的地址

我们在来看MemberName的对象结构

代码语言:javascript
复制
 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地址位移分析,我们都可以很准确的判定,0x000000079ac5fe30对应的是vmtarget的对象。(在JVM里经常会内部修改一些类的内部结构用于记录状态,但是又不能被Java应用修改)

但是有点不对,刚才不是地址是 0x0000000410bc55c0,怎么现在变成了0x0000000782178ab8? 要知道这两个地址为何不一样,我们先要对应代码段,地址 0x0000000410bc55c0是怎么获取到的?Crash report里会有堆栈信息 crash report就不贴了,最后调用的是VerifyFieldColsure:do_oop

代码语言:javascript
复制
class VerifyFieldClosure: public OopClosure {
 protected:
  template <class T> 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); }
};

日志里打印的

代码语言:javascript
复制
Failed: 0x000000079ac5fe30 -> 0x0000000410bc55c0

就是这个函数打印出来的,在代码里obj的地址很明显的调用了函数load_decode_heap_oop(p)

代码语言:javascript
复制
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. 指针的压缩

在继续分析下去之前,我们先要介绍oop, narrowOop的背景

在JVM 1.6后面为了节省heap的堆内存会使用压缩指针地址的设计,因为对象结构里指向别的对象是指针引用oop,这个地址是保存在Heap中的,保存Bit 64的地址太浪费Heap空间,所以JVM里保存了一个以heap的基地址为基本地址,计算对象真实地址和基本地址差值并且通过位移(shift)来节省空间,该指针定义为narrow_oop而不同于常见的oop 一个小坑:虽然使用了narrow_oop,当指定的heap的地址空间低于一个阀值的情况下会将narrow_oop的基地址和shift都设置为0,也就是不压缩指针可以通过设置参数:-XX:+PrintCompressedOopsMode 打印来判断narrowoop的base和shift

0x0000000410bc55c0 是个无效地址,而0x0000000782178ab8却是个有效地址,对应的是method instance同时也能匹配上MemberName.vmtarget,我们可以认为0x0000000782178ab8的地址是有效的,为何JVM通过decode地址是0x0000000410bc55c0确实个无效地址,非常有可能存在JVM并没有把压缩后的地址保存在vmtarget中,而是直接把真实的地址赋给了vmtarget,为了猜测是否有效,我们来看jvm的代码

代码语言:javascript
复制
void java_lang_invoke_MemberName::adjust_vmtarget(oop mname, oop ref) {
mname->address_field_put(_vmtarget_offset, (address)ref);
}

4. MethodHandler

虽然我们找到了JVM crash问题的根因,但我们还需要继续深入的找到谁才是罪魁祸首,就是JVM为何会调整vmtarget的值 分析谁调用了adjust_vmtarget函数即可

代码语言:javascript
复制
 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()));
-      }

很幸运,只有methodhandles.cpp调用,而函数adjust_method_entries,只在redefineclass的时候调用就是在instrument的时候,目前比较红火的RASP技术的核心关键。

5. 如何修复?

既然问题出现在地址压缩上,那么修复就变的非常简单,只要压缩地址后保存就可以了

代码语言:javascript
复制
mname->address_field_put(_vmtarget_offset, (address)ref);

改成

代码语言:javascript
复制
mname->obj_field_put(_vmtarget_offset, new_method);

如果你不想修改代码?

  • 一种方法比较简单,就是instrument的时候不修改methodhandle的类就好
  • 既然问题出在压缩指针上,不压缩不就没问题了么?JVM提供了环境参数可以控制是否压缩指针
代码语言:javascript
复制
 -XX:+UseCompressedOops

这样一个完成的通过JVM crash 日志和core dump进行JVM的问题定位和分析结束了,希望能对你有所帮助。

下一篇
举报
领券