Unsafe类

最后更新:2020-04-10

Java和C++语言的一个重要区别就是Java中我们无法直接操作一块内存区域,不能像C++中那样可以自己申请内存和释放内存。Java中的Unsafe类为我们提供了类似C++手动管理内存的能力。 Unsafe类,全限定名是sun.misc.Unsafe,从名字中我们可以看出来这个类对普通程序员来说是“危险”的,一般应用开发者不会用到这个类。

建议先看这个知乎帖子第一楼R大的回答:为什么JUC中大量使用了sun.misc.Unsafe 这个类,但官方却不建议开发者使用

使用Unsafe要注意以下几个问题:

  • 1、Unsafe有可能在未来的Jdk版本移除或者不允许Java应用代码使用,这一点可能导致使用了Unsafe的应用无法运行在高版本的Jdk。
  • 2、Unsafe的不少方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量要自己计算,一旦出现问题就是JVM崩溃级别的异常,会导致整个JVM实例崩溃,表现为应用程序直接crash掉。
  • 3、Unsafe提供的直接内存访问的方法中使用的内存不受JVM管理(无法被GC),需要手动管理,一旦出现疏忽很有可能成为内存泄漏的源头。

暂时总结出以上三点问题。Unsafe在JUC(java.util.concurrent)包中大量使用(主要是CAS),在netty中方便使用直接内存,还有一些高并发的交易系统为了提高CAS的效率也有可能直接使用到Unsafe。总而言之,Unsafe类是一把双刃剑。

https://www.cnblogs.com/throwable/p/9139947.html

查看Unsafe的源码,可以看到Unsafe是一个单例类

public final class Unsafe {
    private static final Unsafe theUnsafe;

    private Unsafe() {
    }

    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }
    
    static {
        ...
        theUnsafe = new Unsafe();
       	...
    }

通过源码我们看出,Unsafe提供静态方法getUnsafe获取Unsafe实例,当且仅当调用getUnsafe方法的类为引导类加载器所加载时才合法,否则抛出SecurityException异常。

如果我们要获取Unsafe实例,可以通过反射获取

Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);

功能介绍

Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类

内存管理

addressSize

  • public native int addressSize();

获取本地指针的大小(单位是byte),通常值为4或者8。常量ADDRESS_SIZE就是调用此方法。

pageSize

  • public native int pageSize();

获取本地内存的页数,此值为2的幂次方。

allocateMemory

  • public native long allocateMemory(long bytes);

分配一块新的本地内存,通过bytes指定内存块的大小(单位是byte),返回新开辟的内存的地址。如果内存块的内容不被初始化,那么它们一般会变成内存垃圾。生成的本机指针永远不会为零,并将对所有值类型进行对齐。可以通过freeMemory方法释放内存块,或者通过reallocateMemory方法调整内存块大小。bytes值为负数或者过大会抛出IllegalArgumentException异常,如果系统拒绝分配内存会抛出OutOfMemoryError异常。

reallocateMemory

  • public native long reallocateMemory(long address, long bytes);

通过指定的内存地址address重新调整本地内存块的大小,调整后的内存块大小通过bytes指定(单位为byte)。可以通过freeMemory方法释放内存块,或者通过reallocateMemory方法调整内存块大小。bytes值为负数或者过大会抛出IllegalArgumentException异常,如果系统拒绝分配内存会抛出OutOfMemoryError异常。

setMemory

  • public native void setMemory(Object o, long offset, long bytes, byte value);

将给定内存块中的所有字节设置为固定值(通常是0)。内存块的地址由对象引用o和偏移地址共同决定,如果对象引用o为null,offset就是绝对地址。第三个参数就是内存块的大小,如果使用allocateMemory进行内存开辟的话,这里的值应该和allocateMemory的参数一致。value就是设置的固定值,一般为0(这里可以参考netty的DirectByteBuffer)。一般而言,o为null,所有有个重载方法是public native void setMemory(long offset, long bytes, byte value);,等效于setMemory(null, long offset, long bytes, byte value);

堆外内存

通常,我们在Java中创建的对象都处于堆内内存(heap)中,堆内内存是由JVM所管控的Java进程内存,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理堆内存。与之相对的是堆外内存,存在于JVM管控之外的内存区域,Java中对堆外内存的操作,依赖于Unsafe提供的操作堆外内存的native方法。

使用堆外内存的原因

  • 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模,从而在GC时减少回收停顿对于应用的影响。
  • 提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。

类、对象和变量相关方法

主要包括类的非常规实例化、基于偏移地址获取或者设置变量的值、基于偏移地址获取或者设置数组元素的值等。

getObject

  • public native Object getObject(Object o, long offset);

通过给定的Java变量获取引用值。这里实际上是获取一个Java对象o中,获取偏移地址为offset的属性的值,此方法可以突破修饰符的抑制,也就是无视private、protected和default修饰符。类似的方法有getInt、getDouble等等。

putObject

  • public native void putObject(Object o, long offset, Object x);

将引用值存储到给定的Java变量中。这里实际上是设置一个Java对象o中偏移地址为offset的属性的值为x,此方法可以突破修饰符的抑制,也就是无视private、protected和default修饰符。类似的方法有putInt、putDouble等等。

getObjectVolatile

  • public native Object getObjectVolatile(Object o, long offset);

此方法和上面的getObject功能类似,不过附加了’volatile’加载语义,也就是强制从主存中获取属性值。类似的方法有getIntVolatile、getDoubleVolatile等等。这个方法要求被使用的属性被volatile修饰,否则功能和getObject方法相同。

putObjectVolatile

  • public native void putObjectVolatile(Object o, long offset, Object x);

此方法和上面的putObject功能类似,不过附加了’volatile’加载语义,也就是设置值的时候强制(JMM会保证获得锁到释放锁之间所有对象的状态更新都会在锁被释放之后)更新到主存,从而保证这些变更对其他线程是可见的。类似的方法有putIntVolatile、putDoubleVolatile等等。这个方法要求被使用的属性被volatile修饰,否则功能和putObject方法相同。

putOrderedObject

  • public native void putOrderedObject(Object o, long offset, Object x);

设置o对象中offset偏移地址offset对应的Object型field的值为指定值x。这是一个有序或者有延迟的putObjectVolatile方法,并且不保证值的改变被其他线程立即看到。只有在field被volatile修饰并且期望被修改的时候使用才会生效。类似的方法有putOrderedIntputOrderedLong

staticFieldOffset

  • public native long staticFieldOffset(Field f);

返回给定的静态属性在它的类的存储分配中的位置(偏移地址)。不要在这个偏移量上执行任何类型的算术运算,它只是一个被传递给不安全的堆内存访问器的cookie。注意:这个方法仅仅针对静态属性,使用在非静态属性上会抛异常。下面源码中的方法注释估计有误,staticFieldOffset和objectFieldOffset的注释估计是对调了,为什么会出现这个问题无法考究。

objectFieldOffset

  • public native long objectFieldOffset(Field f);

返回给定的非静态属性在它的类的存储分配中的位置(偏移地址)。不要在这个偏移量上执行任何类型的算术运算,它只是一个被传递给不安全的堆内存访问器的cookie。注意:这个方法仅仅针对非静态属性,使用在静态属性上会抛异常。

staticFieldBase

  • public native Object staticFieldBase(Field f);

返回给定的静态属性的位置,配合staticFieldOffset方法使用。实际上,这个方法返回值就是静态属性所在的Class对象的一个内存快照。注释中说到,此方法返回的Object有可能为null,它只是一个’cookie’而不是真实的对象,不要直接使用的它的实例中的获取属性和设置属性的方法,它的作用只是方便调用上面提到的像getInt(Object,long)等等的任意方法。

shouldBeInitialized

  • public native boolean shouldBeInitialized(Class<?> c);

检测给定的类是否需要初始化。通常需要使用在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)。 此方法当且仅当ensureClassInitialized方法不生效的时候才返回false。

ensureClassInitialized

  • public native void ensureClassInitialized(Class<?> c);

检测给定的类是否已经初始化。通常需要使用在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)。

arrayBaseOffset

  • public native int arrayBaseOffset(Class<?> arrayClass);

返回数组类型的第一个元素的偏移地址(基础偏移地址)。如果arrayIndexScale方法返回的比例因子不为0,你可以通过结合基础偏移地址和比例因子访问数组的所有元素。Unsafe中已经初始化了很多类似的常量如ARRAY_BOOLEAN_BASE_OFFSET等。

arrayIndexScale

  • public native int arrayIndexScale(Class<?> arrayClass);

返回数组类型的比例因子(其实就是数据中元素偏移地址的增量,因为数组中的元素的地址是连续的)。此方法不适用于数组类型为”narrow”类型的数组,”narrow”类型的数组类型使用此方法会返回0(这里narrow应该是狭义的意思,但是具体指哪些类型暂时不明确,笔者查了很多资料也没找到结果)。Unsafe中已经初始化了很多类似的常量如ARRAY_BOOLEAN_INDEX_SCALE等。

defineClass

  • public native Class<?> defineClass(String name, byte[] b, int off, int len,ClassLoader loader,ProtectionDomain protectionDomain);

告诉JVM定义一个类,返回类实例,此方法会跳过JVM的所有安全检查。默认情况下,ClassLoader(类加载器)和ProtectionDomain(保护域)实例应该来源于调用者。

defineAnonymousClass

  • public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);

这个方法的使用可以看R大的知乎回答:JVM crashes at libjvm.so,下面截取一点内容解释此方法。

  • 1、VM Anonymous Class可以看作一种模板机制,如果程序要动态生成很多结构相同、只是若干变量不同的类的话,可以先创建出一个包含占位符常量的正常类作为模板,然后利用sun.misc.Unsafe#defineAnonymousClass()方法,传入该类(host class,宿主类或者模板类)以及一个作为”constant pool path”的数组来替换指定的常量为任意值,结果得到的就是一个替换了常量的VM Anonymous Class。
  • 2、VM Anonymous Class从VM的角度看是真正的”没有名字”的,在构造出来之后只能通过Unsafe#defineAnonymousClass()返回出来一个Class实例来进行反射操作。

还有其他几点看以自行阅读。这个方法虽然翻译为”定义匿名类”,但是它所定义的类和实际的匿名类有点不相同,因此一般情况下我们不会用到此方法。在Jdk中lambda表达式相关的东西用到它,可以看InnerClassLambdaMetafactory这个类。

allocateInstance

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

通过Class对象创建一个类的实例,不需要调用其构造函数、初始化代码、JVM安全检查等等。同时,它抑制修饰符检测,也就是即使构造器是private修饰的也能通过此方法实例化。

内存屏障

内存屏障相关的方法是在Jdk8添加的。内存屏障(也称内存栅栏,内存栅障,屏障指令等)是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作,避免代码重排序。

在Java 8中引入了一种锁的新机制——StampedLock就是通过内存屏障实现的

loadFence

  • public native void loadFence();

在该方法之前的所有读操作,一定在load屏障之前执行完成。

storeFence

  • public native void storeFence();

在该方法之前的所有写操作,一定在store屏障之前执行完成

fullFence

  • public native void fullFence();

在该方法之前的所有读写操作,一定在full屏障之前执行完成,这个内存屏障相当于上面两个(load屏障和store屏障)的合体功能。

线程的挂起和恢复

unpark

  • public native void unpark(Object thread);

释放被park创建的在一个线程上的阻塞。这个方法也可以被使用来终止一个先前调用park导致的阻塞。这个操作是不安全的,因此必须保证线程是存活的(thread has not been destroyed)。从Java代码中判断一个线程是否存活的是显而易见的,但是从native代码中这机会是不可能自动完成的。

park

  • public native void park(boolean isAbsolute, long time);

阻塞当前线程直到一个unpark方法出现(被调用)、一个用于unpark方法已经出现过(在此park方法调用之前已经调用过)、线程被中断或者time时间到期(也就是阻塞超时)。在time非零的情况下,如果isAbsolute为true,time是相对于新纪元之后的毫秒,否则time表示纳秒。这个方法执行时也可能不合理地返回(没有具体原因)。并发包java.util.concurrent中的框架对线程的挂起操作被封装在LockSupport类中,LockSupport类中有各种版本pack方法,但最终都调用了Unsafe#park()方法。

多线程同步

主要包括监视器锁定、解锁以及CAS相关的方法。

monitorEnter

  • public native void monitorEnter(Object o);

锁定对象,必须通过monitorExit方法才能解锁。此方法经过实验是可以重入的,也就是可以多次调用,然后通过多次调用monitorExit进行解锁。

monitorExit

  • public native void monitorExit(Object o);

解锁对象,前提是对象必须已经调用monitorEnter进行加锁,否则抛出IllegalMonitorStateException异常。

tryMonitorEnter

  • public native boolean tryMonitorEnter(Object o);

尝试锁定对象,如果加锁成功返回true,否则返回false。必须通过monitorExit方法才能解锁。

CAS

compareAndSwapObject

  • public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);

针对Object对象进行CAS操作。即是对应Java变量引用o,原子性地更新o中偏移地址为offset的属性的值为x,当且仅的偏移地址为offset的属性的当前值为expected才会更新成功返回true,否则返回false。

  • o:目标Java变量引用。
  • offset:目标Java变量中的目标属性的偏移地址。
  • expected:目标Java变量中的目标属性的期望的当前值。
  • x:目标Java变量中的目标属性的目标更新值。

类似的方法有compareAndSwapIntcompareAndSwapLong,在Jdk8中基于CAS扩展出来的方法有getAndAddIntgetAndAddLonggetAndSetIntgetAndSetLonggetAndSetObject,它们的作用都是:通过CAS设置新的值,返回旧的值。

CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。

系统相关

这部分包含两个获取系统相关信息的方法。

//返回系统指针的大小。返回值为4(32位系统)或8(64位系统)。
public native int addressSize();  
//内存页的大小,此值为2的幂次方。
public native int pageSize();

其他

getLoadAverage

  • public native int getLoadAverage(double[] loadavg, int nelems);

获取系统的平均负载值,loadavg这个double数组将会存放负载值的结果,nelems决定样本数量,nelems只能取值为1到3,分别代表最近1、5、15分钟内系统的平均负载。如果无法获取系统的负载,此方法返回-1,否则返回获取到的样本数量(loadavg中有效的元素个数)。实验中这个方法一直返回-1,其实完全可以使用JMX中的相关方法替代此方法。

throwException

  • public native void throwException(Throwable ee);

绕过检测机制直接抛出异常

PS: 对于各个方法的典型应用,可以参考下面美团的技术文章,有详细介绍

参考资料

https://mp.weixin.qq.com/s/h3MB8p0sEA7VnrMXFq9NBA

https://www.cnblogs.com/throwable/p/9139947.html

Edgar

Edgar
一个略懂Java的小菜比