Java中Unsafe类的原理详解与使用案例

Unsafe类位于rt.jar包,Unsafe类提供了硬件级别的原子操作,类中的方法都是native方法,它们使用JNI的方式访问本地C++实现库。由此提供了一些绕开JVM的更底层功能,可以提高程序效率。

JNI:Java Native Interface。使得Java 与 本地其他类型语言(如C、C++)直接交互。

Unsafe 是用于扩展 Java 语言表达能力、便于在更高层(Java 层)代码里实现原本要在更低层(C 层)实现的核心库功能用的。这些功能包括直接内存的申请/释放/访问,低层硬件的 atomic/volatile 支持,创建未初始化对象,通过偏移量操作对象字段、方法、实现线程无锁挂起和恢复等功能。

所谓Java对象的“布局”就是在内存里Java对象的各个部分放在哪里,包括对象的实例字段和一些元数据之类。Unsafe里关于对象字段访问的方法把对象布局抽象出来,它提供了objectFieldOffset()方法用于获取某个字段相对Java对象的“起始地址”的偏移量,也提供了getInt、getLong、getObject之类的方法可以使用前面获取的偏移量来访问某个Java对象的某个字段。

Unsafe作用可以大致归纳为:

  • 内存管理,包括分配内存、释放内存等。

  • 非常规的对象实例化。

  • 操作类、对象、变量。

  • 自定义超大数组操作。

  • 多线程同步。包括锁机制、CAS操作等。

  • 线程挂起与恢复。

  • 内存屏障。

2 API详解


Unsafe中一共有82个public native修饰的方法,还有几十个基于这82个public native方法的其他方法,一共有114个方法。

2.1 初始化方法

我们可以直接在源码里面看到,Unsafe是单例模式的类:

private static final Unsafe theUnsafe;

//构造器私有

private Unsafe() {

}

//静态块初始化

static {

Reflection.registerMethodsToFilter(Unsafe.class, new String[]{“getUnsafe”});

theUnsafe = new Unsafe();

}

//静态方法获取实例

@CallerSensitive

public static Unsafe getUnsafe() {

Class var0 = Reflection.getCallerClass();

if (!VM.isSystemDomainLoader(var0.getClassLoader())) {

throw new SecurityException(“Unsafe”);

} else {

return theUnsafe;

}

}

从上面的代码知道,好像是可以通过getUnsafe()方法获取实例,但是如果我们调用该方法会得到一个异常:

java.lang.SecurityException: Unsafe

at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)

//……………………

实际上我们可以看到getUnsafe()方法上有个 @CallerSensitive 注解,就是因为这个注解,在执行时候需要做权限判断:只有由主类加载器(BootStrap classLoader

因此最简单的使用方式是基于反射获取Unsafe实例,代码如下:

Field f = Unsafe.class.getDeclaredField(“theUnsafe”);

f.setAccessible(true);

Unsafe unsafe = (Unsafe) f.get(null);

2.2 类、对象和变量相关方法

主要包括基于偏移地址获取或者设置变量的值、基于偏移地址获取或者设置数组元素的值、class初始化以及对象非常规的创建等。

2.2.1 对象操作

/对象操作/

/获取对象字段的值/

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

// 类似的方法有getInt、getDouble等等。

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

//此方法和上面的getObject功能类似,不过附加了’volatile’加载语义,也就是强制从主存中获取属性值。类似的方法有getIntVolatile、getDoubleVolatile等等。

// 这个方法要求被使用的属性被volatile修饰,否则功能和getObject方法相同。

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

/修改对象字段的值/

//设置Java对象o中偏移地址为offset的属性的值为x,此方法可以突破修饰符的抑制,也就是无视private、protected和default修饰符。用于修改修改非基本数据类型的值。

//类似的方法有putInt、putDouble等等,用于修改基本数据类型的值,再次不再赘述。

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

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

// 类似的方法有putIntVolatile、putDoubleVolatile等等。这个方法要求被使用的属性被volatile修饰,否则功能和putObject方法相同。

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

//设置o对象中offset偏移地址offset对应的Object型field的值为指定值x。这是一个有序或者有延迟的putObjectVolatile方法,并且不保证值的改变被其他线程立即看到。

// 只有在field被volatile修饰并且期望被修改的时候使用才会生效。类似的方法有putOrderedInt和putOrderedLong。

// 最终会设置成x,但是可能导致其他线程在之后的一小段时间内还是可以读到旧的值。关于该方法的更多信息可以参考并发编程网翻译的一篇文章《AtomicLong.lazySet是如何工作的?》,文章地址是“http://ifeve.com/how-does-atomiclong-lazyset-work/”。

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

/获取对象的字段相对该对象地址的偏移量/

//返回给定的静态属性在它的类的存储分配中的位置(偏移地址)。即相对于 className.class 的偏移量,通过这个偏移量可以快速定位字段.

// 注意:这个方法仅仅针对静态属性,使用在非静态属性上会抛异常。

public native long staticFieldOffset(Field f);

//返回给定的非静态属性在它的类的存储分配中的位置(偏移地址)。即字段到对象头的偏移量,通过这个偏移量可以快速定位字段.

// 注意:这个方法仅仅针对非静态属性,使用在静态属性上会抛异常。

public native long objectFieldOffset(Field f);

//返回给定的静态属性的位置,配合staticFieldOffset方法使用。实际上,这个方法返回值就是静态属性所在的Class对象的一个内存快照

// 注释中说到,此方法返回的Object有可能为null,它只是一个’cookie’而不是真实的对象,不要直接使用的它的实例中的获取属性和设置属性的方法,它的作用只是方便调用上面提到的像getInt(Object,long)等等的任意方法。

public native Object staticFieldBase(Field f);

/创建对象/

//绕过构造方法、初始化代码来非常规的创建对象

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

2.2.2 class 相关

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

//此方法当且仅当ensureClassInitialized方法不生效的时候才返回false。

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

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

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

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

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

///定义一个匿名类,与Java8的lambda表达式相关,会用到该方法实现相应的函数式接口的匿名类,可以看结尾文章链接。

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

2.2.3 数组元素相关

//返回数组类型的第一个元素的偏移地址(基础偏移地址)。如果arrayIndexScale方法返回的比例因子不为0,你可以通过结合基础偏移地址和比例因子访问数组的所有元素。

// Unsafe中已经初始化了很多类似的常量如ARRAY_BOOLEAN_BASE_OFFSET等。

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

//返回数组单个元素的大小,数组中的元素的地址是连续的。

// Unsafe中已经初始化了很多类似的常量如ARRAY_BOOLEAN_INDEX_SCALE等。

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

2.3 内存管理

该部分包括了allocateMemory(分配内存)、reallocateMemory(重新分配内存)、copyMemory(拷贝内存)、freeMemory(释放内存 )、getAddress(获取内存地址)、addressSize、pageSize、getInt(获取内存地址指向的整数)、getIntVolatile(获取内存地址指向的整数,并支持volatile语义)、putInt(将整数写入指定内存地址)、putIntVolatile(将整数写入指定内存地址,并支持volatile语义)、putOrderedInt(将整数写入指定内存地址、有序或者有延迟的方法)等方法。getXXX和putXXX包含了各种基本类型的操作。

利用copyMemory方法,我们可以实现一个通用的对象拷贝方法,无需再对每一个对象都实现clone方法,当然这通用的方法只能做到对象浅拷贝。

Unsafe分配的内存,不受Integer.MAX_VALUE的限制,并且分配在非堆内存,使用它时,需要非常谨慎:忘记手动回收时,会产生内存泄露,可以通过Unsafe#freeMemory方法手动回收;非法的地址访问时,会导致JVM崩溃。在需要分配大的连续区域、实时编程(不能容忍JVM延迟)时,可以使用它,因为直接内存的效率会更好,详细介绍可以去看看Java的NIO源码,NIO中使用了这一技术。

JDK nio包中通过ByteBuffer#allocateDirect方法分配直接内存时,DirectByteBuffer的构造函数中就使用到了Unsafe的allocateMemory和setMemory方法:通过Unsafe.allocateMemory分配内存、Unsafe.setMemory进行内存初始化,而后构建一个虚引用Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,分配的堆外内存一起被释放(通过在Cleaner中调用Unsafe#freeMemory方法)。

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

public native int addressSize();

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

//java.nio下的工具类Bits中计算待申请内存所需内存页数量的静态方法,其依赖于Unsafe中pageSize方法获取系统内存页大小实现后续计算逻辑

public native int pageSize();

//分配一块新的本地内存,通过bytes指定内存块的大小(单位是byte),返回新开辟的内存的地址。可以通过freeMemory方法释放内存块,或者通过reallocateMemory方法调整内存块大小。

//bytes值为负数或者过大会抛出IllegalArgumentException异常,如果系统拒绝分配内存会抛出OutOfMemoryError异常。

public native long allocateMemory(long bytes);

//通过指定的内存地址address重新调整本地内存块的大小,调整后的内存块大小通过bytes指定(单位为byte)。可以通过freeMemory方法释放内存块,或者通过reallocateMemory方法调整内存块大小。

//bytes值为负数或者过大会抛出IllegalArgumentException异常,如果系统拒绝分配内存会抛出OutOfMemoryError异常。

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

//在给定的内存块中设置值。内存块的地址由对象引用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);。

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

//释放内存

public native void freeMemory(long address);

2.4 多线程同步

主要包括监视器锁定、解锁以及CAS相关的方法。这部分包括了monitorEnter、tryMonitorEnter、monitorExit、compareAndSwapInt、compareAndSwap等方法。其中monitorEnter、tryMonitorEnter、monitorExit已经被标记为deprecated,不建议使用。

Unsafe类的CAS操作可能是用的最多的,它为Java的锁机制提供了一种新的解决办法,比如AtomicInteger等类都是通过该方法来实现的。这是一种乐观锁,通常认为在大部分情况下不出现竞态条件,如果操作失败,会不断重试直到成功。

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

@Deprecated

public native void monitorEnter(Object o);

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

@Deprecated

public native void monitorExit(Object o);

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

@Deprecated

public native boolean tryMonitorEnter(Object o);

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

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

//类似的方法有compareAndSwapInt和compareAndSwapLong,在Jdk8中基于CAS扩展出来的方法有getAndAddInt、getAndAddLong、getAndSetInt、getAndSetLong、getAndSetObject,它们的作用都是:通过CAS设置新的值,返回旧的值。

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

//获取对象obj 中偏移量为offset 的变量volatile语义的当前值,并设置变量volatile 语义的值为update

long getAndSetLong(Object obj, long offset, long update)

//获取对象obj同中偏移量为offset 的变量volatile语义的当前值,并设置变量值为原始值+addValue

long getAndAddLong(Object obj, long offset, long addValue)

2.5 线程的挂起和恢复

这部分包括了park、unpark等方法。

将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。整个并发框架中对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本pack方法,但最终都调用了Unsafe.park()方法。

Java8的新锁StampedLock使用该系列方法。

//释放被park阻塞的线程,也可以被使用来终止一个先前调用park导致的阻塞,即这两个方法的调用顺序可以是先unpark再park。

public native void unpark(Object thread);

//阻塞当前线程直到一个unpark方法出现(被调用)、一个用于unpark方法已经出现过(在此park方法调用之前已经调用过)、线程被中断或者time时间到期(也就是阻塞超时)。

// 在time非零的情况下,如果isAbsolute为true,time是相对于新纪元之后的毫秒,否则time表示纳秒。

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

2.6 内存屏障

这部分包括了loadFence、storeFence、fullFence等方法。这是在Java 8新引入的,用于定义内存屏障,避免代码重排序。如果你了解JVM的volatile、锁的内存寓意,那么理解“内存屏障”这几个字应该不会太难,这里只是把它包装成了Java代码。

loadFence() 表示该方法之前的所有load操作在内存屏障之前完成。同理storeFence()表示该方法之前的所有store操作在内存屏障之前完成。fullFence()表示该方法之前的所有load、store操作在内存屏障之前完成。

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

public native void loadFence();

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

public native void storeFence();

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

public native void fullFence();

2.7 其他

//获取系统的平均负载值,loadavg这个double数组将会存放负载值的结果,nelems决定样本数量,nelems只能取值为1到3,分别代表最近1、5、15分钟内系统的平均负载。

//如果无法获取系统的负载,此方法返回-1,否则返回获取到的样本数量(loadavg中有效的元素个数)。实验中这个方法一直返回-1,其实完全可以使用JMX中的相关方法替代此方法。

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

//绕过检测机制直接抛出异常。这让我们可以做些特别的事。

public native void throwException(Throwable ee);

3 应用


3.0 根据偏移量(指针)修改属性值

public class TestUnSafe {

static final Unsafe UNSAFE;

//要更新的字段

private volatile long state;

//记录字段的偏移量

private static final long stateOffset;

/**

* 静态块初始化unsafe,并且获取state字段的偏移量

*/

static {

try {

//反射获取unsafe

Field f = Unsafe.class.getDeclaredField(“theUnsafe”);

f.setAccessible(true);

UNSAFE = (Unsafe) f.get(null);

//获取偏移量

stateOffset = UNSAFE.objectFieldOffset(TestUnSafe.class.getDeclaredField(“state”));

} catch (Exception ex) {

throw new Error(ex);

}

}

public TestUnSafe(long state) {

this.state = state;

}

public static void main(String[] args) {

TestUnSafe testUnSafe = new TestUnSafe(0);

//尝试更改变量值

boolean b = UNSAFE.compareAndSwapLong(testUnSafe, stateOffset, testUnSafe.state, 2);

System.out.println(b);

System.out.println(testUnSafe.state);

}

}

3.1 对象的非常规实例化

我们通常所用到的创建对象的方式,有直接new创建、也有反射创建,其本质都是调用相应的构造器,而使用有参构造函数时,必须传递相应个数的参数才能完成对象实例化。

而Unsafe中提供allocateInstance方法,仅通过Class对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM安全检查等。并且它抑制修饰符检测,也就是即使构造器是private修饰的也能通过此方法实例化,只需提类对象即可创建相应的对象。

由于这种特性,allocateInstance在java.lang.invoke、Objenesis(提供绕过类构造器的对象生成方式)、Gson(反序列化时用到)中都有相应的应用。在Gson反序列化时,如果类有默认构造函数,则通过反射调用默认构造函数创建实例,否则通过UnsafeAllocator来实现对象实例的构造,UnsafeAllocator通过调用Unsafe的allocateInstance实现对象的实例化,保证在目标类无默认构造函数时,反序列化不够影响。

推荐:Java进阶视频资源

案例:

public class UnsafeTest {

private static Unsafe UNSAFE;

static {

try {

Field field = Unsafe.class.getDeclaredField(“theUnsafe”);

field.setAccessible(true);

UNSAFE = (Unsafe) field.get(null);

} catch (Exception ignored) {

}

}

public static void main(String[] args) {

//reflect();

unsafe();

}

/**

* 反射测试,注释掉无参构造器,方法报错;开放注释,方法执行成功,type字段有值。

*/

public static void reflect() {

/如果没有无参构造器,该反射会抛出异常,其内部还是使用的new关键字/

try {

Class<?> aClass = Class.forName(“com.thread.test.juc.unsafe.User”);

Constructor<?> constructor = aClass.getDeclaredConstructor();

constructor.setAccessible(true);

User o = (User) constructor.newInstance(null);

System.out.println(o);

/值为vip,正常/

System.out.println(o.type);

System.out.println(o.age);

} catch (Exception e) {

e.printStackTrace();

}

}

/**

  • UNSAFE测试,注释掉无参构造器,照样成功构造对象,但是type字段为null。这就是没有走构造器的后果之一:没有对字段进行初始化

*/

public static void unsafe() {

try {

/不需要相应的构造器即可创建对象/

User user = (User) UNSAFE.allocateInstance(User.class);

user.setName(“user1”);

System.out.println("instance: " + user);

user.test();

/通过unsafe设置属性值/

Field name = user.getClass().getDeclaredField(“name”);

UNSAFE.putObject(user, UNSAFE.objectFieldOffset(name), “user2”);

user.test();

/值为null,说明unsafe并没有初始化字段。/

System.out.println(user.type);

System.out.println(user.age);

} catch (Exception e) {

e.printStackTrace();

}

}

}

class User {

public String type = “VIP”;

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

金三银四到了,送上一个小福利!

image.png

image.png

专题+大厂.jpg
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
滞不前!**

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-ngLAjhUF-1713125373495)]

[外链图片转存中…(img-NbpN7pjL-1713125373495)]

[外链图片转存中…(img-25UMA9QD-1713125373496)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

金三银四到了,送上一个小福利!

[外链图片转存中…(img-Xc0OlFaM-1713125373496)]

[外链图片转存中…(img-IlURqS0D-1713125373496)]

[外链图片转存中…(img-LAJVDcDN-1713125373497)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值