IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 四、JVM类加载机制 -> 正文阅读

[Java知识库]四、JVM类加载机制

Java代码编写好之后,需要将其编译成Class文件,才能被JVM虚拟机执行。JVM将Class文件加载到内存,并对其进行校验、转换解析和初始化,最终形成可以直接被JVM使用的Java类型,就是JVM的类加载机制。

Java是一门动态语言,也就是在Java里面,类型的加载、连接和初始化过程是在程序的运行期间完成的。例如:如果编写一个面向接口的程序,可以等到在运行时再指定其实际的实现类。(C语言的链接过程是在编译期间完成的,C通过编译链接之后会直接生成一个可执行文件。)

1、类加载的时机

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

?JVM规范中没有强制约束什么时候开始一个类的加载阶段,但是对类的初始化阶段,严格规定了有且只有以下5种情况必须立即对类进行“初始化”,这5种行为称为对类的主动引用:

  1. new关键字实例化对象,读取或设置一个类的静态字段(被final修饰,已在编译器把结果放入常量池的除外),调用一个类的静态方法;
  2. 使用java.lang.reflect包的方法对类进行反射调用;
  3. 初始化一个类时,若其父类尚未初始化,需要先初始化其父类;
  4. JVM启动时,JVM会初始化包含main()函数的主类;
  5. 当使用JDK 1.7的动态语言支持时,若一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

除了以上的几种情况,其他引用类的方式不会触发类的初始化,称为被动引用,如下示例:

  • ?例1:通过子类引用父类的静态字段,不会导致子类初始化
class SuperClass {
    static {
        System.out.println("SuperClass init!");
    }

    public static int value = 123;
}

class SubClass extends SuperClass {
    static {
        System.out.println("SubClass init!");
    }
}

public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

代码运行结果如下,没有打印"SuperClass init!"

SuperClass init!
123
  • 例2:通过数组定义来引用类,不会触发此类的初始化:
class SuperClass {
    static {
        System.out.println("SuperClass init!");
    }

    public static int value = 123;
}


public class NotInitialization {
    public static void main(String[] args) {
        SuperClass[] s = new SuperClass[10];
        System.out.println("execute over!");
    }
}

代码运行结果如下,没有打印"SuperClass init!"

execute over!
  • 例3:常量在编译阶段会存储调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化:
class Constant {
    static {
        System.out.println("Constant init!");
    }

    public static final String str = "Hello World";
}

public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(Constant.str);
    }
}

代码运行结果,没有打印"Constant init!"。实际上,虽然源码中引用了Constant类的常量str,但是在编译阶段通过常量的传播优化,已经将常量的值“Hello World”存放到了NotInitialization类的常量池中,也就是编译之后说NotInitialization的Class文件中并没有对Constant类的符号引用了,这两个类再编译成Class文件之后就不存在任何联系了。

Hello World

接口的初始化与类的初始化有点区别:

当一个类初始化时,要求其父类全部已经初始化过了;但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用父接口中定义的常量)才会初始化。

2、类加载的过程

1、加载

在加载阶段,JVM需要做以下事情:

  • 根据全限定名获取类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

对于HotSpot JVM而言,java.lang.Class对象比较特殊,它时存放在方法区里面的。

2、验证

验证是连接阶段的第一步,目的是为了确保Class文件中的字节流包含的信息符合JVM规范。虽然在Java代码层面,语法错误将会导致编译器编译失败,无法将具有语法错误的代码编译为Class文件。但是Class文件的来源不仅仅只是java代码编译而来的,可以通过任何途径生成Class文件,甚至直接编写Class文件。即Class文件的来源是不可信的,对Class文件的校验是非常重要的。包括以下几方面的验证:

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

3、准备

准备阶段是正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。(此时没有实例化对象,不会为实例变量分配内存。)这里所说的初始值通常指的是对应数据类型的零值(0、null)。特殊情况下,如果类变量是final变量,则会直接初始化为属性指定的值。

4、解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

5、初始化

类的初始化阶段是类加载过程中的最后一步。前面的过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余的动作都是有JVM主导和控制的。而到了初始化阶段,就是真正开始执行类型定义的Java程序代码(或者说是字节码)了。

在准备阶段,类变量已经被赋值过一次初始值,而在初始化阶段,则会根据代码对类变量再次进行初始化,并为其赋予真正的值。

从另一个角度来讲,初始化阶段也是执行类构造器<clinit>()方法的过程。

  • <clinit>()方法是由编译器自动收集类中所有类变量(static变量)的赋值动作和静态语句块(static语句)中的语句合并产生的,收集的顺序与源代码顺序相同。静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。

  • ?<clinit>()方法与类的构造函数(实例构造器<init>()方法)不同,无需显示调用父类构造器,JVM保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此JVM中第一个被执行的<clinit>()方法就是Object类的。
  • 父类的<clinit>()方法优先于子类的<clinit>()方法执行,所以父类中定义的静态语句块要优先于子类的静态变量的赋值操作。

所以以下代码执行结果为:2

class SuperClass {
    public static int a = 1;
    static {
        a = 2;
    }
}

class SubClass extends SuperClass{
    public static int b = a;
}

class Test {
    public static void main(String[] args) {
        System.out.println(SubClass.b);
    }
}
  • ?如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
  • 接口中不能使用静态语句块,但是仍然有变量初始化的赋值操作,因此接口也会生成<clinit>()方法。但与类不同的是,执行接口的<clinit>()方法,不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量被使用时,才会初始化父接口。此外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
  • JVM会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法。
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-08-23 16:31:00  更:2021-08-23 16:34:20 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 10:15:20-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码