什么是 Unsafe?
首先,
Unsafe
是 Java 中一个非常底层的类,属于
sun.misc
包。它提供了一些相当低级的操作能力,类似于 C 语言中的指针,可以直接访问和操作系统内存。这种能力,放在 Java 里就像是打开了一个 Pandora's Box,既有超强的力量,也充满了危险。
为什么要使用
Unsafe
?
Java 是一门安全性很高的语言,JVM 的垃圾回收机制、内存管理、线程安全等方面都做得相当不错。然而,在某些极端情况下(例如对性能的要求特别高时),我们可能需要越过这些安全壁垒,直接对内存进行控制,这时候
Unsafe
就派上用场了。
它提供的这些方法通常会涉及直接内存访问、内存管理、甚至是对象的直接操作。比如,它允许我们直接分配和释放内存,进行内存拷贝,甚至修改内存的某些字节值。
换句话说,
Unsafe
可以让你像 C 语言一样做内存操作,但它也意味着你有可能一不小心就把内存给弄乱了,导致程序崩溃或产生难以追踪的错误。
Unsafe
的创建
获取
Unsafe
实例
如果你尝试直接调用
Unsafe.getUnsafe()
来获取这个类的实例,绝对会碰壁。因为该方法会做一系列的安全检查,要求调用者的
classLoader
必须是引导类加载器(Bootstrap ClassLoader)加载的类。如果不是,就会抛出
SecurityException
。
这是什么鬼?我们都知道
Unsafe
的功能相当强大,不想让它随便被不可信代码使用,所以 Java 通过这个安全检查来限制对它的访问。
这是为了防止一些不受信任的代码使用
Unsafe
时,搞乱程序的内存操作,甚至带来安全隐患。
不过,不怕。我们可以通过反射来绕过这个安全检查,获取
Unsafe
的实例。代码如下:
private static Unsafe reflectGetUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
这个方法通过反射获取
Unsafe
类中已经创建的单例对象
theUnsafe
,绕过了安全机制。
另外,还可以通过
-Xbootclasspath/a
命令行参数,将包含
Unsafe
调用的类所在的 jar 包路径添加到默认的引导类加载路径中,进而绕过安全限制。
java -Xbootclasspath/a: ${path} // ${path} 为包含 Unsafe 调用类的 jar 包路径
Unsafe
的功能
虽然
Unsafe
的功能很多,但总的来说,它提供了以下几类操作:
-
-
内存屏障
:控制 CPU 指令的执行顺序,确保内存操作的有序性。
-
-
-
CAS 操作
:提供原子操作,确保多线程环境中的数据一致性。
-
-
-
接下来,我们主要看一下
Unsafe
在内存操作方面的几个常用方法。
内存操作
对于程序员来说,内存的直接操作并不陌生。在 C/C++ 中,我们可以通过指针来直接访问内存,这在 Java 中是不允许的,因为 Java 在 JVM 层面做了严格的内存管理,所有的内存操作都被封装和控制。但是,
Unsafe
就给我们提供了这样一个“特权”,让我们能够直接操作内存。
常用的内存操作方法
// 分配一块本地内存
public native long allocateMemory(long bytes);
// 重新调整内存大小
public native long reallocateMemory(long address, long bytes);
// 将内存设置为指定的值
public native void setMemory(Object o, long offset, long bytes, byte value);
// 内存拷贝
public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
// 释放内存
public native void freeMemory(long address);
举个简单的例子,假设我们想分配一块内存,向它写入数据,再把数据拷贝到另一块内存中,代码可以这么写:
private void memoryTest() {
int size = 4;
long addr = unsafe.allocateMemory(size);
long addr3 = unsafe.reallocateMemory(addr, size * 2);
System.out.println("addr: "+addr);
System.out.println("addr3: "+addr3);
try {
unsafe.setMemory(null, addr, size, (byte) 1);
for (int i = 0; i 2; i++) {
unsafe.copyMemory(null, addr, null, addr3 + size * i, 4);
}
System.out.println(unsafe.getInt(addr));
System.out.println(unsafe.getLong(addr3));
} finally {
unsafe.freeMemory(addr);
unsafe.freeMemory(addr3);
}
}
输出结果:
addr: 2433733895744
addr3: 2433733894944
16843009
72340172838076673
结果分析
首先,我们使用
allocateMemory
方法分配了 4 字节的内存空间,并通过
setMemory
方法填充内存,接着使用
copyMemory
将内存拷贝到了另一块内存区域。最后,通过
getInt
和
getLong
方法读取内存中的内容。
比如,输出的
16843009
就是由于我们写入的 4 个字节数据拼接成了一个整数。详细的字节内容和转换过程,可以参考下图: