Java之Unsafe技术揭秘


Java与C++最主要的区别是 无法直接操作内存卡,包括申请内存和释放内存。但是,jre rt.jar包却悄然提供了 Unsafe类,让Java拥有C++低层次内存、线程操作能力,可以认为 Unsafe是Java 留下的后门
Unsafe可坐落于jre/lib/rt.jar包,全类名 sun.misc.UnsafeUnsafe不属于Java标准,官方也不推荐使用;然而,很多Java基础操作,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Hadoop和Kafka等。主要原因是Unsafe可以直接访问系统内存资源并进行自主管理,极大提升运行效率和增强Java底层操作能力。

一、概述

Unsafe提供的API大都是native方法,主要包括以下几类方法操作:

  • Class类:Class类和类静态属性;
  • Object对象:Object对象和对象属性;
  • Array对象:数组和数组元素;
  • 直接内存操作:直接内存读写操作;
  • 并发相关:低级别同步原语,如CAS、线程调度、volatile、内存屏障等;
  • 系统信息:返回某些低级别的内存信息,如地址大小、内存页大小。

二、获取Unsafe对象

查看Unsafe源码,Unsafe是单例设计模式设计的类,既然官网不推荐使用,那么可以解释为什么UnsafetheSafe字段由private修饰了。Unsafe提供getUnsafe方法获取实例,但是限定调用的类是又BootClassLoader加载,所以反射方式成为获取Unsafe实例的最佳路径。

public final class Unsafe {
	/** 类实例对象 **/
    private static final Unsafe theUnsafe;
    
    @CallerSensitive
    public static Unsafe getUnsafe() {
    	// 反射获取调用者类对象
        Class var0 = Reflection.getCallerClass();
        // 类加载器判断,限定类加载器必须是BootClassLoader
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }
}

示例:

private static Unsafe getUnsafe(){
    try {
    	// 通过反射机制获取Unsafe类theUnsafe属性的Field对象
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        // 设置访问权限
        field.setAccessible(true);
        // 获取Field对应的实例对象
        Unsafe unsafe = (Unsafe) field.get(null);
        return unsafe;
    } catch (Exception e) {
        // TODO
    }
    
    return null;
}

三、内存操作

Unsafe类中提供的3个本地方法allocateMemoryreallocateMemoryfreeMemory分别用于分配内存,扩充内存和释放内存,与C语言中的3个方法对应。

3.1 直接内存分配

Unsafe提供了native long allocateMemory(long bytes)方法用于分配给定字节大小的本地内存块,并返回一个非零内存地址,如果分配失败会抛出java.lang.OutOfMemoryError异常。其中,申请的内存块是未被初始化的,也就是内容是Undefined,此时如果调用方法只能获得内存地址值只会返回内存地址。可以通过调用{@link #freeMemory}释放该地址内存,或使用{@link #reallocateMemory}调整其大小。

import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class UnsafeSample
{
    public static void main(String[] args) throws Exception
    {
        // 获取Unsafe类theUnsafe属性的Field
        Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        // 设置访问权限
        theUnsafeField.setAccessible(true);
        // 获取Unsafe属性值
        Unsafe unsafe = (Unsafe) theUnsafeField.get(null);

        // Integer类型长度
        int intByteLength = Integer.BYTES;
        // 分配内存块大小, 分配的内存块必须是类型的整数倍
        int allocateMemorySize = intByteLength * 3;
        // 分配内存
        long memoryAddress = unsafe.allocateMemory(allocateMemorySize);

        // 内存地址数据赋值
        unsafe.putInt(memoryAddress, 15);
        unsafe.putInt(memoryAddress + intByteLength, 20);

        // 读取内存数据
        int value1 = unsafe.getInt(memoryAddress);
        int value2 = unsafe.getInt(memoryAddress + intByteLength);
        // 未被初始化, 返回内存地址
        int value3 = unsafe.getInt(memoryAddress + intByteLength * 2);

        // 打印数据
        System.out.printf("allocate value1=%d %d %d\n", value1, value2, value3);
    }
}

注释Unsafe提供了putXgetX方法,更新和获取直接内存地址数据,包括基本数据类型赋值和取值操作。如果内存地址没有初始化操作,方法返回操作地址值。方法申明如下:

/**
 * 对给定地址赋值
 * @param address 赋值地址
 * @param x 赋值地址值
 */
public native void putInt(long address, int x);
/**
 * 获取给定地址值
 * @param offset 给定地址偏移量,初始地址 + 数据类型位数
 */
public native int getInt(long offset);

注意Unsafe allocateMemory方法分配的是直接内存,不由JVM管理,必须手动管理分配的内存空间并将其释放。

3.2 重新分配内存

Unsafe提供public native long reallocateMemory(long address, long bytes)方法用于重新分配内存,并把数据从原内存缓冲区(address指向的地方)拷贝到新分配内存块中,返回新内存缓冲区地址。如果参数address=0,等同于allocateMemory方法的效果。

3.3 释放分配内存

Unsafe提供public native void freeMemory(long address)方法用于释放由allocateMemory和reallocateMemory两方法分配的内存空间,如果address=0不进行任何操作直接返回 。

四、类操作

Unsafe提供了对类、对象和数组的操作。其中,类对象包括Class和类静态属性,对象包括Object和对象属性,数组包括Array和数组元素。

4.1 类对象操作

Unsafe可以直接对类进行操作,包括类加载、类对象初始化,操作方法如下:

// 获取字段对应的类对象
public native Object staticFieldBase(Field f);

// 判断是否需要初始化一个类
public native boolean shouldBeInitialized(Class<?> clazz);
// 确保类被初始化
public native void ensureClassInitialized(Class<?> c);
// 定义一个类,可用于动态创建类
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);

4.2 创建类实例

Unsafe可以通过allocateMemory方法会创建类对象创建实例以及分配内存。
注意:调用allocateMemory方法,会检查并且初始化类对象(即class对象),然后为类对象创建实例并分配内存,但是不会调用类构造器,类对象实例仍旧由JVM管理。

import lombok.Data;
import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class UnsafeSample
{
    @Data
    static class User {
        private String name;
        private Integer age;
    }

    public static void main(String[] args) throws Exception
    {
        // 获取Unsafe类theUnsafe属性的Field
        Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        // 设置访问权限
        theUnsafeField.setAccessible(true);
        // 获取Unsafe属性值
        Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
        // 为类对象创建实例并分配内存
        User user = (User)unsafe.allocateInstance(User.class);
    }
}

4.3 类静态属性读写

Unsafe可以获取类对象静态属性内存地址偏移量,然后根据内存地址偏移量对静态属性进行修改。注意,操作无视熟悉的可见性。

/** 获取静态属性所在的内存地址 */
public native long staticFieldOffset(Field f);

示例

import com.google.common.collect.Lists;
import lombok.Data;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.util.List;

public class UnsafeSample
{
    @Data
    static class User {
        private static String UID;
    }

    public static void main(String[] args) 
    throws NoSuchFieldException, IllegalAccessException
    {
        // 获取Unsafe类theUnsafe属性的Field
        Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        // 设置访问权限
        theUnsafeField.setAccessible(true);

        // 获取Unsafe对象
        Unsafe unsafe = (Unsafe) theUnsafeField.get(null);

        // 创建对象
        User user = (User) unsafe.allocateInstance(User.class);

        // 获取字段
        Field uidField = user.getClass().getDeclaredField("UID");

        // 获取静态属性(static)内存地址偏移量
        long uidFieldAddress = unsafe.staticFieldOffset(uidField);
        // 设置属性值,包括静态属性值
        // 注意: user不能为空,否则会异常退出
        unsafe.putObject(user, uidFieldAddress, "USER_ID");
        // 获取值
        Object value = unsafe.getObject(user, uidFieldAddress);

        // 打印
        System.out.printf("uid: %s\n", value);
    }
}

注意:Unsafe中任何函数如果标注对基本类型(Int、Float和Boolean等)操作,不能自动转换为包装类,否则会抛出NullPointerException异常。

4.4 类对象属性读写

Unsafe可以获取类对象属性内存地址偏移量,然后根据内存地址偏移量对属性进行修改。注意,属性是否可见都可以进行操作。

/** 获取类对象属性所在的内存地址 */
public native long objectFieldOffset(Field f);

示例

import lombok.Data;
import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class UnsafeSample
{
    @Data
    static class User {
        private static String UID;

        private String name;
        private String[] array;
    }

    public static void main(String[] args) 
    throws NoSuchFieldException, IllegalAccessException
    {
        // 获取Unsafe类theUnsafe属性的Field
        Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        // 设置访问权限
        theUnsafeField.setAccessible(true);

        // 获取Unsafe对象
        Unsafe unsafe = (Unsafe) theUnsafeField.get(null);

        // 创建对象
        User user = (User) unsafe.allocateInstance(User.class);

        // 获取属性
        Field nameField = user.getClass().getDeclaredField("name");

        // 获取属性内存地址偏移量
        long nameFieldAddress = unsafe.objectFieldOffset(nameField);
        // 设置属性值
        unsafe.putObject(user, nameFieldAddress, "think");
        // 获取值
        Object value = unsafe.getObject(user, nameFieldAddress);

        // 打印
        System.out.printf("name: %s\n", value);
    }
}

4.4 对数组进行读写

Unsafe提供获取数组第一个元素地址偏移量和元素占用字节大小方法,获取数组地址偏移量进而对元素进行读写操作。

/**
 * 返回数组第一个元素地址偏移量
 * @param arrayClass 数字Class
 * @return 地址偏移量
 */
public native int arrayBaseOffset(Class<?> arrayClass);
// 基本类型可以通过静态属性获取其第一个偏移地址
public static final int ARRAY_BYTE_BASE_OFFSET
            = theUnsafe.arrayBaseOffset(byte[].class);

/**
 * 返回数组元素占用字节大小
 * @param arrayClass 数组Class
 * @return 数组元素占用字节大小
 */
public native int arrayIndexScale(Class<?> arrayClass);
// 基本类型可以通过静态属性获取其元素占用字节大小
public static final int ARRAY_BYTE_INDEX_SCALE
            = theUnsafe.arrayIndexScale(byte[].class);

示例

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class UnsafeSample
{
    private static Unsafe unsafe;
    
    static {
        try {
            // 获取Unsafe类theUnsafe属性的Field
            Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
            // 设置访问权限
            theUnsafeField.setAccessible(true);

            // 获取Unsafe实例
            unsafe = (Unsafe) theUnsafeField.get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {}
    }


    public static void main(String[] args)
    {
        // 数组
        int[] intArray = new int[3];
        // 获取Class对象
        Class<?> intArrayClass = intArray.getClass();

        // 获取第一个元素的偏移位置
        int intArrayOffset = unsafe.arrayBaseOffset(intArrayClass);

        // 获取元素占用字节数
        int intArrayByte = unsafe.arrayIndexScale(intArrayClass);


        for(int i=0; i<intArray.length; i++) {
            // 计算偏移位置
            long elementOffset = intArrayOffset + i * intArrayByte;
            // 更新数组位置元素值
            unsafe.putInt(intArray, elementOffset, i * 5);
        }

        System.out.print("int array: ");
        for(Integer e: intArray) {
            System.out.printf("%d\t",  e);
        }
    }
}

五、同步操作

Unsafe提供对线程相关操作,包括内存可见性和线程同步。

5.1 CAS

CAS(Compare And Swap)是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。注意,方法是通过native修饰,数据是内存可见的。

/**
 * 对比对象偏移地址数据是否和预期相同,如果相同则修改内存地址数据.
 * @param object 类对象
 * @param offset 偏移内存地址
 * @param expectedValue 期望值
 * @param newValue 新值
 * @return 修改成功返回True,否则返回False
 */
public final native boolean compareAndSwapObject(Object object, long offset,
       Object expectedValue, Object newValue);

/**
 * 获取对象内存偏移地址的数据,读取数据是内存可见的,和Java @volatitle修饰一致
 * @param object 对象
 * @param offset 偏移内存地址
 * @return 返回对象偏移内存地址值
 */
public native Object getObjectVolatile(Object object, long offset);
    
/**
 * 对象偏移内存地址数据增加值
 * @param object 对象
 * @param offset 偏移内存地址
 * @param delta 增加值
 * @return 返回旧数据
 */
public final int getAndAddInt(Object object, long offset, int delta) {
	int v;
	do {
		v = getIntVolatile(o, offset);
	} while (!compareAndSwapInt(o, offset, v, v + delta));
	return v;
}

    /**
     * 改变对象或者数组偏移内存地址值
     * @param object 对象、数组
     * @param offset 属性或者数组的偏移
     * @param newValue 新值
     * @return 返回旧数据
     * @since 1.8
     */
    public final int getAndSetInt(Object object, long offset, int newValue) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!compareAndSwapInt(o, offset, v, newValue));
        return v;
    }

注意:Unsafe中任何函数如果标注对基本类型(Int、Float和Boolean等)操作,那么不能自动转换为包装类,否则会抛出NullPointerException异常。
示例

import lombok.Data;
import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class UnsafeSample
{
    @Data
    static class User {
        private volatile int age;
    }

    public static void main(String[] args)
            throws NoSuchFieldException, IllegalAccessException,
                   InstantiationException
    {
        // 获取Unsafe类theUnsafe属性的Field
        Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        // 设置访问权限
        theUnsafeField.setAccessible(true);

        // 获取Unsafe实例
        Unsafe unsafe = (Unsafe) theUnsafeField.get(null);

        // 创建实例对象
        User user = (User) unsafe.allocateInstance(User.class);

        // 获取属性Field
        Field ageField = user.getClass().getDeclaredField("age");

        // 获取属性偏移地址
        long ageFieldOffset = unsafe.objectFieldOffset(ageField);

        // 设置数值
        unsafe.getAndSetInt(user, ageFieldOffset, 17);

        // CAS
        unsafe.compareAndSwapInt(user, ageFieldOffset, 17, 18);

        System.out.println("user = " + user);
    }
}

5.2 线程调度

jdk包中J.U.C底层实现引用到Unsafeparkunpark操作,也就是阻塞线程。注意,Unsafe实现的锁同样也是重入锁。

/**
 * 阻塞当前线程。如果isAbsolute=true,实现ms定时, 否则为ns定时。方法返回的条件:
 *    1. 如果time <= 0,则直接返回;
 *    2. 如果之前有调用unpark或interrupt方法,则park不会挂起直接返回;
 *    3. 如果未调用,则会挂起当前线程,直到到达时间或者收到唤醒信号;
 *    4. park未知原因调用出错则直接返回(一般不会出现)
 * @param isAbsolute 绝对时间标志。
 * @param time 阻塞时间
 */
public native void park(boolean isAbsolute, long time);

/**
 * 取消阻塞线程
 * @param thread 线程
 */
public native void unpark(Object thread);

/**
 * 获取对象锁,
 * @param object 锁对象
 */
@Deprecated
public native void monitorEnter(Object object);

/**
 * 释放对象锁
 * @param object 锁对象
 */
@Deprecated
public native void monitorExit(Object object);

/**
 * 尝试获取对象锁
 * @param object 锁对象
 * @return 成功获取锁对象返回true,否则返回false
 */
@Deprecated
public native boolean tryMonitorEnter(Object object);

5.3 volatile

java JMM模型,对基本类型(boolean、byte、char、short、int、long、float、double)和引用类型对象都存在可见性问题,下面代码将揭示解决方法。

/**
 * 使用volatile的加载语义,从对象指定偏移量处获取变量的引用
 * @param object 对象
 * @param offset 偏移位置
 * @return 返回对象属性引用
 */
public native Object getObjectVolatile(Object object, long offset);
 
/**
 * 使用volatile的存储语义,存储属性引用到对象的指定偏移量处
 * @param object 对象
 * @param offset 偏移位置
 * @param value 引用值
 */
public native void putObjectVolatile(Object object, long offset, Object value);

/**
 * 仅仅保证@volatile修饰字段的可见性
 * @param object 对象
 * @param offset 偏移位置
 * @param value 引用值
 */
public native void putOrderedObject(Object object, long offset, Object value);

5.4 内存屏障

Java 8引入内存屏障实现 ,主要用途是避免代码重排序

/** 禁止load前后代码操作重排序。*/
public native void loadFence();

/** 禁止store前后代码操作重排序 */
public native void storeFence();

/** 禁止load、store前后代码操作重排序 */
public native void fullFence();

六、系统信息

Unsafe提供一些方法用于获取系统信息,比如:

/**
 * 获取本地指针所占用的字节大小,值为4/8。
 */
public native int addressSize();

/**
 * 获取处理器平均负载,分别代表最近1、5和15分钟的平均值
 * @param loadavg 采用接收数组
 * @param nelems 采用样本数,取值为[1, 3]
 * @return 实际采用样本数,无法获取返回-1
public native int getLoadAverage(double[] loadavg, int nelems);
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值