Java代码的静态编译和动态编译中的问题比较Word格式.docx

上传人:b****3 文档编号:17617387 上传时间:2022-12-07 格式:DOCX 页数:9 大小:110.17KB
下载 相关 举报
Java代码的静态编译和动态编译中的问题比较Word格式.docx_第1页
第1页 / 共9页
Java代码的静态编译和动态编译中的问题比较Word格式.docx_第2页
第2页 / 共9页
Java代码的静态编译和动态编译中的问题比较Word格式.docx_第3页
第3页 / 共9页
Java代码的静态编译和动态编译中的问题比较Word格式.docx_第4页
第4页 / 共9页
Java代码的静态编译和动态编译中的问题比较Word格式.docx_第5页
第5页 / 共9页
点击查看更多>>
下载资源
资源描述

Java代码的静态编译和动态编译中的问题比较Word格式.docx

《Java代码的静态编译和动态编译中的问题比较Word格式.docx》由会员分享,可在线阅读,更多相关《Java代码的静态编译和动态编译中的问题比较Word格式.docx(9页珍藏版)》请在冰豆网上搜索。

Java代码的静态编译和动态编译中的问题比较Word格式.docx

可将此格式看作Java平台,因为它定义了执行Java程序所需的所有信息。

Java程序执行引擎,也称作Java运行时环境(JRE),包含了为特定的本地平台实现Java平台的虚拟机。

例如,基于Linux的Intelx86平台、SunSolaris平台和AIX操作系统上运行的IBMSystemp平台,每个平台都拥有一个JRE。

这些JRE实现实现了所有的本地支持,从而可以正确执行为Java平台编写的程序。

事实上,操作数堆栈的大小有实际限制,但是编程人员极少编写超出该限制的方法。

JVM提供了安全性检查,对那些创建出此类方法的编程人员进行通知。

Java平台程序表示的一个重要部分是字节码序列,它描述了Java类中每个方法所执行的操作。

字节码使用一个理论上无限大的操作数堆栈来描述计算。

这个基于堆栈的程序表示提供了平台无关性,因为它不依赖任何特定本地平台的CPU中可用寄存器的数目。

可在操作数堆栈上执行的操作的定义都独立于所有本地处理器的指令集。

Java虚拟机(JVM)规范定义了这些字节码的执行(参见参考资料)。

执行Java程序时,用于任何特定本地平台的任何JRE都必须遵守JVM规范中列出的规则。

因为基于堆栈的本地平台很少(IntelX87浮点数协处理器是一个明显的例外),所以大多数本地平台不能直接执行Java字节码。

为了解决这个问题,早期的JRE通过解释字节码来执行Java程序。

即JVM在一个循环中重复操作:

◆获取待执行的下一个字节码;

◆解码;

◆从操作数堆栈获取所需的操作数;

◆按照JVM规范执行操作;

◆将结果写回堆栈。

这种方法的优点是其简单性:

JRE开发人员只需编写代码来处理每种字节码即可。

并且因为用于描述操作的字节码少于255个,所以实现的成本比较低。

当然,缺点是性能:

这是一个早期造成很多人对Java平台不满的问题,尽管拥有很多其他优点。

解决与C或C++之类的语言之间的性能差距意味着,使用不会牺牲可移植性的方式开发用于Java平台的本地代码编译。

编译Java代码

尽管传闻中Java编程的“一次编写,随处运行”的口号可能并非在所有情况下都严格成立,但是对于大量的应用程序来说情况确实如此。

另一方面,本地编译本质上是特定于平台的。

那么Java平台如何在不牺牲平台无关性的情况下实现本地编译的性能?

答案就是使用JIT编译器进行动态编译,这种方法已经使用了十年(参见图1):

图1.JIT编译器

使用JIT编译器时,Java程序按每次编译一个方法的形式进行编译,因为它们在本地处理器指令中执行以获得更高的性能。

此过程将生成方法的一个内部表示,该表示与字节码不同但是其级别要高于目标处理器的本地指令。

(IBMJIT编译器使用一个表达式树序列表示方法的操作。

)编译器执行一系列优化以提高质量和效率,最后执行一个代码生成步骤将优化后的内部表示转换成目标处理器的本地指令。

生成的代码依赖运行时环境来执行一些活动,比如确保类型转换的合法性或者对不能在代码中直接执行的某些类型的对象进行分配。

JIT编译器操作的编译线程与应用程序线程是分开的,因此应用程序不需要等待编译的执行。

图1中还描述了用于观察执行程序行为的分析框架,通过周期性地对线程取样找出频繁执行的方法。

该框架还为专门进行分析的方法提供了工具,用来存储程序的此次执行中可能不会改变的动态值。

因为这个JIT编译过程在程序执行时发生,所以能够保持平台无关性:

发布的仍然是中立的Java平台代码。

C和C++之类的语言缺乏这种优点,因为它们在程序执行前进行本地编译;

发布给(本地平台)执行环境的是本地代码。

挑战

尽管通过JIT编译保持了平台无关性,但是付出了一定代价。

因为在程序执行时进行编译,所以编译代码的时间将计入程序的执行时间。

任何编写过大型C或C++程序的人都知道,编译过程往往较慢。

为了克服这个缺点,现代的JIT编译器使用了下面两种方法的任意一种(某些情况下同时使用了这两种方法)。

第一种方法是:

编译所有的代码,但是不执行任何耗时多的分析和转换,因此可以快速生成代码。

由于生成代码的速度很快,因此尽管可以明显观察到编译带来的开销,但是这很容易就被反复执行本地代码所带来的性能改善所掩盖。

第二种方法是:

将编译资源只分配给少量的频繁执行的方法(通常称作热方法)。

低编译开销更容易被反复执行热代码带来的性能优势掩盖。

很多应用程序只执行少量的热方法,因此这种方法有效地实现了编译性能成本的最小化。

动态编译器的一个主要的复杂性在于权衡了解编译代码的预期获益使方法的执行对整个程序的性能起多大作用。

一个极端的例子是,程序执行后,您非常清楚哪些方法对于这个特定的执行的性能贡献最大,但是编译这些方法毫无用处,因为程序已经完成。

而在另一个极端,程序执行前无法得知哪些方法重要,但是每种方法的潜在受益都最大化了。

大多数动态编译器的操作介于这两个极端之间,方法是权衡了解方法预期获益的重要程度。

Java语言需要动态加载类这一事实对Java编译器的设计有着重要的影响。

如果待编译代码引用的其他类还没有加载怎么办?

比如一个方法需要读取某个尚未加载的类的静态字段值。

Java语言要求第一次执行类引用时加载这个类并将其解析到当前的JVM中。

直到第一次执行时才解析引用,这意味着没有地址可供从中加载该静态字段。

编译器如何处理这种可能性?

编译器生成一些代码,用于在没有加载类时加载并解析类。

类一旦被解析,就会以一种线程安全的方式修改原始代码位置以便直接访问静态字段的地址,因为此时已获知该地址。

IBMJIT编译器中进行了大量的努力以便使用安全而有效率的代码补丁技术,因此在解析类之后,执行的本地代码只加载字段的值,就像编译时已经解析了字段一样。

另外一种方法是生成一些代码,用于在查明字段的位置以前一直检查是否已经解析字段,然后加载该值。

对于那些由未解析变成已解析并被频繁访问的字段来说,这种简单的过程可能带来严重的性能问题。

动态编译的优点

动态地编译Java程序有一些重要的优点,甚至能够比静态编译语言更好地生成代码,现代的JIT编译器常常向生成的代码中插入挂钩以收集有关程序行为的信息,以便如果要选择方法进行重编译,就可以更好地优化动态行为。

关于此方法的一个很好的例子是收集一个特定arraycopy操作的长度。

如果发现每次执行操作时该长度基本不变,则可以为最频繁使用的arraycopy长度生成专门的代码,或者可以调用调整为该长度的代码序列。

由于内存系统和指令集设计的特性,用于复制内存的最佳通用例程的执行速度通常比用于复制特定长度的代码慢。

例如,复制8个字节的对齐的数据可能需要一到两条指令直接复制,相比之下,使用可以处理任意字节数和任意对齐方式的一般复制循环可能需要10条指令来复制同样的8个字节。

但是,即使此类专门的代码是为某个特定的长度生成的,生成的代码也必须正确地执行其他长度的复制。

生成代码只是为了使常见长度的操作执行得更快,因此平均下来,性能得到了改进。

此类优化对大多数静态编译语言通常不实用,因为所有可能的执行中长度恒定的操作比一个特定程序执行中长度恒定的操作要少得多。

此类优化的另一个重要的例子是基于类层次结构的优化。

例如,一个虚方法调用需要查看接收方对象的类调用,以便找出哪个实际目标实现了接收方对象的虚方法。

研究表明:

大多数虚调用只有一个目标对应于所有的接收方对象,而JIT编译器可以为直接调用生成比虚调用更有效率的代码。

通过分析代码编译后类层次结构的状态,JIT编译器可以为虚调用找到一个目标方法,并且生成直接调用目标方法的代码而不是执行较慢的虚调用。

当然,如果类层次结构发生变化,并且出现另外的目标方法,则JIT编译器可以更正最初生成的代码以便执行虚调用。

在实践中,很少需要作出这些更正。

另外,由于可能需要作出此类更正,因此静态地执行这种优化非常麻烦。

因为动态编译器通常只是集中编译少量的热方法,所以可以执行更主动的分析来生成更好的代码,使编译的回报更高。

事实上,大部分现代的JIT编译器也支持重编译被认为是热方法的方法。

可以使用静态编译器(不太强调编译时间)中常见的非常主动的优化来分析和转换这些频繁执行的方法,以便生成更好的代码并获得更高的性能。

这些改进及其他一些类似的改进所产生的综合效果是:

对于大量的Java应用程序来说,动态编译已经弥补了与C和C++之类语言的静态本地编译性能之间的差距,在某些情况下,甚至超过了后者的性能。

缺点

但是,动态编译确实具有一些缺点,这些缺点使它在某些情况下算不上一个理想的解决方案。

例如,因为识别频繁执行的方法以及编译这些方法需要时间,所以应用程序通常要经历一个准备过程,在这个过程中性能无法达到其最高值。

在这个准备过程中出现性能问题有几个原因。

首先,大量的初始编译可能直接影响应用程序的启动时间。

不仅这些编译延迟了应用程序达到稳定状态的时间(想像Web服务器经历一个初始阶段后才能够执行实际有用的工作),而且在准备阶段中频繁执行的方法可能对应用程序的稳定状态的性能所起的作用也不大。

如果JIT编译会延迟启动又不能显著改善应用程序的长期性能,则执行这种编译就非常浪费。

虽然所有的现代JVM都执行调优来减轻启动延迟,但是并非在所有情况下都能够完全解决这个问题。

其次,有些应用程序完全不能忍受动态编译带来的延迟。

如GUI接口之类交互式应用程序就是这样的例子。

在这种情况下,编译活动可能对用户使用造成不利影响,同时又不能显著地改善应用程序的性能。

最后,用于实时环境并具有严格的任务时限的应用程序可能无法忍受编译的不确定性性能影响或动态编译器本身的内存开销。

因此,虽然JIT编译技术已经能够提供与静态语言性能相当(甚至更好)的性能水平,但是动态编译并不适合于某些应用程序。

在这些情况下,Java代码的提前(Ahead-of-time,AOT)编译可能是合适的解决方案。

AOTJava编译

大致说来,Java语言本地编译应该是为传统语言(如C++或Fortran)而开发的编译技术的一个简单应用。

不幸的是,Java语言本身的动态特性带来了额外的复杂性,影响了Java程序静态编译代码的质量。

但是基本思想仍然是相同的:

在程序执行前生成Java方法的本地代码,以便在程序运行时直接使用本地代码。

目的在于避免JIT编译器的运行时性能消耗或内存消耗,或者避免解释程序的早期性能开销。

挑战

动态类加载是动态JIT编译器面临的一个挑战,也是AOT编译的一个更重要的问题。

只有在执行代码引用类的时候才加载该类。

因为是在程序执行前进行AOT编译的,所以编译器无法预测加载了哪些类。

就是说编译器无法获知任何静态字段的地址、任何对象的任何实例字段的偏移量或任何调用的实际目标,甚至对直接调用(非虚调用)也是如此。

在执行代码时,如果证明对任何这类信息的预测是错误的,这意味着代码是错误的并且还牺牲了Java的一致性。

因为代码可以在任何环境中执行,所以类文件可能与代码编译时不同。

例如,一个JVM实例可能从磁盘的某个特定位置加载类,而后面一个实例可能从不同的位置甚至网络加载该类。

设想一个正在进行bug修复的开发环境:

类文件的内容可能随不同的应用程序的执行而变化。

此外,Java代码可能在程序执行前根本不存在:

比如Java反射服务通常在运行时生成新类来支持程序的行为。

缺少关于静态、字段、类和方法的信息意味着严重限制了Java编译器中优化框架的大部分功能。

内联可能是静态或动态编译器应用的最重要的优化,但是由于编译器无法获知调用的目标方法,因此无法再使用这种优化。

内联

内联是一种用于在运行时生成代码避免程序开始和结束时开销的技术,方法是将函数的调用代码插入到调用方的函数中。

但是内联最大的益处可能是优化方可见的代码的范围扩大了,从而能够生成更高质量的代码。

下面是一个内联前的代码示例:

intfoo(){intx=2,y=3;

returnbar(x,y);

}finalintbar(inta,intb){returna+b;

}

如果编译器可以证明这个bar就是foo()中调用的那个方法,则bar中的代码可以取代foo()中对bar()的调用。

这时,bar()方法是final类型,因此肯定是foo()中调用的那个方法。

甚至在一些虚调用例子中,动态JIT编译器通常能够推测性地内联目标方法的代码,并且在绝大多数情况下能够正确使用。

编译器将生成以下代码:

returnx+y;

在这个例子中,简化前名为值传播的优化可以生成直接返回5的代码。

如果不使用内联,则不能执行这种优化,产生的性能就会低很多。

如果没有解析bar()方法(例如静态编译),则不能执行这种优化,而代码必须执行虚调用。

运行时,实际调用的可能是另外一个执行两个数字相乘而不是相加的bar方法。

所以不能在Java程序的静态编译期间直接使用内联。

AOT代码因此必须在没有解析每个静态、字段、类和方法引用的情况下生成。

执行时,每个这些引用必须利用当前运行时环境的正确值进行更新。

这个过程可能直接影响第一次执行的性能,因为在第一次执行时将解析所有引用。

当然,后续执行将从修补代码中获益,从而可以更直接地引用实例、静态字段或方法目标。

另外,为Java方法生成的本地代码通常需要使用仅在单个JVM实例中使用的值。

例如,代码必须调用JVM运行时中的某些运行时例程来执行特定操作,如查找未解析的方法或分配内存。

这些运行时例程的地址可能在每次将JVM加载到内存时变化。

因此AOT编译代码需要绑定到JVM的当前执行环境中,然后才能执行。

其他的例子有字符串的地址和常量池入口的内部位置。

在WebSphereRealTime中,AOT本地代码编译通过jxeinajar工具(参见图2)来执行。

该工具对JAR文件中所有类的所有方法应用本地代码编译,也可以选择性地对需要的方法应用本地代码编译。

结果被存储到名为JavaeXEcutable(JXE)的内部格式中,但是也可轻松地存储到任意的持久性容器中。

您可能认为对所有的代码进行静态编译是最好的方法,因为可以在运行时执行最大数量的本地代码。

但是此处可以作出一些权衡。

编译的方法越多,代码占用的内存就越多。

编译后的本地代码大概比字节码大10倍:

本地代码本身的密度比字节码小,而且必须包含代码的附加元数据,以便将代码绑定到JVM中,并且在出现异常或请求堆栈跟踪时正确执行代码。

构成普通Java应用程序的JAR文件通常包含许多很少执行的方法。

编译这些方法会消耗内存却没有什么预期收益。

相关的内存消耗包括以下过程:

将代码存储到磁盘上、从磁盘取出代码并装入JVM,以及将代码绑定到JVM。

除非多次执行代码,否则这些代价不能由本地代码相对解释的性能优势来弥补。

图2.jxeinajar

跟大小问题相违背的一个事实是:

在编译过的方法和解释过的方法之间进行的调用(即编译过的方法调用解释过的方法,或者相反)可能比这两类方法各自内部之间进行的调用所需的开销大。

动态编译器通过最终编译所有由JIT编译代码频繁调用的那些解释过的方法来减少这项开销,但是如果不使用动态编译器,则这项开销就不可避免。

因此如果是选择性地编译方法,则必须谨慎操作以使从已编译方法到未编译方法的转换最小化。

为了在所有可能的执行中都避免这个问题而选择正确的方法会非常困难。

优点

虽然AOT编译代码具有上述的缺点和挑战,但是提前编译Java程序可以提高性能,尤其是在不能将动态编译器作为有效解决方案的环境中。

可以通过谨慎地使用AOT编译代码加快应用程序启动,因为虽然这种代码通常比JIT编译代码慢,但是却比解释代码快很多倍。

此外,因为加载和绑定AOT编译代码的时间通常比检测和动态编译一个重要方法的时间少,所以能够在程序执行的早期达到那样的性能。

类似地,交互式应用程序可以很快地从本地代码中获益,无需使用引起较差响应能力的动态编译。

RT应用程序也能从AOT编译代码中获得重要的收益:

更具确定性的性能超过了解释的性能。

WebSphereRealTime使用的动态JIT编译器针对在RT系统中的使用进行了专门的调整。

使编译线程以低于RT任务的优先级操作,并且作出了调整以避免生成带有严重的不确定性性能影响的代码。

但是,在一些RT环境中,出现JIT编译器是不可接受的。

此类环境通常需要最严格的时限管理控制。

在这些例子中,AOT编译代码可以提供比解释过的代码更好的原始性能,又不会影响现有的确定性。

消除JIT编译线程甚至消除了启动更高优先级RT任务时发生的线程抢占所带来的性能影响。

优缺点统计

动态(JIT)编译器支持平台中立性,并通过利用应用程序执行的动态行为和关于加载的类及其层次结构的信息来生成高质量的代码。

但是JIT编译器具有一个有限的编译时预算,而且会影响程序的运行时性能。

另一方面,静态(AOT)编译器则牺牲了平台无关性和代码质量,因为它们不能利用程序的动态行为,也不具有关于加载的类或类层次结构的信息。

AOT编译拥有有效无限制的编译时预算,因为AOT编译时间不会影响运行时性能,但是在实践中开发人员不会长期等待静态编译步骤的完成。

表1总结了本文讨论的Java语言动态和静态编译器的一些特性:

表1.比较编译技术

两种技术都需要谨慎选择编译的方法以实现最高的性能。

对动态编译器而言,编译器自身作出决策,而对于静态编译器,由开发人员作出选择。

让JIT编译器选择编译的方法是不是优点很难说,取决于编译器在给定情形中推断能力的好坏。

在大多数情况下,我们认为这是一种优点。

因为它们可以最好地优化运行中的程序,所以JIT编译器在提供稳定状态性能方面更胜一筹,而这一点在大量的生产Java系统中最为重要。

静态编译可以产生最佳的交互式性能,因为没有运行时编译行为来影响用户预期的响应时间。

通过调整动态编译器可以在某种程度上解决启动和确定性性能问题,但是静态编译在需要时可提供最快的启动速度和最高级别的确定性。

表2在四种不同的执行环境中对这两种编译技术进行了比较:

表2.使用这些技术的最佳环境

图3展示了启动性能和稳定状态性能的总体趋势:

图3.AOT和JIT的性能对比

使用JIT编译器的初始阶段性能很低,因为要首先解释方法。

随着编译方法的增多及JIT执行编译所需时间的缩短,性能曲线逐渐升高最后达到性能峰值。

另一方面,AOT编译代码启动时的性能比解释的性能高很多,但是无法达到JIT编译器所能达到的最高性能。

将静态代码绑定到JVM实例中会产生一些开销,因此开始时的性能比稳定状态的性能值低,但是能够比使用JIT编译器更快地达到稳定状态的性能水平。

没有一种本地代码编译技术能够适合所有的Java执行环境。

某种技术所擅长的通常正是其他技术的弱项。

出于这个原因,需要同时使用这两种编译技术以满足Java应用程序开发人员的要求。

事实上,可以结合使用静态和动态编译以便提供最大可能的性能提升——但是必须具备平台无关性,它是Java语言的主要卖点,因此不成问题。

结束语

本文探讨了Java语言本地代码编译的问题,主要介绍了JIT编译器形式的动态编译和静态AOT编译,比较了二者的优缺点。

虽然动态编译器在过去的十年里实现了极大的成熟,使大量的各种Java应用程序可以赶上或超过静态编译语言(如C++或Fortran)所能够达到的性能。

但是动态编译在某些类型的应用程序和执行环境中仍然不太合适。

虽然AOT编译号称动态编译缺点的万能解决方案,但是由于Java语言本身的动态特性,它也面临着提供本地编译全部潜能的挑战。

这两种技术都不能解决Java执行环境中本地代码编译的所有需求,但是反过来又可以在最有效的地方作为工具使用。

这两种技术可以相互补充。

能够恰当地使用这两种编译模型的运行时系统可以使很大范围内的应用程序开发环境中的开发人员和用户受益。

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 高等教育 > 院校资料

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1