Java直接内存访问的技巧

来源:互联网 发布:淘宝现在是什么模式 编辑:程序博客网 时间:2024/06/05 05:59

Java被设计成一个安全,可管理的环境,然而 Java HotSpot有一个后门,提供了对低级别的,对直接内存和线程的操作。这个后门是—-sun.misc.Unsafe。这个类在JDK中有广泛的应用,例如,java.nio和java.util.concurrent。很难想象在日常开发中使用这些危险的,不可移植和未经校验的API。然而,Unsafe提供一种简单的方法来观察HotSpot JVM内部的一些技巧。
获取Unsafe
sun.misc.Unsafe这个类的访问是受限的,它的构造方法是私有的,相应的工厂方法要求必须被Bootloader载入才能使用,也就是说,只有JDK内部分才能使用这个工厂方法来构造Unsafe对象。


publicfinal class Unsafe {
    ...
    privateUnsafe() {}
    privatestatic final Unsafe theUnsafe = newUnsafe();
    ...
    publicstatic Unsafe getUnsafe() {
       Class cc = sun.reflect.Reflection.getCallerClass(2);
       if(cc.getClassLoader() != null)
           thrownew SecurityException("Unsafe");
       returntheUnsafe;
    }
    ...
}
幸运地是,有一个theUnsafe属性可以被利用来检索Unsafe实例,我们可以见到的写一个反射方法,来获取Unsafe实例:


publicstatic Unsafe getUnsafe() {
try{
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
return(Unsafe)f.get(null);
}catch(Exception e) { /* ... */ }
}
下面将学习一些Unsafe的方法。
1.long getAddress(long address) 和void putAddress(long address, long x) 
对直接内存进行读写。
2.int getInt(Object o, long offset) , void putInt(Object o, long offset, int x)
另一个类似的方法对直接内存进行读写,将C语言的结构体和Java对象进行转换。
3.long allocateMemory(long bytes)
这个可以看做是C语言的malloc()函数的一种包装。
sizeof()函数
Java对象的结构如下图所示:


第一个技巧,是模拟C语言的sizefo()函数,这个函数返回对象的字节大小。我们可以用如下的代码实现sizeof()函数:


publicstatic long sizeOf(Object object) {
   Unsafe unsafe = getUnsafe();
   returnunsafe.getAddress( normalize( unsafe.getInt(object, 4L) ) + 12L );
}
 
publicstatic long normalize(intvalue) {
   if(value >= 0)returnvalue;
   return(~0L >>> 32) & value;
}
我们需要使用normalize()函数,因为如果内存地址如果在2^31和2^32之间,将会自动的覆盖邻近的整型,也就是说用补码的方式进行存储。让我们在32位JVM(JDK6或者7)中进行测试:


// 执行sizeOf(new MyStructure())得到如下的结果:
 
classMyStructure { } // 8: 4 (起始标记) + 4 (指向类的指针)
classMyStructure { intx; } // 16: 4 (起始标记) + 4 (指向类的指针) + 4 (int) + 4 填充字节用来对齐64位块
classMyStructure { intx; inty; } // 16: 4 (起始标记) + 4 (指向类的指针) + 2*4
直接内存管理
Unsafe允许通过allcateMemory和freeMemory方法对内存进行显示的分配和回收,直接分配的内存不在GC的控制内,并且不受限于JVM堆的大小。通常,通过NIO的脱离堆约束的缓冲,这些方法是安全有效的,但是有趣的是这让标准的Java引用映射非堆内存变成了可能:


MyStructure structure = newMyStructure(); // create a test object
structure.x = 777;
 
longsize = sizeOf(structure);
longoffheapPointer = getUnsafe().allocateMemory(size);
getUnsafe().copyMemory(
                structure,     // source object
                0,             // source offset is zero - copy an entire object
                null,          // destination is specified by absolute address, so destination object is null
                offheapPointer,// destination address
                size
);// test object was copied to off-heap
 
Pointer p = newPointer(); // Pointer is just a handler that stores address of some object
longpointerOffset = getUnsafe().objectFieldOffset(Pointer.class.getDeclaredField("pointer"));
getUnsafe().putLong(p, pointerOffset, offheapPointer); // set pointer to off-heap copy of the test object
 
structure.x = 222;// rewrite x value in the original object
System.out.println(  ((MyStructure)p.pointer).x  ); // prints 777
 
....
 
classPointer {
    Object pointer;
}
所以,事实上是可以对真实对象进行内存分配和回收的,不单单只是NIO中的字节缓冲。当然,有一个比较大的问题是,GC将会在这样的内存欺骗之后发生。
继承自Final类和void*
想象一下有一个以String为参数的方法,但它需要经行外部的重载。具体代码如下:


Carrier carrier = newCarrier();
carrier.secret = 777;
 
String message = (String)(Object)carrier; // ClassCastException
handler( message );
 
...
 
voidhandler(String message) {
   System.out.println( ((Carrier)(Object)message).secret );
}
 
...
 
classCarrier {
   intsecret;
}
为了让这段代码能工作,首先需要更改Carrier类去伪装成String的子类。superclasses列表被存储在Carrier类结构体28的位置,如上文图中所示。原则上,添加如下的代码可以让Carrer转化成String:
longcarrierClassAddress = normalize( unsafe.getInt(carrier, 4L) );
longstringClassAddress = normalize( unsafe.getInt("", 4L) );
unsafe.putAddress(carrierClassAddress + 32, stringClassAddress); // insert pointer to String class to the list of Carrier's superclasses
这样,类型转化可以正常工作。然而,这样的转换方式是不切当并且违反虚拟机规范的。更详细的方法将包含如下的步骤:
1.在Carrier类中32的位置实际上包含了一个指向Carrier类自己的指针,所以这个指针将被转移到36的位置上,不仅仅是被指针重写到String类。
2.当Carrier继承自String的时候,String类的final标记将被移掉。
结论

sun.misc.Unsafe提供了几乎是不受限制的监控和修改虚拟机运行时数据结构的能力。尽管这些能力几乎是和Java开发本身不相干的,但是对于想要学习HotSpot虚拟机但是没有C++代码调试,或者需要去创建特别的分析工具的人来说,Unsafe是一个伟大的工具。




1、通过Unsafe类可以分配内存,可以释放内存;

类中提供的3个本地方法allocateMemory、reallocateMemory、freeMemory分别用于分配内存,扩充内存和释放内存,与C语言中的3个方法对应。

2、可以定位对象某字段的内存位置,也可以修改对象的字段值,即使它是私有的;

[java] view plaincopyprint?
  1. public native long allocateMemory(long l);  
  2. public native long reallocateMemory(long l, long l1);  
  3. public native void freeMemory(long l);  
字段的定位:
JAVA中对象的字段的定位可能通过staticFieldOffset方法实现,该方法返回给定field的内存地址偏移量,这个值对于给定的filed是唯一的且是固定不变的。
getIntVolatile方法获取对象中offset偏移地址对应的整型field的值,支持volatile load语义。
getLong方法获取对象中offset偏移地址对应的long型field的值
数组元素定位:
Unsafe类中有很多以BASE_OFFSET结尾的常量,比如ARRAY_INT_BASE_OFFSET,ARRAY_BYTE_BASE_OFFSET等,这些常量值是通过arrayBaseOffset方法得到的。arrayBaseOffset方法是一个本地方法,可以获取数组第一个元素的偏移地址。Unsafe类中还有很多以INDEX_SCALE结尾的常量,比如 ARRAY_INT_INDEX_SCALE , ARRAY_BYTE_INDEX_SCALE等,这些常量值是通过arrayIndexScale方法得到的。arrayIndexScale方法也是一个本地方法,可以获取数组的转换因子,也就是数组中元素的增量地址。将arrayBaseOffset与arrayIndexScale配合使用,可以定位数组中每个元素在内存中的位置。
[java] view plaincopyprint?
  1. public final class Unsafe {  
  2.     public static final int ARRAY_INT_BASE_OFFSET;  
  3.     public static final int ARRAY_INT_INDEX_SCALE;  
  4.   
  5.     public native long staticFieldOffset(Field field);  
  6.     public native int getIntVolatile(Object obj, long l);  
  7.     public native long getLong(Object obj, long l);  
  8.     public native int arrayBaseOffset(Class class1);  
  9.     public native int arrayIndexScale(Class class1);  
  10.   
  11.     static   
  12.     {  
  13.         ARRAY_INT_BASE_OFFSET = theUnsafe.arrayBaseOffset([I);  
  14.         ARRAY_INT_INDEX_SCALE = theUnsafe.arrayIndexScale([I);  
  15.     }  
  16. }  

0 0
原创粉丝点击