文章目录
类加载机制
类加载过程
类的加载触发条件
- 
一般在这些情况下,如果类没有被加载,那么会被自动加载: - 
使用new关键字创建对象时 
- 
使用某个类的静态成员(包括方法和字段)的时候 - 当然,final类型的静态字段有可能在编译的时候被放到了当前类的常量池中,这种情况下是不会触发自动加载的
- 在编译阶段自动替换
 
 
- 当然,final类型的静态字段有可能在编译的时候被放到了当前类的常量池中,这种情况下是不会触发自动加载的
- 
使用反射对类信息进行获取的时候 
- 
加载一个类的子类时 
- 
加载接口的实现类,且接口带有 default的方法默认实现时
 
- 
比如这种情况,那么需要用到另一个类中的成员字段,所以就必须将另一个类加载之后才能访问:
            
            
              java
              
              
            
          
          public class Main {
    public static void main(String[] args) {
        System.out.println(Test.str);
    }
    public static class Test{
        static {
            System.out.println("我被初始化了!");
        }
        public static String str = "都看到这里了,不给个三连+关注吗?";
    }
}这里我们就演示一个不太好理解的情况,我们现在将静态成员变量修改为final类型的:
            
            
              java
              
              
            
          
          public class Main {
    public static void main(String[] args) {
        System.out.println(Test.str);
    }
    public static class Test{
        static {
            System.out.println("我被初始化了!");
        }
        public final static String str = "都看到这里了,不给个三连+关注吗?";
    }
}可以看到,在主方法中,我们使用了Test类的静态成员变量,并且此静态成员变量是一个final类型的,也就是说不可能再发生改变。那么各位觉得,Test类会像上面一样被初始化吗?
按照正常逻辑来说,既然要用到其他类中的字段,那么肯定需要加载其他类,但是这里我们结果发现,并没有对Test类进行加载,那么这是为什么呢?我们来看看Main类编译之后的字节码指令就知道了:

很明显,这里使用的是ldc指令从常量池中将字符串取出并推向操作数栈顶,也就是说,在编译阶段,整个Test.str直接被替换为了对应的字符串(因为final不可能发生改变的,编译就会进行优化,直接来个字符串比你去加载类在获取快得多不是吗,反正结果都一样),所以说编译之后,实际上跟Test类半毛钱关系都没有了。
所以说,当你在某些情况下疑惑为什么类加载了或是没有加载时,可以从字节码指令的角度去进行分析
一般情况下,只要遇到new、getstatic、putstatic、invokestatic这些指令时,都会进行类加载,比如:

这里很明显,是一定会将Test类进行加载的。
类的详细加载流程
类的生命周期一共有7个阶段

- 
加载阶段 - 
加载阶段需要获取此类的二进制数据流 ,比如我们要从硬盘中读取一个class文件,那么就可以通过文件输入流来获取类文件的 byte[]- 也可以是其他各种途径获取类文件的输入流,甚至网络传输并加载一个类也不是不可以。
 
- 
然后交给类加载器进行加载 - 类加载器可以是JDK内置的,也可以是开发者自己撸的
- 类的所有信息会被加载到方法区中,并且在堆内存中会生成一个代表当前类的Class类对象
- 我们可以通过此对象以及反射机制来访问这个类的各种信息。
 
- 
数组类要稍微特殊一点,数组类型本身不会通过类加载器进行加载的 - 不过你既然要往里面丢对象进去,那最终依然是要加载类的。
 
 
- 
- 
验证阶段 - 
验证阶段相当于是对加载的类进行一次规范校验 - 因为一个类并不一定是由我们使用IDEA编译出来的,有可能是像我们之前那样直接用ASM框架写的一个
- 如果说类的任何地方不符合虚拟机规范,那么这个类是不会验证通过的,如果没有验证机制
- 那么一旦出现危害虚拟机的操作,整个程序会出现无法预料的后果。
 
 
- 因为一个类并不一定是由我们使用IDEA编译出来的,有可能是像我们之前那样直接用ASM框架写的一个
- 
验证阶段,首先是文件格式的验证: - 
是否魔数为CAFEBABE开头。 
- 
主、次版本号是否可以由当前Java虚拟机运行 
- 
Class文件各个部分的完整性如何。 
- 
... 
- 
有关类验证的详细过程,可以参考《深入理解Java虚拟机 第三版》268页。 
 
- 
 
- 
- 
准备阶段 - 
这个阶段会为类变量分配内存 
- 
并为一些字段设定初始值 - 注意是系统规定的初始值,不是我们手动指定的初始值。
 
 
- 
- 
解析阶段 - 
此阶段是将常量池内的符号引用替换为直接引用的过程 
- 
也就是说,到这个时候,所有引用变量的指向都是已经切切实实地指向了内存中的对象了。 
- 
到这里,链接过程就结束了 - 也就是说这个时候类基本上已经完成大部分内容的初始化了。
 
 
- 
- 
最后就是真正的初始化阶段了,从这里开始,类中的Java代码部分,才会开始执行 - 
<clinit>方法就是在这个时候执行的- 
这个是类在初始化时会调用的方法(是隐式的,自动生成的) 
- 
它主要是用于静态变量初始化语句和静态块的执行,因为我们这里给静态成员变量a赋值为10,所以会在一开始为其赋值:  
 
- 
 
- 
- 
全部完成之后,我们的类就算是加载完成了。 
类加载器
- 
Java提供了类加载器,以便我们自己可以更好地控制类加载 - 我们可以自定义类加载器,也可以使用官方自带的类加载器去加载类。
 
- 
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性。 - 也就是说,一个类可以由不同的类加载器加载
- 并且,不同的类加载器加载的出来的类,即使来自同一个Class文件,也是不同的
- 只有两个类来自同一个Class文件并且是由同一个类加载器加载的,才能判断为是同一个。
- 默认情况下,所有的类都是由JDK自带的类加载器进行加载。
 
比如,我们先创建一个Test类用于测试:
            
            
              java
              
              
            
          
          package com.test;
public class Test {
    
}接着我们自己实现一个ClassLoader来加载我们的Test类,同时使用官方默认的类加载器来加载:
            
            
              java
              
              
            
          
          public class Main {
    public static void main(String[] args) throws ReflectiveOperationException {
        Class<?> testClass1 = Main.class.getClassLoader().loadClass("com.test.Test");
        CustomClassLoader customClassLoader = new CustomClassLoader();
        Class<?> testClass2 = customClassLoader.loadClass("com.test.Test");
     	  //看看两个类的类加载器是不是同一个
        System.out.println(testClass1.getClassLoader());
        System.out.println(testClass2.getClassLoader());
				
      	//看看两个类是不是长得一模一样
        System.out.println(testClass1);
        System.out.println(testClass2);
      	//两个类是同一个吗?
        System.out.println(testClass1 == testClass2);
      
      	//能成功实现类型转换吗?
        Test test = (Test) testClass2.newInstance();
    }
    static class CustomClassLoader extends ClassLoader {
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            try (FileInputStream stream = new FileInputStream("./target/classes/"+name.replace(".", "/")+".class")){
                byte[] data = new byte[stream.available()];
                stream.read(data);
                if(data.length == 0) return super.loadClass(name);
                return defineClass(name, data, 0, data.length);
            } catch (IOException e) {
                return super.loadClass(name);
            }
        }
    }
}- 
结果: 
- 
通过结果我们发现,即使两个类是同一个Class文件加载的,只要类加载器不同,那么这两个类就是不同的两个类。 
- 
实际上,JDK内部提供的类加载器一共有三个 - 比如上面我们的Main类,其实是被AppClassLoader加载的,而JDK内部的类
- 都是由BootstrapClassLoader加载的,这其实就是为了实现双亲委派机制而做的。
 
双亲委派机制
- 类加载器
- 实际上类加载器就是用于加载一个类的,但是类加载器并不是只有一个。
 
**思考:**既然说Class对象和加载的类唯一对应,那如果我们手动创建一个与JDK包名一样,同时类名也保持一致,JVM会加载这个类吗?
            
            
              java
              
              
            
          
          package java.lang;
public class String {    //JDK提供的String类也是
    public static void main(String[] args) {
        System.out.println("我姓🐴,我叫🐴nb");
    }
}我们发现,会出现以下报错:
            
            
              java
              
              
            
          
          错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)但是我们明明在自己写的String类中定义了main方法啊,为什么会找不到此方法呢?实际上这是ClassLoader的双亲委派机制在保护Java程序的正常运行:

- 
实际上类最开始是由BootstarpClassLoader进行加载 - BootstarpClassLoader用于加载JDK提供的类
 
- 
而我们自己编写的类实际上是AppClassLoader加载的 - 只有BootstarpClassLoader都没有加载的类,才会让AppClassLoader来加载
 
- 
因此我们自己编写的同名包同名类不会被加载,而实际要去启动的是真正的String类,也就自然找不到 main方法了。javapublic class Main { public static void main(String[] args) { System.out.println(Main.class.getClassLoader()); //查看当前类的类加载器 System.out.println(Main.class.getClassLoader().getParent()); //父加载器 System.out.println(Main.class.getClassLoader().getParent().getParent()); //爷爷加载器 System.out.println(String.class.getClassLoader()); //String类的加载器 } } 
- 由于BootstarpClassLoader是C++编写的,我们在Java中是获取不到的。
 
手动将class文件加载到JVM
既然通过ClassLoader就可以加载类,那么我们可以自己手动将class文件加载到JVM中吗?先写好我们定义的类:
            
            
              java
              
              
            
          
          package com.test;
public class Test {
    public String text;
    public void test(String str){
        System.out.println(text+" > 我是测试方法!"+str);
    }
}通过javac命令,手动编译一个.class文件:
            
            
              java
              
              
            
          
          nagocoler@NagodeMacBook-Pro HelloWorld % javac src/main/java/com/test/Test.java编译后,得到一个class文件,我们把它放到根目录下,然后编写一个我们自己的ClassLoader,因为普通的ClassLoader无法加载二进制文件,因此我们编写一个自定义的来让它支持:
            
            
              java
              
              
            
          
          //定义一个自己的ClassLoader
static class MyClassLoader extends ClassLoader{
    public Class<?> defineClass(String name, byte[] b){
        return defineClass(name, b, 0, b.length);   //调用protected方法,支持载入外部class文件
    }
}
public static void main(String[] args) throws IOException {
    MyClassLoader classLoader = new MyClassLoader();
    FileInputStream stream = new FileInputStream("Test.class");
    byte[] bytes = new byte[stream.available()];
    stream.read(bytes);
    Class<?> clazz = classLoader.defineClass("com.test.Test", bytes);   //类名必须和我们定义的保持一致
    System.out.println(clazz.getName());   //成功加载外部class文件
}现在,我们就将此class文件读取并解析为Class了,现在我们就可以对此类进行操作了(注意,我们无法在代码中直接使用此类型,因为它是我们直接加载的),我们来试试看创建一个此类的对象并调用其方法:
            
            
              java
              
              
            
          
          try {
    Object obj = clazz.newInstance();
    Method method = clazz.getMethod("test", String.class);   //获取我们定义的test(String str)方法
    method.invoke(obj, "哥们这瓜多少钱一斤?");
}catch (Exception e){
    e.printStackTrace();
}我们来试试看修改成员字段之后,再来调用此方法:
            
            
              java
              
              
            
          
          try {
    Object obj = clazz.newInstance();
    Field field = clazz.getField("text");   //获取成员变量 String text;
    field.set(obj, "华强");
    Method method = clazz.getMethod("test", String.class);   //获取我们定义的test(String str)方法
    method.invoke(obj, "哥们这瓜多少钱一斤?");
}catch (Exception e){
    e.printStackTrace();
}探讨Tomcat类加载机制
- Tomcat服务器既然要同时运行多个Web应用程序
- 那么就必须要实现不同应用程序之间的隔离
- 也就是说,Tomcat需要分别去加载不同应用程序的类以及依赖
- 还必须保证应用程序之间的类无法相互访问,而传统的类加载机制无法做到这一点,同时每个应用程序都有自己的依赖
 
- 如果两个应用程序使用了同一个版本的同一个依赖,那么还有必要去重新加载吗,带着诸多问题,Tomcat服务器编写了一套自己的类加载机制。
 
- 那么就必须要实现不同应用程序之间的隔离

- 
首先我们要知道,Tomcat本身也是一个Java程序 - 
它要做的是去动态加载我们编写的Web应用程序中的类 - 
而要解决以上提到的一些问题,就出现了几个新的类加载器,我们来看看各个加载器的不同之处: - 
Common ClassLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Web应用程序访问。 
- 
Catalina ClassLoader:Tomcat容器私有的类加载器 ,加载路径中的class对于Web应用程序不可见。 
- 
Shared ClassLoader:各个Web应用程序共享的类加载器 ,加载路径中的class对于所有Web应用程序可见,但是对于Tomcat容器不可见。 
- 
Webapp ClassLoader:各个Web应用程序私有的类加载器,加载路径中的class只对当前Web应用程序可见,每个Web应用程序都有一个自己的类加载器,此加载器可能存在多个实例。 
- 
JasperLoader:JSP类加载器,每个JSP文件都有一个自己的类加载器,也就是说,此加载器可能会存在多个实例。 
 
- 
 
- 
 
- 
- 
通过这样进行划分,就很好地解决了我们上面所提到的问题 - 但是我们发现,这样的类加载机制,破坏了JDK的双亲委派机制(在JavaSE阶段讲解过 )- 比如Webapp ClassLoader,它只加载自己的class文件,它没有将类交给父类加载器进行加载
- 也就是说,我们可以随意创建和JDK同包同名的类,岂不是就出问题了?
- 实际上,WebAppClassLoader的加载机制是这样的:
- WebAppClassLoader 加载类的时候,绕开了 AppClassLoader
- 直接先使用 ExtClassLoader 来加载类。
 
- 这样的话,如果定义了同包同名的类,就不会被加载
- 而如果是自己定义 的类,由于该类并不是JDK内部或是扩展类,所有不会被加载,而是再次回到WebAppClassLoader进行加载
- 如果还失败,再使用AppClassloader进行加载。
 
- WebAppClassLoader 加载类的时候,绕开了 AppClassLoader
 
 
- 但是我们发现,这样的类加载机制,破坏了JDK的

