Java中的魔法类:sun.misc.Unsafe示例详解

前言

Unsafe类在jdk 源码的多个类中用到,这个类的提供了一些绕开JVM的更底层功能,基于它的实现可以提高效率。但是,它是一把双刃剑:正如它的名字所预示的那样,它是Unsafe的,它所分配的内存需要手动free(不被GC回收)。Unsafe类,提供了JNI某些功能的简单替代:确保高效性的同时,使事情变得更简单。

这个类是属于sun.* API中的类,并且它不是J2SE中真正的一部份,因此你可能找不到任何的官方文档,更可悲的是,它也没有比较好的代码文档。

这篇文章主要是以下文章的整理、翻译。

http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/

1. Unsafe API的大部分方法都是native实现,它由105个方法组成,主要包括以下几类:

(1)Info相关。主要返回某些低级别的内存信息:addressSize(), pageSize()

(2)Objects相关。主要提供Object和它的域操纵方法:allocateInstance(),objectFieldOffset()

(3)Class相关。主要提供Class和它的静态域操纵方法:staticFieldOffset(),defineClass(),defineAnonymousClass(),ensureClassInitialized()

(4)Arrays相关。数组操纵方法:arrayBaseOffset(),arrayIndexScale()

(5)Synchronization相关。主要提供低级别同步原语(如基于CPU的CAS(Compare-And-Swap)原语):monitorEnter(),tryMonitorEnter(),monitorExit(),compareAndSwapInt(),putOrderedInt()

(6)Memory相关。直接内存访问方法(绕过JVM堆直接操纵本地内存):allocateMemory(),copyMemory(),freeMemory(),getAddress(),getInt(),putInt()

2. Unsafe类实例的获取

Unsafe类设计只提供给JVM信任的启动类加载器所使用,是一个典型的单例模式类。它的实例获取方法如下:

public static Unsafe getUnsafe() {
 Class cc = sun.reflect.Reflection.getCallerClass(2);
 if (cc.getClassLoader() != null)
  throw new SecurityException("Unsafe");
 return theUnsafe;
}

非启动类加载器直接调用Unsafe.getUnsafe()方法会抛出SecurityException(具体原因涉及JVM类的双亲加载机制)。

解决办法有两个,其一是通过JVM参数-Xbootclasspath指定要使用的类为启动类,另外一个办法就是java反射了。

Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);

通过将private单例实例暴力设置accessible为true,然后通过Field的get方法,直接获取一个Object强制转换为Unsafe。在IDE中,这些方法会被标志为Error,可以通过以下设置解决:

Preferences -> Java -> Compiler -> Errors/Warnings ->
Deprecated and restricted API -> Forbidden reference -> Warning

3. Unsafe类“有趣”的应用场景

(1)绕过类初始化方法。当你想要绕过对象构造方法、安全检查器或者没有public的构造方法时,allocateInstance()方法变得非常有用。

class A {
 private long a; // not initialized value
 public A() {
  this.a = 1; // initialization
 }
 public long a() { return this.a; }
}

以下是构造方法、反射方法和allocateInstance()的对照

A o1 = new A(); // constructor
o1.a(); // prints 1

A o2 = A.class.newInstance(); // reflection
o2.a(); // prints 1

A o3 = (A) unsafe.allocateInstance(A.class); // unsafe
o3.a(); // prints 0

allocateInstance()根本没有进入构造方法,在单例模式时,我们似乎看到了危机。

(2)内存修改

内存修改在c语言中是比较常见的,在Java中,可以用它绕过安全检查器。

考虑以下简单准入检查规则:

class Guard {
 private int ACCESS_ALLOWED = 1;

 public boolean giveAccess() {
  return 42 == ACCESS_ALLOWED;
 }
}

在正常情况下,giveAccess总会返回false,但事情不总是这样

Guard guard = new Guard();
guard.giveAccess(); // false, no access

// bypass
Unsafe unsafe = getUnsafe();
Field f = guard.getClass().getDeclaredField("ACCESS_ALLOWED");
unsafe.putInt(guard, unsafe.objectFieldOffset(f), 42); // memory corruption

guard.giveAccess(); // true, access granted

通过计算内存偏移,并使用putInt()方法,类的ACCESS_ALLOWED被修改。在已知类结构的时候,数据的偏移总是可以计算出来(与c++中的类中数据的偏移计算是一致的)。

(3)实现类似C语言的sizeOf()函数

通过结合Java反射和objectFieldOffset()函数实现一个C-like sizeOf()函数。

public static long sizeOf(Object o) {
 Unsafe u = getUnsafe();
 HashSet fields = new HashSet();
 Class c = o.getClass();
 while (c != Object.class) {
  for (Field f : c.getDeclaredFields()) {
   if ((f.getModifiers() & Modifier.STATIC) == 0) {
    fields.add(f);
   }
  }
  c = c.getSuperclass();
 }

 // get offset
 long maxSize = 0;
 for (Field f : fields) {
  long offset = u.objectFieldOffset(f);
  if (offset > maxSize) {
   maxSize = offset;
  }
 }
 return ((maxSize/8) + 1) * 8; // padding
}

算法的思路非常清晰:从底层子类开始,依次取出它自己和它的所有超类的非静态域,放置到一个HashSet中(重复的只计算一次,Java是单继承),然后使用objectFieldOffset()获得一个最大偏移,最后还考虑了对齐。

在32位的JVM中,可以通过读取class文件偏移为12的long来获取size。

public static long sizeOf(Object object){
 return getUnsafe().getAddress(
  normalize(getUnsafe().getInt(object, 4L)) + 12L);
}

其中normalize()函数是一个将有符号int转为无符号long的方法

private static long normalize(int value) {
 if(value >= 0) return value;
 return (0L >>> 32) & value;
}

两个sizeOf()计算的类的尺寸是一致的。最标准的sizeOf()实现是使用java.lang.instrument,但是,它需要指定命令行参数-javaagent。

(4)实现Java浅复制

标准的浅复制方案是实现Cloneable接口或者自己实现的复制函数,它们都不是多用途的函数。通过结合sizeOf()方法,可以实现浅复制。

static Object shallowCopy(Object obj) {
 long size = sizeOf(obj);
 long start = toAddress(obj);
 long address = getUnsafe().allocateMemory(size);
 getUnsafe().copyMemory(start, address, size);
 return fromAddress(address);
}

以下的toAddress()和fromAddress()分别将对象转换到它的地址以及相反操作。

static long toAddress(Object obj) {
 Object[] array = new Object[] {obj};
 long baseOffset = getUnsafe().arrayBaseOffset(Object[].class);
 return normalize(getUnsafe().getInt(array, baseOffset));
}

static Object fromAddress(long address) {
 Object[] array = new Object[] {null};
 long baseOffset = getUnsafe().arrayBaseOffset(Object[].class);
 getUnsafe().putLong(array, baseOffset, address);
 return array[0];
}

以上的浅复制函数可以应用于任意java对象,它的尺寸是动态计算的。

(5)消去内存中的密码

密码字段存储在String中,但是,String的回收是受到JVM管理的。最安全的做法是,在密码字段使用完之后,将它的值覆盖。

Field stringValue = String.class.getDeclaredField("value");
stringValue.setAccessible(true);
char[] mem = (char[]) stringValue.get(password);
for (int i=0; i < mem.length; i++) {
 mem[i] = '?';
}

(6)动态加载类

标准的动态加载类的方法是Class.forName()(在编写jdbc程序时,记忆深刻),使用Unsafe也可以动态加载java 的class文件。

byte[] classContents = getClassContent();
Class c = getUnsafe().defineClass(
    null, classContents, 0, classContents.length);
 c.getMethod("a").invoke(c.newInstance(), null); // 1
getClassContent()方法,将一个class文件,读取到一个byte数组。

private static byte[] getClassContent() throws Exception {
 File f = new File("/home/mishadoff/tmp/A.class");
 FileInputStream input = new FileInputStream(f);
 byte[] content = new byte[(int)f.length()];
 input.read(content);
 input.close();
 return content;
}

动态加载、代理、切片等功能中可以应用。

(7)包装受检异常为运行时异常。

getUnsafe().throwException(new IOException());

当你不希望捕获受检异常时,可以这样做(并不推荐)。

(8)快速序列化

标准的java Serializable速度很慢,它还限制类必须有public无参构造函数。Externalizable好些,它需要为要序列化的类指定模式。流行的高效序列化库,比如kryo依赖于第三方库,会增加内存的消耗。可以通过getInt(),getLong(),getObject()等方法获取类中的域的实际值,将类名称等信息一起持久化到文件。kryo有使用Unsafe的尝试,但是没有具体的性能提升的数据。(http://code.google.com/p/kryo/issues/detail?id=75)

(9)在非Java堆中分配内存

使用java 的new会在堆中为对象分配内存,并且对象的生命周期内,会被JVM GC管理。

class SuperArray {
 private final static int BYTE = 1;

 private long size;
 private long address;

 public SuperArray(long size) {
  this.size = size;
  address = getUnsafe().allocateMemory(size * BYTE);
 }

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

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

 public long size() {
  return size;
 }
}

Unsafe分配的内存,不受Integer.MAX_VALUE的限制,并且分配在非堆内存,使用它时,需要非常谨慎:忘记手动回收时,会产生内存泄露;非法的地址访问时,会导致JVM崩溃。在需要分配大的连续区域、实时编程(不能容忍JVM延迟)时,可以使用它。java.nio使用这一技术。

(10)Java并发中的应用

通过使用Unsafe.compareAndSwap()可以用来实现高效的无锁数据结构。

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

 public CASCounter() throws Exception {
  unsafe = getUnsafe();
  offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter"));
 }

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

 @Override
 public long getCounter() {
  return counter;
 }
}

通过测试,以上数据结构与java的原子变量的效率基本一致,Java原子变量也使用Unsafe的compareAndSwap()方法,而这个方法最终会对应到cpu的对应原语,因此,它的效率非常高。这里有一个实现无锁HashMap的方案(http://www.azulsystems.com/about_us/presentations/lock-free-hash ,这个方案的思路是:分析各个状态,创建拷贝,修改拷贝,使用CAS原语,自旋锁),在普通的服务器机器(核心<32),使用ConcurrentHashMap(JDK8以前,默认16路分离锁实现,JDK8中ConcurrentHashMap已经使用无锁实现)明显已经够用。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

时间: 2018-05-23

Java中unsafe操作实例总结

Unsafe是Java无锁操作的基石,在无锁并发类中都少不了它们的身影,比如ConcurrentHashMap, ConcurrentLinkedQueue, 都是由Unsafe类来实现的.相对于与Java中的锁,它基本无开销,会原地等待.本文主要介绍下Unsafe中的主要操作. 1 compareAndSwap /** * 比较obj的offset处内存位置中的值和期望的值,如果相同则更新.此更新是不可中断的. * * @param obj 需要更新的对象 * @param offset ob

详解Java多线程编程中LockSupport类的线程阻塞用法

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语. LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程,而且park()和unpark()不会遇到"Thread.suspend 和 Thread.resume所可能引发的死锁"问题. 因为park() 和 unpark()有许可的存在:调用 park() 的线程和另一个试图将其 unpark() 的线程之间的竞争将保持活性. 基本用法 LockSupport 很类似于二元信号

Java并发编程学习之Unsafe类与LockSupport类源码详析

一.Unsafe类的源码分析 JDK的rt.jar包中的Unsafe类提供了硬件级别的原子操作,Unsafe里面的方法都是native方法,通过使用JNI的方式来访问本地C++实现库. rt.jar 中 Unsafe 类主要函数讲解, Unsafe 类提供了硬件级别的原子操作,可以安全的直接操作内存变量,其在 JUC 源码中被广泛的使用,了解其原理为研究 JUC 源码奠定了基础. 首先我们先了解Unsafe类中主要方法的使用,如下: 1.long objectFieldOffset(Field

简单谈一谈Java中的Unsafe类

Unsafe类是啥? Java最初被设计为一种安全的受控环境.尽管如此,Java HotSpot还是包含了一个"后门",提供了一些可以直接操控内存和线程的低层次操作.这个后门类--sun.misc.Unsafe--被JDK广泛用于自己的包中,如java.nio和java.util.concurrent.但是丝毫不建议在生产环境中使用这个后门.因为这个API十分不安全.不轻便.而且不稳定.这个不安全的类提供了一个观察HotSpot JVM内部结构并且可以对其进行修改.有时它可以被用来在不

java线程阻塞中断与LockSupport使用介绍

上周五和周末,工作忙里偷闲,在看java cocurrent中也顺便再温故了一下Thread.interrupt和java 5之后的LockSupport的实现. 在介绍之前,先抛几个问题. Thread.interrupt()方法和InterruptedException异常的关系?是由interrupt触发产生了InterruptedException异常? Thread.interrupt()会中断线程什么状态的工作? RUNNING or BLOCKING? 一般Thread编程需要关注

一篇看懂Java中的Unsafe类

前言 本文主要给大家介绍了关于Java中Unsafe类的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧 1.Unsafe类介绍 Unsafe类是在sun.misc包下,不属于Java标准.但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty.Hadoop.Kafka等. 使用Unsafe可用来直接访问系统内存资源并进行自主管理,Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用. Un

Java concurrency之LockSupport_动力节点Java学院整理

LockSupport介绍 LockSupport是用来创建锁和其他同步类的基本线程阻塞原语. LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程,而且park()和unpark()不会遇到"Thread.suspend 和 Thread.resume所可能引发的死锁"问题. 因为park() 和 unpark()有许可的存在:调用 park() 的线程和另一个试图将其 unpark() 的线程之间的竞争将保持活性. LockSupport

十大常见Java String问题_动力节点Java学院整理

本文介绍Java中关于String最常见的10个问题: 1. 字符串比较,使用 "==" 还是 equals() ? 简单来说, "==" 判断两个引用的是不是同一个内存地址(同一个物理对象). 而 equals 判断两个字符串的值是否相等. 除非你想判断两个string引用是否同一个对象,否则应该总是使用 equals()方法. 如果你了解 字符串的驻留 ( String Interning ) 则会更好地理解这个问题 2. 对于敏感信息,为何使用char[]要比

Java数据结构之队列(动力节点Java学院整理)

队列的定义: 队列(Queue)是只允许在一端进行插入,而在另一端进行删除的运算受限的线性表. (1)允许删除的一端称为队头(Front). (2)允许插入的一端称为队尾(Rear). (3)当队列中没有元素时称为空队列. (4)队列亦称作先进先出(First In First Out)的线性表,简称为FIFO表. 队列的修改是依先进先出的原则进行的.新来的成员总是加入队尾,每次离开的成员总是队列头上的(不允许中途离队). 队列的存储结构及实现 队列的顺序存储结构 (1) 顺序队列的定义: 队列

Java二进制操作(动力节点Java学院整理)

移位 位运算中大多数操作都是向左移位和向右移位.在Java中,这对应着<<和>>这两个操作符,示例如下: /* 00000001 << 1 = 00000010 */ 1 << 1 == 2 /* 00000001 << 3 = 00001000 */ 1 << 3 == 8 /* 11111111 11111111 11111111 11110000 >> 4 = 11111111 11111111 11111111 1

Java数组优点和缺点_动力节点Java学院整理

数组是Java语言的基本知识之一,在深入学习Java基础后,我们都知道那些容器,在之后,在我们学习.工作的过程中基本就是使用容器了,很少很使用数组,那么为什么还要有数组呢,我也是今天才遇到这个问题,专门的找资料学习了一下. 数组与其他种类的容器之间的区别有三方面:效率.类型和保存基本类型的能力,当然现在有泛型,保存类型的区别已经不大了. 数组较容器,最大的优点就是效率.在Java中,数组是一种效率最高的存储和随机访问对象引用序列的方式,数组就是一个简单的线性序列,这使得元素访问非常快速,无论使用

Java Map简介_动力节点Java学院整理

Map简介 将键映射到值的对象.一个映射不能包含重复的键:每个键最多只能映射到一个值.此接口取代 Dictionary 类,后者完全是一个抽象类,而不是一个接口. Map 接口提供三种collection 视图,允许以键集.值集或键-值映射关系集的形式查看某个映射的内容.映射顺序 定义为迭代器在映射的 collection 视图上返回其元素的顺序.某些映射实现可明确保证其顺序,如 TreeMap 类:另一些映射实现则不保证顺序,如HashMap 类. 注:将可变对象用作映射键时必须格外小心.当对

Java线程让步_动力节点Java学院整理

yield()介绍 yield()的作用是让步.它能让当前线程由"运行状态"进入到"就绪状态",从而让其它具有相同优先级的等待线程获取执行权:但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权:也有可能是当前线程又进入到"运行状态"继续运行! yield()示例 下面,通过示例查看它的用法. // YieldTest.java的源码 class ThreadA extends Thread{ public

Java关键字this(动力节点Java学院整理)

我们通常在用Java中的this关键字的时候,都知道this是代表正在调用这个类的方法的当前实例.通常情况下理解this关键字还是很容易的,但是在我初学的时候,有一个疑问却一直不能很清晰的理解,现在慢慢的理解了,就想把它记下来,也许有人和我有相同的疑问,说不定可以帮助到别人.我们还是先简单的看看通常情况下this的作用吧.比如下面的代码: public class Leaf { private int i = 0; Leaf increment() { i++; return this; } v

Java初学者问题图解(动力节点Java学院整理)

1. String对象不可改变的特性 下图显示了如下代码运行的过程: String s = "abcd"; s = s.concat("ef"); 图1 2. equals()与hashCode()方法协作约定 HashCode(哈希编码,散列码)是设计了用来提高性能的. equals()与hashCode()方法之间的关系可以概括为: 1.如果两个对象相等(equal),那么必须拥有相同的哈希码(hash code) 2.即使两个对象有相同的哈希值(hash co

Java Iterator迭代器_动力节点Java学院整理

迭代器是一种模式,它可以使得对于序列类型的数据结构的遍历行为与被遍历的对象分离,即我们无需关心该序列的底层结构是什么样子的.只要拿到这个对象,使用迭代器就可以遍历这个对象的内部. 1.Iterator Java提供一个专门的迭代器<<interface>>Iterator,我们可以对某个序列实现该interface,来提供标准的Java迭代器.Iterator接口实现后的功能是"使用"一个迭代器. 文档定义: Package java.util; publici