master
Omooo 4 years ago
parent 450ff7026d
commit 50e50dc445
  1. 14
      blogs/Java/口水话/JVM 相关口水话.md
  2. BIN
      images/JVM/解释执行和编译执行.png

@ -150,9 +150,19 @@ ZGC 作为新一代的垃圾回收器,在设计之初就定义了三大目标
下面我就主要讲一下方法内联和逃逸分析。
方法内联,它指的是在编译的过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。方法内联不仅可以消除调用本身带来的性能开销,还可以进一步触发更多的优化。因此,它可以算是编译优化里最为重要的一环。以 getter/setter 为例,如果没有方法内联,在调用 getter/setter 时,程序需要保存当前方法的执行位置,创建并压入用于 getter/setter 的栈桢、访问字段、弹出栈桢,最后再恢复当前方法的执行。而当内联了对 getter/setter 的方法调用后,上述操作就只剩下字段访问了。但是即时编译器不会无限制的进行方法内联,它会根据方法的调用次数、方法体大小、Code cache 的空间等去决定是否要进行内联。其次,对于需要动态绑定的虚方法调用来说,即时编译器则需要先对虚方法调用进行去虚化,即转化为一个或多个直接调用,然后才能进行方法内联。说到这,你应该就明白 final/static 的好处了。
方法内联,它指的是在编译的过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。方法内联不仅可以消除调用本身带来的性能开销,还可以进一步触发更多的优化。因此,它可以算是编译优化里最为重要的一环。以 getter/setter 为例,如果没有方法内联,在调用 getter/setter 时,程序需要保存当前方法的执行位置,创建并压入用于 getter/setter 的栈桢、访问字段、弹出栈桢,最后再恢复当前方法的执行。而当内联了对 getter/setter 的方法调用后,上述操作就只剩下字段访问了。但是即时编译器不会无限制的进行方法内联,它会根据方法的调用次数、方法体大小、Code cache 的空间等去决定是否要进行内联。比如即使是热点代码,如果方法体太大,也不会进行内联,因为会占用更多内存空间。所以平时编码中,尽可能使用小方法体。对于需要动态绑定的虚方法调用来说,即时编译器则需要先对虚方法调用进行去虚化,即转化为一个或多个直接调用,然后才能进行方法内联。说到这,你应该就明白 final/static 的好处了。所以尽量使用 final、private、static 关键字修饰方法,虚方法因为继承,会需要额外的类型检查才能知道实际上调用的是哪个方法
逃逸分析是一种确定指针动态范围的静态分析,即时编译器可以根据逃逸分析的结果进行诸如锁消除、栈上分配以及标量替换的优化。我们先看一下锁消除,如果即时编译器能够证明锁对象不逃逸,那么对该锁对象的加锁、解锁操作没有任何意义,因为其他线程并不能获得该锁对象,在这种情况下,即时编译器就可以消除对该不逃逸对象的加锁、解锁操作。比如 synchronized(new Object) 这种操作会被完全优化掉。不过一般不会有人这么写,事实上,逃逸分析的结果更多被用于将新建对象操作转换成栈上分配或者标量替换。我们知道,Java 虚拟机中对象都是在堆上进行分配的,而堆上的内容对任何线程可见,与此同时,JVM 需要对所分配的堆内存进行管理,并且在对象不再被引用时回收其所占据的内存。如果逃逸分析能够证明某些新建的对象不逃逸,那么 JVM 完全可以将其分配至栈上,并且在方法退出时,通过弹出当前方法的栈桢来自动回收所分配的内存空间。不过,由于实现起来需要更改大量假设了 “对象只能堆分配” 的代码,因此 HotSpot 虚拟机并没有采用栈上分配,而是使用了标量替换这么一项技术。所谓的标量,就是仅能存储一个值的变量,如果 Java 代码中的局部变量。标量替换这项优化技术,可以看成将原本对对象的字段的访问,替换成一个个的局部变量的访问。
逃逸分析是判断一个对象是否被外部方法引用或外部线程访问的分析技术,即时编译器可以根据逃逸分析的结果进行诸如锁消除、栈上分配以及标量替换的优化。我们先看一下锁消除,如果即时编译器能够证明锁对象不逃逸,那么对该锁对象的加锁、解锁操作没有任何意义,因为其他线程并不能获得该锁对象,在这种情况下,即时编译器就可以消除对该不逃逸对象的加锁、解锁操作。比如 synchronized(new Object) 这种操作会被完全优化掉。不过一般不会有人这么写,事实上,逃逸分析的结果更多被用于将新建对象操作转换成栈上分配或者标量替换。我们知道,Java 虚拟机中对象都是在堆上进行分配的,而堆上的内容对任何线程可见,与此同时,JVM 需要对所分配的堆内存进行管理,并且在对象不再被引用时回收其所占据的内存。如果逃逸分析能够证明某些新建的对象不逃逸,那么 JVM 完全可以将其分配至栈上,并且在方法退出时,通过弹出当前方法的栈桢来自动回收所分配的内存空间。不过,由于实现起来需要更改大量假设了 “对象只能堆分配” 的代码,因此 HotSpot 虚拟机并没有采用栈上分配,而是使用了标量替换这么一项技术。所谓的标量,就是仅能存储一个值的变量,如果 Java 代码中的局部变量。标量替换这项优化技术,可以看成将原本对对象的字段的访问,替换成一个个的局部变量的访问。
#### 虚拟机相关
先说 HotSpot 虚拟机。
从硬件视角来看呢,Java 字节码是无法直接运行的,因此 JVM 需要将字节码翻译成机器码。在 HotSpot 里面,翻译过程有两种,一种是解释执行,即逐条将字节码翻译成机器码并执行,第二种是即时编译执行,即以方法为单位整体编译为机器码后再执行。前者的优势在于无需等待编译,而后者的优势在于实际运行速度更快。HotSpot 默认采用混合模式,综合了解释执行和编译执行两者的优点。它会先解释执行字节码,而后将其中反复执行的热点代码,以方法为单位进行编译执行。
HotSpot 内置了多个 JIT 即时编译器,C1 和 C2,之所以引入多个即时编译器,是为了在编译时间和生成代码的执行效率之间进行取舍。Java 7 引入了分层编译,分层编译将 JVM 的执行状态分为 5 个层次。第 0 层是解释执行,默认开启性能监控;第 1 层到第 3 层都是称为 C1 编译,将字节码编译成本地代码,进行简单、可靠的优化;第 4 层是 C2 编译,也是将字节码编译成本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。
至此,HotSpot 及 JIT 就讲完了。

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Loading…
Cancel
Save