关于Java的类加载机制

1、概述

类会在运行期间第一次使用时,被类加载器动态加载至JVM。JVM不会一次性加载所有类。因为如果一次性加载,会占用很多的内存。

2、类的生命周期

类的生命周期包括以下 7 个阶段:

  • 加载(Loading)
  • 验证(Verification)
  • 准备(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 卸载(Unloading)

2.1类的加载过程

包含:加载、验证、准备、解析和初始化 ,一共包括 5****个阶段。

2.1.1加载

(1)加载过程

  1. 通过类的完全限定名称获取定义该类的二进制字节流
  2. 将该字节流表示的静态存储结构转换为Metaspace元空间区的运行时存储结构。
  3. 在内存中生成一个代表该类的 Class 对象,作为元空间区中该类各种数据的访问入口。

(2)二进制字节流获取方式

  • 从 ZIP 包读取,成为 JAR、EAR、WAR格式的基础。
  • 从网络中获取,最典型的应用是 Applet。
  • 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
  • 由其他文件或容器生成,例如由 JSP 文件生成对应的 Class 类。

2.1.2验证

确保 Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

2.1.3准备

使用的是元空间区的内存,为类变量 (被 static 修饰的变量)分配内存并设置初始值。

实例变量不会在这阶段分配内存,它会在对象实例化时,随着对象一起被分配在堆中。

注意:根据不用类型初始值不同,int初始值一般为 0 值。

例如:下面的类变量 value 被初始化为 0 而不是 123

java 复制代码
public static int value = 100;

如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0。

java 复制代码
public static final int value = 123;

2.1.4解析

将常量池的符号引用替换为直接引用的过程。其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。

2.1.5初始化

初始化阶段是虚拟机执行类构造器 <clinit>()方法的过程。准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。

<clinit>()是由编译器自动收集 类中所有类变量的赋值动作静态代码块中的语句合并产生的,编译器收集的顺序与源文件顺序一致。

所以,静态语句块只能访问定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问

java 复制代码
public class Test {
    static {
        i = 0;                // 给变量赋值可以正常编译通过
        System.out.print(i);  // 这句编译器会提示"非法向前引用"
    }
    static int i = 1;
}

父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类 。

在接口中有类变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法,但接口中不可以使用静态语句块。接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。

此外一个类的 <clinit>() 方法是线程安全的。多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。

3、类加载的时机

3.1主动引用

虚拟机规范中并没有强制约束何时进行加载,但是严格规定了下列六种情况必须对类进行加载:

3.1.1遇到特殊指令时

演示需要类

java 复制代码
class Prent{
    static int A = 23;
    static final int VALUE = 10;
    static {
        System.out.println("Prent被加载");
    }
    public static void dosth(){};
}
class Son extends Prent{
    static {
        System.out.println("Son被加载");
    }
}

(1)当 jvm 执行 new指令时会加载类。即:当程序创建一个类的实例对象

java 复制代码
public class Demo01 {
    public static void main(String[] args) {
        // new
        Prent p = new Prent();
}

(2)当 jvm 执行 getstatic指令时会加载类。即:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)

java 复制代码
// getstatic
System.out.println(Prent.A);

(3)当 jvm 执行 putstatic指令时会加载类。即:程序给类的静态变量赋值。

java 复制代码
        // putstatic
        Prent.A = 188;

(4) 当 jvm 执行 invokestatic指令时会加载类。即:程序调用类的静态方法。

java 复制代码
// invokestatic
        Prent.dosth();

3.1.2反射

使用 java.lang.reflect包的方法对类进行反射调用时如 Class.forname("..."), 或newInstance() 等等。如果类没初始化,需要触发类的加载。

3.1.3继承

加载一个类,如果其父类还未加载,则先触发该父类的加载。

3.1.4main方法所在类

当虚拟机启动时,用户需要定义一个要执行的主类 ,虚拟机先加载这个类。

3.1.5接口实现

当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了加载,则该接口要在实现类之前被加载

3.2被动引用

除主动引用之外,所有引用类的方式都不会触发加载,称为被动引用

(1)通过子类引用父类的静态字段,不会导致子类加载。

java 复制代码
System.out.println(Son.A);

(2)数组类型

通过数组定义来引用类,不会触发此类的加载。该过程会对数组类进行加载,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。

java 复制代码
//array继承自object与Prent无继承关系
Prent[] array = new Prent[10];

(3)类中常量

常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类

java 复制代码
System.out.println(Prent.VALUE);
相关推荐
何陈陈10 分钟前
【Linux】线程池
linux·服务器·开发语言·c++
清风玉骨15 分钟前
Qt-QHBoxLayout布局类控件(42)
开发语言·qt
2401_8572979127 分钟前
秋招内推2025-招联金融
java·前端·算法·金融·求职招聘
一 乐31 分钟前
考研论坛平台|考研论坛小程序系统|基于java和微信小程序的考研论坛平台小程序设计与实现(源码+数据库+文档)
java·数据库·学习·考研·微信·小程序·源码
一 乐32 分钟前
租拼车平台|小区租拼车管理|基于java的小区租拼车管理信息系统小程序设计与实现(源码+数据库+文档)
java·数据库·vue.js·微信·notepad++·拼车
夏旭泽34 分钟前
C-include
开发语言·c++
通信仿真实验室36 分钟前
MATLAB使用眼图分析QPSK通信系统接收端匹配滤波后的信号
开发语言·算法·matlab
通信仿真实验室41 分钟前
(15)衰落信道模型作用于信号是相乘还是卷积
开发语言·人工智能·算法·matlab
xmh-sxh-13141 小时前
如何选择数据库架构
java
jxxchallenger1 小时前
踩坑spring cloud gateway /actuator/gateway/refresh不生效
java·数据库·gateway