有什么有趣的网站,做兼职网站的项目初衷,怎样在百度能搜到自己的网站,简述网站建设和推广评价指标前言Unsafe是位于sun.misc包下的一个类#xff0c;主要提供一些用于执行低级别、不安全操作的方法#xff0c;如直接访问系统内存资源、自主管理内存资源等#xff0c;这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java…前言Unsafe是位于sun.misc包下的一个类主要提供一些用于执行低级别、不安全操作的方法如直接访问系统内存资源、自主管理内存资源等这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大使得Java这种安全的语言变得不再“安全”因此对Unsafe的使用一定要慎重。注本文对sun.misc.Unsafe公共API功能及相关应用场景进行介绍。基本介绍如下Unsafe源码所示Unsafe类为一单例实现提供静态方法getUnsafe获取Unsafe实例当且仅当调用getUnsafe方法的类为引导类加载器所加载时才合法否则抛出SecurityException异常。public final class Unsafe {// 单例对象private static final Unsafe theUnsafe;其一从 getUnsafe 方法的使用限制条件出发通过Java命令行命令 -Xbootclasspath/a 把调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中使得A被引导类加载器加载从而通过 Unsafe.getUnsafe 方法安全的获取Unsafe实例。java -Xbootclasspath/a: ${path} // 其中path为调用Unsafe相关方法的类所在jar包路径其二通过反射获取单例对象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提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类下面将对其相关方法和应用场景进行详细介绍。内存操作这部分主要包含堆外内存的分配、拷贝、释放、给定地址值操作等方法。//分配内存, 相当于C的malloc函数public native long allocateMemory(long bytes);//扩充内存public native long reallocateMemory(long address, long bytes);//释放内存public native void freeMemory(long address);//在给定的内存块中设置值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);//获取给定地址值忽略修饰限定符的访问限制。与此类似操作还有: getIntgetDoublegetLonggetChar等public native Object getObject(Object o, long offset);//为给定地址设置值忽略修饰限定符的访问限制与此类似操作还有: putInt,putDoubleputLongputChar等public native void putObject(Object o, long offset, Object x);//获取给定地址的byte类型的值(当且仅当该内存地址为allocateMemory分配时此方法结果为确定的)public native byte getByte(long address);//为给定地址设置byte类型的值(当且仅当该内存地址为allocateMemory分配时此方法结果才是确定的)public native void putByte(long address, byte x);通常我们在Java中创建的对象都处于堆内内存(heap)中堆内内存是由JVM所管控的Java进程内存并且它们遵循JVM的内存管理机制JVM会采用垃圾回收机制统一管理堆内存。与之相对的是堆外内存存在于JVM管控之外的内存区域Java中对堆外内存的操作依赖于Unsafe提供的操作堆外内存的native方法。使用堆外内存的原因对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是JVM所以当我们使用堆外内存时即可保持较小的堆内内存规模。从而在GC时减少回收停顿对于应用的影响。提升程序I/O操作的性能。通常在I/O通信过程中会存在堆内内存到堆外内存的数据拷贝操作对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据都建议存储到堆外内存。典型应用DirectByteBuffer是Java用于实现堆外内存的一个重要类通常用在通信过程中做缓冲池如在Netty、MINA等NIO框架中应用广泛。DirectByteBuffer对于堆外内存的创建、使用、销毁等逻辑均由Unsafe提供的堆外内存API来实现。下图为DirectByteBuffer构造函数创建DirectByteBuffer的时候通过Unsafe.allocateMemory分配内存、Unsafe.setMemory进行内存初始化而后构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收以实现当DirectByteBuffer被垃圾回收时分配的堆外内存一起被释放。那么如何通过构建垃圾回收追踪对象Cleaner实现堆外内存释放呢Cleaner继承自Java四大引用类型之一的虚引用PhantomReference(众所周知无法通过虚引用获取与之关联的对象实例且当对象仅被虚引用引用时在任何发生GC的时候其均可被回收)通常PhantomReference与引用队列ReferenceQueue结合使用可以实现虚引用关联对象被垃圾回收时能够进行系统通知、资源清理等功能。如下图所示当某个被Cleaner引用的对象将被回收时JVM垃圾收集器会将此对象的引用放入到对象引用中的pending链表中等待Reference-Handler进行相关处理。其中Reference-Handler为一个拥有最高优先级的守护线程会循环不断的处理pending链表中的对象引用执行Cleaner的clean方法进行相关清理工作。所以当DirectByteBuffer仅被Cleaner引用(即为虚引用)时其可以在任意GC时段被回收。当DirectByteBuffer实例对象被回收时在Reference-Handler线程操作中会调用Cleaner的clean方法根据创建Cleaner时传入的Deallocator来进行堆外内存的释放。CAS相关如下源代码释义所示这部分主要为CAS相关操作的方法。/**CASparam o 包含要修改field的对象param offset 对象中某field的偏移量param expected 期望值param update 更新值return true | false*/public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);什么是CAS? 即比较并替换实现并发算法时常用到的一种技术。CAS操作包含三个操作数——内存位置、预期原值及新值。执行CAS操作的时候将内存位置的值与预期原值比较如果相匹配那么处理器会自动将该位置值更新为新值否则处理器不做任何操作。我们都知道CAS是一条CPU的原子指令(cmpxchg指令)不会造成所谓的数据不一致问题Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。典型应用CAS在java.util.concurrent.atomic相关类、Java AQS、CurrentHashMap等实现上有非常广泛的应用。如下图所示AtomicInteger的实现中静态字段valueOffset即为字段value的内存偏移地址valueOffset的值在AtomicInteger初始化时在静态代码块中通过Unsafe的objectFieldOffset方法获取。在AtomicInteger中提供的线程安全方法中通过字段valueOffset的值可以定位到AtomicInteger对象中value的内存地址从而可以根据CAS实现对value字段的原子操作。下图为某个AtomicInteger对象自增操作前后的内存示意图对象的基地址baseAddress0x110000通过baseAddressvalueOffset得到value的内存地址valueAddress0x11000c然后通过CAS进行原子性的更新操作成功则返回否则继续重试直到更新成功为止。线程调度这部分包括线程挂起、恢复、锁机制等方法。典型应用Java锁和同步器框架的核心类AbstractQueuedSynchronizer就是通过调用 LockSupport.park() 和 LockSupport.unpark() 实现线程的阻塞和唤醒的而LockSupport的park、unpark方法实际是调用Unsafe的park、unpark方式来实现。Class相关此部分主要提供Class和它的静态字段的操作相关方法包含静态字段内存定位、定义类、定义匿名类、检验确保初始化等。//获取给定静态字段的内存地址偏移量这个值对于给定的字段是唯一且固定不变的public native long staticFieldOffset(Field f);//获取一个静态类中给定字段的对象指针public native Object staticFieldBase(Field f);//判断是否需要初始化一个类通常在获取一个类的静态属性的时候(因为一个类如果没初始化它的静态属性也不会初始化)使用。 当且仅当ensureClassInitialized方法不生效时返回false。public native boolean shouldBeInitialized(Class c);//检测给定的类是否已经初始化。通常在获取一个类的静态属性的时候(因为一个类如果没初始化它的静态属性也不会初始化)使用。public native void ensureClassInitialized(Class c);//定义一个类此方法会跳过JVM的所有安全检查默认情况下ClassLoader(类加载器)和ProtectionDomain(保护域)实例来源于调用者public native Class defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);//定义一个匿名类public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches);典型应用从Java 8开始JDK使用invokedynamic及VM Anonymous Class结合来实现Java语言层面上的Lambda表达式。invokedynamic invokedynamic是Java 7为了实现在JVM上运行动态语言而引入的一条新的虚拟机指令它可以实现在运行期动态解析出调用点限定符所引用的方法然后再执行该方法invokedynamic指令的分派逻辑是由用户设定的引导方法决定。VM Anonymous Class 可以看做是一种模板机制针对于程序动态生成很多结构相同、仅若干常量不同的类时可以先创建包含常量占位符的模板类而后通过Unsafe.defineAnonymousClass方法定义具体类时填充模板的占位符生成具体的匿名类。生成的匿名类不显式挂在任何ClassLoader下面只要当该类没有存在的实例对象、且没有强引用来引用该类的Class对象时该类就会被GC回收。故而VM Anonymous Class相比于Java语言层面的匿名内部类无需通过ClassClassLoader进行类加载且更易回收。在Lambda表达式实现中通过invokedynamic指令调用引导方法生成调用点在此过程中会通过ASM动态生成字节码而后利用Unsafe的defineAnonymousClass方法定义实现相应的函数式接口的匿名类然后再实例化此匿名类并返回与此匿名类中函数式方法的方法句柄关联的调用点而后可以通过此调用点实现调用相应Lambda表达式定义逻辑的功能。下面以如下图所示的Test类来举例说明。Test类编译后的class文件反编译后的结果如下图一所示(删除了对本文说明无意义的部分)我们可以从中看到main方法的指令实现、invokedynamic指令调用的引导方法BootstrapMethods、及静态方法 lambda$main$0 (实现了Lambda表达式中字符串打印逻辑)等。在引导方法执行过程中会通过Unsafe.defineAnonymousClass生成如下图二所示的实现Consumer接口的匿名类。其中accept方法通过调用Test类中的静态方法 lambda$main$0 来实现Lambda表达式中定义的逻辑。而后执行语句 consumer.accept(lambda) 其实就是调用下图二所示的匿名类的accept方法。对象操作此部分主要包含对象成员属性相关操作及非常规的对象实例化方式等相关方法。//返回对象成员属性在内存地址相对于此对象的内存地址的偏移量public native long objectFieldOffset(Field f);//获得给定对象的指定地址偏移量的值与此类似操作还有getIntgetDoublegetLonggetChar等public native Object getObject(Object o, long offset);//给定对象的指定地址偏移量设值与此类似操作还有putIntputDoubleputLongputChar等public native void putObject(Object o, long offset, Object x);//从对象的指定偏移量处获取变量的引用使用volatile的加载语义public native Object getObjectVolatile(Object o, long offset);//存储变量的引用到对象的指定的偏移量处使用volatile的存储语义public native void putObjectVolatile(Object o, long offset, Object x);//有序、延迟版本的putObjectVolatile方法不保证值的改变被其他线程立即看到。只有在field被volatile修饰符修饰时有效public native void putOrderedObject(Object o, long offset, Object x);//绕过构造方法、初始化代码来创建对象public native Object allocateInstance(Class cls) throws InstantiationException;典型应用常规对象实例化方式 我们通常所用到的创建对象的方式从本质上来讲都是通过new机制来实现对象的创建。但是new机制有个特点就是当类只提供有参的构造函数且无显示声明无参构造函数时则必须使用有参构造函数进行对象构造而使用有参构造函数时必须传递相应个数的参数才能完成对象实例化。非常规的实例化方式 而Unsafe中提供allocateInstance方法仅通过Class对象就可以创建此类的实例对象而且不需要调用其构造函数、初始化代码、JVM安全检查等。它抑制修饰符检测也就是即使构造器是private修饰的也能通过此方法实例化只需提类对象即可创建相应的对象。由于这种特性allocateInstance在java.lang.invoke、Objenesis(提供绕过类构造器的对象生成方式)、Gson(反序列化时用到)中都有相应的应用。如下图所示在Gson反序列化时如果类有默认构造函数则通过反射调用默认构造函数创建实例否则通过UnsafeAllocator来实现对象实例的构造UnsafeAllocator通过调用Unsafe的allocateInstance实现对象的实例化保证在目标类无默认构造函数时反序列化不够影响。数组相关这部分主要介绍与数据操作相关的arrayBaseOffset与arrayIndexScale这两个方法两者配合起来使用即可定位数组中每个元素在内存中的位置。//返回数组中第一个元素的偏移地址public native int arrayBaseOffset(Class arrayClass);//返回数组中一个元素占用的大小public native int arrayIndexScale(Class arrayClass);典型应用这两个与数据操作相关的方法在java.util.concurrent.atomic 包下的AtomicIntegerArray(可以实现对Integer数组中每个元素的原子性操作)中有典型的应用如下图AtomicIntegerArray源码所示通过Unsafe的arrayBaseOffset、arrayIndexScale分别获取数组首元素的偏移地址base及单个元素大小因子scale。后续相关原子性操作均依赖于这两个值进行数组中元素的定位如下图二所示的getAndAdd方法即通过checkedByteOffset方法获取某数组元素的偏移地址而后通过CAS实现原子性操作。内存屏障在Java 8中引入用于定义内存屏障(也称内存栅栏内存栅障屏障指令等是一类同步屏障指令是CPU或编译器在对内存随机访问的操作中的一个同步点使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作)避免代码重排序。//内存屏障禁止load操作重排序。屏障前的load操作不能被重排序到屏障后屏障后的load操作不能被重排序到屏障前public native void loadFence();//内存屏障禁止store操作重排序。屏障前的store操作不能被重排序到屏障后屏障后的store操作不能被重排序到屏障前public native void storeFence();//内存屏障禁止load、store操作重排序public native void fullFence();典型应用在Java 8中引入了一种锁的新机制——StampedLock它可以看成是读写锁的一个改进版本。StampedLock提供了一种乐观读锁的实现这种乐观读锁类似于无锁的操作完全不会阻塞写线程获取写锁从而缓解读多写少时写线程“饥饿”现象。由于StampedLock提供的乐观读锁不阻塞写线程获取读锁当线程共享变量从主内存load到线程工作内存时会存在数据不一致问题所以当使用StampedLock的乐观读锁时需要遵从如下图用例中使用的模式来确保数据的一致性。如上图用例所示计算坐标点Point对象包含点移动方法move及计算此点到原点的距离的方法distanceFromOrigin。在方法distanceFromOrigin中首先通过tryOptimisticRead方法获取乐观读标记然后从主内存中加载点的坐标值 (x,y)而后通过StampedLock的validate方法校验锁状态判断坐标点(x,y)从主内存加载到线程工作内存过程中主内存的值是否已被其他线程通过move方法修改如果validate返回值为true证明(x, y)的值未被修改可参与后续计算否则需加悲观读锁再次从主内存加载(x,y)的最新值然后再进行距离计算。其中校验锁状态这步操作至关重要需要判断锁状态是否发生改变从而判断之前copy到线程工作内存中的值是否与主内存的值存在不一致。下图为StampedLock.validate方法的源码实现通过锁标记与相关常量进行位运算、比较来校验锁状态在校验逻辑之前会通过Unsafe的loadFence方法加入一个load内存屏障目的是避免上图用例中步骤②和StampedLock.validate中锁状态校验运算发生重排序导致锁状态校验不准确的问题。系统相关这部分包含两个获取系统相关信息的方法。//返回系统指针的大小。返回值为4(32位系统)或 8(64位系统)。public native int addressSize();//内存页的大小此值为2的幂次方。public native int pageSize();典型应用如下图所示的代码片段为java.nio下的工具类Bits中计算待申请内存所需内存页数量的静态方法其依赖于Unsafe中pageSize方法获取系统内存页大小实现后续计算逻辑。结语本文对Java中的sun.misc.Unsafe的用法及应用场景进行了基本介绍我们可以看到Unsafe提供了很多便捷、有趣的API方法。即便如此由于Unsafe中包含大量自主操作内存的方法如若使用不当会对程序带来许多不可控的灾难。因此对它的使用我们需要慎之又慎。