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类。

 测试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结合以上两个方法,进行数组元素地址定位。

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);*/
    }
}

 

反射与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 系统信息

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

    //本地指针宽度,通常是4或8
    public native int addressSize();

    /**
     *内存页的大小,它总是2的幂次方
     */
    public native int pageSize();

 使用示例:

sun.nio.ch包下NativeObject类的addressSize()方法直接委托Unsafe.addressSize()实现

java.nio包下Bit类pageSize()方法:当pageSize非法时,将Unsafe.pageSize()作为返回值

 

 可以看出addressSize()、 pageSize()方法的调用者都是nio相关类,这是因为nio是直接使用JVM堆外的本地内存。

7 线程管理

 唤醒/休眠线程

    public native void unpark(Object thread);//唤醒

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

 使用示例:

以上两个方法是"等待/通知模型"的关键,它们的并发编程中常使用到的底层方法。以上两个方法主要被LockSupport类直接引用,LockSupport.parkUtil(long) 、 LockSupport.upark(Thread)方法中没有其他逻辑,就是直接委托以上两个方法实现的。

 

 抢锁与释放锁(已经被弃用)

   //获取锁对象
    @Deprecated
    public native void monitorEnter(Object o);
    //释放锁对象
    @Deprecated
    public native void monitorExit(Object o);
    //尝试获取锁对象
    @Deprecated
    public native boolean tryMonitorEnter(Object o);

 

 8 内存屏障

在Java 8中引入,用于定义内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序

    /**
     * 内存屏障,禁止load重排序。屏障前不能重排序load,且只能在屏障后load或store
     */
    public native void loadFence();

    /**
     * 内存屏障,禁止store重排序。 屏障前不能重排序store操作,且只能在屏障后load或store
     */
    public native void storeFence();

    /**
     * 内存屏障,禁止store load重排序。
     */
    public native void fullFence();

 

 使用示例:

 loadFence()方法在StampedLock的validate方法有使用到,StampedLock是为了防止CAS更新时出现ABA问题而在JDK1.8新引入的并发工具。

 

参考:《Java魔法类:Unsafe应用解析

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值