手把手教你用代码实现 Unsafe 的 9 大使用场景

JAVA herman 224浏览
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:xttblog,发送下载链接帮助你免费下载!
本博客日IP超过1800,PV 2600 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog,之前的微信号好友位已满,备注:返现
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领

Unsafe 这个类是你用就觉得好用的类。今天我借助它,边介绍它的使用场景便给大家讲实现。

Unsafe 的8大主要功能

使用场景一:Unsafe 可以用在避免类初始化的场景。也就是不需要执行类构造函数的场景,跳过对象初始化阶段,或绕过构造器的安全检查,或实例化一个没有任何公共构造器的类。看下面的代码:

class Xttblog {
    private long xttblog; // not initialized value
    public Xttblog() {
        this.xttblog = 1; // initialization
    }
    public long xttblog() {
        return this.xttblog;
    }
}

下面分别使用构造器、反射和 Unsafe 初始化它,看看最终的结果。

Xttblog obj1 = new Xttblog(); // constructor
obj1.xttblog(); // prints 1
Xttblog obj2 = Xttblog.class.newInstance(); // reflection
obj2.xttblog(); // prints 1
Xttblog obj3 = (Xttblog) unsafe.allocateInstance(Xttblog.class); // unsafe
obj3.xttblog(); // prints 0

运行结果表明,通过 Unsafe 初始化的类,能跳过构造函数。

使用场景二:不通过反射来改变某些访问规则。比如绕过安全的常用技术等,黑客常用的伎俩。模拟代码如下:

class Access {
   private int ACCESS_ALLOWED = 66;
   public boolean giveAccess() {
          return 88 == ACCESS_ALLOWED;
   }
}

正常情况下,我们调用 giveAccess 方法,返回的一定是 false。现在,我们通过 Unsafe 让它返回 true。

Access xttblog = new Access();
xttblog.giveAccess();   // false
Unsafe unsafe = getUnsafe();
Field f = xttblog.getClass().getDeclaredField("ACCESS_ALLOWED");
unsafe.putInt(xttblog, unsafe.objectFieldOffset(f), 88);
xttblog.giveAccess(); // true

这样做之后 ACCESS_ALLOWED 的值就彻底的变成 88 了,后面所有的访问都会是 true,当然你还可以再把值改过来。

使用场景三:浅拷贝。普通的做法是使用 Cloneable。但是如果有太多的类,每个类都用 Cloneable 就太麻烦,这时就可以借助 Unsafe 来实现。

static Object shallowCopy(Object obj) {
    long size = sizeOf(obj);
    long start = toAddress(obj);
    long address = getUnsafe().allocateMemory(size);
    getUnsafe().copyMemory(start, address, size);
    return fromAddress(address);
}
static long toAddress(Object obj) {
    Object[] array = new Object[] {obj};
    long baseOffset = getUnsafe().arrayBaseOffset(Object[].class);
    return normalize(getUnsafe().getInt(array, baseOffset));
}
static Object fromAddress(long address) {
    Object[] array = new Object[] {null};
    long baseOffset = getUnsafe().arrayBaseOffset(Object[].class);
    getUnsafe().putLong(array, baseOffset, address);
    return array[0];
}

使用场景四:破解某些密码,获取内存中的密码。通过 Unsafe 可以访问到某些没有被回收的内存。

检索用户密码的大多数 API 的签名为 byte[] 或 char[],为什么是数组呢?这是出于安全的缘故,因为我们可以删除不需要的数组元素。如果将用户密码检索成字符串,这可以像一个对象一样在内存中保存,而删除该对象只需执行解除引用的操作。但是,这个对象仍然在内存中,由 GC 决定的时间来执行清除。

String password = new String("www.xttblog.com");
String fake = new String(password.replaceAll(".", "?"));
System.out.println(password); // www.xttblog.com
System.out.println(fake); // ????????????

getUnsafe().copyMemory(fake, 
    0L, null, toAddress(password), sizeOf(password));
System.out.println(password); // ????????????
System.out.println(fake); // ????????????

Field stringValue = String.class.getDeclaredField("value");
stringValue.setAccessible(true);
char[] mem = (char[]) stringValue.get(password);
for (int i=0; i < mem.length; i++) {
  mem[i] = '?';
}

使用场景五:Java 多继承。Java 中虽然没有多继承,但是我们可以通过 Unsafe 来破坏这个规则。

long intClassAddress = normalize(getUnsafe().getInt(new Integer(0), 4L));
long strClassAddress = normalize(getUnsafe().getInt("", 4L));
getUnsafe().putAddress(intClassAddress + 36, strClassAddress);

(String) (Object) (new Integer(666))

这个代码片段将 String 类型添加到 Integer 超类中,因此我们可以强制转换,且没有运行时异常。但是我们必须预先强制转换对象,以欺骗编译器。

使用场景六:动态类。从已编译的 .class 文件中。将类内容读取为字节数组,并正确地传递给 defineClass 方法。

byte[] classContents = getClassContent();
Class c = getUnsafe().defineClass(
              null, classContents, 0, classContents.length);
    c.getMethod("xttblog").invoke(c.newInstance(), null); // 1
    
private static byte[] getClassContent() throws Exception {
    File f = new File("/www/xttblog/newclass/Xttblog.class");
    FileInputStream input = new FileInputStream(f);
    byte[] content = new byte[(int)f.length()];
    input.read(content);
    input.close();
    return content;
}

使用场景七:抛出异常。

getUnsafe().throwException(new IOException());

该方法抛出受检异常,但你的代码不必捕捉或重新抛出它,正如运行时异常一样。

使用场景八:快速序列化。

标准 Java 的 Serializable 的序列化能力是非常慢的。它同时要求类必须有一个公共的、无参数的构造器。Externalizable 比较好,但它需要定义类序列化的模式。流行的高性能库,比如 kryo 具有依赖性,这对于低内存要求来说是不可接受的。

这个时候 unsafe 类就可以派上用场,大显身手了。Gson、Fastjson、commons-collections 等中都有使用,我就不贴代码了。

使用场景九:超越 Integer.MAX_VALUE 限制的大数组。

class SuperArray {
    private final static int BYTE = 1;
    private long size;
    private long address;
    public SuperArray(long size) {
        this.size = size;
        address = getUnsafe().allocateMemory(size * BYTE);
    }
    public void set(long i, byte value) {
        getUnsafe().putByte(address + i * BYTE, value);
    }
    public int get(long idx) {
        return getUnsafe().getByte(address + idx * BYTE);
    }
    public long size() {
        return size;
    }
}
class SuperArray {
    private final static int BYTE = 1;
    private long size;
    private long address;
    public SuperArray(long size) {
        this.size = size;
        address = getUnsafe().allocateMemory(size * BYTE);
    }
    public void set(long i, byte value) {
        getUnsafe().putByte(address + i * BYTE, value);
    }
    public int get(long idx) {
        return getUnsafe().getByte(address + idx * BYTE);
    }
    public long size() {
        return size;
    }
}

这是堆外内存(off-heap memory)技术,在 java.nio 包中部分可用。这种方式的内存分配不在堆上,且不受 GC 管理,所以必须小心 Unsafe.freeMemory() 的使用。它也不执行任何边界检查,所以任何非法访问可能会导致 JVM 崩溃。这可用于数学计算,代码可操作大数组的数据。此外,这可引起实时程序员的兴趣,可打破 GC 在大数组上延迟的限制。

使用场景十:无锁高并发。这一类的比如:AtomicInteger、AtomicLong 等 java.util.concurrent.atomic 包下的类。当然我们还可以扩展自己的实现。

以上 10 种场景,平常你使用的多少框架中可能都有使用,你只是不知道罢了!

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加QQ1群:135430763(2000人群已满),QQ2群:454796847(已满),QQ3群:187424846(已满)。QQ群进群密码:xttblog,想加微信群的朋友,之前的微信号好友已满,请加博主新的微信号:xttblog,备注:“xttblog”,添加博主微信拉你进群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作可添加助理微信进行沟通!

本文原文出处:业余草: » 手把手教你用代码实现 Unsafe 的 9 大使用场景