Java代码运行的方式
1:在开发工具中运行
2:双击 jar 文件运行
3:在命令行中运行
4:在网页中运行
上述运行方式都离不开 JRE,也就是 Java 运行时环境。实际上 JRE 仅包含运行 Java 程序的必须组件,包括 Java 虚拟机以及 Java 核心类库等。Java 程序员经常接触到的 JDK 同样包含了 JRE,并且还附带了一系列开发和诊断工具。
为什么 Java 要在虚拟机里运行
Java 是一门高级程序语言,语法复杂,抽象程度高,因此直接在硬件上运行并不现实。所以,在 Java 程序运行之前,需要对其进行转换。
设计一个面向 Java 语言的特性的虚拟机,并通过编译器将 Java 程序转换成该虚拟机所能识别的指令程序,也叫 Java 字节码。之所以叫做字节码,是因为 Java 字节码指令的操作码被固定为一个字节。
使用虚拟机的好处
Java 虚拟机也可以由硬件直接实现,但是更为常见的是基于软件实现。
如果一个 Java 程序被转换成 Java 字节码,那么他便可以在不同平台上的 Java 虚拟机实现里运行。这就是通常所说的:一次编译,到处运行。
虚拟机的另一个好处就是它提供了一个托管环境。这个托管环境代替我们处理一些代码中冗长而且容易出错的代码。最明显的使用就是自动内存管理和垃圾回收。托管环境还提供了诸如数组越界,动态类型,安全权限等等动态监测。
Java 虚拟机如何运行 Java 字节码
下面以标准 JDK 中的 HotSpot 虚拟机为例,从虚拟机以及底层硬件两个角度进行分析。
从虚拟机的视角来看,首先将 Java 代码编译成 class 文件加载到 Java 虚拟机中。加载后的 Java 类会被存放于方法区,实际运行时,虚拟机会执行方法区的代码。
Java 虚拟机在内存中划分出堆和栈来存储运行时的数据。对于栈, Java 虚拟机会把其细分为面向 Java 方法的 Java 方法栈,面向本地方法(用 C++ 写的 native 方法)的本地方法栈,以及存放各个线程执行位置的 PC 寄存器。
在虚拟机运行过程中,每当调用进入一个 Java 方法, Java 虚拟机会在当前线程的 Java 方法栈中生成一个栈帧,用以存放局部变量以及字节码的操作数。
当退出当前执行的方法时,不管是正常返回还是异常返回,Java 虚拟机均会弹出当前线程的当前栈帧,将之舍弃。
从硬件的视角来看,Java 字节码无法直接执行,因此 Java 虚拟机需要将字节码翻译成机器码。
翻译的形式有两种,一种是解释执行,即逐条将字节码翻译成机器码执行;一种是即时编译,即将一个方法中包含的所有字节码编译成机器码后再执行。
解释执行的优势在于无需等待编译,及时编译的优势在于实际运行速度会更快。HotSpot 默认采用混合模式,综合了解释执行和即时编译两者的优点。它会先解释执行字节码,而后将其中反复执行的热点代码,以方法为单位进行即时编译。
Java 虚拟机的运行效率
HotSpot 采用了多种技术来提升启动性能以及峰值性能,即时编译就是其中最重要的技术之一。
为了满足不同用户场景的需要,HotSpot 内置了多个即时编译器:C1,C2 和 Graal。Graal 是 Java 10 正式引入的实验性即时编译器,后续再总结。引入多个即时编译器,是为了在编译时间和生成代码的执行效率之间进行取舍。
C1 又叫做 Client 编译器,面向的是对启动性能有要求的客户端 GUI 程序,采用的优化手段相对简单,因此编译时间较短。
C2 又叫做 Server 编译器,面向的是对峰值性能有要求的服务器端程序,采用的优化手段相对复杂,因此编译时间较长,但生成代码的执行效率较高。
从 Java 7 开始,HotSpot 默认次用分层编译的方式:热点方法首先会被 C1 编译,而后热点方法中的热点会进一步被 C2 编译。
为了不干扰应用正常的运行,HotSpot 的即时编译是放在额外的编译线程中进行的。HotSpot 会根据 CPU 的数量设置编译线程的数目,并且按 1:2 的比例分配给 C1 和 C2 编译器。
问答
Q:对于发布一次就长时间运行的程序,为什么不选择直接将 Java 字节码编译成机器码
事实上 JVM 却是有考虑做 AOT 的这种事情。AOT 能够在线下将 Java 字节码编译成机器码,主要用来解决启动性能不好的问题。其实线下编译和即时编译都一样,至多一两个小时后该编译的都已经编译完成了。另外,即时编译器因为有程序运行时信息,优化效果更好,也就是说峰值性能更好。
Q:如何区分热点代码和非热点代码
关于热点代码的统计有两种算法:一种是基于采样的热点探测,一种是基于计数器的热点探测。基于计数器的热点探测又有两个计数器:一种是方法调用计数器,一种是回边计数器,他们在 C1 和 C2 中有不同的阈值。默认的分层编译应该是达到两千调用 C1,达到一万五调用 C2。一般采用的都是基于计数器的热点探测。
Q:对于 JVM 的及时编译,当方法体中有很多 if,else if 这样的判断,如何编译
JVM 有两种编译方式,一种是对整个方法进行编译,一种是对热循环进行编译。无论哪种,都要比 if else 的粒度大。
总结
本文创作灵感来源于 极客时间 郑雨迪老师的《深入拆解 Java 虚拟机》课程,通过课后反思以及借鉴各位学友的发言总结,现整理出自己的知识架构,以便日后温故知新,查漏补缺。