java中一半是天使一半是魔鬼的Unsafe类详解

简介: 可能我们会奇怪,java中竟然给一个类起名字叫做“不安全”。慢慢看,你就会发现这个类的神奇之处,虽然功能很强大,但是的确不那么安全。

一、简单介绍


首先在Oracle的Jdk8无法获取到sun.misc包的源码,想看此包的源码可以直接下载openjdk。


1、预备工作


openjdk的源码我下载的是openjdk-8u40-src-b25-10_feb_2015


windows->preference->installed jres->选中jre->edit->rt.jar->source attachment->external folders->openjdk源码路径。此时就可以查看我们的Unsafe类的源码了。

v2-48775aa811ca49bc7eb70a2c568d8bbe_1440w.jpg2、简介说明


如果你学习了一些java并发包里面的类源码的话,对这个Unsafe类一定不陌生,整个java并发包底层实现的核心就是它,在很久之前盛传着这个类将要在jdk9移除,实时上如果移除了那么一大批框架将会消失,比如说赫赫有名的Netty框架。最终jdk9出现的时候也只是对其进行了改进和优化。不过这也再一次说明了这个类的重要地位。


为什么说它一半是天使一半是魔鬼呢?要回答这个问题,我们还是要从其特性来解释。

Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,一旦能够直接操作内存,这也就意味着(1)不受jvm管理,也就意味着无法被GC,需要我们手动GC,稍有不慎就会出现内存泄漏。 (2)Unsafe的不少方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量要自己计算,一旦出现问题就是JVM崩溃级别的异常,会导致整个JVM实例崩溃,表现为应用程序直接crash掉。 (3)直接操作内存,也意味着其速度更快,在高并发的条件之下能够很好地提高效率。


因此,从上面三个角度来看,虽然在一定程度上提升了效率但是也带来了指针的不安全性。


下面我们深入到源码中看看,提供了什么方法直接操作内存。


二、源码分析


Unsafe中一共有82个public native修饰的方法,还有几十个基于这82个public native方法的其他方法。这些方法大体可以归结为以下几类:


(1)初始化操作

(2)操作对象属性

(3)操作数组元素

(4)内存管理

(5)内存屏障

(6)线程挂起和回复

(7)CAS机制


下面我们对这些方法尽进行一个大致的分析。


1、初始化


//1、注册native方法,是的Unsafe类可以操作C语言
    private static native void registerNatives();
    static {
        registerNatives();
        sun.reflect.Reflection.registerMethodsToFilter(Unsafe.class, "getUnsafe");
    }
    //2、构造方法
    private Unsafe() {}
    //3、初始化方法
    private static final Unsafe theUnsafe = new Unsafe();
    //4、初始化方法实现
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class<?> caller = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(caller.getClassLoader()))
            throw new SecurityException("Unsafe");
        return theUnsafe;
    }

在这里我们看到Unsafe的初始化方法主要是通过getUnsafe方法的单例模式实现的,调用JVM本地方法registerNatives()和sun.reflect.Reflection,通过Reflection的getCallerClass判断当前调用的类是否是主类加载器(BootStrap classLoader)加载的,否则的话抛出一个SecurityException。这也证明了一个问题,那就是只有由主类加载器(BootStrap classLoader)加载的类才能调用这个类中的方法。


2、操作属性方法


(1)public native Object getObject(Object o, long offset);


通过给定的Java变量获取引用值。这里实际上是获取一个Java对象o中,获取偏移地址为offset的属性的值,此方法可以突破修饰符的抑制,也就是无视private、protected和default修饰符。类似的方法有getInt、getDouble等等。同理还有putObject方法。


(2)public native Object getObjectVolatile(Object o, long offset);


强制从主存中获取属性值。类似的方法有getIntVolatile、getDoubleVolatile等等。同理还有putObjectVolatile。


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

设置o对象中offset偏移地址offset对应的Object型field的值为指定值x。这是一个有序或者有延迟的putObjectVolatile方法,并且不保证值的改变被其他线程立即看到。只有在field被volatile修饰并且期望被修改的时候使用才会生效。类似的方法有putOrderedInt和putOrderedLong。


(4)public native long staticFieldOffset(Field f);

返回给定的静态属性在它的类的存储分配中的位置(偏移地址)。


(5)public native long objectFieldOffset(Field f);

返回给定的非静态属性在它的类的存储分配中的位置(偏移地址)。


(6)public native Object staticFieldBase(Field f);


返回给定的静态属性的位置,配合staticFieldOffset方法使用。


3、操作数组


(1)public native int arrayBaseOffset(Class<?> arrayClass);

返回数组类型的第一个元素的偏移地址(基础偏移地址)。


(2)public native int arrayIndexScale(Class<?> arrayClass);

返回数组中元素与元素之间的偏移地址的增量。

这两个方法配合使用就可以定位到任何一个元素的地址。


4、内存管理


(1)public native int addressSize();

获取本地指针的大小(单位是byte),通常值为4或者8。常量ADDRESS_SIZE就是调用此方法。


(2)public native int pageSize();

获取本地内存的页数,此值为2的幂次方。


(3)public native long allocateMemory(long bytes);

分配一块新的本地内存,通过bytes指定内存块的大小(单位是byte),返回新开辟的内存的地址。


(4)public native long reallocateMemory(long address, long bytes);

通过指定的内存地址address重新调整本地内存块的大小,调整后的内存块大小通过bytes指定(单位为byte)。


(5)public native void setMemory(Object o, long offset, long bytes, byte value);

将给定内存块中的所有字节设置为固定值(通常是0)。


5、线程挂起和恢复


(1)public native void unpark(Object thread);

释放被park创建的在一个线程上的阻塞。由于其不安全性,因此必须保证线程是存活的。


(2)public native void park(boolean isAbsolute, long time);`

阻塞当前线程,一直等道unpark方法被调用。


6、内存屏障


(1)public native void loadFence();

在该方法之前的所有读操作,一定在load屏障之前执行完成。


(2)public native void storeFence();

在该方法之前的所有写操作,一定在store屏障之前执行完成


(3)public native void fullFence();

在该方法之前的所有读写操作,一定在full屏障之前执行完成,这个内存屏障相当于上面两个(load屏障和store屏障)的合体功能。


7、CAS机制


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

这个Unsafe类其实是贯穿到整个java并发包体系中的,不管是你看原子包还是lock包底部都有这样的一个类,我们需要记住的不是每一个方法,而是上面七类的标题。也就是具体有什么功能。


三、使用


说了这么久的源码在这里才介绍其使用,是因为官方并不推荐我们使用,也就是说我们无法直接new出来一个Unsafe类出来,那我们该如何使用呢?在很久之前我曾写过一篇介绍java反射机制的文章,没错就是这个反射机制,牛的不行。Unsafe就可以通过反射机制来获取。

public class UnsafeTest {
    public static void main(String[] args) throws Exception {
        //这里的theUnsafe就是我们源码中的那个theUnsafe
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);
        //1、创建对象实例
        Author author = (Author) unsafe.allocateInstance(Author.class);
        //2、操作对象的属性
        Field ageField = Author.class.getDeclaredField("age");
        long fieldOffset = unsafe.objectFieldOffset(ageField);
        //3、操作数组
        String[] strings = new String[]{"1", "2", "3"};
        long i = unsafe.arrayBaseOffset(String[].class);
        //4、操作内存
        long address = unsafe.allocateMemory(8L);        
    }
}

在这里只是给出一些简单的例子,其用法可以参照源码分析中那七个方向。

仔细阅读jdk的源码有时候真的可以收获一些意想不到的东西。

相关文章
|
2月前
|
Java 开发者
在 Java 中,一个类可以实现多个接口吗?
这是 Java 面向对象编程的一个重要特性,它提供了极大的灵活性和扩展性。
156 57
|
8天前
|
JSON Java Apache
Java基础-常用API-Object类
继承是面向对象编程的重要特性,允许从已有类派生新类。Java采用单继承机制,默认所有类继承自Object类。Object类提供了多个常用方法,如`clone()`用于复制对象,`equals()`判断对象是否相等,`hashCode()`计算哈希码,`toString()`返回对象的字符串表示,`wait()`、`notify()`和`notifyAll()`用于线程同步,`finalize()`在对象被垃圾回收时调用。掌握这些方法有助于更好地理解和使用Java中的对象行为。
|
2月前
|
存储 缓存 安全
java 中操作字符串都有哪些类,它们之间有什么区别
Java中操作字符串的类主要有String、StringBuilder和StringBuffer。String是不可变的,每次操作都会生成新对象;StringBuilder和StringBuffer都是可变的,但StringBuilder是非线程安全的,而StringBuffer是线程安全的,因此性能略低。
58 8
|
2月前
|
存储 安全 Java
java.util的Collections类
Collections 类位于 java.util 包下,提供了许多有用的对象和方法,来简化java中集合的创建、处理和多线程管理。掌握此类将非常有助于提升开发效率和维护代码的简洁性,同时对于程序的稳定性和安全性有大有帮助。
75 17
|
2月前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
2月前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
137 4
|
2月前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
83 2
|
2月前
|
存储 安全 Java
如何保证 Java 类文件的安全性?
Java类文件的安全性可以通过多种方式保障,如使用数字签名验证类文件的完整性和来源,利用安全管理器和安全策略限制类文件的权限,以及通过加密技术保护类文件在传输过程中的安全。
65 4
|
2月前
|
Java 数据格式 索引
使用 Java 字节码工具检查类文件完整性的原理是什么
Java字节码工具通过解析和分析类文件的字节码,检查其结构和内容是否符合Java虚拟机规范,确保类文件的完整性和合法性,防止恶意代码或损坏的类文件影响程序运行。
53 5
|
2月前
|
Java API Maven
如何使用 Java 字节码工具检查类文件的完整性
本文介绍如何利用Java字节码工具来检测类文件的完整性和有效性,确保类文件未被篡改或损坏,适用于开发和维护阶段的代码质量控制。
110 5