JVM-类初始化与类加载

对象创建的详细过程

在Java中,对象的创建是一个复杂且多步骤的过程,涉及类加载、内存分配、初始化等多个环节。以下是创建对象的详细步骤及其背后的原理:

  1. 类加载检查

当Java虚拟机(JVM)遇到一个new指令时,首先会在常量池中查找该对象的符号引用,并检查该类是否已经被加载、解析和初始化。如果该类尚未被加载,JVM将触发类加载过程。类加载过程包括加载、验证、准备、解析和初始化五个阶段,确保类在内存中正确地表示和初始化。

  1. 内存分配

类加载完成后,JVM将为新生对象分配内存。内存分配的大小在类加载阶段已经确定,这通常是通过类的元数据信息(如字段和方法的数量)计算得出。内存分配的方式取决于Java堆的内存模型,可能采用指针碰撞(Bump-the-Pointer)或空闲列表(Free List)等策略。分配内存的过程实际上是从Java堆中划分出一块固定大小的内存空间。

  1. 初始化零值

内存分配完成后,JVM需要对对象分配到的所有内存空间进行初始化零值操作。这一步骤确保对象的字段在未显式赋值的情况下,也能访问到默认的零值(如int为0,booleanfalse等)。初始化零值操作不包括对象头(Object Header),对象头中存储的是对象的元数据信息。

  1. 对象头设置

初始化零值完成后,JVM需要对对象进行必要的设置,这些设置信息存储在对象头中。对象头包含以下关键信息:

  • 类元数据指针:指向该对象所属类的元数据信息。
  • 哈希码:用于唯一标识对象的哈希码。
  • GC分代信息:记录对象的分代年龄,用于垃圾回收(GC)策略。
  • 锁状态:记录对象的锁状态,用于同步机制。

这些信息对于对象的运行时行为和垃圾回收机制至关重要。

  1. 执行<init>方法

完成上述步骤后,JVM已经创建了一个基本结构的对象,但尚未执行任何用户定义的初始化逻辑。接下来,JVM会调用对象的<init>方法(即构造方法),执行用户定义的初始化逻辑。<init>方法的执行包括:

  • 调用父类的构造方法:如果当前类是子类,JVM会先调用父类的构造方法。
  • 初始化字段:按照字段的声明顺序,初始化字段的值。
  • 执行构造方法体:执行构造方法中的用户代码。

<init>方法的执行确保对象的状态符合用户的预期,从而完成对象的创建过程。

对象的生命周期

对象的生命周期是指对象从创建到销毁的整个过程,这一过程通常包括三个主要阶段:创建、使用和销毁。理解对象的生命周期对于掌握编程语言的内存管理和资源分配机制至关重要。

1. 创建阶段

在创建阶段,对象通过内存分配和初始化过程被引入到程序中。具体步骤如下:

  • 内存分配:在Java中,对象的创建通常通过new关键字触发。new关键字会指示Java虚拟机(JVM)在堆内存中为对象分配必要的内存空间。

  • 实例化:内存分配完成后,JVM会调用对象的构造函数(Constructor)。构造函数负责执行对象的初始化操作,包括设置对象的初始状态、分配必要的资源等。这一过程确保对象在创建后处于一个有效且可用的状态。

2. 使用阶段

在对象的使用阶段,对象被程序中的其他部分引用,并执行相应的操作。具体包括:

  • 引用与操作:对象通过引用变量被程序的其他部分访问。程序可以通过调用对象的方法(Methods)和访问对象的属性(Fields)来执行各种操作。这些操作可能涉及对象状态的修改、数据的处理、与其他对象的交互等。

  • 生命周期管理:在对象的使用过程中,程序需要确保对象的生命周期管理得当。例如,避免出现内存泄漏(Memory Leak),即对象不再被使用但仍占用内存空间。合理的生命周期管理有助于提高程序的性能和稳定性。

3. 销毁阶段

当对象不再被引用时,JVM会通过垃圾回收机制(Garbage Collection, GC)自动回收对象所占用的内存空间。销毁阶段的具体过程如下:

  • 垃圾回收:垃圾回收器是JVM的一部分,负责自动管理内存。它会定期检测堆内存中不再被引用的对象,并将其标记为可回收状态。垃圾回收器使用多种算法(如标记-清除、复制、标记-整理等)来确定哪些对象可以被回收。

  • 内存释放:一旦对象被标记为可回收,垃圾回收器会在适当的时候执行回收操作,释放对象所占用的内存空间。这一过程是自动进行的,程序员无需显式调用销毁方法。

  • 资源回收:除了内存回收,对象的销毁还可能涉及其他资源的释放,如文件句柄、数据库连接等。在某些情况下,程序员需要通过显式调用close()dispose()等方法来确保资源的正确释放。

类加载器概述

类加载器

在Java虚拟机(JVM)中,类加载器(Class Loader)是负责将类的字节码加载到内存中的组件。类加载器按照层次结构组织,形成了所谓的“双亲委派模型”(Parent Delegation Model)。以下是Java中常见的类加载器及其功能:

1. 启动类加载器(Bootstrap Class Loader)

  • 职责:启动类加载器是JVM中最顶层的类加载器,负责加载Java的核心库,如rt.jar中的类。这些类包括java.lang.*java.util.*等。

  • 实现:启动类加载器通常由JVM的一部分通过C++语言实现,因此无法直接在Java程序中引用。

  • 特点:由于其特殊性,启动类加载器没有父加载器,它是所有其他类加载器的祖先。

2. 扩展类加载器(Extension Class Loader)

  • 职责:扩展类加载器负责加载Java扩展目录(如$JAVA_HOME/lib/ext)中的JAR包和类库。这些扩展类库提供了额外的功能,但不是Java核心库的一部分。

  • 实现:扩展类加载器是Java语言实现的,继承自java.lang.ClassLoader。它的父加载器是启动类加载器。

  • 特点:扩展类加载器允许Java平台在不修改核心库的情况下扩展功能。

3. 系统类加载器/应用程序类加载器(System Class Loader/Application Class Loader)

  • 职责:系统类加载器负责加载用户路径(如CLASSPATH环境变量指定的路径)上的类库。通常,我们编写的应用程序默认使用的就是该类加载器。

  • 实现:系统类加载器同样是Java语言实现的,继承自java.lang.ClassLoader。它的父加载器是扩展类加载器。

  • 特点:系统类加载器是大多数Java应用程序的默认类加载器,负责加载应用程序的类和依赖库。

4. 自定义类加载器(Custom Class Loader)

  • 职责:开发者可以根据需求定制类加载器,以实现特定的加载策略。例如,从网络、数据库或其他非标准位置加载类。

  • 实现:自定义类加载器通常继承自java.lang.ClassLoader,并重写findClass()loadClass()方法。

  • 特点:自定义类加载器提供了灵活性,允许开发者根据具体需求实现个性化的类加载逻辑。

双亲委派模型

双亲委派模型(Parent Delegation Model)

双亲委派模型是Java类加载器体系的核心机制,它定义了类加载器之间的层次关系和加载顺序。其核心思想如下:

  • 委派机制:当一个类加载器收到类加载请求时,它不会立即尝试加载类,而是将请求委派给父类加载器。每一层次的类加载器都会遵循这一原则,直到请求到达顶层的启动类加载器。

  • 加载顺序:如果父类加载器无法加载该类(即在父类加载器的搜索路径中找不到该类),子类加载器才会尝试加载。这种机制确保了类的加载顺序和层次结构,避免了类的重复加载和潜在的冲突。

  • 安全性:双亲委派模型增强了Java平台的安全性,因为它确保了核心库的类只能由启动类加载器加载,防止了恶意代码替换核心类库的可能性。

双亲委派模型的作用

1. 保证类的唯一性

通过委派机制,双亲委派模型确保了所有类加载请求都会传给启动类加载器。这一机制避免了不同类加载器重复加载相同类的情况,确保了Java核心类库的统一性。同时,它也防止了用户自定义的类覆盖Java核心类库的可能性,从而保证了类的唯一性。

2. 保证安全性

由于Java核心类库被启动类加载器加载,而启动类加载器只加载信任的核心类库,双亲委派模型可以防止恶意代码类冒充核心类,增加系统的安全性。这种机制确保了核心类库的完整性和安全性,防止了潜在的安全漏洞。

3. 支持隔离和层次划分

双亲委派模型支持不同层次的类加载器服务于不同的类加载需求。例如:

  • 启动类加载器:加载Java核心库类代码。
  • 扩展类加载器:加载框架的扩展代码。
  • 应用程序类加载器:加载用户的代码。

这种层次划分可以实现沙盒安全机制,保证各个层级加载器的职责分明,也便于维护和扩展。每个层次的类加载器只负责加载特定范围内的类,从而实现了类加载的隔离和层次划分。

4. 便于维护和扩展

双亲委派模型的层次结构使得类加载器的职责清晰,便于维护和扩展。开发者可以根据需求定制自定义类加载器,并将其插入到现有的类加载器层次结构中。这种灵活性使得Java平台能够适应不同的应用场景和需求,增强了系统的可扩展性。

类加载过程

类加载过程

类从被虚拟机加载到内存开始,到卸载出内存为止,它的整个生命周期包括以下七个阶段:

  1. 加载(Loading)

加载阶段是类加载过程的第一个阶段,主要完成以下任务:

  • 获取二进制字节流:通过类的全限定名(包名+类名),获取该类的.class文件的二进制字节流。这个过程可以通过文件系统、网络、动态生成等方式实现。

  • 转换为运行时数据结构:将二进制字节流所代表的静态存储结构,转化为方法区(Method Area)运行时的数据结构。方法区是JVM中的一块内存区域,用于存储类的结构信息。

  • 生成Class对象:在内存中生成一个代表该类的java.lang.Class对象,作为方法区该类的各种数据的访问入口。这个Class对象是反射机制的基础,通过它可以获取类的所有信息。

  1. 连接(Linking)

连接阶段包括验证、准备和解析三个子阶段,统称为连接。

​ 2.1 验证(Verification)

验证阶段的目的是确保.class文件中的字节流包含的信息符合当前虚拟机的要求,保证这个被加载的类的正确性,不会危害到虚拟机的安全。验证阶段大致包括以下四个阶段的检验动作:

  • 文件格式校验:检查字节流是否符合Class文件格式的规范,如魔数、版本号、常量池等。

  • 元数据验证:对类的元数据信息进行语义分析,确保其符合Java语言规范,如类是否有父类、是否继承了不允许继承的类等。

  • 字节码验证:通过数据流和控制流分析,确保方法体的字节码符合逻辑,如操作数栈的数据类型与指令代码序列是否匹配、跳转指令是否指向合理的位置等。

  • 符号引用验证:在解析阶段之前,确保符号引用能够正确解析为直接引用,如检查符号引用中的类、字段、方法是否存在且具有正确的访问权限。

​ 2.2 准备(Preparation)

准备阶段为类中的静态字段分配内存,并设置初始值。具体包括:

  • 分配内存:为类的静态字段分配内存空间。

  • 设置初始值:为静态字段设置默认初始值,如int类型初始值为0boolean类型初始值为false。被final修饰的static字段不会在此阶段设置,因为final在编译时就已经分配好了。

​ 2.3 解析(Resolution)

解析阶段是虚拟机将常量池中的“符号引用”替换为“直接引用”的过程。具体包括:

  • 符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时可以无歧义地定位到目标即可。

  • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或者是一个可以间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的。如果有了直接引用,那引用的目标必定是已存在于内存中的。

  1. 初始化(Initialization)

初始化阶段是类加载的最后一个阶段,主要任务是执行类的构造器方法(<clinit>方法)。具体包括:

  • 执行<clinit>方法<clinit>方法是编译器自动生成的,用于收集类中的静态字段赋值语句和静态代码块。初始化阶段会按照代码顺序执行这些赋值语句和静态代码块。

  • 线程安全<clinit>方法在多线程环境中是线程安全的,虚拟机会保证在多线程环境下只有一个线程执行<clinit>方法,其他线程会被阻塞。

  1. 使用(Using)

使用阶段是指类或对象在程序中被实际使用的过程,包括创建对象实例、调用类的方法、访问类的字段等。

  1. 卸载(Unloading)

卸载阶段是指类从内存中被移除的过程。类只有在以下情况下才会被卸载:

  1. 所有对象实例被回收:该类的所有对象实例都已被垃圾回收器回收。

  2. 类加载器被回收:加载该类的类加载器已被回收。

  3. Class对象无引用:类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。