JVM 类加载机制和类加载器

一、类加载机制

1.1 简介

        虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。

        在 java 语言中,类的加载,连接和初始化的过程都是在程序运行期间完成的。

1.2 生命周期

        类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括:加载,验证,准备,解析,初始化,使用,卸载这 7 个阶段。其中验证,准备,解析这 3 个阶段统称为连接。

        加载,验证,准备,初始化和卸载这 5 个阶段的顺序是确定的,而解析阶段则不一定,他在某些情况下可以在初始化之后再开始,因为 java 语言支持动态绑定。

1.3 加载阶段

        1、将类的字节码载入到方法区中。

        2、如果这个类还有父类没有加载,先加载父类。

        3、加载和连接可能是交替运行的。

1.4 连接阶段

1.4.1 验证

        验证类是否符合 JVM 规范,安全性检查。可以修改 .class ,然后再执行该文件,就可以发现该类无法运行了,因为此时进行了验证。 

1.4.2 准备

        为 static 变量分配空间,设置默认值。

        1、static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成。

        2、如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成。

        3、如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成。

1.4.3 解析

        将常量池中的符号引用解析为直接引用。

1.5 初始化

        初始化即调用 <cinit>()V ,虚拟机会保证这个类的构造方法的线程安全。

1.5.1 类会初始化情况

        1、main 方法所在的类,总会被首先初始化

        2、首次访问这个类的静态变量或静态方法时,类会初始化

        3、子类初始化,如果父类还没初始化,会引发父类初始化

        4、子类访问父类的静态变量,只会触发父类的初始化

        5、Class.forName,会引发此类的初始化

        6、new 会导致初始化

1.5.2 类不会初始化情况

        1、访问类的 static final 静态常量(基本类型和字符串)不会触发初始化

        2、类对象.class 不会触发初始化

        3、创建该类的数组不会触发初始化

        4、类加载器的 loadClass 方法,不会触发初始化

        5、Class.forName 的参数 2 false 时,不会触发初始化

1.5.3 实验

class A {
    static int a = 0;
    static {
        System.out.println("a init");
    }
}
class B extends A {
    final static double b = 5.0;
    static boolean c = false;
    static {
        System.out.println("b init");
    }
}

        下面的方法执行时,请先全部注释掉,每次只执行其中一个

public class Load3 {
    static {
        System.out.println("main init");
    }
    public static void main(String[] args) throws ClassNotFoundException {
        // 1. 静态常量(基本类型和字符串)不会触发初始化
        System.out.println(B.b);
        // 2. 类对象.class 不会触发初始化
        System.out.println(B.class);
        // 3. 创建该类的数组不会触发初始化
        System.out.println(new B[0]);
        // 4. 不会初始化类 B,但会加载 B、A
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        cl.loadClass("cn.itcast.jvm.t3.B");
        // 5. 不会初始化类 B,但会加载 B、A
        ClassLoader c2 = Thread.currentThread().getContextClassLoader();
        Class.forName("cn.itcast.jvm.t3.B", false, c2);
        // 1. 首次访问这个类的静态变量或静态方法时
        System.out.println(A.a);
        // 2. 子类初始化,如果父类还没初始化,会引发
        System.out.println(B.c);
        // 3. 子类访问父类静态变量,只触发父类初始化
        System.out.println(B.a);
        // 4. 会初始化类 B,并先初始化类 A
        Class.forName("cn.itcast.jvm.t3.B");
    }
}

1.5.4 练习

        从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始化。

public class Load4 {
    public static void main(String[] args) {
        // 不会初始化
        System.out.println(E.a);
        // 不会初始化
        System.out.println(E.b);
        // 会初始化,因为 Integer 不属于基本数据类型或字符串类型
        System.out.println(E.c);
    }
}
class E {
    public static final int a = 10;
    public static final String b = "hello";
    public static final Integer c = 20;
}

二、类加载器

2.1 简介

        类加载器:JVM 设计团队把类加载阶段中的 “通过一个类的全限定名称或者这个类的二进制字节流” 这个动作放到 Java 虚拟机的外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为 “类加载器”。

        比较两个类是否 “相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。

2.1.1 类加载器分类

        以 jdk8 为例,所有的类加载器所下所示

中文名称英文名称加载哪的类说明
启动类加载器Bootstrap ClassLoaderJAVA_HOME/jre/lib无法直接访问
扩展类加载器Extension ClassLoaderJAVA_HOME/jre/lib/ext

上级为 Bootstrap,但是 getParent 时显示为 null,因为 Bootstrap 是 c++ 代码实现的

应用程序类加载器Application ClassLoaderclasspath上级为 Extension
自定义类加载器自定义类加载器自定义上级为 Application

2.1.2 双亲委派原则

        以应用程序类加载器为例,他如果想要加载类,首先会去它的上级扩展类加载器是否已经加载,如果没有,那么它就会委托扩展类加载器去它的上级启动类加载器问问,有没有加载这个类,如果这两个上级都没有加载,才会轮到应用程序类加载器去加载这个类。

2.2 启动类加载器

        用 Bootstrap 类加载器加载类,代码如下

package cn.itcast.jvm.t3.load;
public class F {
    static {
        System.out.println("bootstrap F init");
    }
}

        执行

package cn.itcast.jvm.t3.load;
public class Load5_1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
        System.out.println(aClass.getClassLoader());
    }
}

        输出,需要使用 idea 的命令行执行才可以

# -Xbootclasspath 表示设置 bootclasspath
# 其中 /a:. 表示将当前目录追加至 bootclasspath 之后

E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:. cn.itcast.jvm.t3.load.Load5_1
bootstrap F init
null

2.3 扩展类加载器

        用 Extension 类加载器加载类,代码如下

package cn.itcast.jvm.t3.load;
public class G {
    static {
        System.out.println("classpath G init");
    }
}

        执行

public class Load5_2 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
        System.out.println(aClass.getClassLoader());
    }
}

         输出

classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2

2.4 双亲委派模式

        所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则。这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系。

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 1. 检查该类是否已经加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        // 2. 有上级的话,委派上级 loadClass
                        c = parent.loadClass(name, false);
                    } else {
                        // 3. 如果没有上级了(ExtClassLoader),则委派
                        BootstrapClassLoader c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    
                }
                if (c == null) {
                    long t1 = System.nanoTime();
                    // 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
                    c = findClass(name);
                    // 5. 记录耗时
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

        例如下面的这段代码,执行的流程如下所示

public class Load5_3 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Load5_3.class.getClassLoader()
                .loadClass("cn.itcast.jvm.t3.load.H");
        System.out.println(aClass.getClassLoader());
    }
}
1、sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有

2、sun.misc.Launcher$AppClassLoader // 2 处,委派上级 sun.misc.Launcher$ExtClassLoader.loadClass()

3、sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有

4、sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派 BootstrapClassLoader 查找

5、 BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有

6、sun.misc.Launcher$ExtClassLoader // 4 处,调用自己的 findClass 方法,是在nJAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,回到 sun.misc.Launcher$AppClassLoader 的 // 2 处

7、继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在classpath 下查找,找到了

2.5 线程上下文类加载器

        我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写下面这个

Class.forName("com.mysql.jdbc.Driver")

也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗?让我们追踪一下源码:

public class DriverManager {
    // 注册驱动的集合
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers
            = new CopyOnWriteArrayList<>();
    // 初始化驱动
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
}

        先不看别的,看看 DriverManager 的类加载器:

System.out.println(DriverManager.class.getClassLoader());

        打印为 null,表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但 JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在 DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?

        继续看 loadInitialDrivers() 方法:

private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>
                    () {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        // 1)使用 ServiceLoader 机制加载驱动,即 SPI
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader<Driver> loadedDrivers =
                        ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try {
                    while (driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch (Throwable t) {
                    // Do nothing
                }
                return null;
            }
        });
        println("DriverManager.initialize: jdbc.drivers = " + drivers);
        // 2)使用 jdbc.drivers 定义的驱动名加载驱动
        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                // 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

        先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载,在这个地方是打破了双亲委派机制的。

        再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI) 约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称

        这样就可以使用下面的这种方法来得到实现类,体现的是【面向接口编程+解耦】的思想。 

ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
	while(iter.hasNext()) {
		iter.next();
}

        接着看 ServiceLoader.load 方法:

public static <S> ServiceLoader<S> load(Class<S> service) {
	// 获取线程上下文类加载器
	ClassLoader cl = Thread.currentThread().getContextClassLoader();
	return ServiceLoader.load(service, cl);
}

        线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由 Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类 LazyIterator 中。

2.6 自定义类加载器

2.6.1 使用场景

        1、想加载非 classpath 随意路径中的类文件。

        2、都是通过接口来使用实现,希望解耦时,常用在框架设计。

        3、这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

2.6.2 实现步骤

        1、继承 ClassLoader 父类

        2、要遵从双亲委派机制,重写 findClass 方法注意不是重写 loadClass 方法,否则不会走双亲委派机制。

        3、读取类文件的字节码

        4、调用父类的 defineClass 方法来加载类。

        5、使用者调用该类加载器的 loadClass 方法。

2.6.3 代码演练

        准备好两个 .class 文件放入 F:\myclasspathjava 代码如下,需要提前将两个类编译下

public class MapImpl1 extends AbstractMap implements Map {
    static {
        System.out.println("I am MapImpl1 static ");
    }
    @Override
    public Set<Entry> entrySet() {
        return null;
    }
    @Override
    public String toString() {
        return "MapImpl1{}";
    }
}
public class MapImpl2 extends AbstractMap implements Map {
    static {
        System.out.println("I am MapImpl2 static ");
    }
    @Override
    public Set<Entry> entrySet() {
        return null;
    }
    @Override
    public String toString() {
        return "MapImpl2{}";
    }
}

        编写测试类和自定义类加载器

public class Load7 {
    public static void main(String[] args) throws Exception {
        MyClassLoader m = new MyClassLoader();
        Class<?> c1 = m.loadClass("MapImpl1");
        Class<?> c2 = m.loadClass("MapImpl1");
        // 判断加载的两个对象是否相等
        System.out.println(c1 == c2);
        // 使用不同的类加载器去加载同一个类
        MyClassLoader m2 = new MyClassLoader();
        Class<?> c3 = m2.loadClass("MapImpl1");
        System.out.println(c1 == c3);
        // 使用反射
        c1.newInstance();
    }
}

class MyClassLoader extends ClassLoader {
    // name 就是类的名称
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String path = "F:\\myclasspath\\" + name + ".class";
        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            Files.copy(Paths.get(path), os);
            // 得到字节数组
            byte[] bytes = os.toByteArray();
            // 字节数组转换成 *.class 对象
            return defineClass(name, bytes, 0, bytes.length);

        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException("文件未找到", e);
        }
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

快乐的小三菊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值