Java Unsafe 类

Wesley13
• 阅读 485

Unsafe类是啥?

Java最初被设计为一种安全的受控环境。尽管如此,Java HotSpot还是包含了一个“后门”,提供了一些可以直接操控内存和线程的低层次操作。这个后门类——sun.misc.Unsafe——被JDK广泛用于自己的包中,如java.nio和java.util.concurrent。但是丝毫不建议在生产环境中使用这个后门。因为这个API十分不安全、不轻便、而且不稳定。这个不安全的类提供了一个观察HotSpot JVM内部结构并且可以对其进行修改。有时它可以被用来在不适用C++调试的情况下学习虚拟机内部结构,有时也可以被拿来做性能监控和开发工具。

为什么叫Unsafe?

Java官方不推荐使用Unsafe类,因为官方认为,这个类别人很难正确使用,非正确使用会给JVM带来致命错误。而且未来Java可能封闭丢弃这个类。

如何使用Unsafe?

1. 获取Unsafe实例:

通读Unsafe源码,Unsafe提供了一个私有的静态实例,并且通过检查classloader是否为null来避免java程序直接使用unsafe:

//Unsafe源码
private static final Unsafe theUnsafe;
@CallerSensitive
public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    if(var0.getClassLoader() != null) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}

我们可以通过如下代码反射获取Unsafe静态类:

/** * 获取Unsafe */
Field f = null;
Unsafe unsafe = null;
try {
    f = Unsafe.class.getDeclaredField("theUnsafe");
    f.setAccessible(true);
    unsafe = (Unsafe) f.get(null);
} catch (NoSuchFieldException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
}

2. 通过Unsafe分配使用堆外内存:

C++中有malloc,realloc和free方法来操作内存。在Unsafe类中对应为:

//分配var1字节大小的内存,返回起始地址偏移量
public native long allocateMemory(long var1);
//重新给var1起始地址的内存分配长度为var3字节大小的内存,返回新的内存起始地址偏移量
public native long reallocateMemory(long var1, long var3);
//释放起始地址为var1的内存
public native void freeMemory(long var1);

分配内存方法还有重分配内存方法都是分配的堆外内存,返回的是一个long类型的地址偏移量。这个偏移量在你的Java程序中每块内存都是唯一的。
举例:

/** * 在堆外分配一个byte */
long allocatedAddress = unsafe.allocateMemory(1L);
unsafe.putByte(allocatedAddress, (byte) 100);
byte shortValue = unsafe.getByte(allocatedAddress);
System.out.println(new StringBuilder().append("Address:").append(allocatedAddress).append(" Value:").append(shortValue));
/** * 重新分配一个long */
allocatedAddress = unsafe.reallocateMemory(allocatedAddress, 8L);
unsafe.putLong(allocatedAddress, 1024L);
long longValue = unsafe.getLong(allocatedAddress);
System.out.println(new StringBuilder().append("Address:").append(allocatedAddress).append(" Value:").append(longValue));
/** * Free掉,这个数据可能脏掉 */
unsafe.freeMemory(allocatedAddress);
longValue = unsafe.getLong(allocatedAddress);
System.out.println(new StringBuilder().append("Address:").append(allocatedAddress).append(" Value:").append(longValue));

输出:

Address:46490464 Value:100
Address:46490480 Value:1024
Address:46490480 Value:22

3. 操作类对象

我们可以通过Unsafe类来操作修改某一field。原理是首先获取对象的基址(对象在内存的偏移量起始地址)。之后获取某个filed在这个对象对应的类中的偏移地址,两者相加修改。

 /** * 获取类的某个对象的某个field偏移地址 */
try {
    f = SampleClass.class.getDeclaredField("i");
} catch (NoSuchFieldException e) {
    e.printStackTrace();
}
long iFiledAddressShift = unsafe.objectFieldOffset(f);
SampleClass sampleClass = new SampleClass();
//获取对象的偏移地址,需要将目标对象设为辅助数组的第一个元素(也是唯一的元素)。由于这是一个复杂类型元素(不是基本数据类型),它的地址存储在数组的第一个元素。然后,获取辅助数组的基本偏移量。数组的基本偏移量是指数组对象的起始地址与数组第一个元素之间的偏移量。
Object helperArray[]    = new Object[1];
helperArray[0]      = sampleClass;
long baseOffset     = unsafe.arrayBaseOffset(Object[].class);
long addressOfSampleClass    = unsafe.getLong(helperArray, baseOffset);
int i = unsafe.getInt(addressOfSampleClass + iFiledAddressShift);
System.out.println(new StringBuilder().append(" Field I Address:").append(addressOfSampleClass).append("+").append(iFiledAddressShift).append(" Value:").append(i));

输出:

Field I Address:3610777760+24 Value:5

4. 线程挂起和恢复

将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。整个并发框架中对线程的挂起操作被封装在 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 parkUntil(Object blocker, long deadline) {  
        Thread t = Thread.currentThread();  
        setBlocker(t, blocker);  
        unsafe.park(true, deadline);  
        setBlocker(t, null);  
    }  

    public static void park() {  
        unsafe.park(false, 0L);  
    }  

    public static void parkNanos(long nanos) {  
        if (nanos > 0)  
            unsafe.park(false, nanos);  
    }  

    public static void parkUntil(long deadline) {  
        unsafe.park(true, deadline);  
    }  
}  

5. CAS操作

/** * 比较obj的offset处内存位置中的值和期望的值,如果相同则更新。此更新是不可中断的。 * * @param obj 需要更新的对象 * @param offset obj中整型field的偏移量 * @param expect 希望field中存在的值 * @param update 如果期望值expect与field的当前值相同,设置filed的值为这个新值 * @return 如果field的值被更改返回true */  
public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);  

6. Clone

如何实现浅克隆?在clone(){…}方法中调用super.clone(),对吗?这里存在的问题是首先你必须继续Cloneable接口,并且在所有你需要做浅克隆的对象中实现clone()方法,对于一个懒懒的程序员来说,这个工作量太大了。
我不推荐上面的做法而是直接使用Unsafe,我们可以仅使用几行代码就实现浅克隆,并且它可以像某些工具类一样用于任意类的克隆。
首先,我们需要一个计算Object大小的工具类:

class ObjectInfo {
    /** * Field name */
    public final String name;
    /** * Field type name */
    public final String type;
    /** * Field data formatted as string */
    public final String contents;
    /** * Field offset from the start of parent object */
    public final int offset;
    /** * Memory occupied by this field */
    public final int length;
    /** * Offset of the first cell in the array */
    public final int arrayBase;
    /** * Size of a cell in the array */
    public final int arrayElementSize;
    /** * Memory occupied by underlying array (shallow), if this is array type */
    public final int arraySize;
    /** * This object fields */
    public final List<ObjectInfo> children;

    public ObjectInfo(String name, String type, String contents, int offset, int length, int arraySize,
                      int arrayBase, int arrayElementSize) {
        this.name = name;
        this.type = type;
        this.contents = contents;
        this.offset = offset;
        this.length = length;
        this.arraySize = arraySize;
        this.arrayBase = arrayBase;
        this.arrayElementSize = arrayElementSize;
        children = new ArrayList<ObjectInfo>(1);
    }

    public void addChild(final ObjectInfo info) {
        if (info != null)
            children.add(info);
    }

    /** * Get the full amount of memory occupied by a given object. This value may be slightly less than * an actual value because we don't worry about memory alignment - possible padding after the last object field. * <p/> * The result is equal to the last field offset + last field length + all array sizes + all child objects deep sizes * * @return Deep object size */
    public long getDeepSize() {
        //return length + arraySize + getUnderlyingSize( arraySize != 0 );
        return addPaddingSize(arraySize + getUnderlyingSize(arraySize != 0));
    }

    long size = 0;

    private long getUnderlyingSize(final boolean isArray) {
        //long size = 0;
        for (final ObjectInfo child : children)
            size += child.arraySize + child.getUnderlyingSize(child.arraySize != 0);
        if (!isArray && !children.isEmpty()) {
            int tempSize = children.get(children.size() - 1).offset + children.get(children.size() - 1).length;
            size += addPaddingSize(tempSize);
        }

        return size;
    }

    private static final class OffsetComparator implements Comparator<ObjectInfo> {
        @Override
        public int compare(final ObjectInfo o1, final ObjectInfo o2) {
            return o1.offset - o2.offset; //safe because offsets are small non-negative numbers
        }
    }

    //sort all children by their offset
    public void sort() {
        Collections.sort(children, new OffsetComparator());
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        toStringHelper(sb, 0);
        return sb.toString();
    }

    private void toStringHelper(final StringBuilder sb, final int depth) {
        depth(sb, depth).append("name=").append(name).append(", type=").append(type)
                .append(", contents=").append(contents).append(", offset=").append(offset)
                .append(", length=").append(length);
        if (arraySize > 0) {
            sb.append(", arrayBase=").append(arrayBase);
            sb.append(", arrayElemSize=").append(arrayElementSize);
            sb.append(", arraySize=").append(arraySize);
        }
        for (final ObjectInfo child : children) {
            sb.append('\n');
            child.toStringHelper(sb, depth + 1);
        }
    }

    private StringBuilder depth(final StringBuilder sb, final int depth) {
        for (int i = 0; i < depth; ++i)
            sb.append("\t");
        return sb;
    }

    private long addPaddingSize(long size) {
        if (size % 8 != 0) {
            return (size / 8 + 1) * 8;
        }
        return size;
    }

}

class ClassIntrospector {

    private static final Unsafe unsafe;
    /** * Size of any Object reference */
    private static final int objectRefSize;

    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);

            objectRefSize = unsafe.arrayIndexScale(Object[].class);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /** * Sizes of all primitive values */
    private static final Map<Class, Integer> primitiveSizes;

    static {
        primitiveSizes = new HashMap<Class, Integer>(10);
        primitiveSizes.put(byte.class, 1);
        primitiveSizes.put(char.class, 2);
        primitiveSizes.put(int.class, 4);
        primitiveSizes.put(long.class, 8);
        primitiveSizes.put(float.class, 4);
        primitiveSizes.put(double.class, 8);
        primitiveSizes.put(boolean.class, 1);
    }

    /** * Get object information for any Java object. Do not pass primitives to * this method because they will boxed and the information you will get will * be related to a boxed version of your value. * * @param obj Object to introspect * @return Object info * @throws IllegalAccessException */
    public ObjectInfo introspect(final Object obj)
            throws IllegalAccessException {
        try {
            return introspect(obj, null);
        } finally { // clean visited cache before returning in order to make
            // this object reusable
            m_visited.clear();
        }
    }

    // we need to keep track of already visited objects in order to support
    // cycles in the object graphs
    private IdentityHashMap<Object, Boolean> m_visited = new IdentityHashMap<Object, Boolean>(
            100);

    private ObjectInfo introspect(final Object obj, final Field fld)
            throws IllegalAccessException {
        // use Field type only if the field contains null. In this case we will
        // at least know what's expected to be
        // stored in this field. Otherwise, if a field has interface type, we
        // won't see what's really stored in it.
        // Besides, we should be careful about primitives, because they are
        // passed as boxed values in this method
        // (first arg is object) - for them we should still rely on the field
        // type.
        boolean isPrimitive = fld != null && fld.getType().isPrimitive();
        boolean isRecursive = false; // will be set to true if we have already
        // seen this object
        if (!isPrimitive) {
            if (m_visited.containsKey(obj))
                isRecursive = true;
            m_visited.put(obj, true);
        }

        final Class type = (fld == null || (obj != null && !isPrimitive)) ? obj
                .getClass() : fld.getType();
        int arraySize = 0;
        int baseOffset = 0;
        int indexScale = 0;
        if (type.isArray() && obj != null) {
            baseOffset = unsafe.arrayBaseOffset(type);
            indexScale = unsafe.arrayIndexScale(type);
            arraySize = baseOffset + indexScale * Array.getLength(obj);
        }

        final ObjectInfo root;
        if (fld == null) {
            root = new ObjectInfo("", type.getCanonicalName(), getContents(obj,
                    type), 0, getShallowSize(type), arraySize, baseOffset,
                    indexScale);
        } else {
            final int offset = (int) unsafe.objectFieldOffset(fld);
            root = new ObjectInfo(fld.getName(), type.getCanonicalName(),
                    getContents(obj, type), offset, getShallowSize(type),
                    arraySize, baseOffset, indexScale);
        }

        if (!isRecursive && obj != null) {
            if (isObjectArray(type)) {
                // introspect object arrays
                final Object[] ar = (Object[]) obj;
                for (final Object item : ar)
                    if (item != null)
                        root.addChild(introspect(item, null));
            } else {
                for (final Field field : getAllFields(type)) {
                    if ((field.getModifiers() & Modifier.STATIC) != 0) {
                        continue;
                    }
                    field.setAccessible(true);
                    root.addChild(introspect(field.get(obj), field));
                }
            }
        }

        root.sort(); // sort by offset
        return root;
    }

    // get all fields for this class, including all superclasses fields
    private static List<Field> getAllFields(final Class type) {
        if (type.isPrimitive())
            return Collections.emptyList();
        Class cur = type;
        final List<Field> res = new ArrayList<Field>(10);
        while (true) {
            Collections.addAll(res, cur.getDeclaredFields());
            if (cur == Object.class)
                break;
            cur = cur.getSuperclass();
        }
        return res;
    }

    // check if it is an array of objects. I suspect there must be a more
    // API-friendly way to make this check.
    private static boolean isObjectArray(final Class type) {
        if (!type.isArray())
            return false;
        if (type == byte[].class || type == boolean[].class
                || type == char[].class || type == short[].class
                || type == int[].class || type == long[].class
                || type == float[].class || type == double[].class)
            return false;
        return true;
    }

    // advanced toString logic
    private static String getContents(final Object val, final Class type) {
        if (val == null)
            return "null";
        if (type.isArray()) {
            if (type == byte[].class)
                return Arrays.toString((byte[]) val);
            else if (type == boolean[].class)
                return Arrays.toString((boolean[]) val);
            else if (type == char[].class)
                return Arrays.toString((char[]) val);
            else if (type == short[].class)
                return Arrays.toString((short[]) val);
            else if (type == int[].class)
                return Arrays.toString((int[]) val);
            else if (type == long[].class)
                return Arrays.toString((long[]) val);
            else if (type == float[].class)
                return Arrays.toString((float[]) val);
            else if (type == double[].class)
                return Arrays.toString((double[]) val);
            else
                return Arrays.toString((Object[]) val);
        }
        return val.toString();
    }

    // obtain a shallow size of a field of given class (primitive or object
    // reference size)
    private static int getShallowSize(final Class type) {
        if (type.isPrimitive()) {
            final Integer res = primitiveSizes.get(type);
            return res != null ? res : 0;
        } else
            return objectRefSize;
    }
}

我们通过这两个类计算一个Object的大小,通过Unsafe的 public native void copyMemory(Object var1, long var2, Object var4, long var5, long var7)方法来拷贝:
两个工具方法:

private static Object helperArray[] = new Object[1];
/** * 获取对象起始位置偏移量 * @param unsafe * @param object * @return */
public static long getObjectAddress(Unsafe unsafe, Object object){
    helperArray[0] = object;
    long baseOffset = unsafe.arrayBaseOffset(Object[].class);
    return unsafe.getLong(helperArray, baseOffset);
}

private final static ClassIntrospector ci = new ClassIntrospector();

/** * 获取Object的大小 * @param object * @return */
public static long getObjectSize(Object object){
    ObjectInfo res = null;
    try {
        res = ci.introspect(object);
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    return res.getDeepSize();
}

测试:

SampleClass sampleClass = new SampleClass();
sampleClass.setI(999);
sampleClass.setL(999999999L);

SampleClass sampleClassCopy = new SampleClass();
long copyAddress = getObjectAddress(unsafe,sampleClassCopy);
unsafe.copyMemory(sampleClass, 0, null,copyAddress, getObjectSize(sampleClass));
i = unsafe.getInt(copyAddress + iFiledAddressShift);
System.out.println(i);
System.out.println(sampleClassCopy.getL());

输出:

999
999999999
点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
2年前
Java日期时间API系列31
  时间戳是指格林威治时间1970年01月01日00时00分00秒起至现在的总毫秒数,是所有时间的基础,其他时间可以通过时间戳转换得到。Java中本来已经有相关获取时间戳的方法,Java8后增加新的类Instant等专用于处理时间戳问题。 1获取时间戳的方法和性能对比1.1获取时间戳方法Java8以前
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这