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();
    }
     //.....        
}
在classpath环境中使用Unsafe

 

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的部分反编译源码

Student部分代码

 

 测试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);
    }
}
AtomicIntegerArray部分代码

 

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);*/
    }
}
反射与unsafe创建对象

 

两种方式创建的对象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 系统信息

获取指定宽度、内存页大小等系统软硬件信息,这些信息对于本地内存的分配、使用、寻址很重要。