原文地址 译者:许巧辉 校对:梁海舰
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" ); |
这就是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" ); |
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.仅返回一些低级的内存信息
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
方法是非常有用的。考虑以下类:
2 |
private long a; // not initialized value |
5 |
this .a = 1 ; // initialization |
8 |
public long a() { return this .a; } |
使用构造器、反射和unsafe初始化它,将得到不同的结果。
1 |
A o1 = new A(); // constructor |
4 |
A o2 = A. class .newInstance(); // reflection |
7 |
A o3 = (A) unsafe.allocateInstance(A. class ); // unsafe |
想想所有单例发生了什么。
内存崩溃(Memory corruption)
这对于每个C程序员来说是常见的。顺便说一下,它是绕过安全的常用技术。
考虑下那些用于检查“访问规则”的简单类:
2 |
private int ACCESS_ALLOWED = 1 ; |
4 |
public boolean giveAccess() { |
5 |
return 42 == ACCESS_ALLOWED; |
客户端代码是非常安全的,并且通过调用giveAccess()
来检查访问规则。可惜,对于客户,它总是返回false。只有特权用户可以以某种方式改变ACCESS_ALLOWED
常量的值并且得到访问(giveAccess()方法返回true,译者注)。
实际上,这并不是真的。演示代码如下:
1 |
Guard guard = new Guard(); |
2 |
guard.giveAccess(); // false, no access |
5 |
Unsafe unsafe = getUnsafe(); |
6 |
Field f = guard.getClass().getDeclaredField( "ACCESS_ALLOWED" ); |
7 |
unsafe.putInt(guard, unsafe.objectFieldOffset(f), 42 ); // memory corruption |
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 ) { |
11 |
c = c.getSuperclass(); |
16 |
for (Field f : fields) { |
17 |
long offset = u.objectFieldOffset(f); |
18 |
if (offset > maxSize) { |
23 |
return ((maxSize/ 8 ) + 1 ) * 8 ; // padding |
算法如下:通过所有非静态字段(包含父类的),获取每个字段的偏移量(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); |
normalize
是一个为了正确内存地址使用,将有符号的int类型强制转换成无符号的long类型的方法。
1 |
private static long normalize( int value) { |
2 |
if (value >= 0 ) return value; |
3 |
return (~0L >>> 32 ) & value; |
真棒,这个方法返回的结果与我们之前的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); |
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)); |
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); |
这个拷贝方法可以用来拷贝任何类型的对象,动态计算它的大小。注意,在拷贝后,你需要将对象转换成特定的类型。
隐藏密码(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); // ???????????? |
06 |
getUnsafe().copyMemory( |
07 |
fake, 0L, null , toAddress(password), sizeOf(password)); |
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++) { |
感谢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()]; |
当你必须动态创建类,而现有代码中有一些代理, 这是很有用的。
抛出异常(Throw an Exception)
不喜欢受检异常?没问题。
1 |
getUnsafe().throwException( new IOException()); |
该方法抛出受检异常,但你的代码不必捕捉或重新抛出它,正如运行时异常一样。
快速序列化(Fast Serialization)
这更有实用性。
大家都知道,标准Java的Serializable的序列化能力是非常慢的。它同时要求类必须有一个公共的、无参数的构造器。
Externalizable
比较好,但它需要定义类序列化的模式。
流行的高性能库,比如kryo具有依赖性,这对于低内存要求来说是不可接受的。
unsafe类可以很容易实现完整的序列化周期。
序列化:
- 使用反射构建模式对象,类只可做一次。
- 使用
Unsafe
方法,如getLong
、getInt
、getObject
等来检索实际字段值。
- 添加类标识,以便有能力恢复该对象
- 将它们写入文件或任意输出
你也可以添加压缩(步骤)以节省空间。
反序列化:
- 创建已序列化对象实例,使用
allocateInstance
协助(即可),因为不需要任何构造器。
- 构建模式,与序列化的步骤1相同。
- 从文件或任意输入中读取所有字段。
- 使用
Unsafe
方法,如putLong
、putInt
、putObject
等来填充该对象。
实际上,在正确的实现过程中还有更多的细节,但思路是明确的。
这个序列化将非常快。
顺便说一下,在kryo中有使用Unsafe
的一些尝试http://code.google.com/p/kryo/issues/detail?id=75
大数组(Big Arrays)
正如你所知,Java数组大小的最大值为Integer.MAX_VALUE
。使用直接内存分配,我们创建的数组大小受限于堆大小。
SuperArray的实现
:
02 |
private final static int BYTE = 1 ; |
07 |
public SuperArray( long size) { |
09 |
address = getUnsafe().allocateMemory(size * BYTE); |
12 |
public void set( long i, byte value) { |
13 |
getUnsafe().putByte(address + i * BYTE, value); |
16 |
public int get( long idx) { |
17 |
return getUnsafe().getByte(address + idx * BYTE); |
简单用法:
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); |
8 |
System.out.println( "Sum of 100 elements:" + sum); // 300 |
实际上,这是堆外内存(off-heap memory
)技术,在java.nio
包中部分可用。
这种方式的内存分配不在堆上,且不受GC管理,所以必须小心Unsafe.freeMemory()的使用。它也不执行任何边界检查,所以任何非法访问可能会导致JVM崩溃。
这可用于数学计算,代码可操作大数组的数据。此外,这可引起实时程序员的兴趣,可打破GC在大数组上延迟的限制。
并发(Concurrency)
几句关于Unsafe
的并发性。compareAndSwap
方法是原子的,并且可用来实现高性能的、无锁的数据结构。
比如,考虑问题:在使用大量线程的共享对象上增长值。
首先,我们定义简单的Counter
接口:
然后,我们定义使用Counter的工作线程CounterClient
:
01 |
class CounterClient implements Runnable { |
05 |
public CounterClient(Counter c, int num) { |
12 |
for ( int i = 0 ; i < num; i++) { |
测试代码:
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)); |
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 ; |
05 |
public void increment() { |
10 |
public long getCounter() { |
输出:
1 |
Counter result: 99542945 |
运行快,但没有线程管理,结果是不准确的。第二次尝试,添加上最简单的java式同步:
01 |
class SyncCounter implements Counter { |
02 |
private long counter = 0 ; |
05 |
public synchronized void increment() { |
10 |
public long getCounter() { |
输出:
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(); |
06 |
public void increment() { |
13 |
public long getCounter() { |
输出:
1 |
Counter result: 100000000 |
2 |
Time passed in ms: 8065 |
仍然正确,耗时较短。atomics的运行效果如何?
01 |
class AtomicCounter implements Counter { |
02 |
AtomicLong counter = new AtomicLong( 0 ); |
05 |
public void increment() { |
06 |
counter.incrementAndGet(); |
10 |
public long getCounter() { |
输出:
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; |
06 |
public CASCounter() throws Exception { |
08 |
offset = unsafe.objectFieldOffset(CASCounter. class .getDeclaredField( "counter" )); |
12 |
public void increment() { |
13 |
long before = counter; |
14 |
while (!unsafe.compareAndSwapLong( this , offset, before, before + 1 )) { |
20 |
public long getCounter() { |
输出:
1 |
Counter result: 100000000 |
2 |
Time passed in ms: 6454 |
看起来似乎等价于atomics。atomics使用Unsafe
?(是的)
实际上,这个例子很简单,但它展示了Unsafe
的一些能力。
如我所说,CAS原语可以用来实现无锁的数据结构。背后的原理很简单:
- 有一些状态
- 创建它的副本
- 修改它
- 执行CAS
- 如果失败,重复尝试
实际上,现实中比你现象的更难。存在着许多问题,如ABA问题、指令重排序等。
如果你真的感兴趣,可以参考lock-free HashMap的精彩展示。
修改:给counter变量添加volatile
关键字,以避免无限循环的风险。
结论(Conclusion)
即使Unsafe
对应用程序很有用,但(建议)不要使用它。
- 一文了解sun.misc.Unsafe
Java语言和JVM平台已经度过了20岁的生日.它最初起源于机顶盒.移动设备和Java-Card,同时也应用在了各种服务器系统中,Java已成为物联网(Internet of Things)的通用语言 ...
- sun.misc.Unsafe的理解
以下sun.misc.Unsafe源码和demo基于jdk1.7: 最近在看J.U.C里的源码,很多都用到了sun.misc.Unsafe这个类,一知半解,看起来总感觉有点不尽兴,所以打算对Unsaf ...
- Java--如何使用sun.misc.Unsafe完成compareAndSwapObject原子操作
package com; import sun.misc.Unsafe; import java.lang.reflect.Field; /** * Created by yangyu on 16/1 ...
- Java sun.misc.Unsafe类的学习笔记
Java未开源的Unsafe类 Unsafe类可以为我们提供高效并且线程安全方式操作变量,直接和内存数据打交道. 获取Unsafe实体的方法 private static Unsafe getUnsa ...
- java对象的内存布局(二):利用sun.misc.Unsafe获取类字段的偏移地址和读取字段的值
在上一篇文章中.我们列出了计算java对象大小的几个结论以及jol工具的使用,jol工具的源代码有兴趣的能够去看下.如今我们利用JDK中的sun.misc.Unsafe来计算下字段的偏移地址,一则验证 ...
- JDK 1.8 sun.misc.Unsafe类CAS底层实现
在java.util.concurrent包下面的很多类为了追求性能都采用了sun.misc.Unsafe类中的CAS操作,从而避免使用synchronized等加锁方式带来性能上的不足. 在sun. ...
- Java中的sun.misc.Unsafe包
chronicle项目:https://github.com/peter-lawrey/Java-Chronicle 这个项目是利用mmap机制来实现高效的读写数据,号称每秒写入5到20百万条数据. ...
- Java sun.misc.unsafe类
Java是一个安全的开发工具,它阻止开发人员犯很多低级的错误,而大部份的错误都是基于内存管理方面的.如果你想搞破坏,可以使用Unsafe这个类.这个类是属于sun.*API中的类,并且它不是J2SE中 ...
- Java Magic. Part 4: sun.misc.Unsafe
Java Magic. Part 4: sun.misc.Unsafe @(Base)[JDK, Unsafe, magic, 黑魔法] 转载请写明:原文地址 系列文章: -Java Magic. P ...
随机推荐
- BigDecimal常用的加减乘除算法、比较大小、保存两位小数点
项目中涉及到了BigDecimal的加.减.乘.比较大小.精确度的问题.所以在此总结一下,方便以后复习. //加法 BigDecimal coins = new BigDecimal("0& ...
- .net捕捉全局未处理异常的3种方式
前言: 我们在实际项目开发中,经常会遇到一些不可预见的异常产生,有的异常在程序运行时就对其进行处理(try)但是,有的程序不需要每一个地方都用try进行处理,那么针对这种情况,可以参照下面的方式,实现 ...
- C#实现的HttpGet请求
话不多说,代码贴上: /// <summary> /// HTTP Get请求 /// </summary> /// <param name="url" ...
- Spring Cloud(十二):分布式链路跟踪 Sleuth 与 Zipkin【Finchley 版】
Spring Cloud(十二):分布式链路跟踪 Sleuth 与 Zipkin[Finchley 版] 发表于 2018-04-24 | 随着业务发展,系统拆分导致系统调用链路愈发复杂一个前端请 ...
- springmvc 请求经过controller后静态资源无法访问的问题
经过RequestMapping(“xx”)后 转发请求时会在url里面附带地址, 导致访问静态资源文件失败, 解决办法是在 spring-mvc.xml文件中加上 <mvc:default-s ...
- 用Java制作一个简单的图片验证码
//Java实现简单验证码功能 package project; import java.awt.Color; import java.awt.Font;import java.awt.Graphic ...
- 集群搭建(一)克隆虚拟机静态IP设置
[抛出问题] 当在搭建集群的时候,我们会将一个安装好相关程序的虚拟机进行克隆,克隆之后,我们会发下一些问题:就是原先的eth0 网卡不见了 原先的网卡 而克隆之后的网卡 会发现原来的网卡eth0 变为 ...
- 安装ubuntu系统及ubuntu安装Python的几点心得
一.安装ubuntu系统 1.ubuntu系统是Linux系统的一种,和centos差别不大,但是个人还是建议大家安装ubuntu,它更适合国内使用习惯,换句话说更亲切. 2.安装方法不再赘述,网上有 ...
- PAT1110:Complete Binary Tree
1110. Complete Binary Tree (25) 时间限制 400 ms 内存限制 65536 kB 代码长度限制 16000 B 判题程序 Standard 作者 CHEN, Yue ...
- PAT1064: Compelte Binary Search Tree
1064. Complete Binary Search Tree (30) 时间限制 100 ms 内存限制 65536 kB 代码长度限制 16000 B 判题程序 Standard 作者 CHE ...