Java中Unsafe类详解

本文主要介绍一下 JDK 里的核弹之 sun.misc.Unsafe 类。

0、概述

本文主要介绍一下 JDK 里的核弹之 sun.misc.Unsafe 类。这个类提供了更加底层的操作,主要是服务于 Java 的核心类库。普通用户一般情况下,并不会直接用到该类。

1、 获取 Unsafe 实例

Unsafe 提供了 getUnsafe 方法,代码实例如下所示:

public class Main {
 public static void main(String[] args) {
     Unsafe.getUnsafe();
 }
}

但是,会有如下报错:

Exception in thread "main" java.lang.SecurityException: Unsafe
 at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
 at com.bendcap.java.jvm.unsafe.Main.main(Main.java:13)

这是因为 Unsafe 类主要是 JDK 内部使用,并不提供给普通用户调用,也就是其名字所暗示的那样,这些操作不安全。

但是我们仍让可以通过反射获取到实例:

public static Unsafe createUnsafe() {
    try {
        Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
        Field field = unsafeClass.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        return unsafe;
       } catch (Exception e) {
        e.printStackTrace();
    }
 return null;
 }

2、 不调用构造方法方法创建对象

我们知道,JVM 在创建对象实例的时候会调用默认或者有参构造方法,在字节码中对应 init 方法,那如何实例话一个对象,而不调用 init 方法呢? 答案就是 Unsafe

假设我们有一个类 Person:

class Person {
 private String name;
 private int age;
 .....
}

那么我们可以通过 Unsafe 在内存中直接创建 Person 的实例:

public Person instancePerson() {
        Person person = null;
        try {
            person = (Person) createUnsafe().allocateInstance(Person.class);
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
        person.setName("Unsafer");
        return person;
    }

Unsafe 只会分配 Person 对应的内存空间,而不触发构造函数。
: Class 文件中的 clinit 仍然会执行。

举一个生产实例。Gson 中反序列化的时候,ReflectiveTypeAdapterFactory 类负责通过反射设置字段值,其中在或许反序列化对应的 Class 实例的时候就用到了 Unsafe, 关键代码摘抄如下:

public abstract class UnsafeAllocator {
  public abstract <T> T newInstance(Class<T> c) throws Exception;

  public static UnsafeAllocator create() {
    // try JVM
    // public class Unsafe {
    //   public Object allocateInstance(Class<?> type);
    // }
    try {
      Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
      Field f = unsafeClass.getDeclaredField("theUnsafe");
      f.setAccessible(true);
      final Object unsafe = f.get(null);
      final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class);
      return new UnsafeAllocator() {
        @Override
        @SuppressWarnings("unchecked")
        public <T> T newInstance(Class<T> c) throws Exception {
          assertInstantiable(c);
          return (T) allocateInstance.invoke(unsafe, c);
        }
      };
    } catch (Exception ignored) {
    }
    ...
    // give up
    return new UnsafeAllocator() {
      @Override
      public <T> T newInstance(Class<T> c) {
        throw new UnsupportedOperationException("Cannot allocate " + c);
      }
    };
  }
}

3、改变私有字段

假设我们有如下类:

public class SecretHolder {
    private int SECRET_VALUE = 0;

    public boolean secretValueDisclosed() {
        return SECRET_VALUE == 1;
    }
}

下面我们通过 Unsafe 来改变私有属性 SECRET_VALUE 的值。

       SecretHolder secretHolder = new SecretHolder();
       Field field = null;
       try {
           field = secretHolder.getClass().getDeclaredField("SECRET_VALUE");
       } catch (NoSuchFieldException e) {
           e.printStackTrace();
       }
       Unsafe unsafe = createUnsafe();
       unsafe.putInt(secretHolder, unsafe.objectFieldOffset(field), 1);
       return secretHolder.secretValueDisclosed();
}

我们通过 unsafe.putInt 直接改变了 SecretHolder 的私有属性的值。一旦我们通过反射获得了类的私有属性字段,我们就可以直接操作它的值。

4、 抛出异常而不会触发 CE(Checked Exception)

通过 unsafe.throwExceptio 创建的异常不会被编译器检查,方法的调用者也不需要处理异常。

public void throwException() {
       Unsafe unsafe = createUnsafe();
       unsafe.throwException(new IOException());
   }

5、 堆外内存

Java 中对象分配一般是在 Heap 中进行的(例外是 TLAB等),当应用内存不足的时候,可以通过触发 GC 进行垃圾回收,但是如果有大量对象存活到永久代,并且仍然引用可达,那么我们就需要堆外内存(Off-Heap Memory)来缓解频繁 GC 造成的压力。

Unsafe.allocateMemory 给了我们在直接内存中分配对象的能力,这块内存是非堆内存,因此,不会受到 GC 的频繁分析和干扰。

虽然这样可以缓解大量对象占用内存对 GC 和 JVM 造成的压力,这也就需要我们手动管理内存,因此,在合适的事后我们需要手动调用 freeMemory 来释放内存。

举例,我们在内存中分配字节数组:

public class OffHeapArray {
    private final static int BYTE = 1;
    private long size;
    private long address;
    private Unsafe unsafe;

    public OffHeapArray(long size, Unsafe unsafe) {
        this.size = size;
        this.unsafe = unsafe;
        address = unsafe.allocateMemory(size * BYTE);
    }

    public void set(long i, byte value) {
        unsafe.putByte(address + i * BYTE, value);
    }

    public int get(long idx) {
        return unsafe.getByte(address + idx * BYTE);
    }

    public long size() {
        return size;
    }

    public void freeMemory() {
        unsafe.freeMemory(address);
    }
}

6、CAS(CompareAndSwap)

java.concurrent 包中提供了大量并发相关的操作,例如 AtomicInteger 就用了 Unsafe.compareAndSwap 操作来实现 lock-free 的操作,保证更好的性能。

假设我们做一个累加器,开启 1000 个线程,每个线程循环累加 10_000 次。

public class CASCounter {
    private Unsafe unsafe;
    private volatile long counter = 0;
    private long offset;

    public CASCounter(Unsafe unsafe) {
        this.unsafe = unsafe;
        try {
            offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    public void increment() {
        long before = counter;
        while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
            before = counter;
        }
    }


    public long getCounter() {
        return counter;
    }
}

counter 需要声明为 volatile 来保证对所有线程可见(参考 Java 内存模型以及指令集重排)

这里的关键是 increment 方法,我们在 while 循环里不断尝试调用 compareAndSwapLong,检查在我们方法内部累加的同事,counter 的值有没有被其他线程改变。如有没有,就提交更改,如果不一致,那么继续尝试提交更改。

7、Park/Unpark

Park/Unpark 主要是被 JVM 用来做线程的上下文切换。当线程需要等待某个条件的时候,JVM 会调用 park 来阻塞该线程。

这和 Object.await 非常类似,但是 park 是操作系统调用,因此,在某些操作系统架构上,这会带来更好的性能。

当线程阻塞后,需要再次执行, JVM 会调用 unpark 方法使得该线程变得活跃。

结论

虽然 Unsafe 看起来不会被用到,但是能帮助我们更好的理解 JVM 以及 JDK 中 lock-free 的实现。还有一点就是 Off-Heap Memory, 如果做服务端开发中确实遇到了大内存对象并且常驻内存的情况,堆外分配不失为一个好的策略来减轻 GC 以及 GC 带来的系统负担(可参见 R 大在阿里 JVM 中所做的一些优化),与之对应的就是 TLAB(thread local allocation buffer)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,635评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,628评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,971评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,986评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,006评论 6 394
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,784评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,475评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,364评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,860评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,008评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,152评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,829评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,490评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,035评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,428评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,127评论 2 356

推荐阅读更多精彩内容