空间模块克隆(关于 Unsafe,我只说这么“多”)

2023-09-19 05:32:24

jdk 版本: OpenJDK 11

什么是 Unsafe?

这是一个别有用心的名字,直接了当地告诫开发者,这是一个“不安全”的类。


我们知道 Java 不同于 C,由于存在 JVM 这个中间层,一般开发者是无法通过代码直接去操作内存的,一切都是 JVM 在幕后操作的。


而 Unsafe 定义了低层次、不安全的操作。有多低、有多不安全呢?


是的,对应第一句话,Unsafe 允许直接访问或操作到内存上的数据。这样固然更快捷,但是牺牲的却是 JVM 对对象或变量访问操作的检查和限制,就有点类似于通过反射来操作 private 的变量。


因此从中得出第一个特性: 在性能是最高优先级的情况下,该类方法不保证输入参数的检查;甚至于在运行时编译器层面,都会在优化该类时,省略部分或全部的检查。


所以:


调用方一定不能依赖于该类的检查或相应的异常;限制该类的使用,只有可信的代码可以使用,一般是 JDK 类库。

我们能用 Unsafe?

如果按照上面的说法,那我写的 BUG 肯定不算是可信代码,说这个图啥呢。


自然是有办法的,首先得知道是咋限制的。


获取 Unsafe 实例的限制

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


sun.misc.Unsafe 内部都是委托 jdk.internal.misc.Unsafe 来操作的,所以后面功能分析都是基于 jdk.internal.misc.Unsafe 的。 而且 jdk.internal.misc.Unsafe 提供更全的操作, sun.misc.Unsafe 只开放了部分。


jdk.internal.misc.Unsafe


public final class Unsafe { private Unsafe() {} private static final Unsafe theUnsafe = new Unsafe(); public static Unsafe getUnsafe() { return theUnsafe; }}复制代码

sun.misc.Unsafe


public final class Unsafe { private Unsafe() {} private static final Unsafe theUnsafe = new Unsafe(); private static final jdk.internal.misc.Unsafe theInternalUnsafe = jdk.internal.misc.Unsafe.getUnsafe(); @CallerSensitive public static Unsafe getUnsafe() { Class<?> caller = Reflection.getCallerClass(); if (!VM.isSystemDomainLoader(caller.getClassLoader())) throw new SecurityException("Unsafe"); return theUnsafe; }}复制代码

public static boolean isSystemDomainLoader(ClassLoader loader) { return loader == null || loader == ClassLoader.getPlatformClassLoader();}复制代码

当我们在应用代码中通过 Unsafe.getUnsafe()获取实例时,会被要求当前调用类的加载器是否为 Bootstrap 加载器(loader == null)或 Platform 加载器(1.9以前叫 Extensions 加载器)。

强制获取 Unsafe 实例

一种是我们通过 JVM 参数 -Xbootclasspath: 来使调用类被 Bootstrap 加载器加载。


但是该参数在 1.9 以后已经不被支持了,会导致启动 JVM 失败。


所以使用另一种方式,通过反射获取。


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

基于 JDK 11,由于模块化的限制,其实无法在代码中获取到 jdk.internal.misc.Unsafe


但是 要用魔法打败魔法 ,修改模块访问限制,也是有办法的。


在 VM 参数中添加 --add-opens java.base/jdk.internal.misc=ALL-UNNAMED ,就可以在代码中直接访问到了, jdk.internal.misc.Unsafe。getUnsafe()


不过并不建议使用 jdk.internal.misc.Unsafe


一是放开了模块限制,安全性降低了;


二是相对于 sun.misc.Unsafe , jdk.internal.misc.Unsafe 提供的底层操作更多,也就是不安全性更高了。


Unsafe 使用建议

内容来自参考 1。


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

Unsafe 有啥用?怎么用?

注意:内容很多,选择性使用。


1. 根据偏移量,读写对象属性

这里有一系列的 put 、get 方法,针对了基础类型、Object 以及指针地址。


以上列举的操作,都是基于 JVM 堆上的。


基本类型

Int、Boolean、Long、Byte、Float、Double、Short、Char

Get :


@HotSpotIntrinsicCandidate// 从 o 中根据 offset 作为偏移量,获取值;或 o 为 null,则直接从以 offset 作为内存地址中获取。public native int getInt(Object o, long offset);public int getInt(long address) { return getInt(null, address);}复制代码

Unsafe 的方法是没有检查的,如何保证获取到的值是类型确定,结果明确呢?


那就要从参数入手:


o 不为 null,偏移量是 o 的类上对应字段的反射类 Filed 调用 Unsafe.objectFieldOffset()(指定字段在类中的偏移量)获取到的。其次 o 所代表的的类肯定是向父类兼容的。 后面没有特别说明的方法,默认是 Unsafe 类的。静态变量字段,无论 o 是否为 null,分别通过 staticFieldOffset() (静态字段在 Class 的偏移量)和 staticFieldBase() (对应静态字段起始位置)获取的。o 是一个数组,offset 是 B N*S 的整数。N 是数组的有效索引,就是要第 N 给元素的值;B 是数组对象内存地址,通过 arrayBaseOffset() 获取;S 是数组一个元素的偏移量,指一个元素在内存中占据多少空间,通过 arrayIndexScale 获得。最特殊的是,当 o == null 或 使用 getInt(long address) 时,offset 代表了指定内存地址。如果地址为零,或未指向从 allocateMemory 获取的内存,则结果不确定。

定义一个用于实验的模型,后续就不再说明了:


public class Demo { static int n1 = 1; int n2 = 2; Integer n3 = 3;}复制代码

private static void _getInt(Unsafe unsafe) throws NoSuchFieldException { Demo o = new Demo(); // 第一种情况 int i1 = unsafe.getInt(o, unsafe.objectFieldOffset(Demo.class.getDeclaredField("n2"))); System.out.println("第一种情况: " i1); // 第二种情况 Field n1 = Demo.class.getDeclaredField("n1"); int i2 = unsafe.getInt(unsafe.staticFieldBase(n1), unsafe.staticFieldOffset(n1)); System.out.println("第二种情况: " i2); // 第三种 int[] ns = {9, 8, 7, 6}; int i3 = unsafe.getInt(ns, unsafe.arrayBaseOffset(ns.getClass()) 3 * unsafe.arrayIndexScale(ns.getClass())); System.out.println("第三种情况: " i3); // 第四种:直接是内存地址 // VM.current().addressOf(o) 依赖:compile("org.openjdk.jol:jol-core:0.9") int i4 = unsafe.getInt(VM.current().addressOf(o) unsafe.objectFieldOffset(Demo.class.getDeclaredField("n2"))); System.out.println("第四种情况: " i4);}// 结果第一种情况: 2第二种情况: 1第三种情况: 6第四种情况: 2 复制代码

Put :


@HotSpotIntrinsicCandidatepublic native void putInt(Object o, long offset, int x);复制代码

将 x 存放到上述四种情况下的内存地址上。即 o 和 offset 的情况同 getInt(Object o, long offset)


private static void _putInt(Unsafe unsafe) throws NoSuchFieldException { Demo o = new Demo(); // 第一种情况 unsafe.putInt(o, unsafe.objectFieldOffset(Demo.class.getDeclaredField("n2")), o.n2 1); System.out.println("第一种情况: " o.n2); // 第二种情况 Field n1 = Demo.class.getDeclaredField("n1"); unsafe.putInt(unsafe.staticFieldBase(n1), unsafe.staticFieldOffset(n1), o.n1 1); System.out.println("第二种情况: " o.n1); // 第三种 int[] ns = {9, 8, 7, 6}; unsafe.putInt(ns, unsafe.arrayBaseOffset(ns.getClass()) 3 * unsafe.arrayIndexScale(ns.getClass()), ns[3] - 1); System.out.println("第三种情况: " ns[3]); // 第四种:直接是内存地址 unsafe.putInt(null, VM.current().addressOf(o) unsafe.objectFieldOffset(Demo.class.getDeclaredField("n2")), o.n2 1); System.out.println("第四种情况: " o.n2);}// 结果第一种情况: 3第二种情况: 2第三种情况: 5第四种情况: 4复制代码

Object

@HotSpotIntrinsicCandidatepublic native Object getObject(Object o, long offset);@HotSpotIntrinsicCandidatepublic native void putObject(Object o, long offset, Object x);复制代码

put 和 get 类同 上述情况。


只是 put 时:


x 为 null 或类型匹配,结果才是明确的;否则就有可能发生其他错误。如果 o 不为 null,则更新 card marks 或其他内存屏障(基于 JVM 的管理)。

private static void _putAndGetObject(Unsafe unsafe) throws NoSuchFieldException { Demo o = new Demo(); // 第一种情况 Object i1 = unsafe.getObject(o, unsafe.objectFieldOffset(Demo.class.getDeclaredField("n3"))); System.out.println("before put: " i1); unsafe.putObject(o, unsafe.objectFieldOffset(Demo.class.getDeclaredField("n3")), Integer.valueOf(5)); Object i2 = unsafe.getObject(o, unsafe.objectFieldOffset(Demo.class.getDeclaredField("n3"))); System.out.println("after put: " i2);}// 结果before put: 3after put: 5复制代码

Address

操作的是内存地址上的指针句柄, 直接句柄间接句柄 ?如果是间接句柄,并不能完全代表是一个对象在堆上的地址。


@ForceInline// 被要求强制内联public long getAddress(Object o, long offset) { if (ADDRESS_SIZE == 4) { // 如果本机指针的宽度小于 64 位,则将其作为无符号数字扩展为 long。 return Integer.toUnsignedLong(getInt(o, offset)); } else { return getLong(o, offset); }}@ForceInlinepublic void putAddress(Object o, long offset, long x) { if (ADDRESS_SIZE == 4) { putInt(o, offset, (int)x); } else { putLong(o, offset, x); }}// 获取未压缩的指针,忽略 JVM 的压缩指针设置public native Object getUncompressedObject(long address);复制代码

get:从指定内存地址获取到本地指针。


put:将本机指针存储到给定的内存地址中。


如果地址为零,或不是指向从 allocateMemory() 获取的内存,则结果不确定。如果本机指针的宽度小于 64 位,则将其作为无符号数字扩展为 long。addressSize()

private static void _putAndGetAddress(Unsafe unsafe) throws NoSuchFieldException { long start = unsafe.allocateMemory(4); int i = unsafe.getInt(start); System.out.println("before put:" i); unsafe.putAddress(start, 1000); i = unsafe.getInt(start); System.out.println("after put1:" i); System.out.println("after put2:" unsafe.getAddress(start));}// 结果before put:0after put1:1000after put2:1000复制代码

2.内存管理

/** * 分配内存(以字节为单位),内存上的内容还未初始化,属于会被回收的垃圾. * 返回的正常分配内存起始地址永远不会为0, 且将针对所有值类型进行对齐. * 通过调用freeMemory处理此内存,或使用reallocateMemory调整其大小。 */public long allocateMemory(long bytes);/** * 将新的本机内存块调整为给定的字节大小。 超出旧块大小的新块的内容未初始化;它们通常是垃圾。 * 当且仅当请求的大小为零时,结果本机指针才为零。 * 结果本机指针将针对所有值类型对齐。 * 通过调用freeMemory处理此内存,或使用reallocateMemory调整其大小。 * 传递给此方法的地址可以为null,在这种情况下将执行第一次分配 */public long reallocateMemory(long address, long bytes);/** * 将给定内存块中指定数量的字节设置为固定值(通常为零)。 * * 此方法通过两个参数确定块的基地址,因此它(实际上)提供了双寄存器寻址模式,如getInt(Object, long) 。 * 当对象引用为空时,偏移量将提供绝对基地址。 * * 存储以连贯的(原子的)单位表示,其大小由地址和长度参数确定。 如果有效地址和长度均为偶数模8,则存储以“long ”单位进行。 * 如果有效地址和长度(以模4或2为模数),则存储以“int”或“short”为单位 */public void setMemory(Object o, long offset, long bytes, byte value);/** * 将给定内存块中指定数量的字节设置为另一个块的副本。 */public void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);public void copyMemory(long srcAddress, long destAddress, long bytes);/** * 处置从allocateMemory或reallocateMemory获得的本机内存块。 传递给此方法的地址可以为null,在这种情况下,不采取任何措施 */public void freeMemory(long address);复制代码

应用(图片来源:https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html)


Cleaner 中的应用同见上面链接。


3.类相关操作

获取属性偏移量

在第一节 根据偏移量,读写对象属性 中,经常需要根据属性在对象中的偏移量,来找到属性的内存地址。


而这些计算偏移量的操作,Unsafe 也提供了。


//实例字段在对象中的偏移量。如果是第一个字段,那实际就是对象头的长度了。public long objectFieldOffset(Field f)// 根据 Class 和字段名, 获取实例字段在对象中的偏移量public long objectFieldOffset(Class<?> c, String name)// 以下两个一起,可以获取静态字段的内存地址。实际用于类似 getInt 的一系列方法。public long staticFieldOffset(Field f)public Object staticFieldBase(Field f)// 以下两个都预设了基本类型和 Object 类型数组的相应值// 给定数组类的存储分配中第一个元素的偏移量,实际就是数组头的长度public int arrayBaseOffset(Class<?> arrayClass)// 数组中每个元素的长度public int arrayIndexScale(Class<?> arrayClass)复制代码

检测初始化

// 判断是否需要初始化一个类,通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。 // 当且仅当 ensureClassInitialized 方法不生效时返回false。public boolean shouldBeInitialized(Class<?> c)//检测给定的类是否已经初始化。通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。public void ensureClassInitialized(Class<?> c)复制代码

创建类

//定义一个类,此方法会跳过JVM的所有安全检查,默认情况下,ClassLoader(类加载器)和ProtectionDomain(保护域)实例来源于调用者public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);//定义一个匿名类public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);复制代码

检测类初始化以及创建类或匿名类的应用场景,其实对于 1.8 版本以上的 JDK版本中,是非常常见的。


因为是用在 Lambda 表达式的处理中。


public void test() { List<Integer> list = new ArrayList<>(16); list.add(1); list.add(2); list.add(3); list.forEach(i -> { System.out.println(i); }); list.forEach(i -> { System.out.println(i num); });}复制代码

反编译的结果如下:


编译器会为 Lambda 表达式生成特殊名称的实例方法或静态方法。在通过 UNSAFE.defineAnonymousClass 创建匿名类,然后实例化。最后返回与此匿名类中函数式方法的方法句柄关联的调用点;而后可以通过此调用点实现调用相应Lambda表达式定义逻辑的功能。


LambdaMetafactory.metafactory() -> InnerClassLambdaMetafactory.buildCallSite() -> InnerClassLambdaMetafactory.spinInnerClass() ->UNSAFE.defineAnonymousClass()


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


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

还有其他几点可以自行阅读。这个方法虽然翻译为"定义匿名类",但是它所定义的类和实际的匿名类有点不相同,因此一般情况下我们不会用到此方法。


4.系统相关

// 内存页的大小,以字节为单位。一定是 2 的 n 次幂。public native int pageSize();// 本地指针的字节长度, 4 or 8public int addressSize() { return ADDRESS_SIZE;}// 获取系统的平均负载值,loadavg这个double数组将会存放负载值的结果,// nelems决定样本数量,nelems只能取值为1到3,分别代表最近1、5、15分钟内系统的平均负载。// 如果无法获取系统的负载,此方法返回-1,否则返回获取到的样本数量(loadavg中有效的元素个数)。public int getLoadAverage(double[] loadavg, int nelems) { if (nelems < 0 || nelems > 3 || nelems > loadavg.length) { throw new ArrayIndexOutOfBoundsException(); } return getLoadAverage0(loadavg, nelems);}复制代码

5.对象管理

非常规实例化

// 绕过实例构造方法 init(实例变量初始化、代码块、构造函数),JVM 的检查等,仅靠 Class 实例化对象// 同时,它抑制修饰符检测,也就是即使构造器是private修饰的也能通过此方法实例化。public native Object allocateInstance(Class<?> cls)// 仅实例化基本类型的数组, 性能会比正常 new 来的更高些public Object allocateUninitializedArray(Class<?> componentType, int length) 复制代码常规实例化通过 new 关键字实例化。实例化过程会执行实例变量初始化、代码块的执行、构造函数;其次,当类定义了有参构造函数(不重新定义无参构造函数)以后,实例化时就必须指定参数;再者,例如单例的情况下,构造函数都是 private 的,是无法被访问到的。非常规实例化绕过实例构造方法 init(实例变量初始化、代码块、构造函数),JVM 的检查等,仅靠 Class 实例化对象。

public class InstanceDemo { Integer n1 = 1; Integer n2; Integer n3; { n2 = 2; } public InstanceDemo(int n3) { this.n3 = n3; } private InstanceDemo() { this.n3 = 3; } public static void main(String[] args) throws Exception { _unsafeInstance(); _newInstance(); } private static void _newInstance() { InstanceDemo demo = new InstanceDemo(3); System.out.println("n1 : " demo.n1); System.out.println("n2 : " demo.n2); System.out.println("n3 : " demo.n3); } private static void _unsafeInstance() throws Exception { Unsafe unsafe = UnsafeTest.reflectUnsafe(); InstanceDemo instance = (InstanceDemo) unsafe.allocateInstance(InstanceDemo.class); System.out.println("n1 : " instance.n1); System.out.println("n2 : " instance.n2); System.out.println("n3 : " instance.n3); }}// 结果://unsafen1 : nulln2 : nulln3 : null// newn1 : 1n2 : 2n3 : 3复制代码

new :


Unsafe:


jvm发出了 ldc , invokevirtual 以及强制类型转化检查的 checkcast 指令。


带有 Volatile 语义的put/get

public native Object getObjectVolatile(Object o, long offset);/** Acquire version of {@link #getObjectVolatile(Object, long)} */public final Object getObjectAcquire(Object o, long offset) { return getObjectVolatile(o, offset);}/** Opaque version of {@link #getObjectVolatile(Object, long)} */public final Object getObjectOpaque(Object o, long offset) { return getObjectVolatile(o, offset);} public native void putObjectVolatile(Object o, long offset, Object x);/** Opaque version of {@link #putObjectVolatile(Object, long, Object)} */public final void putObjectOpaque(Object o, long offset, Object x) { putObjectVolatile(o, offset, x);}复制代码

使用与 getObject / putObject 相同,只是带有 volatile 语义。即操作的内存会失效缓存。


同比,还有基本类型的操作方法。


有序延迟版本的put/get

//有序、延迟版本的putObjectVolatile方法,不保证值的改变被其他线程立即看到。// 只有在field被volatile修饰符修饰时有效public final void putObjectRelease(Object o, long offset, Object x) { putObjectVolatile(o, offset, x);}复制代码

同比,还有基本类型的操作方法。


6.CAS

/** * CAS * @param o 包含要修改field的对象 * @param offset 对象中某field的偏移量 * @param expected 期望值 * @param update 更新值 * @return true | false */public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);复制代码

什么是 CAS ? 即比较并替换,实现并发算法时常用到的一种技术。CAS 操作包含三个操作数——内存位置、预期原值及新值。


执行 CAS 操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。


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


相应的操作,都具有volatile读写的内存语义。


延伸出来的有:


对基本类型操作:getAndSet,同上;getAndAdd,同上。

7.内存屏障

/** * 确保屏障前的读,不会和屏障后的读写重排序:相当于一个 "读读"屏障 "读写"屏障 * 因为大部分都会需要"读写"屏障,所以不提供一个单一的"读读"屏障. */@HotSpotIntrinsicCandidatepublic native void loadFence();/** * 屏障前的读写,不会和屏障后的写重排序,相当于一个 "写写" "读写" * 因为大部分都会需要"读写"屏障,所以不提供一个单一的"写写"屏障. */@HotSpotIntrinsicCandidatepublic native void storeFence();/** * 屏障前的读写,不会和屏障后的的读写重排序: loadFence storeFence 写读 */@HotSpotIntrinsicCandidatepublic native void fullFence();public final void loadLoadFence() { // loadFence 额外带有 读读, 直接用 loadFence();}public final void storeStoreFence() { // storeFence 额外带有 写写, 直接用 storeFence();}复制代码

8.线程调度

线程挂起恢复

//取消阻塞线程public native void unpark(Object thread);//阻塞线程public native void park(boolean isAbsolute, long time);复制代码

从应用上来讲, LockSupport 会对其进行包装,划分出更多细粒度的方法以供使用。


常用的,肯定是并发包的相关类,比如 AQS、FutureTask、Exchanger等等。


低级同步原语

1.9 以后已经被移除了。


//获得对象锁(可重入锁)@Deprecatedpublic native void monitorEnter(Object o);//释放对象锁@Deprecatedpublic native void monitorExit(Object o);//尝试获取对象锁@Deprecatedpublic native boolean tryMonitorEnter(Object o);复制代码

总结

内容很丰富,理解很困难。


只要脑洞大,我觉得可以做很多事情。


但是还是建议,尽量不要使用。在规则内做好自己的事情。


原文链接:https://juejin.cn/post/6933078830336704520


如果觉得本文对你有帮助,可以转发关注支持一下


TAGS:
声明

1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;
2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;
3.作者投稿可能会经我们编辑修改或补充。

搜索
排行榜