Java底层魔术类Unsafe用法简述
Posted 蜀中孤鹰
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java底层魔术类Unsafe用法简述相关的知识,希望对你有一定的参考价值。
1 引子
Java中没有指针,不能直接对内存地址的变量进行控制,但Java提供了一个特殊的类Unsafe工具类来间接实现。Unsafe主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用 。正如其名字unsafe,直接去使用这个工具类是不安全的,它能直接在硬件层(内存上)修改访问变量,而无视各种访问修饰符的限制。它几乎所有的公共方法API都是本地方法,这些方法是使用C/C++方法实现的,它越过了虚拟机层面,直接在操作系统本地执行。因为这是一个底层类,如果在不了解其内部原理、未掌握其使用技巧的情况下,我们直接使用Unsafe类可能会造成一些意想不到或未知的错误,所以它被限制开发者直接使用,只能由JDK类库的维护者使用。如果您喜欢阅读JDK的源码,那么你会发现在各种并发工具类的内部常常见到这个类的踪影,它们经常通过这个类的一些方法根据相应内存地址在内存上直接CAS修改访问共享变量的值。
Unsafe类在Oracle的官方JDK中没有提供源码,我们只能通过IDEA的反编译工具看到反编译后的源代码,因此我们看不到方法注释。而只OpenJDK中带有所有JDK的源代码,这里使用OpenJDK作参考讲解材料。以下是OpenJDK中Unsafe的类注释
A collection of methods for performing low-level, unsafe operations. Although the class and all methods are public, use of this class is limited because only trusted code can obtain instances of it.
直译过来大致意思是:此类拥有一组用于执行低级,不安全操作的方法。 尽管此类和所有方法都是公共的,但是由于只有可信代码才能获取该类的实例,因此此类的使用受到限制。
可以看出构造方法被私有化,只能通过静态方法getUnsafe()才能获取此Unsafe单例对象,而此静态方法的使用也是受到限制的,只能由JDK中的其它类来调用,普通开发者使用此方法将抛出异常。
private Unsafe() {} private static final Unsafe theUnsafe = new Unsafe(); @CallerSensitive public static Unsafe getUnsafe() { Class<?> caller = Reflection.getCallerClass(); //调用者Class对象 if (!VM.isSystemDomainLoader(caller.getClassLoader())) //判断调用者的类加载器是否为系统类加载器 //不是JAVA_HOME/jre/lib目录下jar包中的类来调用此方法getUnsafe()就会抛出异常 throw new SecurityException("Unsafe"); return theUnsafe; }
此方法getUnsafe()上的注释也说:
为调用提供执行不安全操作的能力。返回的Unsafe对象应由调用方小心保护,因为它可用于在任意内存地址处读取和写入数据。 绝不能将其传递给不受信任的代码。此类中的大多数方法都是非常底层的,并且对应于少量的硬件指令(在典型的机器上)。 应鼓励编译器相应地优化这些方法,而不是使用Unsafe类来控制。
getUnsafe()要求JDK类库自身调用,当然将开发者可以将自己定义的类放在JDK系统类库中,但这种方式明显是不安全、不方便的,其可行性太低。倘若开发者的确需要使用Unsafe类,我们可以使用反射的方式获取Unsafe实例。
private static Unsafe getUnsafeByReflect() { try { Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); return (Unsafe) f.get(null); } catch (Exception e) { throw new Error(e); } }
使用反射方式,在开发者的classpath中获取到Unsafe实例
package com.aaxis; import java.lang.reflect.Field; import sun.misc.Unsafe; public class Student { private int stuId; private String name; private int age; private static final long STUID_OFFSET; private static final Unsafe UNSAFE = getUnsafeByReflect(); static { try { STUID_OFFSET = UNSAFE.objectFieldOffset(Student.class.getDeclaredField("stuId")); } catch (NoSuchFieldException | SecurityException e) { throw new Error(e); } } private static Unsafe getUnsafeByReflect() { try { Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); return (Unsafe) f.get(null); } catch (Exception e) { throw new Error(e); } } public static void unsafedPrintStuId() { Student student = new Student(34124, "小黄"); int stuId = UNSAFE.getInt(student, STUID_OFFSET); System.out.println(student.getName() + "学号:" + stuId); } public static void main(String[] args) { unsafedPrintStuId(); } //..... }
Unsafe类的主要功能如图:
2 Java对象相关操作
注意:因为反射中使用Field描述实例变量和静态变量,现在将实例变量和静态变量统称为字段。
获取字段相对偏移量
/** * 根据反射的字段f,获取相应实例变量的偏移量 * 此偏移量是实例变量的起始地址与对象的起始地址之差,对于一确定的java类,某字段与对象之间的起始地址之差是常数, * 静态变量的偏移量与此类似 */ public native long staticFieldOffset(Field f); //根据反射的字段f,获取相应静态变量的偏移量(静态变量的起始地址与相应静态区Klass对象起始地址之差) public native long objectFieldOffset(Field f);
这里提到了字段的偏移量,这与Java对象的内存布局有密切关系。Java对象由对象头和实际数据两部分组成。
下图中MarkWord包含对象的hashCode、锁信息、垃圾回收的分代信息等,占32/64位;Class Metadata Pointer表示一个此对象数据类型的Class对象(虚拟机中的Klass对象)的指针,占32/64位;ArrayLength是数组对象特有的内容,表示数组的长度,占32位。数组对象的实际数据是各个元素的值或引用,普通对象的实际数据是各实例字段的值或引用。另外为了快速内存分配、快速内存寻址、提高性能,Java语言规范要求Java对象要做内存对齐处理,每个对象占用的内存字节数必须是8的倍数,若不是则要填零补足对齐。
从下图可以看出,字段与对象头之间的偏移量是固定的,只要知道字段的相对偏移量和对象起始地址,我们就能获取此字段的绝对内存地址(fieldAddress=objAddress+fieldOffset),根据此绝对内存地址,我们就能忽略访问修饰符的限制而可直接读取/修改此字段的值或引用。
数组对象的元素内存定址,相对对于普通对象的字段定址有些不一样,它要先计算出对象头的长度,作为基础偏移量;由于数组元素的数据类型是相同的,每个元素的值或引用所占内存空间是相同的,因此将元素值或引用或占内存作为每两相邻元素的相对偏移量。根据对象起始位置、基础偏移量、相邻元素相对偏移量及数组下标,就可以获取到某个元素值或引用的绝对内存地址(itemAddress=arrayAddress+baseOffset+index*indexOffset),进而通过绝对内存地址读取或修改此元素的值或引用。
根据字段偏移量设置/获取字段值
//根据反射的字段f,获取相应的静态变量的值 public native Object staticFieldBase(Field f); /** *参数o是字段所属的对象,offset表示相对偏移量,参数x是此字段要设置的新值 */ /*字段是引用数据类型*/ public native Object getObject(Object o, long offset);//获取字段值 public native void putObject(Object o, long offset, Object x);//设置字段值 /*字段为基本数据类型*/ public native void putInt(Object o, long offset, int x); public native int getInt(Object o, long offset); public native boolean getBoolean(Object o, long offset); public native void putBoolean(Object o, long offset, boolean x); public native byte getByte(Object o, long offset); public native void putByte(Object o, long offset, byte x); public native short getShort(Object o, long offset); public native void putShort(Object o, long offset, short x); public native char getChar(Object o, long offset); public native void putChar(Object o, long offset, char x); public native long getLong(Object o, long offset); public native void putLong(Object o, long offset, long x); public native float getFloat(Object o, long offset); public native void putFloat(Object o, long offset, float x); public native double getDouble(Object o, long offset); public native void putDouble(Object o, long offset, double x);
使用示例:
我将一个自定义的普通(编译后的)Java类放在JDK类库的charset.jar包中,这个Student类使用了Unsafe类。
Student.class的部分反编译源码
测试Unsafe能否忽略访问限制,读取私有变量
package other; import sun.awt.Student; public class UnsafeTest { public static void main(String[] args) { Student.unsafedPrintStuId(); } }
控制台输出结果正确
volatile版本根据字段偏移量设置/获取字段值(加上volatile语义)
保证对其他线程的可见性(只有字段被volatile修饰时有效)
//volatile形式地获取字段值,即使在多线条件下,从主内存中获取值,使当前线程的工作内存的缓存值失效 public native Object getObjectVolatile(Object o, long offset); //volatile形式地设置字段值,即使在多线条件下,设置的值将只保存到主内存中,不加载到线程本地缓存,保证可见性 public native void putObjectVolatile(Object o, long offset, Object x); public native int getIntVolatile(Object o, long offset); public native void putIntVolatile(Object o, long offset, int x); public native boolean getBooleanVolatile(Object o, long offset); public native void putBooleanVolatile(Object o, long offset, boolean x); public native byte getByteVolatile(Object o, long offset); public native void putByteVolatile(Object o, long offset, byte x); public native short getShortVolatile(Object o, long offset); public native void putShortVolatile(Object o, long offset, short x); public native char getCharVolatile(Object o, long offset); public native void putCharVolatile(Object o, long offset, char x); public native long getLongVolatile(Object o, long offset); public native void putLongVolatile(Object o, long offset, long x); public native float getFloatVolatile(Object o, long offset); public native void putFloatVolatile(Object o, long offset, float x); public native double getDoubleVolatile(Object o, long offset); public native void putDoubleVolatile(Object o, long offset, double x);
有序延迟化地设置字段值
有序延迟化设值,对其他线程不保证可见性
//有序延迟化地设置字段值, public native void putOrderedObject(Object o, long offset, Object x); /** Ordered/Lazy version of {@link #putIntVolatile(Object, long, int)} */ public native void putOrderedInt(Object o, long offset, int x); /** Ordered/Lazy version of {@link #putLongVolatile(Object, long, long)} */ public native void putOrderedLong(Object o, long offset, long x);
数组相关的偏移量
//第一个元素与数组对象两者间起始地址之差(首元素与对象头的相对偏移量) public native int arrayBaseOffset(Class<?> arrayClass); //相邻元素间相对偏移量的位移表示(返回值的二进制形式的有效位数是x,那么相邻元素的偏移量就是2的x次方) public native int arrayIndexScale(Class<?> arrayClass);
使用示例:
java.util.concurrent.atomic.AtomicIntegerArray包下的AtomicIntegerArray结合以上两个方法,进行数组元素地址定位。
class AtomicIntegerArray implements java.io.Serializable { private static final long serialVersionUID = 2862133569453604235L; private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final int base = unsafe.arrayBaseOffset(int[].class); private static final int shift; private final int[] array; static { int scale = unsafe.arrayIndexScale(int[].class); if ((scale & (scale - 1)) != 0) throw new Error("data type scale not a power of two"); shift = 31 - Integer.numberOfLeadingZeros(scale); } private long checkedByteOffset(int i) { if (i < 0 || i >= array.length) throw new IndexOutOfBoundsException("index " + i); return byteOffset(i); } private static long byteOffset(int i) { return ((long) i << shift) + base; } public final void set(int i, int newValue) { unsafe.putIntVolatile(array, checkedByteOffset(i), newValue); } }
3 Class相关操作
创建Java类
/** * 让虚拟机知道我们定义一个类,但不进行安全检查。 * 默认情况下,类加载器和保护域来自调用者的类。 */ public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain); /* * 在类加载器和系统字典(system dictionary)不知道的情况下根据字节码数据定义一个匿名的Class对象,相当于创建了一个Java类 * @params hostClass context for linkage, access control, protection domain, and class loader * @params data 字节码文件对应的字节数组 * @params cpPatches where non-null entries exist, they replace corresponding CP entries in data */ public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
Java类初始化
shouldBeInitialized(Class)方法检测Class对应的Java类是否被初始化
ensureClassInitialized(Class)方法强制Java类初始化,若没初始化则进行初始化。
这两个方法常与staticFieldBase(Field)一起使用,因为如果Java类没有被初始化,静态变量便没有初始化,就不能直接获取静态变量的引用。
/** * Detect if the given class may need to be initialized. This is often * needed in conjunction with obtaining the static field base of a * class. * @return false only if a call to {@code ensureClassInitialized} would have no effect */ public native boolean shouldBeInitialized(Class<?> c); /** * Ensure the given class has been initialized. This is often * needed in conjunction with obtaining the static field base of a * class. */ public native void ensureClassInitialized(Class<?> c);
使用示例:
java.lang.invoke.DirectMethodHandle中的checkInitialized(MemberName)方法调用了以上两个与类初始化相关的方法
根据Class创建对象
仅通过Class对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM安全检查,等,。它抑制修饰符检测,也就是即使构造器是private修饰的也能通过此方法实例化,只需提类对象即可创建相应的对象 .
/** Allocate an instance but do not run any constructor. Initializes the class if it has not yet been. */ public native Object allocateInstance(Class<?> cls) throws InstantiationException;
使用示例:
Employe类的唯一构造方法被私有化,外界不能直接创建此类的对象。但通过"Constructor.setAccessible(true)"将私有构造器设为外部可访问,使用反射机制也能创建一个Employee对象。
package other; import sun.misc.Unsafe; import java.lang.reflect.Field; public class Employee { private static int count; private static long countL=1000; private long id; private String name; private int sex;// 1代表男性,0代表女性 private long mgrId=11111; static { count = 1000;//目前员工人数的基数 } private Employee() { sex = 1;//默认为男性 name = ""; count++; countL++; } @Override public String toString() { return "{Employee [id=" + id + ", name=" + name + ", sex=" + sex + ", mgrId=" + mgrId + "]}"+" ,{count="+count+", countLong="+countL+"}"; } } class EmployeeTest { private static final Unsafe UNSAFE; static { try { Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); UNSAFE = (Unsafe) f.get(null); } catch (Exception e) { throw new Error(e); } } public static void main(String[] args) throws Exception { Employee employee = (Employee) UNSAFE.allocateInstance(Employee.class); System.out.println(employee); /* Class<Employee> clazz = Employee.class; Constructor<Employee> constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); Employee emp = constructor.newInstance(); System.out.println(emp);*/ } }
两种方式创建的对象toString()信息
Unsafe创建的对象 | |
反射创建的对象 |
从上面的控制台输出信息可以看出,反射与Unsafe能均创建一个构造方法被私有化的对象。不同之处在于allocateInstance(Class)方法创建对象过程中不会进行对象初始化,但会进行类初始化;即不会执行实例变量初始化赋值、不执行构造代码块、不调用构造方法,但会执行静态变量的初始化赋值、执行静态代码块。
4 CAS更新操作
CAS是Java并发编程的最底层依据,它实现了非阻塞式地更新共享变量,自旋锁与乐观锁的实现均依赖它。
/** * CAS更新共享变量 * * @param o 字段所属对象 * @param offset 字段的相对偏移量 * @param expected 预期值 * @param x 更新值 * @return 更新成功则返回true */ public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x); public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x); public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);
使用示例:
同步器AQS的compareAndSetXxx()方法都直接委托上面的CAS方法实现的
5 内存操作
根据内存地址,设置/获取对应的值
/** * 参数address是绝对内存地址,参数x是设定的值 * 如果address是零或不是通过allocMemery()方法分配的地址,那么结果未定义 */ public native byte getByte(long address); public native void putByte(long address, byte x); /** @see #getByte(long) */ public native short getShort(long address); /** @see #putByte(long, byte) */ public native void putShort(long address, short x); /** @see #getByte(long) */ public native char getChar(long address); /** @see #putByte(long, byte) */ public native void putChar(long address, char x); /** @see #getByte(long) */ public native int getInt(long address); /** @see #putByte(long, byte) */ public native void putInt(long address, int x); /** @see #getByte(long) */ public native long getLong(long address); /** @see #putByte(long, byte) */ public native void putLong(long address, long x); /** @see #getByte(long) */ public native float getFloat(long address); /** @see #putByte(long, byte) */ public native void putFloat(long address, float x); /** @see #getByte(long) */ public native double getDouble(long address); /** @see #putByte(long, byte) */ public native void putDouble(long address, double x);
根据内存地址设置/获取指针
//根据内存地址获取一个指针 public native long getAddress(long address); //根据内存地址设置一个指针,adress是内存地址,x是指定的指针值 public native void putAddress(long address, long x);
分配、扩展、释放内存
//分配一块指定的内存空间,返回一个指向此内存起始位置的指针 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 void setMemory(long address, long bytes, byte value) { setMemory(null, address, bytes, value); } //将一处内存的数据复制另一处内存 public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes); public void copyMemory(long srcAddress, long destAddress, long bytes) { copyMemory(null, srcAddress, null, destAddress, bytes); } //释放内存 public native void freeMemory(long address);
使用示例:
java.nio包下的DirectByteBuffer类的构造方法调用Unsafe.allocateMemory(int)分配初始条件下的的内存缓冲区
DirectByteBuffer的静态内部类Deallocator的run()调用Unsafe.freeMemory(long)释放相应地址的内存空间
6 系统信息
获取指定宽度、内存页大小等系统软硬件信息,这些信息对于本地内存的分配、使用、寻址很重要。
//本地指针宽度,通常是4或8 public native int addressSize(); <以上是关于Java底层魔术类Unsafe用法简述的主要内容,如果未能解决你的问题,请参考以下文章
JVMday03类文件结构 字节码指令 多态的原理 异常 synchronized代码块底层原理