jdk1.8 Unsafe类初探

2022-06-23 14:30:43 浏览数 (1)

    在看java原子类时里有很多方法都调用了Unsafe类方法,Unsafe类方法在jdk里没找到源码,然后下载open jdk找到了源码,在/src/share/classes/sun/misc 目录下。定义如下:   

代码语言:javascript复制
public final class Unsafe {

    private static native void registerNatives();
    static {
        registerNatives();
        sun.reflect.Reflection.registerMethodsToFilter(Unsafe.class, "getUnsafe");
    }
    //私有构造方法
    private Unsafe() {}
    
    //实例化Unsafe
    private static final Unsafe theUnsafe = new Unsafe();

    //获取Unsafe实例,这个方法会检查类加载器类型,如果非Bootstrap classloader就会抛异常
    public static Unsafe getUnsafe() {
        Class<?> caller = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(caller.getClassLoader()))
            throw new SecurityException("Unsafe");
        return theUnsafe;
    }


    .......
    
    .......
    
    .......
}

   这个类不能使用Unsafe.getUnsafe()获取Unsafe实例,在getUnsafe()方法中会调用VM.isSystemDomainLoader检查类加载器,java自带三种类加载器,bootstrap类加载器是JVM启动的时候负责加载jre/lib/rt.jar 这个类是c 写的,在java中看不到。其它两个是ExtClassLoader 和  AppClassLoader都是继承ClassLoader类。isSystemDomainLoader会进行判断如果传入的null返回true,否则返回false,在启动阶段,加载rt.jar所有类的是bootstrap类加载器,所以调用caller.getClassLoader()会返回null,isSystemDomainLoader就会返回true。但是在我们自己写的类代码中直接调用这个类就不行了,此时是AppClassLoader,会返回false,直接抛异常。

代码语言:javascript复制
/**
     * Returns true if the given class loader is in the system domain
     * in which all permissions are granted.
     */
    public static boolean isSystemDomainLoader(ClassLoader loader) {
        return loader == null;
    }

所以一般用反射获取Unsafe类实例

代码语言:javascript复制
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe)f.get(null);

     这样就可以调用Unsafe类的方法。 Unsafe类很多方法都给了注释,从字面意思可以知道是干啥的,大部分方法都是native方法,所以想要弄清楚底层原理,必须看jvm的底层源码。Unsafe类里native方法实现在hotspot的src/share/vm/prims/unsafe.cpp里。先从public native int getInt(Object o, long offset)看,这个方法是从java堆对象或者数组中获取偏移offset的值。这中操作在c或者c 语言中很正常,直接通过指针就获取到了。在java中由于没有指针,所以需要通过native方法获取。这个方法对应的c 函数宏定义比较复杂,需要一步步把它还原出来。

代码语言:javascript复制
//本地方法结构体
typedef struct {
    char *name;     //方法名
    char *signature; //方法签名
    void *fnPtr;    //函数地址
} JNINativeMethod;
#define CC (char*)
#define CAST_FROM_FN_PTR(new_type, func_ptr) ((new_type)((address_word)(func_ptr)))
#define FN_PTR(f) CAST_FROM_FN_PTR(void*, &f)
#define LANG "Ljava/lang/"
#define OBJ LANG"Object;"   //"Ljava/lang/""Object;"
// jdk1.8的本地方法结构体数组
static JNINativeMethod methods_18[] = {
    //方法名               //方法签名           //函数入口地址
    //(char*)"getObject"                        ((void*)((uintptr_t)(&Unsafe_GetObject)))
    {CC"getObject",        CC"("OBJ"J)"OBJ"",   FN_PTR(Unsafe_GetObject)},
    {CC"putObject",        CC"("OBJ"J"OBJ")V",  FN_PTR(Unsafe_SetObject)},
    {CC"getObjectVolatile",CC"("OBJ"J)"OBJ"",   FN_PTR(Unsafe_GetObjectVolatile)},
    {CC"putObjectVolatile",CC"("OBJ"J"OBJ")V",  FN_PTR(Unsafe_SetObjectVolatile)},

    DECLARE_GETSETOOP(Boolean, Z),
    DECLARE_GETSETOOP(Byte, B),
    DECLARE_GETSETOOP(Short, S),
    DECLARE_GETSETOOP(Char, C),
    DECLARE_GETSETOOP(Int, I), //
    DECLARE_GETSETOOP(Long, J),
    DECLARE_GETSETOOP(Float, F),
    DECLARE_GETSETOOP(Double, D),
    .....
    .....
}

java的本地方法有个结构体JNINativeMethod,包括了方法名,方法签名,对应的c函数地址。所有的Unsafe类所有的本地方法都定义在了methods_18结构体数组里。getInt在DECLARE_GETSETOOP宏里。看DECLARE_GETSETOOP宏

代码语言:javascript复制
//(char*)"getInt"   Unsafe_GetInt
#define DECLARE_GETSETOOP(Boolean, Z) 
    {CC"get"#Boolean,      CC"("OBJ"J)"#Z,      FN_PTR(Unsafe_Get##Boolean)}, 
    {CC"put"#Boolean,      CC"("OBJ"J"#Z")V",   FN_PTR(Unsafe_Set##Boolean)}, 
    {CC"get"#Boolean"Volatile",      CC"("OBJ"J)"#Z,      FN_PTR(Unsafe_Get##Boolean##Volatile)}, 
    {CC"put"#Boolean"Volatile",      CC"("OBJ"J"#Z")V",   FN_PTR(Unsafe_Set##Boolean##Volatile)}

 通过# 和##号达到复用类似的定义,可以看到getInt对应的c函数应该是Unsafe_GetInt。但是这个函数名也被宏定义了,并不是直接定义的,下面看函数定义:

代码语言:javascript复制
#define JNICALL   //在linux下为空
#define JVM_END } }
#define JVM_ENTRY(result_type, header)                               
extern "C" {                                                         
  result_type JNICALL header {                                       
    JavaThread* thread=JavaThread::thread_from_jni_environment(env); 
    ThreadInVMfromNative __tiv(thread);                              
    debug_only(VMNativeEntryWrapper __vew;)                          
    VM_ENTRY_BASE(result_type, header, thread)

#define UNSAFE_ENTRY(result_type, header) 
  JVM_ENTRY(result_type, header)

//这个宏会把jdk1.4到1.8所有版本的函数都定义一遍,只看1.8
#define DEFINE_GETSETOOP(jboolean, Boolean) 
UNSAFE_ENTRY(jboolean, Unsafe_Get##Boolean(JNIEnv *env, jobject unsafe, jobject obj, jlong offset)) 
  UnsafeWrapper("Unsafe_Get"#Boolean); 
  GET_FIELD(obj, offset, jboolean, v); 
  return v; 
UNSAFE_END 

//Unsafe_GetInt函数定义
DEFINE_GETSETOOP(jint, Int);

   用DEFINE_GETSETOOP定义了jdk1.4到1.8同名的函数,最终转换为

代码语言:javascript复制
typedef int jint;
extern "C" {
    jint Unsafe_GetInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset) {
        JavaThread* thread=JavaThread::thread_from_jni_environment(env); 
        ThreadInVMfromNative __tiv(thread);                              
        debug_only(VMNativeEntryWrapper __vew;)                          
        VM_ENTRY_BASE(result_type, header, thread)
        oop p = JNIHandles::resolve(obj); 
        jint v = *(jint*)index_oop_from_field_offset_long(p, offset)
        return v;
    } 
}

 是一个int类型的函数,下面主要看resolve和index_oop_from_field_offset_long函数

代码语言:javascript复制
class _jobject {};
typedef _jobject *jobject;
//java对象头部描述符,从这个可以看出java对象头未开启指针压缩会占12字节,开启后占16字节
class oopDesc {
  friend class VMStructs;
 private:
  volatile markOop  _mark;  //markOop是指针,64位占8字节
  union _metadata {      
    Klass*      _klass;    //指针8字节
    narrowKlass _compressed_klass;  //压缩指针,unsigned int类型
  } _metadata;
  .......
  .......
};
typedef class oopDesc*                            oop;
inline oop JNIHandles::resolve(jobject handle) {
  //如果handle不为null则将handle从jobject类型转换为oop类型,oop类型
  oop result = (handle == NULL ? (oop)NULL : *(oop*)handle);
  assert(result != NULL || (handle == NULL || !CheckJNICalls || is_weak_global_handle(handle)), "Invalid value read from jni handle");
  assert(result != badJNIHandle, "Pointing to zapped jni handle area");
  return result;
};

resolve函数主要将handle从jobject类型转换为oop类型,也就是java对象头描述符。

代码语言:javascript复制
//返回对象p的基地址加偏移
inline void* index_oop_from_field_offset_long(oop p, jlong field_offset) {
  //获取偏移量
  jlong byte_offset = field_offset_to_byte_offset(field_offset);
#ifdef ASSERT
  if (p != NULL) {
    assert(byte_offset >= 0 && byte_offset <= (jlong)MAX_OBJECT_SIZE, "sane offset");
    if (byte_offset == (jint)byte_offset) {
      void* ptr_plus_disp = (address)p   byte_offset;
      assert((void*)p->obj_field_addr<oop>((jint)byte_offset) == ptr_plus_disp,
             "raw [ptr disp] must be consistent with oop::field_base");
    }
    jlong p_size = HeapWordSize * (jlong)(p->size());
    assert(byte_offset < p_size, err_msg("Unsafe access: offset " INT64_FORMAT " > object's size " INT64_FORMAT, byte_offset, p_size));
  }
#endif
  //如果是32位机器,则将long long转换为int
  if (sizeof(char*) == sizeof(jint))    // (this constant folds!)
    return (address)p   (jint) byte_offset;
  else
    return (address)p          byte_offset; //返回对象p的基地址 上偏移
}

index_oop_from_field_offset_long返回java对象内的偏移地址。最后返回偏移地址存储的一个4字节int类型的是数据。getLong和getShort等等原理类似。

再看一个public native int     getInt(long address);方法,直接从指定地址获取值

代码语言:javascript复制
//public native int     getInt(long address);对应的c函数
int Unsafe_GetNativeInt(JNIEnv *env, jobject unsafe, jlong addr) {
    void* p = addr_from_java(addr);   //将addr转换为void*指针
    JavaThread* t = JavaThread::current(); //获取当前线程
    t->set_doing_unsafe_access(true);  //设置安全访问
    java_type x = *(volatile native_type*)p;  //获取地址p存储的值
    t->set_doing_unsafe_access(false); 
    return x; //返回
}

其它这类函数原理一样。

再看Unsafe对内存的操作,public native long allocateMemory(long bytes);方法对应标准c的malloc函数,jvm对内存做一些校验后会直接调用malloc分配内存。public native long reallocateMemory(long address, long bytes);对应c的realloc函数,分配bytes字节内存,并且将以address为起始地址的数据复制到新分配的内存。

public native void setMemory(Object o, long offset, long bytes, byte value);类似与c的memset函数,将以o为起始地址,偏移为offset的地址处,填充bytes个value。

代码语言:javascript复制
void Copy::fill_to_memory_atomic(void* to, size_t size, jubyte value) {
  address dst = (address) to; //获取起始地址,to是java对象,起始地址必然是8字节对齐
  uintptr_t bits = (uintptr_t) to | (uintptr_t) size; //获取结束地址
  if (bits % sizeof(jlong) == 0) { //如果余数为0说明要填充整数个long long
    jlong fill = (julong)( (jubyte)value );
    if (fill != 0) {//将fill的8个字节,每个字节都变为value
      fill  = fill << 8;
      fill  = fill << 16;
      fill  = fill << 32;
    }
    //开始复制,复制size/sizeof(long long)个
    for (uintptr_t off = 0; off < size; off  = sizeof(jlong)) {
      *(jlong*)(dst   off) = fill;
    }
  } else if (bits % sizeof(jint) == 0) { //和上类似
    jint fill = (juint)( (jubyte)value ); // zero-extend
    if (fill != 0) {
      fill  = fill << 8;
      fill  = fill << 16;
    }
    //Copy::fill_to_jints_atomic((jint*) dst, size / sizeof(jint));
    for (uintptr_t off = 0; off < size; off  = sizeof(jint)) {
      *(jint*)(dst   off) = fill;
    }
  } else if (bits % sizeof(jshort) == 0) {//和上类似
    jshort fill = (jushort)( (jubyte)value ); // zero-extend
    fill  = fill << 8;
    //Copy::fill_to_jshorts_atomic((jshort*) dst, size / sizeof(jshort));
    for (uintptr_t off = 0; off < size; off  = sizeof(jshort)) {
      *(jshort*)(dst   off) = fill;
    }
  } else { //如果复制的个数没和以上几个类型对齐,则调用fill_to_bytes,此函数调用了memset
    // Not aligned, so no need to be atomic.
    Copy::fill_to_bytes(dst, size, value);
  }

  再看一个比较有代表性的CAS操作,看看jvm是如何实现的,public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);以对象o为起始地址,偏移为offset处的值如果和expected相等,则将该处的值设置为x,返回true,否则不变返回false。

代码语言:javascript复制
jboolean Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x) {
  JavaThread* thread=JavaThread::thread_from_jni_environment(env); 
  ThreadInVMfromNative __tiv(thread);                              
  debug_only(VMNativeEntryWrapper __vew;)                          
  VM_ENTRY_BASE(result_type, header, thread)
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);  //将obj转为oop类型
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);//获取偏移地址
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e; //交换
}

//比较cmp $0,mp,如果mp是0,则跳到标号1,否则加上lock
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
//调用了intel的cmpxchg指令,如果是多核处理器则加lock前缀
inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  int mp = os::is_MP(); //如果是多核返回1,否则返回0
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)" //判断是否是多核处理器,如果是加lock前缀
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

在x86下最终也是调用的cmpxchg指令。并且如果是多核的话加上lock前缀,保证这条指令的原子性。cmpxchg的实现和linux kernel的实现差不多。cmpxchg指令已经在前面的博客详细分析过了,这里就不多说了。

下面再看public native int     getIntVolatile(Object o, long offset);方法,获取int值但是加了个volatile,对应的c函数如下

代码语言:javascript复制
//返回地址p的值,注意c/c  的volatile语义和java不同,volatile主要特性就是
//防止编译器优化将p地址的值缓冲到寄存器,防止编译器将变量访问打乱顺序,而java的volatile语义
//有内存屏障的作用
inline jint     OrderAccess::load_acquire(volatile jint*    p) { return *p; }
int Unsafe_GetIntVolatile(JNIEnv *env, jobject unsafe, jobject obj, jlong offset))
  UnsafeWrapper("Unsafe_Get"#Boolean); 
  oop p = JNIHandles::resolve(obj); //将obj从jobject类型转换为oop类型
  //获取偏移地址然后取值
  volatile  int v = OrderAccess::load_acquire((volatile int*)index_oop_from_field_offset_long(p, offset));
  return v; 
}

发现对地址的访问加了volatile,注意这里和java的volatile修饰符语义不一样,c/c 的volatile修饰符只是阻止编译器对变量进行优化,防止将地址p里的值缓冲的寄存器,而不是从cache或者内存读。volatile这个一般在操作IO寄存器或者多线程编程的时候有用。

再看 public native void    putIntVolatile(Object o, long offset, int x);方法,在对象偏移offset处存储x。对应的c函数如下:

代码语言:javascript复制
//xchg指令是两个寄存器内容交换,此处可以将*p的值给v,v的值给*p,为什么用xchg,不直接用mov
//因为这个操作保证store不会发生重排序,xchg指令是会自动加lock前缀的(可以查看intel官方手册)
//所以这个操作保证了原子性和起了内存屏障的作用
inline void     OrderAccess::release_store_fence(volatile jint*   p, jint   v) {
  __asm__ volatile (  "xchgl (%2),%0"
                    : "=r" (v)
                    : "0" (v), "r" (p)
                    : "memory");
}

void Unsafe_SetIntVolatile(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jboolean x))  {
    oop p = JNIHandles::resolve(obj); //将obj转换为oop类型
    //获取偏移地址并将值存到偏移地址处,这个存储不会重排序
    OrderAccess::release_store_fence((volatile type_name*)index_oop_from_field_offset_long(p, offset), x);

}

这个操作有了内存屏障的作用,防止了内存重排序,底层使用了xchg指令,这个指令会自动加lock前缀,lock前缀不但具有原子性也具有屏障作用。

public native void    putOrderedInt(Object o, long offset, int x);方法和 putIntVolatile一样。 putOrderedInt也是调用了Unsafe_SetIntVolatile函数。

代码语言:javascript复制
#define SET_FIELD_VOLATILE(obj, offset, type_name, x) 
  oop p = JNIHandles::resolve(obj); 
  OrderAccess::release_store_fence((volatile type_name*)index_oop_from_field_offset_long(p, offset), x);


UNSAFE_ENTRY(void, Unsafe_SetOrderedInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint x))
  UnsafeWrapper("Unsafe_SetOrderedInt");
  SET_FIELD_VOLATILE(obj, offset, jint, x);
UNSAFE_END

然后再看下Unsafe类里的内存屏障,public native void loadFence();读内存屏障,load屏障调用了acquire函数。

代码语言:javascript复制
//将寄存器栈顶值复制给局部变量,保证了编译器不会重排序,这里没使用lfence指令,因为
//x86不会发生read read重排序
inline void OrderAccess::acquire() {
  volatile intptr_t local_dummy;
#ifdef AMD64
  __asm__ volatile ("movq 0(%%rsp), %0" : "=r" (local_dummy) : : "memory");
#else
  __asm__ volatile ("movl 0(%%esp),%0" : "=r" (local_dummy) : : "memory");
#endif // AMD64
}

没使用lfence,lfence是保证lfence之前所有的读操作完成之前,lfence之后的读操作不会越过屏障先读,由于x86 load load不会重排序,所以只需要保证编译器不会重排序指令即可,linux kernel下的读内存屏障smp_rmb和这个实现类似,lfence主要针对奔腾pro cpu使用的,奔腾pro有勘误表某些情况下可能会违反x86的标准内存序,所以使用lfence指令防止load load重排序,虽然都支持lfence指令,但是毕竟lfence指令开销大,所以除了奔腾pro处理器,其它处理器的读内存屏障操作,只需要防止编译器重排序就可以了。

public native void storeFence();方法jvm实现更简单,就一个赋值为0的操作,由于x86 store store不会重排序,所以store内存屏障不需要sfence保护。

最后看public native void fullFence();这个是全屏障,也就是不管读写都要遵守顺序,不能越过屏障执行。

代码语言:javascript复制
//使用了lock前缀做内存屏障,add一个无用的操作,这样的方式比直接使用mfence指令效率高
inline void OrderAccess::fence() {
  if (os::is_MP()) {
    // always use locked addl since mfence is sometimes expensive
#ifdef AMD64
    __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  }
}

使用了lock前缀,保证了cpu对屏障前后的指令不会重排序,这里没使用mfence指令,是因为lock 加一条无意义的指令,要比mfence效率高,由于x86只会发生store load重排序,所以可以使用fullFence阻止这个重排序。

0 人点赞