Java之Unsafe-越迷人的越危险

·  阅读 388
Java之Unsafe-越迷人的越危险

「这是我参与11月更文挑战的第29天,活动详情查看:2021最后一次更文挑战

简要介绍:

Java语言先比较与C和C++有一个非常大的不同点在于Java语言无法直接操作内存,实际开发中,默认都是由JVM来进行内存分配和垃圾回收,而JVM在进行垃圾回收的时候,绝大多数垃圾回收器都需要STW(stop the world)这个问题往往会导致服务短暂或者较长时间的暂停。因此Unsafe提供了通过Java直接操作内存的API,尽管Unsafe是JavaNIO和并发的核心类,但是其如其名,这是一个官方不推荐开发者使用的及其不安全的类!

主要作用:

序号作用API
1内存管理。(包括分配内存、释放内存等。)allocateMemory(分配内存)、reallocateMemory(重新分配内存)、copyMemory(拷贝内存)、freeMemory(释放内存 )、getAddress(获取内存地址)、addressSize、pageSize、getInt(获取内存地址指向的整数)、getIntVolatile(获取内存地址指向的整数,并支持volatile语义)、putInt(将整数写入指定内存地址)、putIntVolatile(将整数写入指定内存地址,并支持volatile语义)、putOrderedInt(将整数写入指定内存地址、有序或者有延迟的方法)
2非常规的对象实例化allocateInstance()方法提供了另一种创建实例的途径
3操作类、对象、变量staticFieldOffset(静态域偏移)、defineClass(定义类)、defineAnonymousClass(定义匿名类)、ensureClassInitialized(确保类初始化)、objectFieldOffset(对象域偏移)
4数组操作arrayBaseOffset(获取数组第一个元素的偏移地址)、arrayIndexScale(获取数组中元素的增量地址)等方法
5多线程同步。包括锁机制、CAS操作等monitorEnter、tryMonitorEnter、monitorExit、compareAndSwapInt、compareAndSwap
6挂起与恢复park、unpark
7内存屏障loadFence、storeFence、fullFence

一、获取Unsafe

源码-基于jdk1.8

/* 
 * 在Unsafe源码中限制了获取Unsafe的ClassLoader,如果这个方法调用实例不是由BootStrap类加载器加载的,则会报错
 * 因此,我们如果需要使用Unsafe类,可以通过反射的方式来获取。
 */
@CallerSensitive
public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    // 此处会判断ClassLoader是否为空,BootStrap由C语言编写,在Java中获取会返回null。
    if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}
复制代码

获取方式

/**
 * 反射获取Unsafe
 *
 * @return Unsafe
 */
public static final Unsafe getUnsafe() {

    Unsafe unsafe = null;
    try {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        unsafe = (Unsafe) theUnsafe.get(null);
    } catch (NoSuchFieldException | IllegalAccessException e) {
        e.printStackTrace();
    }
    return unsafe;
}
复制代码

二、操作方法

数组操作

package com.liziba.unsafe;

import sun.misc.Unsafe;

/**
 * <p>
 *      操作数组示例
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/5/23 13:33
 */
public class OperateArrayExample {


    /**
     *  1、public native int arrayBaseOffset(Class<?> var1); 获取数组第一个元素的偏移地址
     *  2、public native int arrayIndexScale(Class<?> var1); 获取数组中元素的增量地址
     *  3、public Object getObject(Object var1, int var2); 通过对象和地址偏移量获取元素
     */
    public static void operateArrayUseUnsafe() {
        // 测试数组
        String[] exampleArray = new String [] {"李" , "子", "捌"};

        Unsafe unsafe = UnsafeFactory.getUnsafe();
        // 获取数组的基本偏移量
        int baseOffset = unsafe.arrayBaseOffset(String[].class);
        System.out.println("String[] base offset is : " + baseOffset);

        // 获取数组中元素的增量地址
        int scale = unsafe.arrayIndexScale(String[].class);
        System.out.println("String[] index scale is : " + scale);

        // 获取数组中第n个元素 i = (baseOffset + (scale * n-1))
        System.out.println("third element is :" + unsafe.getObject(exampleArray, baseOffset + (scale * 2)));

        // 修改数组中第n个元素 i = (baseOffset + (scale * n-1))
        unsafe.putObject(exampleArray, baseOffset + scale * 2, "柒");
        System.out.println("third element is :" + unsafe.getObject(exampleArray, baseOffset + (scale * 2)));
    }

    public static void main(String[] args) {
        OperateArrayExample.operateArrayUseUnsafe();
    }

}
复制代码

输出结果

对象操作

package com.liziba.unsafe;

import com.liziba.unsafe.pojo.User;
import sun.misc.Unsafe;

import java.io.File;
import java.io.FileInputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

/**
 * <p>
 *      操作对象示例
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/5/24 20:40
 */
public class OperateObjectExample {

    /**
     *  1、public native Object allocateInstance(Class<?> var1); 分配内存
     *  2、public native Class<?> defineClass(String var1, byte[] var2, int var3, int var4, ClassLoader var5, ProtectionDomain var6); 方法定义一个类用于动态的创建类
     * @throws Exception
     */
    public static void operateObjectUseUnsafe() throws Exception{

        Unsafe unsafe = UnsafeFactory.getUnsafe();

        // 使用Unsafe的allocateInstance()方法,可以无需使用构造函数的情况下实例化对象
        User user = (User) unsafe.allocateInstance(User.class);
        user.setId(1);
        user.setName("李子捌");
        System.out.println(user);

        // 返回对象成员属性在内存中相对于对象在内存中地址的偏移量
        Field name = User.class.getDeclaredField("name");
        long fieldOffset = unsafe.objectFieldOffset(name);

        // 使用Unsafe的putXxx()方法,可以直接修改内存地址指向的数据(可以越过权限访问控制符)
        unsafe.putObject(user, fieldOffset, "李子柒");
        System.out.println(user);

        // 使用Unsafe在运行时通过.class文件,创建类
        File classFile = new File("E:\workspaceall\liziba-javap5\out\production\liziba-javap5\com\liziba\unsafe\pojo\User.class");
        FileInputStream fis = new FileInputStream(classFile);
        byte [] classContent = new byte[(int) classFile.length()];
        fis.read(classContent);
        Class<?> clazz = unsafe.defineClass(null, classContent, 0, classContent.length, null, null);
        Constructor<?> constructor = clazz.getDeclaredConstructor(int.class, String.class);
        System.out.println(constructor.newInstance(1, "李子玖"));

    }

    public static void main(String[] args) {
        try {
            OperateObjectExample.operateObjectUseUnsafe();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
复制代码

输出结果

内存操作

package com.liziba.unsafe;

import sun.misc.Unsafe;

/**
 * <p>
 *      內存地址操作示例
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/5/24 21:32
 */
public class OperateMemoryExample {

    /**
     * 1、public native long allocateMemory(long var1);  分配var1字节大小的内存,返回起始地址偏移量
     * 2、public native long reallocateMemory(long var1, long var3);  重新给var1起始地址的内存分配长度为var3字节的内存,返回新的内存起始地址偏移量
     * 3、public native void freeMemory(long var1); 释放起始地址为var1的地址
     *
     * 分配地址的方法还有重分配,都是分配在堆外内存,返回的是一个long类型的地址偏移量。这个偏移量在Java程序中的每一块内存都是唯一的
     *
     */
    public static void operateMemoryUseUnsafe() {

        Unsafe unsafe = UnsafeFactory.getUnsafe();
        // 申请分配8byte的内存
        long address = unsafe.allocateMemory(1L);
        // 初始化内存填充值
        unsafe.putByte(address, (byte)1);
        // 测试输出
        System.out.println(new StringBuilder().append("address: ").append(address).append(" byte value: ").append(unsafe.getByte(address)));

        // 重新分配一个地址
        long newAddress = unsafe.reallocateMemory(address, 8L);
        unsafe.putLong(newAddress, 8888L);
        System.out.println(new StringBuilder().append("address: ").append(newAddress).append(" long value: ").append(unsafe.getLong(newAddress)));

        // 释放地址,注意地址可能被其他使用
        unsafe.freeMemory(newAddress);
        System.out.println(new StringBuilder().append("address: ").append(newAddress).append(" long value: ").append(unsafe.getLong(newAddress)));

    }


    public static void main(String[] args) {
        OperateMemoryExample.operateMemoryUseUnsafe();
    }

}
复制代码

输出结果

CAS操作

package com.liziba.unsafe;

import com.liziba.unsafe.pojo.User;
import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * <p>
 *      CAS操作示例
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/5/24 22:18
 */
public class OperateCASExample {

    /**
     * CAS == compare and swap(比较并替换)
     * 当需要改变的值为期望值的时候,就替换为新的值,是原子(不可再分割)操作。Java中大量的并发框架底层使用到了CAS操作。
     * 优势:无锁操作,减少线程切换带来的开销
     * 缺点:CAS容易在并发的情况下失败从而引发性能问题,也存在ABA问题。
     *
     * Unsafe中提供了三个方法
     * 1、compareAndSwapInt
     * 2、compareAndSwapLong
     * 3、compareAndSwapObject
     *
     */
    public static void operateCASUseUnsafe() throws Exception {

        User user = new User(1, "李子捌");
        System.out.println("pre user value: " + user);

        Unsafe unsafe = UnsafeFactory.getUnsafe();
        Field id = user.getClass().getDeclaredField("id");
        Field name = user.getClass().getDeclaredField("name");
        // 获取ID字段的内存偏移量
        long idFieldOffset = unsafe.objectFieldOffset(id);
        // 获取name字段的内存偏移量
        long nameFieldOffset = unsafe.objectFieldOffset(name);
        // 如果ID的期望值是1,则修改为18  success
        unsafe.compareAndSwapInt(user, idFieldOffset, 1, 18);
        // 如果name的期望值是小荔枝,则修改为李子柒  fail
        unsafe.compareAndSwapObject(user, nameFieldOffset, "小荔枝", "李子柒");
        // 输出修改的user对象
        System.out.println("post user value: " + user);

    }


    public static void main(String[] args) {
        try {
            OperateCASExample.operateCASUseUnsafe();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


}
复制代码

输出结果

\

\

线程的挂起和恢复

/**
 * 查看Java的java.util.concurrent.locks.LockSupport源代码可以发现LockSupport类
 * 中有各种版本的pack方法但是最终都是通过调用Unsafe.park()方法实现的。
 */

public class LockSupport {

	public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }
    
    
    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }
    
    
    public static void parkNanos(Object blocker, long nanos) {
        if (nanos > 0) {
            Thread t = Thread.currentThread();
            setBlocker(t, blocker);
            UNSAFE.park(false, nanos);
            setBlocker(t, null);
        }
    }
    
    public static void parkNanos(long nanos) {
        if (nanos > 0)
            UNSAFE.park(false, nanos);
    }
    
    public static void parkUntil(Object blocker, long deadline) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(true, deadline);
        setBlocker(t, null);
    }
    
     public static void parkUntil(long deadline) {
        UNSAFE.park(true, deadline);
    }
}
复制代码

我们平时如何实现浅克隆?

  • 实现Closeable接口
  • 重写close()方法

一、Unsafe实现浅克隆

浅克隆工具类

package com.liziba.unsafe.clone;

import com.liziba.unsafe.UnsafeFactory;
import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;

/**
 * <p>
 *      浅克隆工具类
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/5/26 21:08
 */
public class ShallowCloneUtil {


    /**
     * 获取对象的内存地址
     *
     * @Description
     * Unsafe类没有提供直接获取实例对象内存地址的方法,但是可以通过以下方式间接获取。
     * 构建对象A,A包含了我们需要获取内存地址的B对象的引用,这样只有获取到A对象持有的B对象的引用地址,就可以知道B对象的地址了。
     * 我们可以通过Unsafe类获取内存地址的方法public native long getLong(Object var1, long var2)来获取;
     * 此处我们为了方便,通过数组Object[] 添加Object元素,持有Object的引用
     *
     * @return
     */
    public static Long getAddress(Object obj) {

        Object[] objects = new Object[] {obj};
        Unsafe unsafe = UnsafeFactory.getUnsafe();
        int arrayBaseOffset = unsafe.arrayBaseOffset(Object[].class);
        return unsafe.getLong(objects, arrayBaseOffset);
    }


    /**
     * 获取对象的大小
     *
     * @Dscription
     * Java中实例化一个对象时,JVM会在堆中分配非static的Field的内存,其他的static属性或者method在类加载期间或者JVM启动时已经存放在内存中。
     * 所以我们计算对象的大小的时候只需要求和Field的大小就行了,JVM分配内存时,单个实例对象中的Field内存是连续不断地,
     * 因此我们只需获取最大偏移量Filed的偏移量 + 最大偏移量Filed本身的大小即可
     *
     * Java中基本数据类型所占的字节数
     * byte/boolean     1 字节
     * char/short       2 字节
     * int/float        4 字节
     * long/double      8 字节
     * boolean 理论上占1/8字节,实际上按照1byte处理。
     * Java采用的是Unicode编码,每一个字节占8位,一个字节由8个二进制位组成。
     *
     * @param clazz
     * @return
     */
    public static Long size(Class clazz) {

        // 最后一个Filed的内存偏移量
        long maxOffset = 0;
        Class lastFiledClass = null;
        Unsafe unsafe = UnsafeFactory.getUnsafe();
        do {
            for (Field field : clazz.getDeclaredFields()) {
                if (!Modifier.isStatic(field.getModifiers())) {
                    long tmpOffset = unsafe.objectFieldOffset(field);
                    if (tmpOffset > maxOffset) {
                        maxOffset = tmpOffset;
                        lastFiledClass = field.getType();
                    }
                }
            }
        } while ((clazz = clazz.getSuperclass()) != null);
        // 最后一个Field本身的大小
        int lastFiledSize = (boolean.class.equals(lastFiledClass) || byte.class.equals(lastFiledClass)) ? 1 :
                                (short.class.equals(lastFiledClass) || char.class.equals(lastFiledClass)) ? 2 :
                                     (int.class.equals(lastFiledClass) || float.class.equals(lastFiledClass)) ? 4 :  8 ;

        return maxOffset + lastFiledSize;
    }


    /**
     * 申请一块固定大小的内存空间
     *
     * @Description
     * 通过Unsafe的public native long allocateMemory(long var1);申请一块内存空间。
     *
     * @param bytes  需要申请的内存大小
     * @return
     */
    public static Long allocateMemory(long bytes) {
        return UnsafeFactory.getUnsafe().allocateMemory(bytes);
    }


    /**
     * 从原对象内存地址srcAddr复制大小位size的内存到destAddr地址处
     *
     * @param srcAddr           源地址
     * @param destAddr          目标地址
     * @param size              复制内存大小
     */
    public static void copyMemory(long srcAddr, long destAddr, long size) {
        UnsafeFactory.getUnsafe().copyMemory(srcAddr, destAddr, size);
    }


    /**
     * Unsafe未提供直接读取内存转为Java对象的方法,但是可以通过新建一个包含T类型属性的对象将申请的内存地址赋值给T属性
     *
     * @param addr
     * @param <T>
     * @return
     */
    public static <T> T addressConvertObject(long addr) {

        Object[] objects = new Object[] {null};
        Unsafe unsafe = UnsafeFactory.getUnsafe();
        int baseOffset = unsafe.arrayBaseOffset(Object.class);
        unsafe.putLong(objects, baseOffset, addr);
        return (T)objects[0];
    }


    /**
     * 实现对象的浅克隆
     *
     * @Description
     * 数组的无法通过此方法实现克隆,数组类是在JVM运行时动态生成的
     *
     * @param t
     * @param <T>
     * @return
     */
    public static <T> T shallowClone(T t) {
        Class<?> clazz = t.getClass();
        if (clazz.isArray()) {
            Object[] objects = (Object[]) t;
            return (T) Arrays.copyOf(objects, objects.length);
        }
        Long srcAddr = getAddress(t);
        Long size = size(clazz);
        Long destAddr = allocateMemory(size);
        copyMemory(srcAddr, destAddr, size);
        return addressConvertObject(destAddr);
    }


}
复制代码

测试

/**
 * <p>
 *      浅克隆测试
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/5/24 23:11
 */
public class ShallowCloneTest {

    public static void main(String[] args){

        SimpleInfo simpleInfo = new SimpleInfo();
        simpleInfo.setId(1);
        System.out.println(simpleInfo.hashCode());
        SimpleInfo clone = ShallowCloneUtil.shallowClone(simpleInfo);
        System.out.println(clone.hashCode());
    }

}
复制代码

更完善的方法

blog.csdn.net/zhxdick/art…

分类:
后端
标签: