你编写的Java代码是咋跑起来的?

若是你是一名 Java 开发人员,你确定指定 Java 代码有不少种不一样的运行方式。好比说能够在开发工具(IDEA、Eclipse等)中运行,能够双击执行 jar 文件运行,也能够在命令行中运行,甚至能够在网页(好比各类 OJ)中运行。固然,这些执行方式都离不开 JRE(Java 运行时环境)。java

JRE 包含运行 Java 程序的必需组件,包括 JVM(Java 虚拟机)以及 Java 核心类库等。Java 程序员常常接触到的 JDK(Java 开发工具包)一样包含了 JRE,而且还附带了一系列开发、诊断工具。程序员

本篇文章主要针对如下两个问题和你们一块儿探讨:算法

  1. 为何须要 JVM?
  2. JVM 是怎样运行 Java 代码的呢?

为何须要 JVM?

Java 的一个很是重要的特色就是与平台的无关性,而使用 JVM 是实现这一特色的关键。Java 做为一门高级程序语言,语法复杂,抽象程度高。所以,直接在硬件上运行这种复杂的程序并不现实。因此在运行 Java 程序以前,咱们须要对其进行转换。数组

设计一个面向 Java 语言特性的虚拟机,并经过编译器将 Java 程序转换成该虚拟机所能识别的指令序列(由于 Java 字节码指令的操做码(opcode)被固定为一个字节,故又称 Java 字节码)。安全

JVM 通常是在各个现有平台(如 Windows、Linux)上提供软件实现,这样可使一旦一个程序被转换成 Java 字节码,那么即可以在不一样平台上的虚拟机实现里运行(一次编写,处处运行)。服务器

JVM 另一个好处是带有托管环境(Managed Runtime),托管环境可以代替处理一些代码中冗长并且容易出错的部分,其中包括自动内存管理与垃圾回收(GC)。工具

另外,托管环境还提供了诸如数组越界、动态类型、安全权限等等的动态检测,使咱们免于书写这些无关业务逻辑的代码。性能

JVM 是怎样运行 Java 代码的呢?

JVM 具体是怎么运行 Java 字节码的呢?下面咱们一块儿来看一下:开发工具

从 JVM 来看,执行 Java 代码首先须要将它编译而成的 class 文件加载到 JVM 中。加载后的 Java 类会被存放于方法区(Method Area)中。实际运行时,JVM 会执行方法区内的代码。优化

JVM 会在内存中划分出堆和栈来存储运行时数据,JVM 会将栈细分为面向 Java 方法的 Java 方法栈,面向本地方法(用 C++ 写的 native 方法)的本地方法栈,以及存放各个线程执行位置的 PC 寄存器。

在运行过程当中,每当调用进入一个 Java 方法,JVM 会在当前线程的 Java 方法栈中生成一个栈帧,用以存放局部变量以及字节码的操做数。栈帧的大小是提早计算好的,并且 JVM 不要求栈帧在内存空间里连续分布。

当退出当前执行的方法时,不论是正常返回仍是异常返回,JVM 均会弹出当前线程的当前栈帧,并将之舍弃。

从硬件视角来看,Java 字节码没法直接执行。所以,JVM 须要将字节码翻译成机器码。

在 HotSpot 里面,上述翻译过程有两种形式:第一种是解释执行(interpreter),即逐条将字节码翻译成机器码并执行;第二种是即时编译(Just-In-Time compilation,JIT),即将一个方法中包含的全部字节码编译成机器码后再执行。

前者的优点在于无需等待编译,然后者的优点在于实际运行速度更快。HotSpot 默认采用混合模式,综合了解释执行和即时编译二者的优势。它会先解释执行字节码,然后将其中反复执行的热点代码,以方法为单位进行即时编译。

整个 Java 代码执行过程以下:

  1. 使用 javac 把 .java 源文件编译为字节码(文件后缀名为 .class)
  2. 字节码通过 JIT 环境变量进行判断,是否属于热点代码(屡次调用的方法或循环体)
  3. 热点代码使用 JIT 编译为可执行的机器码
  4. 非热点代码使用解释器解释执行全部字节码

其中,在运行过程当中会被即时编译的热点代码有两类:

  1. 被屡次调用的方法
  2. 被屡次执行的循环体

针对第一类,编译器会将整个方法做为编译对象,这也是标准的 JIT 编译方式。对于第二类是由循环体出发的,可是编译器依然会以整个方法做为编译对象,由于发生在方法执行过程当中,称为栈上替换。

HotSpot 采用了多种技术来提高启动性能以及峰值性能,刚刚提到的即时编译即是其中最重要的技术之一。

即时编译创建在程序符合二八定律的假设上,也就是百分之二十的代码占据了百分之八十的计算资源。

对于占据大部分的不经常使用的代码,咱们无需耗费时间将其编译成机器码,而是采起解释执行的方式运行;另外一方面,对于仅占据小部分的热点代码,咱们则能够将其编译成机器码,以达到理想的运行速度。

为了知足不一样用户场景的须要,HotSpot 内置了多个即时编译器:C一、C2。之因此引入多个即时编译器,是为了在编译时间和生成代码的执行效率之间进行取舍。

  • C1 (Client 编译器)面向的是对启动性能有要求的客户端 GUI 程序,采用的优化手段相对简单,所以编译时间较短。
  • C2 (Server 编译器)面向的是对峰值性能有要求的服务器端程序,采用的优化手段相对复杂,所以编译时间较长,但同时生成代码的执行效率较高。

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

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

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

其中判断一段代码是否为热点代码,是否是须要触发即时编译,这样的行为称为热点探测(Hot Spot Detection),探测算法有两种:

  1. 基于采样的热点探测(Sample Based Hot Spot Detection):虚拟机会周期的对各个线程栈顶进行检查,若是某些方法常常出如今栈顶,这个方法就是热点方法。优势是实现简单、高效,很容易获取方法调用关系。缺点是很难确认方法的 reduce,容易受到线程阻塞或其余外因扰乱。
  2. 基于计数器的热点探测(Counter Based Hot Spot Detection):为每一个方法(甚至是代码块)创建计数器,执行次数超过阈值就认为是热点方法。优势是统计结果精确严谨。缺点是实现麻烦,不能直接获取方法的调用关系。

HotSpot 使用的是第二种-基于计数器的热点探测,而且有两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。

总结

这篇文章主要介绍了为何须要 JVM 以及 JVM 是怎样运行 Java 代码的。

为何须要 JVM:

  1. 提供了可移植性。一次编译,处处执行。
  2. 提供了代码托管的环境,代替处理部分冗长并且容易出错的部分。

JVM 将运行时内存区域划分为五个部分,分别为方法区、堆、PC 寄存器、Java 方法栈和本地方法栈。Java 程序编译而成的 class 文件,须要先加载至方法区中,方能在 JVM 中运行。

为了提升运行效率,HotSpot 虚拟机采用的是一种混合执行的策略,会解释执行 Java 字节码,而后会将其中反复执行的热点代码,以方法为单位进行即时编译,翻译成机器码后直接运行在底层硬件之上。

HotSpot 装载了多个不一样的即时编译器,以便在编译时间和生成代码的执行效率之间作取舍。

判断热点代码的探测算法包括基于采样和基于计数器两种,HotSpot 采用基于计数器的热点探测,计数器又分为方法调用计数器和回边计数器。

相关文章
相关标签/搜索