创建学校网站吗,做网站要源代码,网站建设作用图片,免费企业名录前一段时间在研究juc源码的时候#xff0c;发现在很多工具类中都调用了一个Unsafe类中的方法#xff0c;出于好奇就想要研究一下这个类到底有什么作用#xff0c;于是先查阅了一些资料#xff0c;一查不要紧#xff0c;很多资料中对Unsafe的态度都是这样的画风#xff1a… 前一段时间在研究juc源码的时候发现在很多工具类中都调用了一个Unsafe类中的方法出于好奇就想要研究一下这个类到底有什么作用于是先查阅了一些资料一查不要紧很多资料中对Unsafe的态度都是这样的画风其实看到这些说法也没什么意外毕竟Unsafe这个词直译过来就是“不安全的”从名字里我们也大概能看来Java的开发者们对它有些不放心。但是作为一名极客不能你说不安全我就不去研究了毕竟只有了解一项技术的风险点才能更好的避免出现这些问题嘛。下面我们言归正传先通过简单的介绍来对Unsafe类有一个大致的了解。Unsafe类是一个位于sun.misc包下的类它提供了一些相对底层方法能够让我们接触到一些更接近操作系统底层的资源如系统的内存资源、cpu指令等。而通过这些方法我们能够完成一些普通方法无法实现的功能例如直接使用偏移地址操作对象、数组等等。但是在使用这些方法提供的便利的同时也存在一些潜在的安全因素例如对内存的错误操作可能会引起内存泄漏严重时甚至可能引起jvm崩溃。因此在使用Unsafe前我们必须要了解它的工作原理与各方法的应用场景并且在此基础上仍需要非常谨慎的操作下面我们正式开始对Unsafe的学习。Unsafe 基础首先我们来尝试获取一个Unsafe实例如果按照new的方式去创建对象不好意思编译器会报错提示你Unsafe() has private access in sun.misc.Unsafe
查看Unsafe类的源码可以看到它被final修饰不允许被继承并且构造函数为private类型即不允许我们手动调用构造方法进行实例化只有在static静态代码块中以单例的方式初始化了一个Unsafe对象public final class Unsafe {private static final Unsafe theUnsafe;...private Unsafe() {}...static {theUnsafe new Unsafe();}
}
在Unsafe类中提供了一个静态方法getUnsafe看上去貌似可以用它来获取Unsafe实例CallerSensitive
public static Unsafe getUnsafe() {Class var0 Reflection.getCallerClass();if (!VM.isSystemDomainLoader(var0.getClassLoader())) {throw new SecurityException(Unsafe);} else {return theUnsafe;}
}
但是如果我们直接调用这个静态方法会抛出异常Exception in thread main java.lang.SecurityException: Unsafeat sun.misc.Unsafe.getUnsafe(Unsafe.java:90)at com.cn.test.GetUnsafeTest.main(GetUnsafeTest.java:12)
这是因为在getUnsafe方法中会对调用者的classLoader进行检查判断当前类是否由Bootstrap classLoader加载如果不是的话那么就会抛出一个SecurityException异常。也就是说只有启动类加载器加载的类才能够调用Unsafe类中的方法来防止这些方法在不可信的代码中被调用。那么为什么要对Unsafe类进行这么谨慎的使用限制呢说到底还是因为它实现的功能过于底层例如直接进行内存操作、绕过jvm的安全检查创建对象等等概括的来说Unsafe类实现功能可以被分为下面8类创建实例看到上面的这些功能你是不是已经有些迫不及待想要试一试了。那么如果我们执意想要在自己的代码中调用Unsafe类的方法应该怎么获取一个它的实例对象呢答案是利用反射获得Unsafe类中已经实例化完成的单例对象public static Unsafe getUnsafe() throws IllegalAccessException {Field unsafeField Unsafe.class.getDeclaredField(theUnsafe);//Field unsafeField Unsafe.class.getDeclaredFields()[0]; //也可以这样作用相同unsafeField.setAccessible(true);Unsafe unsafe (Unsafe) unsafeField.get(null);return unsafe;
}
在获取到Unsafe的实例对象后我们就可以使用它为所欲为了先来尝试使用它对一个对象的属性进行读写public void fieldTest(Unsafe unsafe) throws NoSuchFieldException {User usernew User();long fieldOffset unsafe.objectFieldOffset(User.class.getDeclaredField(age));System.out.println(offset:fieldOffset);unsafe.putInt(user,fieldOffset,20);System.out.println(age:unsafe.getInt(user,fieldOffset));System.out.println(age:user.getAge());
}
运行代码输出如下可以看到通过Unsafe类的objectFieldOffset方法获取了对象中字段的偏移地址这个偏移地址不是内存中的绝对地址而是一个相对地址之后再通过这个偏移地址对int类型字段的属性值进行了读写操作通过结果也可以看到Unsafe的方法和类中的get方法获取到的值是相同的。offset:12
age:20
age:20
在上面的例子中调用了Unsafe类的putInt和getInt方法看一下源码中的方法public native int getInt(Object o, long offset);
public native void putInt(Object o, long offset, int x);
先说作用getInt用于从对象的指定偏移地址处读取一个intputInt用于在对象指定偏移地址处写入一个int并且即使类中的这个属性是private私有类型的也可以对它进行读写。但是有细心的小伙伴可能发现了这两个方法相对于我们平常写的普通方法多了一个native关键字修饰并且没有具体的方法逻辑那么它是怎么实现的呢native方法在java中这类方法被称为native方法Native Method简单的说就是由java调用非java代码的接口被调用的方法是由非java 语言实现的例如它可以由C或C语言来实现并编译成DLL然后直接供java进行调用。native方法是通过JNIJava Native Interface实现调用的从 java1.1开始 JNI 标准就是java平台的一部分它允许java代码和其他语言的代码进行交互。Unsafe类中的很多基础方法都属于native方法那么为什么要使用native方法呢原因可以概括为以下几点需要用到 java 中不具备的依赖于操作系统的特性java在实现跨平台的同时要实现对底层的控制需要借助其他语言发挥作用对于其他语言已经完成的一些现成功能可以使用java直接调用程序对时间敏感或对性能要求非常高时有必要使用更加底层的语言例如C/C甚至是汇编在juc包的很多并发工具类在实现并发机制时都调用了native方法通过它们打破了java运行时的界限能够接触到操作系统底层的某些功能。对于同一个native方法不同的操作系统可能会通过不同的方式来实现但是对于使用者来说是透明的最终都会得到相同的结果至于java如何实现的通过JNI调用其他语言的代码不是本文的重点会在后续的文章中具体学习。Unsafe 应用在对Unsafe的基础有了一定了解后我们来看一下它的基本应用。由于篇幅有限不能对所有方法进行介绍如果大家有学习的需要可以下载openJDK的源码进行学习。1、内存操作如果你是一个写过c或者c的程序员一定对内存操作不会陌生而在java中是不允许直接对内存进行操作的对象内存的分配和回收都是由jvm自己实现的。但是在Unsafe中提供的下列接口可以直接进行内存操作//分配新的本地空间
public native long allocateMemory(long bytes);
//重新调整内存空间的大小
public native long reallocateMemory(long address, long bytes);
//将内存设置为指定值
public native void setMemory(Object o, long offset, long bytes, byte value);
//内存拷贝
public native void copyMemory(Object srcBase, long srcOffset,Object destBase, long destOffset,long bytes);
//清除内存
public native void freeMemory(long address);
使用下面的代码进行测试private void memoryTest() {int size 4;long addr unsafe.allocateMemory(size);long addr3 unsafe.reallocateMemory(addr, size * 2);System.out.println(addr: addr);System.out.println(addr3: addr3);try {unsafe.setMemory(null,addr ,size,(byte)1);for (int i 0; i 2; i) {unsafe.copyMemory(null,addr,null,addr3size*i,4);}System.out.println(unsafe.getInt(addr));System.out.println(unsafe.getLong(addr3));}finally {unsafe.freeMemory(addr);unsafe.freeMemory(addr3);}
}
先看结果输出addr: 2433733895744
addr3: 2433733894944
16843009
72340172838076673
分析一下运行结果首先使用allocateMemory方法申请4字节长度的内存空间在循环中调用setMemory方法向每个字节写入内容为byte类型的1当使用Unsafe调用getInt方法时因为一个int型变量占4个字节会一次性读取4个字节组成一个int的值对应的十进制结果为16843009可以通过图示理解这个过程在代码中调用reallocateMemory方法重新分配了一块8字节长度的内存空间通过比较addr和addr3可以看到和之前申请的内存地址是不同的。在代码中的第二个for循环里调用copyMemory方法进行了两次内存的拷贝每次拷贝内存地址addr开始的4个字节分别拷贝到以addr3和addr34开始的内存空间上拷贝完成后使用getLong方法一次性读取8个字节得到long类型的值为72340172838076673。需要注意通过这种方式分配的内存属于堆外内存是无法进行垃圾回收的需要我们把这些内存当做一种资源去手动调用freeMemory方法进行释放否则会产生内存泄漏。通用的操作内存方式是在try中执行对内存的操作最终在finally块中进行内存的释放。2、内存屏障在介绍内存屏障前需要知道编译器和CPU会在保证程序输出结果一致的情况下会对代码进行重排序从指令优化角度提升性能。而指令重排序可能会带来一个不好的结果导致CPU的高速缓存和内存中数据的不一致而内存屏障Memory Barrier就是通过组织屏障两边的指令重排序从而避免编译器和硬件的不正确优化情况。在硬件层面上内存屏障是CPU为了防止代码进行重排序而提供的指令不同的硬件平台上实现内存屏障的方法可能并不相同。在java8中引入了3个内存屏障的函数它屏蔽了操作系统底层的差异允许在代码中定义、并统一由jvm来生成内存屏障指令来实现内存屏障的功能。Unsafe中提供了下面三个内存屏障相关方法//禁止读操作重排序
public native void loadFence();
//禁止写操作重排序
public native void storeFence();
//禁止读、写操作重排序
public native void fullFence();
内存屏障可以看做对内存随机访问的操作中的一个同步点使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。以loadFence方法为例它会禁止读操作重排序保证在这个屏障之前的所有读操作都已经完成并且将缓存数据设为无效重新从主存中进行加载。看到这估计很多小伙伴们会想到volatile关键字了如果在字段上添加了volatile关键字就能够实现字段在多线程下的可见性。基于读内存屏障我们也能实现相同的功能。下面定义一个线程方法在线程中去修改flag标志位注意这里的flag是没有被volatile修饰的Getter
class ChangeThread implements Runnable{/**volatile**/ boolean flagfalse;Overridepublic void run() {try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();} System.out.println(subThread change flag to: flag);flag true;}
}
在主线程的while循环中加入内存屏障测试是否能够感知到flag的修改变化public static void main(String[] args){ChangeThread changeThread new ChangeThread();new Thread(changeThread).start();while (true) {boolean flag changeThread.isFlag();unsafe.loadFence(); //加入读内存屏障if (flag){System.out.println(detected flag changed);break;}}System.out.println(main thread end);
}
运行结果subThread change flag to:false
detected flag changed
main thread end
而如果删掉上面代码中的loadFence方法那么主线程将无法感知到flag发生的变化会一直在while中循环。可以用图来表示上面的过程了解java内存模型JMM的小伙伴们应该清楚运行中的线程不是直接读取主内存中的变量的只能操作自己工作内存中的变量然后同步到主内存中并且线程的工作内存是不能共享的。上面的图中的流程就是子线程借助于主内存将修改后的结果同步给了主线程进而修改主线程中的工作空间跳出循环。3、对象操作a、对象成员属性的内存偏移量获取以及字段属性值的修改在上面的例子中我们已经测试过了。除了前面的putInt、getInt方法外Unsafe提供了全部8种基础数据类型以及Object的put和get方法并且所有的put方法都可以越过访问权限直接修改内存中的数据。阅读openJDK源码中的注释发现基础数据类型和Object的读写稍有不同基础数据类型是直接操作的属性值value而Object的操作则是基于引用值reference value。下面是Object的读写方法//在对象的指定偏移地址获取一个对象引用
public native Object getObject(Object o, long offset);
//在对象指定偏移地址写入一个对象引用
public native void putObject(Object o, long offset, Object x);
除了对象属性的普通读写外Unsafe还提供了volatile读写和有序写入方法。volatile读写方法的覆盖范围与普通读写相同包含了全部基础数据类型和Object类型以int类型为例//在对象的指定偏移地址处读取一个int值支持volatile load语义
public native int getIntVolatile(Object o, long offset);
//在对象指定偏移地址处写入一个int支持volatile store语义
public native void putIntVolatile(Object o, long offset, int x);
相对于普通读写来说volatile读写具有更高的成本因为它需要保证可见性和有序性。在执行get操作时会强制从主存中获取属性值在使用put方法设置属性值时会强制将值更新到主存中从而保证这些变更对其他线程是可见的。有序写入的方法有以下三个public native void putOrderedObject(Object o, long offset, Object x);
public native void putOrderedInt(Object o, long offset, int x);
public native void putOrderedLong(Object o, long offset, long x);
有序写入的成本相对volatile较低因为它只保证写入时的有序性而不保证可见性也就是一个线程写入的值不能保证其他线程立即可见。为了解决这里的差异性需要对内存屏障的知识点再进一步进行补充首先需要了解两个指令的概念Load将主内存中的数据拷贝到处理器的缓存中Store将处理器缓存的数据刷新到主内存中顺序写入与volatile写入的差别在于在顺序写时加入的内存屏障类型为StoreStore类型而在volatile写入时加入的内存屏障是StoreLoad类型如下图所示在有序写入方法中使用的是StoreStore屏障该屏障确保Store1立刻刷新数据到内存这一操作先于Store2以及后续的存储指令操作。而在volatile写入中使用的是StoreLoad屏障该屏障确保Store1立刻刷新数据到内存这一操作先于Load2及后续的装载指令并且StoreLoad屏障会使该屏障之前的所有内存访问指令包括存储指令和访问指令全部完成之后才执行该屏障之后的内存访问指令。综上所述在上面的三类写入方法中在写入效率方面按照put、putOrder、putVolatile的顺序效率逐渐降低b、使用Unsafe的allocateInstance方法允许我们使用非常规的方式进行对象的实例化首先定义一个实体类并且在构造函数中对其成员变量进行赋值操作Data
public class A {private int b;public A(){this.b 1;}
}
分别基于构造函数、反射以及Unsafe方法的不同方式创建对象进行比较public void objTest() throws Exception{A a1new A();System.out.println(a1.getB());A a2 A.class.newInstance();System.out.println(a2.getB());A a3 (A) unsafe.allocateInstance(A.class);System.out.println(a3.getB());
}
打印结果分别为1、1、0说明通过allocateInstance方法创建对象过程中不会调用类的构造方法。使用这种方式创建对象时只用到了Class对象所以说如果想要跳过对象的初始化阶段或者跳过构造器的安全检查就可以使用这种方法。在上面的例子中如果将A类的构造函数改为private类型将无法通过构造函数和反射创建对象但allocateInstance方法仍然有效。4、数组操作在Unsafe中可以使用arrayBaseOffset方法可以获取数组中第一个元素的偏移地址使用arrayIndexScale方法可以获取数组中元素间的偏移地址增量。使用下面的代码进行测试private void arrayTest() {String[] arraynew String[]{str1str1str,str2,str3};int baseOffset unsafe.arrayBaseOffset(String[].class);System.out.println(baseOffset);int scale unsafe.arrayIndexScale(String[].class);System.out.println(scale);for (int i 0; i array.length; i) {int offsetbaseOffsetscale*i;System.out.println(offset : unsafe.getObject(array,offset));}
}
上面代码的输出结果为16
4
16 : str1str1str
20 : str2
24 : str3
通过配合使用数组偏移首地址和各元素间偏移地址的增量可以方便的定位到数组中的元素在内存中的位置进而通过getObject方法直接获取任意位置的数组元素。需要说明的是arrayIndexScale获取的并不是数组中元素占用的大小而是地址的增量按照openJDK中的注释可以将它翻译为元素寻址的转换因子scale factor for addressing elements。在上面的例子中第一个字符串长度为11字节但其地址增量仍然为4字节。那么基于这两个值是如何实现的寻址和数组元素的访问呢这里需要借助一点在前面的文章中讲过的Java对象内存布局的知识先把上面例子中的String数组对象的内存布局画出来就很方便大家理解了在String数组对象中对象头包含3部分mark word标记字占用8字节klass point类型指针占用4字节数组对象特有的数组长度部分占用4字节总共占用了16字节。第一个String的引用类型相对于对象的首地址的偏移量是就16之后每个元素在这个基础上加4正好对应了我们上面代码中的寻址过程之后再使用前面说过的getObject方法通过数组对象可以获得对象在堆中的首地址再配合对象中变量的偏移量就能获得每一个变量的引用。5、CAS操作在juc包的并发工具类中大量地使用了CAS操作像在前面介绍synchronized和AQS的文章中也多次提到了CAS其作为乐观锁在并发工具类中广泛发挥了作用。在Unsafe类中提供了compareAndSwapObject、compareAndSwapInt、compareAndSwapLong方法来实现的对Object、int、long类型的CAS操作。以compareAndSwapInt方法为例public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
参数中o为需要更新的对象offset是对象o中整形字段的偏移量如果这个字段的值与expected相同则将字段的值设为x这个新值并且此更新是不可被中断的也就是一个原子操作。下面是一个使用compareAndSwapInt的例子private volatile int a;
public static void main(String[] args){CasTest casTestnew CasTest();new Thread(()-{for (int i 1; i 5; i) {casTest.increment(i);System.out.print(casTest.a );}}).start();new Thread(()-{for (int i 5 ; i 10 ; i) {casTest.increment(i);System.out.print(casTest.a );}}).start();
}private void increment(int x){while (true){try {long fieldOffset unsafe.objectFieldOffset(CasTest.class.getDeclaredField(a));if (unsafe.compareAndSwapInt(this,fieldOffset,x-1,x))break;} catch (NoSuchFieldException e) {e.printStackTrace();}}
}
运行代码会依次输出1 2 3 4 5 6 7 8 9
在上面的例子中使用两个线程去修改int型属性a的值并且只有在a的值等于传入的参数x减一时才会将a的值变为x也就是实现对a的加一的操作。流程如下所示需要注意的是在调用compareAndSwapInt方法后会直接返回true或false的修改结果因此需要我们在代码中手动添加自旋的逻辑。在AtomicInteger类的设计中也是采用了将compareAndSwapInt的结果作为循环条件直至修改成功才退出死循环的方式来实现的原子性的自增操作。6、线程调度Unsafe类中提供了park、unpark、monitorEnter、monitorExit、tryMonitorEnter方法进行线程调度在前面介绍AQS的文章中我们提到过使用LockSupport挂起或唤醒指定线程看一下LockSupport的源码可以看到它也是调用的Unsafe类中的方法public static void park(Object blocker) {Thread t Thread.currentThread();setBlocker(t, blocker);UNSAFE.park(false, 0L);setBlocker(t, null);
}
public static void unpark(Thread thread) {if (thread ! null)UNSAFE.unpark(thread);
}
LockSupport的park方法调用了Unsafe的park方法来阻塞当前线程此方法将线程阻塞后就不会继续往后执行直到有其他线程调用unpark方法唤醒当前线程。下面的例子对Unsafe的这两个方法进行测试public static void main(String[] args) {Thread mainThread Thread.currentThread();new Thread(()-{try {TimeUnit.SECONDS.sleep(5);System.out.println(subThread try to unpark mainThread);unsafe.unpark(mainThread);} catch (InterruptedException e) {e.printStackTrace();}}).start();System.out.println(park main mainThread);unsafe.park(false,0L);System.out.println(unpark mainThread success);
}
程序输出为park main mainThread
subThread try to unpark mainThread
unpark mainThread success
程序运行的流程也比较容易看懂子线程开始运行后先进行睡眠确保主线程能够调用park方法阻塞自己子线程在睡眠5秒后调用unpark方法唤醒主线程使主线程能继续向下执行。整个流程如下图所示此外Unsafe源码中monitor相关的三个方法已经被标记为deprecated不建议被使用//获得对象锁
Deprecated
public native void monitorEnter(Object var1);
//释放对象锁
Deprecated
public native void monitorExit(Object var1);
//尝试获得对象锁
Deprecated
public native boolean tryMonitorEnter(Object var1);
monitorEnter方法用于获得对象锁monitorExit用于释放对象锁如果对一个没有被monitorEnter加锁的对象执行此方法会抛出IllegalMonitorStateException异常。tryMonitorEnter方法尝试获取对象锁如果成功则返回true反之返回false。7、Class操作Unsafe对Class的相关操作主要包括类加载和静态变量的操作方法。a、静态属性读取相关的方法//获取静态属性的偏移量
public native long staticFieldOffset(Field f);
//获取静态属性的对象指针
public native Object staticFieldBase(Field f);
//判断类是否需要实例化用于获取类的静态属性前进行检测
public native boolean shouldBeInitialized(Class? c);
创建一个包含静态属性的类进行测试Data
public class User {public static String nameHydra;int age;
}
private void staticTest() throws Exception {User usernew User();System.out.println(unsafe.shouldBeInitialized(User.class));Field sexField User.class.getDeclaredField(name);long fieldOffset unsafe.staticFieldOffset(sexField);Object fieldBase unsafe.staticFieldBase(sexField);Object object unsafe.getObject(fieldBase, fieldOffset);System.out.println(object);
}
运行结果false
Hydra
在Unsafe的对象操作中我们学习了通过objectFieldOffset方法获取对象属性偏移量并基于它对变量的值进行存取但是它不适用于类中的静态属性这时候就需要使用staticFieldOffset方法。在上面的代码中只有在获取Field对象的过程中依赖到了Class而获取静态变量的属性时不再依赖于Class。在上面的代码中首先创建一个User对象这是因为如果一个类没有被实例化那么它的静态属性也不会被初始化最后获取的字段属性将是null。所以在获取静态属性前需要调用shouldBeInitialized方法判断在获取前是否需要初始化这个类。如果删除创建User对象的语句运行结果会变为true
null
b、使用defineClass方法允许程序在运行时动态地创建一个类方法定义如下public native Class? defineClass(String name, byte[] b, int off, int len,ClassLoader loader,ProtectionDomain protectionDomain);
在实际使用过程中可以只传入字节数组、起始字节的下标以及读取的字节长度默认情况下类加载器ClassLoader和保护域ProtectionDomain来源于调用此方法的实例。下面的例子中实现了反编译生成后的class文件的功能private static void defineTest() {String fileNameF:\\workspace\\unsafe-test\\target\\classes\\com\\cn\\model\\User.class;File file new File(fileName);try(FileInputStream fis new FileInputStream(file)) {byte[] contentnew byte[(int)file.length()];fis.read(content);Class clazz unsafe.defineClass(null, content, 0, content.length, null, null);Object o clazz.newInstance();Object age clazz.getMethod(getAge).invoke(o, null);System.out.println(age);} catch (Exception e) {e.printStackTrace();}
}
在上面的代码中首先读取了一个class文件并通过文件流将它转化为字节数组之后使用defineClass方法动态的创建了一个类并在后续完成了它的实例化工作流程如下图所示并且通过这种方式创建的类会跳过JVM的所有安全检查。除了defineClass方法外Unsafe还提供了一个defineAnonymousClass方法public native Class? defineAnonymousClass(Class? hostClass, byte[] data, Object[] cpPatches);
使用该方法可以用来动态的创建一个匿名类在Lambda表达式中就是使用ASM动态生成字节码然后利用该方法定义实现相应的函数式接口的匿名类。在jdk15发布的新特性中在隐藏类Hidden classes一条中指出将在未来的版本中弃用Unsafe的defineAnonymousClass方法。8、系统信息Unsafe中提供的addressSize和pageSize方法用于获取系统信息调用addressSize方法会返回系统指针的大小如果在64位系统下默认会返回8而32位系统则会返回4。调用pageSize方法会返回内存页的大小值为2的整数幂。使用下面的代码可以直接进行打印private void systemTest() {System.out.println(unsafe.addressSize());System.out.println(unsafe.pageSize());
}
执行结果8
4096
这两个方法的应用场景比较少在java.nio.Bits类中在使用pageCount计算所需的内存页的数量时调用了pageSize方法获取内存页的大小。另外在使用copySwapMemory方法拷贝内存时调用了addressSize方法检测32位系统的情况。总结在本文中我们首先介绍了Unsafe的基本概念、工作原理并在此基础上对它的API进行了说明与实践。相信大家通过这一过程能够发现Unsafe在某些场景下确实能够为我们提供编程中的便利。但是回到开头的话题在使用这些便利时确实存在着一些安全上的隐患在我看来一项技术具有不安全因素并不可怕可怕的是它在使用过程中被滥用。尽管之前有传言说会在java9中移除Unsafe类不过它还是照样已经存活到了jdk16按照存在即合理的逻辑只要使用得当它还是能给我们带来不少的帮助因此最后还是建议大家在使用Unsafe的过程中一定要做到使用谨慎使用、避免滥用。