一个名为不安全的类Unsafe

最近为了更加深入了解NIO的实现原理,学习NIO的源码时,遇到了一个问题。即在WindowsSelectorImpl中的

pollWrapper属性,当我点进去查看它的PollArrayWrapper类型时,发现它和AllocatedNativeObject类型有关,而AllocatedNativeObject继承了NativeObject类,随着又发现了NativeObject是基于一个Unsafe类实现的。不安全的类????

 

Unsafe

Unsafe,顾名思义,它真是一个不安全的类,那它为什么是不安全的呢?这就要从Unsafe类的功能说起。

学过C#的就可以知道,C#和Java的一个重要区别就是:C#可以直接操作一块内存区域,如自己申请内存和释放,而在Java中这是做不到的。而Unsafe类就可以让我们在Java中像C#一样去直接操作一块内存区域,正因为Unsafe类可以直接操作内存,意味着其速度更快,在高并发的条件之下能够很好地提高效率,所以java中很多并发框架,如Netty,都使用了Unsafe类。

虽然,Unsafe可以提高运行速度,但是因为Java本身是不支持自己直接操作内存的,这就意味着Unsafe类所做的操作不受jvm管理的,所以不会被GC(垃圾回收),需要我们手动GC,稍有不慎就会出现内存泄漏问题。且Unsafe的不少方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量要自己计算,一旦出现问题就是JVM崩溃级别的异常,会导致整个JVM实例崩溃。这就是为什么Unsafe被称为不安全的原因。Unsafe可以让你全力踩油门,提高自己的速度,但是它会让你的方向盘更难握稳,一不小心就可能导致车毁人亡。

源码查看

初始化

因为Unsafe的构造方法是private类型的,所以无法通过new方式实例化获取,只能通过它的getUnsafe()方法获取。又因为Unsafe是直接操作内存的,为了安全起见,Java的开发人员为Unsafe的获取设置了限制,所以想要获取它只能通过Java的反射机制来获取。

@CallerSensitive
public static Unsafe getUnsafe() {
    //通过getCallerClass方法获取Unsafe类
    Class var0 = Reflection.getCallerClass();
    //如过该var0类不是启动类加载器(Bootstrap),则抛出异常
    //正因为该判断,所以Unsafe只能通过反射获取
    if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}
  • Reflection.getCallerClass():可以返回调用类或Reflection类,或者层层上传

  • VM.isSystemDomainLoader(ClassLoader var0):判断该类加载器是否是启动类加载器(Bootstrap)。

  • @CallerSensitive:为了防止黑客通过双重反射来提升权限,所以所有跟反射相关的接口方法都标注上CallerSensitive

所以使用下面的方式是获取不了Unsafe类的:

//使用这样的方式获取会抛出异常,因为是通过系统类加载器加载(AppClassLoader)
public class Test {
    public static void main(String[] args) {
        Unsafe unsafe = Unsafe.getUnsafe();
    }
}

那怎么才用使用启动类加载Unsafe类并获取它呢?在Unsafe类的最下面的static代码块中有这样一段代码:

private static final Unsafe theUnsafe;
//.....
static {
    registerNatives();
    Reflection.registerMethodsToFilter(Unsafe.class, new String[]{"getUnsafe"});
    theUnsafe = new Unsafe();
    //......
}

学过反射机制看过以上代码就可以知道我们可以通过getDeclaredField()返回获取Un safe类的theUnsafe属性,然后通过该属性获取Unsafe类的实例,因为在Unsafe类里的theUnsafe属性已经被new实例化了。

public class Test {
    public static void main(String[] args) throws Exception {
        //通过getDeclaredField方法获取Unsafe类中名为theUnsafe的属性
        //注意,该属性是private类型的,所以不能用getField获取,只能用getDeclaredField
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        //将该属性设为可访问
        field.setAccessible(true);
        //实例该属性并转为Unsafe类型
        //因为theUnsafe属性是Unsafe类所在的包的启动类加载的,所以可以成功获得
        Unsafe unsafe = (Unsafe)field.get(null);
    }
}

 

获取偏移量方法

偏移量

在实际模式中,内存是被分成段的,如果想要获取内存中的某个储存单元,需知道储存单元的所在段地址(段头)和偏移量,即使你知道该储存单元的实际地址。而偏移量就是实际地址与所在段地址(段头)的距离,偏移量=实际地址-所在段地址(段头)

举个例子,假设有个书架,我需要找由左到右、由上到下数的第1024本书,那我只能一本本的数,直到数到第1024本,但如果我知道书架的第4层的第一本书是第1000本书,那我只用从第1000本书开始数,数到1024,只需数1024-1000=24本。在这里,书架是内存,要找的书就是储存单元,书架的第4层就是内存段,第4层的第一本书即书架的第1000本书就是段地址(段头),第1024本书就是实际地址,而偏移量的就是第1000本书到第1024本书的距离24.

public native long objectFieldOffset(Field var1);

获取非静态变量var1的偏移量。

public native long staticFieldOffset(Field var1);

获取静态变量var1的偏移量。

public native Object staticFieldBase(Field var1);

获取静态变量var1的实际地址,配合staticFieldOffset方法使用,可求出变量所在的段地址

public native int arrayBaseOffset(Class<?> var1);

获取数组var1中的第一个元素的偏移量,即数组的基础地址。

在内存中,数组的存储是以一定的偏移量增量连续储存的,如数组的第一个元素的实际地址为24,偏移量为4,而数组的偏移量增量为1,那数组的第二个元素的实际地址就是25,偏移量为5.

public native int arrayIndexScale(Class<?> var1);

获取数组var1的偏移量增量。结合arrayBaseOffset(Class<?> var1)方法就可以求出数组中各个元素的地址。

 

操作属性方法

public native Object getObject(Object var1, long var2);

获取var1对象中偏移量为var2的Object对象,该方法可以无视修饰符限制。相同方法有getInt、getLong、getBoolean等。

public native void putObject(Object var1, long var2, Object var4);

将var1对象中偏移量为var2的Object对象的值设为var4,该方法可以无视修饰符限制。相同的方法有putInt、putLong、putBoolean等。

public native Object getObjectVolatile(Object var1, long var2);

功能与getObject(Object var1, long var2)一样,但该方法可以保证读写的可见性和有序性,可以无视修饰符限制。相同的方法有getIntVolatile、getLongVolatile、getBooleanVolatile等。

public native void putObjectVolatile(Object var1, long var2, Object var4);

功能与putObject(Object var1, long var2, Object var4)一样,但该方法可以保证读写的可见性和有序性,可以无视修饰符限制。相同的方法有putIntVolatile、putLongVolatile、putBooleanVolatile等。

public native void putOrderedObject(Object var1, long var2, Object var4);

功能与putObject(Object var1, long var2, Object var4)一样,但该方法可以保证读写的有序性(不保证可见性),可以无视修饰符限制。相同的方法有putOrderedInt、putOrderedLong等。

 

操作内存方法

public native int addressSize();

获取本地指针大小,单位为byte,通常值为4或8。

public native int pageSize();

获取本地内存的页数,该返回值会是2的幂次方。

public native long allocateMemory(long var1);

开辟一块新的内存块,大小为var1(单位为byte),返回新开辟的内存块地址。

public native long reallocateMemory(long var1, long var3);

将内存地址为var3的内存块大小调整为var1(单位为byte),返回调整后新的内存块地址。

public native void setMemory(long var2, long var4, byte var6);

从实际地址var2开始将后面的字节都修改为var6,修改大小为var4(通常为0)。

public native void copyMemory(Object var1, long var2, Object var4, long var5, long var7);

从对象var1中偏移量为var2的地址开始复制,复制到var4中偏移量为var5的地址,复制大小为var7。

当var1为空时,var2就不是偏移量而是实际地址,当var4为空时,var5就不是偏移量而是实际地址。

public native void freeMemory(long var1);

释放实际地址为var1的内存。

 

线程挂起和恢复方法

public native void unpark(Object var1);

将被挂起的线程var1恢复,由于其不安全性,需保证线程var1是存活的。

public native void park(boolean var1, long var2);

当var2等于0时,线程会一直挂起,知道调用unpark方法才能恢复。

当var2大于0时,如果var1为false,这时var2为增量时间,即线程在被挂起var2秒后会自动恢复,如果var1为true,这时var2为绝对时间,即线程被挂起后,得到具体的时间var2后才自动恢复。

 

CAS方法

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

CAS机制相关操作,对对象var1里偏移量为var2的变量进行CAS修改,var4为期待值,var5为修改值,返回修改结果。相同方法有compareAndSwapInt、compareAndSwapLong。

 

类加载方法

public native boolean shouldBeInitialized(Class<?> var1);

判断var1类是否被初始。

public native void ensureClassInitialized(Class<?> var1);

确保var1类已经被初始化。

public native Class<?> defineClass(String var1, byte[] var2, int var3, int var4, ClassLoader var5, ProtectionDomain var6);

定义一个类,用于动态的创建类。var1为类名,var2为类的文件数据字节数组,var3为var2的输入起点,var4为输入长度,var5为加载该类的加载器,var6为保护领域。返回创建后的类。

public native Class<?> defineAnonymousClass(Class<?> var1, byte[] var2, Object[] var3);

用于动态的创建匿名内部类。var1为需创建匿名内部类的类,var2为匿名内部类的文件数据字节数组,var3为修补对象。返回创建后的匿名内部类。

public native Object allocateInstance(Class<?> var1) throws InstantiationException;

创建var1类的实例,但是不会调用var1类的构造方法,如果var1类还没有初始化,则进行初始化。返回创建实例对象。

 

内存屏障方法

public native void loadFence();

所有读操作必须在loadFence方法执行前执行完毕。

public native void storeFence();

所有写操作必须在storeFence方法执行前执行完毕。

public native void fullFence();

所有读写操作必须在fullFence方法执行前执行完毕。

 

疑惑

看到这里可能有人会有一个疑惑,为什么这些方法都没有具体的功能实现代码呢?

在文章开头时就说过,Java不支持直接操作内存,那怎么可能用Java来具体实现功能呢。你可以发现Unsafe类内的大多方法都有native修饰符,native接口可以让你调用本地的代码文件(包括其他语言,如c语言),既然Java实现不了,那就让能实现的人来做,所以Unsafe的底层实现语言其实是C语言,这也是为什么Unsafe类内会有偏移量和指针这些Java中没有的概念了。

 

(以上为本人自己对Unsafe类的理解,如果有错误,欢迎各位前辈指出)

 

Tags: