前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >jdk1.8 Unsafe类初探

jdk1.8 Unsafe类初探

作者头像
用户4415180
发布2022-06-23 14:30:43
6430
发布2022-06-23 14:30:43
举报
文章被收录于专栏:高并发

    在看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阻止这个重排序。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018-10-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档