为什么Java代码需要在虚拟机中运行
- Java 代码作为一种高级语言直接在硬件中执行是不现实的,所以在代码运行前需要进行转换。Java虚拟机通过编译器将Java代码进行转换为虚拟机可识别的指令序列(Java字节码)。
- 虚拟机可以由硬件来实现,但通常是由软件来实现。由软件实现的有各种平台的虚拟机(Windows,Mac等),由虚拟机转换后的Java字节码文件,即可在不同平台实现的虚拟机中运行。
- 虚拟机还带来的另一个好处就是有一个托管环境,使程序员更主要关心业务代码。例如自动内存管理和垃圾回收。
Java虚拟机怎样运行字节码
分两个视角:
- 虚拟机视角:执行Java代码需要先编译成class文件,然后将其加载到虚拟机中。加载后的Java类会放在方法区中,实际运行时,虚拟机会执行方法区中的代码。
在运行过程中,每当调用进入一个方法,虚拟机就会在当前线程的方法栈中生成一个栈桢,用来存放局部变量和字节码的操作数。这个栈桢是提前计算好的,并且在内存中不需要连续。 - 硬件视角:Java字节码是无法直接运行的,需要虚拟机将字节码翻译为机器可识别的机器码。这个翻译的过程有两种形式:解释执行:逐条将字节码翻译为机器码并执行 和 即时编译(JIT):将一个方法中包含的所有字节码翻译为机器码后再执行。
- 前者的优势在于无需等待编译,后者的优势在于实际运行速度更快。HotSpot采用混合模式,先解释执行字节码,然后将需要反复执行的热点代码,进行以方法为单位进行即时编译。
Java虚拟机的运行效率
根据二八定律,其实大部分的代码是不需要提前编译的,是可以直接解释执行的。仅需将少部分的热点代码进行编译,达到提升效率。
为了满足不同客户的需求,HotSpot内置了多个即时编译器:C1、C2 和 Graal。在编译时间和生成代码的执行效率之间进行取舍。
- C1编译器又叫Client编译器,主要面向对启动时间有要求的GUI用户。
- C2编译器又叫Server编译器,主要面向对峰值性能有要求的服务端。编译时间长但是生成的代码执行效率高。
- Java7以后采用分层编译,优先被C1编译,而后被C2编译,并根据CPU的数量设置编程线程的数量,按照1:2比例分配给C1,C2。
再回顾一下Java内存区域划分
Java虚拟机运行时将内存划分为5个部分:方法区、堆、PC寄存器、Java方法栈和本地方法栈
对于热点代码的判断可看链接
JVM会统计每个方法被调用了多少次,超过多少次,那就是热点方法。(还有个循环回边计数器,用来编译热循环的。) 默认的分层编译应该是达到两千调C1,达到一万五调C2。
https://mp.weixin.qq.com/s/GO2fAeGgaB2jIC02gWp5Aw
分享一个工具 asmtools 可以修改字节码指令文件
https://ci.adoptopenjdk.net/view/Dependencies/job/asmtools/lastSuccessfulBuild/
public class Foo {
public static void main(String[] args) {
boolean flag = true;
if(flag){
System.out.println("Hello,Java");
}
if(flag==true){
System.out.println("Hello,JVM");
}
}
}
super public class Foo
version 52:0
{
public Method "<init>":"()V"
stack 1 locals 1
{
aload_0;
invokespecial Method java/lang/Object."<init>":"()V";
return;
}
public static Method main:"([Ljava/lang/String;)V"
stack 2 locals 2
{
iconst_1; //@1 用asmtools 工具将此处修改为 iconst_2 那么程序输出结果为 Hello,Java。
istore_1;
iload_1;
ifeq L14;
getstatic Field java/lang/System.out:"Ljava/io/PrintStream;";
ldc String "Hello,Java";
invokevirtual Method java/io/PrintStream.println:"(Ljava/lang/String;)V";
L14: stack_frame_type append;
locals_map int;
iload_1;
iconst_1;
if_icmpne L27;
getstatic Field java/lang/System.out:"Ljava/io/PrintStream;";
ldc String "Hello,JVM";
invokevirtual Method java/io/PrintStream.println:"(Ljava/lang/String;)V";
L27: stack_frame_type same;
return;
}
} // end Class Foo
jvm把boolean当做int来处理
flag = iconst_1 = true
修改后flag改为iconst_2
if(flag)比较时ifeq指令做是否为零判断,常数2仍为true,打印输出
if(true == flag)比较时if_cmpne做整数比较,iconst_1是否等于flag,比较失败,不再打印输出。
-- 这个来自极客时间中的评论。