Java类加载过程

Java类加载 笔记

首先我们知道Java编码文件的.java文件是不能直接执行的,需要编译成.class文件,并且需要加载到JVM中才能使用,当使用某个类的时候如果该类未被加载则JVM会进行加载,这个过程被称为类的加载过程。

类加载过程.jpg
一 类加载过程

1.加载
加载是把class文件读取到内存中,并创建一个java.lang.Class对象,因为我们使用任何类时都要为之创建一个java.lang.Class对象,类的加载都是由类加载器完成的,JVM为之提供了类加载器,这些类加载器是程序运行的基础,JVM提供的加载器统称为系统类加载器,我们自己也可以通过继承ClassLoader创建自己的类加载器。
通过不同的类加载器,可以从不同来源加载类的二进制数据。

  • 从磁盘加载class文件,这是绝大部分程序的加载方式
  • 从zip或jar包加载class文件,如数据库驱动类的加载
  • 通过网络加载class文件
  • 动态编译java文件,并执行加载
    在JVM Java虚拟机规范中允许对类的预先加载,不是每当第一次使用该类的时候才能加载

2.链接
类被加载完成将会进入到链接阶段,链接阶段又分为三个阶段

2.1验证阶段: 这个阶段会检查被加载的类内部结构是否正确。验证阶段是非常重要的一个阶段,他能保证应用程序不会被恶意侵入,目的是要保证Class文件字节流中包含的信息符合当前虚拟机要求,不会影响到虚拟机自身的安全。

  • 文件格式验证: 验证字节流是否符合Class文件格式的规范,例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型或指向常量池中常量的索引值是否存没有的常量或不符合类型的常量
  • 元数据验证:对字节码描述的信息进行语义的分析,检查是否符合java的语法规范,以保证其描述的信息符合Java语言规范,例如:这个类是否有父类等(java.lang.Object除外)
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的符合逻辑的,主要针对方法体的验证,保证类方法运行时不会产生危害
  • 符号引用验证确保解析动作能正确执行,主要检查确定访问类型等涉及到引用的情况,要保证引用一定会被访问到,不会出现类等无法访问到的情况

验证阶段非常重要的但不是必须的,它对程序运行期没有影响,如果所引用的类会经过反复验证时则可以考虑使用-Xverifynone这个参数关闭大部分的类验证措施,加快虚拟机的类加载时间

2.2准备阶段:类准备阶段会为类的静态变量分配内存(方法区中),并设置默认初始值,这时候进行内存分配的仅包括类中static的变量,不包括实例变量,实例变量是在对象实例化时随着对象一起在堆中分配,这时所设置的初始值通常情况下是数据类型默认的零值(如0,0L,null,false),而不是在java代码中被显示赋予的值如:

static int num = 3;
//这是num在准备阶段之后的初始值=0,而不是=3  
原因是这个时候并没有执行Java的任何方法  
而把null赋值为3的putstatic指令是在编译之后存放于类构造器()方法之中  
所以把num赋值为3的动作将在初始化阶段才会执行
这里还需要注意如下几点:
1.基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
2.对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过,而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
3.对于引用数据类型reference来说,如数组引用,对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的为null
4.如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值
5.如果类字段属性表中存在ConstantValue属性,即被final和static同时修饰,如:static final int num = 3;那么javac编译时会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将nul赋值为3,我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中

2.3 解析阶段:将类的二进制数据中的符号引用替换成直接引用,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行,布局和内存无关。直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄,该引用是和内存中的布局有关的,并且一定加载进来的。
3.初始化: 初始化是为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要是对类的变量进行初始化,如直接声明变量是指定初始值或使用静态代码块为类变量指定初始值
JVM初始化步骤
1.假如这个类还没有被加载和连接,则程序先加载并连接该类
2.假如该类的直接父类还没有被初始化,则先初始化其直接父类
3.假如类中有初始化语句,则系统依次执行这些初始化语句

二 类加载时机

只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

  • 创建类的实例,也就是new的方式
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射Class.forName("com.mysql.cj.jdbc.Driver")
  • 初始化一个类的子类则其父类也将初始化
  • JVM启动时标明的启动类,直接使用java.exe命令来运行某个主类

对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。

结束生命周期: 在如下几种情况下,Java虚拟机将结束生命周期

  • 执行了System.exit()方法
  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误而异常终止
  • 由于操作系统出现错误而导致Java虚拟机进程终止
三 类加载器

类加载器负责加载所有需要使用的类,并为所有被载入内存中的类生成一个java.lang.Class实例对象,一个类只能被加载载入一次,正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识,在Java中,一个类用其全限定类名如(com.test.A)作为标识,而在JVM中一个类用其全限定类名和其类加载器作为其唯一标识,如:在com.test包中有一个名为Test的类,被类加载器ClassLoader的实例MyClassLoader负责加载,则该类对应的Class对象在JVM中表示为(Test.com.test.MyClassLoader)这意味着两个类加载器加载的同名类:(Test.com.test.MyClassLoader1)和(Test.com.test.MyClassLoader2)是不同的,它们所加载的类也是完全不同互不兼容
类加载器分为两种:第一种是Java虚拟机自带的类加载器,分别为启动类加载器,扩展类加载器和系统类加载器,第二种使用户自定义的类加载器,是java.lang.ClassLoader的子类实例
虚拟机内置三个类加载器,当一个 JVM启动的时候便开始使用:
1.根类加载器(Bootstrap)

  • 根类加载器是最底层的类加载器,是虚拟机的一部分,他是C++实现的,且没有父类加载器,也没有继承ClassLoader类,主要负责加载系统属性"sun.boot.class.path"指定路径下的核心类即(/jre/lib)目录,出于安全考虑根加载器只加载java,javax,sun开头的类
public class ClassLoaderDemo {
    public static void main(String[] args) {
        ClassLoader classLoader = Object.class.getClassLoader();
        //可以看出Object的加载器=null 因为是Bootstrap ClassLoader 此加载器是c++语言实现的
        //是根加载器一个比较特殊的加载器
        System.out.println(classLoader);//输出null
    }
}

2.扩展类加载器(ExtClassLoader)

  • 扩展类加载器是原sun公司实现的sun.misc.LauncherPlatformClassLoader),他是java语言编写的,父加载器是根加载器(注意这个父子关系不是extends继承关系),扩展加载器主要负责加载/jre/lib/ext目录下的类库或者系统变量"java.ext.dirs"指定的目录下的类库
public class ClassLoaderDemo {
    public static void main(String[] args) {
        //扩展类加载器会加载/jre/lib/ext下的类库 这里ECDHKeyAgreement类在ext目录下的sunec.jar
        final ClassLoader classLoader = ECDHKeyAgreement.class.getClassLoader();
        //输出sun.misc.Launcher$ExtClassLoader@4769b07b
        //从输出可以看到ECDHKeyAgreement的类加载器 是扩展类加载器
        System.out.println(classLoader);
    }
}
//扩展类加载器源码,从源码可以看出扩展类加载器会加载java.ext.dirs指定目录下的类库
private static File[] getExtDirs() {
            String var0 = System.getProperty("java.ext.dirs");
            File[] var1;
            if (var0 != null) {
                StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
                int var3 = var2.countTokens();
                var1 = new File[var3];

                for(int var4 = 0; var4 < var3; ++var4) {
                    var1[var4] = new File(var2.nextToken());
                }
            } else {
                var1 = new File[0];
            }

            return var1;
        }

3.系统类加载器(AppClassLoader)

  • 系统类加载器也称为应用类加载器,也是java代码实现,是原sun公司的实现sum.misc.Launcher$AppClassLoader,父类加载器是扩展类加载器(这里父子非继承关系),它负责从classpath环境变量或者系统属性java.class.path所指定的目录中加载类,他是自定义加载器的默认附加载器,一般情况下该类加载器是程序默认的类加载器,可以通过ClassLoader.getSystemClassLoader()方法直接获取
public class ClassLoaderDemo {
    public static void main(String[] args) {
        //自己编写的类使用的类加载器
        final ClassLoader classLoader = ClassLoaderDemo.class.getClassLoader();
        //sun.misc.Launcher$AppClassLoader@18b4aac2
        //从输出可以看出自己编写的类使用的就是AppClassLoader加载器
        System.out.println(classLoader);
    }
}

在程序开发中,类加载几乎是由上述3种类加载器相互配合执行的,我们还可以自定义类加载器,但是需要注意java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用类时才会将它的class文件加载到内存中生成java.lang.Class对象,而且加载某个class文件的时候,虚拟机是采用双亲委派模式,即把类的加载请求交由父类加载器处理,它是一种任务委派模式
当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会去加载该类,因为缓存的存在也就导致了每次类的修改都要重启虚拟机

四 双亲委派

除了根加载器之外,其他的类加载器都需要️自己的父加载器,类加载是通过双亲委派机制,这种机制能够很好的保护java程序的安全,除虚拟机自带的根加载器之外,其余的类加载器都有唯一的父加载器。比如classloader加载一个类时,会先让自己的父加载器去加载这个类,若父加载器能够加载则由父加载器加载,否则才会自己加载这个类,每一个类加载器都很懒,都会先让父类加载去去尝试加载,一直到根加载器,加载不到时才会自己加载。真正加载类的加载器我们叫做启动类加载器,注意:双亲委派机制的父子关系并不是java中的继承,而是通过组合模式来复用父加载器代码,这种机制如下图:


类加载器双亲委派.jpg
public class ClassLoaderDemo {
    public static void main(String[] args) {
        ClassLoader classLoader = ClassLoaderDemo.class.getClassLoader();
        while (classLoader != null){
            //1.sun.misc.Launcher$AppClassLoader@18b4aac2
            //2.sun.misc.Launcher$ExtClassLoader@9807454
            //从输出可以看出 当前类的加载器是AppClassLoader 
            //然后ExtClassLoader是AppClassLoader的父类加载器
            //因为ExtClassLoader的类加载器是根类加载器是c++的实现 所以=null未输出 循环结束
            System.out.println(classLoader);
            classLoader = classLoader.getParent();//赋值为它的父类加载器
        }
    }
}

使用双亲委派机制的好处:
1.可以避免类的重复加载,当父类加载器已经加载了该类,就没有必要子classLoader再加载一次
2.考虑到安全因素,java核心api中定义的内容不能被随意替换,比如通过网络传递一个java.lang.Object的类,通过双亲委派模式传递到启动器加载器(根加载器),而启动类加载器在核心API发现了这个名字的类已经被加载,并不会重新加载网络传递过来的java.lang.Object,而是直接返回一加载过Object.class,这样便可防止核心API被随意篡改

ClassLoader
所有的类加载器(根加载器除外)都必须继承java.lang.ClassLoader,它是一个抽象类

//ClassLoader主要方法由四个
loadClass(); //自定义加载器 不要覆盖这个方法这个方法实现了双亲委派
findClass();//自定义加载器时覆盖的方法 
//用来将byte字节解析成虚拟机识别的class对象,此方法通常与findClass方法一起使用
//在自定义类加载器时,会直接覆盖ClassLoader的findClass方法获取要加载的字节码,然后调用defineClass方法生成Class对象
defineClass();
//链接指定类,类加载器可以使用此方法来链接类å
resolveClass();
protected Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查类是否已加载
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //首先使用自己的父类加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //没有父类加载器使用内置类加载器加载
                        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.
                    long t1 = System.nanoTime();
                    //自己加载查找类
                    c = findClass(name);

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

URLClassLoader使用

  • 在java.net包中,JDK提供了一个更加易用的类加载器URLClassLoader,它扩展了ClassLoader,能够从本地或者网络上的指定位置加载类,我们可以使用该类作为自定义加载器使用
//要加载的类文件
public class TestClassLoaderDemo {
    public TestClassLoaderDemo(){
        System.out.println("TestClassLoaderDemo");
    }
}

//本地文件加载
public class ClassLoaderDemo {
    public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException {
        //类文件在的项目文件夹
        File file = new File("/Users/qyq/MyCode/code-example");
        final URL url = file.toURI().toURL();
        //指定要加载类的url地址,父类加载器默认为系统类加载器
        final URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
        //输出 sun.misc.Launcher$AppClassLoader@18b4aac2 可以确认默认父加载器为系统加载器
        System.out.println(urlClassLoader.getParent());
        //指定要加载类的url地址,并且指定父类加载器
        //final URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url},Object.class.getClassLoader());
        //类文件的全称
        final Class aClass = urlClassLoader.loadClass("com.classloader.test.TestClassLoaderDemo");
        aClass.newInstance();//输出TestClassLoaderDemo 表示TestClassLoaderDemo的构造方法执行了 类已经成功加载
    }
}
//从网络加载类文件
public class ClassLoaderDemo {
    public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException {
        //网络地址
        URL url = new URL("http://localhost:8080/codeexample/");
        final URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
        //加载类的全类名
        final Class aClass = urlClassLoader.loadClass("com.classloader.test.TestClassLoaderDemo");
        aClass.newInstance();
    }
}

自定义类加载器

//自定义类加载器 加载磁盘上的类文件
//从网络加载类似 即本地io流为网络流这里不做example
public class MyClassLoader extends ClassLoader {

    private String folder;//被加载类所在的目录

    public MyClassLoader(final String folder) {
        this.folder = folder;
    }

    public MyClassLoader(final String folder, ClassLoader parent) {
        super(parent);
        this.folder = folder;
    }

    //覆盖掉findCLass方法
    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        //文件夹 + 文件分割符如(/) 加上全类名("."替换成文件分隔符) 加上一.class后缀
        //如folder= F:/code-example/ name=com.classloader.test.TestClassLoaderDemo
        //替换结果 filePath = F:/code-example/com/classloader/test/TestClassLoaderDemo.class
        String filePath = folder + File.separator + name.replace(".", File.separator) + ".class";
        try {
            FileInputStream fileInputStream = new FileInputStream(filePath);
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            byte[] b = new byte[1024];
            int len = -1;
            while ((len = fileInputStream.read(b)) != -1) {
                byteArrayOutputStream.write(b, 0, len);
            }
            byte[] data = byteArrayOutputStream.toByteArray();
            fileInputStream.close();
            byteArrayOutputStream.close();
            //name=类全名称, data读取到的二进制数据,从0到data.length
            return super.defineClass(name, data, 0, data.length);
        } catch (FileNotFoundException e) {
            //e.printStackTrace();
            throw new ClassNotFoundException();
        } catch (IOException e) {
            //e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        //使用方法
        MyClassLoader classLoader = new MyClassLoader("F:/code-example/");
        final Class aClass = classLoader.loadClass("com.classloader.test.TestClassLoaderDemo");
        aClass.newInstance();
    }
}

热部署类加载器

  • 当我们调用loadClass方法加载类时,会采用双亲委派模式,即如果类已经被加载,就会从缓存中获取,不会重复加载如果同一个class被同一个类加载器重复加载则会抛出一场,因此我们需要实现热部署让同一个class文件可以被不同的类加载器重复加载,但是不能调用loadClass方法,应该调用findClass方法,从而来避开双亲委派模式,从而实现不同一个类被多次加载,实现热部署,即上述自定义类加载器使用时调用findClass方法即可而不是调用loadClass

类的显示和隐式加载
类的加载方式是指虚拟机将class文件加载到内存中的方式
1.显示加载指的是java代码中通过调用ClassLoader加载的class对象,比如Class.forName(className)或this.getClass().getClassLoader().loadClass()使用类加载器加载
2.隐式加载是指不需要在java代码中明确调用加载的代码,而是通过虚拟机自动的加载到内存中来,比如加载某个class,该class引用了另外一类的对象,那么这个被引用的对象的字节码文件就会被虚拟机加载到内存中
线程上下文类加载器
Java中存在着很多的服务提供者接口SPI(SPI概念自行百度),是Java提供的一套用来被第三方实现或者扩展的API,这些接口一般情况下由第三方实现如JNDI,JDBC等,这些spi接口属于核心类库,一般存放在rt.jar包中,由根加载器负责加载,而第三方实现的代码一般都是作为依赖jar存放在classpath路径下,由于SPI接口中的代码需要加载具体的第三方实现类并调用其相关方法,SPI的接口类是由根加载器加载的,Bootstrap类加载器无法直接加载位于classpath下的具体实现类,由于双亲委派模式的存在Bootstrap类加载器也无法反向委托AppClassLoader加载SPI的具体实现,在这种情况下java提供了线程上下文类加载器用于解决上述问题
线程上下文类加载器可以通过java.lang.Thread的getContextClassLoader()来获取,或者通过setContextClassLoader(ClassLoader classLoader)来设置线程的上下文类加载器,如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类或者资源
显然这种记载方式破坏了双亲委托模式,但是它使得java类加载器变得更加灵活

Thread.currentThread().getContextClassLoader();

笔记记录文章 如有错误欢迎您指出

参考文章:
https://blog.csdn.net/m0_38075425/article/details/81627349
https://www.cnblogs.com/ityouknow/p/5603287.html

你可能感兴趣的:(Java类加载过程)