神奇的魔法类和双刃剑-Unsafe

神奇的魔法类和双刃剑-Unsafe

前提

JDK9或者以后,sun.misc包的源码也可以上传到JDK类库中,可以直接导入IDE进行注释的阅读,这一点是比较好的改进。本文基于JDK11的源码阅读Unsafe类的注释,介绍一下这个类的使用方式。

Unsafe简介

在JDK9之后,sun.misc.Unsafe被移动到jdk.unsupported模块中,同时在java.base模块克隆了一个jdk.internal.misc.Unsafe类,代替了JDK8以前的sun.misc.Unsafe的功能,jdk.internal包不开放给开发者调用。

Unsafe是用于在实质上扩展Java语言表达能力、便于在更高层(Java层)代码里实现原本要在更低层(C层)实现的核心库功能用的。这些功能包括裸内存的申请/释放/访问,低层硬件的atomic/volatile支持,创建未初始化对象等。它原本的设计就只应该被标准库使用。

为了让开发者有机会过渡到尽量不使用sun.misc.Unsafe,默认不允许Java应用代码访问sun.misc.Unsafe类,同时在java.base模块克隆了一个不能被外部访问的jdk.internal.misc.Unsafe类用于JDK内部API演进。

获取Unsafe实例

sun.misc.Unsafe提供了一个静态方法来获取其实例:

@CallerSensitive
public static Unsafe getUnsafe() {
Class<?> caller = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(caller.getClassLoader()))
throw new SecurityException("Unsafe");
return theUnsafe;
}
// VM类中的代码
public static boolean isSystemDomainLoader(ClassLoader loader) {
return loader == null || loader == ClassLoader.getPlatformClassLoader();
}

这个静态方法getUnsafe()必须在当前调用的类对应的类加载器为null(类加载器为null也就是当前调用的类必须使用Bootstrap类加载器加载)或者为PlatformClassLoader才不会抛出SecurityException异常。由于对PlatformClassLoader理解不深入,所以我们可以用VM参数-Xbootclasspath:让当前的调用类被Bootstrap类加载器加载。

j-unsafe-1

但是试验了一下(其实文档中已经提到此参数已经失效,不过这里还是试了下),发现这个参数在JDK9之后已经不支持,使用的时候会导致JVM启动失败,异常信息是:

Error: Could not create the Java Virtual Machine.
-Xbootclasspath is no longer a supported option.
Error: A fatal exception has occurred. Program will exit.

此特性暂时在JDK9以后找不到替代的VM参数,所以这里只能选择其他可行方法。查看sun.misc.Unsafe所在模块的信息:

module jdk.unsupported {
exports com.sun.nio.file;
exports sun.misc;
exports sun.reflect;

opens sun.misc;
opens sun.reflect;
}

由于sun.misc是opens修饰的,可以使用反射直接调用。因此可以像下面这样获取sun.misc.Unsafe对象:

public class UnsafeMain {

public static void main(String[] args) throws Exception {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
}
}

其实还有特殊的技巧可以直接暴露jdk.internal.misc.Unsafe所在的模块让它可以直接使用反射调用,只需要使用参数addExports:java.base/jdk.internal.misc=ALL-UNAMED,这样子就可以反射获取jdk.internal.misc.Unsafe的实例,不过推荐这种做法,毕竟jdk.internal.misc.Unsafe中提供更多底层的方法,能力越大越容易失去控制。

Unsafe的使用建议

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

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

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

Unsafe的核心方法

sun.misc.Unsafe一共提供了89个public修饰的方法,下面针对核心方法按功能分组简单介绍一下。

类操作

类操作相关主要和类实例化、属性地址获取等等操作,原来存在一个defineClass方法,已经被移除,但是该方法在jdk.internal.misc.Unsafe中依然存在。

ensureClassInitialized

  • public boolean shouldBeInitialized(Class<?> c)

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

shouldBeInitialized

  • public boolean shouldBeInitialized(Class<?> c)

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

defineAnonymousClass

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

    • hostClass:宿主类。
    • data:字节码字节数组。
    • cpPatches:替换常量池(Constant Pool)中的字面量得到的引用数组。

这个方法的使用可以看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)

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

staticFieldBase

  • public Object staticFieldBase(Field f)

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

staticFieldOffset

  • public long staticFieldOffset(Field f)

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

objectFieldOffset

  • public long staticFieldOffset(Field f)

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

defineClass

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

这个方法位于jdk.internal.misc.Unsafe,也就是开发者无法直接使用。作用是:绕过安全检查告知JVM定义一个类。默认情况下,ClassLoader(类加载器)和ProtectionDomain(保护域)实例应该来源于调用者。

基于内存地址直接存取属性

前一节中提供了一些方法可以直接获取静态或者非静态成员属性的内存地址,这一节介绍的API可以基于成员属性内存地址直接获取或者设置其值。

getObject

  • public Object getObject(Object o, long offset)

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

putObject

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

    • o:当前操作的对象。
    • offset:成员属性的内存地址。
    • x:需要设置的目标属性值。

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

getObjectVolatile

  • public Object getObjectVolatile(Object o, long offset)

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

putObjectVolatile

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

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

putOrderedObject

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

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

数组操作

arrayBaseOffset

  • public int arrayBaseOffset(Class<?> arrayClass)

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

arrayIndexScale

  • public int arrayIndexScale(Class<?> arrayClass)

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

低级同步原语

低级同步原语的相关方法在JDK8还能通过sun.misc.Unsafe使用,在JDK9以后sun.misc.Unsafejdk.internal.misc.Unsafe都移除了相关的方法。低级同步原语主要包括监视器锁定、解锁方法。

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方法才能解锁。

线程挂起与恢复

JDK1.5引入了并发包java.util.concurrent中组件控制线程挂起和恢复就是依赖于java.util.concurrent.locks.LockSupport完成,而LockSupport底层依赖于sun.misc.Unsafe中下面提到线程的挂起和恢复方法完成的。相关方法主要是用于替代线程类Thread中过时并且不安全的suspendresume方法。

park

  • public void park(boolean isAbsolute, long time)

    • time:时间长度,单位由isAbsolute控制,0表示永久阻塞。
    • isAbsolute:如果isAbsolute为true,time是相对于新纪元之后的毫秒,否则time表示纳秒。

注释:阻塞当前线程直到一个unpark方法出现(被调用)、一个用于unpark方法已经出现过(在此park方法调用之前已经调用过)、线程被中断或者time时间到期(也就是阻塞超时)。在time非零的情况下,如果isAbsolute为true,time是相对于新纪元之后的毫秒,否则time表示纳秒。这个方法执行时也可能不合理地返回(没有具体原因)。

unpark

  • public void unpark(Object thread)

释放被park创建的在一个线程上的阻塞。这个方法也可以被使用来终止一个先前调用park导致的阻塞。这个操作是不安全的,因此必须保证线程是存活的(thread has not been destroyed)。从Java代码中判断一个线程是否存活的是显而易见的,所以解除阻塞的时候需要对线程的存活性做判断。

重点注意:

  • unpark方法调用多次,实际上只有一次会生效,可以简单理解为它是一个只有0和1两个值的计数器,调用unpark多次,计数仍然为1。
  • park方法总是针对当前线程,如果预先已经调用过一次unpark方法后再调用park方法,那么将不会进入阻塞状态直接释放

CAS操作

CAS,也就是Compare And Swap,也就是在一个原子操作中完成比较和交互。

compareAndSwapObject

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

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

针对Object对象进行CAS操作。即是对应Java变量引用o,原子性地更新o中偏移地址为offset的属性的值为x,当且仅的偏移地址为offset的属性的当前值为expected才会更新成功返回true,否则返回false。类似的方法有compareAndSwapIntcompareAndSwapLong,在Jdk8中基于CAS扩展出来的相关方法有getAndAddIntgetAndAddLonggetAndSetIntgetAndSetLonggetAndSetObject,它们的作用都是:通过CAS设置新的值,返回旧的值。

getAndSetObject

  • public final Object getAndSetObject(Object o, long offset, Object newValue)

compareAndSwapObject中的描述。

内存管理

addressSize

  • public int addressSize();

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

pageSize

  • public int pageSize();

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

allocateMemory

  • public long allocateMemory(long bytes);

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

reallocateMemory

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

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

setMemory

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

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

copyMemory

  • public void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes)

拷贝给定内存地址的字节长度对应的字节到指定内存地址中。如果srcBase或者destBase为null,则srcOffset或者destOffset分别指代绝对地址。

内存屏障

内存屏障相关的方法是在Jdk8添加的。内存屏障相关的知识可以先自行查阅,笔者目前也没有深入了解相关知识。

loadFence

  • public void loadFence();

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

storeFence

  • public void storeFence();

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

fullFence

  • public void fullFence();

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

其它

invokeCleaner

  • public void invokeCleaner(java.nio.ByteBuffer directBuffer)

清空使用了堆外内存的ByteBuffer实例占据的内存,一般是DirectBuffer的子类。

throwException

  • public void throwException(Throwable ee)

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

getLoadAverage

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

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

使用例子

先封装一下获取Unsafe实例的方法:

private static Unsafe getUnsafe() throws Exception {
Field f = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
return (Unsafe) f.get(null);
}

通过内存地址直接操作属性

通过staticFieldOffsetobjectFieldOffset可以获取静态和非静态成员属性的偏移地址,然后直接进行存取值操作。

public static class Simple {

static Integer STATIC_INT = 10086;

Long longField = 1024L;
}

public static void main(String[] args) throws Exception {
Unsafe unsafe = getUnsafe();
Field staticInt = Simple.class.getDeclaredField("STATIC_INT");
staticInt.setAccessible(true);
Object staticFieldBase = unsafe.staticFieldBase(staticInt);
long staticFieldOffset = unsafe.staticFieldOffset(staticInt);
// 注意这里一定要getObject,getInt是针对原始类型int,包装类型要自己强转
System.out.println("Sample初始化前,STATIC_INT = " + unsafe.getObject(staticFieldBase, staticFieldOffset));
Simple simple = new Simple();
System.out.println("Sample初始化后,STATIC_INT = " + unsafe.getObject(staticFieldBase, staticFieldOffset));

Field longField = Simple.class.getDeclaredField("longField");
longField.setAccessible(true);
long objectFieldOffset = unsafe.objectFieldOffset(longField);
System.out.println("Sample初始化后,longField = " + unsafe.getObject(simple, objectFieldOffset));
unsafe.putObject(simple,objectFieldOffset, 4201L);
System.out.println("Sample属性被覆盖后,longField = " + simple.longField);
}


// 输出如下:
Sample初始化前,STATIC_INT = null
Sample初始化后,STATIC_INT = 10086
Sample初始化后,longField = 1024
Sample属性被覆盖后,longField = 4201

线程挂起和恢复

主要介绍一下parkunpark的用法:

	public static void main(String[] args) throws Exception {
Unsafe unsafe = getUnsafe();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程park!");
unsafe.park(false, 0);
System.out.println("线程恢复运行!");
}
});
thread.start();
TimeUnit.SECONDS.sleep(2);
System.out.println("主线程unpark阻塞着的线程!");
unsafe.unpark(thread);
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
}

// 运行后输出:
线程park!
主线程unpark阻塞着的线程!
线程恢复运行!

###

小结

存在即合理,虽然不推荐使用Unsafe,但是如果有需要的还是要挥动这把双刃剑。

参考资料:

(本文完 e-a-20181213 c-3-d)

文章作者: throwable
文章链接: http://www.throwable.club/2018/12/13/java-magic-unsafe/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Throwable
❤支付宝打赏❤
❤微信打赏❤