2009年5月16日星期六

blogspot在中国大陆再次被封

blogspot在中国大陆再次被封,气愤!!!!
blogspot is blocked in china.

这个博客即将关闭,博客另寻新址。
I will leave blogspot soon and stop refresh my blog here....

bye.

2009年5月10日星期日

Java性能优化之实时性[3]

——万事开头难


如果你的应用程序能够满足内存和速度的要求,有时还是不够的,对于某些应用程序来说,尤其是实时系统,它还必须满足良好的用户体验,这就要求你的程序能够做到好的实时性。

关于用户体验,有很多心理学的研究。如果你了解用户懒惰和缺乏耐心等心理,你也许会更加明白,实时性有时会如此重要。相关的文章比如网站打开速度的心理学人之初,性本懒 等等。但是实时性的要求也不局限于用户体验,有许多被称之为 RT 应用的程序要求必须严格地满足实时同步需求,比如控制飞机方向的应用程序不能够有任何原因的延迟,否则将导致灾难性的后果。

由于很多重要原因,Java 语言在实时系统中的应用非常有限,导致Java写出来的应用程序有时实时性很差。这些原因包括 Java 语言设计中固有的不确定性性能影响,例如动态类加载,以及 Java 运行时环境(Java Runtime Environment,JRE)本身的不确定性性能影响,例如垃圾收集器和本地代码编译。

当然,为了解决这些问题,使得Java能够用来构建实时系统,一些规范应运而生,比如RTSJ。关于实时系统和RTSJ,可以参照文章:实时 Java: 使用 Java 语言编写实时系统或者这里的转载。我这里不再赘述。

我主要讲一下我碰到的一个例子: 在某些Swing应用程序中发现,做某些操作时,第一次总是比较慢,以后就好了,有时候时间相差一个数量级,导致使用起来达不到正常的用户RT需求,用户就不高兴了,这正是没有满足用户缺乏耐心的需求,可以说是软障碍。为什么第一次总是比较慢呢,第一次新建慢,第一次编辑慢,第一次弹出某个对话框也慢?这真是奇怪。我之所以出现这样的困惑,其实是因为那时候我还不理解类加载和本地代码编译的具体细节。

在运行时,当我们想生成这个类的对象时,JVM首先检查这个类的Class对象是否已经加载。如果尚未加载,JVM就会根据类名查找.class文件,并将其载入内存。一个与 Java 一致的 JVM 必须延迟加载类,直到程序第一次引用该类。根据被加载类所在的介质(磁盘或其他)的速度、类的大小、类加载器本身的开销,类加载的时间有所不同。加载类的延迟通常高达 10 毫秒。如果需要加载几十或几百个类,则加载时间本身就会引起很长时间的意外延迟。仔细地设计应用程序,使应用程序在启动时加载所有的类,但是这必须手动完成,因为 Java 语言规范不让 JVM 提前执行这一步。

再让我们来看看编译:将 Java 代码编译为本地代码引发了与类加载类似的问题。大多数现代 JVM 开始先解释 Java 方法,然后仅将频繁执行的方法编译成本地代码。延迟编译促成了快速启动,并减少了应用程序运行期间执行的编译数量。但是使用解释后的代码执行任务和使用编译后的代码执行任务在时间上有巨大的差异。对于硬 RT 应用程序来说,由于无法预测何时发生编译,将导致很大程度的不确定性,从而无法有效地规划应用程序的行为。对于类加载,通过在应用程序启动阶段使用 Compiler 类以编程的方式编译方法可以减轻这一问题,但是维护这样的方法非常乏味并且容易发生错误。

一旦找到的问题的原因,那么解决问题的方法很简单,可以在某个合适的空闲的时候,提前进行类加载和编译,那么当第一次进行一些实时操作时,所要用到的类已经加载或者相关代码已经编译,第一次操作便不再慢了。

Java性能优化之实战算法[2]

——当你拿着一把锤子的时候,所有的东西看起来都像钉子。
——当你看到钉子的时候,手上拿着的东西看起来就像一把锤子。


性能优化有时候很像玩一个策略游戏(免费),对手不是计算机,程序员,设计架构,编译器,控制流,而是时间,内存,竞争条件,预算限制等等。有三个资源限制几乎所有应用都会面临:1.CPU速度和有效性2.系统内存3.磁盘I/O。

性能分析的关键是找到瓶颈所在,符合二八原理的是,计算机通常会将80%的时间花在20%的程序上,所以找出这20%的瓶颈很重要,这就是我上篇中所说的,为什么在优化之前一定要分析,不要相当然。我从二八原理学到的就是要统筹分析,使做事效率最优化,这个优化跟我们标题中提到Java性能优化道理上都是一样的。即应该把80%的时间花在造成问题的80%原因上面。呵呵,绕口吧。

比如,我在分析的时候,就发现了这么一个瓶颈,一个微不足道的步骤却消耗了大部分CPU资源,某些情况下CPU花了它96%的时间来处理它。

简化模型,描述起来,就是:

一维数组,每个元素都有保持它是第几个的一个index, 从头到尾插入元素,已有的元素的index需要做出调整,以保持一致性(即:array[i].index = i)。按照我们现在的算法,是每插入一条,其后面的每个元素记录的索引都要做出相应调整。简单估计一下,假设输入规模是N,大概需要O(N^2)的时间,当只有两三万的时候还好,大到8万就显得慢了(是两万时的16倍)。

这个问题本质上是个算法问题,就是怎么样把O(N^2)降到O(N)或者更低的问题,这样便可以大大的提高CPU效率和速度。

开始之前,我想了两种方法:
方法一:绕过这个问题,想办法直接计算出某元素在插入后的值,或者等到多次插入后,一起调整。
方法二:直接解决这个问题,不用字段index保存这个问题,想办法借助类似C++的指针地址,和数组地址来计算index。

对于方法二,需要借助语言特性,因为Java是没有办法取到对象值实例的地址,估计C++也悬,这条途径的复杂度和陷阱太多了,所以很快被抛弃了。
思考方法一:其实对于插入记录整个这个过程,许多记录被调整了多次,其实中间的这些调整是不用关心的,我们只需要知道最后状态是在某个位置就行了。因此,我们想要直接计算插入后的位置,让每条记录只进行一次索引调整。

最后的解决办法就是,首先将插入的记录排序,即从前往后插入,然后要做到取消中间的这些调整,使每条记录只调整一次,对于每个记录来说,只要用一个临时变量来记录它的前面已经插入的多少条记录,从前往后调整,轮到某条记录时,便可以计算出位置索引index的值,保证初始状态和插入结束后状态的一致性。

这个问题看起来是不是似乎很简单?但是,实际情况中的问题要复杂的多,比如,有可能这个插入是在某个大的循环中进行的,也可能在插入的同时,程序需要对这个数组中的记录做操作,可能还是个二维数组等等。关键的是,你需要做出某些努力,重构简化模型,然后抽象出模型,找出问题的本质。

原来的做法,每插入一次就调整一次为的是保证插入过程中每个记录状态都要保持一致性,这个要求太高了,所以导致了O(N^2)的高代价。真正的要求没有那么高,我们只需要开始和结束两个状态的一致性就行了,而这只需要N(O)就可以做到,这个代价就低了。这样这个瓶颈就通过算法来解决了,可以看出算法在性能优化中还是很重要的。

2009年5月9日星期六

Java性能优化之实战漫谈[1]

——I can feel the need, the need of speed...
——提前优化是万恶之源

最近由于工作的原因,对Java的性能优化比较关注,其间也看到不少比较好的文章系列,比如Program-Think同学的Java性能优化系列。平常工作的时候,发现稍微有点好想法的时候,把自己独自一个人关在小屋子里集中精力思考,效果很不错,俗称“闭关”,大家也可以试一试。

谈到Java优化,我认为一切开始之前,最最重要的是找到一个合适的profile工具,这里有篇文章介绍的很详细:How to Fix Memory Leaks in Java,里面介绍了许多有用的profile工具,不用花太多时间,找到合适你自己的就行了,我用的是jprofile,感觉还不错。记住,重要的是,在每个优化开始之前,一定要用profile工具分析性能的问题出在哪边,千万不要想当然。

今天就先将两个简单的例子作为开头吧,大家轻松轻松。
例子一:
class A {
int a_num;
B b;
}
class B{
int b_num;
A a;
}

如果改成下面这样怎么样?

class AB {
int a_num;
int b_num;
}
某些Java应用程序当对象太多而垃圾回收器还没有来得及回收的时候,就可能会导致堆内存溢出,其实内存溢出是分为栈内存溢出和堆内存溢出的,关于堆和栈的区别,可以参考program-think的这篇文章,讲的通俗易懂。关于上面的这个例子,就是我在实际编码中发现的一个造成内存消耗严重的一个问题,你或许还会认为上面那种写法更技术,更××。如果A的实例和B的实例个数很少的时候还好,但是如果他们的实例个数达到上百万的级别的时候,你就会思考一下是不是要换成下面这种写法了。首先你要了解怎么去计算一个对象会占多少字节的内存,然后思考为什么下面的写法会比上面的占用更少的内存?

例子二:
如果你用Java写程序,那么你会怎么样把一个数组objct_array:Object[]加到一个list:java.util.List中去?
我并不清楚你是怎么写的,不过我却见过一种非常常用的写法:

list.addAll(java.util.Arrays.asList(objct_array));
其实,如果不是在内核运算的地方,或者某个对性能要求很高的地方写这句话完全没有问题,但如果在某个关键的地方,比如在某个要调用极多次数的方法里写用这个方法,也许就会出现问题。在这种地方,最好还是不要偷懒,用最原始但很高效的数组来解决问题。

Object[] old_array = array;
array = new Object[old_array.length + new_array.length];
System.arraycopy(old_array, 0, array, 0, old_array.length);
System.arraycopy(new_array, 0, array, old_array.lengyh, new_array.length);
事实上:ce_list.addAll(java.util.Arrays.asList(ce_array));这句效率是非常慢的,它将
一个数组变成了一个list又变成一个数组又变成一个list,因为内部是这样实现的:
public static List asList(Object[] a) {
return new ArrayList(a); // 新建了一个对象,同时数组变成一个list.
}
public boolean addAll(Collection c) {
Object[] a = c.toArray(); // 看这里, list又变成了一个数组,绕了一圈啊
int numNew = a.length;
ensureCapacity(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew); // 这个跟修改后做的差不多
size += numNew;
return numNew != 0;
}
结果证明:修改后这部分消耗的临时内存没有了,速度也变快了。

关注者