1.JVM具体是如何运行Java字节码?
- 从虚拟机视角来看,执行Java代码首先需要将它编译而成的class文件加载到Java虚拟机中。加载后的Java类会被存放于方法区(Method Area)中。实际运行时,虚拟机会执行方法区内的代码。
- Java虚拟机在内存中划分出堆和栈来存储运行时数据,其中栈又细分为面向Java方法的Java方法栈,面向本地方法(用C++写的native方法)的本地方法栈,以及存放各个线程执行位置的PC寄存器。
- 在运行过程中,每当调用进入一个Java方法,JVM会在当前线程的Java方法栈中生成一个栈帧,用以存放局部变量以及字节码的操作数。这个栈帧的大小是提前计算好的,而且JVM不要求栈帧在内存空间里连续分布。
- 当退出当前执行的方法时,不管是正常返回还是异常返回,JVM均会弹出当前线程的当前栈帧,并将其舍弃。
- 从硬件视角来看,Java字节码无法直接执行。因此,JVM需要将字节码翻译成机器码。
- 在HotSpot里面,上述翻译过程有两种形式:第一种是解释执行,即逐条将字节码翻译成机器码并执行;第二种是即时编译(Just-In-Time compilation,JIT),即将一个方法中包含的所有字节码编译成机器码后在执行。
- 前者的优势在于无需等待编译,二后者的优势在于实际运行速度更快。HotSpot默认采用混合模式,综合了解释执行和即时编译两者的优点。它会解释执行字节码,而后将其中反复执行的热点代码,以方法为单位进行即时编译。
2.JVM的运行效率如何?
- 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编译器。
- 在计算资源充足的情况下,字节码的解释执行和即时编译可同时进行。编译完成的机器码会在下次调用该方法是启用,以替换原本的解释执行。
3.HotSpot 的热点代码探测技术
- 基于采样的热点探测
- 基于计数器的热点探测(大多数采用)
- 基于计数器的热点探测的虚拟机会为每个方法建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值,就认为它是热点方法。
- 方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间内方法被调用的次数,当超过一定的时间限度,如果方法的调动次数仍然不足以让它提交给即时编译器,那这个方法的调用计时器就会被减少一半,这个过程称为方法调用计时器热度的衰减,而这段时间就称为方法统计的半衰周期,进行热度衰减的动作在虚拟机进行垃圾收集时顺便进行了。
- 判断一个循环体代码是否为热点代码的方式: 回边计数器。它的作用是统计一个方法体重循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为回边,显然,建立回边计数器的目的就是为了触发OSR(On Stack Replace)编译。没有计数热度衰减的过程,因此这个计数器统计的就是该方法执行循环的绝对次数,当计数器溢出的时候它还会把方法计数器的值也调整到溢出的状态,这样下去在再次进入该方法的时候就会执行标准编译过程
4.Java基本类型

- boolean 和 char 是唯二的无符号类型。在不考虑违反规范的情况下,boolean类型的取值范围是0 或者 1. char类型的取值范围是 [0,65535]。
- NaN(Not-a-Number) 有一个有趣的特性:除了“!=”始终返回 true 之外,所有其他比较结果都会返回false。
- JVM每调用一个Java方法,便会创建一个栈帧,有两个组成部分,分别是局部变量区,以及字节码的操作数栈。这里的局部变量是广义的,除了普遍意义下的局部变量之外,他还包括实例方法的 “this 指针” 以及方法所接受的参数。
- 在 Java 虚拟机规范中, 局部变量区等价于一个数组, 并且可以用正整数来索引。 除了 long、double 需要用两个数组单元来存储之外, 其他基本类型以及引用类型的值均占用一个数组单元。
- 也就是说,boolean、 byte、 char、 short这四种类型, 在栈上占用的空间和 int 是一样的, 和引用类型也是一样的。 因此,在32位的 HotSpot 中, 这些类型在栈上将占用4个字节; 而在 64位的HotSpot 中, 他们将占 8个字节(
仅限于局部变量
)。 - 然而对于byte、char以及short 这三种类型的字段或者数组单元,他们在堆上占用的空间分别为一字节、两字节以及两字节,也即是说,跟这些类型 的 值域相吻合。
- 存储:这些类型的字段或数组存储时,相当于做了一次隐式的掩码操作。如:当我们把0xFFFFFFFF(-1)存储到一个声明为char类型的字段里,由于该字段仅占两字节,所以高两位的字节便会被截取掉,最终存入“\uFFFF”。
- 加载:JVM的算术运算几乎全部依赖于操作数栈。也就是说,我们需要将堆中的 boolean、byte、char以及short 加载到操作数栈上,而后将栈上的值当成int 类型来运算。故对于非负数进行高位补 0 ,否则用 1来充填
网友评论