java中类的加载ClassLoader

目录

java中类的加载ClassLoader_第1张图片
类加载的相关知识点

概念:

1,类加载的机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析、初始化,最终形成可以直接被Java虚拟机使用的Java类型,这就是虚拟机的类加载机制。
2,类加载的特点:运行期类加载。即在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期完成的,从而通过牺牲一些性能开销来换取Java程序的高度灵活性。
3,类加载的目的:class文件读入内存后,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法>区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

类的加载器:

类加载器(ClassLoader)用来加载 Java 类到 Java 虚拟机中。每个类加载器都拥有一个独立的类名称空间,它不仅用于加载类,还和这个类本身一起作为在JVM中的唯一标识。所以比较两个类是否相等,不仅需要它们来源于同一个class文件,而且还需要是有同一个ClassLoader加载。否则,这两个类不相同。

java中类的加载ClassLoader_第2张图片
类加载器的层级关系

1,启动类加载器(Bootstrap Class-Loader)

由C++语言实现,是虚拟机自身的一部分。负责加载存放在<JAVA_HOME>\lib目录中、或被-Xbootclasspath参数所指定路径中的、且可被虚拟机识别的类库。
无法被Java程序直接引用,如果自定义类加载器想要把加载请求委派给引导类加载器的话,可直接用null代替。

2,扩展类加载器(Extension or Ext Class-Loader)

这个类加载器是由sun.misc.Launcher$ExtClassLoader实现的。它负责将/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。

3,应用类加载器(Application or App Class-Loader)

该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(classpath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

4,自定义类加载器(? extends ClassLoader)

继承自java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节文件(.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成java.lang.Class 类的实例。示例代码如下:

public class MyClassLoader extends ClassLoader {

    private String mRoot;

    public MyClassLoader(ClassLoader parent, String root) {
        super(parent);
        this.mRoot = root;
    }

    //1,文件名需要是类的全限定性名称
    protected Class findClass(String name) throws ClassNotFoundException {
        byte[] classData = getNetData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    //2,最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。
    @Override
    protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
        return super.loadClass(name, resolve);
    }

    private byte[] getNetData(String className) {
        String fileName = mRoot + File.separator + className.replace(".", File.separator + ".class");
        try {
            FileInputStream fileInputStream = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int buffer_length = 1024 * 8;
            byte[] buffer = new byte[buffer_length];
            int length;
            while ((length = fileInputStream.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            //3,此处可以对加密的class文件进行对应的解密
            return baos.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

类的加载时机

1,创建类的实例,也就是 new 一个对象
2,访问某个类或接口的静态变量,或者静态方法
3,使用反射Class.forName()
4,初始化该类的子类
5,JVM 启动时标明的启动类或者直接使用java.exe命令来运行某个主类。

注意点:
1,通过子类来引用父类的静态字段,不会导致子类初始化
2,通过数组定义来引用类,不会触发此类的初始化
3,常量会在编译阶段存入调用者的常量池,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类初始化

类的加载过程

java中类的加载ClassLoader_第3张图片
类加载的生命周期

1,加载 :将java字节码数据从不同的数据源读取到JVM中,并映射为JVM认可的数据结构class对象。

主要完成3件事:
1,通过类的全限定名来获取定义此类的二进制字节流。如从ZIP包读取、从网络中获取、通过运行时计算生成、由其他文件生成、从数据库中读取等等途径......如果输入数据不是 ClassFile 的结构,则会抛出ClassFormatError。
2,将该二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构,该数据存储结构由虚拟机实现自行定义。
3,在内存中生成一个代表这个类的java.lang.Class对象,它将作为程序访问方法区中的这些类型数据的外部接口。

加载源(二进制字节流可以从以下方式获取)
1,文件:从 ZIP 包读取,这很常见,最终成为日后 JAR、EAR、WAR 格式的基础。
2,网络:从网络中获取,这种场景最典型的应用是 Applet。
3,其它文件动态生成:典型场景是 JSP 应用,即由 JSP 文件生成对应的 Class 类。
4,数据库:将二进制字节流存储至数据库中,然后在加载时从数据库中读取。这种场景相对少见,例如有些中间件服务器(如 SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。
5,计算生成:运行时计算生成,这种场景使用得最多的就是动态代理技术。在 java.lang.reflect.Proxy 中,就是用了ProxyGenerator.generateProxyClass 的代理类的二进制字节流。

2,验证:这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合 Java 虚拟机规范的,否则就被认为是VerifyError,这样就防止了恶意信息或者不合规的信息危害JVM的运行,验证阶段有可能触发更多class的加载。

1,文件格式验证:验证字节流是否符合Class文件格式的规范、以及是否能被当前版本的虚拟机处理。只有保证二进制字节流通过了该验证后,它才会进入内存的方法区中进行存储,而后续3个验证阶段全部是基于方法区而不是字节流了。
2,对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。
3,对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
4,对类自身以外(如常量池中的各种符号引用)的信息进行匹配性校验。目的是确保解析动作能正常执行,如果无法通过符号引用验证,那么将会抛出一个java.lang.IncompatibleClassChangeError异常的子类。(注意:该验证发生在虚拟机将符号引用转化为直接引用的时候,即解析阶段。)

3,准备:创建类的接口中的静态变量,并初始化赋值。这里侧重的是分配内存。所有的赋值均未变量类型的默认值(如:int变量赋值0),而final修饰的变量直接赋予常量值。

4,解析:虚拟机将常量池内的符号引用替换为直接引用的过程。

1,符号引用(Symbolic References):以一组符号来描述所引用的目标。可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
与虚拟机实现的内存布局无关,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中,所以即使各种虚拟机实现的内存布局不同,但是能接受符号引用都是一致的。
2,直接引用(Direct References):可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
与虚拟机实现的内存布局相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不同。

5,初始化:这一步是真正的执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态代码块内的逻辑,编译器在编译阶段将此逻辑整理好。父类的初始化逻辑优先于当前类型的逻辑。同时也是执行类构造器()的过程。

():由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生。
1,是线程安全的,在多线程环境中被正确地加锁、同步。
2,对于类或接口来说是非必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成 ()。
3,接口与类不同的是,执行接口的 ()不需要先执行父接口的 (),只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的()。

6,使用:通过类的class对象进行相应的逻辑操作。

7,卸载:由JVM自带的类加载器所加载的类,在JVM的生命周期中,始终不会被卸载。JVM本身会始终引用这些类加载器,而这些类加载器始终引用它们所加载的类的Class对象。所以说,这些Class对象始终是可触及的(除非异常退出等)。 而用户自定义的类加载器所加载的类是可以被卸载的。

类的加载特点

1,双亲委派机制:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

//ClassLoader中类加载的源码
protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException{
    // First, check if the class has already been loaded
    //1,检查该类是否被加载
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                //2,如果父类的加载器存在,使用父类加载器进行加载
                c = parent.loadClass(name, false);
            } else {
                //3,如果不存在父类加载器,使用启动类加载器进行加载,通过调用本地方法native Class findBootstrapClass(String name)
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // ClassNotFoundException thrown if class not found
            // from the non-null parent class loader
        }
        if (c == null) {
            // If still not found, then invoke findClass in order
            // to find the class.
            c = findClass(name);
        }
    }
    return c;
}

2,可见性:子类加载器可以访问父类的类加载器。但反过来则不允许。
3,单一性:由于父类加载器对子类可见,所以父类加载器加载过的类,子类不会重复加载,保证了同一个类只加载一次。

类的卸载

1,执行了System.exit()方法。
2,程序正常执行结束。
3,程序在执行过程中遇到了异常或错误而异常终止。
4,由于操作系统出现错误而导致Java虚拟机进程终止。

对象创建的过程

示例类如下:

public class Father {

    private int a = 10;      //静态代码块与静态方法无法访问变量a
    public static int b = 20;
    public static final int c = 30;

    //静态代码块
    static {
        Log.d(Constant.CLASS_LOAD, "Father : 静态代码块 : b = " + b);
        Log.d(Constant.CLASS_LOAD, "Father : 静态代码块 : c = " + c);
        b++;
    }

    //代码块
    {
        Log.d(Constant.CLASS_LOAD, "Father : 代码块 : a = " + a);
        Log.d(Constant.CLASS_LOAD, "Father : 代码块 : b = " + b);
        Log.d(Constant.CLASS_LOAD, "Father : 代码块 : c = " + c);
        a++;
        b++;
    }

    //静态方法
    public static void set() {
        Log.d(Constant.CLASS_LOAD, "Father : 静态方法 : b = " + b);
        Log.d(Constant.CLASS_LOAD, "Father : 静态方法 : c = " + c);
        b++;
    }

    //普通方法
    public void setA() {
        Log.d(Constant.CLASS_LOAD, "Father : 普通方法 : a = " + a);
        Log.d(Constant.CLASS_LOAD, "Father : 普通方法 : b = " + b);
        Log.d(Constant.CLASS_LOAD, "Father : 普通方法 : c = " + c);
        a++;
        b++;
    }

    //无参构造方法
    public Father() {
        Log.d(Constant.CLASS_LOAD, "Father : 无参构造方法 : a = " + a);
        Log.d(Constant.CLASS_LOAD, "Father : 无参构造方法 : b = " + b);
        Log.d(Constant.CLASS_LOAD, "Father : 无参构造方法 : c = " + c);
        a++;
        b++;
    }
}

public class Son extends Father {

    private int a1 = 100;      //静态代码块与静态方法无法访问变量a
    public static int b1 = 110;
    public static final int c1 = 120;

    //静态代码块
    static {
        Log.d(Constant.CLASS_LOAD, "Son : 静态代码块 : b = " + b1);
        Log.d(Constant.CLASS_LOAD, "Son : 静态代码块 : c = " + c1);
        b1++;
    }

    //代码块
    {
        Log.d(Constant.CLASS_LOAD, "Son : 代码块 : a = " + a1);
        Log.d(Constant.CLASS_LOAD, "Son : 代码块 : b = " + b1);
        Log.d(Constant.CLASS_LOAD, "Son : 代码块 : c = " + c1);
        a1++;
        b1++;
    }

    //静态方法
    public static void set1() {
        Log.d(Constant.CLASS_LOAD, "Son : 静态方法 : b = " + b1);
        Log.d(Constant.CLASS_LOAD, "Son : 静态方法 : c = " + c1);
        b1++;
    }

    //普通方法
    public void setA() {
        Log.d(Constant.CLASS_LOAD, "Son : 普通方法 : a = " + a1);
        Log.d(Constant.CLASS_LOAD, "Son : 普通方法 : b = " + b1);
        Log.d(Constant.CLASS_LOAD, "Son : 普通方法 : c = " + c1);
        a1++;
        b1++;
    }

    //无参构造方法
    public Son() {
        Log.d(Constant.CLASS_LOAD, "Son : 无参构造方法 : a = " + a1);
        Log.d(Constant.CLASS_LOAD, "Son : 无参构造方法 : b = " + b1);
        Log.d(Constant.CLASS_LOAD, "Son : 无参构造方法 : c = " + c1);
        a1++;
        b1++;
    }
}

1,首次创建Son对象,同时无father对象(调用顺序:父类的静态代码块 ——>子类的静态代码块 ——>父类的代码块,构造方法 ——>子类的代码块,构造方法)

10-01 09:11:48.221 12740-12740/com.learn.study D/class_load: Father : 静态代码块 : b = 20
10-01 09:11:48.222 12740-12740/com.learn.study D/class_load: Father : 静态代码块 : c = 30
    Son : 静态代码块 : b = 110
    Son : 静态代码块 : c = 120
    Father : 代码块 : a = 10
    Father : 代码块 : b = 21
    Father : 代码块 : c = 30
    Father : 无参构造方法 : a = 11
    Father : 无参构造方法 : b = 22
    Father : 无参构造方法 : c = 30
    Son : 代码块 : a = 100
    Son : 代码块 : b = 111
    Son : 代码块 : c = 120
    Son : 无参构造方法 : a = 101
    Son : 无参构造方法 : b = 112
    Son : 无参构造方法 : c = 120

2,再次创建son对象(调用顺序:父类的代码块,构造方法 ——>子类的代码块,构造方法)。静态的代码块只调用一次。同一个进程其它地方再次创建son对象也不会调用静态代码块,除非进程结束后重新创建。

Father : 代码块 : a = 10
    Father : 代码块 : b = 23
    Father : 代码块 : c = 30
    Father : 无参构造方法 : a = 11
    Father : 无参构造方法 : b = 24
    Father : 无参构造方法 : c = 30
    Son : 代码块 : a = 100
    Son : 代码块 : b = 113
10-01 09:16:05.174 13424-13424/com.learn.study D/class_load: Son : 代码块 : c = 120
    Son : 无参构造方法 : a = 101
    Son : 无参构造方法 : b = 114
    Son : 无参构造方法 : c = 120

3,首次创建father对象(调用顺序:父类的静态代码块 ——>父类的代码块,构造方法 )。父类加载时子类不处理。符合类加载的可见性特点。

10-01 09:20:59.776 14020-14020/com.learn.study D/class_load: Father : 静态代码块 : b = 20
    Father : 静态代码块 : c = 30
    Father : 代码块 : a = 10
    Father : 代码块 : b = 21
    Father : 代码块 : c = 30
    Father : 无参构造方法 : a = 11
    Father : 无参构造方法 : b = 22
    Father : 无参构造方法 : c = 30

4,存在father对象后,首次创建son对象(调用顺序:子类的静态代码块 ——>父类的代码块,构造方法 ——>子类的代码块,构造方法)

  Son : 静态代码块 : b = 110
    Son : 静态代码块 : c = 120
    Father : 代码块 : a = 10
    Father : 代码块 : b = 23
    Father : 代码块 : c = 30
    Father : 无参构造方法 : a = 11
    Father : 无参构造方法 : b = 24
    Father : 无参构造方法 : c = 30
    Son : 代码块 : a = 100
    Son : 代码块 : b = 111
    Son : 代码块 : c = 120
    Son : 无参构造方法 : a = 101
    Son : 无参构造方法 : b = 25
    Son : 无参构造方法 : c = 120

5,子类调用父类的静态方法(调用顺序:父类的静态代码块 ——>父类的静态方法)

10-01 09:30:35.699 15160-15160/com.learn.study D/class_load: Father : 静态代码块 : b = 20
    Father : 静态代码块 : c = 30
    Father : 静态方法 : b = 21
    Father : 静态方法 : c = 30

//补充:子类调用父类静态变量。Son.b (父类的静态代码块)
10-01 09:32:38.949 15502-15502/com.learn.study D/class_load: Father : 静态代码块 : b = 20
    Father : 静态代码块 : c = 30
//调用Son.c时无任何方法执行

6,子类使用自身的静态方法(调用顺序:父类的静态代码块 ——>子类的静态代码块 ——>子类的静态方法)

10-01 09:35:27.431 16112-16112/com.learn.study D/class_load: Father : 静态代码块 : b = 20
10-01 09:35:27.432 16112-16112/com.learn.study D/class_load: Father : 静态代码块 : c = 30
    Son : 静态代码块 : b = 110
    Son : 静态代码块 : c = 120
    Son : 静态方法 : b = 21
    Son : 静态方法 : c = 120

//补充:子类调用自身静态变量。Son.b1(调用顺序:父类的静态代码块 ——>子类的静态代码块)
10-01 09:37:35.302 16489-16489/com.learn.study D/class_load: Father : 静态代码块 : b = 20
    Father : 静态代码块 : c = 30
    Son : 静态代码块 : b = 110
    Son : 静态代码块 : c = 120

//调用Son.c1时无任何方法执行

总结:

  • 1,类的加载只会执行一次。下次再创建对象时,可以直接在方法区中获取class信息。而静态代码块时在类的加载时执行的,故静态代码块只执行一次。
  • 2,每new一次对象就调用一次非静态的代码块,构造方法。
  • 3,创建类的对象时都会优先调用父类中的对象方法,同时静态优先于非静态
  • 4,子类调用父类的静态变量时,只会执行父类的静态代码块,子类不执行。
  • 5,调用static final常量时类的静态代码块,非静态代码块,构造方法均不会执行。

你可能感兴趣的:(java中类的加载ClassLoader)