Unsafe
Java和C语言的一个重要区别就是,在Java中我们无法直接操作一块内存地址,但Java提供了一个特殊的类Unsafe工具类来间接实现。Unsafe主要提供一些用于执行低级别、不安全操作的方法,(如:直接访问系统内存资源、自主管理内存资源等)。
注:
Unsafe就和其名字一样,直接使用这个类是不安全的,它能直接在内存上修改访问变量,而无视各种访问修饰符的限制。它几乎所有的public方法都是native(本地方法)的,这些方法都是使用C/C++实现的,它越过了虚拟机层面,直接在操作系统本地执行。
由于Unsafe是一个底层类,如果在不了解其内部原理并未掌握使用技巧的情况下,直接使用Unsafe类可能会造成一些意想不到或未知的错误,所以它被限制开发者直接使用,只能由JDK类库的维护者使用。如果看过JUC的源码,那么会发现在各种并发工具类的内部经常会通过使用这个类的一些方法根据相应内存地址在内存上直接CAS修改访问共享变量的值。
获取Unsafe类实例
Unsafe类是final的且构造函数是private的,只能通过静态方法getUnsafe()才能获取Unsafe单例对象。
1 | public final class Unsafe { |
然而此静态方法的使用也是受到限制的,只能由JDK中的其他类来调用,普通开发者使用此方法将抛出异常。
虽然无法实例化,但还是可以通过反射获取Unsafe类对象
1 | public static Unsafe getUnsafe() throws IllegalAccessException { |
再次提示:由于Unsafe类为调用者提供执行不安全操作的能力,返回的Unsafe对象应该由调用方小心保护,绝不能将其传递给不受信任的代码。
Unsafe类的主要功能
Java对象操作相关
获取字段相对偏移量
1 | /** |
这里提到字段的偏移量,这与Java对象的内存布局有密切关系。Java中对象由对象头和实际数据两部分组成。如下图:
- MarkWord包含对象的hashCode、锁信息、垃圾回收的分代信息等,占32/64位;
- Class Metadata Pointer表示一个此对象数据类型的Class对象(虚拟机中的Klass对象)的指针,占32/64位;
- ArrayLength是数组对象特有的内容,表示数组的长度,占32位。数组对象的实际数据是各个元素的值或引用,普通对象的实际数据是各实例字段的值或引用。
- 另外为了快速分配内存、快速内存寻址、调高性能,Java语言规范要求Java对象要做内存对齐处理,每个对象占用的内存字节数必须是8的倍数,若不是则要填0补位对齐。
从上图可用看出,字段与对象头之间的偏移量是固定的,只要知道字段的相对偏移量和对象起始地址,我们就能获取此字段的绝对内存地址(fieldAddress = objAddress + fieldOffset),根据此绝对内存地址,我们就能忽略访问修饰符的限制而可以直接读取/修改此字段的值或引用。
注:数组对象的元素内存地址,相对于普通对象的字段地址有些不一样,它要先计算出对象头的长度,作为基础偏移量;
由于数组元素的数据类型是相同的,每个元素的值或引用所占用的内存空间是相同的,因此将元素值或引用所占内存作为每两个相邻元素的相对偏移量。根据对象起始位置、基础偏移量、相邻元素相对偏移量及数组下标,就可以获取到某个元素值或引用的绝对内存地址(itemAddress = arrayAddress + baseOffset + index * indexOffset),进而通过绝对内存地址读取或修改此元素的值或引用。
根据字段偏移量设置/获取字段值
1 | /** |
volatile版本根据字段偏移量设置/获取字段值
被volatile修饰时可以保证对其他线程的可见性。
1 | // volatile形式地获取字段值,即使在多线条件下,从主内存中获取值,使当前线程的工作内存的缓存值失效 |
volatile读写相对普通读写是更加昂贵的,因为需要保证可见性和有序性,而与volatile写入相比putOrderedXx写入代价相对较低,putOrderedXx写入不保证可见性,但是保证有序性,所谓有序性,就是保证指令不会重排序。
有序延迟化的设置字段值
有序延迟化设置值,对其他线程不保证可见性。
1 | // 有序延迟化地设置字段值, |
数组相关的偏移量
1 | // 第一个元素与数组对象两者间起始地址之差(首元素与对象头的相对偏移量) |
源码示例
此示例是JUC atomic包下的AtomicIntegerArray结合以上两个方法进行数组元素地址定位。
1 | class AtomicIntegerArray implements java.io.Serializable { |
Class相关操作
创建Java类
1 | /** |
Java类初始化
1 | /** |
- shouldBeInitialized(Class):检测对应的Java类是否被初始化。
- ensureClassInitialized(Class):强制Java类初始化,若没有初始化则进行初始化。
这两个方法常与staticFieldBase(Field)一起使用,因为如果Java类没有被初始化,静态变量便没有初始化,就不能直接获取静态变量的引用。
源码示例
java.lang.invoke.DirectMethodHandle中的checkInitialized(MemberName)方法调用了以上两个与类初始化相关的方法。
1 | private static boolean checkInitialized(MemberName member) { |
根据Class创建对象
仅通过Class对象就可以创建此类的实例对象,而且不需要调用其他构造函数、初始化代码、JVM安全检查等。它抑制修饰符检测,也就是即使构造器是private修饰的也能通过此方法实例化。
1 | /** Allocate an instance but do not run any constructor. |
代码示例
私有化Employee类的唯一构造方法,让外界不能直接创建此类的对象;
下面通过allocateInstance(Class)方法和反射分别来创建Employee对象,看有什么区别。
1 | package net.ityizhan.juc; |
结果如下:
1 | Unsafe创建的对象:{Employee [id=0, name=null, sex=0, mgrId=0]} ,{count=1000, countLong=1000} |
从控制台输出的信息可以看出,反射与Unsafe均能创建一个构造方法被私有化的对象。不同之处在于allocateInstance(Class)方法创建对象过程中不会进行对象初始化,但会进行类初始化,既不会执行实例变量初始化赋值、不执行构造代码块、不调用构造方法,但会执行静态变量的初始化赋值、执行静态代码块。
CAS更新操作
CAS是Java并发编程的最底层依据,它实现了非阻塞式地更新共享变量,自旋锁与乐观锁的实现均依赖它。
1 | /** |
源码示例
同步容器AQS中的compareAndSetXxx方法都是直接委托上面的CAS方法实现的。
内存操作
根据内存地址设置/获取对应的值
1 | /** |
根据内存地址设置/获取指针
1 | // 根据内存地址获取一个指针 |
分配、扩展、释放内存
1 | // 分配一块指定的内存空间,返回一个指向此内存起始位置的指针 |
源码示例
java.nio包下的DirectByteBuffer类的构造方法调用Unsafe.allocateMemory(int)分配初始条件下的内存缓冲区
DirectByteBuffer的静态内部类Deallocator的run()调用Unsafe.freeMemory(long)释放相应地址的内存空间
系统信息
获取指定款第、内存也大小等系统软硬件信息,这些信息对于本地内存的分配、使用、寻址很重要。
1 | // 本地指针宽度,通常是4或8 |
源码示例
sun.nio.ch包下NativeObject类的addressSize()方法直接委托Unsafe.addressSize()实现
java.nio包下Bit类pageSize()方法:当pageSize非法时,将Unsafe.pageSize()作为返回值
可以看出addressSize()、pageSize()方法的调用者都是nio相关类,这是因为nio是直接使用JVM堆外的本地内存。
线程管理
唤醒/休眠线程
1 | // 唤醒 |
以上两个方法是 等待/通知模型
的关键,它们是并发编程中使用到的底层方法。以上两个方法主要被LockSupport类直接引用,LockSupport.parkUntil(long)、LockSupport.unpark(Thread)方法中没有其他逻辑,就是直接委托以上两个方法实现的。
源码示例
1 | public static void park() { |
抢锁与释放锁(已弃用)
1 | // 获取锁对象 |
内存屏障
这些方法在Java8中引入,用于定义内存屏障(是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。
1 | // 内存屏障,禁止load重排序。屏障前不能重排序load,且只能在屏障后load或store |
源码示例
loadFence()方法在StampedLock的validate方法有使用到,StampedLock是为了防止CAS更新时出现ABA问题而在JDK1.8新引入的并发工具。