JVM(1)---虚拟机在运行期的优化策略

简介: 1.解释器与JIT编译器首先我们先来了解一下运行在虚拟机之上的解释器与JIT编译器。当我们的虚拟机在运行一个java程序的时候,它可以采用两种方式来运行这个java程序:采用解释器的形式,也就是说,在运行.class运行的时候,解释器一边把.class文件翻译成本地机器码,一边执行。

1.解释器与JIT编译器

首先我们先来了解一下运行在虚拟机之上的解释器JIT编译器
当我们的虚拟机在运行一个java程序的时候,它可以采用两种方式来运行这个java程序:

  1. 采用解释器的形式,也就是说,在运行.class运行的时候,解释器一边把.class文件翻译成本地机器码,一边执行。显然这种一边解释翻译一边执行发方式,可以使我们立即启动和执行程序,省去编译的时间。不过由于需要一遍解释翻译,会让程序的执行速度比较慢。
  2. 采用JIT编译器的方式:注意,JIT编译器是把.class文件翻译成本地机器码,而javac编译器是把.java源文件编译成.class文件。如果采用JIT编译器的方式则是在启动运行一个程序的时候,先把.class文件全部翻译成本地机器码,然后再来执行,显然,这种方式在执行的时候由于不用对.clasa文件进行翻译,所以执行的速度会比较快。当然,代价就是我们需要花销一定的时间来把字节码翻译成本地机器码。这样,程序在启动的时候,会有更多的延迟。

这两种方式可以说是各有优势,虚拟机(特指HotSpot虚拟机)在执行的时候,一般会采用两种方式结合的策略。

也就是说,在程序执行的时候,有些代码采用解释器的方式,有些代码采用编译器,称之为即时编译。一般我们会对热点代码采用编译器的方式。

2.编译对象与触发条件

上面已经说了,运行过程中,如果遇到热点代码就会触发对该代码进行编译,编译成本地机器码。
什么是热点代码?
热点代码主要有一下两类:

  1. 被多次调用的方法。
  2. 被多次执行的循环体。
    不过这里需要注意的是,由于循环体是存在方法之中的,尽管编译动作是由循环体触发的,但编译器仍然会以这个方法来作为编译的对象。

3.热点探测

判断一段代码是不是热点代码,是不是需要触发即时编译,这样的行为我们称之为热点探测。热点探测判定有以下两种方式:

  1. 基于采样的热点探测:这种方式虚拟机会周期性着检查各个线程的栈顶,如果发现某个方法经常出现在栈顶,那么这个方法就是热点方法。可能有人会问,所谓经常,那什么样才算经常,对于这个我只能告诉你,这个取决于你自己的设置,如果自己没有进行相应的设置的话,就采用虚拟机的默认设置。
  2. 基于计数器的热点探测:这种方法我们会为每个方法设置一个计数器,统计方法被调用的次数,如果到达一定的次数,我们就把它当作是热点方法

两种方法的优缺点
显然第一种方法在实现上是比较简单、高效的,但是缺点也很明显,精确度不高,容易受到线程阻塞等别的外界因素的干扰。
第二种方式的统计结果会很精确,但需要为每个方法建立并维护一个计数器。实现上会相对复杂一点并且开销也会大点。
不过,这里需要指出的是,我们的HotSpot虚拟机采用的是基于计数器的方式。

说明:虚拟机在执行方法的时候,会先判断该方法是否存在已经编译好的版本,如果存在,则执行编译好的本地机器码,否则,采用一边解释一边编译的方式。

4.编译优化技术

先看一段代码:

int a = 1;
if(false){
    System.out.println("无用代码");
}
int b = 2;

对于这段代码,我们都知道是if语句体里面的代码是一定不可能会被执行到的,也就是说,这实际上是一段一点用处也没有的代码,在执行时只能浪费判断时间。
实际上,对于我们书写的代码,编译器在编译的时候是会进行优化的。对于上面的代码,编译优化之后会变成这样:

int a = 1;
int b = 2;

那段无用的代码会被消除掉。

各种编译优化策略

我们刚才已经说了,对于有些被多次调用的方法或者循环体,虚拟机会先把他们编译成本地机器码。由于这些热点代码都是一些会被多次重复执行的代码,为了使得编译好的代码更加完美,运行的更快。编译器做了很多的编译优化策略,例如上面的无用代码消除就是其中的一种。
下面我们来讲讲大概都有那些优化策略:
大概预览一波:

  1. 公共子表达式消除。
  2. 数组范围检查消除。
  3. 方法内联。
  4. 逃逸分析。

(1).公共子表达式消除
含义:如果一个表达式 E 已经计算过了,并且从先前的计算到现在 E 中的所有变量的值都没有发生变化,那个 E 的这次出现就成为了公共子表达式。对于这样的表示式,没有必要对它再次进行计算了,直接沿用之前的结果就可以了。
我们来举个例子。例如

int d = (c * b) * 10 + a + (a + b * c);

这段代码到了即时编译器的手里,它会进行如下优化:
表达式中有两个 b * c的表达式,并且在计算期间b与c的值并不会变。所以这条表达式可能会被视为:

int d = E * 10 + a+ (a + E);

接着继续优化成

int d = E * 11 + a + a;

接着

int d = E * 11 + 2a;

这样,代码在执行的时候,就会节省了一些时间了。

(2).数组范围检查消除
我们知道,java是一门动态安全的语言,对数组的访问不像c/c++那样,可以采用指针指向一块可能不存在的区域。例如假如有一个数组arr[],在java语言中访问数组arr[i]的时候,是会先进行上下界范围检查的,即先检查i是否满足i >= 0 && i < arr.length这个条件。如果不满足则会抛出相应的异常。这种安全检查策略可以避免溢出。但每次数组访问都会进行这样一次检查无疑在速度性能上造成一定的影响。
实际上,对于这样一种情况,编译器也是可以帮助我们做出相应的优化的。例如对于数组的下标是一个常量的,如arr[2],只要在编译期根据数据流分析来确定arr.length的值,并判断下标‘2’并没有越界,这样在执行的时候就无需在判断了。
更常见的情况是数组访问发生在循环体中,并且使用循环变量来进行数组的访问,对于这样的情况,只要编译器通过数据流就可以判断循环变量的取值范围是否在[0, arr.length)之内,如果是,那么整个循环中就可以节省很多次数组边界检测判断的操劳了。

对于这些安全检查所消耗的时间,实际上,我们还可以采用另外一种策略—隐式异常处理。例如当我们在访问一个对象arr的属性arr.value的时候,没有优化之前虚拟机是这样处理的:

if(arr != null){
    return arr.value;
}else{
    throw new NollPointException();
}

采用优化策略之后编程这样子:

try{
    return arr.value;
}catch(segment_fault){
    uncommon_trap();
}

就是说,虚拟机会注册一个Segment Fault信号的异常处理器(uncommon_trap()),这样当arr不为空的时候,对value的访问可以省去对arr的判断。代价就是当arr为空时,必须转入到异常处理器中恢复并抛出NullPointException异常,这个过程会从用户态转到内核态中处理,结束后在回到用户态,速度远比一次判断空检查慢。当arr极少为null的时候,这样做是值得的,但假如arr经常为null时,那么会得不偿失。
不过,虚拟机还是挺聪明的,它会根据运行期收集到的信息来自动选择最优方案。

(3).方法内联
先看一段代码

public static void f(Object obj){
    if)(obj != null){
        System.out.println("do something");
    }
}
public static void test(String[] args){
    Object obj = null;
    f(obj);
}

对于这段代码,如果把两个方法结合在一起看,我们可以发现test()方法里面都是一些无用的代码。因为f(obj)这个方法的调用,没啥卵用。但是如果不做内联优化,后续尽管进行了无用代码的消除,也是无法发现任何无用代码的,因为如果把f(Object obj)和test(String[] args)两个发放分开看的话,我们就无法得只f(obj)是否有用了。
内联优化后的代码可以是这样:

public static void f(Object obj){
    if)(obj != null){
        System.out.println("do something");
    }
}
public static void test(String[] args){
    Object obj = null;
    //该方法直接不执行了
}

(4).逃逸分析
逃逸分析是目前Java虚拟机比较前沿的优化技术,它并非是直接优化代码,而是为其他优化手段提供依据发分析技术。
逃逸分析主要是对对象动态作用域进行分析:当一个对象在某个方法被定义后,它有可能被外部的其他方法所引用,例如作为参数传递给其他方法,称之为方法逃逸,也有可能被外部线程访问到,例如类变量,称之为线程逃逸


假如我们可以证明一个对象并不会发生逃逸的话,我们就可以通过一些方式对这个变量进行一些高效的优化了。如下所示:


1).栈上分配
我们都知道一个对象创建之后是放在上的,这个对象可以被其他线程所共享,并且我们知道在堆上的对象如果不再使用时,虚拟机的垃圾收集系统就会对它进行帅选并回收。但无论是回收还是帅选,都是需要花费时间的。
但是假如我们知道这个对象不会逃逸的话,我们就可以直接在栈上对这个对象进行内存分配了,这样,这个对象所占用的内存空间就可以随进栈和出栈而自动被销毁了。这样,垃圾收集系统就可以省了很多帅选、销毁的时间了。

2).同步消除
线程同步本身是一个相对耗时的过程,如果我们能判断这个变量不会逃出线程的话,那么我们就可以对这个变量的同步措施进行消除了。


3).标量替换
什么是标量?
当一个数据无法分解成更小的时候,我们称之为变量,例如像int,long,char等基本数据类型。相对地,如果一个变量可以分解成更小的,我们称之为聚合量,例如Java中的对象。
假如这个对象不会发生逃逸。
我们可以根据程序访问的情况,如果一个方法只是用到一个对象里面的若干个属性,我们在真正执行这个方法的时候,我们可以不创建这个对象,而是直接创建它那几个被使用到的变量来代替。这样,不仅可以节省内存以及时间,而且这些变量可以随出栈入栈而销毁。

不过,对于编译器优化的技术还有很多,上面这几种算是比较典型的。
本次讲解到这里。

参考书籍:深入Java虚拟机

 

目录
相关文章
|
Linux 虚拟化 iOS开发
VMware Fusion 13.6.4 OEM BIOS 2.7 - 在 macOS 中运行 Windows 虚拟机的最佳方式
VMware Fusion 13.6.4 OEM BIOS 2.7 - 在 macOS 中运行 Windows 虚拟机的最佳方式
1514 3
|
缓存 监控 算法
JVM简介—2.垃圾回收器和内存分配策略
本文介绍了Java垃圾回收机制的多个方面,包括垃圾回收概述、对象存活判断、引用类型介绍、垃圾收集算法、垃圾收集器设计、具体垃圾回收器详情、Stop The World现象、内存分配与回收策略、新生代配置演示、内存泄漏和溢出问题以及JDK提供的相关工具。
JVM简介—2.垃圾回收器和内存分配策略
|
监控 网络安全 虚拟化
Hyper-V中Win10,虚拟机运行错误处理的方案
当Hyper-V中的Windows 10虚拟机出现运行错误时,可按以下步骤处理:首先进行基本检查与修复,包括检查虚拟机配置、确保Hyper-V服务正常运行及重启相关服务。其次,使用PowerShell命令或DISM工具修复虚拟机配置和系统组件。接着,查看事件查看器中的错误日志,分析问题原因。调整虚拟机资源分配,优化性能。针对特定错误情况,如启动失败或网络问题,采取相应措施解决。若问题仍未解决,考虑克隆、重置或重新安装虚拟机,必要时联系技术支持。操作前请备份重要数据并以管理员身份运行命令。
1516 22
|
监控 Java 编译器
聊聊JVM如何优化
JVM的优化是一个复杂而细致的过程,涉及内存管理、垃圾回收、即时编译、线程调度等多个方面。通过合理配置JVM参数、选择合适的垃圾回收器、优化线程调度和使用专业的监控工具,可以大幅提升Java应用的性能和稳定性。掌握这些优化技巧,能够帮助开发者在高并发、高负载的生产环境中保持系统的高效运行。
886 13
|
存储 设计模式 监控
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
429 0
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
|
存储 设计模式 监控
快速定位并优化CPU 与 JVM 内存性能瓶颈
本文介绍了 Java 应用常见的 CPU & JVM 内存热点原因及优化思路。
1425 166
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
监控 算法 Java
Java虚拟机(JVM)垃圾回收机制深度剖析与优化策略####
本文作为一篇技术性文章,深入探讨了Java虚拟机(JVM)中垃圾回收的工作原理,详细分析了标记-清除、复制算法、标记-压缩及分代收集等主流垃圾回收算法的特点和适用场景。通过实际案例,展示了不同GC(Garbage Collector)算法在应用中的表现差异,并针对大型应用提出了一系列优化策略,包括选择合适的GC算法、调整堆内存大小、并行与并发GC调优等,旨在帮助开发者更好地理解和优化Java应用的性能。 ####
485 27
|
10月前
|
存储 缓存 Java
我们来说一说 JVM 的内存模型
我是小假 期待与你的下一次相遇 ~
613 5