在我们编程的过程中经常会遇到这样的问题。

 for (int i=0;i<n;i++)
for (int i=0;i<n;i++) {
{
 String str = //
    String str = //
 }
}
 String str = null;
String str = null;

 for(int i=0;i<n;i++)
for(int i=0;i<n;i++) {
{
 str = //
    str = //
 }
}


在印象中一直认为方法二的性能好于方法一,但是差距应该很小。但因为一位别人的回文说方法一极大的影响了性能,所以想写个例子证明一下相差很小。例子如下:

 public class TestOt
public class TestOt  {
{

 public static void main(String[] args)
    public static void main(String[] args)  {
{
 long n=1000000000;
        long n=1000000000;
 long start = System.currentTimeMillis();
        long start = System.currentTimeMillis();
 test2(n);
        test2(n);
 long end = System.currentTimeMillis();
        long end = System.currentTimeMillis();
 System.out.println(end-start);
        System.out.println(end-start);
 }
    }

 public static void test1(long n)
    public static void test1(long n) {
{

 for (int i=0;i<n;i++)
        for (int i=0;i<n;i++) {
{
 String str = "";
            String str = "";
 }
        }
 }
    }

 public static void test2(long n)
    public static void test2(long n) {
{
 String str = null;
        String str = null;

 for (int i=0;i<n;i++)
        for (int i=0;i<n;i++) {
{
 str = "";
            str = "";
 }
        }
 }
    }
 }
}测试的结果是当n=10亿次的时候差距是1秒,所以说差距应该是很小的,符合我原始的记忆,但是另一个问题来了,测试的结果是
方法一:3300毫秒左右
方法二:4300毫秒左右
结果刚好相反,于是更改方法


 public class TestOt
public class TestOt  {
{

 public static void main(String[] args)
    public static void main(String[] args)  {
{
 long n=1000000000;
        long n=1000000000;
 long start = System.currentTimeMillis();
        long start = System.currentTimeMillis();
 test1(n);
        test1(n);
 long end = System.currentTimeMillis();
        long end = System.currentTimeMillis();
 System.out.println(end-start);
        System.out.println(end-start);
 }
    }

 public static void test1(long n)
    public static void test1(long n) {
{

 for (int i=0;i<n;i++)
        for (int i=0;i<n;i++) {
{
 String str = null;
            String str = null;
 }
        }
 }
    }

 public static void test2(long n)
    public static void test2(long n) {
{
 String str = null;
        String str = null;

 for (int i=0;i<n;i++)
        for (int i=0;i<n;i++) {
{
 }
        }
 }
    }
 }
}

结果依旧。
没办法,取得字节码,对比

 public class TestOt extends java.lang.Object
public class TestOt extends java.lang.Object {
{
 public TestOt();
public TestOt();
 Code:
  Code:
 0:   aload_0
   0:   aload_0
 1:   invokespecial   #8; //Method java/lang/Object."<init>":()V
   1:   invokespecial   #8; //Method java/lang/Object."<init>":()V
 4:   return
   4:   return

 public static void test1(int);
public static void test1(int);
 Code:
  Code:
 0:   iconst_0
   0:   iconst_0
 1:   istore_1
   1:   istore_1
 2:   goto    10
   2:   goto    10   
 5:   aconst_null
   5:   aconst_null
 6:   astore_2
   6:   astore_2
 7:   iinc    1, 1
   7:   iinc    1, 1
 10:  iload_1
   10:  iload_1   
 11:  iload_0
   11:  iload_0
 12:  if_icmplt       5
   12:  if_icmplt       5
 15:  return
   15:  return

 public static void test2(int);
public static void test2(int);
 Code:
  Code:
 0:   aconst_null
   0:   aconst_null
 1:   astore_1
   1:   astore_1
 2:   iconst_0
   2:   iconst_0
 3:   istore_2
   3:   istore_2
 4:   goto    10
   4:   goto    10   
 7:   iinc    2, 1
   7:   iinc    2, 1
 10:  iload_2
   10:  iload_2   
 11:  iload_0
   11:  iload_0  
 12:  if_icmplt       7
   12:  if_icmplt       7
 15:  return
   15:  return
 }
}结果是感觉还是应该是方法二快,那为什么反而方法一快了1秒左右呢?
不得而知,现在我个人猜测的想法是可能有两种情况:
1,JLS的底层定义决定的,有什么特殊的优化?
2,因为方法二比方法一虽然少了在循环中的部分,但是引用的声明周期反而是更长了,是否因为引用存在造成了方法二的栈操作消耗了大部分时间?
猜想一有待于JLS文档的查阅,我会在有空的时候查询,猜想二正在想办法证明。
看文章的朋友,如果谁了解麻烦指点一下,是我的测试方法写的有问题,还是别的原因,谢谢。
最后:问题已经基本了解了原因,见回复中的讨论,谢谢--daydream 的帮忙。 
# re: 开启一个新的问题(关于声明变量的性能问题) 2007-02-11 03:29 
最开始的测试是在Eclipse中测试的,怕是eclipse的问题,在控制台下也做了测试,虽然得到的数字有微小偏差,但依然是方法一比方法二快1秒左右。问题继续。。  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题) 2007-02-11 06:10 
据说是因为局部变量放在堆栈中的原因。  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题) 2007-02-11 09:26 
在我的机器上测试,两者可以看作是一样快的。对每一个方法运行多次,结果也会稍有偏差,第一个方法:3375(次数比较多)、3390。 
第二个方法:3375(次数比较多)、3390、3391、3406。 
对于下面2段代码来说: 
for (int i=0;i<n;i++){ 
String str = // 
} 
String str = null; 
for(int i=0;i<n;i++){ 
str = // 
} 
区别只是str变量的作用域不同---意味着:代码1的str变量的偏移位置在出了循环的作用域以后,可以被分配给下一个出现的局部变量,而代码2str的位置会一直占有,直到方法结束。 
之所以,有人感觉代码2快,我想是一种错觉吧,就是以为代码1会在循环中重复声明变量str--实际上不是这样。 
另外,JLS是什么东东?  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题)[未登录] 2007-02-11 10:32 
to:JonneyQuest 
详细说说? 
  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题)[未登录] 2007-02-11 10:36 
to:daydream 
你怎么运行的,不是两个一块调用的吧,如果是用循环多次调用求平均值,活着两个一块调用是不准的。如果你是循环调用,活着同时调用,你可以把两个函数的调用顺序换一下,会有较大的差距。 
JLS=The Java Language Specification 
讲述的是Java语言的特性,很多东西C++和Java是不同的,JLS中有描述。例如lazyloading的单态在Java中是不可实现的,这个就因为Java的优化造成的,通过JLS可以查到。  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题) 2007-02-11 12:40 
没有,我是分开运行的,直接运行你的代码。  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题) 2007-02-11 13:55 
这里问题大了。创建对象第一忌:不要在循环体中创建对象。这种做法会在内存中保存N份这个对象的引用会浪费大量的内存空间(虽说内存便宜,可以进行硬件升级),同时JVM的GC机制会因为这些无谓的对象做大量的回收工作,系统不慢都不行呀 
......................................................................................... 
很简单的一个道理,你用String对象根本看不出效果。如果你换成个自定义对象或者图形对象呢?你的第一种做法会在堆内存中生成大量的垃圾对象,这些对象首先占用内存,二则在对于速度的影响上它不会马上体现出来(毕竟在内存够用的情况下无法体现),一旦堆内存中的eden area满了,GC机制开始起作用了那么你就会觉得你的程序速度狂降。。。。 
所以说,第二种做法才是王道! 
呵呵,刚刚写了一篇关于java优化编程的文字,希望可以提供帮助 
http://www.blogjava.net/sinoly/archive/2007/02/11/99205.html  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题) 2007-02-11 14:05 
相对而言,性能的影响并不只是这段代码的执行速度。需要考虑在JVM种它的处理方式,以及这种方式对资源占用的情况。很多性能问题都是在日积月累中体现的。只是丂一条语句所谓的执行速度来判断效率,个人感觉很不合理  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题) 2007-02-11 17:05 
to sinoly : 
“创建对象第一忌:不要在循环体中创建对象。这种做法会在内存中保存N份这个对象的引用会浪费大量的内存空间” 
这是误解。完全没有在内存中保存N份对象的引用,循环体内声明的对象也只是在java栈中占据一个位置。 
反而在循环体内声明的对象因为其作用域只是在循环体内,更节约内存(虽然微乎其微)。 
  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题)[未登录] 2007-02-11 17:06 
to:sinoly 
我在平时写代码的时候也不是在循环体内创建,但是记忆中差别不大。 
对于你的说法, 
1,String看不出效果,自定义对象和图形对象能有效果? 
在印象中无论是什么,这里保留的都是一个引用,应该是一样大的。所以应该没有对象和图形的差别。而且第一种做法不会产生垃圾对象,只会出现大量的引用。一个引用占用的内存是很小的,不会是大量的。但是如果循环次数很多,也是可观的,所以我平时也是写在循环体之外。 
2,如果写在外边,其实并不一定就快,因为在里边写的话过了循环体就过了它的有效范围,可以被回收了,虽然并不一定立即回收,但如果第二种写法对象则不能回收。恰恰相反,如果对象很大,例如图形控件,活着保存大量数据的Bean,这个时候这个对象要到函数结束才会被回收,如果函数体很长,活着函数的执行时间很长,那么这个才是更消耗内存的。所以说哪种写法要看情况而定。 
3,如何判定一个程序的好坏?这个是个综合问题,要考虑很多因素,但是在印象种无论如何方法二应该是比方法一快的,结果刚好相反,开启这个帖子主要是为了这个问题。就是为什么会这样?而不是讨论哪个方法更好。 
  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题) 2007-02-11 17:17 
java字节码中,对于每一个方法都有一个max_locals属性,指出方法的局部变量所需要的存储空间(以字为单位)。 
对于一楼的例子,如果把 
String str = // 
移到循环体外,则max_locals会比在循环体内更大。  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题)[未登录] 2007-02-11 17:35 
to:sinoly 
看了你写的关于对象创建的问题,我们说的不是一个问题啊。 
你的问题是:在循环体种创建相同的对象,就是作用一样的对象,这个当然是浪费内存了。这种问题不需要再讨论。 
我的问题是这样的,并不再创建对象上,例如,如下问题,从List种取出对象,可以有两种写法, 
List<Object> list= //...一个已经存在的List 
方法一 
for(int i=0;i<list.size();i++){ 
Object obj = list.get(i); 
} 
方法二 
Object obj= null; 
for(int i=0;i<list.size();i++){ 
obj= list.get(i); 
} 
这个里边根本没有创建对象的问题,有的问题是方法一会多很多引用,方法二会让一个引用保存期很长,同时对象有效期也变的很长。(其实你的性能优化的文章种应该指出这个问题的。) 
另外提示一下,对于我第一个例子中写的: 
for (int i=0;i<n;i++){ 
String str = ""; 
} 
在这个函数中只会创建一个对象,因为String是非可变对象,虚拟机会自动重用,这个你可以参照一下JLS中的解释。只有这种情况才是浪费 
for (int i=0;i<n;i++){ 
String str = new String(""); 
} 
最后感谢你参与,另外提一点建议: 
1,回文或者写文章前应该先确认一下自己的观点是否是对的,最好给出证明,虽然确认了也不能保证一定是对的,但是至少做过了,这是一种态度。我也是一直这么要求自己,无论多么简单的问题,都给出一个思考的过程,因为这样对看文章的人有帮助。例如你上边说到的,如果是自定义对象活着图形对象的观点,刚好是错误的证明,你可以这样试试。虚拟机内存设置64M,如果你在List中取出一个60M的对象,然后在循环之后再new一个10M的对象,方法一是可以运行的,虽然说慢,但方法二就OutOfMemory了。除非你在循环之后设置 
变量= null,但这种做法是否更好只得商榷. 
最后说明一点,应用开发和底层框架开发其实有很多东西是不同的。如果实际负责过项目就了解的。  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题) 2007-02-11 17:41 
@daydream 
谢谢再次回复,想问一下关于max_locals这个属性, 
1,为什么放到循环体外反而会更大呢?能给简单讲一下为什么吗?活着给一个能查到原因的方向。 
2,另外这个max_locals变大后为什么会影响性能呢?在什么时候会使用到max_locals这个属性呢? 
谢谢 
  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题) 2007-02-11 18:40 
对下面2个方法,foo1需要的max_locals是4,foo2是5, 
foo1需要的4大概是:this变量占1个字、方法参数x占1个字、 
第一段循环的时候,变量i占一个字、s1占一个字,第二个循环的时候,i、s1已经超出作用域,所以,变量j、s2占用了和i、s1重叠的空间,所以最多需要4个字就够了。 
foo2方法中s1的作用域直到方法结束,所以需要5个字长度。 
max_locals变大后应该不会影响到性能,但是我这儿的意思是说,将局部变量放在循环体内声明并不会导致性能下降。 
void foo1(int x) { 
for (int i = 0; i < 1000; i++) { 
String s1 = "..."; 
} 
for (int j = 0; j < 1000; j++) { 
String s2 = "...."; 
} 
} 
void foo2(int x) { 
String s1 = "..."; 
for (int i = 0; i < 1000; i++) { 
// other code..... 
} 
for (int j = 0; j < 1000; j++) { 
String s2 = "...."; 
} 
} 
  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题) 2007-02-11 18:44 
另外,将局部变量放在循环体内声明,也不会导致多出来很多引用。 
因为,局部变量对应于java栈的偏移是在编译时就确定的,并不是在运行期动态分配的。循环体内的局部变量对应的是同一个偏移位置。  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题) 2007-02-11 18:48 
另外,max_locals只是编译器在编译时确定,存放在字节码中,供JVM在运行期调用方法时分配java栈帧大小用的,对于程序员应该没什么用,因为程序也没办法访问java栈。  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题) 2007-02-11 19:07 
现在大概明白了max_locals的作用,但是我这测试出的问题还在。不知道为什么第一个方法要比第二个方法快。  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题) 2007-02-11 19:10 
另外我看你上边写的用我的代码,测试结果是接近的,可我怎么测都是差距一秒啊,你的运行环境 ? 
我是xp下,试过eclipse运行,试过直接控制台用java命令运行。 
jdk1.5  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题) 2007-02-11 19:37 
运行你的第二段测试代码: 
先运行test1共5次,结果:3406、3453、3391、3453、3391 
然后把代码改成test2,运行5次,结果: 
3406、3406、3406、3437、3391 
运行环境:XP、512M内存、Eclipse3.2下,JDK6.0,没有加启动参数,默认最大内存好像是64M。  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题)[未登录] 2007-02-12 06:00 
郁闷了,我的两台电脑,测试的结果都是稳定的差不到1秒 
xp sp2 eclipse3.2.1 jdk1.5 启动参数也没加,最大内存开始是512,后来改成256和64都试了。 
结果test1:2938 2953 2954 2954 2969 
test2:3797 3797 3781 4000 3797 3797 
我今天再找别人试一下,看看什么情况。  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题)[未登录] 2007-02-12 09:56 
又找了几个同事帮忙测试了一下,果然,有的差距是100毫秒,有的200,有的基本没差距。我公司的电脑差距是200,也就是说我家里的测试不准确,真实不可思议,我在家测了很多次,都稳定在差距1000毫秒,呵呵。不过问题总算解决了。 
另外经过测试发现在这个问题上,amd的cpu比intel的快,单核的比双核的快。。有意思。  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题) (问题结束)[未登录] 2007-02-12 18:13 
注意jdk版本,一些基本的东西随着JDK不断的更新,都会有改变。对JAVA没什么好印象!  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题) (问题结束) 2007-02-12 19:15 
有些是跟版本有关的,这个跟版本没关系,呵呵。 
为什么对java印象不好呢,每个语言都有它的好处和坏处。  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题) (问题结束) 2007-02-12 20:10 
刚才又做了个测试,一个很有意思的结果:使用ibm的ibm_sdk50测试结果刚好相反,执行10亿次,方法二比方法一快了200毫秒。挺有意思。 
不过结论是一样的,就是两种方法性能差别很小,但方法二让对象的有效期变长了,如果是大对象(例如图形对象,数据bean对象)则不好,所以一般情况下应选择方法一的写法。或者方法二的写法加上手动清空释放对象。  回复  更多评论  
# re: 开启一个新的问题(关于声明变量的性能问题) (问题结束)2007-02-13 09:02 
牛啊! 
俺可很少关注到这一层。俺通常来说会用第一种的。可以省下俺4300ms写代码的时间。  回复  更多评论