C++的速度是由C++编译器在程序员开发时编译出来的机器语言的优化程度决定的。
Java的速度是由Java的JIT和HotSpot编译器将java bytecode在运行时“即时”编译成针对本地CPU的优化的本地代码决定的。
比速度的实际就是在比:看C++编译器和java编译器谁能产生更优化的机器代码。
很明显,C++的编译器不如java的JIT和HotSpot编译器,因为JIT和HotSpot编译器能针对CPU指令集进行人优化、能在运行时根据使用频率对method进行内联和优化。而C++的静态编译器永远也做不到这些
两者有着本质的不同,但是有些Qpper用一辈子也无法理解这其中的差别,
他们只能抱着一个可怜的、没有任何根据的“C++比Java快”进棺材
某些Qpper们认为:根本就不存在Java,只存在C++,Java只是一个C++的程序。
但他们却在有意回避一个事实:
用C++编译器编译出来的本地代码才是C++程序
用Fortran编译器编译出来的本地代码才是Fortran程序
用Delphi编译器编译出来的本地代码才是Delphi程序
用C#编译器编译出来的本地代码才是C#程序
用VB.net编译器编译出来的本地代码才是VB.net程序
用Java编译器编译出来的本地代码才是Java程序
如果照某些Qpper的观点,那么世界上只有机器语言一种语言,因为其它语言归根结底都是机器语言程序,岂不荒唐?
决定一个程序是什么语言的程序,并不是由“编译器是什么语言写成的”来决定的,而是由“这是个用来编译什么语言的编译器”来决定的。
如果用delphi写一个C语言的编译器,难道就能说“C语言实际是个delphi程序”么?Java中的JIT编译器可能是用C++写的,但就能因此说“Java程序是个C++程序”吗?!
如果用java写一个将C语言编译成本地代码的程序,难道就能说“C语言实际是个java程序”吗?!
而Java的本地代码是用Java的JIT和HotSpot编译器在程序运行时编译出来的,根本不是C++编译器编译出来的,所以java程序根本不是一个C++程序!
JIT和HotSpot编译器可以根据程序运行的CPU进行指令集进行优化,C++编译器可以吗?
JIT和HotSpot编译器可以根据程序运行时的profile对本地代码进行inline等优化,C++编译器可以吗?
JIT和HotSpot编译器可以根据程序运行时根据程序运行的情况进行更准确的分支预测,C++编译器可以吗?
大家可以去jre1.5.0的安装路径中去看看:
其中的jar文件共有50.5M,由于jar文件是压缩文件,并且bytecode的代码要比native code精简的多(前面已经说过了:一个构造方法在bytecode中只要一个指令,构造方法在C++的编译器却要11个指令。Java 一個 method call 只要一個machine code,但用 x86 相對需要 4 個),所以这50.5M的java程序完成的工作大约相当于200M以上的本地代码完成的工作。
而其中的.exe和.dll共有7.7M,本地代码在java运行环境中占的比例连5%都不到。
而这仅有的5%的“C++编译器产生的本地代码”(比如AWT.dll)在java程序运行时还要被JIT和HotSpot编译器重新编译成新的指令序列 (例如:符合SSE2的指令集的指令),并根据运行时的profile 来内联到其它java编译器编译出来的native code中成为全新的NativeCode序列
所以C++编译器编译出来java本地库的机器代码序列在java运行的时候根本看不到,这些机器代码也被Java的JIT和HotSpot编译器重新编译并更改执行序列,这些“C++编译器编译出来的机器代码”已经变成了“Java编译器编译出来的机器代码”。最终,在CPU中执行的所有机器语言序列全部是由Java编译器产生的,与C++编译器没有一点关系
以下是Java比C++快的理论依据:
关于java如此快的原因:
有些东西确实只有JIT能做到:
比如
http://java.about.com/gi/dynamic/offsite.htm?site=http://www.idiom.com/%257Ezilla/Computer/javaCbenchmark.htmlhttp://java.ittoolbox.com/browse.asp?c=JAVAPeerPublishing&r=http%3A%2F%2Fwww%2Eidiom%2Ecom%2F%7Ezilla%2FComputer%2FjavaCbenchmark%2Ehtmlhttp://www.idiom.com/~lla/Computer/javaCbenchmark.html 中提到:
The compiler knows what processor it is running on, and can generate code specifically for that processor。
JIT编译器知道什么处理器正在运行,可以产生对应此处理器的优化代码。在这一点上,C++的静态编译器肯定做不到。
# The compiler knows what processor it is running on, and can generate code specifically for that processor. It knows whether (for example) the processor is a PIII or P4, if SSE2 is present, and how big the caches are. A pre-compiler on the other hand has to target the least-common-denominator processor, at least in the case of commercial software.
JIT编译器知道什么处理器正在运行,可以产生对应此处理器的优化代码。它知道是否处理器是P3或P4,是否SSE2存在,cache有多大。一个C++之类的预先编译器只能针对所有处理器的指令集的“最小公分母”进行编译,至少在商业软件中是这样的。
# Because the compiler knows which classes are actually loaded and being called, it knows which methods can be de-virtualized and inlined. (Remarkably, modern java compilers also know how to "uncompile" inlined calls in the case where an overriding method is loaded after the JIT compilation happens.)
因为JIT编译器知道哪些类被实际装载和调用,它知道哪些方法可以被“消虚拟化(de-virtualized)”
和内联(值得一提的是:当代java编译器还能知道在被覆盖的方法被装载的情况下,在JIT编译后进行“逆编译”内联的方法调用)
# A dynamic compiler may also get the branch prediction hints right more often than a static compiler.
动态编译器可以进行更多的分支预测
重点强调:
***
Java速度如此快的原因在此(来源:IBM东京研究院)
http://www.is.titech.ac.jp/ppl2004/proceedings/ishizaki_slides.pdf ***
其中有6大步优化,
每个大步中又有若干小步的优化,
共几十步的优化。
自己看吧,英语比较简单,只是内函比较难理解,
如果自己看不明白,别人解释也一样听不懂。
对Java HotSpotTM 性能引擎的深入研究
省略,见:
http://www.zahui.com/html/6/9089.htm
另一篇文章:
Java HotSpot性能引擎的体系结构
--有关Sun的第二代性能技术的白皮书
内容 - 引言
- 概述
- 体系结构
- 内存模型
- 无句柄对象(Handleless Object)
- 双字对象头
- 将映射数据表示为对象
- 本地线程支持,包括任务抢先和多重处理技术
- 内存垃圾回收
- 背景说明
- Java HotSpot垃圾回收
- 精确性
- 相继的复制回收
- 采用标记-整理算法的"旧对象"回收器
- 增量"无暂停"垃圾回收器
- 超快速线程同步
- 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的二进制兼容性)。
http://www.zahui.com/html/6/9088.htm
http://www.csdn.com.cn/program/1945.htm证据
“很难相信Java居然能和C++一样快,甚至还能更快一些。”
据我自己的实践,这种说法确实成立。然而,我也发现许多关于速度的怀疑都来自一些早期的实现方式。由于这些方式并非特别有效,所以没有一个模型可供参考,不能解释Java速度快的原因。
我之所以想到速度,部分原因是由于C++模型。C++将自己的主要精力放在编译期间“静态”发生的所有事情上,所以程序的运行期版本非常短小和快速。C++ 也直接建立在C模型的基础上(主要为了向后兼容),但有时仅仅由于它在C中能按特定的方式工作,所以也是C++中最方便的一种方法。最重要的一种情况是C 和C++对内存的管理方式,它是某些人觉得Java速度肯定慢的重要依据:在Java中,所有对象都必须在内存“堆”里创建。
而在C ++中,对象是在堆栈中创建的。这样可达到更快的速度,因为当我们进入一个特定的作用域时,堆栈指针会向下移动一个单位,为那个作用域内创建的、以堆栈为基础的所有对象分配存储空间。而当我们离开作用域的时候(调用完毕所有局部构建器后),堆栈指针会向上移动一个单位。然而,在C++里创建 “内存堆”(Heap)对象通常会慢得多,因为它建立在C的内存堆基础上。这种内存堆实际是一个大的内存池,要求必须进行再循环(再生)。在C++里调用 delete以后,释放的内存会在堆里留下一个洞,所以再调用new的时候,存储分配机制必须进行某种形式的搜索,使对象的存储与堆内任何现成的洞相配,否则就会很快用光堆的存储空间。之所以内存堆的分配会在C++里对性能造成如此重大的性能影响,对可用内存的搜索正是一个重要的原因。所以创建基于堆栈的对象要快得多。
同样地,由于C++如此多的工作都在编译期间进行,所以必须考虑这方面的因素。但在Java的某些地方,事情的发生却要显得“动态”得多,它会改变模型。创建对象的时候,垃圾收集器的使用对于提高对象创建的速度产生了显著的影响。从表面上看,这种说法似乎有些奇怪——存储空间的释放会对存储空间的分配造成影响,但它正是JVM采取的重要手段之一,这意味着在Java中为堆对象分配存储空间几乎能达到与C++中在堆栈里创建存储空间一样快的速度。
可将C++的堆(以及更慢的Java堆)想象成一个庭院,每个对象都拥有自己的一块地皮。在以后的某个时间,这种“不动产”会被抛弃,而且必须再生。但在某些JVM里,Java堆的工作方式却是颇有不同的。它更象一条传送带:每次分配了一个新对象后,都会朝前移动。这意味着对象存储空间的分配可以达到非常快的速度。“堆指针”简单地向前移至处女地,所以它与C++的堆栈分配方式几乎是完全相同的(当然,在数据记录上会多花一些开销,但要比搜索存储空间快多了)。
现在,大家可能注意到了堆事实并非一条传送带。如按那种方式对待它,最终就要求进行大量的页交换(这对性能的发挥会产生巨大干扰),这样终究会用光内存,出现内存分页错误。所以这儿必须采取一个技巧,那就是著名的“垃圾收集器”。它在收集“垃圾”的同时,也负责压缩堆里的所有对象,将“堆指针”移至尽可能靠近传送带开头的地方,远离发生(内存)分页错误的地点。垃圾收集器会重新安排所有东西,使其成为一个高速、无限自由的堆模型,同时游刃有余地分配存储空间。
为真正掌握它的工作原理,我们首先需要理解不同垃圾收集器(GC)采取的工作方案。一种简单、但速度较慢的GC技术是引用计数。这意味着每个对象都包含了一个引用计数器。每当一个句柄同一个对象连接起来时,引用计数器就会增值。每当一个句柄超出自己的作用域,或者设为null时,引用计数就会减值。这样一来,只要程序处于运行状态,就需要连续进行引用计数管理——尽管这种管理本身的开销比较少。垃圾收集器会在整个对象列表中移动巡视,一旦它发现其中一个引用计数成为0,就释放它占据的存储空间。但这样做也有一个缺点:若对象相互之间进行循环引用,那么即使引用计数不是0,仍有可能属于应收掉的 “垃圾”。为了找出这种自引用的组,要求垃圾收集器进行大量额外的工作。引用计数属于垃圾收集的一种类型,但它看起来并不适合在所有JVM方案中采用。
在速度更快的方案里,垃圾收集并不建立在引用计数的基础上。相反,它们基于这样一个原理:所有非死锁的对象最终都肯定能回溯至一个句柄,该句柄要么存在于堆栈中,要么存在于静态存储空间。这个回溯链可能经历了几层对象。所以,如果从堆栈和静态存储区域开始,并经历所有句柄,就能找出所有活动的对象。对于自己找到的每个句柄,都必须跟踪到它指向的那个对象,然后跟随那个对象中的所有句柄,“跟踪追击”到它们指向的对象……等等,直到遍历了从堆栈或静态存储区域中的句柄发起的整个链接网路为止。中途移经的每个对象都必须仍处于活动状态。注意对于那些特殊的自引用组,并不会出现前述的问题。由于它们根本找不到,所以会自动当作垃圾处理。
在这里阐述的方法中,JVM采用一种“自适应”的垃圾收集方案。对于它找到的那些活动对象,具体采取的操作取决于当前正在使用的是什么变体。其中一个变体是“停止和复制”。这意味着由于一些不久之后就会非常明显的原因,程序首先会停止运行(并非一种后台收集方案)。随后,已找到的每个活动对象都会从一个内存堆复制到另一个,留下所有的垃圾。除此以外,随着对象复制到新堆,它们会一个接一个地聚焦在一起。这样可使新堆显得更加紧凑(并使新的存储区域可以简单地抽离末尾,就象前面讲述的那样)。
当然,将一个对象从一处挪到另一处时,指向那个对象的所有句柄(引用)都必须改变。对于那些通过跟踪内存堆的对象而获得的句柄,以及那些静态存储区域,都可以立即改变。但在“遍历” 过程中,还有可能遇到指向这个对象的其他句柄。一旦发现这个问题,就当即进行修正(可想象一个散列表将老地址映射成新地址)。
有两方面的问题使复制收集器显得效率低下。第一个问题是我们拥有两个堆,所有内存都在这两个独立的堆内来回移动,要求付出的管理量是实际需要的两倍。为解决这个问题,有些JVM根据需要分配内存堆,并将一个堆简单地复制到另一个。
第二个问题是复制。随着程序变得越来越“健壮”,它几乎不产生或产生很少的垃圾。尽管如此,一个副本收集器仍会将所有内存从一处复制到另一处,这显得非常浪费。为避免这个问题,有些JVM能侦测是否没有产生新的垃圾,并随即改换另一种方案(这便是“自适应”的缘由)。另一种方案叫作“标记和清除”, Sun公司的JVM一直采用的都是这种方案。对于常规性的应用,标记和清除显得非常慢,但一旦知道自己不产生垃圾,或者只产生很少的垃圾,它的速度就会非常快。
标记和清除采用相同的逻辑:从堆栈和静态存储区域开始,并跟踪所有句柄,寻找活动对象。然而,每次发现一个活动对象的时候,就会设置一个标记,为那个对象作上“记号”。但此时尚不收集那个对象。只有在标记过程结束,清除过程才正式开始。在清除过程中,死锁的对象会被释放然而,不会进行任何形式的复制,所以假若收集器决定压缩一个断续的内存堆,它通过移动周围的对象来实现。
“停止和复制”向我们表明这种类型的垃圾收集并不是在后台进行的;相反,一旦发生垃圾收集,程序就会停止运行。在Sun公司的文档库中,可发现许多地方都将垃圾收集定义成一种低优先级的后台进程,但它只是一种理论上的实验,实际根本不能工作。在实际应用中,Sun的垃圾收集器会在内存减少时运行。除此以外,“标记和清除”也要求程序停止运行。
正如早先指出的那样,在这里介绍的JVM中,内存是按大块分配的。若分配一个大块头对象,它会获得自己的内存块。严格的“停止和复制”要求在释放旧堆之前,将每个活动的对象从源堆复制到一个新堆,此时会涉及大量的内存转换工作。通过内存块,垃圾收集器通常可利用死块复制对象,就象它进行收集时那样。每个块都有一个生成计数,用于跟踪它是否依然“存活”。通常,只有自上次垃圾收集以来创建的块才会得到压缩;对于其他所有块,如果已从其他某些地方进行了引用,那么生成计数都会溢出。这是许多短期的、临时的对象经常遇到的情况。会周期性地进行一次完整清除工作——大块头的对象仍未复制(只是让它们的生成计数溢出),而那些包含了小对象的块会进行复制和压缩。JVM会监视垃圾收集器的效率,如果由于所有对象都属于长期对象,造成垃圾收集成为浪费时间的一个过程,就会切换到“标记和清除”方案。类似地,JVM会跟踪监视成功的“标记与清除”工作,若内存堆变得越来越“散乱”,就会换回“停止和复制”方案。 “自定义”的说法就是从这种行为来的,我们将其最后总结为:“根据情况,自动转换停止和复制/标记和清除这两种模式”。
JVM 还采用了其他许多加速方案。其中一个特别重要的涉及装载器以及JIT编译器。若必须装载一个类(通常是我们首次想创建那个类的一个对象时),会找到. class文件,并将那个类的字节码送入内存。此时,一个方法是用JIT编译所有代码,但这样做有两方面的缺点:它会花更多的时间,若与程序的运行时间综合考虑,编译时间还有可能更长;而且它增大了执行文件的长度(字节码比扩展过的JIT代码精简得多),这有可能造成内存页交换,从而显著放慢一个程序的执行速度。另一种替代办法是:除非确有必要,否则不经JIT编译。这样一来,那些根本不会执行的代码就可能永远得不到JIT的编译。
由于JVM对浏览器来说是外置的,大家可能希望在使用浏览器的时候从一些JVM的速度提高中获得好处。但非常不幸,JVM目前不能与不同的浏览器进行沟通。为发挥一种特定JVM的潜力,要么使用内建了那种JVM的浏览器,要么只有运行独立的Java应用程序。
http://pub.chinans-ls.com/~jx/java/chapter/appe.htm 新的JavaTM 虚拟机(VMs)具有能够提高性能的特点, 并且你可以使用许多工具来提高应用程序的性能或减小一般类文件的尺寸。这种Java虚拟机的特性和工具可使你在不改变应用程序、或对应用程序仅做很小改动的情况下, 提高应用程序的性能。
Java虚拟机的特性
Java2与过去的版本相比, 性能已有很大提高, 其中包括更快的内存分配、类尺寸的减小、垃圾收集的改善、最新型的监控器和作为标准的内联JIT技术。当使用新的Java2虚拟机时, 你会看到这种性能的改善; 然而, 如果你能够理解速度是如何提高的, 你就能够调整你的应用程序, 以充分挖掘每一点性能潜力。 方法内联 Java虚拟机的Java2版可在运行时自动内联简单方法。在一个未优化的Java虚拟机中,每调用一次新的方法,就创建一个新的堆栈帧(stack frame)。创建一个新的堆栈帧需要一些额外的资源以及该栈的某些再映射(re-mapping),其结果将导致系统开销的少许增加。
由于方法内联可在你的程序中减少方法调用的次数,因而可提高性能。Java虚拟机内联代码内联了返回常数或仅访问内部域(internal fields)的方法。为了利用方法内联,你可以从以下两件事中选做其一;即:你可以使一个方法对虚拟机所要执行的内联看上去是有吸引力的,或者你可以手工内联一个方法,只要它不破坏你的对象模型。在这一语境中的手工内联意味着可以直接从一个方法中将代码移动到正在调用该方法的方法中。
下面的小例子演示了虚拟机的自动方法内联: public class InlineMe {
int counter=0;
public void method1() { for(int i=0;i<1000;i++) addCount(); System.out.println("counter="+counter); }
public int addCount() { counter=counter+1; return counter; }
public static void main(String args[]) { InlineMe im=new InlineMe(); im.method1(); } }
在当前状态,addCount方法对虚拟机中的内联探测器显得是没有吸引力的,因为addCount方法返回一个值。要发现该方法是否被内联:
java -Xrunhprof:cpu=times InlineMe
它生成一个java.hprof.txt输出文件。前十个方法类似下面的结果:
CPU TIME (ms) BEGIN (total = 510) Thu Jan 28 16:56:15 1999 rank self accum count trace method 1 5.88% 5.88% 1 25 java/lang/Character. 2 3.92% 9.80% 5808 13 java/lang/String.charAt 3 3.92% 13.73% 1 33 sun/misc/Launcher$AppClassLoader.getPermissions 4 3.92% 17.65% 3 31 sun/misc/URLClassPath.getLoader 5 1.96% 19.61% 1 39 java/net/URLClassLoader.access$1 6 1.96% 21.57% 1000 46 InlineMe.addCount 7 1.96% 23.53% 1 21 sun/io/Converters.newConverter 8 1.96% 25.49% 1 17 sun/misc/Launcher$ExtClassLoader.getExtDirs 9 1.96% 27.45% 1 49 java/util/Stack.peek 10 1.96% 29.41% 1 24 sun/misc/Launcher.
如果你将addCount方法改变为不再返回一个值,则虚拟机可在运行时将其内联。为使内联代码更友好,应用下面的程序替换addCount方法:
public void addCount() { counter=counter+1; }
再次运行profiler:
java -Xrunhprof:cpu=times InlineMe
这次,java.hprof.txt的输出应该显得是不同的。 AddCount方法已经消失。它已被内联了!
CPU TIME (ms) BEGIN (total = 560) Thu Jan 28 16:57:02 1999 rank self accum count trace method 1 5.36% 5.36% 1 27 java/lang/Character. 2 3.57% 8.93% 1 23 java/lang/System.initializeSystemClass 3 3.57% 12.50% 2 47 java/io/PrintStream. 4 3.57% 16.07% 5808 15 java/lang/String.charAt 5 3.57% 19.64% 1 42 sun/net/www/protocol/file/Handler.openConnection 6 1.79% 21.43% 2 21 java/io/InputStreamReader.fill 7 1.79% 23.21% 1 54 java/lang/Thread. 8 1.79% 25.00% 1 39 java/io/PrintStream.write 9 1.79% 26.79% 1 40 java/util/jar/JarFile.getJarEntry 10 1.79% 28.57% 1 38 java/lang/Class.forName0
新型同步
在Java 2发布之前,同步的方法和对象总是引发一些额外的性能干扰,这是因为用来实现这种代码锁定的机制采用了一种全局监控器注册,它在某些区域仅仅是单线程的(如搜索现存监控器)。在新发布的Java 2中,每个线程都有一个监控器注册,从而消除了许多现存的性能瓶颈。
如果你曾经使用过其它锁定机制来避免同步方法的性能干扰,现在则有必要重访这些代码并考虑新的Java 2新型锁定技术。
在下面的为同步块创建监控器的例子中,你可以将速度提高40%。所用时间在采用JDK1.1.7和采用Sun Ultra 1上的Java 2时分别为14ms和10ms。
class MyLock {
static Integer count=new Integer(5); int test=0;
public void letslock() { synchronized(count) { test++; } } }
public class LockTest {
public static void main(String args[]) {
MyLock ml=new MyLock(); long time = System.currentTimeMillis();
for(int i=0;i<5000;i++ ) { ml.letslock(); } System.out.println("Time taken="+ (System.currentTimeMillis()-time)); } }
Java Hotspot
Java HotSpotTM虚拟机是Sun Microsystem公司的下一代虚拟机。虽然Java HotSpot 虚拟机所采用的规范与Java 2虚拟机所采用的规范相同,但它已被重新设计,并使用了最先进的技术,从而可在未来许多年内,能够为Java平台提供一个强大而可靠的性能引擎。Java HotSpot虚拟机可提供: 可以探测并加速性能关键性代码的实时动态优化技术。 为发挥线程的最大性能而设计的超快速线程同步。 可最快速获取的精确而可靠的垃圾收集器。 由于其简洁、高层次以及面向对象的设计,因而在可维护性和可扩展性方面的重要改进。 JIT(Just-In-Time)编译器 用来提高应用程序性能的最简单的工具是Just-In-Time(JIT)实时编译器。JIT是一个可将Java字节码转换为本地机器码的代码生成器。由JIT调用的Java程序,其运行速度通常要比由解释程序执行字节码时的速度高得多。
JIT编译器首先是在Java开发工具包(JDKTM)1.1.6中作为一种性能更新出现的,而现在它是你在Java 2平台上使用Java解释程序命令时调用的标准工具。你可以使用Java虚拟机的-Djava.compiler=NONE 选项来使JIT编译器失效,这在JIT的末尾部分有更详细的阐述。
JIT编译器是如何工作的?
JIT编译器是作为一种依赖于平台的本地库提供的。如果JIT编译器库存在,则Java虚拟机将初始化Java本地接口(JNI)的本地代码分支以调用在该库中可获得的JIT函数,而不是调用在解释程序中的相应函数。
java.lang.Compiler 类被用来加载本地库并启动JIT编译器内的初始化。当Java虚拟机调用一个Java方法时,它使用在加载的类对象的方法块中所指定的调用(invoker)方法。Java虚拟机具有若干个调用者方法,例如,如果方法是同步的,或者它是一个本地方法,则将使用不同的调用者。JIT编译器使用它自己的调用者。Sun的产品可以为值ACC_MACHINE_COMPILED检查方法存取位以告知解释程序该方法的代码已被编译并存储在加载类中。
代码何时成为JIT编译的代码?
当一个方法被首次调用时,JIT编译器为该方法将方法块编译为本地代码,并将其存储在该方法的代码块中。
一旦代码被编译完成,在Sun平台上所使用的ACC_MACHINE_COMPILED的位则被设定。
我如何知道JIT编译器在做什么?
环境变量JIT_ARGS允许对Sun Solaris JIT编译器进行简单控制。trace 和 exclude(list)是两个有用的值。要从示例InlineMe中排除(exclude)方法并显示跟踪记录(trace),应将JIT_ARGS 做如下设定:
Unix: export JIT_ARGS="trace exclude(InlineMe.addCount InlineMe.method1)"
$ java InlineMe Initializing the JIT library ... DYNAMICALLY COMPILING java/lang/System.getProperty mb=0x63e74 DYNAMICALLY COMPILING java/util/Properties.getProperty mb=0x6de74 DYNAMICALLY COMPILING java/util/Hashtable.get mb=0x714ec DYNAMICALLY COMPILING java/lang/String.hashCode mb=0x44aec DYNAMICALLY COMPILING java/lang/String.equals mb=0x447f8 DYNAMICALLY COMPILING java/lang/String.valueOf mb=0x454c4 DYNAMICALLY COMPILING java/lang/String.toString mb=0x451d0 DYNAMICALLY COMPILING java/lang/StringBuffer. mb=0x7d690 >>>> Inlined java/lang/String.length (4)
请注意内联方法(如String.length)是免除的。String.length 也是一个特殊的方法,它一般由Java解释程序编译为一个内部快捷字节码。当使用JIT编译器时,由Java解释程序提供的这些优化失效,从而可以使JIT编译器能够理解哪个方法正在被调用。
如何使用JIT来发挥你的优势
首先要记住的一点是,JIT编译器在第二次调用一个方法时,会获得大部分速度上的改善。JIT编译器的确是编译了整个方法,而不是对其进行逐行解释,逐行解释也是一种在运行一个可执行JIT的应用程序时用以改善性能的途径。这意味着如果代码仅被调用一次,你将不会看到太大的性能改善。JIT编译器也将忽略构造函数(class constructor),所以,如果可能的话,应最少量地保留构造函数代码。
如果不能预先检查某些Java边界条件,JIT编译器也不能获得最大的性能改善,这些边界条件包括零指针(Null pointer)或边界外数组等异常。JIT编译器能够知道出现零指针异常的唯一途径是通过由操作系统所提供的信号。由于该信号来自操作系统,而不是来自Java虚拟机,因而你的程序会出现性能上的干扰。为了保证在用JIT运行一个应用程序时,能够获取最好的性能,应确保你的代码完全没有象零指针或边界外数组那样的异常。
如果你要以远程调试状态运行Java虚拟机,或者你要看到源代码行数而不是看到在你的Java栈跟踪中的标签(Compiled Code)的话,你可能需要使JIT编译器失效。要使JIT编译器失效,可在你调用解释器命令时为JIT编译器提供一个空白或无效名称。下面的例子演示了用javac命令将源代码编译为字节码、以及用两种形式的java命令在没有JIT编译器的情况下调用解释程序的过程。 javac MyClass.java java -Djava.compiler=NONE MyClass or javac MyClass.java java -Djava.compiler="" MyClass
第三方工具
其它一些可用的工具包括可用来减小一般Java类文件尺寸的工具。Java类文件包括一个被称作常数池(constant pool)的区域。常数池在某一个地方为类文件保持有一个字符串和其它信息的列表,以备引用。在常数池中可以获取的诸多信息之一是方法和字段的名称。
类文件引用在类中的一个字段作为对常数池中的一个条目的引用。这意味着只要引用保持相同,则无所谓在常数池中存储什么样的值。 一些工具利用这点将常数池中的字段名和方法名重写为缩短的名称。利用这一技术可以大大减小类文件的尺寸,从而使在网上下载类文件变得简单。
|
http://ijsp.net/2/2002-7/30/0000232.shtml
证据
浅谈JAVA VM 发展(作者:黄敬群)
节选:
c++ 的 virtaul method calling:
不算 argument 4 個指令
c 的一般 call:
不算 argument 2 個指令
java 的 virtual call:
不算 argument 2 個指令.
c++ 的 constructor:
11 個指令
C++ destructor:
8 個指令
java 的 constructor:
2 個指令
由此可發現,對動態配置物件的操作而言,Java 一個 method call 只要一個
machine code,但用 x86 相對需要 4 個,這是 Java 在指令集層面直接支援所致。
Joakim Dahlstedt 的來頭可不小,是 JRockit VM 的主要設計者,現任 BEA System
裡頭 Java Runtime Group 的技術長,這篇文章並非老王賣瓜,相反的,Joakim 要
我們明瞭,評斷 Java VM "benchmark" (效能評比) 的方式需要調整,主要是基於以
下幾項:
1.一般的 benchmark 比較僅僅是 micro-benchmark level,不能推廣到 system-
level。
2.軟體產業開發方式發生了很大的變化,大量使用 class library、framework、
Application server,乃至 Web services。援引舊的 benchmark 僅能針對其中
個別 software stack,卻不能進行 system-level 的全面分析,如此的衡量本
身就有問題。
3.Java VM 本身的 Dynamic Optimization,可依據最真實的 profiling 數據調整
元件,使其針對效能進行重組。
4.最後,新的 CPU 架構,像是 Intel EPIC 可能更適合於 Dynamic Optimization
,而非傳統 static compiler。在 EPIC 的架構下,compiler 對效能的影響相
當大,compiler 有責任選擇平行處理的指令集,而非像傳統 Pentium 引入自動
的 out-of-order 亂序執行支援,這意味著,軟體支配了 EPIC 的效能,這對
static compiler 是不利的,因為僅能從 bytecode 取得固定的統計與符號,卻
未能對真實 profiling 作規劃。
Joakim 的結論給予我們很好的啟發:
"In conclusion, ..., but I hope I've convinced you that using runtime
systems like Sun Microsystems' HotSpot or BEA WebLogic JRockit Java
is not slow or inefficient, and that the performance of a large-scale
system built with Java may be superior to that of the same system
built using C."
IBM 的 JDK 曾經一度是最佳性能的 Java VM,但 Sun JDK 1. 4的性能已與 IBM JDK
1.3 相當,其 server 版採用更積極的 JIT 和 GC (Garbage Collection) 技術,尤
其是針對 SMP 的應用提供最適合該硬體架構與多執行緒處理的 GC。
在另一方面,IBM 將內部的 Jalapeno JVM 研究計畫 [6] 的成果以 Open Source 授
權釋出的 JikesRVM 計畫 [7],提供一個測試許多 JIT 與 GC 等技術與演算法的參
考實作平台。IBM Rearch 將 JikesRVM 視作 JIT 方面的一個 research infrastru-
cture,在網站上羅列了相當豐富的論文可參考 。筆者參考了 JikesRVM 的研究方向
,認為 JIT Compiler 發展趨勢可列出以下:
1. 類似於 Hotspot [8] 整合 base interpreter、profiling,以及 adaptive
compiler 三種技術的途徑。
2. 動態 profiling 技術,從簡單的 counter-based algorithm 進化到
performance monitoring event。
3. 應用靜態 compiler 中更 aggressive 的編譯技術 (對 JIT Compiler 而言,
編譯時間已是次要問題),產生最佳化原生碼 (native code)。
4. 應用 inter-procedural 分析,例如 escape analysis 可以 remove synchro-
nization 和實施 stack allocation。
5. 參考 Runtime 所獲得的資訊 (主要是 metadata 和 dynamic allocation),產
生最佳化原生碼。
[6]
http://www.research.ibm.com/jalapeno/ [7]
http://www-124.ibm.com/developerworks/oss/jikesrvm/ [8]
http://java.sun.com/products/hotspot/ 接著,我們來看看 Sun 的 Hotspot 技術。提到 Hotspot 不能不提 Henry Massalin
這位先驅,Henry 的博士論文在 Java Glossary 的 HotSpot 的解釋 [9] 中被譽為
"the mother of all self-modifying code",同時,HotSpot 也是 "builds heavily
on work done in a PhD thesis by Henry Massalin"。
[9]
http://mindprod.com/jgloss/hotspot.html Java 最初的實作是透過直譯器 (interpreter),但這並非意味 Java 一定被解譯執
行的。早期的 Java VM 的確是逐一指令的解譯,因此效能極不理想,於是引入了
JIT 等關鍵技術,而 HotSpot 可說下個世代的 JIT。據 Sun 官方文獻指出,使用
HotSpot 的 Java VM 在 Runtime 時期已經很難分辨 Java bytecode 是否被 JVM 解
釋執行了,因為 HotSpot 實際上是把 Java 的bytecode 編譯成原生碼執行了。
實際上,在 HotSpot 設計中,有兩個技術是相當重要的,也就是所謂 dynamic
compilation 和 profiling。HotSpot 對 bytecode 的編譯,並非在程式執行前預先
編譯的 (這種傳統的方式相對而言稱為 static compilation),相反的,是在程式執
行過程中,以 HotSpot 內建的編譯器做動態的編譯,早期的 JIT(Just In Time) 其
實也是如此的概念,不過沒有 HotSpot 來得全面性。
那麼,HotSpot 是如何動態編譯 Java bytecode 呢﹖HotSpot 採用一個高度彈性的
設計,內部維護了 Profile Monitor,專門監視程式執行中,判斷程式片段中使用的
頻率高寡,並依據對性能影響的程度,交付於若干演算法處理。HotSpot 對於那些對
程式執行時效率影響較大的程式碼片段,特稱為 hot spot (特別以小寫書寫,避免
與 HotSpot 混淆),HotSpot 會這些片段動態編譯成原生碼,同時,會依據之前
profiling 的結果,對這些原生碼進行最佳化,從而提高執行效率。相反的,如果執
行頻率較低的程式碼片段,HotSpot 就沒必要花時間做動態編譯,只需要以直譯器執
行即可。
整體而論,在 HotSpot 下,Java bytecode 是以解譯的方式被載入到 JavaVM 中,
但是 HotSpot 立刻會依據某些程式碼片段執行的狀態,獲知其中對效能影響最大的
部分,透過動態編譯與最佳化處理,建立原生碼,於是,接下來的執行過程中就可獲
得相當程度的效能提昇。我們可以得知,HotSpot 對 bytecode 的執行有以下三種處
理方式:
- 直譯 (不動態編譯)
- 部分動態編譯
- 依據 profiling 結果做動態編譯與最佳化處理
至於程式哪部分只做直譯、部分動態編譯,以及哪部分做何種最佳化處理,則全程由
Profile Monitor 決定。
採用 dynamic compilation 會比傳統的 static compilation 帶來什麼優點呢?撇
開跨平台需求不論,dynamic compilation 在許多方面優越於傳統的途徑,特別是
profiling 的能力。過去 static compilation 很難精準的預知程式執行中究竟何處
才是最需要最佳化處理的部分,僅能透過內建的 template 來建構 micro-level 的
最佳化程式碼。相反的,dynamic compilation 可獲悉最真實的 profiling 表現,
依據需求動態調整,這在日後處理器逐漸軟體化的發展趨勢而言,更顯得重要,因為
過去硬體的工作逐漸移轉到軟體來做,compiler optimization 的責任就格外沈重了
,像是前述 Intel EPIC 架構。
另一個典型的例子,稱為 method inlining。無論是在 C++ 或是 Java 中,呼叫
(invoke) method 是很消耗系統資源的,系統必須維護堆疊操作,以符合預期的
calling convention。於是,有人提出把原本需要做 method invocation 的處理,
改用 inline 的方式,「嵌入」到原本呼叫的地方,如此一來,就只是單純的循序執
行,也不會有堆疊操作。但是,method inlining 的方式對 C++ 與 Java 一類的物
件導向語言來說,編譯器很難有很好的實作方式,因為需要兼顧物件導向的特徵,尤
其是維持 dynamic binding 性質。static compiler 其實可以把 C++/Java 中屬性
為 private 與 static 的 method 做 method inlinng,但是一旦有 overridden 或
dynamic binding 時,static compiler 無法得知實際上的執行狀況,就會保守的不
予實施 inlining。這個難題,恰好可被 HotSpot 一類 dynamic compilation 的設
計迎刃而解,因為 Profiling Monitor 對 method invocation 的狀況可以掌握,當
然可精準的得知 Runtime 中,RTTI (Run-Time Type Identification) 可協助
HotSpot 處理 method inlining,因此,在 server-side 應用這種重複進行某項目
執行時,可獲得頗大的效能提昇。
Sun 的文獻也指出,在某些狀況下,Java 的應用程式甚至可比傳統的 C 程式還快。
以目前發展而言,JIT Compiler 的成本主要在於 profiling 與 dynamic compila-
tion 兩項。理想而言,這兩項成本應該視為 constantant time,於是許多研究論文
相繼提出,以作為效能改進。特製為 JIT Compiler 使用、精度不需很高的 Java
Runtime profiling 可參考〈A Portable Samplling-Based Profiler for Java
Virtual Machines〉[10],該論文提出採用 sampling 的方式來做近似分析。而至於
Dynamic compilation 的 overhead 可以用漸進式最佳化的方式來減少,在 Sun 的
HotSpot 白皮書已有詳盡的介紹。
全文
http://dev.csdn.net/develop/article/54/54057.shtmJava和.NET哪个运行的更快?对于这个问题,有的人回答得非常干脆:“当然是.NET了”。你如果问他为什么,他会简单的回答:“.NET有JIT(Just-in-time-compiler),运行的是编译好的机器代码,Java是解释执行的,速度当然不能和. NET的Native code相提并论了!”
事实是这样的吗?
Java和.NET到底哪个运行的更快可不是一个简单的问题,你千万不要相信任何一个简单的Benchmark程序的测试结果。因为性能问题涉及的方面太广,很难简单武断的下结论的。比如说要全面比较Java和.NET的性能,你可能要测试整数,浮点数,各种String操作,Collection (standard and generics)性能, XML (SAX and DOM) 操作,文件IO (读,写,顺序,随机,文本,二进制),数据库操作,线程同步(Thread synchronization), Lock机制,网络操作,串行化/反串行化,Memory allocate/release, Garbage collection等等等等。就我做过的一些测试来看,Java和.NET各有千秋,各有擅长的地方,也都有自己的弱点。
今天我在这里想说的是Java和.NET在执行程序时各自的特点,希望你看后能对Java和.NET有个更清晰地认识(或是给我扔几块砖,让我有个更清晰地认识

)。
Java的Hotspot引擎性能是早期Java的一个最大问题。为了这事,SUN和业界的其他公司没少下功夫,也曾经出现了很多方案和提议。比较有趣的建议如开发专门执行Java的协处理器;将Java byte code转化为本地代码;甚至是将Java源程序先转化为C++,然后编译成C++可执行文件等等。那时候,不少公司都开发了自己的Java虚拟机,比如 IBM,微软。到后来,随着Java的不断发展和成熟,最后SUN的Hotspot引擎脱颖而出了,成了最广泛使用的Java虚拟机。

Hotspot Engine到底是如何工作的呢?
当Java虚拟机拿到byte code的时候,它总是先要解释执行。与此同时,它观察和记录程序的执行特点。随着程序的不断运行,它对程序的行为越来越清楚,它会发现程序中的热点(就是所谓的Hotspot)。这些热点就是频繁被执行,占用CPU最多的程序段。这时候,它就会编译这段程序,将它转化为本地代码(Native code),,由于它在这个时候掌握了程序的执行特点,所以它敢于“大胆”地进行程序优化,比如说最常见的Method inlining。这样一来,瓶颈被突破了,程序运行速度立刻得到提升,在很多类型的运算上,其速度和C++相似,甚至更快。这就是所谓的“动态优化”。
有人说了,Java code怎么可能比C++更快呢?原因很简单,C++进行的优化是静态优化,都是在编译的时候进行的。一旦编译链接生成的可执行本地代码,就盖棺定论了,不能更改了,除非是Hacker或是病毒。就现在的编译技术来看,静态优化在总体上还是最成熟的,并且在编译的时候它没有时间压力,可以花很长时间来优化程序。这点Java和.NET是不允许的。但是静态优化也有它的缺点,因为它不知道这些程序在运行的时候具体会有什么特征,无法针对性地进行优化。比如它就不可能“大胆”的进行Method inlining。因为它胆子大了就可能犯错误。比如一个Class A,它有个简单函数public int Foo() {return 3;},它的两个子类B和C Override了这个Foo()函数。那么在静态编译的时候,C++的编译器不能将Foo()这个函数作inlining优化,因为它不知道在运行的时候到底是A,还是B或是C的Foo()被调用。而Java的虚拟机在运行时就会知道这些信息。如果发现运行的时候是B的Foo()在反复被调用,那么它就大胆的将B的Foo()拿到调用者的程序里面来,这样的inlining处理避免了Function call的开销(仔细说就是No method call;No dynamic dispatch;Possible to constant-fold the value)。对于简单的小函数,调用开销往往比执行还费时间。省略了这些开销性能会成倍的提高。如果这些小函数被上千上万次的调用,那么这样优化下来的效果就非常明显了。这也就是Java在有的时候比C++更快的原因之一。当然,Java做优化实际上相当复杂,因为“大胆”优化有时候也会出现问题。比如将B的Foo()的inlining了,结果突然的蹦出一个对A的Foo()的调用,那程序岂不是要出问题?这个时候呢,Java要退一步,进行反优化(De-optimization),以确保程序的正确。
就这样,Java的虚拟机“骑着毛驴看账本---走着瞧”。一边执行,一边优化,运行不停,优化不止。从外表上看,Java的程序执行会不停的有小的波动。我说的动态优化/反优化就是原因之一(还有很多其他原因)。Java这种特性非常适合长时间运行的服务器端程序,因为这样Hotspot才有足够的机会对程序进行优化。如果程序只是简单的“Hello world”,那Hotspot一点忙帮不上。并且对于“Hello world”这么个简单的程序,一个Java VM也要启动,这就像你点着了一台锅炉,只是想煎一个鸡蛋。好多人觉得Java慢,最初的影像可能就是来源于此。
有人这时候一定会问:“既然这样,那为什么Hotspot不对程序就行全盘优化,那样岂不是更好?”。问题是这样的,优化是有代价的。比如一段程序运行要 2毫秒,优化要10毫秒。如果这段程序的执行密度很低,那么Hotspot就会觉得优化不划算而不予优化。你不妨这样想,Hotspot是一个精明的商 人,赔本的生意它绝对不会做。
最后再指出一点,那就是Hotspot有客户机和服务器两套(-client, -server),它们有不同的优化方针和策略,具体如何,你可以看看Sun的技术文档。
.NET的JIT.NET的开发研制是在Java之后,它应该仔细研究过Java的各种有缺点。说实在的,当听说.NET采用的是JIT技术的时候,我有些吃惊。因为这种技术Java早期曾经采用过,后来被Hotspot取代了。不知道为什么微软会捡起来。

JIT的工作流程大概是这样的。当它载入一个Type的时候(近似为Java的类),它对这个Type里所有的函数都生成一个Stub。大家可以大概想象为“空壳函数”。当程序执行调用到某一个函数的时候,.NET的虚拟机(CLR, Common Language Runtime)将任务转交给JIT。JIT将这个函数的实体IL代码现场编译成本地机器语言(还要根据.NET的Meta Data),然后将这段程序插入到“空壳函数”中去。从此往后,所有对该函数的调用就是在执行这段机器代码。很简单是吧!至于那些从来没有被调用的函数, CLR也不去理睬它们,任凭那个“空壳函数”挂在那里“随风荡漾”。
就我个人看法,我觉得这种技术比Java的Hotspot要落后,无法达到Java那种“动态优化”的效果。JIT一次编译完成后优化就那样了,不会根据程序运行的特点进行不断的跟踪和调整。
JIT是要花时间的,这会影响程序的性能。并且JIT生成的本地机器代码存储在该程序的内存空间。程序一旦退出,这些代码就消失了。下回你启动运行这个程序,JIT要重复以前的工作。同样的工作反复进行是一种浪费。这些情况微软很清楚。微软的解决办法是"Install-time compilation",或者叫Pre-JIT。在安装.NET Framework的时候,除了将一个个的Assembly拷贝到你的机器上并登记注册外,一个叫做NGen的程序还会将这些Assembly悄悄编译成本地代码,放置在一个秘密的地方(NGen Cache)。这样今后在你的程序调用.NET系统的函数的时候,JIT就不用现场为你编译了,.NET会使用那些事先编译好的本地代码。当然如果你愿意,你可以将你自己开发的应用程序用NGen处理一下,生成Native code。

使用NGen有一些注意事项,尤其是.NET 2.0这部分变化比较大,你在使用前自己要好好看看技术文档。
那.NET的程序能跨平台吗?有人一定会问。
是可以的。虽然可供跨的平台很有限。比如你可以将你在32位Windows平台上开发的.NET程序直接搬到64位Windows平台上去。你的程序立刻就是64位的了。注意,这一点和其他32位程序不一样,那些程序是在64位平台上的一个微环境里运行(叫做Wow64),它们还是32位程序。而你的.NET程序却是正儿八经的64位程序了。这都是托.NET虚拟机的福。不过你先别高兴太早,要做到这点,你写程序的时候还是有一些制约的。比如你如果在.NET程序里调用了32位的COM程序或是其它32位Native Code,那么你的程序就不可能变成64位的程序了。原因吗,那就要说到64位Windows了,不过那就又是一个大话题了,这里且按下不表。
================================================================================
好了,看到这里,大家可能对Java和.NET的程序运行有了一个了解了。大家是不是觉得微软的.NET有些“面”啊?
我自己认为Technology approach是一个问题,具体的Implementation又是一个问题。评判Java和.NET那个设计的更好不是件简单的事情。就现在的情况来看,这两位各有千秋。比如说Java的Object serialization/de-serialization就做的很出色,.NET built-in的object还不错,但一旦serialization/de-serialization User-defined的Object,问题马上就来了。数据量一大,程序Painful slow。为什么这样我不知道,也是只是.NET 2.0 Beta版本的一个Bug(希望是这样)。而另一方面呢,.NET的Generics非常的先进,是彻头彻尾的Generics,彻底消除了 Boxing/Un-boxing和Down-Casting,这方面的性能迅猛提高。而Java的Generics不过是一个“障眼法”,和.NET根本没法比。
Competition makes everybody better。有竞争才会有发展,这是一个最浅显的道理。希望Java和.NET这对冤家对头能相互砥砺,不断的提高,呈现给我们更好的技术。那才是我们广大程序员的福祉!
http://www.cchere.com/cbbs/Ps/RA2.php?AID=402278| IBM Java如何做到高性能GC的实现内幕 |
| 来源:ChinaITLab 收集整理 |
| 2004-8-6 10:10:00 |
|
IBM JVM的GC分为三个步骤,Mark phase(标记),Sweep phase(清扫),Compaction phase(内存紧缩). 在了解这些过程之前,我们先看一下IBMJava中的对象的Layout和Heap lay out 一个Java对象在IBM vm中的结构如下
1.size+flags
2.mptr
3.locknflags
4.objectdata
size+flags
这是一个4byte的slot(32 平台)。这个slot的主要功能就是描述对象的尺寸。由于IBMJava中的对象都是以8byte的倍数分配的,因此对象的尺寸其实就是真实尺寸/8存放在4byte的slot中。另外在这个slot的低三位是保留字段起到标记对象的作用。他们分别为 bit1:swapped bit,这个交换位被用于Compaction phase即内存紧缩阶段使用。同时 这一位在标记堆栈溢出的时候(mark stack overflow)也被用于标记NotYetScanned状态. bit2:dosed bit.这个位用于标示这个对象是否被某个堆栈或者寄存器reference到了。
如果这个标志被至位则这个对象就不能在当前的GC cycle中被删除。而且如果某个reference指向的内存不是一个真实的reference比如是一个简单的float 或者integer变量但是它的值恰巧就是Heap中某个Object的地址的时候,我们就不能修改这个refernece。这种对象的bit2也被置为 1。bit3:pinned bit。标记一个对象是否是一个一个钉扣对象(PINNED object)。一个Pinned Object也不能被GC删除,因为他们可能在Heap之外被reference到了。典型的一个例子就是Thread,还记得我上面说的僵死县城么?它不能被删除的道理就是这个。另外一种PinnedObject就是 JNI Object,即被本地代码使用的对象。
Mptr:
在32平台上也是4byte的slot。Mptr有两个功能,
1。如果mptr不是一个数组,则Mptr指向一个方法块(method block),你可以通过这个method block来得到一个类块(class block)。这个类块,告诉你这个Object是属于哪个class的实例。method block和class block由Class Loader分配,而不是heap在heap中进行分配
2。如果mptr是一个数组(Array),mptr包含了这个对象中,数组的元素个数。 lockflags
在32平台上也是4byte的slot,但是这个slot只有低4位被用到。
bit2:是array flag.如果这个位被置位,那么这个对象就是一个数组同时mptr字段就包含了数组的元素个数。
bit4是hashed和moved bit.如果这个位被置位,那么他就告诉我们这个对象在被hashed以后被删除了。
Object Data:
就是这个对象本身的数据
Heap layout:
heap top
heap limit
heap base
heap base是heap的起始地址,heap top是heap的结束地址。heaplimit 是当前程序使用的那段heap可以进行扩展和收缩的极限。你可以用-Xmx参数在java运行的时候对heap top和heap base进行控制。
Alloc bits 和 mark bits
heap top allocmax markemax
heap limit alloc size marksize
heap base
上面这个结构描述了heap和alloc bits 以及,markbits之间的关系。allocbits和markbits都是元素为1个bit的vector。他们与heap有同样的长度,下面是两个对象被分配以后在heap和两个vector中的表现
heaptop allocmax markmax
heaplimit allocsize marksize
object2top
.
.
object2base object2allocbit object2markbit
object1top
.
object1base object1allocbit
如上面的结构,如果一个对象在heap被alloc出来,那么在allocbits中就标示出这个对象的起始地址所在的地址。allocbits中只标记起始地址。但是这个过程告诉我们这个对象在那里被创建,但是不告诉我们这个对象是否存活。当在mark phase中如果某一个对象比如object2仍然存活,那么就在markbits中对应的地址上标记一下The free list
IBM jvm中的空闲块用用一个free list链标示。如图
freechunck1 freechunck2 freechunckn
size size size
next------------->next--->.........next--->NULL
freeStorage freeStorage freestorge
有了这些基本概念我们来看看Mark phase的工作情况
MarkPhase
GC的Mark phase将标记所有还活着的对象。这个标记所有可达对象的过程称为tracing。Jvm的活动状态(active state)是由下面几个部分组成的。1.每个线程的保存寄存器(saved registers)2.描述线程的堆栈3.Java类中的静态元素3.以及局部和全局的JNI(Java Native Interface)引用。在Jvm中的方法调用都在C Stack上引发一个Frame。这个Frame包含了,对象实例,为局部变量的assignment结果或者传入方法的参数。所有这些引用在 Tracing过程中都被同等对待。实际上,我们可以把一个线程的堆栈看城一系列4-bytes slot的集合,然后对每一个堆栈都从顶向下对这些slot进行扫描。在扫描的过程中都必须校验每个slot是否指向heap当中的一个真实的对象。因为在前面我就说过,很有可能这些slot值仅仅是一个int或float但是他们的值恰巧就等于heap中的一个对象地址。因此在扫描的时候必须相当的保守,扫描的时候必须保证所有的指针都是一个对象,而且这个对象没有在GC中被删除。只有符合下面条件的slot才是一个指向对象的指针。1.必须以8- byte的倍数分配的内存2.必须在heap的范围之内(即大于heapbase小于heaptop)3.对应的allocbit必须置为1。满足这些条件的对象引用我们称为roots,并且把他们的dosed bit置为1表示不能被GC删除。我想大家已经知道C#中为何连Int和Float都是OBject的原因了吧。在C#中因为都是OBject因此,在 tracing的过程中就减少了一次校验。这个减少对性能起到很大的影响。如果扫描完成,那么Tracing过程便能安全精确的执行。也就是说我们可以在roots中通过reference找到他对应的objects,由于他们是真实的reference,那么我们就能够在compactionphase中移动对应的对象并且修改这些reference。
Trace过程使用了一个可以容纳4k的slots的stack。所有的引用逐个push进入这个堆栈并且同时在markbits中进行标记。当push和mark的工作完成之后,我们开始pop出这些slot并且进行trace。
常规的对象(非数组对象)将通过mptr去访问classblock,classblock将会告诉我们从这个对象中找到的其他对象的 reference在那里?当我们在classblock找到一个refernce以后,如果发现他没有被mark,那么我们就在 markallocbits中mark他然后把他再压入堆栈。
数组对象利用mptr去访问每个数组元素,如果他们没有mark则mark然后压入堆栈。
Trace过程一直持续进行,直到堆栈为空。
MarkStack OverFlow
由于markStack限制了尺寸,因此它可能会溢出。如果溢出发生,那么我们就设定一个全局的标志来表明发生了MarkStack OverFlow,然后我们将那些不能push入stack的OBject的bit1设定为NotYetScanned。然后当tracing过程完成以后,检验全局标志如果发现有overflow则把NotYetScanned的对象再次压入堆栈开始新的tracing过程。
并行Mark(Parallel Mark)
由于使用逐位清扫(bitwise sweep)和内存紧缩规避功能,GC将化大部分的时间是用于Mark而非前面两项。这就导致了IBM JVM需要开发一个GC的并行版本。并行GC的目的不是以牺牲单CPU系统上的效能来换取在4,8路对称CPU系统上的高效率。
并行Mark的基本思想就是通过多个辅助线程(helper thread)和一个共享工作的工具来减少Marking的时间。在单CPU系统中,执行GC工作的只有一个主线程。Parallel mark仍然需要这个主线程的参与,他充当了管理协调的角色。这个Thread所要执行的工作和单CPU上的一样多,包括他必须扫描C-Stack来鉴别需要收集的roots指针。一个有N路对称CPU的系统自动含有n-1个helper thread并且平均分布在每个CPU上,master thread将scan完的reference集合进行分块,然后交给helper thread独立完成mark工作。
每个Helper thread都被分配了一个独立的本地mark stack,以及一个shareable queue。sharqueue将存放help thread在mark overflow的时候的NotyetScanned对象。然后由master thread将sharequeue中的对象balance到其他已经空闲的thread上去。
并发Mark(Concurrent mark)
Concurrent mark的主要目的在于当heap增长的时候减少GC的pause time。只要heap到达heap limit的时候,Concurrent mark就会被执行。在Concurrent phase中,GC要求应用中的每个线程(不是指helper thread而是应用程序自己开启的线程以便充分利用系统资源)扫描他们自己的堆栈来得到roots。然后使用这些roots来同步的trace 可达对象。Tracing工作是由一个后台的低优先级的线程执行,同时程序自己开启的线程在分配内存的时候必须执行heap lock allocation。
由于使用程序自己开启的线程并发的执行mark live objects,我们必须纪录那些已经trace过的object的变化。这个功能是采用一个叫写闸(write barrier) 来实现的。这个写闸在每次改变引用的时候被激活。它告诉我们什么时候一个对象被跟新过了,以便我们从新扫描那部分heap。写闸的具体实现是Heap会分配出512byte的内存段每个段都分配了一个byte在卡表中(card table)。无论何时一个对象的reference被更新cardtable将同步纪录这个对象的起始地址。使用Byte而不用bit的原因是写 byte要比写bit快2倍,而且我们可能希望空余的bit会在未来被用到。
当Concurrent mark执行完毕以后,STW collection(stop total world)将会被执行。stw的意思是指suspend所有程序自己开启的线程。因此我们可以看到如果使用Concurrent mark那么在mark的时候应用程序不会完全停止。只有收集需要进行collection时以后才执行stw。在上面的讨论中我们认为STW的 mark,sweep,compaction可能会暂停应用程序很长时间。其实IBM的gc的停止比我们想象中要短的多。STW只有在下面这些条件才执行 1.到达heap limited或者allocation fail2.System.gc方法被调用3.Concurrent mark 完成所有的工作因此我们可以通过调整系统参数来控制STW的执行。当STW执行之前,会扫描卡表检查那些heap需要从新trace,然后执行通常的 sweep。
Concurrent mark带来的好处就是减少STW所带来的停顿时间。但是这也需要程序自己开启的线程付出一定的代价。这个代价就是需要执行heap lock allocation。这个代价的大小主要取决于CPU有多少超标量流水是空闲的。在Sun的HotSpot中仍然使用单个GC线程进行全部的mark工作,因此IBMJava的GC要快的多而且有跟少的延迟。
Sweep phase
执行完mark以后就执行 sweep。sweep phase其实是最有趣的一个阶段,在我们上面的讨论中一个比较尖锐的问题是GC控制对象的生存情况是否必要。这个在Sun的Java中可能存在,但是在 IBMjava中GC根本不知道什么时候sweep了一个对象,甚至不知道sweep了那个对象。
在Sun的HotSpot 种的sweep采用了通常的做法就是扫描allocbits和makrbits的交叉项,把那些没有交叉的内存给sweep掉。而在IBM种采用了一种相当高效的方法叫bitsweep。这种方法直接在markbits中寻找长时间不使用的0位(1位代表mark了0位代表空闲或者
需要sweep的内存)。一旦找到长时间不使用的0位,那么我们就去对照在Heap中对应的地址来决定需要释放的内存。如果空闲的总数超过 512*Header size那么我们就把这个free块移到free list中。而那些小的内存片则不会放入free list,因为他们会在相邻的对象执行清除或者compact heap的时候被一起覆盖掉。采用了bitsweep以后,GC根本不需要删除单个对象,因为我们知道整个要删除的Chunck就是一个free storge。因此实际上,我们删除一个chunck的时候我们根本不知道删除了几个对象以及删除了那些对象。清扫完成以后,GC会把makrbit copy 到 allocbit上,保证所有的对象的reference都有效。因此myan提到effile中把refernece和本地的object分开处理,其实对于gc来说不是一个好主意。全部依靠reference可以一次清除多个对象,而分开处理就必须使用Hotspot的方法降低GC的性能。
Parallel bitwise sweep
IBMJava为了多对称系统也设计了并行版本的bitwise sweep。其原理和并行Mark一致。
Compaction phase
当清扫完毕以后,就开始执行compaction。Java的compaction是相当复杂的。因为移动一个对象,必须修改他们所有的 reference。而且如果一个reference是来自一个stack,并且我们不能确定它是否指向一个真实的object,可能它仅仅是一个 float,那么这些object
就不能被移动。一个对象是否可以被移动设计到它的”dosed”位是被置位。同样 pinned object,那些被JNI引用的对象,只有到Jni unnpined的时候才能被移动。Pinned object的可否移动的判断更加复杂。主要依赖于mptr低三位标示它是否被清扫掉。标示被清扫的 的位存在两个地方:1. The size + flags 字段,如果被标记到OLINK_IsSwapped. 2. mptr 被标记到GC_FirstSwapped。因此看来Java把int 这种普通类型和Object分开处理在GC中会造成过多的不能移动的对象和过多的碎片。对于GC来说很不明智,而且在其他地方也看不出有什么必要分开处理。
否则干吗还要做一个Integer类呢?而C#在这点上来说优势更大。
IBM java中的Compaction算法为了避免过多的移动对象和利用移动处理一些没有被收集的空闲块因而出奇的复杂。他采用了一种和hotspot不同的算法。Sam Borman举了一个很形象的例子,把整个Heap想象成一个仓库,仓库堆放了不同尺寸的家具。由于出库的原因,家具之间存在着一定的空隙。 Compaction的工作就是把家具往一个方向推来清理空隙。把靠近墙的家具推倒墙边,然后让第二个家具与第一件紧靠在一起。以此类推,然后所有得家具靠再一起,而空隙在另外一边。Pinned and dosed objects 不能被移动的情况会复杂化这个算法,但是主要思想不变。
紧缩规避(Compaction avoidance)
Compaction avoidance的主要目的在于开辟较大内存的时候降低Compaction的使用次数来保证GC pause time能够足够短。在Ibm jvm中的Compaction的执行条件如下:
1. 如果开辟一个大内存的时候遍例Free list发现没有合适的free storge激发alloc failure时间
2. 在上次GC过程中出现了一次alloc failure
3. 被激活的Heap(heap limited到heap base之间的heap)只有5%为free
4. 被激活的Heap不大于128K
IBM jvm在上面四个条件中满足一项就执行compaction。其中最为常见的是第一种,
为了避免Companction,Ibmjava采用了紧缩规避的方法。这个方法称为荒野内存(wilderness preservation),也就是在heap limit之上再开辟一块内存。这块内存保持原始状态,其大小为激活Heap的5%,默认设置为3M.如果一旦有一个大块内存需要开辟,而 freelist中没有合适的storge的时候就使用wilderness preservation保证不抛出 alloc failure。一旦wilderness被用尽则产生一个alloc failure通知GC执行Compaction。通常来说wilderness preservation能够保证不使用Compaction,因为基本上使用到wilderness的对象是这个应用程序中最大的对象。
Ibm的JVM关键实现就是这样,我们可以看到ibmJVM使用的很多算法让我们原本考虑的一些gc的困难降低到了一个可以忍受的限度,比如STW的 pause time,其实只涉及了sweep和compaction,mark phase在程序运行的同时就完成了基本不影响程序的正常工作。而且由于使用了bitsweep,和紧缩规避使得STW的时间大大降低,他们两个的工作量的总和不到Mark的30%。而且在多对称处理器上又采用并行mark和sweep,可以近一部的提高GC效率。
http://www.chinaitlab.com/www/news/article_show.asp?id=22098
http://forum.javaeye.com/viewtopic.php?t=9003&postdays=0&postorder=asc&start=0
英语好的朋友可以到SUN的HotSpot网站上看看, java的每个版本都有对hotspot进行大幅的改进:
http://java.sun.com/products/hotspot/index.html"Java HotSpot means Performance" The Java HotSpot product line delivers the highest possible performance for Java applications. Sun Microsystems has a version of Java HotSpot-technology-based VM for server-side performance and now a version for client-side performance. These two solutions share the Java HotSpot runtime environment, but have different compilers suited to the distinctly different performance characteristics of clients and servers.
Both solutions deliver extremely reliable, secure and maintainable environments, to meet the demands of today's enterprise customers. See the whitepaper for more information on reliability and security and easy, complete debugging with the Java HotSpot product line. Java HotSpot is safer than static compilers!
White Paper
A technical white paper describing Java HotSpot technology and architecture as of the Java 2 Platform is available.
Trackback: http://tb.donews.net/TrackBack.aspx?PostId=446051