Java——》Unsafe源码分析

推荐链接:
    总结——》【Java】
    总结——》【Mysql】
    总结——》【Redis】
    总结——》【Kafka】
    总结——》【Spring】
    总结——》【SpringBoot】
    总结——》【MyBatis、MyBatis-Plus】

一、概念

并发作为 Java 中非常重要的一部分,其内部大量使用了 Unsafe 类,它为 java.util.concurrent 包中的类提供了底层支持。然而 Unsafe 并不是 JDK 的标准,它是 Sun 的内部实现,存在于 sun.misc 包中,在 Oracle 发行的 JDK 中并不包含其源代码。
虽然我们在一般的并发编程中不会直接用到 Unsafe,但是很多 Java 基础类库与诸如 Netty、Cassandra 和 Kafka 等高性能库都采用它,它在提升 Java 运行效率、增强 Java 语言底层操作能力方面起了很大作用。

二、作用

Unsafe 为调用者提供执行非安全操作的能力,由于返回的 Unsafe 对象可以读写任意的内存地址数据,调用者应该小心谨慎的使用改对象,一定不用把它传递到非信任代码。该类的大部分方法都是非常底层的操作,并牵涉到一小部分典型的机器都包含的硬件指令,编译器可以对这些进行优化。

  1. 绕过 JVM 直接修改内存(对象)
  2. 使用硬件 CPU 指令实现 CAS 原子操作

三、源码

本文使用 OpenJDK(jdk8u)中 Unsafe 的源码

1、获取实例

public final class Unsafe {
    // 本地静态对象,在静态块中初始化
    private static final Unsafe theUnsafe;

    // 本地静态方法,在静态块中执行
    private static native void registerNatives();
    
    // 静态块
    static {
        registerNatives();
        Reflection.registerMethodsToFilter(Unsafe.class, new String[]{"getUnsafe"});
        theUnsafe = new Unsafe();
    }
    
    // 私有构造函数,该类实例是单例的,不能实例化,只能通过 getUnsafe() 方法获取实例
    private Unsafe() {}

    // 静态单例方法,获取实例
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class<?> caller = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(caller.getClassLoader()))
            throw new SecurityException("Unsafe");
        return theUnsafe;
    }
}

1)@CallerSensitive

说明该方法的调用者不是由系统类加载器(bootstrap classloader)加载,则将抛出 SecurityException。所以默认情况下,应用代码调用此方法将抛出异常。

2)通过反射获取 Unsafe 实例

Field f= Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
U= (Unsafe) f.get(null);

此处通过反射获取类的静态字段,这样就绕过了 getUnsafe() 的安全限制。也可以通过反射获取构造方法再实例化,但这样违法了该类单例的原则,并且在使用上可能会有其它问题,所以不建议这样做。

2、获取指定变量的值

// 获取指定变量的值
public native int getInt(Object o, long offset);

// 获取指定字段的偏移地址
public native long objectFieldOffset(Field f);
// 获取指定静态字段的位置
public native Object staticFieldBase(Field f);
// 获取指定静态字段在该类的偏移地址
public native long staticFieldOffset(Field f);
// 获取指定数组类第一个元素在内存中的偏移地址
public native int arrayBaseOffset(Class arrayClass);
// 获取指定数组类的每个元素在内存中的所占用的字节
public native int arrayIndexScale(Class arrayClass);

3、更新指定变量的值

// 更新指定变量的值
// o		:java堆对象,可以为null
// offset	:指定变量在该对象中偏移地址,如果o为null则是内存的绝对地址
public native void putInt(Object o, long offset, int x);


// 以下代码和上面方法差不多,只是针对不同的数据类型
public native void 	  putObject(Object o, long offset, Object x);
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);

4、分配本地内存

提供了动态获取/释放本地方法区内存的功能

/**
 * 分配指定大小的一块本地内存。分配的这块内存不会初始化,它们的内容通常是没用的数据
 * 返回的本地指针不会是 0,并且该内存块是连续的。调用 freeMemory 方法可以释放此内存,调用
 * reallocateMemory 方法可以重新分配
 */
public native long allocateMemory(long bytes);

/**
 * 重新分配一块指定大小的本地内存,超出老内存块的字节不会被初始化,它们的内容通常是没用的数据
 * 当且仅当请求的大小为 0 时,该方法返回的本地指针会是 0。
 * 该内存块是连续的。调用 freeMemory 方法可以释放此内存,调用 reallocateMemory 方法可以重新分配
 * 参数 address 可以是 null,这种情况下会分配新内存(和 allocateMemory 一样)
 */
public native long reallocateMemory(long address, long bytes);

/** 
 * 将给定的内存块的所有字节设置成固定的值(通常是 0)
 * 该方法通过两个参数确定内存块的基准地址,就像在 getInt(Object,long) 中讨论的,它提供了 double-register 地址模型
 * 如果引用的对象是 null, 则 offset 会被当成绝对基准地址
 * 该写入操作是按单元写入的,单元的字节大小由地址和长度参数决定,每个单元的写入是原子性的。如果地址和长度都是 8 的倍数,则一个单元为 long
 * 型(一个单元 8 个字节);如果地址和长度都是 4 的倍数,则一个单元为 int 型(一个单元 4 个字节);
 * 如果地址和长度都是 2 的倍数,则一个单元为 short 型(一个单元 2 个字节);
 */
public native void setMemory(Object o, long offset, long bytes, byte value);

//将给定的内存块的所有字节设置成固定的值(通常是 0)
//就像在 getInt(Object,long) 中讨论的,该方法提供 single-register 地址模型
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);

//复制指定内存块的字节到另一内存块,但使用 single-register 地址模型
public void copyMemory(long srcAddress, long destAddress, long bytes) {
    copyMemory(null, srcAddress, null, destAddress, bytes);
}

//释放通过 allocateMemory 或者 reallocateMemory 获取的内存,如果参数 address 是 null,则不做任何处理
public native void freeMemory(long address);

1)Java函数对应的C函数

Java函数对应的C函数描述
allocateMemorymalloc用于分配一个全新的未使用的连续内存,但该内存不会初始化,即不会被清零
reallocateMemoryrealloc用于内存的缩容或扩容,有两个参数,从 malloc 返回的地址和要调整的大小,该函数和 malloc 一样,不会初始化,它能保留之前放到内存里的值,很适合用于扩容
freeMemoryfree用于释放内存,该方法只有一个地址参数,那它如何知道要释放多少个字节呢?其实在 malloc 分配内存的时候会多分配 4 个字节用于存放该块的长度,比如 malloc(10) 其实会花费 14 个字节。理论上讲能分配的最大内存是 4G(2^32-1)。在 hotspot 虚拟机的设计中,数组对象也有 4 个字节用于存放数组长度,那么在 hotspot 中,数组的最大长度就是 2^32-1,这样 free 函数只要读取前 4 个字节就知道要释放多少内存了(10+4)
setMemorymemset一般用于初始化内存,可以设置初始化内存的值,一般初始值会设置成 0,即清零操作

2)示例

// 申请内存
long address=U.allocateMemory(10);

// 写入内存
U.setMemory(address,10,(byte)1);
/**
 * 1的二进制码为00000001,int为四个字节,U.getInt将读取四个字节,
 * 读取的字节为00000001 00000001 00000001 00000001
 */
int i=0b00000001000000010000000100000001;

// 读取内存
System.out.println(i==U.getInt(address));

// 释放内存
U.freeMemory(address);

5、获取指针的大小

基本类型不是用指针表示的,它是直接存储的值。
一般情况下,我们会说在 Java 中,基本类型是值传递,对象是引用传递。
Java 官方的表述是在任何情况下 Java 都是值传递。
基本类型是传递值本身,对象类型是传递指针的值。

虚拟机指针大小
32 位4
64 位默认:8
压缩:4(-XX:-UseCompressedOops)

32 位虚拟机: 4
64 位虚拟机: 8(默认),开启指针压缩功能(**-XX:-UseCompressedOops)**4

/**
 * 获取本地指针所占用的字节大小,值为 4 或者 8。其他基本类型的大小由其内容决定
 */
public native int addressSize();
/** The value of {@code addressSize()} */
public static final int ADDRESS_SIZE = theUnsafe.addressSize();
/**
 * 本地内存页大小,值为 2 的 N 次方
 */
public native int pageSize();

6、CAS相关

这几个方法应该是最常用的方法了,用于实现原子性的 CAS 操作,这些操作可以避免加锁,一般情况下,性能会更好, java.util.concurrent 包下很多类就是用的这些 CAS 操作而没有用锁。

/**
 * 如果变量的值为预期值,则更新变量的值,该操作为原子操作
 * 如果修改成功则返回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);
    //下面的方法包含基于 CAS 的 Java 实现,用于不支持本地指令的平台
    /**
     * 在给定的字段或数组元素的当前值原子性的增加给定的值
     * @param o 字段/元素所在的对象/数组
     * @param offset 字段/元素的偏移
     * @param delta 需要增加的值
     * @return 原值
     * @since 1.8
     */
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!compareAndSwapInt(o, offset, v, v + delta));
        return v;
    }
    public final long getAndAddLong(Object o, long offset, long delta) {
        long v;
        do {
            v = getLongVolatile(o, offset);
        } while (!compareAndSwapLong(o, offset, v, v + delta));
        return v;
    }

    /**
     * 将给定的字段或数组元素的当前值原子性的替换给定的值
     * @param o 字段/元素所在的对象/数组
     * @param offset 字段/元素的偏移
     * @param newValue 新值
     * @return 原值
     * @since 1.8
     */
    public final int getAndSetInt(Object o, long offset, int newValue) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!compareAndSwapInt(o, offset, v, newValue));
        return v;
    }
    public final long getAndSetLong(Object o, long offset, long newValue) {
        long v;
        do {
            v = getLongVolatile(o, offset);
        } while (!compareAndSwapLong(o, offset, v, newValue));
        return v;
    }
    public final Object getAndSetObject(Object o, long offset, Object newValue) {
        Object v;
        do {
            v = getObjectVolatile(o, offset);
        } while (!compareAndSwapObject(o, offset, v, newValue));
        return v;
    }

7、线程相关

阻塞和释放当前线程,java.util.concurrent 中的锁就是通过这两个方法实现线程阻塞和释放的。

/**
* 释放当前阻塞的线程。如果当前线程没有阻塞,则下一次调用 park 不会阻塞。这个操作是"非安全"的
* 是因为调用者必须通过某种方式保证该线程没有被销毁
*
*/
public native void unpark(Object thread);

/**
* 阻塞当前线程,当发生如下情况时返回:
* 1、调用 unpark 方法
* 2、线程被中断
* 3、时间过期
* 4、spuriously
* 该操作放在 Unsafe 类里没有其它意义,它可以放在其它的任何地方
*/
public native void park(boolean isAbsolute, long time);

8、CPU相关

/**
 *获取一段时间内,运行的任务队列分配到可用处理器的平均数(平常说的 CPU 使用率)
 *
 */
public native int getLoadAverage(double[] loadavg, int nelems);

9、内存屏障相关

这是实现内存屏障的几个方法,类似于 volatile 的语意,保证内存可见性和禁止重排序。这几个方法涉及到 JMM(Java 内存模型)。

//确保该栏杆前的读操作不会和栏杆后的读写操作发生重排序
public native void loadFence();

//确保该栏杆前的写操作不会和栏杆后的读写操作发生重排序
public native void storeFence();

//确保该栏杆前的读写操作不会和栏杆后的读写操作发生重排序
public native void fullFence();

//抛出非法访问错误,仅用于VM内部
private static void throwIllegalAccessError() {
    throw new IllegalAccessError();
}

版权声明:本文为weixin_43453386原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。