概述
今天要给 JYM 介绍的是 Unsafe
的概念以及各个语言中的现有实现。
Unsafe
(字面意思"不安全")通常指一组绕过语言或运行时常规安全机制的底层 API 。
它们允许开发者直接操作内存、对象布局、线程调度等,打破了编程语言设计时的"安全边界"。
正常情况下,语言会保护你不去做危险操作,比如:
- Java 不让你随便分配/释放堆外内存。
- Python/JavaScript 不让你直接操作对象在内存里的地址。
- C# 不让你轻易越过 CLR 的类型检查。
而 Unsafe 就是提供一个"后门":
- 直接访问内存:读写任意地址的数据。
- 绕过构造函数创建对象。
- 实现原子操作(CAS) 。
- 操作线程/类加载器的底层细节。
为什么要有 Unsafe?
-
性能优化
- 比如 Java 的并发库、Netty、Kafka,都依赖
Unsafe
实现高性能内存和锁操作。
- 比如 Java 的并发库、Netty、Kafka,都依赖
-
实现底层功能
- 高层语言本身没有提供的能力(比如直接分配堆外内存)。
-
框架需要"突破口"
- JVM/CLR/解释器很多 API 不开放,框架只能通过
Unsafe
来实现。
- JVM/CLR/解释器很多 API 不开放,框架只能通过
但是 Unsafe
也会有风险,比如:
- 可能导致 JVM/语言运行时崩溃。
- 可能破坏 类型安全。
- 可能导致 内存泄漏 / 越界访问 。
换句话说:它把本来 C/C++ 才有的风险带进了"安全语言"。
Java的Unsafe类
先来说说 Java 的 Unsafe
类,JDK 通常有这两个类:sun.misc.Unsafe
和 jdk.internal.misc.Unsafe
。
这两个类其实指向的是同一套"底层工具类" ,但它们出现在不同 JDK 版本中,有一些历史和封装上的区别:
sun.misc.Unsafe
:
- 最早出现在 JDK 1.4 ~ JDK 8。
- 属于 Sun 私有 API,但广泛被框架(如 Netty、Hadoop、Kafka 等)使用,用来做堆外内存、CAS 操作、对象实例化等"黑科技"。
- 包路径是
sun.misc
,并不是 Java SE 标准 API。
jdk.internal.misc.Unsafe
:
- 从 JDK 9 引入,随着 模块化系统(Jigsaw) 的推出。
- JDK 开发团队想逐渐替换掉
sun.misc.Unsafe
,把它迁移到jdk.internal.misc
,因为jdk.internal
模块是 JDK 内部专用模块。 - 目的是更好地管理 API 暴露,避免第三方随意依赖。
示例代码:
- 获取
Unsafe
实例
Unsafe
的构造方法和 getUnsafe()
都是受限的,一般要通过反射来获取。
java
package org.codeart;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class Main {
public static void main(String[] args) throws Exception {
// 通过反射获取 Unsafe 实例
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
System.out.println("Unsafe 实例: " + unsafe);
}
}
- 直接分配和释放内存:类似 C 的
malloc
/free
。
java
long size = 8; // 8字节
long address = unsafe.allocateMemory(size); // 分配堆外内存
unsafe.setMemory(address, size, (byte) 1); // 填充为 1
byte value = unsafe.getByte(address);
System.out.println("读取到的值: " + value);
unsafe.freeMemory(address); // 释放内存
- 绕过构造函数创建对象:
java
class A {
private A() {
System.out.println("构造函数被调用");
}
}
A obj = (A) unsafe.allocateInstance(A.class);
System.out.println("创建对象: " + obj);
- CAS(Compare and Swap)
java
class Counter {
volatile int value = 0;
}
Counter counter = new Counter();
long valueOffset = unsafe.objectFieldOffset(Counter.class.getDeclaredField("value"));
boolean success = unsafe.compareAndSwapInt(counter, valueOffset, 0, 42);
System.out.println("CAS 成功了吗? " + success);
System.out.println("新值: " + counter.value);
- 获取对象字段的偏移量并直接修改
java
class Person {
private int age = 20;
}
Person p = new Person();
long ageOffset = unsafe.objectFieldOffset(Person.class.getDeclaredField("age"));
unsafe.putInt(p, ageOffset, 30);
System.out.println("新的 age: " + unsafe.getInt(p, ageOffset));
这些操作能绕过 JVM 的安全检查,写错可能导致 JVM 崩溃。
JDK 9 以后 Unsafe 已被标记为内部 API,建议使用 VarHandle、java.util.concurrent 里的原子类,除非你真的要写类似 Netty/Kafka 这种框架。
Go的unsafe包
unsafe
包是 Go 标准库的一个特殊的包。它提供了一些不安全的底层操作能力,可以绕过 Go 的类型系统和内存安全机制。
主要用于:指针运算、内存布局操作、类型转换。
示例代码:
unsafe.Pointer
一个通用指针类型,可以和任意 *T
相互转换。
go
import (
"fmt"
"unsafe"
)
func main() {
var x int = 10
p := unsafe.Pointer(&x) // *int -> unsafe.Pointer
px := (*int)(p) // unsafe.Pointer -> *int
fmt.Println(*px) // 10
}
uintptr
和指针运算
uintptr
是整数,可以保存指针地址,可以做地址偏移,相当于 C 里的指针算术。
go
package main
import (
"fmt"
"unsafe"
)
func main() {
arr := [4]int{1, 2, 3, 4}
p := unsafe.Pointer(&arr[0])
p2 := (*int)(unsafe.Pointer(uintptr(p) + unsafe.Sizeof(arr[0])))
fmt.Println(*p2) // 2
}
Sizeof
/Alignof
/Offsetof
用来获取结构体的内存布局信息。
go
package main
import (
"fmt"
"unsafe"
)
func main() {
type S struct {
A int8
B int32
}
fmt.Println(unsafe.Sizeof(S{})) // 8
fmt.Println(unsafe.Alignof(S{})) // 4
fmt.Println(unsafe.Offsetof(S{}.B)) // 4
}
unsafe
不是"非法",但会破坏 Go 的内存安全保证。一旦用错,可能导致:程序崩溃、数据错乱、GC 无法正确回收。
因此 Go 官方文档明确说:只有在性能或底层库开发时才用它,普通业务代码应避免使用。
JavaScript的Unsafe替代品
严格来说,JavaScript 并没有类似 Java Unsafe 的官方 API,因为 JS 是运行在 沙箱环境(浏览器 / Node.js)里的,默认是被限制的,不允许直接操作裸内存。
但实际上,JS 里还是有一些"比较底层"的 API,可以起到 半个 Unsafe 的作用:
ArrayBuffer
+TypedArray
+DataView
这是最接近"直接内存访问"的 JS API。允许你分配一段原始二进制缓冲区,然后以不同的数据类型去读写:
js
// 分配 16 字节内存
const buf = new ArrayBuffer(16);
const view = new DataView(buf);
// 写入 long (64-bit)
view.setBigInt64(0, 123456789n, true);
// 读取 long
console.log(view.getBigInt64(0, true));
这块内存还是受 JS 引擎管理,不能越界访问。
- WebAssembly Memory
如果你用 WebAssembly (Wasm) ,可以分配并直接访问线性内存:
js
const memory = new WebAssembly.Memory({ initial: 1 });
const bytes = new Uint8Array(memory.buffer);
bytes[0] = 42;
console.log(bytes[0]); // 42
这就更接近"裸内存指针",能随意读写。
- Node.js 的
Buffer
在 Node.js 里,有一个 Buffer
类,可以直接操作内存:
js
const buf = Buffer.alloc(8);
buf.writeBigInt64LE(123456789n, 0);
console.log(buf.readBigInt64LE(0));
如果用 Buffer.allocUnsafe(size)
,甚至能获得 未清零的内存块(可能包含旧数据)。这非常接近 Java 的 Unsafe.allocateMemory
。
Python的ctypes&cffi
严格来说,Python 并没有类似 Java Unsafe
的官方类或 API,因为 Python 的设计理念就是"安全、高层抽象",屏蔽了底层内存操作。
但如果你想要 突破 Python 的安全边界,去做类似 Java Unsafe
的事情,其实有几种方式:
- ctypes 模块
Python 内置库,可以直接操作内存、调用 C 函数。功能类似 Unsafe.allocateMemory
/ Unsafe.putLong
等:
py
import ctypes
# 分配一块内存
buf = ctypes.create_string_buffer(16)
ctypes.memset(buf, 0x41, 16) # 把内存填充为 'A'
# 读写内存地址
addr = ctypes.addressof(buf)
print("内存地址:", hex(addr))
print("内容:", buf.raw)
通过 ctypes
你可以直接越过 Python 对象的边界,甚至能修改 CPython 内部结构,风险和能力都类似 Unsafe
。
- cffi 库
这是一个更现代的 C 接口,可以写 C 代码并在 Python 里调用。
py
from cffi import FFI
ffi = FFI()
p = ffi.new("int[4]", [1, 2, 3, 4])
print(p[0], p[1])
也能直接操作内存指针,甚至执行内联 C。
总结
为什么上文没有提到 C/C++ 呢?
哈哈哈哈,因为 C/C++ 任何指针操作都是直接操作内存的,本身就是不安全的!
C 和 C++ 是 系统级语言,设计目标就是"给开发者完全的控制权",所以:
- 所有内存操作(malloc/free、new/delete、指针算术、数组下标访问等)本质上都是"不安全"的。
- 编译器不会帮你做边界检查,也没有 GC,能直接操作裸内存。