Java中的天使和魔鬼sun.misc.Unsafe

2024-01-08 09:18
文章标签 java 天使 sun misc 魔鬼 unsafe

本文主要是介绍Java中的天使和魔鬼sun.misc.Unsafe,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

我们在看ConcurrentHashMap源码时经常看到Unsafe类的使用,今天我们来了解下Unsafe类。

Java是一个安全的编程语言,它能最大程度的防止程序员犯一些低级的错误(大部分是和内存管理有关的)。但凡事不是绝对的,使用Unsafe程序员就可以操作内存,因此可能带来一个安全隐患。

这篇文章是就快速学习下sun.misc.Unsafe的公共API和一些有趣的使用例子。

1、Unsafe 实例化

在使用Unsafe之前我们需要先实例化它。但我们不能通过像Unsafe unsafe = new Unsafe()这种简单的方式来实现Unsafe的实例化,这是由于Unsafe的构造方法是私有的。Unsafe有一个静态的getUnsafe()方法,但是如果天真的以为调用该方法就可以的话,那你将遇到一个SecurityException异常,这是由于该方法只能在被信任的代码中调用。

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

那Java是如何判断我们的代码是否是受信的呢?它就是通过判断加载我们代码的类加载器是否是根类加载器。

我们可是通过这种方法将我们自己的代码变为受信的,使用jvm参数bootclasspath。如下所示:

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

但这种方式太难了

Unsafe类内部有一个名为theUnsafe的私有实例变量,我们可以通过反射来获取该实例变量。

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

注意: 忽略你的IDE提示. 例如, eclipse可能会报这样的错误”Access restriction…” 单如果你运行你的代码,会发现一切正常。如果还是还是提示错误,你可以通过如下的方式关闭该错误提示:

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

2、Unsafe API

类 sun.misc.Unsafe 由150个方法组成。事实上这些方法只有几组是非常重要的用来操作不同的对象。下面我们就来看下这些方法中的一部分。

1、Info 仅仅是返回一个低级别的内存相关的信息
addressSize
pageSize

2、Objects. 提供操作对象和对象字段的方法
allocateInstance
objectFieldOffset

3、Classes. 提供针对类和类的静态字段操作的方法
staticFieldOffset
defineClass
defineAnonymousClass
ensureClassInitialized

4、Arrays. 数组操作
arrayBaseOffset
arrayIndexScale

5、Synchronization. 低级别的同步原语
monitorEnter
tryMonitorEnter
monitorExit
compareAndSwapInt
putOrderedInt

6、Memory. 直接访问内存的方法
allocateMemory
copyMemory
freeMemory
getAddress
getInt
putInt

接下来是一些有趣的使用case

3、跳过构造初始化

allocateInstance方法可能是有用的,当你需要在构造函数中跳过对象初始化阶段或绕过安全检查又或者你想要实例化哪些没有提供公共构造函数的类时就可以使用该方法。考虑下面的类:

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

通过构造函数,反射,Unsafe分别来实例化该类结果是不同的:

A o1 = new A(); // constructor
o1.a(); // prints 1A o2 = A.class.newInstance(); // reflection
o2.a(); // prints 1A o3 = (A) unsafe.allocateInstance(A.class); // unsafe
o3.a(); // prints 0

allocateInstance根本没有进入构造方法,对于单例模式,简直是噩梦。

4、内存修改,绕过安全检查器

对C程序员来说这中情况是很常见的。

思考一下一些简单的类是如何坚持访问规则的:

class Guard {private int ACCESS_ALLOWED = 1;public boolean giveAccess() {return 42 == ACCESS_ALLOWED;}
}

客户端代码是非常安全的,调用giveAccess()检查访问规则。不幸的是对所有的客户端代码,它总是返回false。只有特权用户在某种程度上可以改变ACCESS_ALLOWED常量并且获得访问权限。

事实上,这不是真的。这是证明它的代码:

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 corruptionguard.giveAccess(); // true, access granted

现在所有的客户端都没有访问限制了。

事实上同样的功能也可以通过反射来实现。但有趣的是, 通过上面的方式我们修改任何对象,即使我们没有持有对象的引用。

举个例子, 在内存中有另外的一个Guard对象,并且地址紧挨着当前对象的地址,我们就可以通过下面的代码来修改该对象的ACCESS_ALLOWED字段的值。

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

注意,我们没有使用任何指向该对象的引用,16是Guard对象在32位架构上的大小。我们也可以通过sizeOf方法来计算Guard对象的大小。

5、sizeOf 计算内存大小

使用objectFieldOffset方法我们可以实现C风格的sizeof方法。下面的方法实现返回对象的表面上的大小

public static long sizeOf(Object o) {Unsafe u = getUnsafe();HashSet<Field> fields = new HashSet<Field>();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 offsetlong maxSize = 0;for (Field f : fields) {long offset = u.objectFieldOffset(f);if (offset > maxSize) {maxSize = offset;}}return ((maxSize/8) + 1) * 8;   // padding
}

算法逻辑如下:收集所有包括父类在内的非静态字段,获得每个字段的偏移量,发现最大并添加填充。也许,我错过了一些东西,但是概念是明确的。

更简单的sizeof方法实现逻辑是:我们只读取该对象对应的class对象中关于大小的字段值。在JVM 1.7 32 位版本上该表示大小的字段偏移量是12。

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包,但它需要特殊的JVM参数。

6、浅拷贝

在实现了计算对象浅层大小的基础上,我们可以非常容易的添加对象的拷贝方法。标准的办法需要修改我们的代码和Cloneable。或者你可以实现自定义的对象拷贝函数,但它不会变为通用的函数。

浅拷贝:

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];
}

该拷贝函数可以用来拷贝任何类型的对象,因为对象的大小是动态计算的。

注意 在完成拷贝动作后你需要将拷贝对象的类型强转为目标类型。

7、隐藏密码

在Unsafe的直接内存访问方法使用case中有一个非常有趣的用法就是删除内存中不想要的对象。

大多数获取用户密码的API方法的返回值不是byte[]就是char[],这是为什么呢?

这完全是出于安全原因, 因为我们可以在不需要它们的时候将数组元素置为失效。如果我们获取的密码是字符串类型,则密码字符串是作为一个对象保存在内存中的。要将该密码字符串置为无效,我们只能讲字符串引用职位null,但是该字符串的内容任然存在内存直到GC回收该对象后。

这个技巧在内存创建一个假的大小相同字符串对象来替换原来的:

String password = new String("l00k@myHor$e");
String fake = new String(password.replaceAll(".", "?"));
System.out.println(password); // l00k@myHor$e
System.out.println(fake); // ????????????getUnsafe().copyMemory(fake, 0L, null, toAddress(password), sizeOf(password));System.out.println(password); // ????????????
System.out.println(fake); // ????????????

感觉安全了吗?

其实该方法不是真的安全。想要真的安全我们可以通过反射API将字符串对象中的字符数组value字段的值修改为null。

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] = '?';
}

8、多重继承

在Java中本来是没有多重集成的。除非我们可以将任意的类型转为我们想要的任意类型。

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

这段代码将String类添加到Integer的超类集合中,所以我们的强转代码是没有运行时异常的。

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

有个问题是我们需要先将要转的对象转为Object,然后再转为我们想要的类型。这是为了欺骗编译器。

9、动态类

我们可以在运行时创建类, 例如通过一个编译好的class文件。将class文件的内容读入到字节数组中然后将该数组传递到合适的defineClass方法中。

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

读取class文件内如的代码:

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;
}

该方式是非常有用的,如果你确实需要在运行时动态的创建类。比如生产代理类或切面类。

10、抛出一个异常

不喜欢受检异常?这不是问题。

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

该方法抛出一个受检异常,但是你的代码不需要强制捕获该异常就像运行时异常一样。

11、快速序列化

每个人都知道java标准的序列化的功能速度很慢而且它还需要类拥有公有的构造函数。

外部序列化是更好的方式,但是需要定义针对待序列化类的schema。

非常流行的高性能序列化库,像kryo是有使用限制的,比如在内存缺乏的环境就不合适。

但通过使用Unsafe类我们可以非常简单的实现完整的序列化功能。

序列化:
通过反射定义类的序列化。 这个可以只做一次。
通过Unsafe的getLong, getInt, getObject等方法获取字段真实的值。
添加可以恢复该对象的标识符。
将这些数据写入到输出
当然也可以使用压缩来节省空间。

反序列化:
创建一个序列化类的实例,可以通过方法allocateInstance。因为该方法不需要任何构造方法。
创建schama, 和序列化类似
从文件或输入读取或有的字段
使用 Unsafe 的 putLong, putInt, putObject等方法来填充对象。

事实上要正确实现序列化和反序列化需要注意很多细节,但是思路是清晰的。
这种序列化方式是非常快的。

12、大数组

如你所知Java数组长度的最大值是Integer.MAX_VALUE。使用直接内存分配我们可以创建非常大的数组,该数组的大小只受限于堆的大小。

这里有一个SuperArray的实现:

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;}
}

一个简单的用法:

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

事实上该技术使用了非堆内存off-heap memory,在 java.nio 包中也有使用。

通过这种方式分配的内存不在堆上,并且不受GC管理。因此需要小心使用Unsafe.freeMemory()。该方法不会做任何边界检查,因此任何不合法的访问可能就会导致JVM崩溃。

这种使用方式对于数学计算是非常有用的,因为代码可以操作非常大的数据数组。 同样的编写实时程序的程序员对此也非常感兴趣,因为不受GC限制,就不会因为GC导致非常大的停顿。

13、并发

关于并发编程使用Unsafe的只言片语。compareAndSwap 方法是原子的,可以用来实现高性能的无锁化数据结构。

举个例子,多个线程并发的更新共享的对象这种场景:

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

interface Counter {void increment();long getCounter();
}

我们定义工作线程 CounterClient, 它会使用 Counter:

class CounterClient implements Runnable {private Counter c;private int num;public CounterClient(Counter c, int num) {this.c = c;this.num = num;}@Overridepublic void run() {for (int i = 0; i < num; i++) {c.increment();}}
}

这是测试代码:

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

第一个实现-没有同步的计数器:

class StupidCounter implements Counter {private long counter = 0;@Overridepublic void increment() {counter++;}@Overridepublic long getCounter() {return counter;}
}

输出:

Counter result: 99542945
Time passed in ms: 679

速度很多,但是没有对所有的线程进行协调所以结果是错误的。第二个版本,使用Java常见的同步方式来实现

class SyncCounter implements Counter {private long counter = 0;@Overridepublic synchronized void increment() {counter++;}@Overridepublic long getCounter() {return counter;}
}

输出:

Counter result: 100000000
Time passed in ms: 10136

彻底的同步当然会导致正确的结果。但是花费的时间令人沮丧。让我们试试 ReentrantReadWriteLock:

class LockCounter implements Counter {private long counter = 0;private WriteLock lock = new ReentrantReadWriteLock().writeLock();@Overridepublic void increment() {lock.lock();counter++;lock.unlock();}@Overridepublic long getCounter() {return counter;}
}

输出:

Counter result: 100000000
Time passed in ms: 8065

结果依然是正确的,时间也短。那使用原子的类呢?

class AtomicCounter implements Counter {AtomicLong counter = new AtomicLong(0);@Overridepublic void increment() {counter.incrementAndGet();}@Overridepublic long getCounter() {return counter.get();}
}

输出:

Counter result: 100000000
Time passed in ms: 6552

使用AtomicCounter的效果更好一点。最后我们试试Unsafe的原子方法compareAndSwapLong看看是不是更进一步。

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"));}@Overridepublic void increment() {long before = counter;while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {before = counter;}}@Overridepublic long getCounter() {return counter;}
}

输出:

Counter result: 100000000
Time passed in ms: 6454

看起来和使用原子类是一样的效果,难道原子类使用了Unsafe?答案是YES。

事实上该例子非常简单但表现出了Unsafe的强大功能。

就像前面提到的 CAS原语可以用来实现高效的无锁数据结构。实现的原理很简单:
1、拥有一个状态;
2、创建一个它的副本;
3、修改该副本;
4、执行 CAS 操作;
5、如果失败就重复执行;

事实上,在真实的环境它的实现难度超过你的想象,这其中有需要类似ABA,指令重排序这样的问题。

14、结论

尽管Unsafe有这么多有用的应用,但是尽量不要使用。当然了使用JDK中利用了Unsafe实现的类是可以的。或者你对你代码功力非常自信,可以自己挖坑再填坑哈~

欢迎小伙伴们留言交流~~
浏览更多文章可关注微信公众号:diggkr

这篇关于Java中的天使和魔鬼sun.misc.Unsafe的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/582990

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

在cscode中通过maven创建java项目

在cscode中创建java项目 可以通过博客完成maven的导入 建立maven项目 使用快捷键 Ctrl + Shift + P 建立一个 Maven 项目 1 Ctrl + Shift + P 打开输入框2 输入 "> java create"3 选择 maven4 选择 No Archetype5 输入 域名6 输入项目名称7 建立一个文件目录存放项目,文件名一般为项目名8 确定