背景
线上集群突然间宕机几十台,查看集群的流量正常,宕机前系统的内存,CPU,LOAD等指标都是正常的。RPC端口不通,怀疑是Java进程没了,赶紧先恢复线上环境,做批量重启。
同时拉取jvm crash的机器日志,交由中间件同学查看,没多长时间就定位出问题的原因(专业的就是不一样),问题就是上面提到的Unsafe.allocateInstace问题,并给出了复现问题的代码,下面的代码在jdk8版本下稳定复现
package com.jsonqiao.study.jvm;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class UnsafeCrash {
public static Unsafe UNSAFE;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
UNSAFE = (Unsafe) field.get(null);
} catch (Throwable t) {
}
}
public static void main(String[] args) throws InstantiationException {
UNSAFE.allocateInstance(int.class);
}
}
问题根因详述
这其实是jdk8的一个bug, jdk8中的Unsafe.allocateInstance(Class<?> cls)不支持分配基础数据类型。
bug地址:bugs.java.com/bugdatabase…
详细分析
上面问题已经清晰了,下面就结合jvm源码具体分析下代码细节
上面的代码通过查看其main字节码(如下),其实可以发现int.class访问Intger.TYPE字段
0 getstatic #2 <com/jsonqiao/study/jvm/UnsafeCrash.UNSAFE : Lsun/misc/Unsafe;>
3 getstatic #3 <java/lang/Integer.TYPE : Ljava/lang/Class;>
6 invokevirtual #4 <sun/misc/Unsafe.allocateInstance : (Ljava/lang/Class;)Ljava/lang/Object;>
9 pop
10 return
翻看Integer源码,发现调用的是Class.getPrimitiveClass("int")方法,这个是native方法,在jvm层面调用的是JVM_FindPrimitiveClass方法,这个方法其实是从static oop _mirrors[T_VOID+1];缓存里取出一个 oop对象。
缓存是在jvm初始化时初始化的 初始化的关键是调用java_lang_Class::create_basic_type_mirror方法,该方法逻辑比较简单,就是调用Class_klass的allocate_instance创建一个oop实例,但是这里有个关键点是没有传入具体的klass对象,而是传入的null, 关键代码如下
oop java_class = InstanceMirrorKlass::cast(SystemDictionary::Class_klass())->allocate_instance(NULL, CHECK_0);
// 一般对象创建Class实例时的代码
Handle mirror = InstanceMirrorKlass::cast(SystemDictionary::Class_klass())->allocate_instance(k, CHECK);
这样会有什么问题呢,提前说句这也是导致后面jvm crash真正原因。
我们都知道在jvm里是通过klass/oop来描述java里的类与对象的,每加载一个Java类,在jvm层面就会生成klass实例,每创建一个Java类对象时会生成一个oop实例,这个oop实例会持有一个kclass指针,这里需要多说的一点是,在Java语言层面有Class的存在来代表每一个类,所以在加载每一个Java类时除了会生成kclass实例外,还会创建一个Class类对象即jvm里的oop对象,该对象同样会有一个指针指向对应的kclass, 访问Java层面的A.class时,在jvm层面就是访问这个oop对象。
上面描述的这段话需要仔细理解下,我也不清楚我是否描述清楚了,有点小饶。。
下面回到文章开头出错的代码,在贴下
UNSAFE.allocateInstance(int.class);
结合上面的描述,翻下UNSAFE.allocateInstance的jvm代码Unsafe_AllocateInstance,该方法底层调用链是jni_AllocObject->alloc_object, alloc_object方法代码如下, 方法入参为jclass clazz, 这个在java层面就是int.class, 在jvm层面就是一个class mirror oop对象,就是上文描述的普通对象与基础数据类型,他们创建的oop的差异就是一个执行klass的指针不为空,一个klass指针为空(基础类型)。
理解了上面这句描述,看下面的代码就很容易理解下面哪里代码会出问题了,我在代码里也备注说明了大概每句代码的含义
// 获取klass指针
KlassHandle k(THREAD, java_lang_Class::as_Klass(JNIHandles::resolve_non_null(clazz)));
// 出错代码,k()对于int.class类型(基础数据类型)返回值为NULL,导致调用空指针的方法
// 可以理解为Java层面的NullPointerException
k()->check_valid_for_instantiation(false, CHECK_NULL);
InstanceKlass::cast(k())->initialize(CHECK_NULL);
instanceOop ih = InstanceKlass::cast(k())->allocate_instance(THREAD);
return ih;
总结
其实,当知道问题原因时,结合代码去分析是相对来说比较容易,这里也仅仅是自己去浅显的理解一下jvm底层的实现。
这个故障以后我自己也思考了下,如何更快速的定位这个问题,其实最快速的定位应该是结合jvm crash的err.log日志,里面有对应出错的堆栈栈帧,pc寄存器,机器指令等,其实这些能够帮助我们快速定位到代码出错的地方,在结合相关寄存器的值,应该也能快速定位出错代码位置。
最后说一句不相关的话,线上出现问题,第一重要的事情是恢复,而不是去定位问题,定位问题是恢复以后要做的事情