论坛首页 Java企业应用论坛

代码难道不是这么写的?

浏览 65872 次
该帖已经被评为良好帖
作者 正文
   发表时间:2010-07-30  
ei0 写道
我只对s感兴趣!!!
0 请登录后投票
   发表时间:2010-07-30  
mercyblitz 写道
moonranger 写道
qiushily2030 写道
fengfeng925 写道
qiushily2030 写道
A a;
申明在内部,每次遍历都申明一个a的变量.  
申明在外面 只申明一次 一直复用.  申明也是要资源的..
一个变量就相当于一个指针,循环1亿次,LZ你说呢.
评审官说的还是有理的,不过这个得看需求.  JVM回收是在内存不足的时候.
  还是不要想当然了,我就那么干过一回. 程序是最真实的答案...

简直是TMD胡扯。


JVM回收是在内存不足的时候.这个已经修正. 不是内存不足才调用。。
你说我胡扯? 那for里定义的局部变量放哪里?不要也和上面一个的观点一样:只申明一次,后调用索引.
这个我不可理解。。


方法的参数连同方法里的变量所占用的空间,在编译时就已经确定了,它决定了运行时一个方法被调用的时侯要分配多大的栈帧。一个变量放在循环外还是循环里,只是在编译期限定了其作用域而已,到了运行时是没有区别的(也许只是在栈帧的local variables区里的位置不同)。
看一下方法的字节码,一切都非常明了:
private final int[] numbers = {1, 2, 3, 4, 5};
	
public void varOutsideLoop() {
	int n;
	for (int i = 0; i < numbers.length; i++) {
		n = numbers[i];
		System.out.println(n);
	}
}
	
public void varInsideLoop() {
	for (int i = 0; i < numbers.length; i++) {
		int n = numbers[i];
		System.out.println(n);
	}
}


对应的字节码:
public void varOutsideLoop();
  Signature: ()V
  Code:
   Stack=2, Locals=3, Args_size=1
   0:	iconst_0
   1:	istore_2
   2:	goto	22
   5:	aload_0
   6:	getfield	#12; //Field numbers:[I
   9:	iload_2
   10:	iaload
   11:	istore_1
   12:	getstatic	#19; //Field java/lang/System.out:Ljava/io/PrintStream;
   15:	iload_1
   16:	invokevirtual	#25; //Method java/io/PrintStream.println:(I)V
   19:	iinc	2, 1
   22:	iload_2
   23:	aload_0
   24:	getfield	#12; //Field numbers:[I
   27:	arraylength
   28:	if_icmplt	5
   31:	return
  LineNumberTable: 
   line 43: 0
   line 44: 5
   line 45: 12
   line 43: 19
   line 47: 31

  LocalVariableTable: 
   Start  Length  Slot  Name   Signature
   0      32      0    this       Lcom/jstudio/learnjvm/classloader/VarInLoop;
   12      10      1    n       I
   2      29      2    i       I


public void varInsideLoop();
  Signature: ()V
  Code:
   Stack=2, Locals=3, Args_size=1
   0:	iconst_0
   1:	istore_1
   2:	goto	22
   5:	aload_0
   6:	getfield	#12; //Field numbers:[I
   9:	iload_1
   10:	iaload
   11:	istore_2
   12:	getstatic	#19; //Field java/lang/System.out:Ljava/io/PrintStream;
   15:	iload_2
   16:	invokevirtual	#25; //Method java/io/PrintStream.println:(I)V
   19:	iinc	1, 1
   22:	iload_1
   23:	aload_0
   24:	getfield	#12; //Field numbers:[I
   27:	arraylength
   28:	if_icmplt	5
   31:	return
  LineNumberTable: 
   line 50: 0
   line 51: 5
   line 52: 12
   line 50: 19
   line 54: 31

  LocalVariableTable: 
   Start  Length  Slot  Name   Signature
   0      32      0    this       Lcom/jstudio/learnjvm/classloader/VarInLoop;
   2      29      1    i       I
   12      7      2    n       I


仔细看字节码就能发现,除了n和循环变量i在local variable table中的位置不同,其他的所有的都没有区别。
varOutsideLoop那个方法里我故意没有初始化n,如果加上初始化,就会多一条iconst和一条istore指令。
至于位置不一样的原因,个人猜测与变量出现的位置有关,n在循环外的版本,它出现在前,所以占据了第一个slot(第0个slot是对象本身,即this);n在循环里的版本,循环变量i出现在前,所以i占据了第一个slot,而n占用的是第二个slot。
除此之外,两个方法没有任何的区别。

mercyblitz 写道

主要你误解了a变量在遍历中,有入栈和出栈的操作。并没有重复开辟局部变量,再说JVM优化之后,不会重复创建。

个人认为这种说法也不太对,在一个方法内部不会有所谓的“入栈”和“出栈”操作。方法里,一个变量占用一个local variable table的slot,一个萝卜一个坑。所有这些变量都是在方法结束,堆栈帧被销毁的时侯才真正被“回收”,即使方法结束的时侯它早以超出了自己的作用域……

实践中,变量的作用域当然是尽可能小才好,即有利于垃圾收集、也消除了一些潜在的导致bug的隐患。
楼主别理那个“代码评审官”就好……话说如果我遇到这种人这种事,我八成不愿继续干下去……



恩,谢谢指正,我也仔细想了一下。在楼主的例子中,字节码说明两个局部变量指向不同的地方。不会重复开辟。要想开辟更多,哪么需要不同名称的变量申明。


第一个是frame append

第二个是frame same

所以还是有些许区别的吧
0 请登录后投票
   发表时间:2010-07-30   最后修改:2010-07-30
moonranger 写道
mercyblitz 写道
主要你误解了a变量在遍历中,有入栈和出栈的操作。并没有重复开辟局部变量,再说JVM优化之后,不会重复创建。

个人认为这种说法也不太对,在一个方法内部不会有所谓的“入栈”和“出栈”操作。方法里,一个变量占用一个local variable table的slot,一个萝卜一个坑。所有这些变量都是在方法结束,堆栈帧被销毁的时侯才真正被“回收”,即使方法结束的时侯它早以超出了自己的作用域……

ECMA-335 CLI是规定方法中每个局部变量占用局部变量区的一个坑,不在方法中复用局部变量区。但它在设计之初就是倾向于使用JIT编译器来实现执行引擎的;虽然中间代码(CIL)中局部变量不复用局部变量区的坑,但实际JIT编译后的代码不关心这个,局部变量的存储分配(栈/寄存器)还是有复用。可以参考.NET CLR的实现。

(以下说明针对概念中的Java虚拟机)

Java虚拟机在设计之初就同时考虑到要能方便的用解释器或JIT编译器来实现。解释器一般做的优化较少,执行过程与字节码中指定的方式基本是直接对应的,所以字节码上就已经需要考虑到实际执行的情况了。Java虚拟机的局部变量区是可复用的,在同一方法里作用域不交叠的局部变量可以分配在同一个局部变量区的slot上。有兴趣的话可以自己做实验验证,也可以参考之前我做的分享的演示稿,20100621的版本。
例子的话,可以看这么一段代码:
public class LocalsDemo {
    // locals = 5
    public static void main(String[] args) { // args in slot 0
        int a = 1;                           // a    in slot 1
        int b = 2;                           // b    in slot 2
        {
            long c = 3;                      // c    in slot 3 (to slot 4)
        }
        {
            int d = 4;                       // d    in slot 3
            int e = 5;                       // e    in slot 4
        }
        for (int i = 0; i < 3; i++) {        // i    in slot 3
            int j = i + 1;                   // j    in slot 4
        }
    }
}

main()方法编译后对应的字节码和元信息是:
public static void main(java.lang.String[]);
  Code:
   Stack=2, Locals=5, Args_size=1
   0:   iconst_1
   1:   istore_1
   2:   iconst_2
   3:   istore_2
   4:   ldc2_w  #2; //long 3l
   7:   lstore_3
   8:   iconst_4
   9:   istore_3
   10:  iconst_5
   11:  istore  4
   13:  iconst_0
   14:  istore_3
   15:  iload_3
   16:  iconst_3
   17:  if_icmpge       31
   20:  iload_3
   21:  iconst_1
   22:  iadd
   23:  istore  4
   25:  iinc    3, 1
   28:  goto    15
   31:  return
  LineNumberTable:
   line 4: 0
   line 5: 2
   line 7: 4
   line 10: 8
   line 11: 10
   line 13: 13
   line 14: 20
   line 13: 25
   line 16: 31

  LocalVariableTable:
   Start  Length  Slot  Name   Signature
   8      0      3    c       J
   10      3      3    d       I
   13      0      4    e       I
   25      0      4    j       I
   15      16      3    i       I
   0      32      0    args       [Ljava/lang/String;
   2      30      1    a       I
   4      28      2    b       I

要在Class文件里看到LocalVariableTable属性表的话,编译Java源码时请加上-g参数。

Java虚拟机中,
方法调用涉及的栈帧push/pop是对Java栈(Java stack),或者有些文档里叫“Java控制栈”(Java control stack),或者叫“Java方法调用栈”;
在方法中局部变量的写/读操作涉及的push/pop则是对操作数栈(operand stack),或者有些文档叫“表达式栈”(expression stack),或者叫求值栈(evaluation stack)。
两个栈的区别可以参考Java虚拟机规范第二版3.5.23.6.2两个小节,我之前的介绍基于栈/寄存器的虚拟机的帖里也有提到,也可以参考前面说的JVM分享的演示稿。

最后上一老图……逃
11 请登录后投票
   发表时间:2010-07-30  
RednaxelaFX 写道
moonranger 写道
mercyblitz 写道
主要你误解了a变量在遍历中,有入栈和出栈的操作。并没有重复开辟局部变量,再说JVM优化之后,不会重复创建。

个人认为这种说法也不太对,在一个方法内部不会有所谓的“入栈”和“出栈”操作。方法里,一个变量占用一个local variable table的slot,一个萝卜一个坑。所有这些变量都是在方法结束,堆栈帧被销毁的时侯才真正被“回收”,即使方法结束的时侯它早以超出了自己的作用域……

ECMA-335 CLI是规定方法中每个局部变量占用局部变量区的一个坑,不在方法中复用局部变量区。但它在设计之初就是倾向于使用JIT编译器来实现执行引擎的;虽然中间代码(CIL)中局部变量不复用局部变量区的坑,但实际JIT编译后的代码不关心这个,局部变量的存储分配(栈/寄存器)还是有复用。可以参考.NET CLR的实现。

(以下说明针对概念中的Java虚拟机)

Java虚拟机在设计之初就同时考虑到要能方便的用解释器或JIT编译器来实现。解释器一般做的优化较少,执行过程与字节码中指定的方式基本是直接对应的,所以字节码上就已经需要考虑到实际执行的情况了。Java虚拟机的局部变量区是可复用的,在同一方法里作用域不交叠的局部变量可以分配在同一个局部变量区的slot上。有兴趣的话可以自己做实验验证,也可以参考之前我做的分享的演示稿,20100621的版本。
例子的话,可以看这么一段代码:
public class LocalsDemo {
    // locals = 5
    public static void main(String[] args) { // args in slot 0
        int a = 1;                           // a    in slot 1
        int b = 2;                           // b    in slot 2
        {
            long c = 3;                      // c    in slot 3 (to slot 4)
        }
        {
            int d = 4;                       // d    in slot 3
            int e = 5;                       // e    in slot 4
        }
        for (int i = 0; i < 3; i++) {        // i    in slot 3
            int j = i + 1;                   // j    in slot 4
        }
    }
}

main()方法编译后对应的字节码和元信息是:
public static void main(java.lang.String[]);
  Code:
   Stack=2, Locals=5, Args_size=1
   0:   iconst_1
   1:   istore_1
   2:   iconst_2
   3:   istore_2
   4:   ldc2_w  #2; //long 3l
   7:   lstore_3
   8:   iconst_4
   9:   istore_3
   10:  iconst_5
   11:  istore  4
   13:  iconst_0
   14:  istore_3
   15:  iload_3
   16:  iconst_3
   17:  if_icmpge       31
   20:  iload_3
   21:  iconst_1
   22:  iadd
   23:  istore  4
   25:  iinc    3, 1
   28:  goto    15
   31:  return
  LineNumberTable:
   line 4: 0
   line 5: 2
   line 7: 4
   line 10: 8
   line 11: 10
   line 13: 13
   line 14: 20
   line 13: 25
   line 16: 31

  LocalVariableTable:
   Start  Length  Slot  Name   Signature
   8      0      3    c       J
   10      3      3    d       I
   13      0      4    e       I
   25      0      4    j       I
   15      16      3    i       I
   0      32      0    args       [Ljava/lang/String;
   2      30      1    a       I
   4      28      2    b       I

要在Class文件里看到LocalVariableTable属性表的话,编译Java源码时请加上-g参数。

Java虚拟机中,
方法调用涉及的栈帧push/pop是对Java栈(Java stack),或者有些文档里叫“Java控制栈”(Java control stack),或者叫“Java方法调用栈”;
在方法中局部变量的读/写操作涉及的push/pop则是对操作数栈(operand stack),或者有些文档叫“表达式栈”(expression stack),或者叫求值栈(evaluation stack)。
两个栈的区别可以参考Java虚拟机规范第二版3.5.23.6.2两个小节,我之前的介绍基于栈/寄存器的虚拟机的帖里也有提到,也可以参考前面说的JVM分享的演示稿。

最后上一老图……逃


还真没有研究到这种程度。学习了!
2 请登录后投票
   发表时间:2010-07-30  
高人出现了,哈哈哈!!

哪些只懂得代码文本面上搞所谓性能优化的家伙,可以羞愧的匿了~~~
0 请登录后投票
   发表时间:2010-07-30  
精彩,秒杀!
0 请登录后投票
   发表时间:2010-07-30  
太狠了………………
0 请登录后投票
   发表时间:2010-07-30  
没事,实际上,编译以后的字节码是一样的
0 请登录后投票
   发表时间:2010-07-30  
嗯,一样这个词不精确,应该用“等效”
0 请登录后投票
   发表时间:2010-08-03  
没错啊,评审故意找茬吧!
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics