Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >影响Java调用性能有哪些因素

影响Java调用性能有哪些因素

作者头像
用户1289394
发布于 2018-02-26 09:19:32
发布于 2018-02-26 09:19:32
77400
代码可运行
举报
文章被收录于专栏:Java学习网Java学习网
运行总次数:0
代码可运行

影响Java调用性能有哪些因素

当时发生了什么?

这得从一个小故事说起。我在一个Java核心库的邮件列表中提交了一个修改 ——重写了一些本是 final 的方法。一石激起千层浪,这一改动引发了几番讨论。而其中一个讨论的话题是:调用一个去除 final 标记的方法,将导致哪种程度的性能下降(performance regression)。

我不能确定这一改变是否会导致性能下降,但当我决定将此暂时搁置一边,试着寻找在这个讨论里是否有人公布过任何相关的完整基准测试(sane benchmarks)时,结果空手而归。我不能肯定地说有关的基准测试是不存在的,或者说其他人没做过这方面的探讨。但我能肯定的是,在这里,连任何公开的代码评审都没有。唉,看来是时候写一个基准测试了。

基准测试的方法论

我决定选用一个相当不错的框架 —— JMH 来构建基准测试。如果你质疑它测试的准确性,那么建议你看下对这个框架作者(Aleksey Shipilev)的访谈,或者阅读一下由Nitsan Wakart撰写的一篇彰显此框架风采的博文。

现在,我想知道哪些因素影响了Java方法调用的性能。所以我决定以不同方式调用方法,并测算它们的性能开销。以单一变量为前提来构造一套基准测试,我便能逐个排除或确定,哪些因素或哪种组合会影响到方法调用的性能。

内联

方法调用的有无,是一个影响程度既是最高又是最低的因素——对于编译器来说,彻底优化方法调用所带来的开销并非不可能,有两种方法可以实现这样的需求:直接内联该方法本身和使用内联缓存(inline cache)。千万别被引入的这些术语给吓倒——它们都是通俗易懂的。现在我们假设有一个叫Foo的类,该类定义了一个叫bar的方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Foo {
  void bar() { ... }
}

我们以如下的方式调用bar方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Foo foo = new Foo();
foo.bar();

这里有一个重要的知识点:实际调用 bar 的位置,即 foo.bar(),称为调用点(callsite)。当我们说一个方法“被内联”,意指方法体被插入到了调用点的位置上,以代替方法调用。对于那些由许多短小的方法所构成的程序——我称之为被适当分解的程序——内联可以有效地提升性能。这是因为结束以后可以发现,程序并没有把所有时间用在方法调用上,实际上程序并没有工作!我们在JMH中可以借由 CompilerControl 注释控制一个方法是否被内联。关于内联缓存的概念,我稍后再来说明。

层次结构深度与重写子类方法

如果我们移除一个方法的 final 关键字,便意味着我们能够重写它。所以这是另一个在进行测试我们需要考虑的情况。我会选择在同一层次结构中不同层次的子类里调用一些方法,并且在这些方法里有一些是会被不同层次的子类重写的。这样的测试能让我们确定或排除深的层次结构是否影响到重写所带来的性能开销。

多态性

先前我提到调用点这一概念时,我偷偷地回避了一个相当重要的问题——因为在子类中可以重写一个非 final 方法,这使得调用点可以调用不同的方法。现假设我传入一个 Foo 的实例或一个重写了 bar 子类—— Baz的实例,编译器如何得知要调用哪一个 bar 方法呢?在默认情况下,方法将在Java中被虚拟化(可重写)。对于任一调用点,编译器需要在一个称为虚拟表(vtable)的表中寻找与其对应的方法。这是个非常耗时的过程,所以,能进行优化的编译器,总是会试图减少这种查询带来的开销。一种方法就是先前提到的内联,这的确是个良策,但前提是编译器能证明在给定的调用点上调用的方法唯一。而这样的调用点我们称为单态(monomorphic)调用点。

不幸的是,进行这种分析需要耗费大量时间。所以在实际过程中,确定一个调用点是否单态是个不太可取的方法。对此,JIT编译器倾向于使用一种替代方法:列出哪些类可以在此调用点被调用,接着根据之前的N个相同的调用猜测此调用点是否是单态的。以假定某个调用点永远为单态,来进行投机性质的优化往往是可取的行为。因为这样的优化往往都是正确的,但也因它无法确保永远正确,编译器需要在方法调用之前注入一个用于检查方法类型的防护机制。

除了单态的调用点以外,还有两种调用点我们希望对其进行优化。一种称为双态(bimorphic)调用点,在该点上有两个候选方法。对此你依然可以实现内联——借助防护代码,让其检测应调用哪一个方法,并引导程序跳转至内联在调用点的两个方法体中真正对应的那一个。这样的方式还是比查看所有虚拟表的方式要快得多。但在某些情况下,我们得利用内联缓存来进行优化。内联缓存需要借助一张特定的跳转表( jump table),这种表类似于对虚拟表查找做的一份缓存。hotsopt JIT编译器支持双态内联缓存,并定义那些拥有三个及三个以上候选方法的调用点为超多状态(megamorphic)调用点。

这就使得我在基准测试与探究当中,需要额外地把调用情况划分为三类:单态、双态、超多状态。

结果

让我们把结果分类组织,以便研究细节。我已经提供了统计产生的原始数据。但我们的兴趣点不应放在性能测试结果的具体数值上,而应是不同类型的方法调用的性能开销之间的比率以及各自的错误率是否够低。如果最快与最慢的结果之间比率为6.26,则说明这是一个显著性差异。由于测试时使用的是空方法(详见源代码),所以在实际应用中,这样的差异会更大。

你可以在 github上查看此次基准测试的源代码。为了避免产生困惑,待会所有的结果将分块显示。最后显示的多态的基准测试是在 PolymorphicBenchmark 类中进行,其它的则在 JavaFinalBenchmark 类中。

简单调用点

最先看到的的一组结果,是比较调用一个 virtual 方法、一个 final 方法和一个拥有很深的层级结构,同时被所有子类重写的方法所带来的开销。注意,调用这些方法的时候我们都强制编译器不要内联它们。我们可以看到:三者在时间花费上相差甚微,并且各自的误差率都小到可以忽略。对此我们可以断定,仅添加一个 final 关键字并不会大幅度提升调用性能,重写一个方法也不见得会带来什么影响。

内联简单调用

现在,我们在开启内联的情况下再来一次相同的测试。由结果可见,final 方法和 virtual 方法的时间花费依旧相近,并比在没有内联的情况下快了4倍,我将此归功于内联优化。相比而言,被所有子类重写的方法的结果可就没那么好看了。我推测这是由于此方法有多个子类实现,使得编译器必须插入一个类型保护。有关的细节我们将在研究多态性的结果时进行阐述。

类层次结构的影响

哇噢——这儿有好几个的方法!方法名称的编号(1~4)代表该方法调用的层次。因此,parentMethod4 表示我们调用的方法位于class的上面第四级。(译注:在源代码中该方法位于顶层的父类)。由此结果我们能断定,结构层次的深度对性能开销没有影响。在开启内联的实例中,结论也是一样。这个测试中,被内联的方法的性能与 inlinableAlwaysOverriddenMethod 相当,但稍逊于 inlinableVirtualInvoke。我依旧认为这与使用了类型保护有关。事实上JIT编译器能剖析所有候选方法,从而只内联对应的那一个,但这并不证明它总会这么干。

类的层级结构对final方法的影响

该测试的结论与第一个测试一样 —— final 关键字不会产生任何影响。我本以为该测试将证明 inlinableParentFinalMethod4 以无类型保护的方式进行内联,但结果表明事实并非如此。

多态性

最后,我们来看涉及多态分派(polymorphic dispatch)的测试结果。单态调用的性能开销与之前virtual方法相近。但对于双态与超多状态调用,由于需要在一张较大的虚拟表上面进行查找,所以需要更多的时间。而一旦我们开启内联支持,类型分析(type profiling )将会在单态或双态的调用点启用,使得在这些调用点上的方法调用的开销减少。但与层级结构的实例一样,这只会减少少量的时间。相比而言,超多状态的实例则依旧耗时较长。记住,我并没有说在这个测试中hotspot禁用了内联,它只是没有实现多态调用点的多态内联缓存。

我们从中学到了什么?

我认为,需要我们引起注意的是,很多人没有认识到不同方式的方法调用所花费的时间是不一样的。即便有些人发现了这种问题,但他们不去证明是否真的如此。作为第一个吃螃蟹的人,我列出了各种坏的假设,因此我希望这份研究能够帮助到大家。以下是我很乐于与大家分享的一些结论:

  • 最快与最短的方法调用的类型之间存在巨大的性能差别。
  • 在实际应用中,添加或删除final关键字并不会真正影响性能。但如果除此以外,你还在层级结构上进行某些操作,那这些行为则可能导致性能下降。
  • 更深的类的层次结构并不会真正影响到调用的性能。
  • 单态调用比双态调用更快。
  • 双态调用比超多状态调用更快。
  • 我们在能够进行剖析(profile-ably),但是不能进行查验的单态调用点中看到类型保护,这种保护会使得这些调用点的调用性能低于那些能够进行查验的单态调用点。

我想说的是,对我而言,类型保护带来的性能开销是一个“重大发现”。这是一个我之前很少提及,并且总是当做无关事物忽视掉的因素。

注意事项与进一步工作

本文不能囊括这个话题的全部内容。因为:

  • 这篇博文所关注的影响到方法调用的性能的因素,只与类型有关。所以,有一个因素我并未提及:方法的长短或者说调用栈的深度——如果方法太长,那么它将不会被内联,为此你必须承受方法调用所带来的开销。另外,为了使代码具有易读性,你也应当把方法写得短小一些。
  • 在本次测试的所有我并没有尝试引入接口。如果你对此有兴趣的话,这里有一篇有关接口调用的性能的研究Mechanical Sympathy。
  • 还有一个因素被我完全忽视了,那就是方法内联的优化方式在不同编译器上的效果差异。当编译器是仅关注某个方法(内部过程优化)时,它们需要足够地信息才能有效优化。内联的限制可以有效地减少其它优化所需要关注的范围。
  • 试着站在汇编语言的层面进行解释的话,会涉及更多的细节内容。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2015-06-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java学习网 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Java 虚拟机:JVM是如何执行方法调用的?(下)
我在读博士的时候,最怕的事情就是被问有没有新的 Idea。有一次我被老板问急了,就随口说了一个。
码农架构
2021/02/07
1.3K0
Java 虚拟机:JVM是如何执行方法调用的?(下)
泛型会让你的 Go 代码运行变慢
Go 1.18 已经到来,很多人期盼已久的首个支持泛型实现的版本也就此落地。之前,泛型一直是个热度很高、但在整个 Go 社区中备受争议的话题。
深度学习与Python
2022/04/19
1.2K0
泛型会让你的 Go 代码运行变慢
为什么泛型会让你的Go程序变慢
强烈推荐大家读完,可以很好的理解泛型实现,以及当前有哪些性能问题,翻译时我会加些注释,以便大家更好的理解
KevinYan
2023/10/25
4370
为什么泛型会让你的Go程序变慢
《深入理解java虚拟机》学习笔记之编译优化技术
郑重声明:本片博客是学习<深入理解Java虚拟机>一书所记录的笔记,内容基本为书中知识. Java程序员有一个共识,以编译方式执行本地代码比解释方式更快,之所以有这样的共识,除去虚拟机解释执行字节码时额外消耗时间的原因外,还有一个很重要的原因就是虚拟机设计团队几乎把对代码的所有优化措施都集中在了即时编译器之中(在JDK 1.3之 后,Javac就去除了-O选项,不会生成任何字节码级别的优化代码了),因此一般来说,即时编译器产生的本地代码会比Javac产生的字节码更加优秀[1]。本篇博客,我们将一起学习HotSpot虚拟机的即时编译器在生成代码时采用的代码优化技术。
老马的编程之旅
2022/06/22
4770
《深入理解java虚拟机》学习笔记之编译优化技术
Go和C++通用性能优化黑魔法——PGO!
我们在进行性能优化的时候,往往会应用各种花式的优化手段:优化算法复杂度(从 O(N) 优化到 O(logN) ),优化锁的粒度或者无锁化,应用各种池化技术:内存池、连接池、线程池、协程池等。压缩技术、预拉取、缓存、批量处理、SIMD,内存对齐等等手段后,其实还有一种手段就是 Profile-Guided Optimization (PGO)。本文会介绍 PGO 的原理,以及 Go/C++ 语言进行 PGO 的实践。
腾讯云开发者
2023/10/23
2K0
Go和C++通用性能优化黑魔法——PGO!
Java 中的final:不可变性的魔法之旅
在 Java 编程世界中,final 是一个引人注目的关键字,它赋予了变量、方法、类等各种元素不可变性。有些程序员将其视为一种约束,而另一些则将其视为一种保护措施。在这个博客中,我们将探索final的多种用法,从变量的不可变性到类的终结,了解其妙用。final是你代码的最后一道屏障,让我们一起发现它的力量。
一只牛博
2025/05/30
680
【基本功】深入剖析Swift性能优化
美美今天请来了我们技术团队很厉害的iOS女神亚男小姐姐深度剖析Swift,她特别讲解了如何才能开发出高性能的Swift程序。希望对你有所帮助哦~Enjoy Reading!
美团技术团队
2019/03/22
1.5K1
【基本功】深入剖析Swift性能优化
Java内联类初探
内联类(inline classes)的目标是让 Java 程序更好地适应现代硬件。为了实现这一目标,需要重新审视 Java 平台的一个非常基础的组成部分,即 Java 数据值的模型。
Java帮帮
2019/11/07
1.6K0
2024年 Java 面试八股文 5万字(持续更新ing)
封装是OOP的首要原则,它允许对象隐藏其内部实现细节,只暴露出一个可以被外界访问和使用的接口。在Java中,封装通过访问修饰符(如private、public、protected)来实现。
疯狂的KK
2024/05/08
2.4K0
2024年 Java 面试八股文 5万字(持续更新ing)
JVM性能优化系列-(6) 晚期编译优化
在部分的商用虚拟机中,java程序最初是通过解释器(Interpreter) 进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”(Hot Spot Code)。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个过程的编译器称为即时编译器(Just In Time Compiler)
码老思
2023/10/19
2970
秒杀面试题:深入final,掌握C++性能优化
C++11之后有了final,它用来指定不能在派生类中重写虚函数,或者不能从中派生类。
公众号guangcity
2024/01/11
3630
秒杀面试题:深入final,掌握C++性能优化
「MoreThanJava」Day 5:面向对象进阶—继承详解
上一篇文章 中我们简单介绍了继承的作用,它允许创建 具有逻辑等级结构的类体系,形成一个继承树。
我没有三颗心脏
2020/08/11
5450
「MoreThanJava」Day 5:面向对象进阶—继承详解
JVM精通面试系列 | 掘金技术征文
JRE 仅包含运行 Java 程序的必需组件,包括 Java 虚拟机以及 Java 核心类库等。我们 Java 程序员经常接触到的 JDK(Java 开发工具包)同样包含了 JRE,并且还附带了一系列开 发、诊断工具。
蒋老湿
2020/03/27
8260
JVM精通面试系列 | 掘金技术征文
Android面试必备的JVM虚拟机制详解,看完之后简历上多一个技能!
Java 中的运行时数据可以划分为两部分,一部分是线程私有的,包括虚拟机栈、本地方法栈、程序计数器,另一部分是线程共享的,包括方法区和堆。
Android技术干货分享
2020/10/16
9220
Android面试必备的JVM虚拟机制详解,看完之后简历上多一个技能!
Java中的泛型(很细)
非常好,让我们深入探讨Java中的泛型这个重要主题。我将按照之前提供的框架,为您创作一篇全面而专业的技术博客文章。
程序员朱永胜
2024/07/18
2720
Java中的泛型(很细)
【JAVA-Day56】Java面向对象编程:深入理解类、对象、属性和方法的核心概念
作为一名博主,我们将在本篇技术博客中深入研究Java面向对象编程的核心概念,包括类、对象、属性和方法。我们将详细探讨这些概念,加入小表情使文章更生动有趣。让我们一起探索这个令人兴奋的领域!
默 语
2024/11/20
3870
【JAVA-Day56】Java面向对象编程:深入理解类、对象、属性和方法的核心概念
Java性能测试利器:JMH入门与实践|得物技术
在软件开发中,性能测试是不可或缺的一环。但是编写基准测试来正确衡量大型应用程序的一小部分的性能却又非常困难。当基准测试单独执行组件时,JVM或底层硬件可能会对您的组件应用许多优化。当组件作为大型应用程序的一部分运行时,这些优化可能无法应用。因此,实施不当的微基准测试可能会让您相信组件的性能比实际情况更好。编写正确的Java微基准测试通常需要防止JVM和硬件在微基准测试执行期间应用的优化,而这些优化在实际生产系统中是无法应用的。这就是JMH(Java 微基准测试工具)可以帮助您实现的功能。这篇文章我会全面给大家介绍下JMH的各个方面。
得物技术
2024/11/21
2130
JAVA相关编译知识
前端编译可以简单理解为就是将java文件转换为class字节码文件;后端编译可以理解为clas字节码转换为目标机器平台的机器语言。
北洋
2022/03/09
6490
JAVA相关编译知识
GraalVM在Facebook大量使用,性能提升显著!「建议收藏」
Facebook正在使用GraalVM来加速其Spark的工作负载,并减少内存和CPU的使用。请继续阅读,了解它们的迁移故事、性能改进结果和未来计划。
全栈程序员站长
2022/11/05
1.9K0
GraalVM在Facebook大量使用,性能提升显著!「建议收藏」
请不要再说 Java 中 final 方法比非 final 性能更好了
总结:你说final的性能比非final有没有提升呢?可以说有,但几乎可以忽略不计。如果单纯地追求性能,而将所有的方法修改为 final 的话,我认为这样子是不可取的。而且这性能的差别,远远也没有网上有些人说的提升 50% 这么恐怖(有可能他们使用的是10年前的JVM来测试的吧^_^,比如 《35+ 个 Java 代码性能优化总结》这篇文章。雷总:不服?咱们来跑个分!)
哲洛不闹
2018/09/14
1.3K0
请不要再说 Java 中 final 方法比非 final 性能更好了
推荐阅读
相关推荐
Java 虚拟机:JVM是如何执行方法调用的?(下)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验