在我们编程的过程中经常会遇到这样的问题。
for (int i=0;i<n;i++){
String str = //
}
String str = null;
for(int i=0;i<n;i++){
str = //
}
在印象中一直认为方法二的性能好于方法一,但是差距应该很小。但因为一位别人的回文说方法一极大的影响了性能,所以想写个例子证明一下相差很小。例子如下:
public class TestOt {
public static void main(String[] args) {
long n=1000000000;
long start = System.currentTimeMillis();
test2(n);
long end = System.currentTimeMillis();
System.out.println(end-start);
}
public static void test1(long n){
for (int i=0;i<n;i++){
String str = "";
}
}
public static void test2(long n){
String str = null;
for (int i=0;i<n;i++){
str = "";
}
}
} 测试的结果是当n=10亿次的时候差距是1秒,所以说差距应该是很小的,符合我原始的记忆,但是另一个问题来了,测试的结果是
方法一:3300毫秒左右
方法二:4300毫秒左右
结果刚好相反,于是更改方法
public class TestOt {
public static void main(String[] args) {
long n=1000000000;
long start = System.currentTimeMillis();
test1(n);
long end = System.currentTimeMillis();
System.out.println(end-start);
}
public static void test1(long n){
for (int i=0;i<n;i++){
String str = null;
}
}
public static void test2(long n){
String str = null;
for (int i=0;i<n;i++){
}
}
}
结果依旧。
没办法,取得字节码,对比
public class TestOt extends java.lang.Object{
public TestOt();
Code:
0: aload_0
1: invokespecial #8; //Method java/lang/Object."<init>":()V
4: return
public static void test1(int);
Code:
0: iconst_0
1: istore_1
2: goto 10
5: aconst_null
6: astore_2
7: iinc 1, 1
10: iload_1
11: iload_0
12: if_icmplt 5
15: return
public static void test2(int);
Code:
0: aconst_null
1: astore_1
2: iconst_0
3: istore_2
4: goto 10
7: iinc 2, 1
10: iload_2
11: iload_0
12: if_icmplt 7
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写代码的时间。 回复 更多评论