第十章 java重要知识点(Java中Unsafe类)

Unsafe

简介

Unsafe 是位于 sun.misc 包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。但由于 Unsafe 类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用 Unsafe 类会使得程序出错的概率变大,使得 Java 这种安全的语言变得不再“安全”,因此对 Unsafe 的使用一定要慎重。
另外,Unsafe 提供的这些功能的实现需要依赖本地方法(Native Method)。你可以将本地方法看作是 Java 中使用其他编程语言编写的方法。本地方法使用 native 关键字修饰,Java 代码中只是声明方法头,具体的实现则交给 本地代码

使用本地方法的目的

  1. 需要用到java中不具备的依赖于操作系统的特性,java在实现跨平台的同时要实现对底层的控制,则需要借助其他语言发挥作用
  2. 对于其他语言已经完成的一些现成功能,可以使用Java直接调用
  3. 程序对时间敏感或者是对性能要求非常高的话,有必要使用更加底层的语言
  4. 在JUC包的很多并发工具类在实现并发机制时,都调用了本地方法,通过本地方法打破了Java运行时的界限,能够接触到操作系统底层的某些功能。对于同一个本地方法,不同的操作系统可能会通过不同的方式来实现,但是对使用者是透明的,会得到相同的结果。

Unsafe创建

sun.misc.Unsafe 部分源码:

public final class Unsafe {
  // 单例对象
  private static final Unsafe theUnsafe;
  ......
  private Unsafe() {
  }
  @CallerSensitive
  public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    // 仅在引导类加载器`BootstrapClassLoader`加载时才合法
    if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
      throw new SecurityException("Unsafe");
    } else {
      return theUnsafe;
    }
  }
}

Unsafe类是单例实现,提供静态方法getUnsafe获取Unsafe实例。如果直接调用上述静态方法,会抛出SecurityException异常

public static方法无法直接被调用(原因)

因为在getUnsafe方法中,会对调用者的classLoader进行检查,判断当前类是否由Bootstrap classLoader加载,如果不是则抛出上述异常。
结论:只有启动类加载器加载的类才能调用Unsafe类中的方法,来防止这些方法在不可信的代码中被调用。

对Unsafe类进行谨慎的使用限制

Unsafe提供的功能过于底层,安全隐患较大。

获取Unsafe实例

利用反射获得Unsafe类中已实例化完成的单例对象theUnsafe
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.getUnsafe方法安全的获取 Unsafe 实例

从getUnsafe方法的使用限制条件出发,通过 Java 命令行命令
-Xbootclasspath/a 把调用 Unsafe 相关方法的类 A 所在 jar 包路径追加到默认的 bootstrap 路径中,使得 A 被引导类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取 Unsafe 实例

java -Xbootclasspath/a: ${path}   
// 其中path为调用Unsafe相关方法的类所在jar包路径

Unsafe功能

内存操作
内存屏障
对象操作
数据操作
CAS 操作
线程调度
Class 操作
系统信息

内存操作

介绍

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);

注意:
通过该方式分配内存属于堆外内存,是无法进行垃圾回收的。需要人工把内存当做一种资源去手动调用freeMemory方法进行释放,否则会产生内存泄露。
通常是在try中对内存进行操作,在finally中进行内存释放。

堆外内存的使用目的
  1. 对垃圾回收停顿的改善。堆外内存直接受操作系统管理,而不是JVM管理。当我们使用堆外内存时,可以保持较小的堆内内存规模。从而在GC时减少回收停顿对于应用的影响。
  2. 提升程序IO操作的性能。在IO通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。
典型应用

DirectByteBuffer是Java用于实现堆外内存的重要类,通常用在通信过程中做缓冲池,例如:在Netty,MINA等NIO框架中应用广泛。该类对于堆外内存的创建,使用,销毁等逻辑均由Unsafe提供的堆外内存API实现。
示例:
DirectByteBuffer 构造函数,创建 DirectByteBuffer 的时候,通过 Unsafe.allocateMemory 分配内存、Unsafe.setMemory 进行内存初始化,而后构建 Cleaner 对象用于跟踪 DirectByteBuffer 对象的垃圾回收,以实现当 DirectByteBuffer 被垃圾回收时,分配的堆外内存一起被释放

DirectByteBuffer(int cap) {              // package-private

    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        // 分配内存并返回基地址
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    // 内存初始化
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    // 跟踪 DirectByteBuffer 对象的垃圾回收,以实现堆外内存释放
    cleaner = Cleaner.create(this, new Deallocator(base,
     size, cap));
    att = null;
}

内存屏障

介绍

首先,编译器和CPU会在保证程序输出结果一致的情况下,会对代码进行重排序,从指令优化角度提升性能。
指令重排序可能会带来一个不好的结果,导致CPU的高速缓存和内存中数据的不一致。
内存屏障(Memory Barrier)是通过阻止屏障两侧的指令重排序,从而避免编译器和硬件的不正确优化。
在硬件层面上,内存屏障是CPU为了防止代码进行重排序而提供的指令,不同的硬件平台上实现内存屏障的方法不相同。在Java8中,引入了3个内存屏障的函数,屏蔽了操作系统底层的差异,允许在代码中定义,并统一由JVM来生成内存屏障指令,来实现内存屏障的功能。
Unsafe提供了3个内存屏障的相关方法:

//内存屏障,禁止load操作重排序。屏障前的
//load操作不能被重排序到屏障后,屏障后的
//load操作不能被重排序到屏障前
public native void loadFence();
//内存屏障,禁止store操作重排序。
//屏障前的store操作不能被重排序到屏障后,
//屏障后的store操作不能被重排序到屏障前
public native void storeFence();
//内存屏障,禁止load、store操作重排序
public native void fullFence();

内存屏障可以看做是内存随机访问操作中的一个同步点,使得此点之前的所有读写操作都执行后,再开始执行此点之后的操作。
比如:loadFence方法,会禁止读操作重排序,保证在屏障之前的所有读操作都完成,并将缓存数据设为无效,重新从主存中进行加载。
注:
运行中的线程不是直接读取主内存中的变量,只能操作自己工作内存中的变量,然后同步到主内存中,并且线程的工作内存时不能共享的。

典型应用

在Java8中引入了一种锁的新机制–StampedLock,是读写锁的改进版本。StampedLock提供了一种乐观读锁的实现,这种乐观读锁类似于无锁的操作,完全不会阻塞写线程获取写锁,从而缓解读多写少时写线程“饥饿”现象。由于StampedLock提供的乐观读锁不阻塞写线程获取读锁,当线程共享变量从主内存load到线程工作内存时,会存在数据不一致的问题
为了解决上述问题:StampedLock 的 validate 方法会通过 Unsafe 的 loadFence 方法加入一个 load 内存屏障。

public boolean validate(long stamp) {
        //在校验逻辑之前,会通过Unsafe的loadFence方法加入
        //一个load内存屏障,目的是避免copy变量到工作内存中
        //和StampedLock.validate中锁状态校验运算发生重排序导致
        //锁状态校验不准确的问题
        U.loadFence();
        //如果传入进来的stamp & SBITS和state & SBITS相等
        return (stamp & SBITS) == (state & SBITS);
}

对象操作

介绍

对象成员属性的内存偏移量获取,以及字段属性值的修改,除了putInt,getInt方法外,Unsafe提供了全部8种基础数据类型以及Object类的put和get方法,并且所有的put方法都可以越过访问权限,直接修改内存中的数据。
通过openDJK源码可以看到,基础数据类型和Object的读写稍有不同,基础数据类型是直接操作的属性值,而Object的操作则是基于引用值。Object的读写如下:

//在对象的指定偏移地址获取一个对象引用
public native Object getObject(Object o, long offset);
//在对象指定偏移地址写入一个对象引用
public native void putObject(Object o, long offset, Object x);

除了对象属性的普通读写外,Unsafe提供了volatile读写和有序写入方法。volatile读写方法的覆盖范围和普通读写相同,包含了全部基础数据类型和Object类型。int类型为例:

//在对象的指定偏移地址处读取一个int值,支持volatile load语义
public native int getIntVolatile(Object o, long offset);
//在对象指定偏移地址处写入一个int,支持volatile store语义
public native void putIntVolatile(Object o,long offset,int x);

volatile读写具有更高的成本,因为需要保证可见性和有序性。执行get操作时,会强制从主存中获取属性值。在使用put方法设置属性值时,会强制将值更新到主存中,从而保证这些变更对其他线程是可见的。
有序写入的方法有3种:

public native void putOrderedObject(
Object o, long offset, Object x);

public native void putOrderedInt(
Object o, long offset, int x);

public native void putOrderedLong(
Object o, long offset, long x);

有序写入的成本相对volatile较低,因为它只保证写入时的有序性,而不保证可见性,也就是一个线程写入的值不能保证其他线程立即可见。为了解决这里的差异性,需要对内存屏障的知识点再进一步进行补充,首先需要了解两个指令的概念:
Load:将主内存中的数据拷贝到处理器的缓存中
Store:将处理器缓存的数据刷新到主内存中
区别:
有序写入和volatile写入的内存屏障类型不同,分别是:StoreStore,StoreLoad类型。
StoreStore屏障确保在屏障前的数据立刻刷新到内存,这个操作先于屏障后的数据操作,和后续的存储指令的操作
StoreLoad屏障确保在屏障前的数据立刻刷新到内存,这个操作先于屏障后的数据操作,和后续的装载指令,并且该屏障会使屏障之前的所有内存访问指令,包括存储指令和访问指令全部完成后,再执行该屏障之后的内存访问指令。
在写入效率方面:
put>putOrder>putVolatile

对象操作中的对象实例化

使用Unsafe的allocateInstance方法,允许我们使用非常规的方式进行对象的实例化,首先定义一个实体类,并且在构造函数中对其成员变量进行赋值操作:

@Data
public class A {
    private int b;
    public A(){
        this.b =1;
    }
}

分别基于构造函数、反射以及 Unsafe 方法的不同方式创建对象进行比较:

public void objTest() throws Exception{
    A a1=new A();
    System.out.println(a1.getB());//1
    A a2 = A.class.newInstance();
    System.out.println(a2.getB());//1
    A a3= (A) unsafe.allocateInstance(A.class);
    System.out.println(a3.getB());//0
}

说明allocateInstance创建对象过程中,不会调用类的构造方法。使用这种方法创建对象时,只用到了Class对象,如果想要跳过对象的初始化阶段或者跳过构造器的安全检查,就可以使用这种方法。
上述代码中,如果把构造函数改为private类型,则无法通过构造函数和反射创建对象,但是allocateInstance可以创建对象。

典型应用
  1. 常规对象实例化方式
    通常用到的创建对象的方式,从本质上讲,是通过new机制来实现对象的创建。但是,new机制有个特点当类只提供有参的构造函数且无显示声明无参构造函数时,则必须使用有参构造函数进行对象构造,而使用有参构造函数时,必须传递相应个数的参数才能完成对象实例化。
  2. 非常规的实例化方法
    Unsafe 中提供 allocateInstance 方法,仅通过 Class 对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM 安全检查等。它抑制修饰符检测,也就是即使构造器是 private 修饰的也能通过此方法实例化,只需提类对象即可创建相应的对象。由于这种特性,allocateInstance 在 java.lang.invoke、Objenesis(提供绕过类构造器的对象生成方式)、Gson(反序列化时用到)中都有相应的应用。

数组操作

介绍

arrayBaseOffset 与 arrayIndexScale 这两个方法配合起来使用,即可定位数组中每个元素在内存中的位置。

//返回数组中第一个元素的偏移地址
public native int arrayBaseOffset(Class<?> arrayClass);
//返回数组中一个元素占用的大小
public native int arrayIndexScale(Class<?> arrayClass);
典型应用

上述方法在java.util.concurrent.atomic 包下的AtomicIntegerArray(可以实现对Integer数组中每个元素的原子性操作),与操作系统有关。通过 Unsafe 的 arrayBaseOffset 、arrayIndexScale 分别获取数组首元素的偏移地址 base 及单个元素大小因子 scale。后续相关原子性操作,均依赖于这两个值进行数组中的元素定位,而后通过CAS实现原子性操作。

CAS 操作

介绍

CAS是比较并替换(Compare And Swap),是实现并发算法时常用到的一种技术。**CAS操作包含三个操作数,分别是:内存位置,预期原值,新值。**执行CAS操作时,将内存位置的值和预期原值想比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。CAS是一条CPU的原子指令,不会造成数据不一致问题,Unsafe提供的CAS方法(例如:compareAndSwapXXX)底层实现即为CPU指令cmpxchg。

典型应用

在JUC包的并发工具类中大量使用了CAS操作,像synchornized和AQS中也多次用到了CAS,作为乐观锁在并发工具类中广泛发挥了作用。在Unsafe类中,提供了compareAndSwapObject,compareAndSwapInt,compareAndSwapLong方法来实现对Object,int,long类型的CAS操作。例如:

public final native boolean compareAndSwapInt(Object o,
 long offset,int expected,int x);

参数中o为需要更新的对象,offset是对象o中整形字段的偏移量,如果这个字段的值与expected相同,则将字段的值设为x这个新值,并且此更新是不可被中断的,也就是一个原子操作
用compareAndSwapInt实际举例:(需要着重进行解读)

private volatile int a;
public static void main(String[] args){
    CasTest casTest = new CasTest();
    new Thread(()-> {
        for (int i = 1; i < 5; i++) {
            casTest.increment(i);
            System.out.print(casTest.a+" ");
        }
    }).start();
    new Thread(()-> {
        for (int i = 5 ; i <10 ; i++) {
            casTest.increment(i);
            System.out.print(casTest.a+" ");
        }
    }).start();
}

private void increment(int x){
    while (true){
        try {
            long fieldOffset = unsafe.objectFieldOffset(
            CasTest.class.getDeclaredField("a"));
            if (unsafe.compareAndSwapInt(
            this,fieldOffset,x-1,x))
                break;
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
}

线程调度

介绍

Unsafe 类中提供了park、unpark、monitorEnter、monitorExit、tryMonitorEnter方法进行线程调度。

//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程
public native void park(boolean isAbsolute, long time);
//获得对象锁(可重入锁)
@Deprecated
public native void monitorEnter(Object o);
//释放对象锁
@Deprecated
public native void monitorExit(Object o);
//尝试获取对象锁
@Deprecated
public native boolean tryMonitorEnter(Object o);

park,unpark方法是可实现线程的挂起与恢复。
park:将一根线程进行挂起,调用后线程将一直阻塞直到超时或者中断等条件出现
unpark:终止一个挂起的线程,使其恢复正常

此外,Unsafe 源码中monitor相关的三个方法已经被标记为deprecated,不建议被使用:

//获得对象锁
@Deprecated
public native void monitorEnter(Object var1);
//释放对象锁
@Deprecated
public native void monitorExit(Object var1);
//尝试获得对象锁
@Deprecated
public native boolean tryMonitorEnter(Object var1);

monitorEnter方法用于获得对象锁,monitorExit用于释放对象锁,如果对一个没有被monitorEnter加锁的对象执行此方法,会抛出IllegalMonitorStateException异常。tryMonitorEnter方法尝试获取对象锁,如果成功则返回true,反之返回false。

典型应用

Java锁和同步器框架的核心类是AQS,全称叫:AbstractQueuedSynchronizer。通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的,而 LockSupport 的 park 、unpark 方法实际是调用 Unsafe 的 park 、unpark 方式实现的。
代码如下:

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}
public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

LockSupport 的park方法调用了 Unsafe 的park方法来阻塞当前线程,此方法将线程阻塞后就不会继续往后执行,直到有其他线程调用unpark方法唤醒当前线程。下面的例子对 Unsafe 的这两个方法进行测试:

public static void main(String[] args) {
    Thread mainThread = Thread.currentThread();
    new Thread(()->{
        try {
            TimeUnit.SECONDS.sleep(5);
            System.out.println("subThread try to 
            unpark mainThread");
            unsafe.unpark(mainThread);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();

    System.out.println("park main mainThread");
    unsafe.park(false,0L);
    System.out.println("unpark mainThread success");
}
输出如下:
park main mainThread
subThread try to unpark mainThread
unpark mainThread success

Class 操作

介绍

Unsafe 对Class的相关操作主要包括类加载和静态变量的操作方法。
静态属性读取的方法

//获取静态属性的偏移量
public native long staticFieldOffset(Field f);
//获取静态属性的对象指针
public native Object staticFieldBase(Field f);
//判断类是否需要实例化(用于获取类的静态属性前进行检测)
public native boolean shouldBeInitialized(Class<?> c);

创建一个包含静态属性的类,进行测试:

@Data
public class User {
    public static String name="Hydra";
    int age;
}
private void staticTest() throws Exception {
    User user=new User();
  System.out.println(unsafe.shouldBeInitialized(User.class));
  Field sexField = User.class.getDeclaredField("name");
  long fieldOffset = unsafe.staticFieldOffset(sexField);
  Object fieldBase = unsafe.staticFieldBase(sexField);
  Object object = unsafe.getObject(fieldBase, fieldOffset);
  System.out.println(object);
}
输出结果:
falseHydra

在 Unsafe 的对象操作中,我们学习了通过objectFieldOffset方法获取对象属性偏移量并基于它对变量的值进行存取,但是它不适用于类中的静态属性,这时候就需要使用staticFieldOffset方法。在上面的代码中,只有在获取Field对象的过程中依赖到了Class,而获取静态变量的属性时不再依赖于Class。
在上面的代码中首先创建一个User对象,这是因为如果一个类没有被实例化,那么它的静态属性也不会被初始化,最后获取的字段属性将是null。所以在获取静态属性前,需要调用shouldBeInitialized方法,判断在获取前是否需要初始化这个类。如果删除创建 User 对象的语句,运行结果会变为:truenull

关于反编译

使用defineClass方法允许程序在运行时动态地创建一个类

public native Class<?> defineClass(String name, byte[] b, 
int off, int len, ClassLoader loader,
ProtectionDomain protectionDomain);

在实际使用过程中,可以只传入字节数组、起始字节的下标以及读取的字节长度,默认情况下,类加载器(ClassLoader)和保护域(ProtectionDomain)来源于调用此方法的实例。下面的例子中实现了反编译生成后的 class 文件的功能:

private static void defineTest() {
    String fileName="F:\\workspace\\unsafe-test\\target
    \\classes\\com\\cn\\model\\User.class";
    File file = new File(fileName);
    try(FileInputStream fis = new FileInputStream(file)) {
        byte[] content=new byte[(int)file.length()];
        fis.read(content);
        Class clazz = unsafe.defineClass(null, content,
         0, content.length, null, null);
        Object o = clazz.newInstance();
        Object age = clazz.getMethod("getAge").invoke(o,null);
        System.out.println(age);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

上述代码首先读取了一个class文件并通过文件流将它转化为字节数组,之后使用defineClass方法动态的创建了一个类,并在后续完成了它的实例化工作,并且通过这种方式创建的类,跳过 JVM 的所有安全检查。
完成了从文件到对象的转化:
class文件->字节数组->类:field/method->对象
Unsafe 还提供了一个defineAnonymousClass方法

public native Class<?> defineAnonymousClass(Class<?> 
hostClass, byte[] data, Object[] cpPatches);

可以用来动态的创建一个匿名类,在Lambda表达式中就是使用 ASM 动态生成字节码,然后利用该方法定义实现相应的函数式接口的匿名类。在 JDK 15 发布的新特性中,在隐藏类(Hidden classes)一条中,指出将在未来的版本中弃用 Unsafe 的defineAnonymousClass方法

系统信息

介绍

获取系统相关信息的方法:

//返回系统指针的大小。返回值为4(32位系统)或 8(64位系统)。
public native int addressSize();
//内存页的大小,此值为2的幂次方。
public native int pageSize();
典型应用

在java.nio.Bits类中,在使用pageCount计算所需的内存页的数量时,调用了pageSize方法获取内存页的大小。
在使用copySwapMemory方法拷贝内存时,调用了addressSize方法,检测 32 位系统的情况

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值