文章目录
HotSpot 虚拟机
HotSpot
是 Sun Microsystems(现为 Oracle Corporation 的一部分)开发的一款高性能的 Java 虚拟机(JVM)实现。它是当前最流行的 Java 虚拟机之一,广泛应用于各种 Java 应用程序中,从桌面应用到大型企业级服务端应用。HotSpot 的设计目的是通过动态编译和优化技术,提高 Java 应用程序的执行效率。
HotSpot 架构
HotSpot 的架构可以大致分为以下几个主要部分:
- 类加载器子系统(Class Loader Subsystem)
类加载器子系统负责从文件系统或其他来源加载字节码到 JVM 中。HotSpot 包括以下几种加载器:
- Bootstrap ClassLoader:加载 Java 核心类库,如
rt.jar
。 - Extension ClassLoader:加载位于
JAVA_HOME/jre/lib/ext
目录下的扩展类库。 - Application ClassLoader:加载应用程序类路径(classpath)中的类。
- 运行时数据区(Runtime Data Area)
运行时数据区包括:
- 方法区(Method Area):存储每个类的信息(如常量、静态变量、即时编译后的代码等)。
- 堆(Heap):所有线程共享的内存区域,用于存储对象实例和数组。
- 栈(Stack):每个线程都有自己的私有栈,用于存储局部变量、操作数栈、动态链接、方法出口信息等。
- 本地方法栈(Native Method Stack):与 Java 栈类似,但为 JVM 使用到的 Native 方法服务。
- 程序计数器(Program Counter Register):当前线程所执行的字节码的指示器。
- 执行引擎(Execution Engine)
执行引擎负责执行字节码指令。它可以解释执行字节码或通过即时编译器(JIT)将其转换为本地机器码。
HotSpot 包含两个 JIT 编译器:
- Client Compiler (C1):这是一个轻量级的编译器,用于快速生成简单的优化代码。
- Server Compiler (C2):这是一个重量级的编译器,用于在方法达到一定热度后再进行更为复杂的优化,生成高效的机器码。
- 垃圾回收器(Garbage Collector)
HotSpot 提供了多种垃圾回收器,用于管理和回收不再使用的对象所占用的内存。不同的垃圾回收器有不同的实现策略:
- Serial GC:适合单核 CPU 的小型应用。
- Parallel GC:适合多核 CPU 的应用,可以并行执行垃圾回收。
- Concurrent Mark Sweep (CMS) GC:适合对响应时间要求较高的应用,可以在应用程序运行的同时执行大部分垃圾回收工作。
- Garbage First (G1) GC:适合大堆应用,可以更好地平衡吞吐量和暂停时间。
- ZGC 和 Shenandoah:最新的垃圾回收器,专为低延迟设计,适用于对暂停时间敏感的应用。
HotSpot 虚拟机处理对象
HotSpot 虚拟机在 Java 堆中对象的分配、布局和访问是一个复杂的过程,涉及到多个方面的技术和优化策略。下面我们来详细的了解一下 HotSpot 在这三个方面的具体实现。
对象的创建
1. 类加载检查
虚拟机遇到一条 new
指令时,如:
Person person = new Person();
首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
类加载过程
类加载是Java虚拟机将字节码文件转换为可以在运行时使用的类的过程。
类加载过程由三个主要阶段组成:加载(Loading)、链接(Linking)和初始化(Initialization)。
-
加载(Loading):加载阶段是将类的字节码文件加载到内存中的过程。
它会通过
Class Loader
类加载器 定位并读取类的字节码文件,并创建该类在内存中的表示,即在方法区(Method Area)生成一个类的运行时数据结构。同时,也会为该类创建一个对应的Class对象,用于在运行时访问该类的信息。 -
链接(Linking):链接阶段将已加载的类与其他类和符号进行关联,以创建可执行的代码。链接过程包括三个子阶段:
-
验证(Verification):验证阶段确保被加载的字节码文件符合Java虚拟机规范,并进行格式、语义和安全性等方面的验证。
-
准备(Preparation):准备阶段为类的静态变量分配内存空间,并设置默认初始值。
-
解析(Resolution):解析阶段将符号引用转换为直接引用。符号引用是在编译阶段生成的,而直接引用是在解析阶段生成的。解析阶段主要处理类、字段和方法之间的引用关系。
-
-
初始化(Initialization):初始化阶段是为类的静态变量赋予初始值,以及执行静态代码块和静态变量的初始化赋值操作。
在初始化阶段,JVM会保证类在多线程环境下进行安全的初始化,即通过加锁机制来避免并发访问问题。
初始化阶段的触发条件包括:创建类的实例、访问类的静态变量或静态方法、以及使用反射方式调用类的方法等。
要注意的是,类的加载过程是按需进行的。只有当类被首次使用时,才会触发其加载、链接和初始化过程。同时,JVM还提供了 ClassLoader 机制,可以通过不同的类加载器从不同的位置加载类文件,实现更灵活的类加载方式。
总结起来,类加载过程是将类的字节码文件加载到内存中,并进行验证、准备、解析和初始化等处理的过程。将类加载到内存后,Java虚拟机可以通过相应的数据结构和对象来访问和执行类的代码。
2. 分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。
分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配的两种方式
-
指针碰撞:
-
适用场合:堆内存规整(即没有内存碎片)的情况下。
-
原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
-
使用该分配方式的 GC 收集器:
Serial
,ParNew
-
-
空闲列表:
-
适用场合:堆内存不规整的情况下。
-
原理:虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。
-
使用该分配方式的 GC 收集器:
CMS
-
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 JVM 垃圾回收机制中的垃圾收集算法 是"标记-清除",还是
“标记-整理”(也称作"标记-压缩")。
内存分配并发问题
在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
-
CAS + 失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
-
TLAB: 为每一个线程预先在
Eden
区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在TLAB
技术 分配,当对象大于TLAB
中的剩余内存或TLAB
的内存已用尽时,再采用上述的CAS
进行内存分配
3. 初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4. 设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如 这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄 等信息。这些信息都存放在对象头中。
根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
5. 执行 init 方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init>
方法还没有执行,所有的字段都还为零。
执行 new
指令之后会接着执行 <init>
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
对象的内存布局
在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:
对象头、实例数据、对齐填充。
对象头
Hotspot 虚拟机的对象头包括两部分信息:
-
用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等)
-
类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据
实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。
对齐补充
对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。
Hotspot 虚拟机的自动内存管理系统要求 对象起始地址必须是 8 字节的整数倍,即对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),
当对象实例数据部分没有对齐时,就需要通过对齐填充来补全
对象的访问定位
建立对象就是为了使用对象,Java 程序通过栈上的 reference
数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有:使用句柄、直接指针。
句柄
如果使用[[句柄 Handle|句柄]]的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。
使用句柄来访问的最大好处是 reference
中存储的是稳定的句柄地址,
在对象被移动时只会改变句柄中的实例数据指针,而 reference
本身不需要修改
图片来源:JavaGuide
直接指针
如果使用直接指针(对象引用 Object Reference)访问,reference 中存储的直接就是对象的地址
使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。HotSpot 虚拟机主要使用的就是这种方式来进行对象访问。
当需要访问对象的实例变量时,HotSpot 通过对象引用加上偏移量(Offset)来定位实例变量的内存位置,从而实现对实例变量的直接访问。这种方式既简单又高效,可以快速地访问对象的成员。
图片来源:JavaGuide
其他方式
HotSpot 还提供了一些优化技术来提高对象访问的性能,例如对象偏向锁、内联缓存等。
对象偏向锁允许单个线程在没有竞争的情况下对对象进行加锁和解锁操作,提高了同步操作的效率。内联缓存则用于优化虚方法调用,通过缓存最近一次调用的方法地址,减少查找的开销。
HotSpot 使用指针访问的方式与具体的硬件架构和操作系统有关。不同的硬件架构和操作系统可能会有不同的内存模型和访问方式。
HotSpot 会根据不同的硬件和操作系统的特点进行优化和适配,以提供高性能和可移植的 Java 应用程序运行环境。
示例代码
下面是一个简单的 Java 程序示例,展示如何使用 HotSpot 创建和访问对象:
public class Person {
private int id;
private String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public static void main(String[] args) {
Person person = new Person(1, "Alice");
System.out.println(person.getName());
}
}
在这个例子中,Person
类有两个字段:id
和 name
。当执行 new Person(1, "Alice")
时,HotSpot 将按照上述步骤创建一个新的 Person
对象,并将其分配给 person
引用。随后,getName()
方法被调用,输出 "Alice"
。
总结
HotSpot 虚拟机在 Java 堆中对象的分配、布局和访问是一个高度优化的过程。通过动态编译和内存管理技术,HotSpot 能够有效地管理对象的生命周期,提高 Java 应用程序的性能。理解和掌握 HotSpot 在对象管理方面的技术细节,对于开发高效稳定的 Java 应用程序具有重要意义。