Java 教程
- Java - 首页
- Java - 概述
- Java - 历史
- Java - 特性
- Java 与 C++
- JVM - Java 虚拟机
- Java - JDK 与 JRE 与 JVM
- Java - Hello World 程序
- Java - 环境设置
- Java - 基本语法
- Java - 变量类型
- Java - 数据类型
- Java - 类型转换
- Java - Unicode 系统
- Java - 基本运算符
- Java - 注释
- Java - 用户输入
- Java - 日期和时间
Java 控制语句
- Java - 循环控制
- Java - 决策制定
- Java - If-else
- Java - Switch
- Java - For 循环
- Java - For-Each 循环
- Java - While 循环
- Java - do-while 循环
- Java - Break
- Java - Continue
面向对象编程
- Java - OOPs 概念
- Java - 对象和类
- Java - 类属性
- Java - 类方法
- Java - 方法
- Java - 变量作用域
- Java - 构造函数
- Java - 访问修饰符
- Java - 继承
- Java - 聚合
- Java - 多态
- Java - 重写
- Java - 方法重载
- Java - 动态绑定
- Java - 静态绑定
- Java - 实例初始化块
- Java - 抽象
- Java - 封装
- Java - 接口
- Java - 包
- Java - 内部类
- Java - 静态类
- Java - 匿名类
- Java - 单例类
- Java - 包装类
- Java - 枚举
- Java - 枚举构造函数
- Java - 枚举字符串
Java 内置类
Java 文件处理
Java 错误和异常
- Java - 异常
- Java - try-catch 块
- Java - try-with-resources
- Java - 多重 catch 块
- Java - 嵌套 try 块
- Java - Finally 块
- Java - throw 异常
- Java - 异常传播
- Java - 内置异常
- Java - 自定义异常
Java 多线程
- Java - 多线程
- Java - 线程生命周期
- Java - 创建线程
- Java - 启动线程
- Java - 线程连接
- Java - 线程命名
- Java - 线程调度器
- Java - 线程池
- Java - 主线程
- Java - 线程优先级
- Java - 守护线程
- Java - 线程组
- Java - 关闭钩子
Java 同步
Java 网络
- Java - 网络
- Java - 套接字编程
- Java - URL 处理
- Java - URL 类
- Java - URLConnection 类
- Java - HttpURLConnection 类
- Java - Socket 类
- Java - 泛型
Java 集合
Java 接口
Java 数据结构
Java 集合算法
高级 Java
- Java - 命令行参数
- Java - Lambda 表达式
- Java - 发送电子邮件
- Java - Applet 基础
- Java - Javadoc 注释
- Java - 自动装箱和拆箱
- Java - 文件不匹配方法
- Java - REPL (JShell)
- Java - 多版本 Jar 文件
- Java - 私有接口方法
- Java - 内部类菱形运算符
- Java - 多分辨率图像 API
- Java - 集合工厂方法
- Java - 模块系统
- Java - Nashorn JavaScript
- Java - Optional 类
- Java - 方法引用
- Java - 函数式接口
- Java - 默认方法
- Java - Base64 编码解码
- Java - Switch 表达式
- Java - Teeing 收集器
- Java - 微基准测试
- Java - 文本块
- Java - 动态 CDS 存档
- Java - Z 垃圾收集器 (ZGC)
- Java - 空指针异常
- Java - 打包工具
- Java - 密封类
- Java - 记录类
- Java - 隐藏类
- Java - 模式匹配
- Java - 紧凑数字格式化
- Java - 垃圾回收
- Java - JIT 编译器
Java 杂项
- Java - 递归
- Java - 正则表达式
- Java - 序列化
- Java - 字符串
- Java - 进程 API 改进
- Java - 流 API 改进
- Java - 增强的 @Deprecated 注释
- Java - CompletableFuture API 改进
- Java - 流
- Java - 日期时间 API
- Java 8 - 新特性
- Java 9 - 新特性
- Java 10 - 新特性
- Java 11 - 新特性
- Java 12 - 新特性
- Java 13 - 新特性
- Java 14 - 新特性
- Java 15 - 新特性
- Java 16 - 新特性
Java API 和框架
Java 类参考
- Java - Scanner
- Java - 数组
- Java - 字符串
- Java - Date
- Java - ArrayList
- Java - Vector
- Java - Stack
- Java - PriorityQueue
- Java - LinkedList
- Java - ArrayDeque
- Java - HashMap
- Java - LinkedHashMap
- Java - WeakHashMap
- Java - EnumMap
- Java - TreeMap
- Java - IdentityHashMap
- Java - HashSet
- Java - EnumSet
- Java - LinkedHashSet
- Java - TreeSet
- Java - BitSet
- Java - Dictionary
- Java - Hashtable
- Java - Properties
- Java - Collection
- Java - Array
Java 有用资源
Java - 即时 (JIT) 编译器
即时 (JIT) 编译器是 JVM 内部使用的编译器,用于将字节码中的热点转换为机器可理解的代码。JIT 编译器的主要目的是对性能进行大量优化。
Java 编译的代码面向 JVM。Java 编译器 javac 将 Java 代码编译成字节码。现在 JVM 解释此字节码并在底层硬件上执行它。如果某些代码要反复执行,JVM 会将该代码识别为热点,并使用 JIT 编译器 将代码进一步编译到本机机器代码级别,并在需要时重用编译后的代码。
让我们首先了解编译型语言与解释型语言之间的区别,以及 Java 如何利用这两种方法的优势。
编译型语言与解释型语言
诸如 C、C++ 和 FORTRAN 等语言是编译型语言。它们的代码作为面向底层机器的二进制代码交付。这意味着高级代码由专门为底层架构编写的静态编译器一次性编译成二进制代码。生成的二进制文件不会在任何其他架构上运行。
另一方面,解释型语言(如 Python 和 Perl)可以在任何机器上运行,只要它们具有有效的解释器即可。它逐行遍历高级代码,将其转换为二进制代码。
解释型代码通常比编译型代码慢。例如,考虑一个循环。一个解释器将为循环的每次迭代转换相应的代码。另一方面,编译后的代码只会转换一次。此外,由于解释器一次只看到一行代码,因此它们无法执行任何重要的代码优化,例如更改语句的执行顺序,例如编译器。
示例
我们将在下面研究此类优化的一个示例:
将存储在内存中的两个数字相加:由于访问内存可能会消耗多个 CPU 周期,因此一个好的编译器会发出指令从内存中获取数据,并且只有在数据可用时才执行加法运算。它不会等待,并且在此期间执行其他指令。另一方面,在解释期间不可能进行此类优化,因为解释器在任何给定时间都不了解整个代码。
但是,解释型语言可以在任何具有该语言有效解释器的机器上运行。
Java 是编译型语言还是解释型语言?
Java 试图找到一个折衷方案。由于 JVM 位于 javac 编译器和底层硬件之间,因此 javac(或任何其他编译器)编译器会将 Java 代码编译成字节码,字节码由特定于平台的 JVM 理解。然后,JVM 在代码执行时使用JIT(即时)编译将字节码编译成二进制代码。
热点
在典型的程序中,只有一小部分代码会被频繁执行,并且通常,正是这段代码会显著影响整个应用程序的性能。此类代码部分称为热点。
如果某些代码段只执行一次,那么编译它将是一种浪费,并且解释字节码会更快。但是,如果该部分是热点并且执行多次,则 JVM 将对其进行编译。例如,如果多次调用某个方法,那么编译代码所需的多余周期将被生成的更快二进制代码抵消。
此外,JVM 运行特定方法或循环的次数越多,它收集的信息就越多,从而进行各种优化,以便生成更快的二进制代码。
JIT 编译器的工作原理
JIT 编译器通过将某些热点代码编译成本机代码或机器代码来帮助提高 Java 程序的执行时间。
JVM 扫描完整代码并识别热点或需要由 JIT 优化的代码,然后在运行时调用 JIT 编译器,从而提高程序的效率并使其运行得更快。
由于JIT 编译是一项占用处理器和内存的活动,因此需要相应地规划 JIT 编译。
编译级别
JVM 支持五种编译级别 -
- 解释器
- 带有完整优化(无分析)的 C1
- 带有调用和回边计数器(轻量级分析)的 C1
- 带有完整分析的 C1
- C2(使用来自先前步骤的分析数据)
如果您希望禁用所有 JIT 编译器 并仅使用解释器,请使用 -Xint。
客户端与服务器 JIT(即时)编译器
使用 -client 和 -server 激活相应的模式。客户端编译器(C1)比服务器编译器(C2)更早开始编译代码。因此,当 C2 开始编译时,C1 已经编译了部分代码。
但是,在等待时,C2 会分析代码以了解比 C1 更多的信息。因此,等待的时间被可以用来生成更快的二进制文件的优化所抵消。
从用户的角度来看,权衡是在程序的启动时间和程序运行时间之间。如果启动时间是首要因素,则应使用 C1。如果应用程序预计要运行很长时间(服务器上部署的应用程序的典型情况),最好使用 C2,因为它会生成更快的代码,从而大大抵消任何额外的启动时间。
对于 IDE(NetBeans、Eclipse)和其他 GUI 程序等程序,启动时间至关重要。NetBeans 可能需要一分钟或更长时间才能启动。当启动 NetBeans 等程序时,会编译数百个类。在这种情况下,C1 编译器是最佳选择。
请注意,C1 有两个版本 - 32 位和 64 位。C2 仅提供 64 位版本。
JIT 编译器优化的示例
以下示例展示了 JIT 编译器优化
对象情况下 JIT 优化的示例
让我们考虑以下代码 -
for(int i = 0 ; i <= 100; i++) { System.out.println(obj1.equals(obj2)); //two objects }
如果这段代码被解释,解释器将在每次 for each 迭代中推断出 obj1 的类。这是因为 Java 中的每个类都有一个 .equals() 方法,该方法扩展自 Object 类并且可以被覆盖。因此,即使 obj1 在每次迭代中都是字符串,仍然会进行推断。
另一方面,实际上会发生的是,JVM 会注意到每次迭代中 obj1 都是 String 类,因此,它会直接生成对应于 String 类的 .equals() 方法 的代码。因此,不需要查找,编译后的代码将执行得更快。
这种行为只有在 JVM 知道代码的行为时才有可能。因此,它会在编译代码的某些部分之前等待。
基本数据类型情况下 JIT 优化的示例
下面是另一个示例 -
int sum = 7; for(int i = 0 ; i <= 100; i++) { sum += i; }
对于每个循环,解释器都会从内存中获取 'sum' 的值,将 'i' 加到它上面,并将其存储回内存中。内存访问是一项昂贵的操作,通常需要多个 CPU 周期。由于此代码运行多次,因此它是一个热点。JIT 将编译此代码并进行以下优化。
'sum' 的本地副本将存储在寄存器中,该寄存器特定于某个线程。所有操作都将在寄存器中的值上执行,并且当循环完成后,该值将被写回内存。
如果其他线程也在访问该变量怎么办?由于其他线程正在对变量的本地副本进行更新,因此它们将看到过时的值。在这种情况下,需要线程同步。一个非常基本的同步原语是将 'sum' 声明为 volatile。现在,在访问变量之前,线程将刷新其本地寄存器并从内存中获取该值。访问它之后,该值会立即写入内存。
即时 (JIT) 编译器执行的优化
以下是 JIT 编译器执行的一些通用优化 -
- 方法内联
- 死代码消除
- 优化调用站点的启发式方法
- 常量折叠