首页 > 编程知识 正文

慢四九儿第一讲,深入理解java虚拟机第四版

时间:2023-05-03 18:24:06 阅读:119684 作者:4461

本文转载自https://time.geek bang.org/column/article/11289

Java代码有很多执行方法。

当然,在开发工具中双击某个jar文件,并在执行命令行的执行网页中运行它,所有上述执行方法都无法与JRE (Java运行时环境)分离。

JRE仅包含Java程序的必需组件,如Java虚拟机和Java核心类库。

我们Java程序员经常接触的JDK(Java开发工具包)也同样包含JRE,并附带一系列开发和诊断工具。

但是,运行c程序不需要添加运行时环境,c编译器经常将c代码编译成CPU可以理解的机器码。

那么,既然c的行为如此成熟,为什么要在JVM上运行Java代码呢?

为什么Java在虚拟机上运行? Java作为高级程序语言,语法复杂,抽象度也高。 因此,在硬件上运行Java代码是不现实的,因此必须在运行Java程序之前进行转换。

目前,进行转换的主要思路是设计面向Java语言特性的虚拟机,通过编译器将Java程序转换为该虚拟机可以识别的指令序列(Java字节码)。 之所以这样命名,是因为Java字节码指令的操作码固定为一个字节。

Java虚拟机可以用硬件实现

3359 en.Wikipedia.org/wiki/Java _ processor

当然,软件实现通常在每个现有平台(Windows_x64、Linux_aarch64 )上提供。 其目的是在将程序编译成Java字节码后,可以在不同平台上的虚拟机实现中运行。 这就是我们平时说的Java的跨平台特性。

虚拟机的另一个好处是提供了托管环境(Managed Runtime )。 此托管环境代替处理一些代码中冗馀且容易出错的部分。 其中最为人熟知的是自动内存管理和垃圾回收,这部分内容引起了垃圾回收的调整工作。

此外,托管环境还提供了数组越界、动态类型和安全权限等动态监视,从而消除了编写与这些业务逻辑无关的代码的需要。

Java虚拟机具体如何执行Java字节码? 以标准JDK的HotSpot虚拟机为例,从虚拟机和底层硬件两个角度分析该问题。

从虚拟机的角度看,要运行Java代码,必须首先将编译的类文件加载到Java虚拟机中。 加载的Java类存储在方法区域(Method Area )中。 实际运行时,虚拟机将运行方法区域中的代码。

如果您熟悉X86,就会发现这类似于段存储管理的代码段。 Java虚拟机也在内存中划分堆和堆栈,以存储运行时的数据。 区别在于,Java虚拟机将堆栈细分为用于Java方法的Java方法堆栈、用于本地方法(用c编写的native方法)的本地方法堆栈以及用于存储每个线程执行位置的PC寄存器

在运行过程中,每次调用Java方法时,Java虚拟机都会生成一个堆栈帧,其中包含当前线程的Java方法堆栈中的局部变量和字节码操作数。 此堆栈帧的大小是预先计算的,Java虚拟机不需要将堆栈帧连续分布在内存空间中。

退出当前正在运行的方法时,无论是成功返回还是异常返回,Java虚拟机都会弹出并销毁当前线程的堆栈帧。

在HotSpot中,上述翻译过程有两种形式

解释执行是指将字节码逐一翻译成机器码并执行。即时编译(Just-In-Time compilation, JIT),将一个方法中包含的所有字节码编译为机器码后运行。

前者的优点是不需要等待编译,后者的优点是实际运行速度快。

HotSpot默认采用混合模式,集成了解释运行和即时编译两个优点。

首先说明字节码。 并且,以方法为单位立即编译其中反复执行的热点代码。

Java虚拟机的运行效率是什么? HotSpot采用了许多技术来提高峰值性能,上述实时编译技术是其中最重要的技术之一。

实时编译基于程序遵循二八定律的假设。

二八定律: 20%的代码占程序运行中资源的80%。

对于占大多数的不常见代码,我们采用解释执行方式,而不是花时间编译成机器码。

另一方面,对于只占小部分的热点代码,可以将其编译成机器码,命中理想的执行速度。

理论上,即时编译的Java程序的执行效率可以超过c程序。 与静态编译相比,这是即时编译拥有程序的运行时信息,并且能够根据这个信息做出相应的优化。

例如,我们知道虚拟方法用于实现面向对象语言的多态性。 对于虚拟方法调用,有许多目标方法,但在实际运行期间可能只调用了其中一个。 实时编译器将使用此信息来避免虚拟化

方法调用的开销从而达到比静态编译的C++程序更高的性能。

为了满足不同用户场景的需要,HotSpot内置了多个即时编译器:C1、C2和Graal。 Graal是Java 10正式引入的实验性即时编译器。

之所以引入多个即时编译器,是为了在编译时间和生成代码的执行效率之间做取舍。 C1又叫做Client编译器,面向的是对启动性能有要求的客户端GUI程序,采用的优化手段相对简单,因此编译时间较短。C2又叫做Server编译器,面向的是对峰值性能有要求的服务端程序,采用的优化手段相对复杂,因此编译时间较长,但同时生成代码的执行效率较高。

从Java 7开始,HotSpot默认采用分层编译的方式:热点方法首先被C1编译,而后热点方法中的热点会进一步被C2编译。

为了不干扰应用的正常运行,HotSpot的即时编译是放在额外的编译线程中进行的。HotSpot会根据CPU的数量设置编译线程的数目,并且按1:2的比例配置给C1及C2编译器。

在计算资源充足的情况下,字节码的解释执行和即时编译可同时进行。编译完成后的机器码后再下次调用时启用,以替换原本的解释执行。

我们来完成老师布置的作业:了解Java语言和Java虚拟机看待boolean类型的方式是否不同。

首先,撰写代码Foo.java

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!!!");}} javac Foo.javajava Foo

显然,它的执行结果是:

Hello, Java!!
Hello, JVM!!!

我们使用asmtools.jar对其进行反汇编(此命令JDK7无法运行, 需要升级到JDK8)

下载地址:https://download.csdn.net/download/ti_an_di/10555815

java -cp ./asmtools.jar org.openjdk.asmtools.jdis.Main Foo.class > Foo.jasm.1

我们得到它的反汇编代码(在Foo.jasm.1 中)

super public class Fooversion 52:0{public Method "<init>":"()V"stack 1 locals 1{aload_0;invokespecialMethod java/lang/Object."<init>":"()V";return;}public static Method main:"([Ljava/lang/String;)V"stack 2 locals 2{iconst_1;//看这里istore_1;iload_1;ifeqL14;getstaticField java/lang/System.out:"Ljava/io/PrintStream;";ldcString "Hello, Java!!";invokevirtualMethod java/io/PrintStream.println:"(Ljava/lang/String;)V";L14:stack_frame_type append;locals_map int;iload_1;iconst_1;if_icmpneL27;getstaticField java/lang/System.out:"Ljava/io/PrintStream;";ldcString "Hello, JVM!!!";invokevirtualMethod java/io/PrintStream.println:"(Ljava/lang/String;)V";L27:stack_frame_type same;return;}} // end Class Foo

再运行指令(其作用是将Foo.jasm.1文件中第一个iconst_1 替换为iconst_2, 输出到文件Foo.jasm中)

awk 'NR==1,/iconst_1/{sub(/iconst_1/, "iconst_2")} 1' Foo.jasm.1 > Foo.jasm

 

super public class Fooversion 52:0{public Method "<init>":"()V"stack 1 locals 1{aload_0;invokespecialMethod java/lang/Object."<init>":"()V";return;}public static Method main:"([Ljava/lang/String;)V"stack 2 locals 2{iconst_2; //看这里istore_1;iload_1;ifeqL14;getstaticField java/lang/System.out:"Ljava/io/PrintStream;";ldcString "Hello, Java!!";invokevirtualMethod java/io/PrintStream.println:"(Ljava/lang/String;)V";L14:stack_frame_type append;locals_map int;iload_1;iconst_1;if_icmpneL27;getstaticField java/lang/System.out:"Ljava/io/PrintStream;";ldcString "Hello, JVM!!!";invokevirtualMethod java/io/PrintStream.println:"(Ljava/lang/String;)V";L27:stack_frame_type same;return;}} // end Class Foo

我们现在将flag的值由1改为了2, 将修改后的代码汇编到Foo.class文件中

java -cp ./asmtools.jar org.openjdk.asmtools.jasm.Main Foo.jasm

再次运行Foo类

java FooHello, Java!!

可见JVM将true视为1, 不等于修改为2的flag,使用if_icmpne指令判断他们不相等,直接跳到L27执行,所以Hello, JVM!!!不会输出。而第一次判断是使用ifeq判断flag的值是否为0,所以Hello,Java!!会输出。

此文从极客时间专栏《深入理解Java虚拟机》搬运而来,撰写此文的目的: 对自己的学习总结归纳 此篇文章对想深入理解Java虚拟机的人来说是非常不错的文章,希望大家支持一下qxdxwz。

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。