玩转类加载和类加载器

一、类加载

类的生命周期

下图中这7个阶段必须记住,面试会问
在这里插入图片描述

阶段顺序

加载、验证、准备、初始化、卸载这5个阶段的顺序是固定的,解析阶段有时会在初始化之后执行

加载

类加载的时机在虚拟机规范中并没有明确的规定,但是加载需要做的事情如下:

  • 通过类的全限定路径名获取这个类的二进制字节流
  • 将二进制字节流中静态存储结果转化为方法区运行时数据结构
  • 在内存中生成代表这个类的.class对象,作为这个类的各种数据结构的访问入口
验证

验证是为了保护虚拟机自身的安全,大致会完成下面4种检验动作:

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用
    记住上面4种验证的名字即可,仅作了解
准备

准备阶段主要是为静态变量赋初始化的值

package ex7;

public class Preparing {
    static int A;
    public static void main(String[] args) {
        System.out.println(A);
    }
}

上面代码运行后可以看到A为0,如果static修饰的是boolean,则为false

解析

将符号引用替换为直接引用的过程

初始化

这里是类加载的重点
初始化是对一个class中static{}语句进行操作(对应的字节码是clinit)
clinit方法对于一个类并不是必须的,如果一个类中没有static语句块,也没有对static变量的赋值操作,那么编译器就不会生成clinint方法
初始化阶段,虚拟机规范则是严格规定了有且只有 6 种情况必须立即对类进行“初始化”

  • 1.遇到newgetstaticputstaticinvokestatic 这 4 条字节码指令时必须初始化,生成这4个字节码的java场景如下:
    • new 关键字实例化对象的时候
    • 读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候
    • 调用一个类静态方法的时候
  • 2.使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
  • 3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  • 4.当一个接口中定义了 JDK1.8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果这个接口的实现类发生了初始化,那该接口要在其之前 被初始化

初始化代码案例:

package ex7.init;

/**
 * 父类
 */
public class SuperClazz {
	static 	{
		System.out.println("SuperClass init!");
	}
	public static int value=123;
	public static final String HELLOWORLD="hello king";
	public static final int WHAT = value;
}
package ex7.init;

/**
 * 子类
 */
public class SubClaszz extends SuperClazz {
	static{
		System.out.println("SubClass init!");
	}
}
package ex7.init;
/**
 *初始化的各种场景
 * 通过VM参数可以观察操作是否会导致子类的加载 -XX:+TraceClassLoading
 **/
public class Initialization {
	public static void main(String[]args){
		Initialization initialization = new Initialization();
		initialization.M1();//打印子类的静态字段
		initialization.M2();//使用数组的方式创建
		initialization.M3();//打印一个常量
		initialization.M4();//如果使用常量去引用另外一个常量
	}
	public void M1(){
		//触发了上面第3点
		//如果通过子类引用父类中的静态字段,只会触发父类的初始化,而不会触发子类的初始化(但是子类会被加载)
		System.out.println(SubClaszz.value);
	}
	public void M2(){
		//使用数组的方式, 不会触发初始化(触发父类加载,不会触发子类加载)
		SuperClazz[]sca = new SuperClazz[10];
	}
	public void M3(){
		//打印一个常量,不会触发初始化(同样不会触类加载、编译的时候这个常量已经进入了自己class的常量池)
		System.out.println(SuperClazz.HELLOWORLD);
	}
	public void M4(){
		//如果使用常量去引用另外一个常量,这个值在编译的时候无法确定,所以必须要触发初始化
		System.out.println(SuperClazz.WHAT);
	}
}

总结:getstatic的场景是M1和M3方法,面试的时候会被问到,当你的静态变量被赋值一个常量的时候,会触发静态变量所在类的出池化,当你静态变量被赋值一个引用的时候,必须触发引用所在类的初始化

线程安全:
虚拟机会保证一个类的clinit方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类 的clinit方法,其他线程都需要阻塞等待,直到活动线程执行clinit方法完毕。如果在一个类的clinit方法中有耗时很长的操作,就 可能造成多个进程阻塞。所以类的初始化是线程安全的,项目中可以利用这点

使用

一般就是new出对象使用

卸载

不用就会卸载,不是重点,不用管

二、类加载器

类加载器做的就是上面 5 个步骤的事(加载、验证、准备、解析、初始化)

JDK 提供的三层类加载器
启动类加载器:bootstarp ClassLoader

它是最核心的加载器,作用是加载核心类库,也就是jar包,它是有C++编写的,随着JVM启动

拓展类加载器 Extension Class Loader

主要用于加载 lib/ext 目录下的 jar 包和 .class 文件,它是JAVA类,继承自 URLClassLoader

应用程序类加载器 Application Class Loader

这是我们写的Java类的默认加载器,一般会加载classpath下所有的jar包和.class文件,我们写的代码,首先会尝试使用这个类加载

自定义程序加载器 Custom ClassLoader

支持一些扩展功能

双亲委派模型

这里是重点,面试常问!!!
在这里插入图片描述
在上面的类加载器中,除了最顶层的bootstrap类加载器外,其余的类加载器都有父子关系,Bootstrap -> Exception -> Application -> Custom,现在你自己写了一个String类,跟JDK提供的String类的名字、方法等全部都一样,你想用Application类加载器加载,此时,Application类加载会向Exception询问String是否加载过,Exception会向Bootstrap询问String是否加载过,Bootstrap说String我已经加载过,那么Application类加载器就不需要再加载String类,如果你写的类的名字为StringTest,那么BootStrap会说我没有加载过这个类,此时,Application类加载器会自己加载

上面就是双亲委派模型,每一次加载类的时候都要去询问父类的加载器

双亲委派模型的好处
1.防止重复加载同一个.class文件,保护了数据安全
2.保证核心的.class不会被篡改

Tomcat类加载机制

在这里插入图片描述
Tomcat依赖于JDK,所以也是符合双亲委派模型的,但是这里的符合并不是完全符合

  • Catalina类加载器会去加载Catalina.sh中的启动类,这里是说Tomcat启动的时候除了JVM中的启动类外还需要哪些启动类就由Catalina类加载器去加载
  • Tomcat中可以部署war包,当不同的war包里面有用到相同的jar包,比如:log4j,就会由Shared类加载器去加载
  • Tomcat中部署两个war包,这两个war包是同一个项目不同版本,比如:一个叫shop1.war ,另一个叫shop2.war,两个war包内都有一个类叫payShop,如果此时符合双亲委派模型,那么必定有一个类是加载不了的,此时,WebApp类加载器是不符合双亲委派模型的
SPI(Service Provider Interface)

这里是重点,面试会问!!!
什么是SPI?
spi是一套机制,是 Java 提供的一套用来被第三方实现或者扩展的 API,它可以用来启用框架扩展和替换组件。
我觉得上面的说法听起来不舒服,我就用自己的话来解释一下,先来看图
在这里插入图片描述
再来看一段我们常用的JDBC代码

package ex7;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class DBUtil {
    public static final String URL = "jdbc:mysql://localhost:3306/delay_order?serverTimezone=GMT%2b8";
    public static final String USER = "root";
    public static final String PASSWORD = "789456";

    public static void main(String[] args) throws Exception {
        //1.加载驱动程序
        Class.forName("com.mysql.cj.jdbc.Driver");
        //2. 获得数据库连接
        Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
        //3.操作数据库,实现增删改查
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery("SELECT order_no, order_note FROM order_exp");
        //如果有数据,rs.next()返回true
        while(rs.next()){
            System.out.println(rs.getString("order_no")+" 订单内容:"+rs.getString("order_note"));
        }
    }
}

再上面的代码中,有加载程序驱动这一步,这个Driver其实就是再JDK的一个接口,这个接口再JDK本身是没有实现的,也就是如果想用的话就必须去导入一个mysql-connector-java-8.0.11.jar,MySQL 的驱动代码,就是在这里实现的,路径:mysql-connector-java-8.0.11.jar!/META-INF/services/java.sql.Driver,里面的内容是:com.mysql.cj.jdbc.Driver
在这里插入图片描述
通过在 META-INF/services 目录下,创建一个以接口全限定名为命名的文件(内容为实现类的全限定名),即可自动加载这一种实现,这就是 SPI。 SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制,主要使用 java.util.ServiceLoader 类进行动态装载
这种方式,同样打破了双亲委派的机制
DriverManager 类和 ServiceLoader 类都是属于 rt.jar 的。它们的类加载器是 Bootstrap ClassLoader,而具体的数据库驱动,却属于业务代码,这个启动类加载器是无法加载的。这就比较尴尬了,虽然凡事都要祖先过问,但祖先没有能力去做这件事情, 怎么办?
跟踪代码,来看一下。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
通过代码你可以发现它把当前的类加载器,设置成了线程的上下文类加载器。那么,对于一个刚刚启动的应用程序来说,它当前的加载器是谁呢?也就 是说,启动 main 方法的那个加载器,到底是哪一个? 所以我们继续跟踪代码。找到 Launcher 类,就是 jre 中用于启动入口函数 main 的类。我们在 Launcher 中找到以下代码。
在这里插入图片描述
到此为止,事情就比较明朗了,当前线程上下文的类加载器,是应用程序类加载器。使用它来加载第三方驱动
总结:可以让你更好的看到一个打破规则的案例(虽然应该是属于 BootStrap 类加载器加载的,但是还是在 app 类加载器去加载的它)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值