一看就懂的JVM类加载机制

本文深入探讨Java类加载机制,包括加载、验证、准备、解析和初始化五个阶段,以及类加载器的种类和双亲委派机制。解析类初始化顺序和自定义类加载器的方法。

类加载概述

类从被加载到虚拟机内存中开始,到卸载内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析3个部分称为连接,7个阶段执行顺序如下图所示:

在这里插入图片描述

类加载时机

什么情况下需要开始类加载过程的第一个阶段:加载呢? 虚拟机规范严格规定了有且只有5中情况必须立即对类进行“初始化”(而加载、验证、准备、自然需要在此之前):

1、遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行初始化,则需要先触发其初始化。这四条指令对应的java代码场景是:

	 - 通过new创建对象
	 - 读取、设置一个类的静态成员(不包括final修饰的静态变量)
	 - 调用一个类的静态成员函数

2、使用java.lang.reflect进行反射调用的时候,如果类没有初始化,那就需要初始化;

3、当初始化一个类的时候,若其父类尚未初始化,那就先要让其父类初始化,然后再初始化本类;

4、当虚拟机启动时,虚拟机会首先初始化带有main方法的类,即主类;

5、当使用JDK1.7及以上的版本中的动态语言支持时,若一个java.lang.invoke.MethodHandle实例最后的解析结果是:REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先出发它的初始化过程。

类加载过程

接下来我们详细讲解一下Java虚拟机中类加载的全过程,也就是加载、验证、准备、解析和初始化这5个阶段。

加载

在加载阶段,虚拟机需要完成以下3件事情:

1、通过一个类的全限定名来获取定义此类的二进制字节流。

2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

3、在内存这种生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各个数据的访问入口。

验证

加载阶段尚未完成,连接阶段可能已经开始。验证是连接阶段的第一步,验证阶段大致上回完成4个阶段的校验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。 验证阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

准备

准备阶段是正式为类变量分配内存并设置变量初始值的阶段。这个时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时岁对象一起分配在java堆中。这里所说的初始值“通常情况”下是数据类型的零值。

public static int value =123

变量value在准备阶段过后的初始值是0而不是123,因为在准备阶段 并未执行任何java方法,而把value赋值为123的指令是程序编译后,存放在类构造器方法中,所以把value赋值为123的动作将在初始化阶段才执行。
上面提到,在“通常情况下”初始值为零值,“特殊情况”指的是final修饰的值,在准备阶段虚拟机就会将value赋值为123。

解析

解析阶段是虚拟机将常量池中的符号引用替换为符号引用的过程。符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。直接引用可以有不同的实现方式:

  • 直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
  • 相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
  • 一个能间接定位到目标的句柄

初始化

初始化是为 类的静态变量 赋予 正确的初始值,到了初始化阶段,才真正开始执行类中定义的java程序代码(字节码),直接点说:初始化阶段是执行类构造器()方法的过程。

init 和 clinit 区别

1)执行时机不同

init 是对象构造器方法,程序执行 new 一个对象调用该对象类的 constructor 方法时才会执行 init 方法,clinit 是类构造器方法(类的加载过程), jvm 进行类的加载 —–> 验证 —-> 解析 —–> 初始化 ,其中,初始化阶段,jvm会调用 clinit 方法。

2)执行目的不同

init 是 instance 实例构造器,对非静态变量解析初始化;

clinit 是 class 类构造器对静态变量,静态代码块进行初始化。

类的初始化顺序

	普通类:

静态变量
静态代码块
普通变量
普通代码块
构造函数


继承的子类:

父类静态变量
父类静态代码块
子类静态变量
子类静态代码块
父类普通变量
父类普通代码块
父类构造函数
子类普通变量
子类普通代码块
子类构造函数


抽象的实现子类: 接口 - 抽象类 - 实现类

接口静态变量
抽象类静态变量
抽象类静态代码块
实现类静态变量
实现类静态代码块
抽象类普通变量
抽象类普通代码块
抽象类构造函数
实现类普通变量
实现类普通代码块
实现类构造函数

代码示例一:

 public class JavaTest {
    public static void main(String[] args) {
        f1();
    }
    static JavaTest javaTest = new JavaTest();
    static {
        System.out.println("1");
    }
    {
        System.out.println("2");
    }
    JavaTest() {
        System.out.println("3");
        System.out.println("a=" + a + ", b=" + b);
    }
    public static void f1() {
        System.out.println("4");
    }
    int a = 100;
    static int b = 200;   
}

输出结果为:

在这里插入图片描述

解析:

第一步: 在类加载过程的准备阶段,先对b进行系统的赋值,b = 0。

第二步: 在类加载过程的初始化阶段,执行方法,那么先执行类变量的初始化,即:static JavaTest javaTest = new JavaTest();

第三步: 在第二步的时候,正在执行,而且此时进行类 对象的初始化(new JavaTest() ),会去调用方法,

​ 因此会首先执行非静态代码块:System.out.println(“2”),

​ 然后执行非静态变量的初始化:a = 100 (此时的先后顺序依照代码编写的先后顺序),

​ 然后执行构造函数:System.out.println(“3”); System.out.println(“a=” + a + “, b=” + b); , 此时a的值为100,b的值还是0,因为 还只执行到 static JavaTest javaTest = new JavaTest();

第四步: 方法已经执行完了,那么就接下来执行 剩余的部分,

​ 先执行类的静态代码块:System.out.println(“1”),

​ 再执行类的静态变量初始化:static int b = 200 。(此时的先后顺序依照代码编写的先后顺序),

​ 此时,方法就执行完成了。

第五步: 和 方法 都已经执行完成了,类已经加载完成,此时就是函数的调用了,JavaTest 的
函数入口是 main() 方法,因此会调用静态方法f1():System.out.println(“4”);到此,整个程序就执行完成了。

代码示例二:

public class JavaTest {
    public static void main(String[] args) {
        f1();
    }
    int a = 100;
    static int b = 200;
    static JavaTest javaTest = new JavaTest();
    static {
        System.out.println("1");
    }
    {
        System.out.println("2");
    }
    JavaTest() {
        System.out.println("3");
        System.out.println("a=" + a + ", b=" + b);
    }
    public static void f1() {
        System.out.println("4");
    }
}

输出结果为:

在这里插入图片描述

该组代码与第一组的区别是:

static int b = 200;
static JavaTest javaTest = new JavaTest();		

这两行代码的相对位置。加载顺序的先后不同,导致结果也不同。

参考链接:JVM ,类的加载过程中,init 和 clinit 区别

类加载器

类加载器概述

类加载器虽然只用于实现类的加载动作,但是它在java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性。比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使两个类来源与同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么两个类就必定不相等。

类加载器种类

绝大部分java程序都会使用到以下3种系统提供的类加载器。

启动类加载器

这个类加载器负责将存放在<JAVA_HOME>\lib目录下的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中,如rt.jar。

扩展类加载器

这个类加载器由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录中的所有类库,开发者可以直接使用扩展类加载器。

应用程序类加载

这个加载器由sun.misc.Launcher$AppClassLoader实现,一般也称为系统类加载器。它负责加载用户类路径(classpath)上所指定的类库,如果应用程序没有自定义过自己的类加载器,一般情况下这个就是程序的默认的类加载器。

如果有必要还可以加入自己定义的类加载器。这些类加载器之间的关系一般如图所示。

在这里插入图片描述

类加载器的双亲委派机制

双亲委派模式的工作过程是:如果一个类加载器收到了类的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要的类)时,子加载器才会尝试自己去加载。

ClassLoader类

Class loadClass(String name) :name参数指定类装载器需要装载类的名字,必须使用全限定类名,如:com.smart.bean.Car。该方法有一个重载方法 loadClass(String name,boolean resolve),resolve参数告诉类装载器时候需要解析该类,在初始化之前,因考虑进行类解析的工作,但并不是所有的类都需要解析。如果JVM只需要知道该类是否存在或找出该类的超类,那么就不需要进行解析。

Class defineClass(String name,byte[] b,int len):将类文件的字节数组转换成JVM内部的java.lang.Class对象。字节数组可以从本地文件系统、远程网络获取。参数name为字节数组对应的全限定类名。

Class findSystemClass(String name):从本地文件系统在来Class文件。如果本地系统不存在该Class文件。则抛出ClassNotFoundException异常。该方法是JVM默认使用的装载机制

Class findLoadedClass(String name):调用该方法来查看ClassLoader是否已载入某个类。如果已载入,那么返回java.lang.Class对象;否则返回null。如果强行装载某个已存在的类,那么则抛出链接错误。

ClassLoader getParent():获取类装载器的父装载器。除根装载器外,所有的类装载器都有且仅有一个父装载器。ExtClassLoader的父装载器是根装载器,因为根装载器非java语言编写,所以无法获取,将返回null。

类的加载方式

隐式加载

隐式加载有几种情况:

  1. 首次通过 new 创建一个类的实例
  2. 首次调用类的静态成员
  3. 首次加载一个类,会先加载它的父类
  4. JVM 启动时,会自动加载定义了 main 方法的类

显式加载

显式加载有两种方式:

  1. 通过 ClassLoader.loadClass
  2. 通过 Class.forName

如何自定义一个类加载器?

在实现自己的ClassLoader之前,我们先看一下JDK中的ClassLoader是怎么实现的:

protected synchronized Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
    {
    // First, check if the class has already been loaded
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
        if (parent != null) {
            c = parent.loadClass(name, false);
        } else {
            c = findBootstrapClass0(name);
        }
        } catch (ClassNotFoundException e) {
            // If still not found, then invoke findClass in order
            // to find the class.
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
    }

方法原理很简单,一步一步解释一下:

1、第5行,首先查找.class是否被加载过

2、 第6行~第12行,如果.class文件没有被加载过,那么会去找加载器的父加载器。如果父加载器不是null(不是Bootstrap ClassLoader),那么就执行父加载器的loadClass方法,把类加载请求一直向上抛,直到父加载器为null(是Bootstrap ClassLoader)为止

3、第13行~第17行,父加载器开始尝试加载.class文件,加载成功就返回一个java.lang.Class,加载不成功就抛出一个ClassNotFoundException,给子加载器去加载

4、第19行~第21行,如果要解析这个.class文件的话,就解析一下,解析的作用主要就是将符号引用替换为直接引用的过程

我们看一下findClass这个方法:

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
    }

是的,没有具体实现,只抛了一个异常,而且是protected的,这充分证明了:这个方法就是给开发者重写用的。
从上面对于java.lang.ClassLoader的loadClass(String name, boolean resolve)方法的解析来看,可以得出以下2个结论:

1、如果不想打破双亲委派模型,那么只需要重写findClass方法即可

2、如果想打破双亲委派模型,那么就重写整个loadClass方法

当然,我们自定义的ClassLoader不想打破双亲委派模型,所以自定义的ClassLoader继承自java.lang.ClassLoader并且只重写findClass方法。

常见问题

1、为什么不能自定义一个java.lang.String的类?

答案是否定的。我们不能实现。为什么呢?我看很多网上解释是说双亲委托机制解决这个问题,其实不是非常的准确。因为双亲委托机制是可以打破的,你完全可以自己写一个classLoader来加载自己写的java.lang.String类,但是你会发现也不会加载成功,具体就是因为针对java.*开头的类,jvm的实现中已经保证了必须由bootstrp来加载。自定义的类加载器要么不能加载进内存,要么即使你用自定义的类加载器去强行加载,也会收到一个SecurityException。

<think>我们正在为JVM初学者解释“JVM动态加载static代码块”的含义。根据之前的对话历史,我们已经详细介绍了static代码块在类加载过程中的执行机制。现在需要以更简洁、初学者友好的方式重新解释,并参考提供的引用内容。 重点: 1. 引用[1]提到类加载的三个核心步骤:加载、链接、初始化。 2. 引用[3]提到类加载器使用loadClass()方法加载类,并涉及委托机制。 3. static代码块的执行发生在初始化阶段。 因此,我们可以这样组织回答: - 定义:static代码块是什么(静态初始化块)。 - 解释其执行时机:在类加载的初始化阶段执行,且只执行次。 - 强调动态加载:类在第次被主动使用时才会触发加载,从而执行static代码块。 - 结合引用[1]和[3]说明类加载过程。 注意:避免复杂术语,用简单例子说明。 回答结构: 1. 定义static代码块。 2. 解释JVM类加载过程(简化版)。 3. 说明static代码块在何时执行(初始化阶段)。 4. 举例说明动态加载(第次使用时才执行)。 5. 对比说明多次使用类不会重复执行static块。 6. 最后生成相关问题。 同时,我们之前回答中已经有些内容,可以复用: - static代码块在类加载的初始化阶段执行且仅执行次。 - 触发时机:首次主动使用(如new实例、访问静态成员等)。 注意:用户是初学者,所以避免深入细节(如<clinit>方法),但可以简单提及。 最终回答结构: ### 什么是JVM动态加载static代码块? (定义和核心特性) ### JVM类加载过程简述 (加载、链接、初始化,重点在初始化阶段执行static块) ### static代码块的执行时机 (动态加载:第次使用时触发) ### 示例说明 (简单代码示例展示执行顺序) ### 小结 §§相关问题§§ (生成3-5个相关问题) </think> ### 什么是JVM动态加载static代码块? #### 、核心概念 **static代码块**是Java中用`static{}`定义的代码块,用于初始化类的**静态成员**(如静态变量)。所谓"动态加载",指JVM在**类首次被使用时**才会执行这个代码块,而非程序启动时立即执行。这体现了JVM"按需加载"的特性[^1]。 #### 二、执行机制详解 通过类加载流程理解动态加载: ```mermaid flowchart TD A[类首次主动使用] --> B[加载Loading] B --> C[链接Linking] C --> D[初始化Initialization] D --> E[执行static代码块] ``` 1. **触发时机**(类首次主动使用): - 创建实例:`new MyClass()` - 访问静态成员:`MyClass.staticVar` - 调用静态方法:`MyClass.staticMethod()` - 反射调用:`Class.forName("MyClass")`[^3] 2. **关键阶段**: - **初始化阶段**:JVM执行`<clinit>()`方法(编译器自动生成),该方法包含: - 所有静态变量的显式赋值(如 `static int x = 5;`) - 所有`static{}`代码块中的语句 - **顺序规则**:按源码中的声明顺序执行[^1] 3. **动态性体现**: ```java public class Test { static { System.out.println("静态块执行!"); } public static void main(String[] args) { System.out.println("程序启动"); new Test(); // 此时才会触发static块执行 } } ``` **输出**: ``` 程序启动 静态块执行! // 注意:static块在new时执行,而非程序启动时 ``` #### 三、初学者必备示例 ```java class Database { static { System.out.println("加载数据库驱动..."); // 模拟初始化操作 } } public class Main { public static void main(String[] args) { System.out.println("1. 程序开始"); // 首次使用Database类 new Database(); // 触发static块执行 System.out.println("2. 再次创建实例"); new Database(); // 不再执行static块 } } ``` **输出**: ``` 1. 程序开始 加载数据库驱动... // 首次使用时动态加载 2. 再次创建实例 // 后续调用不执行 ``` #### 四、关键特性总结 | 特性 | 说明 | |---------------------|----------------------------------------------------------------------| | **次性** | 无论创建多少实例,static块只执行次 | | **线程安全** | JVM通过加锁保证多线程环境下只执行次[^1] | | **依赖类加载** | 执行前提是类已被加载和链接(由类加载器完成) | | **避免被动引用** | `MyClass[] arr = new MyClass[10]`不会触发static块 | > 动态加载的本质:JVM通过**延迟初始化**优化性能,避免不必要的资源消耗。static代码块作为类初始化的"启动开关",确保静态资源在使用前完成准备。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值