对Java HotSpotTM 性能引擎的深入研究
1. 序言
Java HotSpotTM 性能引擎正式发布于1999年4月27日。它远远不只是一个性能调整引擎,而是一个实际意义上的Java虚拟机(VM),它可以自始至终地发挥最高的性能--常常使服务器端基于Java技术的应用程序的运行速度提高两倍。
图1. SPECjvm98(系统性能评定委员会--译者注)对运行于Windows NT 350Mhz上的Java HotSpot VM的评定结果(援引自:Sun Microsystems)
图2. VolanoMark 对运行于Windows NT 上的Java HotSpot VM的评定结果(援引自:Sun Microsystems)
图3. VolanoMark 对运行于SolarisTM Platform (SPARC Platform Edition)上的Java HotSpot VM的评定结果(援引自:Sun Microsystems)
使用Java编程语言的开发人员都知道,虚拟机居于Java应用程序和底层硬件平台之间,它可以用来执行应用程序字节码、管理系统内存、提供系统安全性及判别处理多重线程的执行等。
利用Java 2平台的新的可插入式体系结构,Java HotSpot可被无缝地植入该平台,以代替传统的虚拟机和Just-In-Time(JIT)编译器。一旦这个新的性能引擎被安装完毕,则任何在这个Java 2 运行环境(应用程序启动器、插件程序或applet浏览器)中进行处理的应用程序或applet,都将默认地使用Java HotSpot性能引擎。
用Java HotSpot编译小组经理Davis Stoutamire的话来讲,本文深入地钻研了Java HotSpot性能引擎的功能性,探索了它是如何做和做了什么的问题--它在什么地方"尖叫(screams)",在什么地方又仅仅是"低鸣(purrs)"--并附有可以演示该引擎的内部工作状况的代码示例。
2. 基础
Java HotSpot性能引擎重点突破了几个关键性的技术,从而获得了它杰出的性能:
? "运行中适配"编译
? 方法内置
? 改进和重新设计的对象布局
? 快速且完全精确的垃圾回收
? 超快速线程同步
这样的技术改进对服务器端应用程序是最有效的。"这是一种调整性能的途径",David Stoutamire解释说,"运行应用程序的时间越长,涉及执行的Java字节码越多,则你从中获得的益处也就越大。如果一个应用程序要在屏幕上表现跳跃的球--它可能使用加强图形功能的C代码或系统调用,并且它可能仅在某人点击链接后才执行--则Java HotSpot VM将不能施展其'才华'"。
用Java编程语言编写的应用程序一般依赖于四个因素:
? 应用程序的总体设计
? 执行Java字节码的速度
? 库执行(用本地代码)的速度
? 底层硬件和操作系统的速度
典型的客户端应用程序的性能(特别是图形应用程序)主要受本地库的影响,而典型的服务器端应用程序注重字节码的执行速度--这正是新的VM的闪光点。
图4.一个虚拟机将它的时间用在了哪些地方?(援引自:Sun Microsystems)
但是,根据Stoutamire的说法,将来发布的引擎,将特别针对于提高客户端应用程序的性能。
3. 适配性编译
多数应用程序都将它们的大部分时间用在了执行它们的一小部分代码上。Java HotSpot性能引擎可对一个运行时的应用程序进行分析,并确认对性能最关键的区域--这里,最大量的时间被用在执行这些字节码上。该性能引擎不是一开始就编译全部程序,或编译每个被调用的方法(就象JIT所做的那样),而是首先使用一个解释器来运行程序,然后在程序运行时对其进行分析,寻找性能"热点(hot spot)";而后则仅对性能关键性区域的代码进行编译和优化。这个监视过程动态地贯穿程序的整个生命之中,伴之以性能引擎"在运行中"适配应用程序的性能需求。
Java HotSpot适配性的编译技术使它远远超过了Just-In-Time编译器。使用JIT编译器时,由于20%的代码可能会占用大部分执行时间,因此在运行时对另外80%代码的优化并不总是有用的--在速度上的可能的增长不一定能够补偿为优化所付出的代价。
Java HotSpot性能引擎的动态优化方法带来如下益处:
? 通常,程序启动得更快。因为,与JIT编译器相比,使用Java HotSpot性能引擎所执行的预先编译更少。
? 编译随着时间的推移而分布,从而可使编译暂停更短,且更不易被用户发觉。
? 仅编译性能关键性代码的做法"购买了时间",从而可将这些时间用在执行更好的优化上。
? 由于只编译程序的较少部分,所以编译器为编译代码使用的内存也较少。
? 由于在编译代码前等待的时间较长,所以可收集更多的信息以执行更好的优化(如内联)。
4. 方法内联
"在运行中"的动态编译只是Java HotSpot性能引擎所进行的优化的开始。"假设我有一个可以完成一些琐事的方法,比如将一项内容添加到一个参数中,然后再将其返回。"Stoutamire说道,"在这种情况下,编译器可能也只是生成代码--而不是实际调用该方法--以将一项内容直接添加到该变量中。用这种方法,可以节省跳转到那个方法的指令的开销,同样也减少返回指令的开销。"
但是这样的"方法内联",在面向对象的代码设计环境中,是有问题的。"经常的情况是,你要跳转的地址是难以用指令编码的,"Stoutamire说,"但也不总是这样。在某些实例中,采用动态调度 (dynamic dispatch)或虚拟方法调用,你可以获得一个运行时指针,该指针可被用来调用一套不同方法之中的某一个方法。"
这种动态调度的概念是Java编程语言的核心--它是指,一个子类可以重载一个已经存在的方法,然后在运行时,这个特有的方法自动终止被调用。"内联的问题是,"Stoutamire继续讲到,"你不能在动态调度中内联。其原因是你从来不可能真正确保你要调用什么样的方法。因此,你不可能将该方法的执行主体带到该调用中。其原因是Java允许你在任何时间装载新的类。这样用一个新的方法,可能引入一个新的类--如果在这种引入之前已有内联发生,则突然之间,所有代码可能成为不正确的代码。
Java HotSpot VM通过动态逆优化(deoptimization)轻易地解决了这个问题。"逆优化是在任意点能够将编译后的代码还原为解释代码的能力",Stoutamire解释到。"实质上,它是具有将编译的栈框架还原为解释的栈框架的能力。解释器具有它自己的对正在执行的方法的表示法。如果你有一个用编译代码表示的在一个正在执行的方法中的本地变量,则它可能在栈的什么地方,或在注册器的什么地方进行表示。但不强迫解释器也具有那样的相同布局。因此,你必须能够接纳所有东西并对它们进行重新分组,使它在解释器能够继续进行之前,看似象是一个解释框架。这是使Java HotSpot性能引擎与其它虚拟机不同的原因之一。"
正象其名称所表示的那样,动态逆优化是在一个特定的程序中的一个进行中的过程。"我们假设你具有一个运行中的程序,"Stoutamire说道,"该程序的给定的性能热点已被编译。在这个编译过程中,编译器利用只有一个单独子类这样一个事实,可以做方法内联。但是没过多久,一个新的类被动态装载,它中断了现存的编译代码。因此,Java HotSpot VM解除了现存的编译,并用解释器重新启动代码;或者,如果它继续是一个性能热点,则用编译器对其进行再次编译,并重新启动它。"
方法内联示例
5. 对象布局
在Java HotSpot虚拟机中的新的、经过改进的对象布局,不是象多数其它Java虚拟机那样,含有三个机器字标题,而是一个具有两个机器字的对象标题。第一个标题字是一个有关对象类的引用;第二个标题字含有其它信息,如身份识别哈希码、垃圾回收状态信息等。只有数组才有第三个标题字段,它是为数组的尺寸大小而设立的。由于Java编程语言的对象的平均尺寸较小,所以上述这种机器字的节省对内存的消耗具有积极的影响(大约节省8%的堆尺寸)。
Java HotSpot VM还消除了"句柄"的概念--一种用来访问内存中的对象的间接工具。这既减少了内存的使用,也加快了处理速度。"传统的对象表示法是,当你具有一个对象,该对象指向另一个对象时",Stoutamire说道,"它将具有一个指向那个"另一个"对象的标题的指针。但是,传统的虚拟机则使用完全不同的对象表示法。它不是直接指向对象,而是指向一个表。对象的标题就位于这个表中,并且指向对象的字段的所在位置的指针也在该表中。"
这种间接表示法对在内存中重新定位对象来说,是特别有用的。它经常在垃圾回收的过程中发挥作用(见下)。"如果对象A想要指向对象B",Stoutamire解释到,"实际上要做的是指向句柄表。假设对象A指向表中的第四个表目(entry),且表中的第四个表目具有指向对象B的指针。现在,当我要移动对象B时,我要为对象A所要做的一切就是更新表中的表目。如果我只有一个对象指向对象B,则这样做的意义不大;但是,如果我具有1000个对象指向对象B(或者我不能确定所有指针的位置),则这时如果要移动对象B,则仅更新那个单一的表目就可以了。"
虽然句柄的使用带来了处理上的更大简易性,但这种间接法是非常慢的,而且表本身也占据了较多的内存空间。进一步讲,句柄表存在的主要原因--在垃圾回收过程中便于对象的重新定位--后来也显得不是那么必要了。
"实际上,在对象重新定位过程中,直接更新所有指针,其开销也大不了多少。"Stoutamire说道,"为了做好垃圾回收,你就不得不总是跟踪全部的堆(heap)--你必须检查每一个指针。所以,你要访问每一个指针这样一种事实,意味着你有机会可以当即改变它,而不需要做更多的额外工作。"
最后,因为Java HotSpot VM使用直接内存引用,所以在分配内存时,它不需要建立内存引用句柄;并且除管理对象内存外,它不必管理句柄。这就使得临时数据结构的分配就象C的基于栈的内存分配一样快。这是一项极大的成功。
6. 垃圾回收
Java编程语言是提供自动内置垃圾回收功能的第一种主流语言。
6.1 在Java HotSpot VM之前的垃圾回收
许多Java虚拟机使用"保守的"或部分精确的垃圾回收器。保守的收集器假定那些看似有效指针的事物,可能实际上就是一个指针。保守的收集器便于实现,但它不能总是确切地了解内存中的所有的对象引用的位置。其结果是,尽管很少,有时可能出现错误--例如将整数误认为是对象指针--这会导致难于调试发现的内存泄露。
另外,一个保守的收集器必须使用句柄来间接引用对象,或者避免重新定位对象;因为重新定位无句柄对象要求更新所有对象的引用,而保守收集器甚至不能确定一个显式引用是否是事实上真实的。这种在重新定位对象上的无能,转而导致内存出现碎片,并阻碍使用更复杂的垃圾回收算法。
保守的垃圾回收对本地方法还具有一定的负面影响。"一个保守的垃圾回收器必须确保,它没有移动任何由Java代码之外程序所指的内容",Stoutamire解释道,"这意味着你必须扫描内存区,寻找指向你的堆的那些指针--这一切都会占用时间。"这就是使用Java的旧的本地方法接口(NMI)规范时所面临的问题之一。耐人寻味的是,这个问题可通过使用不同的句柄来加以解决。
"使用Java 1.1平台时,"Stoutamire说道,"NMI被Java本地接口(JNI)所替代,你决不能从外部直接指向Java对象,你只能指向句柄,然后它再指向对象。这意味着,垃圾回收器不必考虑外部过程。但是,句柄只在本地代码想要指向VM时才被使用。"他继续说道,"这与过去所采用的方法的效率几乎一样,且垃圾回收的效率更高。因此,如果你要使用Java 2 平台及其Java HotSpot VM,你必须要使用JNI。"
6.2 在Java HotSpot VM之后的垃圾回收
象Java HotSpot性能引擎的其它部分一样,垃圾回收的实现也从根本上进行了重新设计。Java HotSpot VM垃圾回收器是"全精确的",它可以提供以下保证:
? 所有不可访问的对象内存都可以可靠地回收;
? 所有对象可被重新定位,允许内存压缩,从而消除了对象内存碎片并增加了内存的本地性。
在Java HotSpot VM垃圾回收中,还有一些先进的特性。"为了实现精确的垃圾回收,"Stoutamire说道,"你必须跟踪全部堆--因为,如果你不跟踪每一个指针,那么它可能指向某个激活的事物,而你却错误地将其收集--这是很糟糕的事情。另一方面,如果跟踪全部堆被严格执行,则垃圾回收的速度会随着堆的增长而变得越来越慢。"
6.3 相继的复制回收
避免这种可能性的一个途径是使用Java HotSpot的先进的相继的复制回收算法。"相继的复制回收利用了多数对象并不真的存活那样长这样一个事实。"Stoutamire说道,"这里,主要设立两个堆--一个是为旧的对象设立的,一个是为新的对象设立的。系统将把对新的对象的引用和对旧的对象的引用,记录在另外一个表中。"
使用相继的复制回收时,大部分对象(一般要多于95%)可通过对新对象空间所做的许多小的"废物利用"(有时也被称作"保育院")而被直接回收。长寿命的对象最终被拷贝(或"占用")返回顶部】
【打印本页】
【关闭窗口】
|