Direct Memory Access in Java
Java is a safe programming language and prevents programmer from doing mistakes mainly on memory management.
Java contains a “backdoor” that provides a number of low-level operations to manipulate memory and threads directly. This backdoor class "sun.misc.Unsafe" is widely used by JDK itself. This article is a quick overview of "sun.misc.Unsafe" API and few interesting cases of its usage.
There is no simple way to create instance of Unsafe object like Unsafe unsafe = new Unsafe(), because it has private constructor.
if you try to call Unsafe.getUnsafe(), which is static method, will get SecurityException.
We has to write method for trusted code which checking that our code was loaded with primary classloader.
We can use bootclasspath, which is too hard to make our code "trusted".
eg:-
java -Xbootclasspath:/usr/jdk1.7.0_56/jre/lib/rt.jar:. com.sample.client.Unsafe
The easy way is to use Unsafe class instance called theUnsafe, which marked as private. In this case you may face error on your IDE. Ignore your IDE error's and if the error is annoying, ignore errors on Unsafe usage in
Preferences -> Java -> Compiler -> Errors/Warnings -> Deprecated and restricted API -> Forbidden reference -> Warning
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
There are, actually, few groups of important methods:
Memory
Holds direct memory access methods
getInt, putInt, copyMemory, freeMemory, getAddress, allocateMemory
Arrays
Holds Arrays manipulation
arrayIndexScale, arrayBaseOffset
Classes
It provides methods for classes and static field manipulation
defineClass, staticFieldOffset, defineAnonymousClass, ensureClassInitialized
Synchronization
For synchronization
monitorEnter, monitorExit, compareAndSwapInt, putOrderedInt, tryMonitorEnter
Info
Returns some low-level memory information
addressSize, pageSize
Objects
For object and its fields manipulation
allocateInstance, objectFieldOffset
Use Cases
Initialization
allocateInstance method can be useful when you need to skip object initialization phase or you want instance of that class but don't have any public constructor.
A o1 = new A(); // constructor
o1.a();
A o2 = A.class.newInstance(); // reflection
o2.a();
A o3 = (A) unsafe.allocateInstance(A.class); // unsafe
o3.a(); // prints 0
Memory corruption
Consider some simple class that check access rules:
class Sample {
private int ACCESS_ALLOWED = 1;
public boolean giveAccess() {
return 42 == ACCESS_ALLOWED;
}
}
For clients, it always returns false.
Now all clients will get unlimited access.
Sample sample = new Sample();
sample.giveAccess(); // false, no access
// bypass
Unsafe unsafe = getUnsafe();
Field f = sample.getClass().getDeclaredField("ACCESS_ALLOWED");
unsafe.putInt(sample, unsafe.objectFieldOffset(f), 42); // memory corruption
sample.giveAccess(); // true, access granted
sizeOf
Much simpler sizeOf can be achieved if we just read size value from the class struct for this object.
public static long sizeOf(Object object){
return getUnsafe().getAddress(
normalize(getUnsafe().getInt(object, 4L)) + 12L);
}
private static long normalize(int value) {
if(value >= 0) return value;
return (~0L >>> 32) & value;
}
normalize is a method for casting signed int to unsigned long, for correct address usage.
Shallow copy
Having implementation of calculating shallow object size, we can simply add function that copy objects.
static Object shallowCopy(Object obj) {
long si= sizeOf(obj);
long st = toAddress(obj);
long addr= getUnsafe().allocateMemory(si);
getUnsafe().copyMemory(st, addr, si);
return fromAddress(addr);
}
toAddress and fromAddress convert object to its address in memory and vice versa.
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];
}
This copy function can be used to copy object of any type, its size will be calculated dynamically. After copying you need to cast object to specific type.
Hide Password
One more interesting usage of direct memory access in Unsafe is removing unwanted objects from memory.
Most of the APIs for retrieving user's password, have signature as byte[] or char[]. Why arrays?
It is completely for security reason, because we can nullify array elements after we don't need them. If we retrieve password as String it can be saved like an object in memory and nullifying that string just perform dereference operation. This object still in memory by the time GC decide to perform cleanup.
String password = new String("vim100@my##$$");
String fake = new String(password.replaceAll(".", "?"));
System.out.println(password); // viml00k@my##$$
System.out.println(fake); // ????????????
getUnsafe().copyMemory(
fake, 0L, null, toAddress(password), sizeOf(password));
System.out.println(password); // ????????????
System.out.println(fake); // ????????????
Most of the APIs for retrieving user's password, have signature as byte[] or char[]. Why arrays?
It is completely for security reason, because we can nullify array elements after we don't need them. If we retrieve password as String it can be saved like an object in memory and nullifying that string just perform dereference operation. This object still in memory by the time GC decide to perform cleanup.
String password = new String("vim100@my##$$");
String fake = new String(password.replaceAll(".", "?"));
System.out.println(password); // viml00k@my##$$
System.out.println(fake); // ????????????
getUnsafe().copyMemory(
fake, 0L, null, toAddress(password), sizeOf(password));
System.out.println(password); // ????????????
System.out.println(fake); // ????????????
More Safe
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] = '?';
}
Multiple Inheritance
here is no multiple inheritance in java.
Correct, except we can cast every type to every another one, if we want.
long intClassAddress = normalize(getUnsafe().getInt(new Integer(0), 4L));
long strClassAddress = normalize(getUnsafe().getInt("", 4L));
getUnsafe().putAddress(intClassAddress + 36, strClassAddress);
This snippet adds String class to Integer superclasses, so we can cast without runtime exception.
(String) (Object) (new Integer(666))
One problem that we must do it with pre-casting to object. To cheat compiler.
Dynamic classes
We can create classes in runtime, for example from compiled .class file. To perform that read class contents to byte array and pass it properly to defineClass method.
byte[] classContents = getClassContent();
Class c = getUnsafe().defineClass(
null, classContents, 0, classContents.length);
c.getMethod("a").invoke(c.newInstance(), null); // 1
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;
}
Throw an Exception
getUnsafe().throwException(new IOException());
This method throws checked exception, but your code not forced to catch or rethrow it. Just like runtime exception.
Fast Serialization
Everyone knows that standard java Serializable capability to perform serialization is very slow. It also require class to have public non-argument constructor.
Externalizable is better, but it needs to define schema for class to be serialized.
Serialization:
Build schema for object using reflection. It can be done once for class.
Use Unsafe methods getLong, getInt, getObject, etc. to retrieve actual field values.
Add class identifier to have capability restore this object.
Write them to the file or any output.
You can also add compression to save space.
Deserialization:
Create instance of serialized class. allocateInstance helps, because does not require any constructor.
Build schema. The same as 1 step in serialization.
Read all fields from file or any input.
Use Unsafe methods putLong, putInt, putObject, etc. to fill the object.
This serialization will be really fast.
Kryo is example.
Big Arrays
As you know Integer.MAX_VALUE constant is a max size of java array. Using direct memory allocation we can create arrays with size limited by only heap size.
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;
}
}
And sample usage:
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
Memory allocated this way not located in the heap and not under GC management, so take care of it
using
Unsafe.freeMemory()
. It also does not perform any boundary checks, so any
illegal access may cause JVM crash.Concurrency
And few words about concurrency with Unsafe. compareAndSwap methods are atomic and can be used to implement high-performance lock-free data structures.
For example, consider the problem to increment value in the shared object using lot of threads.
First we define simple interface Counter:
interface Counter {
void increment();
long getCounter();
}
Then we define worker thread CounterClient, that uses Counter:
class CounterClient implements Runnable {
private Counter c;
private int num;
public CounterClient(Counter c, int num) {
this.c = c;
this.num = num;
}
@Override
public void run() {
for (int i = 0; i < num; i++) {
c.increment();
}
}
}
And this is testing code:
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));
First implementation is not-synchronized counter:
class StupidCounter implements Counter {
private long counter = 0;
@Override
public void increment() {
counter++;
}
@Override
public long getCounter() {
return counter;
}
}
Output:
Counter result: 99542945
Time passed in ms: 679
Working fast, but no threads management at all, so result is inaccurate.
Second attempt, add easiest java-way synchronization:
class SyncCounter implements Counter {
private long counter = 0;
@Override
public synchronized void increment() {
counter++;
}
@Override
public long getCounter() {
return counter;
}
}
Output:
Counter result: 100000000
Time passed in ms: 10136
Radical synchronization always work. But timings is awful.
ReentrantReadWriteLock:
class LockCounter implements Counter {
private long counter = 0;
private WriteLock lock = new ReentrantReadWriteLock().writeLock();
@Override
public void increment() {
lock.lock();
counter++;
lock.unlock();
}
@Override
public long getCounter() {
return counter;
}
}
Output:
Counter result: 100000000
Time passed in ms: 8065
Still correct, and timings are better.
About atomics.
class AtomicCounter implements Counter {
AtomicLong counter = new AtomicLong(0);
@Override
public void increment() {
counter.incrementAndGet();
}
@Override
public long getCounter() {
return counter.get();
}
}
Output:
Counter result: 100000000
Time passed in ms: 6552
AtomicCounter is even better.
Unsafe primitive compareAndSwapLong to see if it is really privilegy to use it.
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;
}
Output:
Counter result: 100000000
Time passed in ms: 6454
Seems equal to atomics. Because Atomics use Unsafe.
In fact this example is easy enough, but it shows some power of Unsafe.
No comments:
Post a Comment