转载:http://www.ibm.com/developerworks/cn/java/buildhp/index.html
由于 Java 的可移植性、易用性和与 HTML的紧密结合, Java已成为动态网页内容的首选编程语言。 Java被用来产生网页上的动画效果,在服务器端动态的选择、格式化网页内容,并用作面向交易应用软件的前端来检查终端用户的输入信息。 Java 还是作为在虚拟现实模型(VRML)[HW96]语言中设计三维动作和在这种虚拟环境[VRML97]中提供多用户交互的重要语言。
<!----><!----><!---->
Java除了以上提及功用以外的,还很少被用来开发需要高性能和高吞吐量的应用程序与服务器。开发人员很少在这些情况下选择 Java的主要原因是由 Java代码编译出来的程序的运行速度无法与传统的编译语言(如C或 C++)相比。 大规模的程序中的垃圾收集和JIT (Just-In-Time)编译方面的局限性也造成开发人员对 Java性能的怀疑。
我们结合了精心的程序设计和有针对性的优化,目前已克服了许多这些方面的不足。本文介绍了利用 Java开 发高吞吐量的应用程序和服务器,并说明了 Java应用程序能够及其方便的得到优化以达到用传统语言,如C和 C++,编写的代码的性能。这些结果都是建立在我们使用 Java开发InVerse (Interactive Universer) 服务器所获得的经验之上。
我们的测试环境是一部拥有48MB内存、运行Windows 95的奔腾150MHz计算机,使用由Sun公司提供的Java 1.1.4编译器和虚拟机,并带-O标志进行优化编译。计时是通过在一紧缩循环(tight loop)中执行目标代码会和调 用 System.currentTimeMillis() 捕获计时信息而得到的。我们还利用 Just-In-Time (JIT) 代码生成器 ( 在运行阶段将 Java byte 代码转换为本地指令 ) 来进行测试。
其它的章节介绍了 Java 应用程序开发者会遇到的一些主要的性能问题,以及优化这些问题的方法:内存访问方式、额外同步化、额外对象创建和垃圾收集、额外错误处理和对一般类库的依赖性。虽然这些内容很多都适应与其它的面向对象编程语言,但与 Java开发者有着密切的关系。文章的结尾介绍了 InVerse 服务器应用程序的整体性能将如何通过优化这些方面得到改进,并讨论了这一领域的今后工作。
在当前处理器速度与内存访问时间差距越来越大的情况下,高性能应用程序的开发者们越来越清醒的认识到最小化内存访问时间的重要性。在诸如C++之类的编译型语言来说,因为内存地址是预先算出的,因此对不同的变量来说内存访问时间几乎是相同的。最坏的情况也只是经过一次间接寻址。但对Java来说,应用程序的效率则相当程度上要受到其访问的变量的类型以及访问方式的影响。例如,栈变量可以直接寻址,而实例变量则一般需要经过额外的间接寻址过程。
读 | 写 | |
无 | JIT | |
本地方法变量 对象的实例变量 父类实例变量 超父类实例变量 类的静态变量 |
1.00 1.92 1.96 1.83 1.23 |
1.00 2.14 2.14 2.14 1.52 |
有 | JIT | |
本地方法变量 对象的实例变量 父类实例变量 超父类实例变量 类的静态变量 |
1.00 1.64 1.73 1.73 1.23 |
1.00 1.99 2.14 2.21 1.62 |
表 1: 不同变量类型及访问模式的访问时间
|
在Java中,一个方法( method)或者代码块可能用 synchronized 这个关键字标注。在同一个类的实例中,任何一个同步的方法都不能执行多线程的同时操作,而对于一个作为参数的对象,一个同步的代码段被视为一个同步的方法。为了支持这种能力, Java虚拟机在每一个拥有同步方法的对象上连接了一个监视器(monitor)。当有线程进入一个标为同步的代码段时,它都需要先从该监视器处获得一个锁定标记
表2:是否同步及有无线程抢占的不同组合下的方法调用时间(微秒)
非同步. | 同步. | |
Without | JIT | |
无线程抢占线程抢占 | 0.54 0.55 |
3.96 8.87 |
With | JIT | |
无线程抢占线程抢占 | 0.50 0.57 |
5.55 11.00 |
如表2所示,同步的方法能使应用程序的运行减慢20%左右。表中显示的是在不同的组合下调用一个空方法所用的时间。在有线程抢占的情况下,两个线程在一个紧密的循环中同时访问了这个对象的方法。
表中的数据揭示了这样一个现象:在程序中添加同步标记使性能下降8%--11%。这还是在单线程的情况下(即监视器从不打断该线程)。由此可见,同步的方法耗时并非仅仅是获得锁定标记,更多的时间耗费在检查锁定标记上。
还应该注意,JIT编译器并不能增强对锁定标记的访问和抢占损耗。这是因为它并没有获得关于该应用结构的信息,事实上,因为JIT编译器的损耗,程序的运行反而减慢了。与此相反,在非同步调用的情况下,它可以稍稍增强程序的效率。
同步对程序效率的影响是微妙的,例如,当内存分配同步时,就意味着创建一个对象会带来效率的降低。
Java类库中的许多通用函数是设计为线程安全的,因此也就是同步的。例如,在向量中访问一个索引的元素就需要调用一个同步的方法。就象在关联的枚举类型中调用nextItem() 一样。以我们的经验,许多集合都只有一个线程去访问,因此这样做显得因小失大。为了让我们的应用程序能够有选择的屏蔽同步,我们创建了象Java Vector的子类来提供非同步的访问方法。在I/O stream库中情况也是类似的。在使用DataOutputStream读写格式化的数据时86%的损耗来自对同步的OutputStream方法的调用。在这里我们不能取消同步,但有时能用在一个同步的代码块中加入多个同步方法调用的方法提高效率
例如将
for (i=0; i x.f(); // synchronized on x |
改写成
synchronized(x) { for (i=0; i x.f(); // synchronized on x } |
替换的代码允许Java虚拟机的同步方法“短路”。这种方法只是在同步方法持续时间很短时使用。否则将阻塞其他线程。或者在不存在线程抢占时使用。但是,Java虚拟机对锁定标记的回写效率会造成差别很大的结果。
|
有了Java的内置垃圾收集机制,程序员再也不需要顾虑他们的应用所需的内存是如何分配以及何时释放的了。尽管这种机制简化了OO开发的任务,但它也带来了明显的效率问题。程序员喜欢随意创建对象,因为他们误以为这样做很“便宜”,而事实上,Java虚拟机要他们为对象内存分配和释放付出代价。对象的创建和销毁在Java中是及其昂贵的。表3显示了不同的对象创建操作所需时间和从一个同步的数组中抽取条目(就象是在一个自由的对象池中所做的那样)所需时间的比较。该表揭示了对象的创建要比重用已存在的对象昂贵得多。差别有1/10左右。这是因为,从我们的经验来看,许多临时类在程序中并不需要同步的点上被实例化了。
无 有 JIT JIT 创建对象 (无构造函数 ) 创建对象 (有构造函数) 创建子类 (无构造函数) 创建子类 (基类构造函数) 12.57 12.97 12.79 12.80 12.36 13.45 12.47 12.53 (同步) 从数组中去除元素 4.06 3.84 | Table 3: 创建Java对象的时间 (微秒) |
在运算密集的应用中,如服务器,对象的创建有着明显的影响。垃圾收集器应当仅仅在没有其他线程运行时执行,以此来使它对应用效率的影响减到最低。但是,一个高性能的应用也许并不会造成这样的等待孔隙,因此垃圾收集器也在应用执行时“窃取”时间。
为了减少创建和销毁的对象,我们使用了如下两种技巧:
当一个对象重复被分配时(例如在一个特定的方法中),在这个方法的循环之间声明一个静态变量来储存这个对象。在方法中对储存的对象调用reinitialize()方法来初始化。当一个对象同时存在多个活跃的实例,并且它们的生命周期不能被当做应用中的一个单一部分时,使用一个矢量或者一个可扩充的数组来当作一个自由实例库。当一个对象不再需要时,则用一个静态方法将其加入自由对象池。相似的,当要获得一个新的类实例时,则调用另一个静态方法从库中释放一个现有的实例并对它调用reinitialize()方法。将它的构造函数声明为private以放应用不慎对它直接分配一个实例。
两种方法都依赖于在最优化的类中的reinitialize()方法。偶尔我们也需要在不支持该技巧的Java类中构造子类来创建这种支持
|
Java虚拟机提供了对例外处理的本地支持。而C++则与此不同,它依靠编译器产生的指令来存储和处理例外信息。Java这些内置的指令使得Java的例外处理速度较快。如表4所示,加入try-catch从句所引起的时间消耗是微不足道的。真正出现例外时处理所需的时间虽然不小,但这种成本只是会偶然出现。
无 JIT JIT 函数调用 try-catch中的函数调用 (无例外抛出) try-catch中的函数调用 (有例外抛出) 0.2 0.2 191.7 0.2 0.2 186.2 If-test array index bounds Array dereference Array dereference throwing bounds exception 0.5 0.4 1613.2 0.5 0.4 1548.9 If-test using instanceof Object type cast Object type cast throwing casting exception 0.8 0.7 1581.9 0.7 0.7 1469.8 | 表4: Java例外处理耗时表 (微秒) |
try-catch从句接近于零的时间消耗告诉我们,在错误不多的情况下我们应该依赖于例外处理而不是进行明确的错误检查。例如如下的例子:
if ((idx >=0) && (idx < array.length)) x = array[idx]; else // error |
try { x = array[idx] } catch (ArrayOutOfBoundsException e) { // error } |
普通情况下(索引在取值范围内),面向例外处理的程序会比普通方法快50%左右。只要通常情况多于例外情况,应用的效率就有所提高。这在循环的情况下显得尤其正确。在进行强制类型转换时情况也类似,使用instanceof不如捕捉ClassCastException效率高。
try-catch让我们重新定义在高性能应用中例外的使用。除了简单地指示错误之外,例外还用于指示所有非通常的情况,即便这些非通常情况有时并非错误。这种方法[C96]与 "making the common case fast."的方法通用。
|
标准Java类库支持很多类型的应用,从简单的网页Applet到较为复杂的系统。但是就象所有的软件一样,这个通用类库并不能对某些特定应用提供最佳性能的支持。认识到可以通过改进JDK的通用构架来提高性能对应用程序员是大有好处的。在对应用的存取形式和类之间交互的了解的基础上对某些类进行优化则可以获得性能的提高。
Java类库中某些方法的优化要牺牲其他方法的性能。设计者要根据应用对这些方法的使用情况进行取舍。如果该应用主要调用未被优化的方法,则应该改变类的实现来提高性能。例如,我们写了一个用插入的元素连接到list的Hashtable以此来支持高速串行存取。
象其他的类库一样,JDK严重依赖于它的界面。这种方法增强了模块化,但牺牲了一些总体性能。例如,因为类之间只能通过通用界面互动,它们就不能利用在实现上的彼此相似之处以提高性能。当特定的类通过这样的界面互动时,可以通过改变互动点的设定的方法来提高效能。例如,我们合并了ObjectOutputStream和ByteArrayOutputStream 以提供快速 非同步的输出到byte数组。
|
在我们开发高性能服务器的过程中我们发现了数种针对Java虚拟机特性的优化方法:
表5:对InVerse性能的优化 (Packets/Second Processing Throughput) and Comparison to Zero-Processing Capabilities
Before | After | |
InVerse pkts/sec processed: 1 User 10 Users 100 Users |
||
InVerse pkts/sec processed: 1 User 10 Users 100 Users |
InVerse pkts/sec processed: 145 99 N/A |
InVerse pkts/sec processed: 810 505 N147 |
Base limits (Java)43 1 User 10 Users 100 Users |
InVerse pkts/sec processed: 930 93 9 |
|
Base limits (C) 1 User 10 Users 100 Users |
1080 1080 11 |
在此向James Clinton先生对我们上一版本的反馈表示感谢。