一个名为不安全的类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中没有的概念了。