Java HotSpot性能引擎的体系结构
--有关Sun的第二代性能技术的白皮书
内容
无句柄对象(Handleless Object)
双字对象头
将映射数据表示为对象
本地线程支持,包括任务抢先和多重处理技术
背景说明
Java HotSpot垃圾回收
精确性
相继的复制回收
采用标记-整理算法的"旧对象"回收器
增量"无暂停"垃圾回收器
背景说明
"热点(Hot Spot)"检测
方法内嵌
动态逆优化
优化编译器
小结
对软件可重用性的影响
Java本地接口(JNI)支持1. 引言
Java>TM平台正在成为软件开发和部署的主流载体。在许多领域,Java平台的使用率正在迅猛增长--从信用卡到大型计算机、从网页applets到大型商务应用程序。因此,Java技术的品质、成熟度和性能就成了对每一个开发人员和用户至关重要的因素。Sun Microsystems,Inc.正在重点投资于能够在许多处理器和操作系统面前"抬起挡路的栏杆"的技术,应用这种技术,软件开发人员可以将基于Java的应用程序,在不考虑处理器和操作系统的情况下有效而可靠地运行。
人们对Java平台感兴趣的一个主要原因是:基于Java技术的程序与用传统语言编写的程序不同,它们是以一种可移植的和安全的形式而分布的。过去,使用可移植的分布形式一般来说都意味着在程序执行中的性能要下降。通过采用现代动态编译技术,这种性能的下降得以减缓,其本质可说是"双收其利"。
举一个简单但很重要的例子:我们可以使一个Java技术编译器为特定版本的处理器"在运行中"生成优化的机器码(例如,尽管奔腾和奔腾II处理器可以运行相同的机器码,但没有一种形式的机器码可以同时对上述二者都是优化的)。于是,Java编程语言的字节码分布形式不仅可以提供移植性,而且实际上还可以为性能的提高提供新的机会。
本文将介绍Java的第二代性能技术--Java HotSpot性能引擎。Java HotSpot性能引擎几乎在其设计的每一个领域都有创新,它使用了广泛的可用来提高性能的技术;这包括可检测并加速性能关键性(performance-critical)代码"在运行中"的适配性优化技术。Java HotSpot还提供了超快速(ultra-fast)线程同步,以获取线程安全的基于Java技术的程序的最大性能;它还提供了垃圾回收器(GC),GC不仅特别快,而且是完全"精确"的(因而也更可靠);另外,采用最新技术的算法也减少或消除了用户对垃圾回收而引起的暂停的感觉。最后,由于Java HotSpot性能引擎在源代码级是以一种简洁、高级的面向对象的设计风格编写的,因而还进一步改善了维护性和扩展性。
2. 概述
下面是Java HotSpot性能引擎的主要结构性优势:
1) 更好的一般性能
无句柄对象(为提高速度,对象的引用被实现为直接指针);
更快的Java编程语言的线程同步;
为达到更快的C代码的调出和调入,C和Java代码可共享相同的激活栈;
与及时编译JIT相比较,大大减小了代码空间和启动时间总开销。2) 最利于繁殖的(best-of-breed)性能
为获得真本地代码性能,优化了本地代码编译器;
适配性的"热点(Hot Spot)"检测主要集中于性能-关键性代码的优化上,从而大大减少了总编译时间和对已编译代码的内存需求;
方法内嵌技术为大部分程序消除了大多数动态方法调用;
对非内嵌方法的更快的方法调用。3) 精确的、相继的(generational)复制垃圾回收器
更快的对象分配;
精确性提供了更准确的对象回收(与保守的(conservative)或半精确的(partially-accurate)那种可引起难以预料的内存泄漏的回收器不同);
相继回收对大多数程序来说极大地提高了回收效率;
对大多数程序来说, 相继回收还极大地减小了回收"旧的对象"而引起暂停所出现的频率;
相继回收也为使用大量"活的(live)"对象的内存的应用程序极大地改善了性能扩展性;
使用标记-整理(mark-compact)算法来回收"旧的"对象,消除了内存碎 片,增加了本地性(locality);
增量"无暂停"垃圾回收器为"长寿"对象、甚至为极大量的"活"的对象在实质上消除了对象回收过程中出现的用户可视的暂停,这对等待时间敏感的应用程序(如服务器)以及大数据量的程序来说是理想的; 4) 先进的高级设计
透明调试和简档(profiling)语意--Java HotSpot体系结构能够使本地代码的生成及优化对程序员完全透明,它可以按照纯字节码语意提供全部简档和调试信息,而不管内部实际上所用的优化方法。 3. 体系结构 Java HotSpot性能引擎的体系结构使多年来在Sun Microsystems的实验室里所做的研究达到了顶点。它综合采用了具有最新技术水平的内存模型、垃圾回收器和适配性优化器;并且它是以一种特别高级的和面向对象的风格写成的。
以下部分将介绍Java HotSpot性能引擎的重要的体系结构及其特性。
4. 内存模型
4.1 无句柄对象
Java 2 软件开发工具包(SDK)使用间接句柄来表示对象的引用。虽然在垃圾回收过程中,这样做会使对象的重新定位变得更加简单,但这会引发一个重要的性能瓶颈,因为大多数对Java编程语言对象的实例变量的访问都需要两个层次的间接引用。Java HotSpot性能引擎消除了句柄的概念:对象的引用被实现为直接指针,从而可提供对实例变量的C-速度访问。垃圾回收器则负责在内存被回收过程中,当对象被重新定位时,寻找并更新所有对在适当位置上的对象的引用。
4.2 双字(Tow-Word)对象头
Java HotSpot性能引擎使用双机器-字对象头,而不是象Java 2 SDK那样使用三字对象头。由于平均的Java编程语言的对象尺寸较小,因而这种技术对节省空间产生了重要作用(大约节省了8%的堆的大小)。第一个对象头的字包含了身份标识哈希码和GC状态等信息;第二个对象头的字是一个对对象的类的引用。只有数组才有第三个对象头字段,它是用来表示数组大小的。
4.3 将映射数据表示为对象
类、方法以及其它内部映射数据被直接表示为堆上的对象(尽管这些对象也许不能被基于Java技术的程序所直接访问)。这不仅简化了内存模型,而且使你可以采用与回收其它Java编程语言对象相同的垃圾回收器来回收这类映射数据。
4.4 本地线程支持,包括任务抢先和多重处理技术
每个线程方法的激活栈是使用宿主操作系统的线程模型来表示的。Java编程语言方法和本地方法可共享相同的栈,从而可允许在C和Java编程语言间的快速调用。使用宿主操作系统的线程调度机制可支持全抢先的Java编程语言线程。
使用本地操作系统的线程和调度机制的一个主要优点是,它能够透明地利用本地操作系统支持多重处理。由于Java HotSpot性能引擎被设计为对在执行Java编程语言代码时的抢先和/或多重处理引起的竞争状态是不敏感的,因而Java编程语言线程将自动利用由本地操作系统所提供的任意调度机制和处理器分配策略。
5. 内存垃圾回收
5.1 背景说明
Java编程语言对程序员的一个主要魅力在于,它是第一个可提供内置自动内存管理(或内存垃圾回收)的主流编程语言。在传统语言中,一般都使用显式分配/释放模型来进行动态内存分配。事实证明, 这不仅是造成内存泄漏、程序错误以及用传统语言编写的程序崩溃的最主要原因之一,而且还是提高性能的瓶颈, 并且是形成模块化和可再使用代码的主要障碍(如果没有显式和难以理解的模块间的协同操作,在模块界限间确定释放点有时几乎是不可能的)。在Java编程语言中,垃圾回收也是支持安全性模型所必需的所谓"安全地"执行这一语义的重要组成部分。
当一个垃圾回收器能够"证明"某个对象对正在运行的程序来说是不可访问的时候,它仅通过回收该对象就可自动地在后台处理对该对象的内存的"释放"。这种自动的处理过程不仅完全消除了由于释放太少而引起的内存泄漏,同时也消除了由于释放太多而引起的程序崩溃和难以发现的引用错误。
从传统上讲,相对于显式释放模型来说, 垃圾回收一直被认为是一种没有效率且会引起性能下降的处理过程。事实上,使用现代垃圾回收技术,可大大改善性能,且这种性能实际上要比由显式释放所提供的性能好得多。
5.2 Java HotSpot垃圾回收器
Java HotSpot性能引擎具有一个先进的垃圾回收器,它除了包含以下将要描述的先进技术特性外,还充分利用了简洁和面向对象的设计优势,提供了一个高层次的垃圾收集结构框架,这个框架可被轻松地配置、使用或扩展以使用新的回收算法。
以下将介绍Java HotSpot垃圾回收器的的主要特性。总体来讲,所用各种技术的综合结果无论是对需要尽可能高的性能的应用程序来说,还是对不期望有由于碎片而引起内存泄漏和内存不可访问的长时运行应用程序来说,都是较好的。Java HotSpot性能引擎不仅能够提供具有最新技术水平的垃圾回收器性能,而且可以保证全部内存回收,并完全消除内存碎片。
5.3 精确性
Java HotSpot垃圾回收器是一种全精确回收器, 与之形成对比的是, 许多垃圾回收器都是保守的(conservative)或半精确的(partially-accurate)。虽然保守的垃圾回收由于易于增加到一个不支持垃圾回收的系统中, 因而具有一定的吸引力, 但它却有一定的缺陷。
一个保守的垃圾回收器不能确切地断定所有对象的引用的分布位置, 其结果是, 它必须保守地假设那些看似要引用一个对象的内存字(memory word)是事实上的对象引用。这就意味着它可能导致某种错误, 例如将一个整数误认为是一个对象指针; 这会造成一些负面影响。首先, 当发生这样的错误时(实际并不普遍), 内存泄漏会不可预知地以一种对应用程序员来说实质上不可再生(reproduce)或调试(debug)的方式出现(尽管由虚悬(dangling)对象引用所引起的崩溃仍可被预防, 并且如果有足够的备份内存, 该程序仍可正确执行);第二, 由于它可能已经导致了某个错误, 因而一个保守的回收器必须使用句柄来间接引用对象(降低性能), 或者避免重新定位对象;因为重新定位无句柄对象需要更新所有对对象的引用, 这在回收器不能确切地断定一个表面上的引用就是一个真的引用时, 是不可能做到的。不能重新定位对象将会导致对象内存碎片, 且更重要的是, 它会妨碍使用以下描述的先进的相继复制回收算法。
因为Java HotSpot回收器是全精确的, 因而它可以提供几个有力的设计保证, 这在保守的回收器上是不可能提供的: · 所有不可访问的对象内存都可以被可靠地回收;
· 所有对象都可以被重新定位, 因而可对对象内存的进行整理;这就消除了对象内存的碎片并增加了内存的本地性。
5.4 相继的复制回收
Java HotSpot性能引擎采用了具有先进技术的相继复制回收器,它有两个主要优点:
· 与Java 2 SDK相比,为大部分程序较大地提高了分配速度和总的垃圾回收效率(通常提高了5倍);
· 相应地减小了用户可感觉的垃圾回收时的"暂停"所出现的频率。
相继回收器利用了在大部分程序中大多数对象(通常为95%)都是非常短命的也就是被用作临时数据结构这样一个事实,通过将新创建的对象隔离到一个对象"幼稚园(nursery)"中,一个相继回收器可以完成以下几件事:第一,因为在对象幼稚园中,新的对象就象堆栈那样被一个接一个地分配,因而分配变得特别的快,因为这样它仅涉及单个指针的更新及对幼稚园溢出的单个检查。第二,到幼稚园溢出时,大部分幼稚园中的对象已经"死了",这就使垃圾回收器可以只简单地将幼稚园中极少数存活的对象移到别处就可以了,从而不必对幼稚园中死去的对象做回收工作。
5.5 采用标记-整理算法的"旧对象"回收器
尽管相继的复制回收器可以有效地回收大部分死的对象,但较长寿命的对象仍然在"旧对象"内存区不断地堆积。从内存不足状态或程序要求的角度考虑,有时必须执行对旧对象的垃圾回收。Java HotSpot性能引擎可以使用一种标准的标记-整理回收算法,它从"根"开始遍历活对象的全部图解,然后扫描内存并整理回收由死的对象遗留的缝隙。通过整理回收堆中的缝隙(而不是将它们回收到一个释放清单中),可消除内存碎片;由于消除了释放清单搜索,则旧对象的分配将是更合理的。
5.6 增量"无暂停"垃圾回收器
标记-整理回收器不能消除所有用户可感觉的暂停, 用户可感觉的垃圾回收暂停是在 "旧的"对象(在机器术语中指已经 "活" 了一段时间的对象)需要做垃圾收集时出现的, 而且这种暂停与现存的活的对象的数据量成比例。这就意味着当有较多数据被处理时, 该暂停可能是任意大的; 这对服务器应用程序、动画或其它软实时应用程序来说,是一种非常不好的的表现。
Java HotSpot性能引擎提供了另一种使用的旧空间垃圾回收器以解决这一问题。该回收器是全增量的, 它消除了用户可探察的垃圾回收暂停。该增量回收器可平滑地按比例增加,即使在处理特大的对象数据集时,也可以提供相对不变的暂停时间。这为如下应用程序创造了极佳的表现:
服务器应用程序, 特别是高可用性的应用程序;
处理非常大的 "活的"对象的数据集的应用程序;
不期望有用户可注意到的暂停的应用程序, 如游戏、动画或其它高交互性的应用程序。无暂停回收器采用的是一种增量旧空间回收方案, 学术上称该方案为"列车(train)"算法。该算法是将旧空间回收时的暂停分离为许多微小的暂停(典型的暂停小于10毫秒), 然后将这些微小的暂停随着时间散布开来, 于是, 实际上的程序对用户来讲,就象是没有暂停一样。由于列车算法不是一个硬实时(hard-real time)算法, 因而它不能保证暂停次数的上限。然而, 实际上特大量的暂停是极罕见的, 并且它们不是由大的数据集直接引起的。
作为一种人们十分欢迎的有益的副产品, 无暂停回收还可以改善内存本地性。因为该算法试图将紧密 "耦合的(coupled)"对象组重新定位到相邻的内存区域中, 从而可以为这些对象提供最好的内存分页和高速缓存本地性之属性。这对操作不同的对象数据集的多线程应用程序来说, 也是非常有益的。 6. 超快速线程同步
Java 编程语言的另一个重要的诱人之处,是它提供了一种语言级的线程同步。这就使得编写带有精细的线程同步加锁的多线程程序变得十分简单。然而不幸的是,目前的同步实现相对于其它Java编程语言中的微操作来说,效率非常底,它使精细的同步的操作变成了性能主要的瓶颈。
Java HotSpot性能引擎在线程的同步实现上取得了突破,它极大地促进了同步性能的提高。其结果是使同步性能变得如此之快,以至于对大多数现实世界的程序来说,它已经不是一个重要的性能问题了。
除了在"内存模型"一节中提到的在空间方面的益处之外,同步机制通过为所有无竞争的同步(它动态地由绝大多数同步所构成)提供超快速和常数-时间(constant-time)性能, 从而也提供了它的在性能方面的益处。
Java HotSpot同步实现完全适合于多重处理并应该展示出色的多处理器性能特征。
7. Java HotSpot编译器
7.1 背景说明
Java编程语言是一种新的具有独特性能特征的编程语言。迄今为止,大部分试图提高其性能的尝试都集中在如何应用为传统语言开发的编译技术上。及时编译器是基本的快速传统编译器,它可以"在运行中"将Java字节码转换为本地机器代码。及时编译器在终端用户的实际执行字节码的机器上运行,并编译每一个被首次执行的方法。
在JIT编译中存在着几个问题。首先,由于编译器是在"用户时间"内运行于执行字节码的机器上,因此它将受到编译速度的严格限制:如果编译速度不是特别快,则用户将会感到在程序的启动或某一部分的明显的延迟。这就不得不采取一种折衷方案,用这种折衷方案将很难进行最好的优化,从而将会大大地降低编译性能。
其次,即使JIT有时间进行全优化,这样的优化对Java编程语言来说,也比对传统语言(如C和C++)的优化效果要差。这有以下几个原因:
Java编程语言是动态"安全的",其含义是保证程序不违反语言的语义或直接访问非结构化内存。这就意味着必须经常进行动态类型测试, 例如,当转型时(casting)和向对象数组进行存储时。
Java编程语言在"堆(heap)"上对所有对象进行分配,而在C++中,许多对象是在栈(stack)上分配的。这就意味着Java编程语言的对象分配效率比C++的对象分配效率要高得多。除此之外,由于Java编程语言是垃圾回收式的,因而它比C++有更多的不同类型的内存分配开销(包括潜在的垃圾清理 (scavenging)和编写-隔离(write-barrier)开销)。
在Java编程语言中,大部分方法调用是"虚拟的"(潜在是多态的),这在C++中很少见。这不仅意味着方法调用的性能更重要,而且意味着更难以为方法调用而执行静态编译器优化(特别是象内嵌方法(inlining)那样的全局优化)。大多数传统优化在调用之间是最有效的,而Java编程语言中的减小的调用间距离可大大降低这种优化的效率,这是因为它们使用了较小的代码段的缘故。
基于Java技术的程序由于其强大的动态类装载的能力,因而可"在运行中"发生改变。这就使得它特别难于进行许多类型的全局优化,因为编译器不仅必须能够检测这些优化何时会由于动态装载而无效,而且还必须能够在程序执行过程中解除和/或重做这些优化,且不会以任何形式损坏或影响基于Java技术的程序的执行语义(即使这些优化涉及栈上的活动方法)。 上述问题的结果是使得任何试图获取Java编程语言的先进性能的尝试,都必须寻求一种非传统的解决方案,而不是盲目地应用传统编译器技术。
Java HotSpot性能引擎的体系结构通过使用适配性的优化技术,解决了以上所提出的Java编程语言的性能问题。适配性的优化技术是Sun公司的研究机构Self小组多年以来在面向对象的语言实现上的研究成果。
7.2 热点Hot Spot检测
适配性的优化技术利用了大多数程序的有趣的属性,解决了JIT编译问题。实际上,所有程序都是花费了它们的大部分时间而执行了它们中的很小一部分代码。Java HotSpot性能引擎不是在程序一启动时就对整个程序进行编译,而是在程序一启动时就立即使用解释器(interpreter)运行该程序,在运行中对该程序进行分析以检测程序中的关键性"热点(Hot Spot)",然后,再将全局本地码(native-code)优化器集中在这些热点上。通过避免编译(大部分程序的)不常执行的代码,Java HotSpot编译器将更多的注意集中于程序的性能关键性部分,因而不必增加总的编译时间。这种动态监测随着程序的运行而不断进行,因而,它可以精确地"在运行中"调整它的性能以适应用户的需要。
这种方法的一个巧妙而重要的益处是,通过将编译延迟到代码已被执行一会儿之后("一会儿"是指机器时间,而不是用户时间!),从而可在代码被使用的过程中收集信息,并使用这些信息进行更智能的优化。除收集程序中的热点信息外,也收集其它类型的信息,如与"虚拟"方法调用有关的调用者-被调用者的关系数据等。
7.3 方法内嵌
正象在"背景说明"中所提到的,Java编程语言中的"虚拟"方法调用的出现频率,是一个重要的妨碍优化的瓶颈。当Java HotSpot适配性优化器在执行过程中,一旦回收了有关程序"热点"的信息后,它不仅能将这些"热点"编译为本地代码,而且还可以执行内嵌在这些代码上的大量的方法。
内嵌具有重要的益处。它极大地减小了方法调用的动态频率,这就节省了执行这些方法调用所需要的时间。而更重要的是,内嵌为优化器生成了大得多的代码块。这种状态可以大大地提高传统编译器的优化技术的效率,从而消除提高Java编程语言性能的障碍。
内嵌对其它代码的优化起到了促进作用,它使优化的效率大大提高。随着Java HotSpot编译器的进一步成熟,操作更大的内嵌代码块的能力将使实现更先进的优化技术成为可能。
7.4 动态逆优化
尽管上述内嵌是一种非常重要的优化方法,但对于象Java编程语言那样的动态的面向对象的编程语言来说,这在传统上一直是非常难以实现的。此外,尽管检测"热点"和内嵌它们所调用的方法已经十分困难,但它仍然还不足以提供全部的Java编程语言的语义。这是因为,用Java编程语言编写的程序不仅能够"在运行中"改变方法调用的模式,而且能够为一个运行的程序动态地装载新的Java代码。
内嵌是基于全局分析的,动态装载使内嵌更加复杂了,因为它改变了一个程序内部的全局关系。一个新的类可能包含了需要被内嵌在适当位置的新的方法。所以,Java HotSpot性能引擎必须能够动态地逆优化(如果需要,然后再重新优化)先前已经优化过的"热点",甚至在"热点"代码的执行过程中进行这种操作。没有这种能力,一般的内嵌将不能在基于Java的程序上安全地执行。
7.5 优化编译器
只有性能关键性代码才被编译,这就"购买了时间",并可将这些时间用于更好的优化。Java HotSpot性能引擎使用全优化编译器,以此替代了相对简单的JIT编译器。全优化编译器可执行所有第一流的优化。例如:死代码删除、循环非变量的提升、普通子表达式删除和连续不断的传送(constant propagation)等。它还赋予优化某些特定于Java技术的性能。如:空-检查(null-check)和值域-检查(range-check)删除等。寄存器分配程序(register allocator)是一个用颜色表示分配程序的全局图形,它充分利用了大的寄存器集(register sets)。Java HotSpot性能引擎的全优化编译器的移植性能很强,它依赖相对较小的机器描述文件来描述目标硬件的各个方面。尽管编译器采用了较慢的JIT标准,但它仍然比传统的优化编译器要快得多。而且,改善的代码质量也是对由于减少已编译代码的执行次数而节省的时间的一种"回报"。
7.6 小结
综上所述,我们可以对Java HotSpot适配性优化器的作用做如下小结:
一般来说,程序启动得更快。这是因为,与JIT编译器相比,预先编译做得较少的缘故。
编译过程随着时间展开,从而使编译暂停时间更短,更不被用户所注意。
仅编译性能关键性代码的做法"购买了时间",从而可将这些时间用在执行更好的优化上。
由于编译较少的代码, 编译代码所需的内存较少.
通过使编译代码前的等待时间变得长一点,可收集信息以执行更好的优化,如内嵌,这种技术将具有深远的意义。
通过高度优化性能关键性代码,使重要的代码的运行速度更快。7.7 对软件可重用性(reusability)的影响
面向对象的编程语言的一个主要优势是,通过为软件的重复使用提供一种强大的语言机制,来增加开发的生产力。然而实际上,很少能够获得这种可重用性。因为大量地使用这些机制可能会极大地减损性能,因而程序员都必须谨慎地使用它们。Java HotSpot技术的一个惊人的副作用是,它大大地减少了这种性能的减损代价。我们相信,这将会对面向对象的软件的开发方法产生重要的影响,它将第一次允许各个公司可以充分地使用面向对象的可重用性机制,且不会减损他们的软件性能。
这种作用的示例很容易获得。一个对使用Java编程语言的程序员的调查结果将会明确表明,许多程序员都避免使用全"虚拟"方法同时也避免编写较大的方法。因为他们确信,每一个虚拟方法的调用都会导致性能的下降。同时,"虚拟"方法(也就是在Java编程语言中的非"static"或"final"那些方法)的精细使用对高可重用性的类的构造特别重要,因为每一个这样的方法的作用就象一个"异常分支(hook)",它允许新的子类修改超类的操作。
由于Java HotSpot性能引擎可自动地内嵌大部分虚拟方法调用,因此,性能下降的程度被大大地减小了,甚至在许多情况下,被全部消除了。
无论怎样强调这种作用的重要性都不会过分。因为使用重要的可重用性机制,可以大大地改变有关性能的权衡关系, 这种技术具有从根本上改变面向对象的代码的编写方式的潜力。除此之外,随着面向对象的编程方法的成熟,有一种明显的向着更细分的对象以及更细分的方法发展的趋势。这两个趋势都旨在以将来的代码风格,极力增加虚拟方法调用的频率。随着这种高级代码风格的流行,Java HotSpot技术的优势将愈发明显。
8. Java本地接口(JNI)支持
Java HotSpot性能引擎可用标准Java本地接口(JNI)支持本地方法。以前用JNI编写的本地方法在源代码和二进制代码格式上都是向上兼容的。初始本地方法接口将不被支持(JNI被部分地引入,因为旧的接口没有提供对本地方法DLLs的二进制兼容性)。