JVM——深入理解类加载器

本文详细解析Java类加载过程,包括加载、连接、初始化等关键步骤,探讨主动使用与被动使用区别,介绍类加载器的类型及其工作原理,特别强调双亲委托机制的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

类加载器相关内容有很多,大概分以下几个关键点进行学习,

参考链接 https://www.bilibili.com/video/av47756459

目录

概览

JVM与程序的生命周期

类加载器生命周期

加载

连接

初始化

主动使用与被动使用

常量

数组成员变量

反编译

助记符

类加载器准备阶段与初始化阶段

接口的初始化

类实例化

类加载器

JVM自带的类加载器

自定义的类加载器

双(父)亲委托机制

命名空间

类的卸载

 


概览

类型(类,接口,枚举加载(从硬盘加载到内存),连接(字节码的校验(字节码可以被人为修改)、类之间关系的确定),初始化(静态变量的赋值)均是在程序运行期间完成的

JVM与程序的生命周期

以下几种情况JVM结束生命周期:

(1)执行System.exit()方法

(2)程序正常执行结束

(3)程序运行过程中遇到错误或异常而异常终止

(4)由于操作系统错误引起JVM进程终止

类加载器生命周期

加载

将类的.class 文件中的二进制数据读取到内存中,将其放入运行时数据区的方法区(1.8后有所改动)内,在内存中创建一个java.lang.Class 对象(规范并未明确说明Class对象放置区域,hotspot将其放置在方法区,不同于其他对象放置在中),用来封装类在方法区内的数据结构。

  • 类的加载的最终产品是位于内存中的class对象
  • class对象封装了类在方法区中的数据结构,并向java程序提供了访问方法区内数据结构的接口

加载.class 文件的方式:

— 从本地系统加载

— 通过网络下载

— 从zip等归档文件中加载

— 从专有数据库中加载

从java源文件动态编译为.class 文件(动态代理)

连接

(1)验证:确保加载类的字节码的正确性(2)准备:为类的静态变量分配内存,并将其初始化为默认值(3)解析:把类中的符号引用(间接引用)变为直接引用(使用指针指向内存位置)

初始化

为类的静态变量赋予正确的初始值(将静态变量的声明语句和静态代码块看做类的初始化语句

使用

卸载:从内存中销毁(OSGI)

 

主动使用与被动使用

在类加载,连接,初始化过程中,java 程序对的使用分为两种:

(1)自动使用(7种)

— 创建类的实例(不是声明)

— 访问某个类或接口的静态变量或给静态变量赋值(子类调用父类的静态变量不会引起子类的主动使用)

— 调用类的静态方法(子类调用父类的静态方法不会引起子类的主动使用)

— 反射

— 初始化类的子类

— JVM启动时被标为启动类(包含main方法)的类(Java Test)

— 从JDK1.7开始的动态语言支持

(2)被动使用

除了主动使用中的7种方法,其余使用java类的方法都称为被动使用,均不会导致类的初始化(不影响加载,连接)

 

所有java虚拟机实现必须在每个类或接口被java程序“首次主动使用”才初始化。

 

package test;

public class MyTest1 {
    public static void main(String[] args) {
        System.out.println(Child.string);
    }
}

class Parent {
    static String string = "hello";

    static {
        System.out.println("Parent static block");
    }
}

class Child extends Parent {
    static {
        System.out.println("Child static block");
    }
}

 

由于没有主动使用Child类,Child类没有初始化,但是进行了加载!

-XX:+TraceClassLoading 用于追踪类的加载信息并打印,添加至idea配置信息的VM options,即JVM参数(关于VM options,参考:https://blog.youkuaiyun.com/upgroup/article/details/81052047)。

常量

对于final属性的成员变量(常量),编译期常量存入调用此常量的方法所在类的常量池中。本质上,调用类并没有直接引用到定义常量的类。因此,并不会触发定义常量的类的初始化。即使删除定义常量的类的class文件也不影响使用,编译过后就完全不需要定义常量的类了。

若是 final 属性的非编译期常量(如 随机数)即 运行期常量,则不同于上述情况。

数组成员变量

建立对象数组,不会主动使用对象类。对于数组实例来说,类型是JVM运行时动态生成的,表示为[***,动态生成的类型其父类型就是Object。

反编译

将已编译的编程语言还原到未编译的状态。java中,就是将.class文件转换成.java文件。使用命令 javap 。(编译使用javac

助记符

ldc, 表示将int,float 或 string 常量值从常量池推送至栈顶

bipush表示将单字节常量(-128 ~ 127)推送至栈顶。

sipush 表示短整型常量推送至栈顶。

iconst_1 (m1 ~ 5)表示将int型常量(m1 ~ 5)推送至栈顶。

anewarray 创建一个引用类型的数组,并将其引用值压入栈顶。

newarray 创建一个指定原始类型的数组,并将其引用值压入栈顶。

类加载器准备阶段与初始化阶段

public class TestClass2 {

    public static void main(String[] args) {
        Test test = Test.getTest();
        System.out.println(test.a);
        System.out.println(test.b);
    }
}

class Test {
    static int a;
    static Test test = new Test();

    Test() {
        a++;
        b++;
        System.out.println(a);
        System.out.println(b);
    }

    static int b = 0;

    static Test getTest() {
        return test;
    }
}

此例中,类加载器先在准备阶段顺序执行静态代码块,将成员变量赋默认值(a,b为0,test为null),然后再执行类的初始化阶段,a不改变;test执行new Test() ,将a,b值改为1;b初始化值改为0。

简单理解,第一次执行等号左边,第二次执行等号右边。

 

接口的初始化

由于接口的成员默认都是final的常量,会放置调用此常量的方法所在类的常量池中

不同于类,初始化接口不会引起父接口初始化 (不影响加载)。在初始化一个类时,也不会初始化它实现的接口。但,仍然需要加载。只有当程序首次使用特定接口的静态变量时才会初始化该接口。


public class TestInterface {
    public static void main(String[] args) {
        System.out.println(ChildInterface.b);
    }
}

interface ParentInterface {
    int a = 1 / 0;
}

interface ChildInterface extends ParentInterface {
    int b = new Random().nextInt(10) + 1;
}

 

这段代码不会报错,即,不会引起ParentInterface 的初始化。若是将接口改为类则会报错。

(能在编译时初始化的,就不延迟到运行时解决)

 

类实例化

  • 为新的对象分配内存
  • 为实例变量赋默认值
  • 为实例变量赋正确的初始值
  • java编译器为它编译的每一个类都至少生成一个实例初始化方法,在class文件中,这个初始化方法称为<init>方法,针对源代码中的每一个构造方法,java编译器都生成一个<init>方法。

 

类加载器

JVM自带的类加载器

  • 跟类加载器(Bootstrap) (听着这么熟悉?一个前端开发框架)
  • 扩展类加载器(Extension)
  • 系统(应用)类加载器(System)

自定义的类加载器

  • java.lang.ClassLoader 的子类
  • 定制类加载方式

类加载器并需要该类“首次使用”才加载该类。JVM规范允许类加载器在预料某个类将被使用时,预先加载。若加载出现错误或缺失class文件,将在该类“首次主动使用”时才报告错误。若一直未使用,则不会报错。

 

 

 

双(父)亲委托机制

在双亲委托机制中,各个类加载器按照父子关系形成树形结构。除了启动类加载器(根类加载器)之外,所有类加载器有且只有一个父加载器。

每一个类加载器通过特定的目录进行加载。

首先不自己加载,而是通过自己的父亲加载,父亲又通过父亲的父亲进行加载直到启动类加载器。再自顶向下进行加载,看看到底谁能够加载。

由父加载器加载的类不能访问由子加载器所加载的类。

 

命名空间

每个类加载器都有自己的命名空间,命名空间由该类加载器及所有父加载器所加载的类组成。

不同的命名空间中类的完整名字可以相同,但是,相同命名空间中不能出现两个相同的完整类名。

因此当实例化多个自己定义的类加载器时,不同的类加载器都会进行加载类。

当在一个类中创建引用对象时,对象所对应的类由加载包含该对象的类的加载器进行加载(同时遵循双亲委托机制)

子加载器所加载的类能够访问父加载器所加载的类。但是,由父加载器加载的类不能访问由子加载器所加载的类。

 

类的卸载

当MySample类被加载、连接、初始化后,其生命周期就开始了。当代表类Class对象不再被引用,不可触及时,Class对象就会终结其生命周期,MySample类在方法区内的数据就会被卸载,从而结束其生命周期。

一个何时结束其生命周期,取决于代表它的Class对象何时结束其生命周期。

由Java虚拟机自带的类加载器(根类、扩展、系统类加载器)所加载的类,在虚拟机的生命周期中始终不会被卸载。JVM会始终使用这些类加载器,这些类加载器会始终使用它们所加载的类的Class对象,这些Class对象始终是可以触及的。

由用户自定义的类加载器所加载的可以被卸载。

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值