今天又在重构“祖传代码”,看到了这一幕:

 

 

 心好累,直接抄网文了。。。

误人子弟之一,估计是 写超长函数(不会合理拆分函数,流水账代码)或者大对象不会使用WeakReferenc,或者动不动就习惯手动GC造成的:

Java : 对象不再使用时,为什么要赋值为null?

前言

许多Java开发者都曾听说过“不使用的对象应手动赋值为null“这句话,而且好多开发者一直信奉着这句话;问其原因,大都是回答“有利于GC更早回收内存,减少内存占用”,但再往深入问就回答不出来了。

鉴于网上有太多关于此问题的误导,本文将通过实例,深入JVM剖析“对象不再使用时赋值为null”这一操作存在的意义,供君参考。本文尽量不使用专业术语,但仍需要你对JVM有一些概念。

示例代码

我们来看看一段非常简单的代码:

public static void main(String[] args) {
    if (true) {
        byte[] placeHolder = new byte[64 * 1024 * 1024];
        System.out.println(placeHolder.length / 1024);
    }
    System.gc();
}

我们在if中实例化了一个数组placeHolder,然后在if的作用域外通过System.gc();手动触发了GC,其用意是回收placeHolder,因为placeHolder已经无法访问到了。来看看输出:

65536
[GC 68239K->65952K(125952K), 0.0014820 secs]
[Full GC 65952K->65881K(125952K), 0.0093860 secs]

Full GC 65952K->65881K(125952K)代表的意思是:本次GC后,内存占用从65952K降到了65881K。意思其实是说GC没有将placeHolder回收掉,是不是不可思议?

下面来看看遵循“不使用的对象应手动赋值为null“的情况:

public static void main(String[] args) {
    if (true) {
        byte[] placeHolder = new byte[64 * 1024 * 1024];
        System.out.println(placeHolder.length / 1024);
        placeHolder = null;
    }
    System.gc();
}

其输出为:

65536
[GC 68239K->65952K(125952K), 0.0014910 secs]
[Full GC 65952K->345K(125952K), 0.0099610 secs]

这次GC后内存占用下降到了345K,即placeHolder被成功回收了!对比两段代码,仅仅将placeHolder赋值为null就解决了GC的问题,真应该感谢“不使用的对象应手动赋值为null“。

等等,为什么例子里placeHolder不赋值为null,GC就“发现不了”placeHolder该回收呢?这才是问题的关键所在。

运行时栈

典型的运行时栈

如果你了解过编译原理,或者程序执行的底层机制,你会知道方法在执行的时候,方法里的变量(局部变量)都是分配在栈上的;当然,对于Java来说,new出来的对象是在堆中,但栈中也会有这个对象的指针,和int一样。

比如对于下面这段代码:

public static void main(String[] args) {
    int a = 1;
    int b = 2;
    int c = a + b;
}

其运行时栈的状态可以理解成:

索引变量1a2b3c

“索引”表示变量在栈中的序号,根据方法内代码执行的先后顺序,变量被按顺序放在栈中。

再比如:

public static void main(String[] args) {
    if (true) {
        int a = 1;
        int b = 2;
        int c = a + b;
    }
    int d = 4;
}

这时运行时栈就是:

索引变量1a2b3c4d

容易理解吧?其实仔细想想上面这个例子的运行时栈是有优化空间的。

Java的栈优化

上面的例子,main()方法运行时占用了4个栈索引空间,但实际上不需要占用这么多。当if执行完后,变量a、b和c都不可能再访问到了,所以它们占用的1~3的栈索引是可以“回收”掉的,比如像这样:

索引变量1a2b3c1d

变量d重用了变量a的栈索引,这样就节约了内存空间。

提醒

上面的“运行时栈”和“索引”是为方便引入而故意发明的词,实际上在JVM中,它们的名字分别叫做“局部变量表”和“Slot”。而且局部变量表在编译时即已确定,不需要等到“运行时”。

GC一瞥

这里来简单讲讲主流GC里非常简单的一小块:如何确定对象可以被回收。另一种表达是,如何确定对象是存活的。

仔细想想,Java的世界中,对象与对象之间是存在关联的,我们可以从一个对象访问到另一个对象。如图所示。

 

 

再仔细想想,这些对象与对象之间构成的引用关系,就像是一张大大的图;更清楚一点,是众多的树。

如果我们找到了所有的树根,那么从树根走下去就能找到所有存活的对象,那么那些没有找到的对象,就是已经死亡的了!这样GC就可以把那些对象回收掉了。

现在的问题是,怎么找到树根呢?JVM早有规定,其中一个就是:栈中引用的对象。也就是说,只要堆中的这个对象,在栈中还存在引用,就会被认定是存活的。

提醒

上面介绍的确定对象可以被回收的算法,其名字是“可达性分析算法”。

JVM的“bug”

我们再来回头看看最开始的例子:

public static void main(String[] args) {
    if (true) {
        byte[] placeHolder = new byte[64 * 1024 * 1024];
        System.out.println(placeHolder.length / 1024);
    }
    System.gc();
}

看看其运行时栈:

LocalVariableTable:
Start  Length  Slot  Name   Signature
    0      21     0  args   [Ljava/lang/String;
    5      12     1 placeHolder   [B

栈中第一个索引是方法传入参数args,其类型为String[];第二个索引是placeHolder,其类型为byte[]。

联系前面的内容,我们推断placeHolder没有被回收的原因:System.gc();触发GC时,main()方法的运行时栈中,还存在有对args和placeHolder的引用,GC判断这两个对象都是存活的,不进行回收。也就是说,代码在离开if后,虽然已经离开了placeHolder的作用域,但在此之后,没有任何对运行时栈的读写,placeHolder所在的索引还没有被其他变量重用,所以GC判断其为存活。

为了验证这一推断,我们在System.gc();之前再声明一个变量,按照之前提到的“Java的栈优化”,这个变量会重用placeHolder的索引。

public static void main(String[] args) {
    if (true) {
        byte[] placeHolder = new byte[64 * 1024 * 1024];
        System.out.println(placeHolder.length / 1024);
    }
    int replacer = 1;
    System.gc();
}

看看其运行时栈:

LocalVariableTable:
Start  Length  Slot  Name   Signature
    0      23     0  args   [Ljava/lang/String;
    5      12     1 placeHolder   [B
   19       4     1 replacer   I

不出所料,replacer重用了placeHolder的索引。来看看GC情况:

65536
[GC 68239K->65984K(125952K), 0.0011620 secs]
[Full GC 65984K->345K(125952K), 0.0095220 secs]

placeHolder被成功回收了!我们的推断也被验证了。

再从运行时栈来看,加上int replacer = 1;和将placeHolder赋值为null起到了同样的作用:断开堆中placeHolder和栈的联系,让GC判断placeHolder已经死亡。

现在算是理清了“不使用的对象应手动赋值为null“的原理了,一切根源都是来自于JVM的一个“bug”:代码离开变量作用域时,并不会自动切断其与堆的联系。为什么这个“bug”一直存在?你不觉得出现这种情况的概率太小了么?算是一个tradeoff了。

总结

希望看到这里你已经明白了“不使用的对象应手动赋值为null“这句话背后的奥义。我比较赞同《深入理解Java虚拟机》作者的观点:在需要“不使用的对象应手动赋值为null“时大胆去用,但不应当对其有过多依赖,更不能当作是一个普遍规则来推广。

参考

周志明. 深入理解Java虚拟机:JVM高级特性与最佳实践[M]. 机械工业出版社, 2013.

推荐阅读(点击即可跳转阅读)

1. SpringBoot内容聚合

2. 面试题内容聚合

3. 设计模式内容聚合

4. Mybatis内容聚合

5. 多线程内容聚合

发布于 2019-11-14  from:https://zhuanlan.zhihu.com/p/91763061

java中将对象赋值为null,对垃圾回收有用吗?

相信,网上很多java性能优化的帖子里都会有这么一条:尽量把不使用的对象显式得置为null.这样有助于内存回收

     可以明确的说,这个观点是基本错误的.sun jdk远比我们想象中的机智.完全能判断出对象是否已经no ref..但是,我上面用的词是"基本".也就是说,有例外的情况.这里先把这个例外情况给提出来,后续我会一点点解释.这个例外的情况是, 方法前面中有定义大的对象,然后又跟着非常耗时的操作,且没有触发JIT编译..总结这句话,就是

写道
除非在一个方法中,定义了一个非常大的对象,并且在后面又跟着一段非常耗时的操作.并且,该方法没有满足JIT编译条件,否则显式得设置 obj = null是完全没有必要的

 上面这句话有点绕,但是,上面说的每一个条件都是有意义的.这些条件分别是

写道
1 同一个方法中
2 定义了一个大对象(小对象没有意义)
3 之后跟着一个非常耗时的操作.
4 没有满足JIT编译条件

 上面4个条件缺一不可,把obj显式设置成null才是有意义的. 下面我会一一解释上面的这些条件

 

在解释上面的条件之前,简略的说一下一些基础知识.

(1)sun jdk的内存垃圾判定,是基于根搜索算法的.也就是说,在GC root为跟,能被搜索到的,就认为是存活对象,搜索不到的,则认为是"垃圾".

(2)GC root  里和我们这篇文章有关的gc root是这一条

写道
Java Local
Local variable. For example, input parameters or locally created objects of methods that are still in the stack of a thread.

 这句话直接翻译就是说是"本地变量,例如方法的参数或者方法中创建的局部变量".如果换一种说法是,

写道
Java 方法栈(Java Method Stack)的局部变量表(Local Variable Table)中引用的对象。

 

下面开始说四大条件. 我们测试是否被垃圾回收的方法是,申请一个64M的byte数组(作为大对象),然后调用System.gc();.运行的时候用 -verbose:gc 观察回收情况来判定是否会回收.

 

同一个方法中

 这个条件是最容易理解的,如果大对象定义在其他方法中,那么是不需要设置成Null的,

Java代码   收藏代码
  1. public class Test  
  2. {  
  3.   
  4.     public static void main(String[] args){  
  5.       
  6.         foo();  
  7.           
  8.         System.gc();  
  9.     }  
  10.       
  11.     public static void foo(){  
  12.         byte[] placeholder = new byte[64*1024*1024];  
  13.     }  
  14. }  

 对应的输出如下,可以看到64M的内存已经被回收

写道
D:\>java -verbose:gc Test
[GC 66798K->66120K(120960K), 0.0012225 secs]
[Full GC  66120K->481K(120960K), 0.0059647 secs]

 其实很好理解,placeholder是foo方法的局部变量,在main方法中调用的时候,其实foo方法对应的栈帧已经结束.那么placeholder指向的大对象自然被gc的时候回收了.

 

定义了一个大对象

这句话的意思也很好理解.只有定义的是大的对象,我们才需要关心他尽快被回收.如果你只是定义了一个 String str = "abc"; 后续手动设置成null让gc回收是没有任何意义的.

 

后面跟着一个非常耗时的操作

这里理解是:后面的这个耗时的可能超过了一个GC的周期.例如

Java代码   收藏代码
  1. public static void main(String[] args) throws Exception{  
  2.         byte[] placeholder = new byte[64*1024*1024];  
  3.         Thread.sleep(3000l);  
  4.         // dosomething  
  5.     }  

 在线程sleep的三秒内,可能jvm已经进行了好几次ygc.但是由于placeholder一直持有这个大对象,所以造成这个64M的大对象一直无法被回收,甚至有可能造成了满足进入old 区的条件.这个时候,在sleep之前,显式得把placeholder设置成Null是有意义的. 但是,

写道
如果没有这个耗时的操作,main方法可以非常快速的执行结束,方法返回,同时也会销毁对应的栈帧.那么就是回到第一个条件,方法已经执行结束,在下一次gc的时候,自然就会把对应的"垃圾"给回收掉.

 

没有满足JIT编译条件

  jit编译的触发条件,这里就不多阐述了.对应的测试代码和前面一样

Java代码   收藏代码
  1. public class Test  
  2. {  
  3.     public static void main(String[] args) throws Exception{  
  4.         byte[] placeholder = new byte[64*1024*1024];  
  5.         placeholder = null;  
  6.         //do some  time-consuming operation  
  7.         System.gc();  
  8.     }  
  9. }  

 在解释执行中,我们认为

写道
placeholder = null;

 是有助于对这个大对象的回收的.在JIT编译下,我们可以通过强制执行编译执行,然后打印出对应的 ASM码的方式查看. 安装fast_debug版本的jdk请查看 

使用-XX:+PrintAssembly打印asm代码遇到的问题

命令是

写道
D:\software\jdk6_fastdebug\jdk1.6.0_25\fastdebug\bin>java -Xcomp -XX:+PrintAssembly Test > log.txt

 

ASM 写道
Decoding compiled method 0x0267f1c8:
Code:
[Disassembling for mach='i386']
[Entry Point]
[Verified Entry Point]
[Constants]
# {method} 'main' '([Ljava/lang/String;)V' in 'Test'
# parm0: ecx = '[Ljava/lang/String;'
# [sp+0x20] (sp of caller)
;; block B1 [0, 0]

0x0267f2d0: mov %eax,-0x8000(%esp)
0x0267f2d7: push %ebp
0x0267f2d8: sub $0x18,%esp ;*ldc ; - Test::main@0 (line 7)
;; block B0 [0, 10]

0x0267f2db: mov $0x4000000,%ebx
0x0267f2e0: mov $0x20010850,%edx ; {oop({type array byte})}
0x0267f2e5: mov %ebx,%edi
0x0267f2e7: cmp $0xffffff,%ebx
0x0267f2ed: ja 0x0267f37f
0x0267f2f3: mov $0x13,%esi
0x0267f2f8: lea (%esi,%ebx,1),%esi
0x0267f2fb: and $0xfffffff8,%esi
0x0267f2fe: mov %fs:0x0(,%eiz,1),%ecx
0x0267f306: mov -0xc(%ecx),%ecx
0x0267f309: mov 0x44(%ecx),%eax
0x0267f30c: lea (%eax,%esi,1),%esi
0x0267f30f: cmp 0x4c(%ecx),%esi
0x0267f312: ja 0x0267f37f
0x0267f318: mov %esi,0x44(%ecx)
0x0267f31b: sub %eax,%esi
0x0267f31d: movl $0x1,(%eax)
0x0267f323: mov %edx,0x4(%eax)
0x0267f326: mov %ebx,0x8(%eax)
0x0267f329: sub $0xc,%esi
0x0267f32c: je 0x0267f36f
0x0267f332: test $0x3,%esi
0x0267f338: je 0x0267f34f
0x0267f33e: push $0x844ef48 ; {external_word}
0x0267f343: call 0x0267f348
0x0267f348: pusha 
0x0267f349: call 0x0822c2e0 ; {runtime_call}
0x0267f34e: hlt 
0x0267f34f: xor %ebx,%ebx
0x0267f351: shr $0x3,%esi
0x0267f354: jae 0x0267f364
0x0267f35a: mov %ebx,0xc(%eax,%esi,8)
0x0267f35e: je 0x0267f36f
0x0267f364: mov %ebx,0x8(%eax,%esi,8)
0x0267f368: mov %ebx,0x4(%eax,%esi,8)
0x0267f36c: dec %esi
0x0267f36d: jne 0x0267f364 ;*newarray
; - Test::main@2 (line 7)
0x0267f36f: call 0x025bb450 ; OopMap{off=164}
;*invokestatic gc
; - Test::main@7 (line 10)
; {static_call}
0x0267f374: add $0x18,%esp
0x0267f377: pop %ebp
0x0267f378: test %eax,0x370100 ; {poll_return}
0x0267f37e: ret 
;; NewTypeArrayStub slow case
0x0267f37f: call 0x025f91d0 ; OopMap{off=180}
;*newarray
; - Test::main@2 (line 7)
; {runtime_call}
0x0267f384: jmp 0x0267f36f
0x0267f386: nop 
0x0267f387: nop 
;; Unwind handler
0x0267f388: mov %fs:0x0(,%eiz,1),%esi
0x0267f390: mov -0xc(%esi),%esi
0x0267f393: mov 0x198(%esi),%eax
0x0267f399: movl $0x0,0x198(%esi)
0x0267f3a3: movl $0x0,0x19c(%esi)
0x0267f3ad: add $0x18,%esp
0x0267f3b0: pop %ebp
0x0267f3b1: jmp 0x025f7be0 ; {runtime_call}
0x0267f3b6: hlt 
0x0267f3b7: hlt 
0x0267f3b8: hlt 
0x0267f3b9: hlt 
0x0267f3ba: hlt 
0x0267f3bb: hlt 
0x0267f3bc: hlt 
0x0267f3bd: hlt 
0x0267f3be: hlt 
0x0267f3bf: hlt 
[Stub Code]
0x0267f3c0: nop ; {no_reloc}
0x0267f3c1: nop 
0x0267f3c2: mov $0x0,%ebx ; {static_stub}
0x0267f3c7: jmp 0x0267f3c7 ; {runtime_call}
[Exception Handler]
0x0267f3cc: mov $0xdead,%ebx
0x0267f3d1: mov $0xdead,%ecx
0x0267f3d6: mov $0xdead,%esi
0x0267f3db: mov $0xdead,%edi
0x0267f3e0: call 0x025f9c40 ; {runtime_call}
0x0267f3e5: push $0x83c8bc0 ; {external_word}
0x0267f3ea: call 0x0267f3ef
0x0267f3ef: pusha 
0x0267f3f0: call 0x0822c2e0 ; {runtime_call}
0x0267f3f5: hlt 
[Deopt Handler Code]
0x0267f3f6: push $0x267f3f6 ; {section_word}
0x0267f3fb: jmp 0x025bbac0 ; {runtime_call}

 可以看到, placeholder = null; 这个语句被消除了! 也就是说,对于JIT编译以后的来说,压根不需要这个语句! 

所以说,如果是解释执行的情况下,显式设置成Null是没有任何必要的!

 

到这里,基本已经把文章开头说的那个论断给说明清楚了.但是,在文章的结尾,补充一下局部变量表会对内存回收有什么影响.这个例子参照<深入理解Java虚拟机:JVM高级特性与最佳实践> 一书

我们认为

Java代码   收藏代码
  1. public class Test  
  2. {  
  3.     public static void main(String[] args) throws Exception{  
  4.         byte[] placeholder = new byte[64*1024*1024];  
  5.         //do some  time-consuming operation  
  6.         System.gc();  
  7.     }  
  8. }  

 这样的情况下,placeholder的对象是不会被回收的.可以理解..然后我们继续修改方法体

Java代码   收藏代码
  1. public class Test  
  2. {  
  3.     public static void main(String[] args) throws Exception{  
  4.         {  
  5.             byte[] placeholder = new byte[64*1024*1024];  
  6.         }  
  7.         System.gc();  
  8.     }  
  9. }  

 我们运行发现

写道
d:\>java -verbose:gc Test
[GC 66798K->66072K(120960K), 0.0021019 secs]
[Full GC  66072K->66017K(120960K), 0.0069085 secs]

 垃圾收集器并不会把对象给回收..明明已经出了作用域,竟然还是不回收!. 好吧,继续修改例子

Java代码   收藏代码
  1. public class Test  
  2. {  
  3.     public static void main(String[] args) throws Exception{  
  4.         {  
  5.             byte[] placeholder = new byte[64*1024*1024];  
  6.         }  
  7.         int a = 0;  
  8.         System.gc();  
  9.     }  
  10. }  

 唯一的变化就是新增了一个 int a = 0; 继续看效果

写道
d:\>java -verbose:gc Test
[GC 66798K->66144K(120960K), 0.0011617 secs]
[Full GC  66144K->481K(120960K), 0.0060882 secs]

 可以看到,大对象被回收了..这是一个神奇的例子..能想到这个,我对书的作者万分佩服! 但是这个例子的解释,在书中的解释有点泛(至少我刚开始没看懂),所以这里就仔细说明一下.

要解释这个,先大概看一下  Java执行机制  里面局部变量表的部分.

写道
局部变量区用于存放方法中的局部变量和方法参数,.局部变量表用Slot为单位.jvm在实现的时候为了节省栈帧空间,做了一个简单的优化,就是slot的复用.如果当前字节码的PC计数器已经超出某些变量的作用域,那么这些变量的slot就可以给其他的复用.

上面的这段话有点抽象,后面一个个解释.其实方法的局部变量表大小在javac的时候就已经确定了.

写道
在局部变量表的slot持有的某个对象,他是无法被垃圾回收的.因为局部变量表本来就是GC Root之一

 

在class文件中,方法体对应的Code属性中就有对应的Locals属性,就是来记录局部变量表的大小的.例子如下:

Java代码   收藏代码
  1. public class Test  
  2. {  
  3.     public void foo(int a,int b){  
  4.         int c = 0;  
  5.         return;  
  6.     }  
  7. }  

 通过 javac -g:vars Test 编译,然后,通过javap -verbose 查看

写道
public void foo(int, int);
Code:
Stack=1,  Locals=4, Args_size=3
0: iconst_0
1: istore_3
2: return
LocalVariableTable:
Start Length Slot Name Signature
0 3 0 this LTest;
0 3 1 a I
0 3 2 b I
2 1 3 c I

 

可以看到,局部变量表的Slot数量是4个.分别是 this,a,b,c ..这个非常好理解.那么,什么叫做Slot的复用呢,继续看例子

Java代码   收藏代码
  1. public class Test  
  2. {  
  3.     public void foo(int a,int b){  
  4.         {  
  5.             int d = 0;  
  6.         }  
  7.         int c = 0;  
  8.         return;  
  9.     }  
  10. }  

 在 int c = 0;之前新增一个作用域,里面定义了一个局部变量.如果没有slot复用机制,那么,理论上说,这个方法中局部变量表的slot个数应该是5个,但是,看具体的javap 输出

写道
public void foo(int, int);
Code:
Stack=1,  Locals=4, Args_size=3
0: iconst_0
1: istore_3
2: iconst_0
3: istore_3
4: return
LocalVariableTable:
Start Length Slot Name Signature
2 0 3 d I
0 5 0 this LTest;
0 5 1 a I
0 5 2 b I
4 1 3 c I

 

可以看到,对应的locals=4 ,也就是对应的slot个数还是4个. 通过查看对应的LocalVariableTable属性,可以看到,局部变量d和c都是在Slot[3]中. 这就是上面说的,在某个作用域结束以后,里面的对应的slot并没有马上消除,而是继续留着给下面的局部变量使用..按照这样理解,

 

Java代码   收藏代码
  1. public class Test  
  2. {  
  3.     public static void main(String[] args) throws Exception{  
  4.         {  
  5.             byte[] placeholder = new byte[64*1024*1024];  
  6.         }  
  7.         System.gc();  
  8.     }  
  9. }  

 这个例子中,在执行System.gc()的时候,虽然placeholder 的作用域已经结束,但是placeholder 对应的slot还存在,继续持有64M数组这个大对象,那么自然的,在GC的时候不会把对应的大对象给清理掉.而在

 

Java代码   收藏代码
  1. public class Test  
  2. {  
  3.     public static void main(String[] args) throws Exception{  
  4.         {  
  5.             byte[] placeholder = new byte[64*1024*1024];  
  6.         }  
  7.         int a = 0;  
  8.         System.gc();  
  9.     }  
  10. }  

 这个例子中,在System.gc的时候,placeholder对应的slot已经被a给占用了,那么对应的大对象就变成了无根的"垃圾",当然会被清楚.这一点,可以通过javap明显的看到

 

写道

 

public static void main(java.lang.String[]) throws java.lang.Exception;
Code:
Stack=1, Locals=2, Args_size=1
0: ldc #2; //int 67108864
2: newarray byte
4: astore_1
5: iconst_0
6: istore_1
7: invokestatic #3; //Method java/lang/System.gc:()V
10: return
LocalVariableTable:
Start Length Slot Name Signature
5 0 1 placeholder [B
0 11 0 args [Ljava/lang/String;
7 4 1 a I

Exceptions:
throws java.lang.Exception
}

 可以看到,placeholder 和 a 都对应于Slot[1].

 

这个例子说明的差不多了,在上面的基础上,再多一个例子

 

Java代码   收藏代码
  1. public class Test  
  2. {  
  3.     public static void main(String[] args) throws Exception{  
  4.         {  
  5.             int b = 0;  
  6.             byte[] placeholder = new byte[64*1024*1024];  
  7.         }  
  8.         int a = 0;  
  9.         System.gc();  
  10.     }  
  11. }  

 这个代码中,这个64M的大对象会被GC回收吗..

 

参考文章:

   http://icyfenix.iteye.com/blog/900737

   http://help.eclipse.org/indigo/index.jsp?topic=%2Forg.eclipse.mat.ui.help%2Fconcepts%2Fgcroots.html

Java执行机制

前言

       这篇里的东西,其实是我在草稿箱里找到的.因为当时写的比较粗,而且这个题目的内容没有完结..所以一直没有发布.但是后续有篇文章 

java中 obj=null对垃圾回收有用吗 

  要引用里面局部变量表的知识,所以就先把这个半吊子发布出来,后续慢慢补充.

类执行机制

jvm采用中间码来实现执行.其中,方法执行的指令有下面几个

(1)invokestatic 执行static方法

(2)invokevirtual 调用对象实例方法

(3)invokeinterface 调用接口方法

(4)invokespecial 调用private方法和Init方法

上面说的其实有点笼统,invokevirtual和invokespecial的知识可以看一下 Java方法分派 里面说的比较详细.

另外,jdk7以后新增了一条方法执行 invokedynamic 提供了一条对动态语言的支持.可惜在jdk7没有在java语言中支持该指令. 可以看一下 http://rednaxelafx.iteye.com/blog/477934

      sun JDK基于栈的体系来执行字节码.线程在创建后,都会产生程序计数器(PC register)和栈(Stack),其中程序计数器存放要执行的指令在方法内的偏移量,栈当中存放栈帧,每个方法每次调用都会产生栈帧.


注:该图来自 毕玄的书 <发布式Java应用基础和实践>
局部变量区用于存放方法中的局部变量和参数,操作数栈用于存放方法执行过程中的中间结果.
 可以这么理解,任何在方法体中特意定义过的局部变量,那么都会放到局部变量表(方法参数也会放这里).其他的中间结果.具体看下面的例子
Java代码  收藏代码
  1. public class Test  
  2. {  
  3.     public int add(int a,int b){  
  4.         int c = a + 1;  
  5.         return c * b;  
  6.     }  
  7. }  
 看javap输出
首先要说明的是,按照之前的说法,方法参数会放到局部变量表中.所以局部变量表中的第一个值对应add方法的参数a,第二个值对应于参数b.

 

写道
public int add(int, int);
Code:
Stack=2, Locals=4, Args_size=3
0: iload_1 //将局部变量表中第一个值压入操作数栈(这个值对应add方法中的参数a)
1: iconst_1 //将int 类型的 1 放入操作数栈. 
2: iadd //执行add指令. 将操作数栈顶两个元素相加,然后把结果放到栈顶 (a + 1)
3: istore_3 //把栈顶元素放到局部变量表中的第三个位置.也就是c
4: iload_3 //把局部变量表第三个元素压入栈顶,也就是c
5: iload_2 //把局部变量表第二个元素压入栈顶,也就是b
6: imul //执行mul指令,将栈顶两个相乘,然后把结果放入栈顶,也就是 c * b
7: ireturn //把栈顶元素返回.
LineNumberTable:
line 8: 0
line 9: 4

}

 补充说明一下,

写道
其实局部变量表是有第零个元素的.不过一般这个元素都是指向this.如果把方法改成static,就可以看到第一个方法参数(也就是a) 就是以iload_0来压栈了.

栈顶缓存

   从上面的例子看到,我们计算 c * b 的时候,需要从存放c的寄存器(也就是第三个局部变量表的元素)先压入操作数栈的栈顶,然后再计算相乘操作.而栈顶缓存的效果就是,取消压栈的这次操作,直接把寄存器的数据拿来做相乘操作,然后把结果压入栈.

使用-XX:+PrintAssembly打印asm代码遇到的问题

      要用PrintAssembly的目的 应该会另开帖子说明,本帖只是为了记录为了简单的记录使用这个命令遇到的问题.

 

      1 ,直接使用,用的是

写道
C:\Users\zhenghui>java -version
java version "1.7.0_25"
Java(TM) SE Runtime Environment (build 1.7.0_25-b17)
Java HotSpot(TM) 64-Bit Server VM (build 23.25-b01, mixed mode)

 该版本,当然,必须用不了咯..继续google.发现,需要用fastjson版本的jdk.然后,继续找.

       2,找到对应的下载地址. http://download.java.net/jdk6/6u25/promoted/b03/index.html

 注意,需要下载debug版本的.下载下来是一个jar,双击运行.然后就可以安装.

这里下载的b03版本,有可能在window下跑不通,每次运行都会造成jvm crash.可以换一下b01的试试

http://download.java.net/jdk6/6u25/promoted/b01/index.html  

 

     3  然后,就运行试试.继续遇到问题

写道
D:\software\jdk6_fastdebug\jdk1.6.0_25\fastdebug\bin>java -XX:+UnlockDiagnosticV
MOptions -XX:+PrintAssembly Test
VM option '+UnlockDiagnosticVMOptions'
VM option '+PrintAssembly'
Java HotSpot(TM) Client VM warning: PrintAssembly is enabled; turning on DebugNo
nSafepoints to gain additional output
Could not load hsdis-i386.dll; library not loadable; PrintAssembly is disabled

  4 后续就是找hsdis-i386.dll 的问题了.这也是我用时间最久的地方.我按照stackoverflow的说明,自己build这货,但是死的很惨,一直没成功.具体就不多说了,最后还是万能的撒迦告诉了我答案. 

RednaxelaFX 写道
是撒迦…  

这个疑问在之前某帖的回复里有提到:HotSpot的JIT编译器遇到简单无限循环时
RednaxelaFX 写道
lvgang 写道
好文,很有深度,就是看不太懂,呵呵。有个问题向博主请教,从上面打印出来的汇编代码来看,貌似是 gnu as,windows 上也能用 gnu as?

是的,这个是由GNU binutils里的as提供反汇编功能。
Sun HotSpot需要一个反汇编插件才可以使用-XX:+PrintAssembly参数来打印JIT编译生成的代码。该插件有一组通用接口,本来是可以用任意反汇编器套个适配器就行。官方提供了一个现成的版本(hsdis)是基于gas的,我懒于是就直接用它了。在Windows上直接build我还没成功过,用MinGW和Cygwin都试过不行。我用的版本是在Ubuntu上cross-compile出来的,根据插件作者提供的cross-compile指引来做没有遇到问题。
编译出来的hsdis-i386.dll放到JDK安装目录中jre/bin/server和jre/bin/client中即可。


不太记得是不是从Sun JDK 6 update 20开始,在product build的HotSpot里要用-XX:+PrintAssembly参数必须同时带上-XX:+UnlockDiagnosticVMOptions参数才可以。
Command prompt代码  收藏代码
  1. java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly YourMainClass  

debug与fastdebug build就不用带。

而在比较老的Sun JDK 6里这个插件的名字要改为hdis-i486.dll才行。具体是从哪个版本开始变的我可以回头查查看。

如果有人需要我编译好的这个插件的话,待会儿可以上传一个到圈子共享里。
已经上传到圈子的共享里了

OpenJDK 7里可以看到还有另外一个附加的选项,-XX:PrintAssemblyOptions,可以用来向反编译插件传递参数。

借助HotSpot SA来反汇编
这帖提到的也是其中一个办法。看图:


=======================================

上面是针对Sun JDK的HotSpot而言。
JRockit的话要用别的办法,不过由于Oracle在输出的日志里说那信息是confidential的,所以抱歉我不能在这里说。
IBM J9的话我还没找到简单的办法。
Harmony、Jikes RVM、Maxine这些都有提供命令行参数可以让JVM把动态编译的汇编吐出来。

    然后 在JE的虚拟机圈子里找到了下载链接.

http://hllvm.group.iteye.com/group/share

 

     最后就是下载对应的 hsdis-i386.dll ,在DK目录下jre/bin/client和jre/bin/server中各放一份 .这个问题终于搞定..

 

在实际的使用中,我们通过直接加-XX:+PrintAssembly 打印ASM码会有两个问题

1 ASM码非常多.因为系统会打印出类似loadclass toString对应这些方法的ASM码.但是我们可能只关心对应的某一个方法而已.系统会打印8W行+的输出,但是我们可能只关心里面的100行

2 经常会帮我们内联.这个其实很纠结.之前为了这个内联,每次都想方设法如何让方法不被内联.当然,对应的解决方法也很简单.

 

对应的代码(我懒得自己写,就直接copy网上的了)

Java代码  收藏代码
  1. public class Test{  
  2.     int a = 1;  
  3.     static int b = 2;  
  4.   
  5.     public int sum(int c){  
  6.         return a + b + c;  
  7.     }  
  8.       
  9.     public static void main(String[] args){  
  10.         new Test().sum(3);  
  11.     }  
  12. }  

 运行的代码如下

写道
D:\software\jdk6_fastdebug\jdk1.6.0_25\fastdebug\bin>java -Xcomp -XX:+PrintAssembly -XX:CompileCommand=dontinline,*Test.sum -XX:CompileCommand=compileonly,*Test.sum Test > log.txt

 其中,

  -XX:CompileCommand=dontinline,*Test.sum 这个表示不要把sum方法给内联了.这是解决内联问题

-XX:CompileCommand=compileonly,*Test.sum 这个表示只编译sum方法,这样的话,只会输出sum方法的ASM码.

对应的输出如下

写道
Java HotSpot(TM) Client VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output
VM option '+PrintAssembly'
VM option 'CompileCommand=dontinline,*Test.sum'
VM option 'CompileCommand=compileonly,*Test.sum'
CompilerOracle: dontinline *Test.sum
CompilerOracle: compileonly *Test.sum
Loaded disassembler from D:\software\jdk6_fastdebug\jdk1.6.0_25\fastdebug\jre\bin\client\hsdis-i386.dll
Decoding compiled method 0x026ab608:
Code:
[Disassembling for mach='i386']
[Entry Point]
[Constants]
# {method} 'sum' '(I)I' in 'Test'  //这个好理解,记录一下这个方法名对应的描述符
# this: ecx = 'Test'  //表示this指针在ecx寄存器中
# parm0: edx = int  //sum 方法对应的参数在edx寄存器中.
# [sp+0x20] (sp of caller)
;; block B1 [0, 0]

0x026ab700: nop
0x026ab701: nop
0x026ab702: nop
0x026ab703: nop
0x026ab704: nop
0x026ab705: nop
0x026ab706: nop
0x026ab707: cmp 0x4(%ecx),%eax
0x026ab70a: jne 0x0266ad90 ; {runtime_call}
[Verified Entry Point]
0x026ab710: mov %eax,-0x8000(%esp) //检查栈溢出
0x026ab717: push %ebp //保存上一栈帧基址
0x026ab718: sub $0x18,%esp ;*aload_0 //给新栈帧分配空间.这个18很奇怪,我试了好多,无聊方法写成什么样,都是$0x18.
; - Test::sum@0 (line 13)
;; block B0 [0, 10]

0x026ab71b: mov 0x8(%ecx),%eax ;*getfield a //获取实例变量a,放入eax寄存器中, %ecx在上面已经说了,是存放this指针的寄存器. 0x8(%ecx)表示越过test对象头(对象头占8个字节,后面就是跟着实例变量a的内存位置)
; - Test::sum@1 (line 13)
0x026ab71e: mov $0x2024d7d8,%esi ; {oop('Test')}//获取Test在方法区的指针, 可以看标记oop(methodName),$0x2024d7d8就是对应的Test方法区位置
0x026ab723: mov 0x150(%esi),%esi ;*getstatic b //获取对应的类变量b,放到esi寄存器中,0x150(%esi)表示在Test方法区指针开始的150偏移量的位置存放类变量b
; - Test::sum@4 (line 13)
0x026ab729: add %esi,%eax// esi存放的是b,eax存放的是a.两者相加,放到eax寄存器中
0x026ab72b: add %edx,%eax //edx 存放的是sum方法对应的参数c,eax存放着a+b的和.两者相加放到eax寄存器中
0x026ab72d: add $0x18,%esp //esp为对应的栈帧指针,之前sub 0x18 ,现在加回去.也就是撤销栈帧
0x026ab730: pop %ebp //恢复上一个栈帧
0x026ab731: test %eax,0x230100 ; {poll_return} //轮询方法返回处的SafePoint
0x026ab737: ret  //返回.
0x026ab738: nop
0x026ab739: nop
;; Unwind handler
0x026ab73a: mov %fs:0x0(,%eiz,1),%esi
0x026ab742: mov -0xc(%esi),%esi
0x026ab745: mov 0x198(%esi),%eax
0x026ab74b: movl $0x0,0x198(%esi)
0x026ab755: movl $0x0,0x19c(%esi)
0x026ab75f: add $0x18,%esp
0x026ab762: pop %ebp
0x026ab763: jmp 0x026a7be0 ; {runtime_call}
0x026ab768: hlt
0x026ab769: hlt
0x026ab76a: hlt
0x026ab76b: hlt
0x026ab76c: hlt
0x026ab76d: hlt
0x026ab76e: hlt
0x026ab76f: hlt
[Exception Handler]
[Stub Code]
0x026ab770: mov $0xdead,%ebx ; {no_reloc}
0x026ab775: mov $0xdead,%ecx
0x026ab77a: mov $0xdead,%esi
0x026ab77f: mov $0xdead,%edi
0x026ab784: call 0x026a9c40 ; {runtime_call}
0x026ab789: push $0x83c8bc0 ; {external_word}
0x026ab78e: call 0x026ab793
0x026ab793: pusha
0x026ab794: call 0x0822c2e0 ; {runtime_call}
0x026ab799: hlt
[Deopt Handler Code]
0x026ab79a: push $0x26ab79a ; {section_word}
0x026ab79f: jmp 0x0266bac0 ; {runtime_call}

 指令码的解析,上面基本都写了.

参照链接

http://stackoverflow.com/questions/1503479/how-to-see-jit-compiled-code-in-jvm/4149878#4149878

http://hllvm.group.iteye.com/group/topic/21769

https://blogs.oracle.com/kto/entry/mustang_jdk_6_0_fastdebug

HotSpot的JIT编译器遇到简单无限循环时

对下面这种带有简单无限循环的Java程序,

Java代码  收藏代码
  1. // java -XX:+PrintCompilation -XX:+PrintAssembly TestC2InfiniteLoop  
  2.   
  3. public class TestC2InfiniteLoop {  
  4.     public static void foo() {  
  5.         while (true) { int i = 1; }  
  6.     }  
  7.       
  8.     public static void main(String[] args) {  
  9.         foo();  
  10.     }  
  11. }  



HotSpot的JIT编译器会:

1、client模式:执行命令:

Command prompt代码  收藏代码
  1. java -XX:+PrintCompilation -XX:+PrintAssembly TestC2InfiniteLoop  


C1会编译代码,会将循环内无用的代码都消除掉,但不会把循环本身消除:

Hotspot log代码  收藏代码
  1.   1%      TestC2InfiniteLoop::foo @ 0 (5 bytes)  
  2. Decoding compiled method 0x00bc6748:  
  3. Code:  
  4. [Disassembling for mach=&apos;i386&apos;]  
  5. [Entry Point]  
  6. [Verified Entry Point]  
  7.   ;;  block B2 [0, 0]  
  8.   
  9.   0x00bc6810: mov    %eax,-0x4000(%esp)  
  10.   0x00bc6817: push   %ebp  
  11.   0x00bc6818: mov    %esp,%ebp  
  12.   0x00bc681a: sub    $0x18,%esp         ;*iconst_1  
  13.                                         ; - TestC2InfiniteLoop::foo@0 (line 5)  
  14.   ;;  block B3 [0, 0]  
  15.   
  16.   0x00bc681d: nop      
  17.   0x00bc681e: nop      
  18.   0x00bc681f: nop                       ; OopMap{off=16}  
  19.                                         ;*goto  
  20.                                         ; - TestC2InfiniteLoop::foo@2 (line 5)  
  21.   ;;  block B0 [0, 2]  
  22.   
  23.   0x00bc6820: test   %eax,0x970100      ;   {poll}  
  24.   ;;   26 branch [AL] [B0]   
  25.   0x00bc6826: jmp    0x00bc6820         ;*goto  
  26.                                         ; - TestC2InfiniteLoop::foo@2 (line 5)  
  27.   ;;  block B1 [0, 0]  
  28.   
  29.   0x00bc6828: mov    %eax,-0x4000(%esp)  
  30.   0x00bc682f: push   %ebp  
  31.   0x00bc6830: mov    %esp,%ebp  
  32.   0x00bc6832: sub    $0x18,%esp  
  33.   0x00bc6835: mov    %ecx,(%esp)  
  34.   0x00bc6838: call   0x082ea120         ;   {runtime_call}  
  35.   ;;   20 branch [AL] [B0]   
  36.   0x00bc683d: jmp    0x00bc6820  
  37.   0x00bc683f: nop      
  38.   0x00bc6840: nop      
  39.   0x00bc6841: hlt      
  40.   0x00bc6842: hlt      
  41.   0x00bc6843: hlt      
  42.   0x00bc6844: hlt      
  43.   0x00bc6845: hlt      
  44.   0x00bc6846: hlt      
  45.   0x00bc6847: hlt      
  46.   0x00bc6848: hlt      
  47.   0x00bc6849: hlt      
  48.   0x00bc684a: hlt      
  49.   0x00bc684b: hlt      
  50.   0x00bc684c: hlt      
  51.   0x00bc684d: hlt      
  52.   0x00bc684e: hlt      
  53.   0x00bc684f: hlt      
  54. [Exception Handler]  
  55. [Stub Code]  
  56.   0x00bc6850: mov    $0xdead,%ebx       ;   {no_reloc}  
  57.   0x00bc6855: mov    $0xdead,%ecx  
  58.   0x00bc685a: mov    $0xdead,%edx  
  59.   0x00bc685f: mov    $0xdead,%esi  
  60.   0x00bc6864: mov    $0xdead,%edi  
  61.   0x00bc6869: jmp    0x00bc1d60         ;   {runtime_call}  
  62.   0x00bc686e: push   $0xbc686e          ;   {section_word}  
  63.   0x00bc6873: jmp    0x00b7ba40         ;   {runtime_call}  


上面位于0x00bc6820和0x00bc6826的两条指令就是无限循环的残余物:

Hotspot log代码  收藏代码
  1. 0x00bc6820: test   %eax,0x970100      ;   {poll}  
  2. ;;   26 branch [AL] [B0]   
  3. 0x00bc6826: jmp    0x00bc6820         ;*goto  
  4.                                       ; - TestC2InfiniteLoop::foo@2 (line 5)  


原本在循环内的代码(int i = 1;)已经消失了,剩下的是在回边处对safepoint的轮询(test),以及循环末尾的无条件跳转(jmp)。
从编译记录看,int i = 1;对应的代码是在寄存器分配过程中被削除的。

2、server模式:执行命令:

Command prompt代码  收藏代码
  1. java -server -XX:+PrintCompilation -XX:+PrintAssembly TestC2InfiniteLoop  


C2会拒绝编译这种代码,并打出日志:

Hotspot log代码  收藏代码
  1. 1%      TestC2InfiniteLoop::foo @ 0 (5 bytes)  
  2. 1   COMPILE SKIPPED: trivial infinite loop (not retryable)  


然后foo()中的无限循环就一直在解释器中执行下去了。

在hotspot/src/share/vm/opto/compile.cpp中,bool Compile::final_graph_reshaping()的代码前面有这样的注释:

C++代码  收藏代码
  1. // (4) Detect infinite loops; blobs of code reachable from above but not  
  2. //     below.  Several of the Code_Gen algorithms fail on such code shapes,  
  3. //     so we simply bail out.  Happens a lot in ZKM.jar, but also happens  
  4. //     from time to time in other codes (such as -Xcomp finalizer loops, etc).  
  5. //     Detection is by looking for IfNodes where only 1 projection is  
  6. //     reachable from below or CatchNodes missing some targets.  
  7. // ...  
  8.   
  9. bool Compile::final_graph_reshaping() {  
  10.   // an infinite loop may have been eliminated by the optimizer,  
  11.   // in which case the graph will be empty.  
  12.   if (root()->req() == 1) {  
  13.     record_method_not_compilable("trivial infinite loop");  
  14.     return true;  
  15.   }  
  16.   // ...  
  17. }  



======================================================================

上面的实验中,如果把foo中的无限循环在源码上就变为空循环的话:

Java代码  收藏代码
  1. public static void foo() {  
  2.     while (true) ;  
  3. }  


则无论是在client还是server模式都不会触发对该方法的JIT编译,一直在解释器中去执行那个循环。
查看javac编译生成的字节码,可以确认该循环在字节码中是存在的:

Javap代码  收藏代码
  1. public static void foo();  
  2.   Signature: ()V  
  3.   Code:  
  4.    Stack=0, Locals=0, Args_size=0  
  5.    0:   goto    0  
  6.   LineNumberTable:  
  7.    line 5: 0  
  8.   
  9.   StackMapTable: number_of_entries = 1  
  10.    frame_type = 0 /* same */  


foo()方法中唯一的一条字节码指令就是goto 0了。有趣的是因为foo()带有无限循环,所以编译出来的字节码里连return都没有。

HotSpot解释器触发JIT编译,是通过两个计数器来进行的:方法调用计数器与回边计数器,当这两个计数器的和超过了方法调用或循环次数的预设阈值就触发对某个方法的JIT编译;这两个计数器在每个方法里都有自己的一份。其中,方法调用计数器自然是用来记录某个方法被调用的次数的,而回变计数器则用于记录某方法中所有循环执行的次数(以方法而不是单个循环为粒度)。一个空的无限循环在字节码中的表现是一条goto字节码指令,参数是字节码的相对偏移量,并且该偏移量为0(意味着它指向该goto指令自身)。解释器中有这样的代码:

X86 asm代码  收藏代码
  1. 0x0097e781: test   %edx,%edx  
  2. 0x0097e783: jns    0x0097e7a7  


此时EDX持有相对偏移量,下面的JNS指令会在EDX为非负值的时候执行跳转——正好跳过了计数器自增的代码;这样就只有真正“向后跳”的goto才会使回边计数器自增。也就是说空的无限循环不会引起回边计数器的累加,默认配置下也就不会触发HotSpot进行JIT编译。

======================================================================

* 本文的测试环境是32位Windows XP SP3,E8400,Sun JDK 1.6.0 update 18

关于Java中的WeakReference

阅读原文请访问我的博客BrightLoong's Blog

 

一. 简介

在看ThreadLocal源码的时候,其中嵌套类ThreadLocalMap中的Entry继承了WeakReferenc,为了能搞清楚ThreadLocal,只能先了解下了WeakReferenc(是的,很多时候为了搞清楚一个东西,不得不往上追好几层,先搞清楚其所依赖的东西。)
下面进入正题,WeakReference如字面意思,弱引用, 当一个对象仅仅被weak reference(弱引用)指向, 而没有任何其他strong reference(强引用)指向的时候, 如果这时GC运行, 那么这个对象就会被回收,不论当前的内存空间是否足够,这个对象都会被回收。

二. 认识WeakReference类

WeakReference继承Reference,其中只有两个构造函数:

public class WeakReference<T> extends Reference<T> {
    public WeakReference(T referent) {
        super(referent);
    }

    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }

}
  • WeakReference(T referent):referent就是被弱引用的对象(注意区分弱引用对象和被弱引用的对应,弱引用对象是指WeakReference的实例或者其子类的实例),比如有一个Apple实例apple,可以如下使用,并且通过get()方法来获取apple引用。也可以再创建一个继承WeakReference的类来对Apple进行弱引用,下面就会使用这种方式。
WeakReference<Apple> appleWeakReference = new WeakReference<>(apple);
Apple apple2 = appleWeakReference.get();
  • WeakReference(T referent, ReferenceQueue<? super T> q):与上面的构造方法比较,多了个ReferenceQueue,在对象被回收后,会把弱引用对象,也就是WeakReference对象或者其子类的对象,放入队列ReferenceQueue中,注意不是被弱引用的对象,被弱引用的对象已经被回收了。

三. 使用WeakReference

下面是使用继承WeakReference的方式来使用软引用,并且不使用ReferenceQueue。

简单类Apple

package io.github.brightloong.lab.reference;

/**
 * Apple class
 *
 * @author BrightLoong
 * @date 2018/5/25
 */
public class Apple {

    private String name;

    public Apple(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    /**
     * 覆盖finalize,在回收的时候会执行。
     * @throws Throwable
     */
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("Apple: " + name + " finalize。");
    }

    @Override
    public String toString() {
        return "Apple{" +
                "name='" + name + '\'' +
                '}' + ", hashCode:" + this.hashCode();
    }
}

继承WeakReference的Salad

package io.github.brightloong.lab.reference;

import java.lang.ref.WeakReference;

/**
 * Salad class
 * 继承WeakReference,将Apple作为弱引用。
 * 注意到时候回收的是Apple,而不是Salad
 *
 * @author BrightLoong
 * @date 2018/5/25
 */
public class Salad extends WeakReference<Apple> {
    public Salad(Apple apple) {
        super(apple);
    }
}

Clent调用和输出

package io.github.brightloong.lab.reference;

import java.lang.ref.WeakReference;

/**
 * Main class
 *
 * @author BrightLoong
 * @date 2018/5/24
 */
public class Client {
    public static void main(String[] args) {
        Salad salad = new Salad(new Apple("红富士"));
        //通过WeakReference的get()方法获取Apple
        System.out.println("Apple:" + salad.get());
        System.gc();
        try {
            //休眠一下,在运行的时候加上虚拟机参数-XX:+PrintGCDetails,输出gc信息,确定gc发生了。
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //如果为空,代表被回收了
        if (salad.get() == null) {
            System.out.println("clear Apple。");
        }
    }
}

输出如下:

Apple:Apple{name='红富士'}, hashCode:1846274136
[GC (System.gc()) [PSYoungGen: 3328K->496K(38400K)] 3328K->504K(125952K), 0.0035102 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (System.gc()) [PSYoungGen: 496K->0K(38400K)] [ParOldGen: 8K->359K(87552K)] 504K->359K(125952K), [Metaspace: 2877K->2877K(1056768K)], 0.0067965 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Apple: 红富士 finalize。
clear Apple。

ReferenceQueue的使用

package io.github.brightloong.lab.reference;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;

/**
 * Client2 class
 *
 * @author BrightLoong
 * @date 2018/5/27
 */
public class Client2 {
    public static void main(String[] args) {
        ReferenceQueue<Apple> appleReferenceQueue = new ReferenceQueue<>();
        WeakReference<Apple> appleWeakReference = new WeakReference<Apple>(new Apple("青苹果"), appleReferenceQueue);
        WeakReference<Apple> appleWeakReference2 = new WeakReference<Apple>(new Apple("毒苹果"), appleReferenceQueue);

        System.out.println("=====gc调用前=====");
        Reference<? extends Apple> reference = null;
        while ((reference = appleReferenceQueue.poll()) != null ) {
            //不会输出,因为没有回收被弱引用的对象,并不会加入队列中
            System.out.println(reference);
        }
        System.out.println(appleWeakReference);
        System.out.println(appleWeakReference2);
        System.out.println(appleWeakReference.get());
        System.out.println(appleWeakReference2.get());

        System.out.println("=====调用gc=====");
        System.gc();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("=====gc调用后=====");

        //下面两个输出为null,表示对象被回收了
        System.out.println(appleWeakReference.get());
        System.out.println(appleWeakReference2.get());

        //输出结果,并且就是上面的appleWeakReference、appleWeakReference2,再次证明对象被回收了
        Reference<? extends Apple> reference2 = null;
        while ((reference2 = appleReferenceQueue.poll()) != null ) {
            //如果使用继承的方式就可以包含其他信息了
            System.out.println("appleReferenceQueue中:" + reference2);
        }
    }
}

结果输出如下:

=====gc调用前=====
java.lang.ref.WeakReference@6e0be858
java.lang.ref.WeakReference@61bbe9ba
Apple{name='青苹果'}, hashCode:1627674070
Apple{name='毒苹果'}, hashCode:1360875712
=====调用gc=====
Apple: 毒苹果 finalize。
Apple: 青苹果 finalize。
=====gc调用后=====
null
null
appleReferenceQueue中:java.lang.ref.WeakReference@6e0be858
appleReferenceQueue中:java.lang.ref.WeakReference@61bbe9ba

Process finished with exit code 0

可以看到在队列中(ReferenceQueue),调用gc之前是没有内容的,调用gc之后,对象被回收了,并且弱引用对象appleWeakReference和appleWeakReference2被放入了队列中。

关于其他三种引用,强引用、软引用、虚引用,可以参考http://www.cnblogs.com/gudi/p/6403953.html

通过代码简单介绍JDK 7的MethodHandle,并与.NET的委托对比

(注意:这篇blog里的信息只描述了发文时的情况。到JDK7正式发布的时候已经这篇文章里介绍的部分内容已经过时,包括MethodHandle的调用语法、java.dyn包改名为java.lang.invoke、该包的里API等。下面内容并未更新,敬请理解。)

JDK 7将会实现JSR 292,为在JVM上实现动态语言提供更多支持。其中,MethodHandle是JSR 292的重要组成部分之一。有了它,意味着Java终于有了引用方法的方式,或者用C的术语说,“函数指针”。(我差点要说“引用‘方法’的‘方法’”了,好pun)。
下面的讨论都是基于当前(2009-09)的设计而进行的,今后相关具体设计可能变化,但大的方向应该比较明确了。JDK 7的代码例子都是在JDK 7 Binary Snapshot build 70下测试的。执行程序时要添加-XX:+EnableMethodHandles参数。

与其说JDK 7的MethodHandle像C的函数指针,还不如说像.NET的委托。
C#与.NET从1.0版开始就有“委托”的概念,通过委托可以在代码中引用任意方法,无论方法的可访问性、所属类型如何,无论是静态还是实例方法。之前一帖也提到了,.NET的委托提供了为方法创建“别名”的能力,使我们可以用统一的方式去调用签名相同但名字和所属类型都不一定相同的方法。与C的函数指针所不同的是,.NET的委托不但引用了方法,还会引用执行该方法所需要的环境,对引用实例方法的委托来说“环境”就是方法所在的实例;而C的函数指针则仅指向函数的代码而已,没有引用环境的功能。而且,.NET的委托包含了足够的元数据,可用于运行时做类型检查;而C的函数指针则仅仅是个裸指针。调用委托的速度接近调用虚方法的速度。
JDK 7将引入的MethodHandle从许多方面说都与.NET的委托非常相似。MethodHandle也可以指向任意方法,提供为方法创建“别名”的能力;可以用统一的方式去调用MethodHandle。此外,MethodHandle还支持组合,可以以适配器的方式将多个MethodHandle串在一起,实现参数过滤、参数转换、返回值转换等许多功能。调用MethodHandle的速度接近调用接口方法的速度。
MethodHandle对许多JVM的内部实现来说并不是一个全新的概念。要实现JVM,在内部总会保留一些指向方法的指针。JDK 7只是把它(和其它许多JVM里原本就支持的概念)具体化为Java类型暴露给Java代码用而已;这就是所谓的“reification”。

好吧,简单介绍了些背景,下面就通过代码来认识和感受一下JDK 7的MethodHandle,并与.NET的委托对比。

第一组例子,照例上hello world:

JDK 7:

Java代码  收藏代码
  1. import java.dyn.*;  
  2. import static java.dyn.MethodHandles.*;  
  3.   
  4. public class TestMethodHandle1 {  
  5.     private static void hello() {  
  6.         System.out.println("Hello world!");  
  7.     }  
  8.       
  9.     public static void main(String[] args) {  
  10.         MethodType type = MethodType.make(void.class);  
  11.         MethodHandle method = lookup()  
  12.             .findStatic(TestMethodHandle1.class, "hello", type);  
  13.         method.<void>invoke();  
  14.     }  
  15. }  


执行这个测试需要使用如下命令:

Command prompt代码  收藏代码
  1. java -XX:+EnableMethodHandles TestMethodHandle1  


(注意“java”要使用JDK 7的,不要用了JDK 6或更早的)

首先,要使用MethodHandle,需要引入的类型都在java.dyn包里。这个例子用到的是MethodHandles、MethodHandles.Lookup、MethodType、MethodHandle几个。
流程是:
0、调用MethodHandles.lookup()方法,遍历调用栈检查访问权限,然后得到一个MethodHandles.Lookup实例;该对象用于确认创建MethodHandle的实例的类对目标方法的访问权限是否满足要求,并提供搜索目标方法的逻辑;
1、指定目标方法的“方法类型”,得到一个MethodType实例;
2、通过MethodHandles.lookup()静态方法得到一个类型为MethodHandles.Lookup的工厂,然后靠它搜索指定的类型、指定的名字、指定的方法类型的方法,得到一个MethodHandle实例;
3、调用MethodHandle上的invoke方法。

其中,第1步中调用的MethodType.make()方法接收的参数是一组类型,第一个参数是返回类型,后面依次是各个参数的类型。上例中MethodType.make(void.class)得到的就是一个返回类型为void,参数列表为空的方法类型。如果熟悉Java字节码的话,这个方法类型的描述符就是()V。关于方法描述符的格式,可以参考JVM规范第二版4.3.3小节。MethodType的实例只代表所有返回值与参数类型匹配的一类方法的方法类型,自身没有名字;在检查某个方法是否与某个MethodType匹配时只考虑结构,可以算是一种特殊的structural-typing。

第2步看起来跟普通的反射很像,但通过反射得到的代表方法的对象是java.lang.reflect.Method的实例,它含有许多跟“执行”没有直接关系的信息,比较笨重;通过Method对象调用方法只是正常方法调用的模拟,所有参数会被包装为一个数组,开销较大。而MethodHandle则是个非常轻量的对象,主要目的就是用来引用方法并调用;通过它去调用方法不会导致参数被包装,原始类型的参数也不会被自动装箱。
MethodHandles.Lookup上有三个find方法,包括findStatic、findVirtual、findSpecial,分别对应invokestatic、invokevirtual/invokeinterface、invokespecial会对应的调用逻辑。注意到findVirtual方法所返回的MethodHandle的方法类型会包含一个显式的“this”参数作为第一个参数;调用这样的MethodHandle要显式传入“receiver”。这个看起来就跟.NET的开放委托相似,可以参考我之前的一帖。由于JDK 7的MethodHandle支持currying,可以把receiver保存在MethodHandle里,所以也可以创建出类似.NET的闭合委托的MethodHandle实例。
MethodHandles.Lookup上还有一组方法可以从通过反射API得到的Constructor、Field或Method对象创建出对应的MethodHandle。

第3步调用的MethodHandle.invoke()看似是一个虚方法,实际上并不是MethodHandle上真的存在的方法,而只是标记用的虚构出来的方法。上例中第13行对应的Java字节码是:

Java bytecode代码  收藏代码
  1. invokevirtual java/dyn/MethodHandle.invoke:()V  


也就是假装MethodHandle上有一个描述符为()V且名为invoke的虚方法,通过invokevirtual指令去调用它。
Java编译器为它做特殊处理:返回值类型如同泛型参数在<>内指定,不写的话默认为返回Object类型;参数列表的类型则由Java编译器根据实际参数的表达式推断出来。与正常的泛型方法不同,MethodHandle.invoke指定返回值类型可以使用void和所有原始类型,不必像使用泛型方法时需要把原始类型写为对应的包装类型。
MethodHandle的方法类型不是Java语言的静态类型系统的一部分。虽然它的实例在运行时带有方法类型信息(MethodType),但在编译时Java编译器却不知道这一点。所以在编译时,调用invoke时传入任意个数、任意类型的参数都可以通过编译;但在运行时要成功调用,由Java编译器推断出来的返回值类型与参数列表必须与运行时MethodHandle实际的方法类型一致,否则会抛出WrongMethodTypeExceptionJohn Rose把MethodHandle.invoke的多态性称为“签名多态性(signature polymorphism)”。

用户可以自行继承java.dyn.JavaMethodHandle来创建自定义的MethodHandle子类,可以添加域或方法等,并可以指定该类型看作MethodHandle时的“入口点”——实际指向的方法。

许多JVM实现在JIT编译的时候会做激进的优化,包括常量传播、内联、逃逸分析、无用代码削除等许多。JDK 7的MethodHandle的一个好处是它就像它所指向的目标方法的替身一样,JVM原本可以做的优化对MethodHandle也一样支持,特别是有需要的时候可以把目标方法内联到调用处。相比之下,通过反射去调用方法则无法被JVM有效的优化。

对比C#的例子:
C# 2.0:

C#代码  收藏代码
  1. using System;  
  2.   
  3. static class TestDelegate1 {  
  4.     static void Hello() {  
  5.         Console.WriteLine("Hello world!");  
  6.     }  
  7.       
  8.     static void Main(string[] args) {         
  9.         Action method = Hello; // Action method = new Action(Hello);  
  10.         method();              // method.Invoke();  
  11.     }  
  12. }  


这段代码与前面Java版的TestMethodHandle1功能基本相同。来观察一下两者的异同点。

要在C#里使用委托的流程是:
0、事先声明好合适的委托类型;
1、适用合适的委托类型,指定目标方法创建出委托的实例;
2、调用委托上的Invoke()方法。

.NET允许用户自定义委托类型,在C#里的语法是:

C#代码  收藏代码
  1. modifiers delegate return_type DelegateTypeName(argument_list);  


该语法与方法的声明语法非常像,只是在返回类型之前多了个delegate关键字而已,比C中typedef函数指针类型的语法容易多了。上例用到的System.Action类型就是标准库里声明好的一个委托类型,其声明形如:

C#代码  收藏代码
  1. namespace System {  
  2.     public delegate void Action();  
  3. }  


这样声明出来的委托类型相当于声明了一个Action类,继承System.MulticastDelegate,并且拥有一个返回值类型为void,参数列表为空的Invoke()方法。在C#里,用户无法像声明普通类型一样通过声明一个继承System.Delegate或System.MulticastDelegate的类来得到一个新的委托类型,而只能用上述语法来声明。不过从CLR的角度看,并没有限制用户不能自行继承上述两种类型来声明新的委托类型。
委托是.NET类型系统的一部分。两个委托类型即便表示的签名一致也会被认为是不同的类型,不能相互赋值/转换。这体现出了C#与.NET类型的nominal-typing性质。创建委托实例时则只考虑目标方法与委托类型在签名上是否吻合,而不考虑名字问题,这点又与JDK 7的MethodHandle相似。
委托上的Invoke方法的签名与委托声明的相吻合。在编译时,调用委托的Invoke()方法与调用一般的虚方法一样会被类型检查。
目前在C#里可以显式调用委托上的Invoke()方法,也可以直接把委托当成方法用括号调用。是显式调用Invoke()还是直接用括号调用委托,现在来说只是程序员的偏好问题而已。事实上在C# 2.0以前编译器会阻止程序员显式调用Invoke()方法。

用C#代码例子再稍微解释一下:

C#代码  收藏代码
  1. // 下面声明两个委托类型,它们的签名是一样的  
  2. // (int, int) -> int  
  3. delegate int BinaryIntOp1(int x, int y);  
  4. delegate int BinaryIntOp2(int i, int j);  
  5. // 形式参数的类型是重要的,名字不重要  
  6. // 上面两个委托上的Invoke()方法都形如:  
  7. // [MethodImpl(0, MethodCodeType=MethodCodeType.Runtime)]  
  8. // public virtual int Invoke(int i, int j);  
  9. // 注意到Invoke()是一个虚方法,其返回值与参数类型都是确定的,  
  10. // 而其实现是由运行时直接提供的。  
  11.   
  12. class Demo {  
  13.     // 定义一个签名为(int, int) -> int的方法  
  14.     public static int Add(int a, int b) {  
  15.         return a + b;  
  16.     }  
  17.       
  18.     static void Main(string[] args) {  
  19.         // 可以,Add方法与BinaryIntOp1要求的签名匹配  
  20.         BinaryIntOp1 op1 = new BinaryIntOp1(Add);  
  21.   
  22.         // 可以,Add方法与BinaryIntOp1要求的签名匹配  
  23.         // C# 2.0的隐式创建委托的新特性:等同于写成new BinaryIntOp2(Add);  
  24.         BinaryIntOp2 op2 = Add;  
  25.   
  26.         // 不行,两者属于不同的委托类型,无法相互赋值/转换  
  27.         //BinaryIntOp2 op3 = op1;  
  28.           
  29.         // 调用Invoke()方法会被编译器检查类型是否匹配  
  30.         int sum = op1.Invoke(4, 2);  
  31.         // 下面这样就会在编译时出错:  
  32.         //int sum2 = op2.Invoke(new object(), 0);  
  33.     }  
  34. }  



Java版例子中,创建MethodHandle对象需要在代码里通过MethodHandles.Lookup工厂来查找目标;在C#里编译器已经帮忙找出了目标方法的token,写起来方便许多;C#编译器会根据方法名与委托的签名去寻找合适重载版本的方法,找出它的token,并用于创建委托实例。如果目标方法无法在编译时确定,使用System.Delegate.CreateDelegate(Type, MethodInfo)方法也可以依靠反射信息创建出委托。

CLI的几个主流实现,微软的CLR、Novell的Mono等的JIT编译器都比较静态,遇到虚方法和委托调用都不会内联。从这点说,.NET比JVM的技术复杂度要低一些。不过至少在.NET里使用委托不会带来多少额外开销,所以还是可以放心使用的。

第二组例子,给hello world添加参数:

JDK 7:

Java代码  收藏代码
  1. import java.dyn.*;  
  2. import static java.dyn.MethodHandles.*;  
  3.   
  4. public class TestMethodHandle2 {  
  5.     private static void hello(String name) {  
  6.         System.out.printf("Hello, %s!\n", name);  
  7.     }  
  8.       
  9.     public static void main(String[] args) {  
  10.         if (0 == args.length) args = new String[] { "Anonymous" };  
  11.           
  12.         MethodType type = MethodType.make(void.class, String.class);  
  13.         MethodHandle method = lookup()  
  14.             .findStatic(TestMethodHandle2.class, "hello", type);  
  15.         method.<void>invoke(args[0]);  
  16.     }  
  17. }  


编译,以下述命令运行

Command prompt代码  收藏代码
  1. java -XX:+EnableMethodHandles TestMethodHandle2 test  


输出结果为:

引用
Hello, test!


基本上跟第一组例子一样,只是让hello()多了个参数而已。留意一下创建MethodType实例的代码如何对应的改变。
第15行对应的Java字节码是:

Java bytecode代码  收藏代码
  1. invokevirtual java/dyn/MethodHandle.invoke:(Ljava/lang/String;)V  


留意Java编译器是如何根据调用invoke时传入的参数的静态类型(编译时类型)来决定invoke的方法描述符。(Ljava/lang/String;)V的意思是返回值类型为void,参数列表有一个参数,类型为java.lang.String。
如果把代码稍微修改,使MethodHandle的方法类型与Java编译器推断的调用类型不相符的话:

Java代码  收藏代码
  1. import java.dyn.*;  
  2. import static java.dyn.MethodHandles.*;  
  3.   
  4. public class TestMethodHandle2 {  
  5.     private static void hello(Object name) {  
  6.         System.out.printf("Hello, %s!\n", name);  
  7.     }  
  8.       
  9.     public static void main(String[] args) {  
  10.         if (0 == args.length) args = new String[] { "Anonymous" };  
  11.           
  12.         MethodType type = MethodType.make(void.class, Object.class);  
  13.         MethodHandle method = lookup()  
  14.             .findStatic(TestMethodHandle2.class, "hello", type);  
  15.         method.<void>invoke(args[0]);  
  16.     }  
  17. }  


编译运行会看到:

引用
Exception in thread "main" java.dyn.WrongMethodTypeException: (Ljava/lang/Object;)V cannot be called as (Ljava/lang/String;)V
        at TestMethodHandle2.main(TestMethodHandle2.java:15)


这演示了Java编译器将invoke的方法类型推断为(Ljava/lang/String;)V,而被调用的MethodHandle实例实际的方法类型却是(Ljava/lang/Object;)V,JVM便认为这个调用不匹配并拒绝执行。关键点是:调用invoke时,参数表达式的静态类型(编译时类型)必须与MethodHandle的方法类型中对于位置的参数类型“准确一致”;虽然String类型的引用可以隐式转换为Object类型的,但不满足“准确一致”的要求。
要想让修改过的TestMethodHandle2再次正确运行,可以把第15行改为:method.<void>invoke((Object)args[0]);,也就是加个类型转换,使Java编译器推断出来的方法描述符为(Ljava/lang/Object;)V。或者也可以加一个适配器:

Java代码  收藏代码
  1. import java.dyn.*;  
  2. import static java.dyn.MethodHandles.*;  
  3.   
  4. public class TestMethodHandle2 {  
  5.     private static void hello(Object name) {  
  6.         System.out.printf("Hello, %s!\n", name);  
  7.     }  
  8.       
  9.     public static void main(String[] args) {  
  10.         if (0 == args.length) args = new String[] { "Anonymous" };  
  11.           
  12.         MethodType type = MethodType.make(void.class, Object.class);  
  13.         MethodType adaptedType = MethodType.make(void.class, String.class);  
  14.         MethodHandle method = lookup()  
  15.             .findStatic(TestMethodHandle2.class, "hello", type);  
  16.         MethodHandle adaptedMethod = MethodHandles.convertArguments(  
  17.             method, adaptedType);  
  18.         adaptedMethod.<void>invoke(args[0]);  
  19.     }  
  20. }  


这里演示了MethodHandle的可组装性:通过给实际调用目标装一个转换参数类型的适配器,方法调用就又可以成功了。

对比C#的例子:
C# 3.0:

C#代码  收藏代码
  1. using System;  
  2.   
  3. static class TestDelegate2 {  
  4.     static void Hello(string name) {  
  5.         Console.WriteLine("Hello, {0}!", name);  
  6.     }  
  7.       
  8.     static void Main(string[] args) {  
  9.         if (0 == args.Length) args = new [] { "Anonymous" };  
  10.         Action<string> method = Hello;  
  11.         method(args[0]);  
  12.     }  
  13. }  



基本上跟第一组例子也是一样的。Action<T>是标准库里预先声明好的一个泛型委托类型,其声明形如:

C#代码  收藏代码
  1. namespace System {  
  2.     public delegate void Action<T>(T t);  
  3. }  


为了对比,下面也把Hello()的参数类型改为object,

C#代码  收藏代码
  1. using System;  
  2.   
  3. static class TestDelegate2 {  
  4.     static void Hello(object name) {  
  5.         Console.WriteLine("Hello, {0}!", name);  
  6.     }  
  7.       
  8.     static void Main(string[] args) {  
  9.         if (0 == args.Length) args = new [] { "Anonymous" };  
  10.         Action<object> method = Hello;  
  11.         method(args[0]);  
  12.     }  
  13. }  


编译和运行都没有任何问题。这里要演示的是Invoke()方法是有确定的签名的,与委托类型声明的相吻合。编译器不会擅自推断Invoke()的签名。

第三组例子,可指定排序条件的快速排序:
前面一直在用hello world作例子或许是无聊了点,下面弄点稍微长一些的。

JDK 7:

Java代码  收藏代码
  1. import java.dyn.*;  
  2. import java.util.*;  
  3. import static java.dyn.MethodHandles.*;  
  4. import static java.lang.Integer.parseInt;  
  5.   
  6. public class TestMethodHandle3 {  
  7.     private static int compareStringsByIntegerValue(String num1, String num2) {  
  8.         return parseInt(num1) - parseInt(num2);  
  9.     }  
  10.       
  11.     private static int compareStringsByLength(String str1, String str2) {  
  12.         return str1.length() - str2.length();  
  13.     }  
  14.       
  15.     public static void sort(String[] array, MethodHandle comparer) {  
  16.         if (0 == array.length) return;  
  17.         sort(array, 0, array.length - 1, comparer);  
  18.     }  
  19.       
  20.     private static void sort(  
  21.         String[] array,  
  22.         int left,  
  23.         int right,  
  24.         MethodHandle comparer) {  
  25.           
  26.         if (left >= right) return;  
  27.           
  28.         String pivot = array[right];  
  29.         int lo = left - 1;  
  30.         int hi = right;  
  31.         while (true) {  
  32.             while (comparer.<int>invoke(array[++lo], pivot) < 0) {  
  33.             }  
  34.             while (hi > left  
  35.                 && comparer.<int>invoke(array[--hi], pivot) > 0) {  
  36.             }  
  37.             if (lo >= hi) break;  
  38.             swap(array, lo, hi);  
  39.         }  
  40.         swap(array, lo, right);  
  41.         sort(array, left, lo - 1, comparer);  
  42.         sort(array, lo + 1, right, comparer);  
  43.     }  
  44.       
  45.     private static <E> void swap(E[] array, int i, int j) {  
  46.         E temp = array[i];  
  47.         array[i] = array[j];  
  48.         array[j] = temp;  
  49.     }  
  50.       
  51.     public static void main(String[] args) {  
  52.         String[] array = new String[] {  
  53.             "25", "02", "250", "48", "0024", "42", "2"  
  54.         };  
  55.           
  56.         MethodType type = MethodType.make(  
  57.             int.class,  
  58.             String.class, String.class);  
  59.         MethodHandle comparer;  
  60.           
  61.         comparer = lookup().findStatic(  
  62.             TestMethodHandle3.class,  
  63.             "compareStringsByIntegerValue",  
  64.             type);  
  65.         sort(array, comparer);  
  66.         for (String s : array) System.out.println(s);  
  67.           
  68.         System.out.println();  
  69.           
  70.         comparer = lookup().findStatic(  
  71.             TestMethodHandle3.class,  
  72.             "compareStringsByLength",  
  73.             type);  
  74.         sort(array, comparer);  
  75.         for (String s : array) System.out.println(s);  
  76.     }  
  77. }  


编译,运行,输出结果为:

引用
02
2
0024
25
42
48
250

2
25
42
48
02
250
0024



核心的sort()方法是个简单的快排实现。为了让用户能够指定排序条件,我让它接收MethodHandle为参数来提供判断逻辑。在没有MethodHandle之前,我可能会选择使用策略模式来达到剥离出部分算法的目的。JDK里的许多API也是这么做的,例如Arrays.sort(T[] a, int fromIndex, int toIndex, Comparator<? super T> c),其中的Comparator参数就是“策略对象”。现在就可以直接用一个MethodHandle来代替它了。
例子的整体结构跟前两组没有显著的不同,应该还是比较好理解的。
值得注意的是,我把swap()写为了泛型方法,方便以后再更多地方能复用。排序也应该是个通用算法,为何不把sort()也写为泛型呢?
如果把例子中sort()方法里出现的String全部直接替换为泛型的,变成:

Java代码  收藏代码
  1. public static <E> void sort(E[] array, MethodHandle comparer) {  
  2.     if (0 == array.length) return;  
  3.     sort(array, 0, array.length - 1, comparer);  
  4. }  
  5.   
  6. private static <E> void sort(  
  7.     E[] array,  
  8.     int left,  
  9.     int right,  
  10.     MethodHandle comparer) {  
  11.       
  12.     if (left >= right) return;  
  13.       
  14.     E pivot = array[right];  
  15.     int lo = left - 1;  
  16.     int hi = right;  
  17.     while (true) {  
  18.         while (comparer.<int>invoke(array[++lo], pivot) < 0) {  
  19.         }  
  20.         while (hi > left  
  21.             && comparer.<int>invoke(array[--hi], pivot) > 0) {  
  22.         }  
  23.         if (lo >= hi) break;  
  24.         swap(array, lo, hi);  
  25.     }  
  26.     swap(array, lo, right);  
  27.     sort(array, left, lo - 1, comparer);  
  28.     sort(array, lo + 1, right, comparer);  
  29. }  


编译没问题,但运行的时候会看到comparer.<int>invoke(...)的调用抛WrongMethodTypeException异常。你可能会纳闷:array数组的元素类型不是E么,是泛型的啊;以String为泛型参数调用sort()的时候,invoke的实际参数类型与comparer的方法类型应该匹配才对,怎么会出错呢?
问题就在于Java的泛型是通过类型擦除法(type-erasure)来实现的,编译过后所有泛型参数都变为了Object,这里也不例外。所以Java编译器推断出来的invoke()的描述符是(Ljava/lang/Object;Ljava/lang/Object;)I,这就出现我们在第二组例子里遇到的问题了——方法类型不准确匹配。
于是解决的办法也很简单,只要改成这样:

Java代码  收藏代码
  1. public static <E> void sort(E[] array, MethodHandle comparer) {  
  2.     if (0 == array.length) return;  
  3.     Class<Object> objClz = Object.class;  
  4.     MethodType adaptedType = MethodType.make(int.class, objClz, objClz);  
  5.     MethodHandle adaptedComparer = MethodHandles.convertArguments(  
  6.         comparer, adaptedType);  
  7.     sort(array, 0, array.length - 1, adaptedComparer);  
  8. }  


在调用核心的sort()方法前,先加一个适配器来解决类型差异的问题,就万事大吉了 ^ ^

对比C#的例子:
C# 2.0

C#代码  收藏代码
  1. using System;  
  2. using System.Collections.Generic;  
  3.   
  4. static class TestDelegate3 {  
  5.     static int CompareStringsByIntegerValue(string num1, string num2) {  
  6.         return Convert.ToInt32(num1) - Convert.ToInt32(num2);  
  7.     }  
  8.       
  9.     static int CompareStringsByLength(string str1, string str2) {  
  10.         return str1.Length - str2.Length;  
  11.     }  
  12.       
  13.     public static void Sort<T>(T[] array, Comparison<T> comparer) {  
  14.         Sort(array, 0, array.Length - 1, comparer);  
  15.     }  
  16.       
  17.     static void Sort<T>(  
  18.         T[] array,  
  19.         int left,  
  20.         int right,  
  21.         Comparison<T> comparer) {  
  22.           
  23.         if (left >= right) return;  
  24.           
  25.         T pivot = array[right];  
  26.         int lo = left - 1;  
  27.         int hi = right;  
  28.         while (true) {  
  29.             while (comparer(array[++lo], pivot) < 0) {  
  30.             }  
  31.             while (hi > left  
  32.                 && comparer(array[--hi], pivot) > 0) {  
  33.             }  
  34.             if (lo >= hi) break;  
  35.             Swap(ref array[lo], ref array[hi]);  
  36.         }  
  37.         Swap(ref array[lo], ref array[right]);  
  38.         Sort(array, left, lo - 1, comparer);  
  39.         Sort(array, lo + 1, right, comparer);  
  40.     }  
  41.       
  42.     static void Swap<T>(ref T t1, ref T t2) {  
  43.         T temp = t1;  
  44.         t1 = t2;  
  45.         t2 = temp;  
  46.     }  
  47.       
  48.     static void Main(string[] args) {  
  49.         String[] array = new String[] { "25", "02", "250", "48", "0024", "42", "2" };  
  50.           
  51.         Sort(array, CompareStringsByIntegerValue);  
  52.         foreach (var s in array) Console.WriteLine(s);  
  53.           
  54.         Console.WriteLine();  
  55.           
  56.         Sort(array, CompareStringsByLength);  
  57.         foreach (var s in array) Console.WriteLine(s);  
  58.     }  
  59. }  



基本上也没什么需要特别解释的了。System.Comparison<T>是标准库里预先声明的一个泛型委托类型,其声明形如:

C#代码  收藏代码
  1. namespace System {  
  2.     public delegate int Comparison<T>(T x, T y);  
  3. }  


注意.NET里委托与泛型可以很好的结合在一起使用,而不必考虑转换参数类型的问题,因为.NET的泛型信息会带到运行时。或者说,“.NET generics are reified”。

如果用上C# 3.0和.NET Framework 3.5的新特性,使用lambda表达式来提供排序依据,

C#代码  收藏代码
  1. using System;  
  2. using System.Collections.Generic;  
  3.   
  4. static class TestDelegate3 {  
  5.     public static void Sort<T>(T[] array, Func<T, T, int> comparer) {  
  6.         Sort(array, 0, array.Length - 1, comparer);  
  7.     }  
  8.       
  9.     static void Sort<T>(  
  10.         T[] array,  
  11.         int left,  
  12.         int right,  
  13.         Func<T, T, int> comparer) {  
  14.           
  15.         if (left >= right) return;  
  16.           
  17.         T pivot = array[right];  
  18.         int lo = left - 1;  
  19.         int hi = right;  
  20.         while (true) {  
  21.             while (comparer(array[++lo], pivot) < 0) {  
  22.             }  
  23.             while (hi > left  
  24.                 && comparer(array[--hi], pivot) > 0) {  
  25.             }  
  26.             if (lo >= hi) break;  
  27.             Swap(ref array[lo], ref array[hi]);  
  28.         }  
  29.         Swap(ref array[lo], ref array[right]);  
  30.         Sort(array, left, lo - 1, comparer);  
  31.         Sort(array, lo + 1, right, comparer);  
  32.     }  
  33.       
  34.     static void Swap<T>(ref T t1, ref T t2) {  
  35.         T temp = t1;  
  36.         t1 = t2;  
  37.         t2 = temp;  
  38.     }  
  39.       
  40.     static void Main(string[] args) {  
  41.         var array = new [] { "25", "02", "250", "48", "0024", "42", "2" };  
  42.           
  43.         Sort(array,  
  44.             (s1, s2) => Convert.ToInt32(s1) - Convert.ToInt32(s2));  
  45.         foreach (var s in array) Console.WriteLine(s);  
  46.           
  47.         Console.WriteLine();  
  48.           
  49.         Sort(array,  
  50.             (s1, s2) => s1.Length - s2.Length);  
  51.         foreach (var s in array) Console.WriteLine(s);  
  52.     }  
  53. }  


我们就不必再为例中用到的排序条件写具名方法了。实际上C#编译器仍然为这两个lambda表达式生成了私有静态方法,我们得到的东西是一样的(硬要说的话,少得到了俩名字),但需要写在代码里的东西减少了。
如果进一步改为使用标准库中现成的方法来排序,

C#代码  收藏代码
  1. using System;  
  2. using System.Collections.Generic;  
  3.   
  4. static class TestDelegate3 {      
  5.     static void Main(string[] args) {  
  6.         var array = new [] { "25", "02", "250", "48", "0024", "42", "2" };  
  7.           
  8.         Array.Sort(array,  
  9.             (s1, s2) => Convert.ToInt32(s1) - Convert.ToInt32(s2));  
  10.         foreach (var s in array) Console.WriteLine(s);  
  11.           
  12.         Console.WriteLine();  
  13.           
  14.         Array.Sort(array,  
  15.             (s1, s2) => s1.Length - s2.Length);  
  16.         foreach (var s in array) Console.WriteLine(s);  
  17.     }  
  18. }  


整个代码就简洁了许多。


Alright,这帖就介绍MethodHandle与.NET的委托到这里。希望上面的例子达到了简单介绍JDK 7的MethodHandle的目的。
你可能会心存疑惑,“我为什么需要引用别的方法呢?” 由于Java以前一直没有提供这样的功能,一直只使用Java的人可能较少思考这个问题。但你可能会碰到过这样的问题,有时候要调用的目标只有到了运行的时候才知道,无法在代码里直接写方法调用,那怎么办?以前的解决办法就只有通过反射了。一个最简单的使用场景就是,原本通过反射做的方法调用,现在都可以通过MethodHandle完成。如果一个方法频繁被反射调用,开销会很明显,而且难以优化;通过MethodHandle调用则跟正常的接口方法调用速度接近,没有各种包装/装箱的开销,而且可以被JVM优化,何乐而不为?
在MethodHandle出现之前,许多JVM上的脚本语言实现,如JRuby,为了提高调用方法的速度,选择生成大量很小的“invoker类”来对方法签名做特化,通过它们去调用目标方法,避免反射调用的开销。但这么做有许多问题,一是实现麻烦,而是对PermGen堆带来巨大的压力。MethodHandle的出现极大的改善了状况,既便于使用,效率又高,而且还不会对PermGen带来多少压力。Charles Nutter在一帖中描述了这个问题
编辑:经指正,Groovy当前还没有用invoke stub,只是做了callsite caching优化。我只读过Groovy的编译器的代码,没读过运行时部分的代码,忽悠了同学们了,抱歉 <(_ _)>)

进一步介绍等以后写invokedynamic的时候再写了~ until then

Have fun ^ ^

 

[转]Java 的强引用、弱引用、软引用、虚引用

1、强引用(StrongReference)

        强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。如下:

Object o=new Object();   //  强引用

       当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。如果不使用时,要通过如下方式来弱化引用,如下:

o=null;     // 帮助垃圾收集器回收此对象

       显式地设置o为null,或超出对象的生命周期范围,则gc认为该对象不存在引用,这时就可以回收这个对象。具体什么时候收集这要取决于gc的算法。

举例:

public void test(){
    Object o=new Object();
    // 省略其他操作
}

       在一个方法的内部有一个强引用,这个引用保存在栈中,而真正的引用内容(Object)保存在堆中。当这个方法运行完成后就会退出方法栈,则引用内容的引用不存在,这个Object会被回收。

       但是如果这个o是全局的变量时,就需要在不用这个对象时赋值为null,因为强引用不会被垃圾回收。

       强引用在实际中有非常重要的用处,举个ArrayList的实现源代码:

复制代码
private transient Object[] elementData;
public void clear() {
        modCount++;
        // Let gc do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;
        size = 0;
}
复制代码

       在ArrayList类中定义了一个私有的变量elementData数组,在调用方法清空数组时可以看到为每个数组内容赋值为null。不同于elementData=null,强引用仍然存在,避免在后续调用 add()等方法添加元素时进行重新的内存分配。使用如clear()方法中释放内存的方法对数组中存放的引用类型特别适用,这样就可以及时释放内存。 

2软引用(SoftReference)

      如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。      

String str=new String("abc");                                     // 强引用
SoftReference<String> softRef=new SoftReference<String>(str);     // 软引用

当内存不足时,等价于:    

If(JVM.内存不足()) {
   str = null;  // 转换为软引用
   System.gc(); // 垃圾回收器进行回收
}

  软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。

(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建

(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出

这时候就可以使用软引用

复制代码
Browser prev = new Browser();               // 获取页面进行浏览
SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用        
if(sr.get()!=null){ 
    rev = (Browser) sr.get();           // 还没有被回收器回收,直接获取
}else{
    prev = new Browser();               // 由于内存吃紧,所以对软引用的对象回收了
    sr = new SoftReference(prev);       // 重新构建
}
复制代码

      这样就很好的解决了实际的问题。

     软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

3、弱引用(WeakReference)

      弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 

String str=new String("abc");    
WeakReference<String> abcWeakRef = new WeakReference<String>(str);
str=null;

    当垃圾回收器进行扫描回收时等价于:    

str = null;
System.gc();

    如果这个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象。   

   下面的代码会让str再次变为一个强引用:    

String  abc = abcWeakRef.get();

    弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

当你想引用一个对象,但是这个对象有自己的生命周期,你不想介入这个对象的生命周期,这时候你就是用弱引用。

   这个引用不会在对象的垃圾回收判断中产生任何附加的影响

复制代码
public class ReferenceTest {

    private static ReferenceQueue<VeryBig> rq = new ReferenceQueue<VeryBig>();

    public static void checkQueue() {
        Reference<? extends VeryBig> ref = null;
        while ((ref = rq.poll()) != null) {
            if (ref != null) {
                System.out.println("In queue: "    + ((VeryBigWeakReference) (ref)).id);
            }
        }
    }

    public static void main(String args[]) {
        int size = 3;
        LinkedList<WeakReference<VeryBig>> weakList = new LinkedList<WeakReference<VeryBig>>();
        for (int i = 0; i < size; i++) {
            weakList.add(new VeryBigWeakReference(new VeryBig("Weak " + i), rq));
            System.out.println("Just created weak: " + weakList.getLast());

        }

        System.gc(); 
        try { // 下面休息几分钟,让上面的垃圾回收线程运行完成
            Thread.currentThread().sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        checkQueue();
    }
}

class VeryBig {
    public String id;
    // 占用空间,让线程进行回收
    byte[] b = new byte[2 * 1024];

    public VeryBig(String id) {
        this.id = id;
    }

    protected void finalize() {
        System.out.println("Finalizing VeryBig " + id);
    }
}

class VeryBigWeakReference extends WeakReference<VeryBig> {
    public String id;

    public VeryBigWeakReference(VeryBig big, ReferenceQueue<VeryBig> rq) {
        super(big, rq);
        this.id = big.id;
    }

    protected void finalize() {
        System.out.println("Finalizing VeryBigWeakReference " + id);
    }
}
复制代码

最后的输出结果为:

复制代码
Just created weak: com.javabase.reference.VeryBigWeakReference@1641c0
Just created weak: com.javabase.reference.VeryBigWeakReference@136ab79
Just created weak: com.javabase.reference.VeryBigWeakReference@33c1aa
Finalizing VeryBig Weak 2
Finalizing VeryBig Weak 1
Finalizing VeryBig Weak 0
In queue: Weak 1
In queue: Weak 2
In queue: Weak 0
复制代码

4、虚引用(PhantomReference)

    “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

    虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

5、总结

    Java4种引用的级别由高到低依次为:

    强引用  >  软引用  >  弱引用  >  虚引用

    通过图来看一下他们之间在垃圾回收时的区别:

     

       当垃圾回收器回收时,某些对象会被回收,某些不会被回收。垃圾回收器会从根对象Object来标记存活的对象,然后将某些不可达的对象和一些引用的对象进行回收,如果对这方面不是很了解,可以参考如下的文章:

      通过表格来说明一下,如下:

      

     参考文献:

    1、http://www.cnblogs.com/skywang12345/p/3154474.html

    2、http://blog.csdn.net/lifetragedy?viewmode=contents

大对象简介+大对象的4种类型+lob类型的优点+lob的组成

数据库

大对象简介
1用来存储大型数据,如图片,视频,音乐等
2可用于存储二进制数据,字符数据,引用外部文件的指针的数据类型

大对象的4种类型
1BLOB数据类型
1)它是用来存储二进制数据。
2)可以存储的最大数据量是(4GB-1)*db_block_size(最大32kb),也就是128TB.

2CLOB数据类型
1)存储字符数据
2)可以存储的最大数据量是(4GB-1)*db_block_size(最大32kb),也就是128TB.

3NCLOB数据类型
1)用来存储多字节字符的数据,一般用于非英文的字符
2)可以存储的最大数据量是(4GB-1)*db_block_size(最大32kb),也就是128TB.
4是BFILE数据类型
1)存储文件指针。
2)数据文件可以存储在数据库之外,数据库只存储对该文件的引用。
3)其最多也可以存储4GB的数据。

lob类型的优点
1lob列最大可以存储128TB的数据,远远超过long和long row列存储的数据量(2GB)
2一个表可以多个lob列,但是只能有一个long或long raw列
3lob数据可以随机访问,long和long raw 数据只能顺序访问。
lob的组成
1lob定位器:一个子针,指定lob数据的位置
2lob数据:存储在lob中的实际数据
注意
1当blob,clob,nclob数据列的数据小于4KB,则lob数据存储在表中,否则lob数据存储在表外。
2bfile数据列,只有lob定位器存储在表中

如何dump出一个Java进程里的类对应的Class文件?

如何dump出一个Java进程里的类对应的Class文件?

大家可能对JVM很好奇,想看看运行中某时刻上JVM里各种内部数据结构的状态。可能有人想看堆上所有对象都有哪些,分别位于哪个分代之类;可能有人想看当前所有线程的stack trace;可能有人想看一个方法是否被JIT编译过,被编译后的native代码是怎样的。对Sun HotSpot JVM而言,这些需求都有现成的API可以满足——通过Serviceability Agent(下面简称SA)。大家熟悉的jstack、jmap等工具在使用-F参数启动时其实就是通过SA来实现功能的。

这里介绍的是按需把class给dump出来的方法。
为什么我们要dump运行中的JVM里的class呢?直接从classpath上把Class文件找到不就好了么?这样的话只要用ClassLoader.getResourceAsStream(name)就能行了。例如说要找foo.bar.Baz的Class文件,类似这样就行:

Java代码  收藏代码
  1. ClassLoader loader = Thread.currentThread().getContextClassLoader();  
  2. InputStream in = loader.getResourceAsStream("foo/bar/Baz.class");  
  3. // 从in把内容拿出来,然后随便怎么处理  


用Groovy的交互式解释器shell来演示一下:

Groovysh代码  收藏代码
  1. D:\>\sdk\groovy-1.7.2\bin\groovysh  
  2. Groovy Shell (1.7.2, JVM: 1.6.0_20)  
  3. Type 'help' or '\h' for help.  
  4. -----------------------------------------------------------------------------  
  5. groovy:000> loader = Thread.currentThread().contextClassLoader  
  6. ===> org.codehaus.groovy.tools.RootLoader@61de33  
  7. groovy:000> stream = loader.getResourceAsStream('java/util/ArrayList.class')  
  8. ===> sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream@5dfaf1  
  9. groovy:000> file = new File('ArrayList.class')  
  10. ===> ArrayList.class  
  11. groovy:000> file.createNewFile()  
  12. ===> true  
  13. groovy:000> file << stream  
  14. ===> ArrayList.class  
  15. groovy:000> quit  


这样就在当前目录建了个ArrayList.class文件,把java.util.ArrayList对应的Class文件拿到手了。

问题是,上述方式其实只是借助ClassLoader把它在classpath上能找到的Class文件复制了一份而已。如果我们想dump的类在加载时被修改过(例如说某些AOP的实现会这么做),或者在运行过程中被修改过(通过HotSwap),或者干脆就是运行时才创建出来的,那就没有现成的Class文件了。

需要注意,java.lang.Class<T>这个类虽然实现了java.io.Serializable接口,但直接将一个Class对象序列化是得不到对应的Class文件的。参考src/share/classes/java/lang/Class.java里的注释:

Java代码  收藏代码
  1. package java.lang;  
  2.   
  3. import java.io.ObjectStreamField;  
  4. // ...  
  5.   
  6. public final  
  7.     class Class<T> implements java.io.Serializable,   
  8.                   java.lang.reflect.GenericDeclaration,   
  9.                   java.lang.reflect.Type,  
  10.                               java.lang.reflect.AnnotatedElement {  
  11.     /** 
  12.      * Class Class is special cased within the Serialization Stream Protocol.  
  13.      * 
  14.      * A Class instance is written initially into an ObjectOutputStream in the  
  15.      * following format: 
  16.      * <pre> 
  17.      *      <code>TC_CLASS</code> ClassDescriptor 
  18.      *      A ClassDescriptor is a special cased serialization of  
  19.      *      a <code>java.io.ObjectStreamClass</code> instance.  
  20.      * </pre> 
  21.      * A new handle is generated for the initial time the class descriptor 
  22.      * is written into the stream. Future references to the class descriptor 
  23.      * are written as references to the initial class descriptor instance. 
  24.      * 
  25.      * @see java.io.ObjectStreamClass 
  26.      */  
  27.     private static final ObjectStreamField[] serialPersistentFields =   
  28.         new ObjectStreamField[0];  
  29.       
  30.     // ...  
  31. }  



=================================================================

HotSpot有一套私有API提供了对JVM内部数据结构的审视功能,称为Serviceability Agent。它是一套Java API,虽然HotSpot是用C++写的,但SA提供了HotSpot中重要数据结构的Java镜像类,所以可以直接写Java代码来查看一个跑在HotSpot上的Java进程的内部状态。它也提供了一些封装好的工具,可以直接在命令行上跑,包括下面提到的ClassDump工具。
SA的一个重要特征是它是“进程外审视工具”。也就是说,SA并不运行在要审视的目标进程中,而是运行在一个独立的Java进程中,通过操作系统上提供的调试API来连接到目标进程上。这样,SA的运行不会受到目标进程状态的影响,因而可以用于审视一个已经挂起的Java进程,或者是core dump文件。当然,这也就意味这一个SA进程不能用于审视自己。
一个被调试器连接上的进程会被暂停下来。所以在SA连接到目标进程时,目标进程也是一直处于暂停状态的,直到SA解除连接。如果需要在线上使用SA的话需要小心,不要通过SA做过于耗时的分析,宁可先把数据都抓出来,把SA的连接解除掉之后再离线分析。目前的使用经验是,连接上一个小Java进程的话很快就好了,但连接上一个“大”的Java进程(堆比较大、加载的类比较多)可能会在连接阶段卡住好几分钟,线上需要慎用。

目前(JDK6)在Windows上SA没有随HotSpot一起发布,所以无法在Windows上使用;在Linux、Solaris、Mac上使用都没问题。从JDK7 build 64开始Windows版JDK也带上SA,如果有兴趣尝鲜JDK7的话可以试试(http://dlc.sun.com.edgesuite.net/jdk7/binaries/index.html),当前版本是build 103;正式的JDK7今年10月份应该有指望吧。
在Windows版JDK里带上SA的相关bug是:
Bug 6743339: Enable building sa-jdi.jar and sawindbg.dll on Windows with hotspot build
Bug 6755621: Include SA binaries into Windows JDK

前面废话了那么多,接下来回到正题,介绍一下ClassDump工具。
SA自带了一个能把当前在HotSpot中加载了的类dump成Class文件的工具,称为ClassDump。它的全限定类名是sun.jvm.hotspot.tools.jcore.ClassDump,有main()方法,可以直接从命令行执行;接收一个命令行参数,是目标Java进程的进程ID,可以通过JDK自带的jps工具查找Java进程的ID。要执行该工具需要确保SA的JAR包在classpath上,位于$JAVA_HOME/lib/sa-jdi.jar。
默认条件下执行ClassDump会把当前加载的所有Java类都dump到当前目录下,如果有全限定名相同但内容不同的类同时存在于一个Java进程中,那么dump的时候会有覆盖现象,实际dump出来的是同名的类的最后一个(根据ClassDump工具的遍历顺序)。
如果需要指定被dump的类的范围,可以自己写一个过滤器,在启动ClassDump工具时指定-Dsun.jvm.hotspot.tools.jcore.filter=filterClassName,具体方法见下面例子;如果需要指定dump出来的Class文件的存放路径,可以用-Dsun.jvm.hotspot.tools.jcore.outputDir=path来指定,path替换为实际路径。

以下演示在Linux上进行。大家或许已经知道,Sun JDK对反射调用方法有一些特别的优化,会在运行时生成专门的“调用类”来提高反射调用的性能。这次演示就来看看生成的类是什么样子的。

首先编写一个自定义的过滤器。只要实现sun.jvm.hotspot.tools.jcore.ClassFilter接口即可。

Java代码  收藏代码
  1. import sun.jvm.hotspot.tools.jcore.ClassFilter;  
  2. import sun.jvm.hotspot.oops.InstanceKlass;  
  3.   
  4. public class MyFilter implements ClassFilter {  
  5.     @Override  
  6.     public boolean canInclude(InstanceKlass kls) {  
  7.         String klassName = kls.getName().asString();  
  8.         return klassName.startsWith("sun/reflect/GeneratedMethodAccessor");  
  9.     }  
  10. }  


InstanceKlass对应于HotSpot中表示Java类的内部对象。Sun JDK为反射调用生成的类的名字形如sun/reflect/GeneratedMethodAccessorN,其中N是一个整数;所以只要看看类名是否以"sun/reflect/GeneratedMethodAccessor"开头就能找出来了。留意到这里包名的分隔符是“/”而不是“.”,这是Java类在JVM中的“内部名称”形式,参考Java虚拟机规范第二版4.2小节

接下来写一个会引发JDK生成反射调用类的演示程序:

Java代码  收藏代码
  1. import java.lang.reflect.Method;  
  2.   
  3. public class Demo {  
  4.     public static void main(String[] args) throws Exception {  
  5.         Method p = System.out.getClass().getMethod("println", String.class);  
  6.         for (int i = 0; i < 16; i++) {  
  7.             p.invoke(System.out, "demo");  
  8.         }  
  9.         System.in.read(); // block the program  
  10.     }  
  11. }  



让Demo跑起来,然后先不要让它结束。通过jps工具看看它的进程ID是多少:

Command prompt代码  收藏代码
  1. [sajia@sajia class_dump]$ jps  
  2. 20542 Demo  
  3. 20554 Jps  



接下来执行ClassDump,指定上面自定义的过滤器(过滤器的类要在classpath上,本例中它在./bin):

Command prompt代码  收藏代码
  1. [sajia@sajia class_dump]$ java -classpath ".:./bin:$JAVA_HOME/lib/sa-jdi.jar" -Dsun.jvm.hotspot.tools.jcore.filter=MyFilter sun.jvm.hotspot.tools.jcore.ClassDump 20542  


执行结束后,可以看到dump出了一个Class文件,在./sun/reflect/GeneratedMethodAccessor1.class;.是默认的输出目录,后面的目录结构对应包名。

用javap看看这个Class文件有啥内容:

Javap代码  收藏代码
  1. [sajia@sajia class_dump]$ javap -verbose sun.reflect.GeneratedMethodAccessor1  
  2. public class sun.reflect.GeneratedMethodAccessor1 extends sun.reflect.MethodAccessorImpl  
  3.   minor version: 0  
  4.   major version: 46  
  5.   Constant pool:  
  6. const #1 = Asciz        sun/reflect/GeneratedMethodAccessor1;  
  7. const #2 = class        #1;     //  sun/reflect/GeneratedMethodAccessor1  
  8. const #3 = Asciz        sun/reflect/MethodAccessorImpl;  
  9. const #4 = class        #3;     //  sun/reflect/MethodAccessorImpl  
  10. const #5 = Asciz        java/io/PrintStream;  
  11. const #6 = class        #5;     //  java/io/PrintStream  
  12. const #7 = Asciz        println;  
  13. const #8 = Asciz        (Ljava/lang/String;)V;  
  14. const #9 = NameAndType  #7:#8;//  println:(Ljava/lang/String;)V  
  15. const #10 = Method      #6.#9;  //  java/io/PrintStream.println:(Ljava/lang/String;)V  
  16. const #11 = Asciz       invoke;  
  17. const #12 = Asciz       (Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;;  
  18. const #13 = Asciz       java/lang/String;  
  19. const #14 = class       #13;    //  java/lang/String  
  20. const #15 = Asciz       java/lang/Throwable;  
  21. const #16 = class       #15;    //  java/lang/Throwable  
  22. const #17 = Asciz       java/lang/ClassCastException;  
  23. const #18 = class       #17;    //  java/lang/ClassCastException  
  24. const #19 = Asciz       java/lang/NullPointerException;  
  25. const #20 = class       #19;    //  java/lang/NullPointerException  
  26. const #21 = Asciz       java/lang/IllegalArgumentException;  
  27. const #22 = class       #21;    //  java/lang/IllegalArgumentException  
  28. const #23 = Asciz       java/lang/reflect/InvocationTargetException;  
  29. const #24 = class       #23;    //  java/lang/reflect/InvocationTargetException  
  30. const #25 = Asciz       <init>;  
  31. const #26 = Asciz       ()V;  
  32. const #27 = NameAndType #25:#26;//  "<init>":()V  
  33. const #28 = Method      #20.#27;        //  java/lang/NullPointerException."<init>":()V  
  34. const #29 = Method      #22.#27;        //  java/lang/IllegalArgumentException."<init>":()V  
  35. const #30 = Asciz       (Ljava/lang/String;)V;  
  36. const #31 = NameAndType #25:#30;//  "<init>":(Ljava/lang/String;)V  
  37. const #32 = Method      #22.#31;        //  java/lang/IllegalArgumentException."<init>":(Ljava/lang/String;)V  
  38. const #33 = Asciz       (Ljava/lang/Throwable;)V;  
  39. const #34 = NameAndType #25:#33;//  "<init>":(Ljava/lang/Throwable;)V  
  40. const #35 = Method      #24.#34;        //  java/lang/reflect/InvocationTargetException."<init>":(Ljava/lang/Throwable;)V  
  41. const #36 = Method      #4.#27; //  sun/reflect/MethodAccessorImpl."<init>":()V  
  42. const #37 = Asciz       java/lang/Object;  
  43. const #38 = class       #37;    //  java/lang/Object  
  44. const #39 = Asciz       toString;  
  45. const #40 = Asciz       ()Ljava/lang/String;;  
  46. const #41 = NameAndType #39:#40;//  toString:()Ljava/lang/String;  
  47. const #42 = Method      #38.#41;        //  java/lang/Object.toString:()Ljava/lang/String;  
  48. const #43 = Asciz       Code;  
  49. const #44 = Asciz       Exceptions;  
  50. const #45 = Asciz       java/lang/Boolean;  
  51. const #46 = class       #45;    //  java/lang/Boolean  
  52. const #47 = Asciz       (Z)V;  
  53. const #48 = NameAndType #25:#47;//  "<init>":(Z)V  
  54. const #49 = Method      #46.#48;        //  java/lang/Boolean."<init>":(Z)V  
  55. const #50 = Asciz       booleanValue;  
  56. const #51 = Asciz       ()Z;  
  57. const #52 = NameAndType #50:#51;//  booleanValue:()Z  
  58. const #53 = Method      #46.#52;        //  java/lang/Boolean.booleanValue:()Z  
  59. const #54 = Asciz       java/lang/Byte;  
  60. const #55 = class       #54;    //  java/lang/Byte  
  61. const #56 = Asciz       (B)V;  
  62. const #57 = NameAndType #25:#56;//  "<init>":(B)V  
  63. const #58 = Method      #55.#57;        //  java/lang/Byte."<init>":(B)V  
  64. const #59 = Asciz       byteValue;  
  65. const #60 = Asciz       ()B;  
  66. const #61 = NameAndType #59:#60;//  byteValue:()B  
  67. const #62 = Method      #55.#61;        //  java/lang/Byte.byteValue:()B  
  68. const #63 = Asciz       java/lang/Character;  
  69. const #64 = class       #63;    //  java/lang/Character  
  70. const #65 = Asciz       (C)V;  
  71. const #66 = NameAndType #25:#65;//  "<init>":(C)V  
  72. const #67 = Method      #64.#66;        //  java/lang/Character."<init>":(C)V  
  73. const #68 = Asciz       charValue;  
  74. const #69 = Asciz       ()C;  
  75. const #70 = NameAndType #68:#69;//  charValue:()C  
  76. const #71 = Method      #64.#70;        //  java/lang/Character.charValue:()C  
  77. const #72 = Asciz       java/lang/Double;  
  78. const #73 = class       #72;    //  java/lang/Double  
  79. const #74 = Asciz       (D)V;  
  80. const #75 = NameAndType #25:#74;//  "<init>":(D)V  
  81. const #76 = Method      #73.#75;        //  java/lang/Double."<init>":(D)V  
  82. const #77 = Asciz       doubleValue;  
  83. const #78 = Asciz       ()D;  
  84. const #79 = NameAndType #77:#78;//  doubleValue:()D  
  85. const #80 = Method      #73.#79;        //  java/lang/Double.doubleValue:()D  
  86. const #81 = Asciz       java/lang/Float;  
  87. const #82 = class       #81;    //  java/lang/Float  
  88. const #83 = Asciz       (F)V;  
  89. const #84 = NameAndType #25:#83;//  "<init>":(F)V  
  90. const #85 = Method      #82.#84;        //  java/lang/Float."<init>":(F)V  
  91. const #86 = Asciz       floatValue;  
  92. const #87 = Asciz       ()F;  
  93. const #88 = NameAndType #86:#87;//  floatValue:()F  
  94. const #89 = Method      #82.#88;        //  java/lang/Float.floatValue:()F  
  95. const #90 = Asciz       java/lang/Integer;  
  96. const #91 = class       #90;    //  java/lang/Integer  
  97. const #92 = Asciz       (I)V;  
  98. const #93 = NameAndType #25:#92;//  "<init>":(I)V  
  99. const #94 = Method      #91.#93;        //  java/lang/Integer."<init>":(I)V  
  100. const #95 = Asciz       intValue;  
  101. const #96 = Asciz       ()I;  
  102. const #97 = NameAndType #95:#96;//  intValue:()I  
  103. const #98 = Method      #91.#97;        //  java/lang/Integer.intValue:()I  
  104. const #99 = Asciz       java/lang/Long;  
  105. const #100 = class      #99;    //  java/lang/Long  
  106. const #101 = Asciz      (J)V;  
  107. const #102 = NameAndType        #25:#101;//  "<init>":(J)V  
  108. const #103 = Method     #100.#102;      //  java/lang/Long."<init>":(J)V  
  109. const #104 = Asciz      longValue;  
  110. const #105 = Asciz      ()J;  
  111. const #106 = NameAndType        #104:#105;//  longValue:()J  
  112. const #107 = Method     #100.#106;      //  java/lang/Long.longValue:()J  
  113. const #108 = Asciz      java/lang/Short;  
  114. const #109 = class      #108;   //  java/lang/Short  
  115. const #110 = Asciz      (S)V;  
  116. const #111 = NameAndType        #25:#110;//  "<init>":(S)V  
  117. const #112 = Method     #109.#111;      //  java/lang/Short."<init>":(S)V  
  118. const #113 = Asciz      shortValue;  
  119. const #114 = Asciz      ()S;  
  120. const #115 = NameAndType        #113:#114;//  shortValue:()S  
  121. const #116 = Method     #109.#115;      //  java/lang/Short.shortValue:()S  
  122.   
  123. {  
  124. public sun.reflect.GeneratedMethodAccessor1();  
  125.   Code:  
  126.    Stack=1, Locals=1, Args_size=1  
  127.    0:   aload_0  
  128.    1:   invokespecial   #36; //Method sun/reflect/MethodAccessorImpl."<init>":()V  
  129.    4:   return  
  130.   
  131. public java.lang.Object invoke(java.lang.Object, java.lang.Object[])   throws java.lang.reflect.InvocationTargetException;  
  132.   Exceptions:   
  133.    throws java.lang.reflect.InvocationTargetException  Code:  
  134.    Stack=5, Locals=3, Args_size=3  
  135.    0:   aload_1  
  136.    1:   ifnonnull       12  
  137.    4:   new     #20; //class java/lang/NullPointerException  
  138.    7:   dup  
  139.    8:   invokespecial   #28; //Method java/lang/NullPointerException."<init>":()V  
  140.    11:  athrow  
  141.    12:  aload_1  
  142.    13:  checkcast       #6; //class java/io/PrintStream  
  143.    16:  aload_2  
  144.    17:  arraylength  
  145.    18:  sipush  1  
  146.    21:  if_icmpeq       32  
  147.    24:  new     #22; //class java/lang/IllegalArgumentException  
  148.    27:  dup  
  149.    28:  invokespecial   #29; //Method java/lang/IllegalArgumentException."<init>":()V  
  150.    31:  athrow  
  151.    32:  aload_2  
  152.    33:  sipush  0  
  153.    36:  aaload  
  154.    37:  checkcast       #14; //class java/lang/String  
  155.    40:  invokevirtual   #10; //Method java/io/PrintStream.println:(Ljava/lang/String;)V  
  156.    43:  aconst_null  
  157.    44:  areturn  
  158.    45:  invokespecial   #42; //Method java/lang/Object.toString:()Ljava/lang/String;  
  159.    48:  new     #22; //class java/lang/IllegalArgumentException  
  160.    51:  dup_x1  
  161.    52:  swap  
  162.    53:  invokespecial   #32; //Method java/lang/IllegalArgumentException."<init>":(Ljava/lang/String;)V  
  163.    56:  athrow  
  164.    57:  new     #24; //class java/lang/reflect/InvocationTargetException  
  165.    60:  dup_x1  
  166.    61:  swap  
  167.    62:  invokespecial   #35; //Method java/lang/reflect/InvocationTargetException."<init>":(Ljava/lang/Throwable;)V  
  168.    65:  athrow  
  169.   Exception table:  
  170.    from   to  target type  
  171.     12    40    45   Class java/lang/ClassCastException  
  172.   
  173.     12    40    45   Class java/lang/NullPointerException  
  174.   
  175.     40    43    57   Class java/lang/Throwable  
  176.   
  177.   
  178. }  



用Java来表现这个类的话,就是:

Java代码  收藏代码
  1. package sun.reflect;  
  2.   
  3. public class GeneratedMethodAccessor1 extends MethodAccessorImpl {  
  4.     public GeneratedMethodAccessor1() {  
  5.         super();  
  6.     }  
  7.       
  8.     public Object invoke(Object obj, Object[] args)  
  9.             throws IllegalArgumentException, InvocationTargetException {  
  10.         // prepare the target and parameters  
  11.         if (obj == null) throw new NullPointerException();  
  12.         try {  
  13.             PrintStream target = (PrintStream) obj;  
  14.             if (args.length != 1) throw new IllegalArgumentException();  
  15.             String arg0 = (String) args[0];  
  16.         } catch (ClassCastException e) {  
  17.             throw new IllegalArgumentException(e.toString());  
  18.         } catch (NullPointerException e) {  
  19.             throw new IllegalArgumentException(e.toString());  
  20.         }  
  21.         // make the invocation  
  22.         try {  
  23.             target.println(arg0);  
  24.             return null;  
  25.         } catch (Throwable t) {  
  26.             throw new InvocationTargetException(t);  
  27.         }  
  28.     }  
  29. }  


这段Java代码跟实际的Class文件最主要的不同的地方在于实际的Class文件是用同一个异常处理器来处理ClassCastException与NullPointerException的。如果用Java 7的多重catch语法来写的话就是:

Java代码  收藏代码
  1. package sun.reflect;  
  2.   
  3. public class GeneratedMethodAccessor1 extends MethodAccessorImpl {  
  4.     public GeneratedMethodAccessor1() {  
  5.         super();  
  6.     }  
  7.       
  8.     public Object invoke(Object obj, Object[] args)  
  9.             throws IllegalArgumentException, InvocationTargetException {  
  10.         // prepare the target and parameters  
  11.         if (obj == null) throw new NullPointerException();  
  12.         try {  
  13.             PrintStream target = (PrintStream) obj;  
  14.             if (args.length != 1) throw new IllegalArgumentException();  
  15.             String arg0 = (String) args[0];  
  16.         } catch (final ClassCastException | NullPointerException e) {  
  17.             throw new IllegalArgumentException(e.toString());  
  18.         }  
  19.         // make the invocation  
  20.         try {  
  21.             target.println(arg0);  
  22.             return null;  
  23.         } catch (Throwable t) {  
  24.             throw new InvocationTargetException(t);  
  25.         }  
  26.     }  
  27. }  



本来想顺带演示一下用Java反编译器把例子里的Class文件反编译为Java源码的,但用了JDJad都无法正确识别这里比较特别的Exceptions属性表,只好人肉反编译写出来……识别不出来也正常,毕竟Java 7之前在Java源码这层是没办法对同一个异常处理器处理指定多个异常类型。

要深究的话,上面人肉反编译的Java文件跟实际Class文件还有些细节差异。
例如说JDK在生成Class文件时为了方便所以把一大堆“很可能会用到”的常量都写到常量池里了,但在代码里可能并没有用到常量池里的所有项;如果用javac编译Java源码就不会出现这种状况。
又例如生成的Class文件里一个局部变量也没用,locals=3之中三个都是参数:第一个是this,第二个是obj,第三个是args。求值的中间结果全部都直接在操作数栈上用掉了。而在Java源码里无法写出这样的代码,像是说try块不能从一个表达式的中间开始之类的。

这次就写到这里吧~
A. Sundararajan有篇不错的文章也是讲如何从Java进程dump出Class文件的,使用的是JVMTI系的API:
Retrieving .class files from a running app
然后也有一篇使用SA从core dump文件中dump出Class文件的文章:
Retrieving .class files from a Java core dump

借助HotSpot SA来反汇编

记一些HotSpot中Serviceability Agent(以下简称SA)的有趣用法。
前面提到过,要在Windows上使用SA的话,可以使用Sun JDK 7 build 64或者更新的版本。下面我们用JDK 7 build 96来跑VisualVM 1.3和Groovy 1.7.2的groovysh做个例子。
(要用JDK7来启动groovysh的话,一个简单的办法是在命令行里把JAVA_HOME设置到JDK7的目录上。
另外,要让Groovy使用某些Java参数启动的话,可以设置JAVA_OPTS环境变量)

在启动VisualVM的时候,确保它使用的是JDK7。可以用--jdkhome参数或者在VisualVM安装目录下的etc/visualvm.conf文件中设置jdkhome。启动后在VisualVM主菜单的Tools -> Plugins里,选择Available Plugins选项卡,在列表里可以看到一个称为SAPlugin的插件,把它装上。SAPlugin是VisualVM为SA提供的图形界面插件,可以方便的使用SA的部分封装好的功能。

然后用JDK7来启动一个groovysh,再在VisualVM里通过SAPlugin接进去,可以看到:

SAPlugin还有这样的功能,可以看到被JIT编译过的方法以及解释器的汇编代码:


那SA是不是有内建的反汇编器,可以把任意x86、x64、IA64、SPARC等架构的机器码给反汇编为汇编代码呢?答案是肯定的,有足够方便的现成的API可用。(x64版尚未实现完,暂时还用了)
(顺带提一下:“反汇编”(disassemble)与“反编译”(decompile)是不一样的,请不要弄混了。)

设想有个场景,HotSpot VM突然crash了,并在退出过程中打出了一个hs_err_pidXXXX.log的日志文件。其中开头的部分是:
Hotspot crash log代码  收藏代码
  1. #  
  2. # A fatal error has been detected by the Java Runtime Environment:  
  3. #  
  4. #  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x6d9039f1, pid=3628, tid=3664  
  5. #  
  6. # JRE version: 6.0_18-b07  
  7. # Java VM: Java HotSpot(TM) Client VM (16.0-b13 mixed mode, sharing windows-x86 )  
  8. # Problematic frame:  
  9. # V  [jvm.dll+0x1039f1]  
  10. #  
  11. # If you would like to submit a bug report, please visit:  
  12. #   http://java.sun.com/webapps/bugreport/crash.jsp  
  13. #  
  14.   
  15. ---------------  T H R E A D  ---------------  
  16.   
  17. Current thread (0x0084c000):  JavaThread "main" [_thread_in_vm, id=3664, stack(0x008e0000,0x00930000)]  
  18.   
  19. siginfo: ExceptionCode=0xc0000005, reading address 0x00000008  
  20.   
  21. Registers:  
  22. EAX=0x00000000, EBX=0x0084c000, ECX=0x00000008, EDX=0x0092e134  
  23. ESP=0x0092dbf8, EBP=0x0092dc3c, ESI=0x00000000, EDI=0x0084c8ec  
  24. EIP=0x6d9039f1, EFLAGS=0x00010246  
  25.   
  26. Top of Stack: (sp=0x0092dbf8)  
  27. 0x0092dbf8:   0084c110 0092e134 0092e134 0092e134  
  28. 0x0092dc08:   04506fef 0092dc30 1000781f 00000000  
  29. 0x0092dc18:   00000001 00000000 0092e054 0084c000  
  30. 0x0092dc28:   00000000 00000002 0084c110 f910dbcd  
  31. 0x0092dc38:   0084c000 0084c110 62f01296 0084c8e8  
  32. 0x0092dc48:   0092e134 62f0f17c 00000049 00000000  
  33. 0x0092dc58:   00000000 00000000 00000000 00000000  
  34. 0x0092dc68:   00000000 00000000 00000000 00000000   
  35.   
  36. Instructions: (pc=0x6d9039f1)  
  37. 0x6d9039e1:   8b f0 eb 07 89 51 08 89 30 8b f0 8b 00 8d 48 08  
  38. 0x6d9039f1:   8b 01 53 ff 50 2c 8b 43 04 85 c0 0f 85 8f 00 00   

可以看到HotSpot遇到了访问异常(EXCEPTION_ACCESS_VIOLATION)。访问异常有很多时候是由本地代码中的空指针访问而引起的,这里是否也是如此呢?
至少得看看出错的指令长啥样吧。可以看到日志里有一块写着“Instructions”,有两行十六进制表示的数据跟在后面。这就是出错位置前后的机器码,其中第二行开头处是出错指令的开头位置。

借助SA里内建的反汇编器,我们可以写段代码来看看日志里出错的机器码到底是什么:
Java代码  收藏代码
  1. import sun.jvm.hotspot.asm.CPUHelper;  
  2. import sun.jvm.hotspot.asm.Disassembler;  
  3. import sun.jvm.hotspot.asm.Instruction;  
  4. import sun.jvm.hotspot.asm.InstructionVisitor;  
  5. import sun.jvm.hotspot.asm.SymbolFinder;  
  6. import sun.jvm.hotspot.asm.x86.X86Helper;  
  7.   
  8. /** 
  9.  * @author sajia 
  10.  * 
  11.  */  
  12. public class TestX86Disassembler {  
  13.     /** 
  14.      * @param args 
  15.      */  
  16.     public static void main(String[] args) {  
  17.         CodeSnippet code = makeSampleCode();  
  18.           
  19.         CPUHelper cpuHelper = new X86Helper();  
  20.         Disassembler dasm = cpuHelper.createDisassembler(code.startPc, code.code);  
  21.         StringBuilder buf = new StringBuilder();  
  22.         dasm.decode(new RawCodeVisitor(buf));  
  23.         String dasmStr = buf.toString();  
  24.           
  25.         System.out.println(dasmStr);  
  26.     }  
  27.       
  28.     private static CodeSnippet makeSampleCode() {  
  29.         CodeSnippet code = new CodeSnippet();  
  30.         code.startPc = 0x6d9039f1;  
  31.         code.code = new byte[] {  
  32.             (byte) 0x8b, (byte) 0x01,  
  33.             (byte) 0x53,  
  34.             (byte) 0xff, (byte) 0x50, (byte) 0x2c,  
  35.             (byte) 0x8b, (byte) 0x43, (byte) 0x04,  
  36.             (byte) 0x85, (byte) 0xc0,  
  37.             (byte) 0x0f, (byte) 0x85, (byte) 0x8f, (byte) 0x00, (byte) 0x00  
  38.         };  
  39.         return code;  
  40.     }  
  41.       
  42.     private static class CodeSnippet {  
  43.         long startPc;  
  44.         byte[] code;  
  45.     }  
  46.       
  47.     private static class RawCodeVisitor implements InstructionVisitor {  
  48.         private final StringBuilder buf;  
  49.         private final SymbolFinder symFinder = new DummySymbolFinder();  
  50.           
  51.         public RawCodeVisitor(StringBuilder buf) {  
  52.             this.buf = buf;  
  53.         }  
  54.   
  55.         @Override  
  56.         public void prologue() {  
  57.             // do nothing  
  58.         }  
  59.   
  60.         @Override  
  61.         public void visit(long currentPc, Instruction instr) {  
  62.             buf.append("0x")  
  63.                 .append(Long.toHexString(currentPc))  
  64.                 .append(":  ")  
  65.                 .append(instr.asString(currentPc, symFinder))  
  66.                 .append("\n");  
  67.         }  
  68.           
  69.         @Override  
  70.         public void epilogue() {  
  71.             // do nothing  
  72.         }  
  73.     }  
  74.       
  75.     public static class DummySymbolFinder implements SymbolFinder {  
  76.         public String getSymbolFor(long address) {  
  77.             return "0x" + Long.toHexString(address);  
  78.         }  
  79.     }  
  80. }  

确保$JAVA_HOME/lib/sa-jdi.jar在classpath上,运行该程序可以看到输出为:
Command prompt代码  收藏代码
  1. 0x6d9039f1:  movl   %eax, [%ecx]  
  2. 0x6d9039f3:  pushl  %ebx  
  3. 0x6d9039f4:  call   44[%eax]  
  4. 0x6d9039f7:  movl   %eax, 4[%ebx]  
  5. 0x6d9039fa:  testl  %eax, %eax  
  6. 0x6d9039fc:  jne    0x6d903a91  

可以看到在地址0x6d9039f1的指令是mov eax, [ecx],对ECX寄存器有一个间接读访问,而从日志上可以看到此时ECX寄存器的值为0x00000008。在32位Windows XP上0x00000000-0x0000FFFF是保护区域,尝试对该区域读写会引发访问异常。这里程序试图从0x00000008读取一个DWORD赋值给EAX,印证了它是日志中访问异常的事发地。当然,真正导致该问题的代码肯定在更前面的地方,但确认事发地的状况也算是个好的开始。

SA反汇编出来的汇编代码语法既不是AT&T系的(GNU as用这种),也不是Intel系的(MASMNASM用这种),感觉有那么点怪。
与Intel系的相似点是参数顺序,目标在前,源在后;间接引用以方括号表示。
与AT&T系相似的是寄存器带有%前缀,而且间接引用的偏移量写在括号的前面;指令带有宽度后缀。
总之需要点时间来习惯…

反汇编器这部分的实现与SA的其它相对独立,要抽出来单独用也没什么问题。有需要的话抽个出来改造成纯MASM语法输出或许会有用。