[娱乐] [JVM] 试试在32位HotSpot上跑这个?

RednaxelaFX 2010-02-12
上周某晚睡觉前觉得无聊,写了同样无聊的一段测试代码来解闷。都怪MVEL让我看到了奇怪的代码……
本来想发一帖讲解原理的,上周偏偏又加班没来得及。于是干脆先把代码扔在这里好了。回头再讲解“为什么”。欢迎回复讨论 ^_^

这个代码测试了在32位Java 6的HotSpot VM上可以跑,在Windows Vista SP1、Windows XP SP3和某版本32位Linux上跑都没问题。64位HotSpot的话肯定不行,HotSpot之外别的JVM也不行,因为涉及到HotSpot当前实现中使用的对象布局细节,事先提醒。
另外这段代码是非常不安全的,它之所以能显示出我想要的结果是因为程序执行中没有引发过GC。当前HotSpot的年轻代堆空间都采用了基于copy的GC算法,会移动对象;而下面的代码的“成功”依赖于对象地址保持稳定。

import java.lang.reflect.Field;
import sun.misc.Unsafe;

class IntValueHolder {
    public int value;
    
    @Override
    public String toString() {
        return String.format("IntValueHolder: 0x%X", this.value);
    }
}

class ReferenceHolder {
    public Object value;
    
    @Override
    public String toString() {
        return String.format("ReferenceHolder: %s", this.value);
    }
}

public class TestUnsafe {
    public static void test() throws Throwable {
        ReferenceHolder refHolder = new ReferenceHolder();
        refHolder.value = "Hello, unsafe!";
        Unsafe unsafe = getUnsafe();
        Field valueField = ReferenceHolder.class.getField("value");
        long valueFieldOffset = unsafe.objectFieldOffset(valueField);
        long objAddr = unsafe.getInt(refHolder, valueFieldOffset);
        System.out.println("object at: 0x"
            + Long.toString(objAddr & 0x0FFFFFFFF, 16).toUpperCase());
        long strlenAddr = objAddr + 16;
        System.out.println("string length: "
            + unsafe.getInt(null, strlenAddr));
        
        int newStringLength = 16;
        unsafe.putInt(null, strlenAddr, newStringLength);
        long charArrAddrAddr = objAddr + 8;
        long charArrAddr = unsafe.getInt(null, charArrAddrAddr);
        long charArrLenAddr = charArrAddr + 8;
        unsafe.putInt(null, charArrLenAddr, newStringLength);
        unsafe.putLong(null, charArrAddr + 28, 0x006F00630069006EL);
        unsafe.putLong(null, charArrAddr + 36, 0x00580046006E0072L);
        
        IntValueHolder intHolder = new IntValueHolder();
        intHolder.value = (int) objAddr;
        Object obj = unsafe.getObject(intHolder, valueFieldOffset);
        System.out.println(obj);
        
        long klassOffset = 4;
        int refHolderKlass = unsafe.getInt(refHolder, klassOffset);
        System.out.println(intHolder.toString());
        unsafe.putInt(intHolder, klassOffset, refHolderKlass);
        System.out.println(intHolder.toString());
        ReferenceHolder fakeRefHolder = (ReferenceHolder)(Object)intHolder;
        System.out.println(fakeRefHolder.value);
    }
    
    public static Unsafe getUnsafe() throws Throwable {
        Class<?> unsafeClass = Unsafe.class;
        for (Field f : unsafeClass.getDeclaredFields()) {
            if ("theUnsafe".equals(f.getName())) {
                f.setAccessible(true);
                return (Unsafe) f.get(null);
            }
        }
        return null;
    }
    
    public static void main(String[] args) throws Throwable {
        test();
        System.out.println("Done testing.");
    }
}


编译时会有警告,那些是关于使用了Sun的私有API的警告,可以忽略。

在我的装着Vista的笔记本上跑的其中一次结果是:
引用
object at: 0x27C79F40
string length: 14
Hello, unicornFX
IntValueHolder: 0x27C79F40
ReferenceHolder: Hello, unicornFX
Hello, unicornFX
Done testing.


值得留意的几个地方:
1、我在源码里写的字符串内容是"Hello, unsafe!",长度为14字符;但输出出来的字符串内容是"Hello, unicornFX",长度是16字符;
2、第52与行第54行的代码完全一样,局部变量intHolder的值也没变过,但输出的结果却不同;
3、第55行在完全没继承关系的IntValueHolder与ReferenceHolder之间成功进行了引用的类型转换而没有抛ClassCastException。

Have fun ^_^
(But don't try this in production.)
kyfxbl 2011-01-06
没有sun.misc包,跑不了,这个包在哪里可以得到呢?

另外,很奇怪为什么会有类的名字叫Unsafe,呵呵,这个类的意义何在?是专门用来测试的吗?
RednaxelaFX 2011-01-06
在rt.jar里,不需要额外写classpath参数。遇到的错误具体是什么?

这个类是JDK内部一些功能使用的。例如说AtomicXXX用到了Unsafe里的compareAndSwapXXX方法,DirectByteBuffer用了Unsafe的memory系方法,反射的一小块用了allocateInstance方法,等等。这个类不是给外部用的,也不是公有API的一部分。如果编译的源码里有用到它的话,一定会被javac警告。

在Eclipse里的话要把项目里的Java编译器警告选项中使用内部API的限制关掉才行。
Willam2004 2011-02-09
        unsafe.putLong(null, charArrAddr + 28, 0x006F00630069006EL);  
        unsafe.putLong(null, charArrAddr + 36, 0x00580046006E0072L);

是这两句的影响导致输出FX么?不知道LZ是怎么算出来像0x006F00630069006EL和0x00580046006E0072L的数字的?

RednaxelaFX 2011-02-09
简短回答:
0x006F00630069006EL是"nico",0x00580046006E0072L是"rnFX"。
由于Java的内部字符编码使用UTF-16BE,而x86是little-endian的,所以这么做。

至于详细回答…楼上要是有兴趣的话我再详细说
Willam2004 2011-02-10
RednaxelaFX 写道
简短回答:
0x006F00630069006EL是"nico",0x00580046006E0072L是"rnFX"。
由于Java的内部字符编码使用UTF-16BE,而x86是little-endian的,所以这么做。

至于详细回答…楼上要是有兴趣的话我再详细说


呵呵,我是挺有兴趣,但我估计需要先准备些基础知识。不然也理解不了。
Willam2004 2011-02-25
Unsafe 这个类有啥用?lz能给些资料看看不?或者解释下?
qz小峰 2011-02-25
Willam2004 写道
Unsafe 这个类有啥用?lz能给些资料看看不?或者解释下?

直接源码,各个方法都有详细的描述.通常是一些底层的操作.
RednaxelaFX 2011-02-25
Willam2004 写道
Unsafe 这个类有啥用?lz能给些资料看看不?或者解释下?


RednaxelaFX 写道
在rt.jar里,不需要额外写classpath参数。遇到的错误具体是什么?

这个类是JDK内部一些功能使用的。例如说AtomicXXX用到了Unsafe里的compareAndSwapXXX方法,DirectByteBuffer用了Unsafe的memory系方法,反射的一小块用了allocateInstance方法,等等。这个类不是给外部用的,也不是公有API的一部分。如果编译的源码里有用到它的话,一定会被javac警告。

在Eclipse里的话要把项目里的Java编译器警告选项中使用内部API的限制关掉才行。

这段就是在说Unsafe啊…资料的话如楼上所说,读源码就好了,里面几乎全都是native方法所以在Java这层也没啥可说的。
Unsafe.java
unsafe.cpp
Global site tag (gtag.js) - Google Analytics