该文章转载自:http://rdc.taobao.com/team/jm/archives/552
你写过超过2500行的方法么?通常来说,这么大的方法并不多见,一般都是使用机器辅助生成的为主,这种情况在模板编译或其它语言的自动转换中比较常见。例如,对一个复杂的JSP页面,机器有可能会为它生成一个复杂的servlet方法去实现。
然而在Hotspot上运行这种大方法,很可能会有性能问题。例如,把文章所附DEMO的play()方法的内容分别重复拷贝1、2、4、8、16、32次并依次运行,在我的机器(Hotspot_1.6u22/Windows)上得到的play()的执行消耗时间分别是28.43、54.72、106.28、214.41、419.30、1476.40毫秒/万次。在重复拷贝1~16次时,随着代码量增加,方法执行所消耗的时间也对应成倍增加。当重复拷贝32次时,方法却多消耗了80%的时间。如果把这个play()方法拆分成play1()和play2(),让它们的方法体都是16次的重复拷贝,play1()最后再调用play2(),那么,play1()+play2()的执行消耗时间是857.75毫秒/万次,恰好是之前重复拷贝16次所消耗的时间的两倍。为什么同样功能的一段代码放在一个方法中执行会变慢,拆分成两个方法就变快?
大家知道,JVM一开始是以解释方式执行字节码的。当这段代码被执行的次数足够多以后,它会被动态优化并编译成机器码执行,执行速度会大大加快,这就是所谓的JIT编译。DEMO的play()方法在被统计消耗时间之前,已经预热执行了2000次,满足ClientVM的方法JIT编译阈值CompileThreshold=1500次的要求,那么,它是不是真的被JIT编译了呢?我们可以增加VM参数”-XX:+PrintCompilation”调查一下。在+PrintCompilation打开以后,列出了JVM在运行时进行过JIT编译的方法。下面是经过32次重复拷贝的play()方法的JIT编译记录(只列出需要关心的部分):
34 HugeMethodDemo::buildTheWorld (184 bytes)
39 HugeMethodDemo::run (59 bytes)
而分成两部分的play1()+plaay2()的JIT编译记录则为:
18 HugeMethodDemo::play1 (4999 bytes)
19 HugeMethodDemo::play2 (4993 bytes)
36 HugeMethodDemo::buildTheWorld (184 bytes)
41 HugeMethodDemo::run (59 bytes)
显然,经过重复拷贝32次的play()方法没有经过JIT编译,始终采用解释方式执行,而分拆开的play1()+play2()经过JIT编译,所以难怪play()要慢80%。
为什么play()方法不受JVM青睐呢,是太长了么?这只能到Hotspot源码中去翻答案了。在compilationPolicy.cpp中有写道:
// Returns true if m is allowed to be compiled
bool CompilationPolicy::canBeCompiled(methodHandle m) {
if (m->is_abstract()) return false;
if (DontCompileHugeMethods && m->code_size() > HugeMethodLimit) return false;
// Math intrinsics should never be compiled as this can lead to
// monotonicity problems because the interpreter will prefer the
// compiled code to the intrinsic version. This can't happen in
// production because the invocation counter can't be incremented
// but we shouldn't expose the system to this problem in testing
// modes.
if (!AbstractInterpreter::can_be_compiled(m)) {
return false;
}
return !m->is_not_compilable();
}
当DontCompileHugeMethods=true且代码长度大于HugeMethodLimit时,方法不会被编译。DontCompileHugeMethods与HugeMethodLimit的值在globals.hpp中定义:
product(bool, DontCompileHugeMethods, true,
"don't compile methods > HugeMethodLimit")
develop(intx, HugeMethodLimit, 8000,
"don't compile methods larger than this if +DontCompileHugeMethods")
上面两个参数说明了Hotspot对字节码超过8000字节的大方法有JIT编译限制,这就是play()杯具的原因。由于使用的是product mode的JRE,我们只能尝试关闭DontCompileHugeMethods,即增加VM参数”-XX:-DontCompileHugeMethods”来强迫JVM编译play()。再次对play()进行测试,耗时855毫秒/万次,性能终于上来了,输出的JIT编译记录也增加了一行:
16 HugeMethodDemo::play (9985 bytes)
使用”-XX:-DontCompileHugeMethods”解除大方法的编译限制,一个比较明显的缺点是JVM会尝试编译所遇到的所有大方法,者会使JIT编译任务负担更重,而且需要占用更多的Code Cache区域去保存编译后的代码。但是优点是编译后可以让大方法的执行速度变快,且可能提高GC速度。运行时Code Cache的使用量可以通过JMX或者JConsole获得,Code Cache的大小在globals.hpp中定义:
define_pd_global(intx, ReservedCodeCacheSize, 48*M);
product_pd(uintx, InitialCodeCacheSize, "Initial code cache size (in bytes)")
product_pd(uintx, ReservedCodeCacheSize, "Reserved code cache size (in bytes) - maximum code cache size")
product(uintx, CodeCacheMinimumFreeSpace, 500*K, "When less than X space left, we stop compiling.")
一旦Code Cache满了,HotSpot会停止所有后续的编译任务,虽然已编译的代码不受影响,但是后面的所有方法都会强制停留在纯解释模式。因此,如非必要,应该尽量避免生成大方法;如果解除了大方法的编译限制,则要留意配置Code Cache区的大小,准备更多空间存放编译后的代码。
最后附上DEMO代码:
import java.io.StringWriter;
|
import java.util.HashMap;
|
public class HugeMethodDemo {
|
public static void main(String[] args) throws Exception {
|
HugeMethodDemo demo = new HugeMethodDemo();
|
double total = demo.run(loop);
|
double avg = total / loop / 1e6 * 1e4;
|
System.out.println(String.format(
|
"Loop=%d次, " + "avg=%.2f毫秒/万次" , loop, avg));
|
private long run( int loop) throws Exception {
|
for ( int i = 0 ; i < loop; i++) {
|
Map theWorld = buildTheWorld();
|
StringWriter console = new StringWriter();
|
long start = System.nanoTime();
|
long end = System.nanoTime();
|
private Map buildTheWorld() {
|
Map context = new HashMap();
|
context.put( "name" , "D&D" );
|
context.put( "version" , "1.0" );
|
Map game = new HashMap();
|
context.put( "game" , game);
|
Map player = new HashMap();
|
game.put( "player" , player);
|
player.put( "level" , "26" );
|
player.put( "name" , "jifeng" );
|
player.put( "job" , "paladin" );
|
player.put( "address" , "heaven" );
|
player.put( "weapon" , "sword" );
|
String[] bag = new String[] { "world_map" , "dagger" ,
|
"magic_1" , "potion_1" , "postion_2" , "key" };
|
private void play(Map theWorld, Writer console) throws Exception {
|
String name = String.valueOf(theWorld.get( "name" ));
|
String version = String.valueOf(theWorld.get( "version" ));
|
console.append( "Game " ).append(name).append( " (v" ).append(version).append( ")\n" );
|
Map game = (Map) theWorld.get( "game" );
|
Map player = (Map) game.get( "player" );
|
String level = String.valueOf(player.get( "level" ));
|
String job = String.valueOf(player.get( "job" ));
|
String address = String.valueOf(player.get( "address" ));
|
String weapon = String.valueOf(player.get( "weapon" ));
|
String hp = String.valueOf(player.get( "hp" ));
|
console.append( " You are a " ).append(level).append( " level " ).append(job)
|
.append( " from " ).append(address).append( ". \n" );
|
console.append( " Currently you have a " ).append(weapon).append( " in hand, " )
|
.append( "your hp: " ).append(hp).append( ". \n" );
|
console.append( " Here are items in your bag: \n" );
|
for (String item : (String[]) player.get( "bag" )) {
|
console.append( " * " ).append(item).append( "\n" );
|
console.append( "\tPlayer not login.\n" );
|
console.append( "\tGame not start yet.\n" );
|
[/java]
分享到:
相关推荐
此外,JVM还有其他优化手段,比如方法内联,可以提高代码执行效率,但这需要谨慎使用,因为过度的内联可能导致方法体过大,反而影响性能。 总结来说,大方法的性能问题主要是由于JVM的JIT编译策略限制。为了避免...
Oracle团队由于对系统的熟悉度较高,似乎拥有一定优势,但KDB团队获得了原厂的技术支持,这在调优过程中显得尤为重要。 在调优策略上,通常会涉及以下几个方面:内核调优、数据库参数调优、文件系统调优和SQL语句...
阿里大数据分析平台使用小记 阿里大数据分析平台是阿里巴巴集团旗下的数据分析平台,提供了大数据处理、存储和分析的能力。该平台主要应用于天池大数据竞赛平台,提供了一些使用经验和教程,但不是完整的开发使用...
JSP最终会被编译为Servlet执行,因此理解JSP生命周期和指令(如page、include、taglib)对优化性能和维护代码至关重要。 JDBC(Java Database Connectivity)是Java访问数据库的标准接口。在Java Web应用中,JDBC...
5. **版本控制与迭代**:多个版本的代码(如不同数字的zip文件)反映了软件开发的迭代过程。这涉及到版本控制工具,如Git,它对于团队协作和项目历史跟踪至关重要。 通过分析这些代码示例,开发者可以学习到实际...
环境: Linux s12084 2.6.9-67.ELsmp #1 SMP Wed Nov 7 13:58:04 EST 2007 i686 i686 i386 GNU/Linux gcc version 3.2.3 20030502 (Red Hat Linux 3.2.3-47.3) boost 1.37.0 ... 全部编译是很痛苦的过程
银行X银行营销服务系统性能测试小记[3]软件测试9、经验在本次性能测试的过程中,我们遇到一些问题,通过解决这些问题,从中获得了一些经验。现总结如下:在我们对系统进行测试的过程中,某些操作是相关联的。例如...
银行X银行营销服务系统性能测试小记[2]软件测试4、测试数据针对以上设计的测试用例,需要准备大量的业务数据。本次性能测试的环境即系统上线后真实运行的环境,所有的业务数据均来自X银行的真实核心系统(通过ETL转换...
- **注意事项**:如果派生类的方法名称与基类的虚方法不同,则编译器将报错。 - **`final`关键字**: - 用于标记虚函数或整个类不可被进一步覆盖或继承。 - **示例**: ```cpp class E { public: virtual ...
【LocalCache 学习小记1】 LocalCache 是一种本地高速缓存机制,它主要用于提升数据获取速度,尤其是在处理大量数据时,可以有效避免频繁访问远程数据库或分布式缓存,如 Redis 中的热键问题。LocalCache 不是...
《RMAN使用小记》 Recovery Manager(RMAN)是Oracle数据库系统中用于数据恢复和备份的关键工具。本文将详细介绍RMAN的一些核心特性和使用方法,帮助读者理解和掌握RMAN的基本操作。 首先,了解RMAN的三个重要特性...
对于大小显著不同的表进行Join时,将较小的表广播至每个Executor节点是一种有效提升性能的方法。这种方式类似于MapReduce和Hive中的Map Join,通过减少网络传输的数据量来提升整体效率。 #### 二、避免数据倾斜问题...
Vuex 使用方法总结 Vuex 是一个专门为 Vue.js 设计的状态管理器,用于管理应用程序的状态。下面是 Vuex 的使用方法总结: State Vuex 的状态管理是通过 State 来实现的。State 是一个对象,存储了应用程序的所有...
严格来说,不能算是真正的scrum实践,但实践敏捷的过程本身也是一种“敏捷方法”,所以就算是“敏捷实践之敏捷开发方法-scrum过程”吧。 1.Scrum团队(5-7个人的小项目组)。 2.Backlog:急待完成的一系列任务,...
ASP.NET 是微软开发的一种用于构建Web应用程序的框架,它提供了丰富的功能和强大的性能。在ASP.NET中,有时候我们需要实现邮件发送的功能,这时Jmail组件就显得尤为重要。Jmail是一款广泛应用于ASP.NET平台的邮件...