Java类加载机制

1 类的加载

类的加载是指将类的.class文件读取进内存中,并将其放在JVM运行时数据区的方法区内,然后在堆中创建一个java.lang.Class对象,用于封装类在方法区中的数据结构,同时作为方法区数据的访问入口。

2 类的生命周期

类的生命周期指一个class文件从加载到卸载的整个过程
​​​​​​​​​​​​​​生命周期

3 类的加载过程

JVM将类的加载分为三个阶段:装载(Load)链接(Link)初始化(Initialize)

3.1 装载:查找并加载类的二进制数据

在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的Java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

3.2 链接:验证、准备、解析

  • 验证
    校验字节码文件的正确性、安全性,包括四种验证:文件格式验证、元数据的验证、字节码验证、符号引用验证
  • 准备
    为类变量(static修饰的字段变量)分配内存并且设置该类变量的初始值,这里不包含final修饰的static,因为final在编译的时候就已经分配了。也不会为实例变量分配初始化,实例变量会随着对象分配到堆内存中。
  • 解析
    把常量池中的符号引用转换为直接引用,静态链接过程

3.3 初始化:初始化静态变量,执行静态代码块

3.3.1 类的初始化时机

如果一个类被直接引用,就会触发类的初始化。在Java中,直接引用的情况有:
● 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法
● 通过反射执行以上三种行为
● 初始化子类的时候,会触发父类的初始化
● 作为程序的入口直接运行时(main方法)

# 类初始化时机DEMO
class InitClass {
    static {
        System.out.println("class init");
    }
    public static String data = null;
    public static void method (){};
}

class SubInitClass extends InitClass{}

public class TestInitClass{
    // 类的初始化4:main方法运行,触发类初始化
    // 初始化仅会进行一次,所以main方法中只会输出一次 "class init"
    public static void main(String[] args) throws Exception{
        // 类初始化1:new对象、读取或设置类静态变量、调用类的静态方法
        new InitClass();
        InitClass.data = "";
        String data = InitClass.data;
        InitClass.method();

        // 类初始化2:反射
        Class<InitClass> clazz = InitClass.class;
        clazz.newInstance();
        Field field = clazz.getDeclaredField("data");
        field.get(null);
        field.set(null, "s");
        Method method = clazz.getDeclaredMethod("method");
        method.invoke(null,null);

        // 类的初始化3:实例化子类,引起父类实例化
        new SubInitClass();
    }
}
3.3.2 类初始化步骤
  • 假如这个类还没有被加载和链接,则程序先加载并连接该类(动态加载:主类在运行过程中如果使用到其他类,会逐步加载这些类 eg:jar包和war包中的类并不是一次性都加载到内存中,而是使用时才加载)
  • 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  • 假如类中有初始化语句,则系统依次执行这些初始化语句
# 动态加载DEMO
public class TestDynamicLoad {
    static {
        System.out.println("load TestDynamicLoad");
    }

    public static void main(String[] args) {
        // new触发类的加载
        new A();

        // 空引用不触发类的加载
        System.out.println("TestDynamicLoad main");
        A a = null;

        // 调用静态常量,不触发类的加载
        System.out.println("TestDynamicLoad main2");
        System.out.println(A.NAME);

        // 调用静态变量,触发类的加载
        System.out.println("TestDynamicLoad main3");
        System.out.println(A.name);
    }
}

class A{
    public static final String NAME = "a";
    public static String name = "aName";
    static {
        System.out.println("load A");
    }
    public A(){
        System.out.println("initial A");
    }
}

4 类加载后方法区存储内容

类被加载到方法区中后主要包含运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等信息。

类加载器的引用:
这个类到类加载器实例的引用

对应class实例的引用:
类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的对象实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。

5 类加载器

类加载过程主要通过类加载器实现,Java中有以下几种类加载器:

  • 引导类加载器(Bootstrap ClassLoader)
    负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar\charsets.jar等,由C++实现,不是ClassLoader子类,Java程序无法直接引用。
  • 拓展类加载器(Extension ClassLoader)
    负责加载支撑JVM运行的位于JRE的lib目录下的ext拓展目录中的类库
  • 应用类加载器(App ClassLoader)
    负责加载ClassPath路径下的类包,主要加载开发人员写的类
  • 自定义类加载器(Custom ClassLoader)
    负责加载用户自定义路径下的类包

类加载器之间存在父子层级结构:
类加载器父子层级

# classloader父子层级DEMO
public static void main(String[] args) {
    ClassLoader app = ClassLoader.getSystemClassLoader();
    ClassLoader ext = app.getParent();
    ClassLoader boot = ext.getParent();
    System.out.println("the bootstrapLoader: " + boot);
    System.out.println("the extClassLoader: " + ext);
    System.out.println("the appClassLoader: " + app);
}

#结果
the bootstrapLoader: null // c++实现,java获取不到引用
the extClassLoader: sun.misc.Launcher$ExtClassLoader@6e0be858
the appClassLoader: sun.misc.Launcher$AppClassLoader@18b4aac2

在这里插入图片描述在这里插入图片描述

6 双亲委派机制

6.1 定义

双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。每个类加载器都是如此,只有在父类加载器在自己的加载路径下找不到指定类时,子类才会尝试自己去加载。

6.2 工作过程

在这里插入图片描述

  1. 当应用程序类加载器收到一个类加载请求时,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器Extension ClassLoader去完成。
  2. 当Extension ClassLoader收到一个类加载请求时,他首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器Bootstrap ClassLoader去完成。
  3. 如果Bootstrap ClassLoader加载失败(在<JAVA_HOME>\lib中未找到所需类),就会让Extension ClassLoader尝试加载。
  4. 如果Extension ClassLoader也加载失败,就会使用Application ClassLoader加载。
  5. 如果Application ClassLoader也加载失败,就会使用自定义加载器去尝试加载。
  6. 如果均加载失败,就会抛出ClassNotFoundException异常。

6.3 双亲委托机制实现源码(JDK 1.8)

在这里插入图片描述

6.4 双亲委托作用

  1. 沙箱安全机制:防止核心类库被篡改,自己写的java.lang.String不会被加载。
  2. 避免类的重复加载:当父加载器已经加载过该类时,子加载器就没必要重新加载一次,保证被加载类的唯一性。
# 自定义java.lang.String,运行异常,查找不到main方法
package java.lang;
public class String {
    public static void main(String[] args) {
        System.out.println("My String Method");
    }
}

在这里插入图片描述

7 自定义类加载器

/**
 * 自定义类加载器
 * 1. 继承ClassLoader父类,实现自定义findClass方法
 * 2. findClass职责:加载class类到内存,并转换为Class类
 **/
public class MyClassLoader extends ClassLoader{
    private final String classPath;
    public MyClassLoader(String classPath){
        this.classPath = classPath;
    }

    /**
     * 通过类全限定名加载类文件byte数组
     * @param name 加载类路径
     * @return class文件字节数组
     * @throws Exception
     */
    private byte[] loadByte(String name) throws Exception {
        name = name.replaceAll("\\.","/");
        FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
        int length = fis.available();
        byte[] data = new byte[length];
        fis.read(data);
        fis.close();
        return data;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = loadByte(name);
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }
}

8 如何打破双亲委派机制

核心:继承ClassLoader,重写类加载方法,实现加载逻辑,不委托给双亲加载
在这里插入图片描述

/**
 * 自定义类加载器
 * 1. 继承ClassLoader父类,实现自定义findClass方法
 * 2. findClass职责:加载class类到内存,并转换为Class类
 **/
public class MyClassLoader extends ClassLoader{
    private final String classPath;
    public MyClassLoader(String classPath){
        this.classPath = classPath;
    }

    /**
     *  通过类路径加载类文件byte数组
     * @param name 加载类路径
     * @return class文件字节数组
     * @throws Exception
     */
    private byte[] loadByte(String name) throws Exception {
        name = name.replaceAll("\\.","/");
        FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
        int length = fis.available();
        byte[] data = new byte[length];
        fis.read(data);
        fis.close();
        return data;
    }

    /**
     * 重写类加载方法,实现自己的加载逻辑,不委托给双亲加载
     * @param name
     * @param resolve
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                
                // 定义某些类不走双亲委派机制,直接进行类的加载
                if (name.startsWith("com.matt.learn")){
                    c = findClass(name);
                }
                else {
                    c = this.getParent().loadClass(name);
                }

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = loadByte(name);
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }
}

拓展
Tomcat类加载机制实现:Tomcat如何保证不同的webapp应用中加载不同版本类库?JSP热加载机制?

参考文档:
https://www.cnblogs.com/ityouknow/p/5603287.html
https://blog.csdn.net/zhengzhb/article/details/7517213

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值