今天又在重构“祖传代码”,看到了这一幕:
心好累,直接抄网文了。。。
误人子弟之一,估计是 写超长函数(不会合理拆分函数,流水账代码)或者大对象不会使用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.
推荐阅读(点击即可跳转阅读)
2. 面试题内容聚合
3. 设计模式内容聚合
4. Mybatis内容聚合
5. 多线程内容聚合
java中将对象赋值为null,对垃圾回收有用吗?
相信,网上很多java性能优化的帖子里都会有这么一条:尽量把不使用的对象显式得置为null.这样有助于内存回收
可以明确的说,这个观点是基本错误的.sun jdk远比我们想象中的机智.完全能判断出对象是否已经no ref..但是,我上面用的词是"基本".也就是说,有例外的情况.这里先把这个例外情况给提出来,后续我会一点点解释.这个例外的情况是, 方法前面中有定义大的对象,然后又跟着非常耗时的操作,且没有触发JIT编译..总结这句话,就是
上面这句话有点绕,但是,上面说的每一个条件都是有意义的.这些条件分别是
2 定义了一个大对象(小对象没有意义)
3 之后跟着一个非常耗时的操作.
4 没有满足JIT编译条件
上面4个条件缺一不可,把obj显式设置成null才是有意义的. 下面我会一一解释上面的这些条件
在解释上面的条件之前,简略的说一下一些基础知识.
(1)sun jdk的内存垃圾判定,是基于根搜索算法的.也就是说,在GC root为跟,能被搜索到的,就认为是存活对象,搜索不到的,则认为是"垃圾".
(2)GC root 里和我们这篇文章有关的gc root是这一条
Local variable. For example, input parameters or locally created objects of methods that are still in the stack of a thread.
这句话直接翻译就是说是"本地变量,例如方法的参数或者方法中创建的局部变量".如果换一种说法是,
下面开始说四大条件. 我们测试是否被垃圾回收的方法是,申请一个64M的byte数组(作为大对象),然后调用System.gc();.运行的时候用 -verbose:gc 观察回收情况来判定是否会回收.
同一个方法中
这个条件是最容易理解的,如果大对象定义在其他方法中,那么是不需要设置成Null的,
- public class Test
- {
- public static void main(String[] args){
- foo();
- System.gc();
- }
- public static void foo(){
- byte[] placeholder = new byte[64*1024*1024];
- }
- }
对应的输出如下,可以看到64M的内存已经被回收
[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的周期.例如
- public static void main(String[] args) throws Exception{
- byte[] placeholder = new byte[64*1024*1024];
- Thread.sleep(3000l);
- // dosomething
- }
在线程sleep的三秒内,可能jvm已经进行了好几次ygc.但是由于placeholder一直持有这个大对象,所以造成这个64M的大对象一直无法被回收,甚至有可能造成了满足进入old 区的条件.这个时候,在sleep之前,显式得把placeholder设置成Null是有意义的. 但是,
没有满足JIT编译条件
jit编译的触发条件,这里就不多阐述了.对应的测试代码和前面一样
- public class Test
- {
- public static void main(String[] args) throws Exception{
- byte[] placeholder = new byte[64*1024*1024];
- placeholder = null;
- //do some time-consuming operation
- System.gc();
- }
- }
在解释执行中,我们认为
是有助于对这个大对象的回收的.在JIT编译下,我们可以通过强制执行编译执行,然后打印出对应的 ASM码的方式查看. 安装fast_debug版本的jdk请查看
使用-XX:+PrintAssembly打印asm代码遇到的问题
命令是
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高级特性与最佳实践> 一书
我们认为
- public class Test
- {
- public static void main(String[] args) throws Exception{
- byte[] placeholder = new byte[64*1024*1024];
- //do some time-consuming operation
- System.gc();
- }
- }
这样的情况下,placeholder的对象是不会被回收的.可以理解..然后我们继续修改方法体
- public class Test
- {
- public static void main(String[] args) throws Exception{
- {
- byte[] placeholder = new byte[64*1024*1024];
- }
- System.gc();
- }
- }
我们运行发现
[GC 66798K->66072K(120960K), 0.0021019 secs]
[Full GC 66072K->66017K(120960K), 0.0069085 secs]
垃圾收集器并不会把对象给回收..明明已经出了作用域,竟然还是不回收!. 好吧,继续修改例子
- public class Test
- {
- public static void main(String[] args) throws Exception{
- {
- byte[] placeholder = new byte[64*1024*1024];
- }
- int a = 0;
- System.gc();
- }
- }
唯一的变化就是新增了一个 int a = 0; 继续看效果
[GC 66798K->66144K(120960K), 0.0011617 secs]
[Full GC 66144K->481K(120960K), 0.0060882 secs]
可以看到,大对象被回收了..这是一个神奇的例子..能想到这个,我对书的作者万分佩服! 但是这个例子的解释,在书中的解释有点泛(至少我刚开始没看懂),所以这里就仔细说明一下.
要解释这个,先大概看一下 Java执行机制 里面局部变量表的部分.
上面的这段话有点抽象,后面一个个解释.其实方法的局部变量表大小在javac的时候就已经确定了.
在class文件中,方法体对应的Code属性中就有对应的Locals属性,就是来记录局部变量表的大小的.例子如下:
- public class Test
- {
- public void foo(int a,int b){
- int c = 0;
- return;
- }
- }
通过 javac -g:vars Test 编译,然后,通过javap -verbose 查看
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的复用呢,继续看例子
- public class Test
- {
- public void foo(int a,int b){
- {
- int d = 0;
- }
- int c = 0;
- return;
- }
- }
在 int c = 0;之前新增一个作用域,里面定义了一个局部变量.如果没有slot复用机制,那么,理论上说,这个方法中局部变量表的slot个数应该是5个,但是,看具体的javap 输出
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并没有马上消除,而是继续留着给下面的局部变量使用..按照这样理解,
- public class Test
- {
- public static void main(String[] args) throws Exception{
- {
- byte[] placeholder = new byte[64*1024*1024];
- }
- System.gc();
- }
- }
这个例子中,在执行System.gc()的时候,虽然placeholder 的作用域已经结束,但是placeholder 对应的slot还存在,继续持有64M数组这个大对象,那么自然的,在GC的时候不会把对应的大对象给清理掉.而在
- public class Test
- {
- public static void main(String[] args) throws Exception{
- {
- byte[] placeholder = new byte[64*1024*1024];
- }
- int a = 0;
- System.gc();
- }
- }
这个例子中,在System.gc的时候,placeholder对应的slot已经被a给占用了,那么对应的大对象就变成了无根的"垃圾",当然会被清楚.这一点,可以通过javap明显的看到
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].
这个例子说明的差不多了,在上面的基础上,再多一个例子
- public class Test
- {
- public static void main(String[] args) throws Exception{
- {
- int b = 0;
- byte[] placeholder = new byte[64*1024*1024];
- }
- int a = 0;
- System.gc();
- }
- }
这个代码中,这个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),其中程序计数器存放要执行的指令在方法内的偏移量,栈当中存放栈帧,每个方法每次调用都会产生栈帧.
可以这么理解,任何在方法体中特意定义过的局部变量,那么都会放到局部变量表(方法参数也会放这里).其他的中间结果.具体看下面的例子
- public class Test
- {
- public int add(int a,int b){
- int c = a + 1;
- return c * b;
- }
- }
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
}
补充说明一下,
栈顶缓存
从上面的例子看到,我们计算 c * b 的时候,需要从存放c的寄存器(也就是第三个局部变量表的元素)先压入操作数栈的栈顶,然后再计算相乘操作.而栈顶缓存的效果就是,取消压栈的这次操作,直接把寄存器的数据拿来做相乘操作,然后把结果压入栈.
使用-XX:+PrintAssembly打印asm代码遇到的问题
要用PrintAssembly的目的 应该会另开帖子说明,本帖只是为了记录为了简单的记录使用这个命令遇到的问题.
1 ,直接使用,用的是
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 然后,就运行试试.继续遇到问题
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这货,但是死的很惨,一直没成功.具体就不多说了,最后还是万能的撒迦告诉了我答案.
这个疑问在之前某帖的回复里有提到:HotSpot的JIT编译器遇到简单无限循环时
是的,这个是由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参数才可以。
- 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网上的了)
- public class Test{
- int a = 1;
- static int b = 2;
- public int sum(int c){
- return a + b + c;
- }
- public static void main(String[] args){
- new Test().sum(3);
- }
- }
运行的代码如下
其中,
-XX:CompileCommand=dontinline,*Test.sum 这个表示不要把sum方法给内联了.这是解决内联问题
-XX:CompileCommand=compileonly,*Test.sum 这个表示只编译sum方法,这样的话,只会输出sum方法的ASM码.
对应的输出如下
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 -XX:+PrintCompilation -XX:+PrintAssembly TestC2InfiniteLoop
- public class TestC2InfiniteLoop {
- public static void foo() {
- while (true) { int i = 1; }
- }
- public static void main(String[] args) {
- foo();
- }
- }
HotSpot的JIT编译器会:
1、client模式:执行命令:
- java -XX:+PrintCompilation -XX:+PrintAssembly TestC2InfiniteLoop
C1会编译代码,会将循环内无用的代码都消除掉,但不会把循环本身消除:
- 1% TestC2InfiniteLoop::foo @ 0 (5 bytes)
- Decoding compiled method 0x00bc6748:
- Code:
- [Disassembling for mach='i386']
- [Entry Point]
- [Verified Entry Point]
- ;; block B2 [0, 0]
- 0x00bc6810: mov %eax,-0x4000(%esp)
- 0x00bc6817: push %ebp
- 0x00bc6818: mov %esp,%ebp
- 0x00bc681a: sub $0x18,%esp ;*iconst_1
- ; - TestC2InfiniteLoop::foo@0 (line 5)
- ;; block B3 [0, 0]
- 0x00bc681d: nop
- 0x00bc681e: nop
- 0x00bc681f: nop ; OopMap{off=16}
- ;*goto
- ; - TestC2InfiniteLoop::foo@2 (line 5)
- ;; block B0 [0, 2]
- 0x00bc6820: test %eax,0x970100 ; {poll}
- ;; 26 branch [AL] [B0]
- 0x00bc6826: jmp 0x00bc6820 ;*goto
- ; - TestC2InfiniteLoop::foo@2 (line 5)
- ;; block B1 [0, 0]
- 0x00bc6828: mov %eax,-0x4000(%esp)
- 0x00bc682f: push %ebp
- 0x00bc6830: mov %esp,%ebp
- 0x00bc6832: sub $0x18,%esp
- 0x00bc6835: mov %ecx,(%esp)
- 0x00bc6838: call 0x082ea120 ; {runtime_call}
- ;; 20 branch [AL] [B0]
- 0x00bc683d: jmp 0x00bc6820
- 0x00bc683f: nop
- 0x00bc6840: nop
- 0x00bc6841: hlt
- 0x00bc6842: hlt
- 0x00bc6843: hlt
- 0x00bc6844: hlt
- 0x00bc6845: hlt
- 0x00bc6846: hlt
- 0x00bc6847: hlt
- 0x00bc6848: hlt
- 0x00bc6849: hlt
- 0x00bc684a: hlt
- 0x00bc684b: hlt
- 0x00bc684c: hlt
- 0x00bc684d: hlt
- 0x00bc684e: hlt
- 0x00bc684f: hlt
- [Exception Handler]
- [Stub Code]
- 0x00bc6850: mov $0xdead,%ebx ; {no_reloc}
- 0x00bc6855: mov $0xdead,%ecx
- 0x00bc685a: mov $0xdead,%edx
- 0x00bc685f: mov $0xdead,%esi
- 0x00bc6864: mov $0xdead,%edi
- 0x00bc6869: jmp 0x00bc1d60 ; {runtime_call}
- 0x00bc686e: push $0xbc686e ; {section_word}
- 0x00bc6873: jmp 0x00b7ba40 ; {runtime_call}
上面位于0x00bc6820和0x00bc6826的两条指令就是无限循环的残余物:
- 0x00bc6820: test %eax,0x970100 ; {poll}
- ;; 26 branch [AL] [B0]
- 0x00bc6826: jmp 0x00bc6820 ;*goto
- ; - TestC2InfiniteLoop::foo@2 (line 5)
原本在循环内的代码(int i = 1;)已经消失了,剩下的是在回边处对safepoint的轮询(test),以及循环末尾的无条件跳转(jmp)。
从编译记录看,int i = 1;对应的代码是在寄存器分配过程中被削除的。
2、server模式:执行命令:
- java -server -XX:+PrintCompilation -XX:+PrintAssembly TestC2InfiniteLoop
C2会拒绝编译这种代码,并打出日志:
- 1% TestC2InfiniteLoop::foo @ 0 (5 bytes)
- 1 COMPILE SKIPPED: trivial infinite loop (not retryable)
然后foo()中的无限循环就一直在解释器中执行下去了。
在hotspot/src/share/vm/opto/compile.cpp中,bool Compile::final_graph_reshaping()的代码前面有这样的注释:
- // (4) Detect infinite loops; blobs of code reachable from above but not
- // below. Several of the Code_Gen algorithms fail on such code shapes,
- // so we simply bail out. Happens a lot in ZKM.jar, but also happens
- // from time to time in other codes (such as -Xcomp finalizer loops, etc).
- // Detection is by looking for IfNodes where only 1 projection is
- // reachable from below or CatchNodes missing some targets.
- // ...
- bool Compile::final_graph_reshaping() {
- // an infinite loop may have been eliminated by the optimizer,
- // in which case the graph will be empty.
- if (root()->req() == 1) {
- record_method_not_compilable("trivial infinite loop");
- return true;
- }
- // ...
- }
======================================================================
上面的实验中,如果把foo中的无限循环在源码上就变为空循环的话:
- public static void foo() {
- while (true) ;
- }
则无论是在client还是server模式都不会触发对该方法的JIT编译,一直在解释器中去执行那个循环。
查看javac编译生成的字节码,可以确认该循环在字节码中是存在的:
- public static void foo();
- Signature: ()V
- Code:
- Stack=0, Locals=0, Args_size=0
- 0: goto 0
- LineNumberTable:
- line 5: 0
- StackMapTable: number_of_entries = 1
- frame_type = 0 /* same */
foo()方法中唯一的一条字节码指令就是goto 0了。有趣的是因为foo()带有无限循环,所以编译出来的字节码里连return都没有。
HotSpot解释器触发JIT编译,是通过两个计数器来进行的:方法调用计数器与回边计数器,当这两个计数器的和超过了方法调用或循环次数的预设阈值就触发对某个方法的JIT编译;这两个计数器在每个方法里都有自己的一份。其中,方法调用计数器自然是用来记录某个方法被调用的次数的,而回变计数器则用于记录某方法中所有循环执行的次数(以方法而不是单个循环为粒度)。一个空的无限循环在字节码中的表现是一条goto字节码指令,参数是字节码的相对偏移量,并且该偏移量为0(意味着它指向该goto指令自身)。解释器中有这样的代码:
- 0x0097e781: test %edx,%edx
- 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());