You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
37 lines
3.7 KiB
37 lines
3.7 KiB
---
|
|
JVM 优化 Java 代码时都做了什么?
|
|
---
|
|
|
|
#### 典型回答
|
|
|
|
来自 [郑雨迪老师](https://time.geekbang.org/column/intro/108?utm_source=app&utm_medium=article&utm_campaign=108-presell&utm_content=java) 的回答:
|
|
|
|
JVM 在对代码执行的优化可分为运行时(Runtime)和即时编译器(JIT)优化。运行时优化主要是解释执行和动态编译通用的一些机制,比如说锁机制(如偏斜锁)、内存分配机制(如 TLAB)等。除此之外,还有一些专门用于优化解释执行效率的,比如说模版解释器、内联缓存(inline cache,用于优化虚方法调用的动态绑定)。
|
|
|
|
JVM 的即时编译器优化是指将热点代码以方法为单位转换成机器码,直接运行在底层硬件之上。它采用了多种优化方式,包括静态编译器可以使用的如方法内联、逃逸分析,也包括基于程序运行 profile 的投机性优化(speculative/optimistic optimization)。这个怎么理解呢?比如我有一条 instanceof 指令,在编译之前的执行过程中,测试对象的类一直是同一个,那么即时编译器可以假设编译之后的执行过程中还会是这一个类,并且根据这个类直接返回 instanceof 的结果。如果出现了其他类,那么就抛弃这段编译后的机器码,并且切换回解释执行。
|
|
|
|
当然,JVM 的优化方式仅仅作用在运行应用代码的时候。如果应用代码本书阻塞了,比如说并发时等待另一线程的结果,这就不在 JVM 的优化范畴了。
|
|
|
|
#### 知识扩展
|
|
|
|
通常所说的编译器,是指 javac 等编译器或者相关 API 等将源码转换成字节码的过程,这个阶段也会进行少量类似常量折叠之类的优化,只要利用反编译工具,就可以直接查看细节。
|
|
|
|
javac 优化与 JVM 内部优化也存在关联,毕竟它负责了字节码的生成。例如,Java 9 中的字符串拼接,会被 javac 替换成对 StringConcatFactory 的调用,进而为 JVM 进行字符串拼接优化提供了统一的入口。
|
|
|
|
然后就是重点要讲的 JVM 运行时优化,在通常情况下,编译器和解释器是共同起作用的。
|
|
|
|
JVM 会根据统计信息,动态决定什么方法被编译,什么方法解释执行,即使是已经编译过的代码,也可能在不同的运行阶段不再是热点,JVM 有必要将这种代码从 Code Cache 中移除出去,毕竟其大小是有限的。
|
|
|
|
解释器和编译器也会进行一些通用优化,例如:
|
|
|
|
- 锁优化
|
|
|
|
- Intrinsic 机制
|
|
|
|
或者叫做内建方法,就是针对特别重要的基础方法,JDK 团队直接提供定制的实现,利用汇编或者编译器的中间表达方式编写,然后 JVM 会直接在运行时进行替换。
|
|
|
|
这么做的理由有很多,例如,不同体系结构的 CPU 在指令等层面存在着差异,定制才能充分发挥出硬件的能力。我们日常使用的典型的字符串操作、数组拷贝等基础方法,Hotspot 都提供了内建实现。
|
|
|
|
而即时编译器,则是更多优化工作的承担者。JIT 对 Java 编译的基本单元是整个方法,通过对方法调用的计数统计,编译为本地代码。另外一个优化场景,则是针对所谓的热点循环代码,利用通常说的栈上替换技术(OSR),如果方法本身的调用频度还不够编译标准,但是内部有大的循环之类,则还是会有进一步优化的价值。
|
|
|
|
从理论上来看,JIT 可以看作就是基于两个计数器实现,方法计数器和回边计数器提供给 JVM 统计数据,以定位到热点代码。实际中的 JIT 机制要复杂的多,比如有逃逸分析、循环展开、方法内联等,包括前面提到的 Intrinsic 等通用机制同样会在 JIT 阶段发生。 |