对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%)可通过对新对象空间所做的许多小的"废物利用"(有时也被称作"保育院")而被直接回收。长寿命的对象最终被拷贝(或"占用")到旧的空间区。"如果事物存活一定时间",Stoutamire说,"则它们或许想要存活更长一段时间。在拷贝它们之前,你希望给它们以成熟的机会,但是,一旦它们证明了它们自己没有消亡,你便可以拷贝它们。"
因为新的对象就象栈那样不断地被添加到"保育院"中,所以分配必须特别地快--因为它直接涉及更新单个指针及检查溢出的工作。当"保育院"满溢时,那里的大多数对象已经消亡。垃圾回收器则可以直接拷贝剩下的存活的对象--从而避免再做任何回收工作。
6.4 标记-整理回收
相继的复制回收技术在保育院区即可处理大多数消亡的对象。但是,长寿命的对象却最终蓄集于旧的对象区。在那里,受低内存条件的限制,或程序的要求,旧的对象的垃圾回收必须时有发生。Java HotSpot VM使用一种标准的标记-整理算法,通过从存活对象的根开始遍历整个存活对象树,来完成这一任务。"标记-整理检查内存,并标记所有可获得的对象,"Stoutamire说,"也就是那些不是垃圾的对象。"这样,任何由消亡的对象所遗留下来的间隙可被整理出来并予以回收。通过整理堆中的间隙--而不是将它们收集到一个自由区--可清除内存碎片,改进对旧的对象的分配(通过取消自由列表(freelist)搜索)及更有效地使用缓存。
6.5 递增"列车"回收
但是,相继/标记-整理垃圾回收算法,不能消除用户可感觉到的所有暂停。这样的暂停一般在旧对象收集过程中发生,且与被使用的存活对象数据量成比例。为满足"无暂停"垃圾回收的要求,这种新的虚拟机还提供了一种递增的或称作"列车"算法。递增收集选项(它可以在程序执行过程中,用 -Xincgc标志来选择)提供了相对较短的暂停时间,甚至在处理大的对象数据集时,也是如此。递增垃圾回收最适合于:
· 服务器应用程序,特别是高可用性的应用程序;
· 处理非常大的"活的"对象数据集的应用程序;
· 不希望有用户可感觉到的暂停的应用程序,如游戏、动画或交互式应用程序。
"列车算法是相继复制回收算法的一种复杂的变种,"Stoutamire说,"它不仅有一个新空间和一个旧空间,而是具有一个由许多小空间组成的中间空间。它试图使这些小空间尽可能的小,并将相互指向的对象组合在相同的空间里"。将紧密"耦合"的对象保存在相邻的内存区内,对处理不同对象数据集的多线程应用程序具有附加的益处。
列车算法将旧-空间垃圾回收的暂停,打碎为许多微小的暂停(大约为几毫秒)。它们随着时间分布开来,这样,对用户来说,暂停在实际上就感觉不到了。"如果你正在运行一个图形程序,"Stoutamire说道,"你要用鼠标拖动某个东西,这时,你不会希望看到突然的'打嗝',即暂停一下。这经常是一些用户在做垃圾回收时的经历--他们常常能够感到这种'结巴'。但是,列车算法可从实质上消除这种现象"。
总体来讲,各种垃圾回收的算法可协同工作,以完成Java HotSpot先进的垃圾回收。"第一阶段是保育院阶段,或有时被称作'伊甸园'",Stoutamire说,"多数对象在年轻时便死去了,于是,它们从未被移出伊甸园。在伊甸园中所使用的拷贝,在对象经常消亡的区域是最为有效的--因为这样你可以终止拷贝一些东西。"
假设递增垃圾回收是开启的,则对象将在下一步被移至由列车算法所管理的区域。并进而从那里被移到由标记-整理算法所管理的,有更多的长寿命对象在其中消亡的永久区域。
但是递增(列车)模式在没有开销的情况下(速度大约降低10%),是不开启的。"递增模式需要一定的开销,"Stoutamire确认道,"这就是它未被设定为默认开启的原因。因为这个版本的产品,定位于服务器,所以我们希望获得最高值的吞吐量,而不关心暂停的次数。假设你要在客户端使用Java HotSpot VM,则可以开启递增模式,因为这样会为你带来更一致且无暂停的响应。"
作为一个旁注,Stoutamire指出,实际上,即使在递增/列车算法未被激活时,一个中间垃圾回收区也是存在的。"仍然有一个中间代,"他解释到,"它是用拷贝收集算法管理的,而不是用具有细密纹理的列车算法来管理的。"
图5. Java HotSpot垃圾回收的实现过程
7. 线程同步
根据Sun估计,一个典型的Java应用程序所使用的40% 的硬件资源,都是用于垃圾回收和处理多线程(可同时处理多重I/O数据流)的。Java HotSpot虚拟机在线程同步方面, 取得了突破性的进展,因而使性能得以明显提高。
"对程序员来说,要了解的最重要的事情是,"Stoutamire说,"我们做了他们希望我们在本地线程上应做的事情。在旧版本的Java VM中,对I/O如何工作曾有过可笑的限制--而不管情况怎样,假设你有一个Java线程,那么实际上有一个本地OS线程来执行它。类似的事情最终可能导致性能下降。"
但是,Java HotSpot线程同步实现,通过使用主机操作系统的线程模型,提供了一种"全抢先式的"线程。"用Java HotSpot VM时,"Stoutamire说道,"每个Java线程都对应一个本地OS线程。而在传统VM中,则并不总是这样--有时一个本地线程可能对应多个Java线程。在这种情况下,如果一个线程由于某种原因受阻,则所有其它相关线程也不能继续运行。一旦你拥有了本地和Java线程的一对一的对应关系,就象Java Hotspot VM那样,那么,如果一个Java线程由于某种原因受阻,则它并不影响其它Java线程。这就是抢先的含义--一个线程可以运行并在任何时候抢先于另一个线程。在非抢先的系统中,一个线程有可能会困死另一个线程。"
8. 未来
在安装了Java HotSpot性能引擎之后,许多程序员要做的第一件事,就是将其提出作自旋(spin)。但是,这种性能引擎被设计为,在与现实世界(real-world)或企业系统、或与这两种系统一起工作时,其性能才是最好的。在一段微基准测试程序(microbenchmarking)代码中,试图预期和运用这种性能引擎的内部工作方式,其结果常常是让人感到沮丧和失望的。因此,Sun已经收集整理了一个"基准测试程序问答",其中的代码示例解释了在运用该性能引擎的许多性能提升时所产生的一般误解。
一个普遍使用的基准测试程序,建立了一段简单的代码,以测试一个循环语句内的许多迭代的执行速度(或许,它递增了循环内的一个数)。Java HotSpot VM是从解释模式下开始运行这样的程序的,但很快它就发现(由于循环的许多重复),这个区域是个"热点"。因此,它便将方法送出,并进行编译。
然而,在目前发布的产品中,一直到下一次方法(main)被调用之后,这种新的编译代码的版本,实际上才被调用。当然,在这种过于简单的微基准测试程序中,这种情况是不会发生的。
"针对这种情况的解决方案是,"Stoutamire解释道,"栈上(on-stack)替换--它已经在下一个Java HotSpot的更新中得以实现,但还未发布。"栈上替换与动态逆优化是恰恰相反的--一个解释框架被转换为一个编译框架,但同时方法仍在运行。"在Java HotSpot 1.0版本中,我们并不关心这种方案,"Stoutamire说,"因为它不是实际应用程序要做的典型的事情,但它针对于这种情形,并能使这种微基准测试程序做人们期望它们所做的事。"
9. 结论
最初,Sun的Java HotSpot性能引擎是为SolarisTM操作系统和Microsoft Windows 操作系统提供的。它对最终用户和独立软件开发商是免费的,但对将其包含在操作系统中的销售商来说,则要支付特许使用费(版税)。Java HotSpot 2.0的beta测试版将在今年夏天发布,其性能将提高30%。