Java Magic. Part 4: sun.misc.Unsafe

简介:

原文地址 译文地址 译者:许巧辉 校对:梁海舰

Java是一门安全的编程语言,防止程序员犯很多愚蠢的错误,它们大部分是基于内存管理的。但是,有一种方式可以有意的执行一些不安全、容易犯错的操作,那就是使用Unsafe类。

本文是sun.misc.Unsafe公共API的简要概述,及其一些有趣的用法。

Unsafe 实例

在使用Unsafe之前,我们需要创建Unsafe对象的实例。这并不像Unsafe unsafe = new Unsafe()这么简单,因为Unsafe的构造器是私有的。它也有一个静态的getUnsafe()方法,但如果你直接调用Unsafe.getUnsafe(),你可能会得到SecurityException异常。只能从受信任的代码中使用这个方法。


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

这就是Java如何验证代码是否可信。它只检查我们的代码是否由主要的类加载器加载。

我们可以令我们的代码“受信任”。运行程序时,使用bootclasspath 选项,指定系统类路径加上你使用的一个Unsafe路径。


1 java -Xbootclasspath:/usr/jdk1.7.0/jre/lib/rt.jar:. com.mishadoff.magic.UnsafeClient

但这太难了。

Unsafe类包含一个私有的、名为theUnsafe的实例,我们可以通过Java反射窃取该变量。


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

注意:忽略你的IDE。比如:eclipse显示”Access restriction…”错误,但如果你运行代码,它将正常运行。如果这个错误提示令人烦恼,可以通过以下设置来避免:


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

Unsafe API

sun.misc.Unsafe类包含105个方法。实际上,对各种实体操作有几组重要方法,其中的一些如下:

Info.仅返回一些低级的内存信息

  • addressSize
  • pageSize

Objects.提供用于操作对象及其字段的方法

  • allocateInstance
  • objectFieldOffset

Classes.提供用于操作类及其静态字段的方法

  • staticFieldOffset
  • defineClass
  • defineAnonymousClass
  • ensureClassInitialized

Arrays.操作数组

  • arrayBaseOffset
  • arrayIndexScale

Synchronization.低级的同步原语

  • monitorEnter
  • tryMonitorEnter
  • monitorExit
  • compareAndSwapInt
  • putOrderedInt

Memory.直接内存访问方法

  • allocateMemory
  • copyMemory
  • freeMemory
  • getAddress
  • getInt
  • putInt

有趣的用例

避免初始化

当你想要跳过对象初始化阶段,或绕过构造器的安全检查,或实例化一个没有任何公共构造器的类,allocateInstance方法是非常有用的。考虑以下类:


1 class A {
2     private long a; // not initialized value
3  
4     public A() {
5         this.a = 1; // initialization
6     }
7  
8     public long a() { return this.a; }
9 }

使用构造器、反射和unsafe初始化它,将得到不同的结果。


1 A o1 = new A(); // constructor
2 o1.a(); // prints 1
3  
4 A o2 = A.class.newInstance(); // reflection
5 o2.a(); // prints 1
6  
7 A o3 = (A) unsafe.allocateInstance(A.class); // unsafe
8 o3.a(); // prints 0

想想所有单例发生了什么。

内存崩溃(Memory corruption)

这对于每个C程序员来说是常见的。顺便说一下,它是绕过安全的常用技术。

考虑下那些用于检查“访问规则”的简单类:


1 class Guard {
2        private int ACCESS_ALLOWED = 1;
3  
4        public boolean giveAccess() {
5               return 42 == ACCESS_ALLOWED;
6        }
7 }

客户端代码是非常安全的,并且通过调用giveAccess()来检查访问规则。可惜,对于客户,它总是返回false。只有特权用户可以以某种方式改变ACCESS_ALLOWED常量的值并且得到访问(giveAccess()方法返回true,译者注)。

实际上,这并不是真的。演示代码如下:


1 Guard guard = new Guard();
2 guard.giveAccess();   // false, no access
3  
4 // bypass
5 Unsafe unsafe = getUnsafe();
6 Field f = guard.getClass().getDeclaredField("ACCESS_ALLOWED");
7 unsafe.putInt(guard, unsafe.objectFieldOffset(f), 42); // memory corruption
8  
9 guard.giveAccess(); // true, access granted

现在所有的客户都拥有无限制的访问权限。

实际上,反射可以实现相同的功能。但值得关注的是,我们可以修改任何对象,甚至没有这些对象的引用。

例如,有一个guard对象,所在内存中的位置紧接着在当前guard对象之后。我们可以用以下代码来修改它的ACCESS_ALLOWED字段:


1 unsafe.putInt(guard, 16 + unsafe.objectFieldOffset(f), 42); // memory corruption

注意:我们不必持有这个对象的引用。16是Guard对象在32位架构上的大小。我们可以手工计算它,或者通过使用sizeOf方法(它的定义,如下节)。

sizeOf

使用objectFieldOffset方法可以实现C-风格(C-style)的sizeof方法。这个实现返回对象的自身内存大小(译者注:shallow size)。


01 public static long sizeOf(Object o) {
02     Unsafe u = getUnsafe();
03     HashSet<Field> fields = new HashSet<Field>();
04     Class c = o.getClass();
05     while (c != Object.class) {
06         for (Field f : c.getDeclaredFields()) {
07             if ((f.getModifiers() & Modifier.STATIC) == 0) {
08                 fields.add(f);
09             }
10         }
11         c = c.getSuperclass();
12     }
13  
14     // get offset
15     long maxSize = 0;
16     for (Field f : fields) {
17         long offset = u.objectFieldOffset(f);
18         if (offset > maxSize) {
19             maxSize = offset;
20         }
21     }
22  
23     return ((maxSize/8) + 1) * 8;   // padding
24 }

算法如下:通过所有非静态字段(包含父类的),获取每个字段的偏移量(offset),找到偏移最大值并填充字节数(padding)。我可能错过一些东西,但思路是明确的。

如果我们仅读取对象的类结构大小值,sizeOf的实现可以更简单,这位于JVM 1.7 32 bit中的偏移量12。


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

normalize是一个为了正确内存地址使用,将有符号的int类型强制转换成无符号的long类型的方法。


1 private static long normalize(int value) {
2     if(value >= 0) return value;
3     return (~0L >>> 32) & value;
4 }

真棒,这个方法返回的结果与我们之前的sizeof方法一样。

实际上,对于良好、安全、准确的sizeof方法,最好使用 java.lang.instrument包,但这需要在JVM中指定agent选项。

浅拷贝(Shallow copy)

为了实现计算对象自身内存大小,我们可以简单地添加拷贝对象方法。标准的解决方案是使用Cloneable修改你的代码,或者在你的对象中实现自定义的拷贝方法,但它不会是多用途的方法。

浅拷贝:


1 static Object shallowCopy(Object obj) {
2     long size = sizeOf(obj);
3     long start = toAddress(obj);
4     long address = getUnsafe().allocateMemory(size);
5     getUnsafe().copyMemory(start, address, size);
6     return fromAddress(address);
7 }

toAddress和fromAddress将对象转换为其在内存中的地址,反之亦然。


01 static long toAddress(Object obj) {
02     Object[] array = new Object[] {obj};
03     long baseOffset = getUnsafe().arrayBaseOffset(Object[].class);
04     return normalize(getUnsafe().getInt(array, baseOffset));
05 }
06  
07 static Object fromAddress(long address) {
08     Object[] array = new Object[] {null};
09     long baseOffset = getUnsafe().arrayBaseOffset(Object[].class);
10     getUnsafe().putLong(array, baseOffset, address);
11     return array[0];
12 }

这个拷贝方法可以用来拷贝任何类型的对象,动态计算它的大小。注意,在拷贝后,你需要将对象转换成特定的类型。

隐藏密码(Hide Password)

Unsafe中,一个更有趣的直接内存访问的用法是,从内存中删除不必要的对象。

检索用户密码的大多数API的签名为byte[]char[],为什么是数组呢?

这完全是出于安全的考虑,因为我们可以删除不需要的数组元素。如果将用户密码检索成字符串,这可以像一个对象一样在内存中保存,而删除该对象只需执行解除引用的操作。但是,这个对象仍然在内存中,由GC决定的时间来执行清除。

创建具有相同大小、假的String对象,来取代在内存中原来的String对象的技巧:


01 String password = new String("l00k@myHor$e");
02 String fake = new String(password.replaceAll(".", "?"));
03 System.out.println(password); // l00k@myHor$e
04 System.out.println(fake); // ????????????
05  
06 getUnsafe().copyMemory(
07           fake, 0L, null, toAddress(password), sizeOf(password));
08  
09 System.out.println(password); // ????????????
10 System.out.println(fake); // ????????????

感觉很安全。

修改:这并不安全。为了真正的安全,我们需要通过反射删除后台char数组:


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

感谢Peter Verhas指定出这一点。

多继承(Multiple Inheritance)

Java中没有多继承。

这是对的,除非我们可以将任意类型转换成我们想要的其他类型。


1 long intClassAddress = normalize(getUnsafe().getInt(new Integer(0), 4L));
2 long strClassAddress = normalize(getUnsafe().getInt("", 4L));
3 getUnsafe().putAddress(intClassAddress + 36, strClassAddress);

这个代码片段将String类型添加到Integer超类中,因此我们可以强制转换,且没有运行时异常。


1 (String) (Object) (new Integer(666))

有一个问题,我们必须预先强制转换对象,以欺骗编译器。

动态类(Dynamic classes)

我们可以在运行时创建一个类,比如从已编译的.class文件中。将类内容读取为字节数组,并正确地传递给defineClass方法。


1 byte[] classContents = getClassContent();
2 Class c = getUnsafe().defineClass(
3               null, classContents, 0, classContents.length);
4     c.getMethod("a").invoke(c.newInstance(), null); // 1

从定义文件(class文件)中读取(代码)如下:


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

当你必须动态创建类,而现有代码中有一些代理, 这是很有用的。

抛出异常(Throw an Exception)

不喜欢受检异常?没问题。


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

该方法抛出受检异常,但你的代码不必捕捉或重新抛出它,正如运行时异常一样。

快速序列化(Fast Serialization)

这更有实用性。

大家都知道,标准Java的Serializable的序列化能力是非常慢的。它同时要求类必须有一个公共的、无参数的构造器。

Externalizable比较好,但它需要定义类序列化的模式。

流行的高性能库,比如kryo具有依赖性,这对于低内存要求来说是不可接受的。

unsafe类可以很容易实现完整的序列化周期。

序列化:

  • 使用反射构建模式对象,类只可做一次。
  • 使用Unsafe方法,如getLonggetIntgetObject等来检索实际字段值。
  • 添加类标识,以便有能力恢复该对象
  • 将它们写入文件或任意输出

你也可以添加压缩(步骤)以节省空间。

反序列化:

  • 创建已序列化对象实例,使用allocateInstance协助(即可),因为不需要任何构造器。
  • 构建模式,与序列化的步骤1相同。
  • 从文件或任意输入中读取所有字段。
  • 使用Unsafe方法,如putLongputIntputObject等来填充该对象。

实际上,在正确的实现过程中还有更多的细节,但思路是明确的。

这个序列化将非常快。

顺便说一下,在kryo中有使用Unsafe的一些尝试http://code.google.com/p/kryo/issues/detail?id=75

大数组(Big Arrays

正如你所知,Java数组大小的最大值为Integer.MAX_VALUE。使用直接内存分配,我们创建的数组大小受限于堆大小。

SuperArray的实现


01 class SuperArray {
02     private final static int BYTE = 1;
03  
04     private long size;
05     private long address;
06  
07     public SuperArray(long size) {
08         this.size = size;
09         address = getUnsafe().allocateMemory(size * BYTE);
10     }
11  
12     public void set(long i, byte value) {
13         getUnsafe().putByte(address + i * BYTE, value);
14     }
15  
16     public int get(long idx) {
17         return getUnsafe().getByte(address + idx * BYTE);
18     }
19  
20     public long size() {
21         return size;
22     }
23 }

简单用法:


1 long SUPER_SIZE = (long)Integer.MAX_VALUE * 2;
2 SuperArray array = new SuperArray(SUPER_SIZE);
3 System.out.println("Array size:" + array.size()); // 4294967294
4 for (int i = 0; i < 100; i++) {
5     array.set((long)Integer.MAX_VALUE + i, (byte)3);
6     sum += array.get((long)Integer.MAX_VALUE + i);
7 }
8 System.out.println("Sum of 100 elements:" + sum);  // 300

实际上,这是堆外内存(off-heap memory)技术,在java.nio包中部分可用。

这种方式的内存分配不在堆上,且不受GC管理,所以必须小心Unsafe.freeMemory()的使用。它也不执行任何边界检查,所以任何非法访问可能会导致JVM崩溃。

这可用于数学计算,代码可操作大数组的数据。此外,这可引起实时程序员的兴趣,可打破GC在大数组上延迟的限制。

并发(Concurrency)

几句关于Unsafe的并发性。compareAndSwap方法是原子的,并且可用来实现高性能的、无锁的数据结构。

比如,考虑问题:在使用大量线程的共享对象上增长值。

首先,我们定义简单的Counter接口:


1 interface Counter {
2     void increment();
3     long getCounter();
4 }

然后,我们定义使用Counter的工作线程CounterClient


01 class CounterClient implements Runnable {
02     private Counter c;
03     private int num;
04  
05     public CounterClient(Counter c, int num) {
06         this.c = c;
07         this.num = num;
08     }
09  
10     @Override
11     public void run() {
12         for (int i = 0; i < num; i++) {
13             c.increment();
14         }
15     }
16 }

测试代码:


01 int NUM_OF_THREADS = 1000;
02 int NUM_OF_INCREMENTS = 100000;
03 ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS);
04 Counter counter = ... // creating instance of specific counter
05 long before = System.currentTimeMillis();
06 for (int i = 0; i < NUM_OF_THREADS; i++) {
07     service.submit(new CounterClient(counter, NUM_OF_INCREMENTS));
08 }
09 service.shutdown();
10 service.awaitTermination(1, TimeUnit.MINUTES);
11 long after = System.currentTimeMillis();
12 System.out.println("Counter result: " + c.getCounter());
13 System.out.println("Time passed in ms:" + (after - before));

第一个无锁版本的计数器:


01 class StupidCounter implements Counter {
02     private long counter = 0;
03  
04     @Override
05     public void increment() {
06         counter++;
07     }
08  
09     @Override
10     public long getCounter() {
11         return counter;
12     }
13 }

输出:


1 Counter result: 99542945
2 Time passed in ms: 679

运行快,但没有线程管理,结果是不准确的。第二次尝试,添加上最简单的java式同步:


01 class SyncCounter implements Counter {
02     private long counter = 0;
03  
04     @Override
05     public synchronized void increment() {
06         counter++;
07     }
08  
09     @Override
10     public long getCounter() {
11         return counter;
12     }
13 }

输出:

1 Counter result: 100000000
2 Time passed in ms: 10136

激进的同步有效,但耗时长。试试ReentrantReadWriteLock


01 class LockCounter implements Counter {
02     private long counter = 0;
03     private WriteLock lock = new ReentrantReadWriteLock().writeLock();
04  
05     @Override
06     public void increment() {
07         lock.lock();
08         counter++;
09         lock.unlock();
10     }
11  
12     @Override
13     public long getCounter() {
14         return counter;
15     }
16 }

输出:


1 Counter result: 100000000
2 Time passed in ms: 8065

仍然正确,耗时较短。atomics的运行效果如何?


01 class AtomicCounter implements Counter {
02     AtomicLong counter = new AtomicLong(0);
03  
04     @Override
05     public void increment() {
06         counter.incrementAndGet();
07     }
08  
09     @Override
10     public long getCounter() {
11         return counter.get();
12     }
13 }

输出:


1 Counter result: 100000000
2 Time passed in ms: 6552

AtomicCounter的运行结果更好。最后,试试Unsafe原始的compareAndSwapLong,看看它是否真的只有特权才能使用它?


01 class CASCounter implements Counter {
02     private volatile long counter = 0;
03     private Unsafe unsafe;
04     private long offset;
05  
06     public CASCounter() throws Exception {
07         unsafe = getUnsafe();
08         offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter"));
09     }
10  
11     @Override
12     public void increment() {
13         long before = counter;
14         while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
15             before = counter;
16         }
17     }
18  
19     @Override
20     public long getCounter() {
21         return counter;
22     }
23 }

输出:


1 Counter result: 100000000
2 Time passed in ms: 6454

看起来似乎等价于atomics。atomics使用Unsafe?(是的)

实际上,这个例子很简单,但它展示了Unsafe的一些能力。

如我所说,CAS原语可以用来实现无锁的数据结构。背后的原理很简单:

  • 有一些状态
  • 创建它的副本
  • 修改它
  • 执行CAS
  • 如果失败,重复尝试

实际上,现实中比你现象的更难。存在着许多问题,如ABA问题、指令重排序等。

如果你真的感兴趣,可以参考lock-free HashMap的精彩展示。

修改:给counter变量添加volatile关键字,以避免无限循环的风险。

结论(Conclusion)

即使Unsafe对应用程序很有用,但(建议)不要使用它。 

目录
相关文章
|
26天前
|
存储 安全 Java
【Java技术专题】「攻破技术盲区」攻破Java技术盲点之unsafe类的使用指南(打破Java的安全管控— sun.misc.unsafe)
【Java技术专题】「攻破技术盲区」攻破Java技术盲点之unsafe类的使用指南(打破Java的安全管控— sun.misc.unsafe)
34 0
|
Oracle Java 关系型数据库
|
Oracle Java 关系型数据库
|
Java API Android开发
Java中的sun.misc.Unsafe包
chronicle项目:https://github.com/peter-lawrey/Java-Chronicle 这个项目是利用mmap机制来实现高效的读写数据,号称每秒写入5到20百万条数据。 作者有个测试,写入1百万条log用时0.234秒,用java自带的logger,用时7.347秒。
859 0
|
13天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第11天】 在Java中,高效的并发编程是提升应用性能和响应能力的关键。本文将探讨Java并发的核心概念,包括线程安全、锁机制、线程池以及并发集合等,同时提供实用的编程技巧和最佳实践,帮助开发者在保证线程安全的前提下,优化程序性能。我们将通过分析常见的并发问题,如竞态条件、死锁,以及如何利用现代Java并发工具来避免这些问题,从而构建更加健壮和高效的多线程应用程序。
|
1天前
|
安全 Java 调度
Java线程:深入理解与实战应用
Java线程:深入理解与实战应用
13 0
|
1天前
|
Java
Java中的并发编程:理解和应用线程池
【4月更文挑战第23天】在现代的Java应用程序中,性能和资源的有效利用已经成为了一个重要的考量因素。并发编程是提高应用程序性能的关键手段之一,而线程池则是实现高效并发的重要工具。本文将深入探讨Java中的线程池,包括其基本原理、优势、以及如何在实际开发中有效地使用线程池。我们将通过实例和代码片段,帮助读者理解线程池的概念,并学习如何在Java应用中合理地使用线程池。
|
5天前
|
安全 Java
深入理解 Java 多线程和并发工具类
【4月更文挑战第19天】本文探讨了Java多线程和并发工具类在实现高性能应用程序中的关键作用。通过继承`Thread`或实现`Runnable`创建线程,利用`Executors`管理线程池,以及使用`Semaphore`、`CountDownLatch`和`CyclicBarrier`进行线程同步。保证线程安全、实现线程协作和性能调优(如设置线程池大小、避免不必要同步)是重要环节。理解并恰当运用这些工具能提升程序效率和可靠性。
|
6天前
|
安全 Java
java多线程(一)(火车售票)
java多线程(一)(火车售票)
|
6天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。