Java 虚拟机笔记
内存结构
垃圾回收
对象存活判断
引用计数法
引用计数法为每个对象维护一个引用计数器,记录该对象当前被引用的次数。每当创建一个新的引用指向该对象时,其计数器加 1;每当指向该对象的引用失效时,计数器减 1。当对象的引用计数器为 0 时,该对象被视为垃圾,可以被回收。
特点:
引用计数法实现简单,易于理解和维护,而且效率较高,适合生命周期较短的对象。缺点是无法解决循环引用的问题,可能会引发内存泄露。
可达性分析算法
可达性分析算法是 JVM 用于判断对象是否可被回收的一种算法。该算法的基本思想是通过从一组称为"GC Roots"的根对象出发,递归地遍历所有的引用关系,标记所有被这些根对象直接或间接引用的对象为可达对象,而未被标记的对象则被认为是不可达的,即可以被回收。
GC Roots 是一组活跃的引用,不是对象,放在 GC Roots Set 集合。在Java中,GC Roots包括:
- 虚拟机栈中引用的对象:即本地变量表中引用的对象
- 方法区中类静态属性引用的对象:即类的静态变量引用的对象
- 方法区中常量引用的对象:即常量池中引用的对象
- 本地方法栈中JNI(Java Native Interface)引用的对象
- JVM内部的引用
- 所有被同步锁
synchronized
持有的对象 - 反映JVM内部情况的
JMXBean
、JVMTI
等等
特点:
- 精确性:可达性分析算法能够准确地判断对象是否可被回收,避免了引用计数算法可能存在的循环引用问题。
- 高效性:虽然需要遍历整个对象图,但是因为只有被根集合直接或间接引用的对象才会被标记为可达,所以可达性分析算法通常能够较快地确定出不可达对象。
引用类型
垃圾回收算法
见:JVM 垃圾回收
垃圾回收实现
见:JVM 垃圾回收
JVM 工具
Java虚拟机(JVM)提供了多种故障排查工具,用于诊断和解决Java应用程序运行时出现的各种问题。这些工具JDK9之前在jdk\lib\tools目录下,经过模块化改造后现在在jdk\jmods目录下,本身用Java语言实现。
基础工具:用于支持基本的程序创建和运行
- 如
jar, java, javac, javadoc, javap, jdb...
- 如
性能监控和故障处理:监控Java虚拟机运行信息,排查问题
- 如
jps, jstat, jinfo, jmap, jhat...
- 如
安全:用于程序签名,设置安全测试等
- 如
keytool, jarsigner, policytool
- 如
国际化:用于创建本地语言文件
- 如
native2ascii
- 如
远程方法调用: 用于跨Web或网络的服务交互
- 如
rmic, rmiregistry, rmid, serialver
- 如
部署工具:用于程序打包、发布和部署
- 如
javapackager, pack200, unpack200
- 如
REPL (Read-Eval-Print Loop)和脚本工具:
jshell, jjs, jrunscript
Java IDL 与 RMI-IIOP: 与JDK11的CORBA一起废弃
WebService工具:与JDK11的CORBA一起废弃
Java Web Start: javaws, jdk11移除
jps
JVM Process Status Tool,虚拟机进程状况工具是Java虚拟机自带的一种命令行工具,用于列出当前系统中正在运行的Java进程的信息。它主要用于查看Java进程的进程ID(PID)以及与之关联的Java应用程序的主类名。
命令格式:
jps [ options ] [ hostid ]
选项:-q
:仅输出Java进程的PID,不输出主类名 -m
:输出主类名和传递给主类的参数 -l
:输出完全限定的主类名(包括包名) -v
:输出传递给Java虚拟机的参数
样例:
jstat
JVM Statistics Monitoring Tool,虚拟机统计信息监视工具,用于监视和显示Java进程的各种运行时统计信息,如垃圾回收情况、类加载情况、JIT编译情况等。
命令格式:
jstat [ option vmid [ interval[s|ms] [ count ] ]
选项:
option
:指定要获取的统计信息类型,如垃圾回收情况、类加载情况等。vmid
:指定要监视的Java虚拟机的进程ID(PID)或标识符。可以是本地Java进程的PID,也可以是远程Java进程的主机名:端口号。interval
:指定获取统计信息的时间间隔,默认单位是秒。可选参数为s
(秒)或ms
(毫秒)。count
:指定获取统计信息的次数。
样例:
jinfo
Configuration Info for Java,Java配置信息工具,用于查看和修改Java进程的运行时配置信息。它可以查看Java进程的启动参数、系统属性、环境变量以及动态链接库信息等。
命令格式:
jinfo [ option ] pid
选项:
-flag <name>
:显示指定名称的JVM标志的值。-flags
:显示所有JVM标志的值。-sysprops
:显示Java系统属性的值。-sysprop <name>
:显示指定名称的Java系统属性的值。-env
:显示Java进程的环境变量。-jvmflags
:显示Java虚拟机的启动参数。
样例:
jmap
Memory Map for Java,Java内存映像工具,用于生成Java进程的堆转储(Heap Dump)。堆转储是Java堆中对象的详细信息的快照,包括对象的类名、实例数量、大小等。
命令格式:
jmap [ option ] pid
选项:
-heap
:显示Java堆的概要信息,包括堆的配置信息、使用情况、垃圾收集器等。-histo[:live]
:生成Java堆中对象的直方图(Histogram)。加上:live
参数可以只统计存活对象。-dump:format=b,file=<filename>
:生成Java堆转储文件,并指定文件名和格式。常用格式有b
(二进制格式)和hprof
(Hprof格式)。-F
:当Java进程不响应时,强制执行堆转储操作。
样例:
jhat
JVM Heap Analysis Tool,虚拟机堆转储快照分析工具,用于分析Java堆转储(Heap Dump)文件。它可以加载堆转储文件,并提供一个简单的基于Web的用户界面,用于浏览和分析Java堆中的对象信息。
命令格式:
jhat [ option ] heap-dump-file
选项:
-port <port>
:指定jhat
工具监听的端口号,默认为7000。-J<option>
:传递给Java虚拟机的参数。
示例:
# 加载指定的Java堆转储文件,并启动jhat服务器
jhat heap_dump.hprof
# 指定端口号并启动jhat服务器
jhat -port 8080 heap_dump.hprof
jstack
Stack Trace for Java,Java堆栈跟踪工具,用于生成Java进程的线程转储(Thread Dump)。线程转储是Java进程中所有线程当前的状态的快照,包括线程的调用栈、线程状态等信息。
命令格式:
jstack [ option ] pid
选项:
- 无选项:生成包含所有线程调用栈信息的线程转储。
-F
:当Java进程不响应时,强制执行线程转储操作。-l
:除了线程调用栈信息外,还会显示关于锁的附加信息。
样例:
JHSDB
JHSDB(Java HotSpot Debugger)是Java HotSpot虚拟机自带的一种调试工具,用于在运行时检查和调试Java应用程序。它提供了一组命令行工具,用于检查和修改Java虚拟机的内部状态,包括堆、线程、对象等。
JHSDB提供了以下几个主要的命令行工具:
jhsdb jmap:用于生成Java进程的堆转储(Heap Dump),类似于
jmap
工具,但是支持更多的选项和功能。jhsdb jstack:用于生成Java进程的线程转储(Thread Dump),类似于
jstack
工具,但是支持更多的选项和功能。jhsdb jinfo:用于查看和修改Java进程的运行时配置信息,类似于
jinfo
工具,但是支持更多的选项和功能。jhsdb jstat:用于监视Java进程的各种运行时统计信息,类似于
jstat
工具,但是支持更多的选项和功能。jhsdb jstack:用于生成Java进程的线程转储(Thread Dump),类似于
jstack
工具,但是支持更多的选项和功能。jhsdb hsdb:启动Java HotSpot Debugger GUI(HSDB),提供一个图形化界面用于检查和调试Java虚拟机。
JHSDB工具集提供了丰富的功能,可以帮助开发人员诊断和调试Java应用程序的各种问题,包括内存泄漏、线程死锁、性能瓶颈等。它通常用于开发和调试阶段,对于生产环境不太适用。
JConsole
JConsole是Java自带的一种监控和管理工具,用于监视和管理Java应用程序的性能和资源使用情况。它提供了一个图形化的用户界面,可以实时查看Java应用程序的各种运行时信息,并且可以对Java应用程序进行一些基本的管理操作,如线程转储、堆转储、执行垃圾回收等。
以下是JConsole的一些主要功能和特点:
图形化监控: JConsole提供了一个直观的图形化界面,可以实时查看Java应用程序的各种运行时信息,包括内存使用情况、线程状态、类加载情况、垃圾收集情况等。
性能分析: JConsole可以帮助开发人员分析Java应用程序的性能瓶颈和优化机会,通过查看各种统计信息,如CPU使用率、内存使用情况等,找出性能问题并进行优化。
远程监控: JConsole支持通过JMX(Java Management Extensions)远程监控Java应用程序,即可以连接到运行在远程服务器上的Java进程,并监视其运行时信息。
基本管理操作: JConsole提供了一些基本的管理功能,如线程转储、堆转储、执行垃圾回收等。这些操作可以帮助开发人员进行调试和故障排查。
可扩展性: JConsole是一个基于JMX的插件化应用程序,可以通过安装不同的插件来扩展其功能,满足不同场景下的监控和管理需求。
VisualVM
VisualVM是一款功能强大的Java监控和调试工具,它提供了丰富的功能,用于监视、分析和调优Java应用程序的性能和内存使用情况。VisualVM是一个基于Java的图形化应用程序,集成了多种监控和调试工具,包括图形化性能分析、堆转储分析、线程分析、垃圾回收分析等。以下是VisualVM的一些主要功能和特点:
图形化监控: VisualVM提供了一个直观的图形化界面,可以实时监视Java应用程序的各种性能指标,包括CPU使用率、内存使用情况、线程状态、类加载情况等。
性能分析: VisualVM集成了多种性能分析工具,如CPU性能分析器、内存分析器等,可以帮助开发人员找出Java应用程序的性能瓶颈,并进行优化。
堆转储分析: VisualVM可以生成Java应用程序的堆转储文件,并提供一套强大的工具用于分析堆转储文件,包括对象分配状况、内存泄漏分析等。
线程分析: VisualVM可以监视Java应用程序中的线程状态,并提供线程分析工具,用于查找线程死锁、线程阻塞等问题。
垃圾回收分析: VisualVM可以监视Java应用程序的垃圾回收情况,并提供垃圾回收分析工具,用于分析垃圾回收的性能和效率。
插件支持: VisualVM是一个基于插件的可扩展应用程序,可以通过安装不同的插件来扩展其功能,满足不同场景下的监控和调试需求。
Java Mission Control
Java Mission Control(JMC)是Oracle提供的一款强大的Java应用程序性能监控和管理工具。它提供了丰富的功能,用于监视、分析和调优Java应用程序的性能和行为。JMC是基于Eclipse Rich Client Platform(RCP)开发的,具有直观的用户界面和灵活的扩展机制,适用于各种Java应用程序的监控和调试。
以下是Java Mission Control的一些主要功能和特点:
实时监控: Java Mission Control提供了实时的性能监控功能,可以监视Java应用程序的各种性能指标,包括CPU使用率、内存使用情况、线程状态、垃圾回收情况等。
事件分析: Java Mission Control可以捕获并分析Java应用程序中的各种事件,如方法调用、异常抛出、内存分配等,帮助开发人员了解Java应用程序的行为和性能瓶颈。
飞行记录: Java Mission Control支持飞行记录(Flight Recorder)功能,可以记录Java应用程序的运行过程和性能数据,并提供强大的工具用于分析录制的数据,如性能分析、事件分析等。
堆转储分析: Java Mission Control集成了堆转储分析工具,可以生成Java应用程序的堆转储文件,并提供一套工具用于分析堆转储文件,如对象分配状况、内存泄漏分析等。
线程分析: Java Mission Control可以监视Java应用程序中的线程状态,并提供线程分析工具,用于查找线程死锁、线程阻塞等问题。
插件支持: Java Mission Control是一个基于插件的可扩展应用程序,可以通过安装不同的插件来扩展其功能,满足不同场景下的监控和调试需求。
类文件和字节码
Java程序可以在不同的操作系统和硬件平台上运行,而不需要修改代码。这种特性主要得益于所有平台统一支持的程序存储格式——字节码Byte Code
,它是构成平台无关性的基石,任何其它语言的实现者都可以将Java虚拟机作为它们语言的运行基础,以Class文件作为它们产品的交付媒介。
类文件结构
Class 文件是Java编译器编译Java源代码生成的文件,是一组以8字节为基础单位的二进制流。它采用一种伪结构来存储数据,包括两种数据类型:
无符号数
:基本数据类型,以u1/u2/u4/u8分别代表1/2/4/8个字节。用来描述数字、索引引用、数量值、UTF-8编码的字符串值表
:多个无符号数或其它表 作为数据项构成的复合数据结构,以_info结尾用来描述有层次关系的复合数据结构
ClassFile {
u4 magic; //Class 文件的标志
u2 minor_version; //Class 的小版本号
u2 major_version; //Class 的大版本号
u2 constant_pool_count; //常量池的数量
cp_info constant_pool[constant_pool_count-1]; //常量池
u2 access_flags; //Class 的访问标记
u2 this_class; //当前类
u2 super_class; //父类
u2 interfaces_count; //接口
u2 interfaces[interfaces_count]; //一个类可以实现多个接口
u2 fields_count; //Class 文件的字段属性
field_info fields[fields_count]; //一个类可以有多个字段
u2 methods_count; //Class 文件的方法数量
method_info methods[methods_count]; //一个类可以有个多个方法
u2 attributes_count; //此类的属性表中的属性数
attribute_info attributes[attributes_count]; //属性表集合
}
魔数(Magic Number):
.class
文件的头4个字节是固定的魔数,用于标识文件是否为Java类文件。魔数的值为0xCAFEBABE
。版本号(Version): 魔数之后的4个字节表示JVM版本号,前2个字节表示主版本号,后2个字节表示次版本号。
版本号从45开始,例如JDK17的主版本就是45+17-1=61,高版本JDK仅向下兼容,JVM必须拒绝执行超过其版本号的Class文件,即使Class格式未发生变化。
常量池(Constant Pool): Class文件的资源仓库,用于存储类中使用的常量,包括字符串、类和接口的全限定名、字段和方法的名称和描述符、字面值常量等。常量池的索引从1开始,索引0不使用,用于表示无效的引用。
访问标志(Access Flags): 访问标志是一个 2Byte 的标志位,用于表示类或接口的访问权限和特性,比如
public
、private
、final
、abstract
等。类索引、父类索引和接口索引: 类索引表示当前类在常量池中的索引,父类索引表示当前类的直接父类在常量池中的索引,接口索引表示当前类实现的接口在常量池中的索引。
字段表(Field Table): 字段表用于描述类中声明的字段,包括字段的访问标志、字段的名称和描述符、字段的常量值等。
方法表(Method Table): 方法表用于描述类中声明的方法,包括方法的访问标志、方法的名称和描述符、方法的字节码等。
属性表(Attribute Table): 属性表用于存储与类、字段、方法相关的附加信息,如源文件名、代码行号、局部变量表、异常表等。属性表的结构由属性名索引和属性长度组成,后面跟着属性内容。具体包括:
ConstantValue(常量值): 用于描述字段的常量值,常用于静态字段的初始化。
Code(代码): 用于存储方法的字节码和异常处理信息,包括方法的指令、局部变量表、操作数栈、异常处理表等。
Exceptions(异常表): 用于描述方法可能抛出的异常类型,包括方法声明的受检异常和未受检异常。
InnerClasses(内部类): 用于描述类中声明的内部类和外部类之间的关系。
LineNumberTable(行号表): 用于存储Java源代码中的行号和字节码指令之间的对应关系,方便调试器在调试时定位源代码行号。
LocalVariableTable(局部变量表): 用于描述方法中局部变量的名称、索引、作用域和数据类型。
SourceFile(源文件): 用于描述Java源文件的文件名,方便反编译工具和调试器定位源文件。
Signature(签名): 用于描述类、字段、方法的泛型签名信息,包括泛型类型参数和泛型方法签名。
RuntimeVisibleAnnotations(可见注解): 用于存储类、字段、方法的可见注解信息,包括注解的类型和属性。
RuntimeVisibleParameterAnnotations(参数可见注解): 用于存储方法参数的可见注解信息。
BootstrapMethods(引导方法): 用于存储动态链接方法调用点的引导方法信息。
Deprecated(已弃用): 用于标记类、字段、方法已弃用,不推荐使用。
Synthetic(合成标记): 用于标记类、字段、方法是由编译器生成的合成成员。
字节码指令
字节码指令是由一个字节表示的操作码(Opcode),它指示了JVM执行何种操作。字节码指令可以分为多种类型,大多数指令都包含其操作对应的数据类型信息。常见的字节码指令包括:
栈操作指令: 这些指令用于操作操作数栈,包括将常量、局部变量、操作数等压入栈、从栈中弹出元素、对栈中的元素进行运算等。例如,
push
指令用于将常量或变量推入栈顶,pop
指令用于将栈顶元素弹出。算术和逻辑指令: 这些指令用于执行算术运算和逻辑运算,包括加减乘除、位运算、逻辑运算等。例如,
iadd
指令用于将栈顶两个整数相加,ior
指令用于执行按位或运算。类型转换指令: 这些指令用于进行数据类型的转换,包括将整数转换为浮点数、将浮点数转换为整数等。例如,
i2f
指令用于将整数转换为浮点数。控制转移指令: 这些指令用于控制程序的流程,包括跳转、条件分支、循环等。例如,
goto
指令用于无条件跳转到指定位置,if_icmpgt
指令用于比较栈顶两个整数并根据比较结果进行条件跳转。方法调用指令: 这些指令用于调用方法,包括静态方法调用、实例方法调用、接口方法调用等。例如,
invokestatic
调用静态方法,invokevirtual
调用实例方法,invokeinterface
调用接口方法,invokespecial
调用一些需要特殊处理的实例方法,invokedynamic
调用在运行时动态解析的方法。对象操作指令: 这些指令用于创建对象、访问对象的字段和数组元素、将对象引用压入栈等。例如,
new
指令用于创建新的对象,getfield
指令用于获取对象的字段值。异常处理指令: 这些指令用于异常处理,包括抛出异常、捕获异常、处理异常等。例如,
athrow
指令用于抛出异常,try-catch
块用于捕获和处理异常。
字节码执行引擎
Java虚拟机(JVM)字节码执行引擎是Java程序在虚拟机上执行的核心组件之一,它负责解释和执行Java字节码指令。字节码执行引擎通常由解释器和即时编译器(JIT Compiler)两部分组成,它们共同协作完成Java程序的执行。
解释器(Interpreter): 解释器是字节码执行引擎的核心组成部分,它负责逐条解释和执行Java字节码指令。解释器通过分析字节码指令,将其转换为底层操作系统的机器码,然后执行相应的操作。解释器的优点是简单、易于实现和移植,但由于每次执行都需要解释字节码指令,执行速度较慢。
即时编译器(Just-In-Time Compiler,JIT Compiler): 即时编译器是字节码执行引擎的另一部分,它负责将频繁执行的热点代码(Hot Spot)编译成本地机器码,以提高执行速度。JIT Compiler会根据程序的运行情况进行动态优化,对热点代码进行适当的优化,如方法内联、循环展开、去除冗余等。JIT Compiler的优点是执行速度快,但缺点是编译过程需要消耗额外的时间和内存。
JVM字节码执行引擎的工作流程如下:
- JVM加载Java类文件,并解析字节码文件。
- 解释器逐条解释和执行Java字节码指令。
- 解释器监控程序的运行情况,并标记热点代码。
- JIT Compiler对热点代码进行编译优化,生成本地机器码。
- 执行引擎执行本地机器码,提高程序的执行速度。
通过解释器和即时编译器的协作,JVM字节码执行引擎能够在不同的平台上执行Java程序,并根据程序的运行情况进行动态优化,从而实现高效的执行性能。
基于栈的指令集和基于寄存器的指令集 基于栈的指令集是一种将操作数存放在栈中的指令集架构。在这种架构中,操作数通常不直接存放在寄存器中,而是存放在一个栈数据结构中。指令通常包括将操作数压入栈、从栈中弹出操作数进行计算等。基于栈的指令集架构的优点是简单、易于实现和移植,但缺点是由于操作数需要频繁压栈和弹栈,执行效率较低。
基于寄存器的指令集是一种将操作数存放在寄存器中的指令集架构。在这种架构中,操作数通常直接存放在寄存器中,指令对寄存器中的操作数进行计算。基于寄存器的指令集架构的优点是执行效率高,因为操作数直接存放在寄存器中,无需频繁的内存访问。但缺点是寄存器的数量有限,可能会导致指令集的复杂性增加,以及寄存器的分配和管理成为挑战。
总的来说,基于栈的指令集架构更适合于虚拟机(如Java虚拟机)等环境,因为它简单且易于移植,而基于寄存器的指令集架构更适合于物理处理器等环境,因为它执行效率高。
方法调用
方法调用阶段唯一的任务是确定被调用方法的版本,Class文件的编译过程中不包含传统程序语言编译的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址。
解析和分派是Java虚拟机执行方法调用和字段访问的关键步骤。解析将符号引用解析为直接引用,确定方法或字段的具体引用;而分派根据调用的接收者确定要调用的具体方法实现。这两个过程共同保证了Java程序的多态性和灵活性。
解析
解析是指在运行时确定方法或字段的具体引用的过程。在Java中,解析通常发生在以下情况下:
- 调用实例方法时,需要确定调用的具体实现;
- 访问实例字段时,需要确定字段的具体引用;
- 访问静态方法或字段时,需要确定静态方法或字段的具体引用。
解析的过程包括根据方法或字段的名字和描述符在类的常量池中查找符号引用,并将其解析为直接引用。解析可以发生在类加载过程中(解析阶段),也可以发生在运行时(动态解析)。解析是Java虚拟机执行方法调用和字段访问的基础,它通过符号引用到直接引用的转换,将程序中的符号引用映射到内存中的具体对象或方法。
分派
分派是指根据方法调用的接收者确定要调用的具体方法实现的过程。在Java中,分派通常发生在以下情况下:
- 调用实例方法时,根据对象的实际类型确定调用的具体方法实现;
- 调用构造方法时,根据对象的实际类型确定要实例化的具体类;
- 调用接口方法时,根据接口的实现类确定调用的具体方法实现。
分派可以分为静态分派(早期绑定)和动态分派(晚期绑定):
- 静态分派:在编译时根据方法调用的静态类型确定要调用的具体方法实现。主要发生在方法重载和重写时。
- 动态分派:在运行时根据方法调用的实际类型确定要调用的具体方法实现。主要发生在方法重写时。
类加载机制
类加载机制指JVM把描述类的数据从Class文件加载到内存,并对数据继续校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。Java中,类型的加载、连接和初始化过程都是在程序运行期完成的。
类加载时机
在Java中,类加载的时机可以分为以下几种情况:
首次使用时加载: 类在首次被使用时,会触发其加载过程。首次使用的情况包括:
- 创建类的实例。
- 访问类的静态变量或静态方法。
- 使用类的静态方法。
- 使用反射创建类的实例。
类初始化时加载: 类在进行初始化时会被加载,包括以下情况:
- 调用类的静态方法。
- 设置或获取类的静态变量。
- 使用
Class.forName()
方法加载类。
虚拟机启动时加载: 虚拟机启动时,会加载主类(包含
main
方法的类),并调用其main
方法。从主类开始,虚拟机会逐步加载和初始化其他类。
类加载过程
类加载到卸载的七个阶段,其中验证、准备、解析三个部分统称连接。这些阶段通常都是互相交叉混合进行的,一个阶段的执行过程中调用、激活另一个阶段。
加载
加载阶段完成三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
获取类的二进制字节流不限于Class文件,还可以是压缩包如jar,网络如applet,动态生成,数据库等等。另外数组类本身不通过类加载器创建,它是由JVM直接在内存中动态构造出来的。
验证
验证阶段目的是确保Class文件的字节流包含的信息符合全部约束要求,保证代码运行不会JVM自身的安全。分四个阶段:
- 文件格式验证:保证输入的字节流能正确解析并存储于方法区之内
- 元数据验证:对字节码描述信息进行语义分析
- 字节码验证:通过数据流分析和控制流分析,确定程序语义合法,符合逻辑。(停机问题:不完全保证合法)
- 符号引用验证:对类自身以外的各类信息进行匹配性校验
准备
为类的静态变量分配内存,并设置默认初始值(零值),不包括对常量变量的赋值。
解析
将常量池内的符号引用替换为直接引用的过程,包括类、接口、字段和方法。符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要能无歧义的定位到目标。直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
初始化
初始化阶段是类加载过程的最后一步,它负责执行类构造器(<clinit>
方法),初始化类的静态变量和执行静态初始化块。<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作
和静态语句块
合并产生的,顺序由源文件中定义顺序决定。同一个类加载器下,一个类型只会被初始化一次。
JVM中第一个被执行<clinit>()
方法的类型一定是java.lang.Object
,接口(实现类)的<clinit>()
不需要先执行(父)接口的<clinit>()
。
类加载器
在Java中,类加载器(ClassLoader)是负责加载类文件(.class文件)并生成对应Class对象的重要组件,对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在JVM中的唯一性。Java中的类加载器主要有以下几种:
启动类加载器(Bootstrap ClassLoader): 启动类加载器是虚拟机的一部分,它负责加载Java的核心类库,如
java.lang.Object
、java.lang.String
等,以及其他的一些基础类库,通常是由虚拟机实现提供的。启动类加载器是虚拟机自身的一部分,通常用本地代码来实现,无法直接获取到。扩展类加载器(Extension ClassLoader): 扩展类加载器负责加载Java的扩展类库,一般位于
<JAVA_HOME>/lib/ext
目录下,或者由系统属性java.ext.dirs
指定的目录中。扩展类加载器是sun.misc.Launcher$ExtClassLoader
类的实例。JDK 9中已移除,取而代之的是PlatformClassLoader平台类加载器,负责加载非核心模块类。应用程序类加载器(Application ClassLoader): 应用程序类加载器,也称为系统类加载器,负责加载应用程序的类路径(classpath)下的类库,通常是项目中编写的Java类,或者第三方库。应用程序类加载器是
sun.misc.Launcher$AppClassLoader
类的实例。自定义类加载器(Custom ClassLoader): 自定义类加载器是用户自己实现的类加载器,可以根据需要定制类加载过程。自定义类加载器需要继承
java.lang.ClassLoader
类,并重写其findClass
方法来实现类的加载逻辑。自定义类加载器可以用于实现一些特殊的类加载需求,比如从网络中加载类、动态生成类等。
其中,Boostrap ClassLoader由C++实现,是JVM自身的一部分,其它类加载器都是由Java实现,独立于JVM,且全部继承自抽象类java.lang.ClassLoader
。
双亲委派模型
Java类加载器采用双亲委派模型(Parent Delegation Model),即先由父类加载器尝试加载类,只有在父类加载器无法加载时才由子类加载器尝试加载。这样可以确保类加载的顺序和可靠性,避免重复加载和类冲突。
前期编译
编译期可分为:
- 前期编译器 javac:*.java -> *.class
- 即时编译器 JIT:运行期字节码 -> 本地机器码
- 静态提前编译器 AOT:程序 -> 二进制代码
JVM对性能的全部优化集中在运行期的JIT中,支撑了程序执行效率的不断提升,并且让非javac产生的Class文件(如 JRuby、Groovy)也能享受编译器的优化措施;而前期编译器(主要是javac)的优化过程则是支撑了程序员的编码效率和语言使用者幸福感的提升。
javac本身是由Java语言编写的程序,编译过程大致分为准备过程和三个处理过程,其中准备过程会初始化插入式注解处理器。
解析与填充符号表
词法分析
将源码中的字符流转变为标记Token的过程。程序编写的最小元素是单个字符,编译时的最小元素是标记。
语法分析
根据标记序列构造抽象语法树的过程。AST是一种用来描述程序代码语法结构的树形表示形式,其中每个节点都代表着程序代码中的一个语法结构,如包、类型、修饰符、运算符、接口等。
填充符号表
符号表是由一组符号地址和符号信息构成的数据结构 (类似键值对的存储形式),符号表登记的信息在后续语义分析、目标代码生成阶段都要使用。
注解处理器
插入式注解器可以看作一组编译器的插件,插件工作时可以读取、修改、添加抽象语法树中的任意元素。处理注解期间,如果注解器对语法树进行过修改,编译器将重新解析、填充符号表,每次循环称为一个轮次Round。注解处理器的典型应用如Lombok工具。
语义分析与字节码生成
语义分析
AST能够表示一个结构正确的源程序,但无法保证语义符合逻辑,因此需要语义分析对结构上正确的源程序进行上下文相关性质的检查。分以下两类:
- 标注检查:检查包括变量使用前是否已被声明、变量与赋值之间的数据类型能否匹配等,另外还有
常量折叠
等少量代码优化 - 数据及控制流:检查诸如程序局部变量使用前是否赋值、方法的每条路径是否都有返回值、是否所有受查异常都被正确处理了等问题。(某些语义只能在编译期,而不能在运行期检查)
语法糖
Java的语法糖是指在Java编程语言中为了简化代码书写和提高可读性而添加的一些语法特性,这些特性在语言的语法上是可选的,但在编译器处理过程中会被转换成标准的Java语法。以下是Java中常见的语法糖:
自动装箱和拆箱(Autoboxing and Unboxing): 自动装箱和拆箱允许基本类型与对应的包装类型之间进行隐式转换。例如,可以直接将
int
类型的值赋给Integer
对象,编译器会自动插入装箱和拆箱的代码。增强的 for 循环(Enhanced For Loop): 增强的 for 循环简化了对数组和集合的遍历操作。例如,可以使用
for (T element : collection)
的语法来遍历集合中的元素,而不需要显式使用迭代器。静态导入(Static Import): 静态导入允许在使用静态成员时省略类名。例如,可以使用
import static java.util.Math.*
来导入Math
类的所有静态方法,然后直接调用静态方法而无需使用类名。可变参数(Varargs): 可变参数允许方法接受可变数量的参数。例如,可以使用
void foo(String... args)
的语法定义可变参数方法,然后在调用该方法时传递任意数量的参数。泛型类型推断(Diamond Operator): 泛型类型推断允许省略泛型类型的声明,编译器会根据上下文自动推断泛型类型。例如,可以使用
List<String> list = new ArrayList<>()
的语法来创建泛型集合,省略了ArrayList<String>
中的泛型类型。泛型通配符(Wildcard): 泛型通配符允许在泛型类型中使用通配符来表示未知类型。例如,可以使用
List<?>
的语法来声明一个未知类型的泛型集合。Lambda 表达式(Lambda Expressions): Lambda 表达式简化了匿名函数的定义和使用。例如,可以使用
(a, b) -> a + b
的语法来定义一个接受两个参数并返回它们之和的 Lambda 表达式。方法引用(Method Reference): 方法引用允许直接引用已有方法作为 Lambda 表达式的参数。例如,可以使用
System.out::println
的语法来引用System.out.println
方法。
字节码生成
Javac编译过程的最后一个阶段,把前面各个步骤生成的信息(语法树、符号表)转化为字节码指令写入到磁盘中,还进行了少量代码添加和转换工作。完成对语法树的遍历和调整之后,就会把填充了所有所需信息的符号表交由ClassWriter::writeClass()
方法输出字节码,生成最终的Class文件。至此编译过程宣告结束。
后期编译
编译器无论在何时、在何种状态下把Class文件转换成本地基础设施(硬件指令集、操作系统)相关的二进制机器码,都可以视为整个编译过程的后期。后期编译器性能的好坏、代码优化质量的高低是衡量一款商用虚拟机优秀与否的关键指标之一,也是商业JVM的核心,最能体现技术水平与价值。
即时编译器
Java最初都是通过解释器执行的,启动迅速,节约内存。但是当JVM发现某个方法或代码块运行频繁,就会把它们判定为热点代码,编译成本地机器码,并通过各种手段进行优化,提高热点代码执行效率。完成这一任务的后期编译器称即时编译器 JIT,主流商用JVM都同时包含解释器和编译器。
分层编译
HotSpot中包括客户端编译器C1,服务端编译器C2,以及Graal编译器。根据编译器编译、优化的规模与耗时,划分5个不同的编译层次:
- 第0层:纯解释执行,不开启性能监控模式
- 第1层:C1编译,进行简单可靠的稳定优化,不开启性能监控
- 第2层:C1编译,仅开启方法及回边次数统计等有限的性能监控
- 第3层:C1编译,开启全部性能监控,收集更多统计信息
- 第4层:C2编译,启用更多耗时更长的优化,并根据性能监控进行一些不可靠的激进优化
实施分层编译后,解释器、C1、C2同时工作,热点代码可能被多次编译,用C1获取更高的编译速度,用C2获取更好的编译质量。
编译对象与触发条件
JIT 编译的对象,即热点代码分两类:1)被多次调用的方法;2)被多次执行的循环体。编译的目标对象都是整个方法体,而非单独的循环体。对于循环体,编译时自动进行"栈上替换"。
热点探测的方式有两种:
- 基于采样: 周期性检查各个线程的调用栈顶,如果某些方法经常出现在栈顶,即是热点方法。采样简单高效,容易获取对象调用关系,但不够精确,容易受线程阻塞或其它外界因素的影响。
- 基于计数器: 为每个方法、代码块设置计数器,统计方法的执行次数,超过阈值即热点方法。计数器法精确严谨,但实现复杂,需要为每个方法建立并维护计数器,且不能直接获取方法调用关系。
HotSpot采用第二种计数器方式,为每个方法设置方法调用计数器和回边计数器。其中,方法调用计数器负责统计方法一段时间内执行的相对频率,超过时间限度会进行热度衰减,计数器减半,而这段时间称半衰周期。
而回边计数器负责统计方法中循环体代码执行的次数,统计的是绝对次数,没有热度衰减。当回边计数器溢出时,会同步设置方法调用计数器为溢出状态
编译过程
默认条件下,无论时方法调用产生的标准编译请求,还是栈上替换编译请求,JVM在编译器未完成编译前,都仍将解释执行代码,编译动作在后台编译线程中进行。
客户端编译器
简单快速的三段式编译器,主要是局部优化。
阶段1:一个平台独立的前端将字节码构造成一个高级中间代码表示HIR
阶段2:一个平台相关的后端从HIR中产生低级中间代码表示LIR
阶段3:在平台相关的后端使用线性扫描算法在LIR上分配寄存器,做窥孔优化,产生机器代码
服务端编译器
专门面向服务端的典型应用场景,针对性调整服务端的性能配置,能够容忍高优化复杂度,可以执行大部分经典的优化动作,同时根据解释器、C1提供的性能监控信息,进行一些不稳定的激进优化。
提前编译器
Java 提前编译器 (AOT compiler) 是一种将 Java 字节码 (*.class) 编译为本地机器码的工具,可以直接由 CPU 执行。与传统的解释器或即时编译器 (JIT) 相比,AOT 编译器能够在程序启动之前完成编译工作,从而提高程序的启动速度和运行效率。
优点
- 提高启动速度: AOT 编译器能够在程序启动之前完成编译工作,从而消除解释器和 JIT 编译器在程序启动时带来的性能开销。
- 提高运行效率: AOT 编译器可以进行更深入的代码优化,生成更优化的本地机器码,从而提高程序的运行效率。
- 减少内存占用: AOT 编译器可以将编译后的代码直接存储在可执行文件中,无需在运行时解释或编译字节码,从而减少内存占用。
缺点
- 增加编译时间: AOT 编译器需要在程序启动之前进行编译,因此会增加程序的编译时间。
- 降低灵活性: AOT 编译后的代码无法动态修改,因此降低了程序的灵活性。
- 增加代码大小: AOT 编译后的代码通常会比字节码更大,因此会增加程序的部署成本。
应用场景
AOT 编译器通常适用于以下场景:
- 对启动速度和运行效率要求较高的应用程序
- 需要在嵌入式系统或移动设备上运行的应用程序
- 需要静态部署的应用程序
编译优化技术
- 常量折叠: 将多个相同的常量合并为一个,以减少程序内存的使用。
- 死代码删除: 将无用的代码删除,以减少程序的执行时间和内存占用。
- 静态变量替换: 将静态变量替换为常量,以减少程序的执行时间。
- 循环展开: 将循环体展开,以提高程序的执行效率。
- 内联函数: 将小型函数直接展开在调用它的位置,以减少程序的调用开销。
- 指令重排序: 调整指令的执行顺序,以提高程序的并行性。
- 类型推断: 根据上下文推断变量的类型,以减少程序的代码量。
内存模型
多线程
线程实现
线程是比进程更轻量化的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源,又可以独立调度。
线程是Java进行处理器资源调度的最基本单位,Java提供了不同硬件和OS平台下对线程操作的统一处理。实现线程有三种方式:内核线程、用户线程、混合线程。目前,主流JVM的线程实现都是基于OS原生线程,即1:1的内核线程模型。
JDK层面的线程实现见:Java-Thread 源码阅读
内核线程
内核线程是由操作系统内核(kernel)创建和管理的线程。内核线程是操作系统的一部分,它可以直接访问操作系统的资源和内核数据结构,并且具有完整的权限和优先级。内核线程通常是由操作系统调度器进行调度,可以在任何时候被抢占或中断。
用户线程
用户线程是由用户空间程序创建和管理的线程,它们在用户空间中运行,不能直接访问内核资源。用户线程的创建、调度和销毁都由用户空间的线程库来管理,而不涉及操作系统内核。用户线程的调度和协作是由用户空间的调度器(如线程库或用户态调度器)来完成的。
混合线程
混合线程是指同时具有内核线程和用户线程特性的线程模型。在混合线程模型中,每个用户线程可以与一个或多个内核线程关联,内核线程负责执行用户线程的工作。用户线程的创建和调度仍然由用户空间的线程库来管理,但是线程库可以利用内核线程来提高并发性能。
锁优化
协程
Java的协程是一种轻量级的线程替代方案,用于实现异步编程和高效的并发处理。协程允许在单个线程内实现多个并发执行的任务,并且可以在任务之间进行切换,而无需使用操作系统线程的上下文切换开销。Java的协程通常使用协程库或框架来实现,其中比较流行的有Project Loom中的Fiber和Quasar框架。
以下是Java协程的一些特点和优势:
轻量级和高效: 协程是轻量级的执行单元,通常比线程更加轻量,因此可以在单个线程内创建大量的协程。与操作系统线程相比,协程的创建、销毁和切换开销更小,可以大大提高程序的性能和并发处理能力。
简化异步编程: 协程可以简化异步编程模型,使得代码更加清晰和易于理解。使用协程可以避免回调地狱(Callback Hell)和复杂的异步处理逻辑,使得代码更加易于维护和调试。
提高资源利用率: 由于协程可以在单个线程内并发执行多个任务,并且可以在任务之间进行切换,因此可以更加有效地利用系统资源。与每个任务都创建一个独立的线程相比,使用协程可以节省内存和线程调度开销。
简化线程同步: 协程通常是通过协作式调度(Cooperative Scheduling)来实现的,因此不需要显式的锁和同步机制。协程之间可以通过消息传递或者共享内存来进行通信,从而避免了线程间的竞争和死锁等问题。
提高可伸缩性: 由于协程可以在单个线程内并发执行大量任务,并且可以动态调整任务的数量和调度策略,因此可以更加灵活地适应不同的工作负载和系统需求,提高了系统的可伸缩性和弹性。