Java虚拟机的类加载机制详解

8 篇文章 0 订阅
本文详细介绍了Java类的生命周期,包括加载、验证、准备、解析、初始化和卸载六个阶段。类加载器的双亲委派模型确保了类的安全加载,而JNDI和数据库驱动的加载则展示了如何打破这一模型。此外,文章还讨论了自定义类加载器的实现。
摘要由CSDN通过智能技术生成

类的生命周期

在这里插入图片描述

1. 加载

加载过程总览

  • 通过一个类的全限定名来获取定义此类的二进制字节流。
  • 通过字节流的方式把 .class文件放入MetaSpace
  • 在堆中生成一个Class对象,指向这个.class文件的地址

动态代理类的加载

java.lang.reflect.Proxy或者CGLib都是在运行时生成字节码,也可以把这个直接在堆内存中的字节码加载进到MetaSpace,同样堆中生成一个Class对象指向这块内存。

数组类的加载

  • 如果数组的元素类型是引用类型,而且这个类还没有被放入MetaSpace,那么先加载元素类,然后创建的数组类的工作空间就是加载这个类的类加载空间。

    这句话好像有点绕,等会介绍完类加载器画个图示吧。

  • 如果数组元素是基本类型(int[] 的元素类型 int),则这数组的名称空间就是BootStrap类加载空间

  • 数组类的可访问性与它的组件类型的可访问性一致,如果组件类型不是引用类型,它的数组类的 可访问性将默认为public

反射的基础

/*
 * @Author 郭学胤
 * @University 深圳大学
 * @Description
 * @Date 2021/2/21 13:14
 */

public class L02_Reflect {
    public static void main(String[] args) throws NoSuchFieldException {
		
        ReflectTest reflectTest = new ReflectTest();
        // 通过指向类文件的引用,查找类中定义的field
        reflectTest.getClass().getField("i");
        // 查找其他属性、方法、父类、接口、注解

    }
}

class ReflectTest{

    public int i;

    public void m(){

    }

}

反射也都是基于这个Class对象实现的,通过Class引用找到相应的类文件,然后查找自己需要的信息。

验证

验证class文件是否以魔数CAFE BABE开头、是否继承了final类、是否实现了抽象类或者接口的全部方法等等等吧

准备

静态变量

准备阶段是正式为静态变量,分配内存并设置类变量初始值的阶段。

static int i = 1;
static Object = new Object;
static boolean flag = true;

这三个变量经过准备阶段之后会变成默认值:0 , null , false

把变量赋值为初始值(自己设定的值)的代码存放于类构造器<clinit>()方法之中,所以把static变量赋值为初始值要到类的初始化阶段才会被执行。

final

被final修饰的变量其实就是常量了,准备阶段会把常量赋值。

static final int constantValue = 1;

会在此阶段赋值为1。

解析

简单来讲就是把符号引用转成直接引用。

符号引用

符号可以是任何形式的字面量。

在上一节中说过的常量池中的第二项指向第十五项,第十五项为一个utf8的字符串java/lang/Object,此时这个就是一个符号引用。

直接引用

是可以直接指向目标的指针、或者是一个能间接定位到目标的句柄。

解析是在干什么

假设现在已经把java/lang/Object这个类加载到了MetaSpace,堆中有一个Object.Class对象指向MetaSpace中的这块内存区域

我的第二项或者第十五项不在单纯的记录这个类的全限定名了,我要把他换成一个指向这块MetaSpace内存的指针,或者换成一个指向Object.Class的指针。

当然除了类的解析之外,还会有接口、Files、方法等的解析。

解析的过程又可能会触发其他类的验证准备过程。

  • 引用类的接口(如果接口中默认方法)

  • 递归加载父类

  • 然后还要确定当前解析的类对引用类是否具有访问权限。如果发现不具备访问权限, 将抛出java.lang.IllegalAccessError异常。

  • 解析一个Field

    class Test{
        main(){
            //OneClass.oneStaticField...
        }
    }
    

    出现上述伪代码情况的时候会解析类中字段,解析OneClass的接口或者父类,然后查找这个Filed的引用,失败则NoSuchFieldError,无权限则IllegalAccessError

  • 解析一个方法

    class Test{
        main(){
            //OneClass.oneStaticMethod()
        }
    }
    

    解析OneClass的接口或者父类,然后查找这个oneStaticMethod()的引用,失败则NoSuchMethodError,无权限则IllegalAccessError

初始化

准备阶段时,变量已经赋过一次系统要求的默认值,而在初始化阶段,需要给这些变量赋值为代码声明的初始值。

<clinit>()方法与类的构造函数<init>()不同这个方法是所有的静态变量 + 静态代码块收集的总和。

<clinit>()<init>()

class Test{
    
    static int i = 1;
    static Object o = new Object();
    
    static{
        System.out.println("来了就是深圳人!");
    }
    
    public Test(){}
}

可以理解成下边,此代码经过JAVAC编译之后

class Test{
    // **************
    // 准备阶段赋值默认值
    // **************
    static int i;
    static Object o;
    
    // *******************************************
    // 虚拟机自动把所有的静态语句收集到一起,当成clinit方法
    // *******************************************
    synchronized <cinit>(){
        i = 1;
        o = new Object();
        static{
            System.out.println("来了就是深圳人!");
        }
    }
    
    // *************
    // 构造方法 <init>
    // *************
    public Test(){}
}

初始化阶段就是在执行<cinit>()

请注意这里的<cinit>()是需要同步的同一个类加载器下保证多个线程同时初始化一个类的时候也是cinit只执行一次

实验代码

/*
 * @Author 郭学胤
 * @University 深圳大学
 * @Description
 * @Date 2021/2/21 14:39
 */

public class L03_ClassLoader {
    public static void main(String[] args) {

        Runnable runnable = ()->{
            new TestClassLoader();
        };

        new Thread(runnable, "t1").start();
        new Thread(runnable, "t2").start();

    }

}

class TestClassLoader{
    // 虚拟机收集 static 代码块为 cinit 方法的时候,会加锁!!!!!!!
    static {
        /*
        * 参考了周志明老师的深入理解Java虚拟机才知道用这个判断不会报错
        * 单独写一个 while(true)编译不通过
        * */
        if(true){
            while (true){
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

此小实验会一直输出进入同步代码块的线程名字。

面试题

class Interview {
    public static T t = new T(); 
    public static int count = 2; 

    private T() {
        count ++;
    }
}
// 这个变量count 最后的值为多少,答案是 2 

class Interview {
    // 两个static语句换了一下位置
    public static int count = 2; 
    public static T t = new T(); 
   
    private T() {
        count ++;
    }
}
// 这个变量count 最后的值为多少,答案是 3

卸载

当一个类的Class对象没有强引用指向他之后,GC线程回收Class对象,此时也没有任何引用指向MetaSpace中的类文件,此时类文件所占用的内存也会被GC回收。完成类的卸载过程。至此一个类的生命周期结束。等到下次再用到这个类的时候再去加载一次。重新走一次刚刚的流程。

类加载器

双亲委派模型

在这里插入图片描述

很多以为双亲委派模型的意思是下层的类加载器是继承自上层的加载器,但是不是的!!!

不是继承自上层的类加载器!!!

不是继承自上层的类加载器!!!

不是继承自上层的类加载器!!!

重要事情说三遍。而是类中的一个属性

在这里插入图片描述

public abstract class ClassLoader {
    // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent; // !!!!!!!!就是它
}

委派工作过程

  • 一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给(父-类加载器)去完成,

  • 每一个层次的类加载器都是如此,因此所有的 加载请求最终都应该传送到最顶层的启动类加载器中

  • 只有当父加载器的搜索范围中没有找到所需的类时,子加载器才会尝试自己去完成加载。

委派的意义

java.lang.Object它存放在rt.jar之中,无论哪一 个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类 在程序的各种类加载器环境中都能够保证是同一个类。

如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中就会出现多个不同的Object类,应用程序将会变得一片混乱。

源码解析

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
        synchronized (getClassLoadingLock(name)) {
            /* 查看此类是否已经加载过,如果已经加载,直接返回 */
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                /* 如果没加载过 */
                long t0 = System.nanoTime();
                try {
                    /* 先让父-类加载器去loadClass,父也是先检查是否已经加载过 */
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        /* BootStrap类加载器的父为空,所以找到这里直接就开始加载 */
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    /* 如果都已经在父加载器中找了一圈,也没找到而且也没加载成功,就只能自己加载了 */
                    long t1 = System.nanoTime();
                    c = findClass(name);
					// ...
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

打破双亲委派模型

JNDI现在已经是Java的标准服务, 它的代码由BootStrap ClassLoader来完成加载(在JDK 1.3时加入到rt.jar的)。JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程 序的ClassPath下的JNDI服务提供者接口(Service Provider Interface, SPI)的代码,现在问题来了,BootStrap ClassLoader是绝不可能认识、加载这些代码的,那该怎么办?

对资源进行查找和集中管理是神魔恋?

数据库驱动

JDBC来说,DriverManagerrt.jar中用来对数据库连接资源进行统一管理的类。进行管理现在的厂商是OracleMySQL

DriverManager是由BootStrap ClassLoader来进行加载的。

现在的标准都是各厂商把配置信息写在META-INF/services/java.sql.Driver中,现在的内容都已经换成了com.mysql.cj.jdbc.Driver

在这里插入图片描述

package com.mysql.cj.jdbc; // 进入文件指明的包中找到对应的Driver文件

import java.sql.DriverManager;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            // 调用 DriverManager 静态方法把自己注册进DriverManager让其管理
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

触发DriverManager类加载

调用DriverManager的静态方法registerDriver()导致DriverManager被加载

public class DriverManager {

    // 用一个列表来管理所有注册的数据库连接驱动
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
  	// ...
    static {
        // 类加载时执行方法(见下)
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    
    
    private static void loadInitialDrivers() {
        String drivers;
        // ...
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
				// 获取到 APP 类加载器
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                /*
                * public static <S> ServiceLoader<S> load(Class<S> service) {
                *		*********************************************
                *		获取到线程上下文加载器,默认就是APP加载器
                *		*********************************************
                *        ClassLoader cl = Thread.currentThread().getContextClassLoader();
                *        return ServiceLoader.load(service, cl);
                * }
                */
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                try{
                    /*
                    * 方法调用见下个代码块详解
                    * 获取到所有的"META-INF/services/java.sql.Driver"中定义的类
                    *  com.alibaba.druid.proxy.DruidDriver -> {DruidDriver@5625} 
                    *  com.alibaba.druid.mock.MockDriver -> {MockDriver@5641} 
                    *  com.mysql.cj.jdbc.Driver -> {Driver@5652} 
                    */
                    while(driversIterator.hasNext()) {
                        // 使用 Class.forName(cn, false, loader);
                        // 其中 loader 就是 APPClassLoader
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                }
                // 仍然返回为空????
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);
		// 代码就在这里返回了????
        if (drivers == null || drivers.equals("")) {
            return;
        }
        // ......
    }
}

但是现在的问题是DriverManager的类加载器是BootStrap ClassLoader,它无法加载rt.jar之外的类,所以为了解决这种问题,线程都有了自己的线程上下文类加载器

public class Thread implements Runnable {
    //...
    private ThreadGroup group;
	/* contextClassLoader 默认为 APPClassLoader */
    private ClassLoader contextClassLoader;
	ThreadLocal.ThreadLocalMap threadLocals = null;
}

通过这个上下文加载器加载各大厂商自己的驱动类。

这就是所谓的打破双亲委派模型。

之前的逻辑是子类加载器加载一个类的时候向上询问,然而当父加载器用到子类加载器加载路径中的类时束手无策,只能等着报ClassNotFoundException

因为父只能继续向上问,Boot没得问,只能自己加载,然而Boot的加载路径只有rt.jar,所以他不可能找到,现在Boot可以拿到APPClassLoader,让他给Boot加载到MetaSpace中,Boot拿过来用。

定义自己的类加载器

findClass()

继续遵循双亲委派

protected Class<?> findClass(String name) throws ClassNotFoundException {
    File f = new File("Urpath");
    try {
        FileInputStream fi = new FileInputStream(f);
        //  ByteArrayInputStream byteIn = new ByteArrayInputStream(fi);
        int CAFEBABE = 0;
        ByteArrayOutputStream op = new ByteArrayOutputStream();
        while ((CAFEBABE = fi.read()) != -1){
            op.write( CAFEBABE );
        }
        byte[] bytes = op.toByteArray();
        op.close();
        fi.close();
        return defineClass(name, bytes, 0, bytes.length);

    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    throw new NoClassDefFoundError();
}

loadClass()

重写此方法打破双亲委派

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    File f = new File("Urpath");
    if(!f.exists()) return super.loadClass(name);

    try {

        InputStream is = new FileInputStream(f);

        byte[] b = new byte[is.available()];
        is.read(b);
        return defineClass(name, b, 0, b.length);
    } catch (IOException e) {
        e.printStackTrace();
    }

    return super.loadClass(name);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值