类加载和类加载器


类加载和类加载器

1. 类加载过程

1.加载

1.1 何为加载

加载类的二进制数据
类的加载指的是将类的.class文件中的二进制数据读到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个Class对象,此Class对象用来封装类在方法区内的数据结构。
(jvm规范并未说明Class对象位于哪里,HosSpot将其放在方法区中)

1.2 加载的方式

  1. 从本地磁盘中加载class二进制文件
  2. 通过网络加载class二进制文件
  3. 从zip,jar等归档文件中加载
  4. 从专有数据库中提取class文件
  5. 源代码动态编译为class文件

    1.3 加载的注意事项

  6. jvm允许类加载器在预料某个类将要被使用时就预先加载他,如果预先加载过程中遇到了class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误。
  7. 如果这个类一直没有被程序使用,那么类加载器就不会报告错误。

    2.连接

    1. 验证:确保类加载的正确性

  • 类文件的结构检查
  • 语义检查
  • 字节码验证
  • 二进制兼容性验证

    2. 准备:为类的静态变量分配内存(也是分配默认值的过程)

    3. 解析:将类中的符号引用转为直接引用

    3.初始化

    为类的静态变量赋正确的初始值

    3.1 初始化时机

    所有的Java虚拟机实现 必须在每个类或接口 被Java程序首次主动使用时 才初始化他们
    1 主动使用
    主动使用包含七种情况
  1. 创建类的实例
  2. 访问某个类或接口的静态变量,或对该静态变量赋值。
  3. 调用类的静态方法
  4. 反射 只有classForName .class 和getClass()都不会初始化
  5. 初始化一个类的子类(子类初始化父类也会初始化)
  6. Java虚拟机启动时被标明为启动类的类(如包含main方法)
  7. jdk1.7开始提供的动态语言支持
2.被动使用

不是主动使用的都为被动使用

3.2初始化步骤

  1. 类没有加载和连接,先进行加载和连接。
  2. 类存在直接父类,并且父类没有初始化,那么先初始化直接父类,
  3. 类中存在初始化语句,那就依次执行初始化语句。

初始化代码示例

  1. 子父类
    package jvmstu.classloader;
    
    public class ClassLoader01 {
        public static void main(String[] args) {
            //当打印str1时,根据主动使用时才会初始化得,Child.str1 使用的是父类的静态变量,不会初始化子类,子类静态代码块不会执行。
            //System.out.println(Child.str1);
            //当打印str1时,根据主动使用时才会初始化得,Child.str2 使用的是子类的静态变量,子类初始化父类也会初始化,子类和父类的静态代码块都会执行。
            System.out.println(Child.str2);
        }
    }
    
    class Papa{
        public static String str1="im Papa";
        static {
            System.out.println("Papa 初始化");
        }
    }
    
    class Child extends Papa {
        public static String str2="im Child";
        static {
            System.out.println("Child 初始化");
        }
    }
  2. 常量
    package jvmstu.classloader;
    
    public class ClassLoader02 {
        public static void main(String[] args) {
            System.out.println(Papa1.str1);
        }
    }
    
    class Papa1{
        //常量在 编译阶段 会存入 调用这个常量的方法所在的类的常量池中。
        //运行时,Papa1.str1 并没有引用到Papa1类,因此Papa1没有初始化。
        //甚至可以将编译好的Child1的class文件删除,也不会影响程序运行。
        public static final String str1="im Papa";
        static {
            System.out.println("Papa 初始化");
        }
    }
  3. 编译期常量和运行期常量
    package jvmstu.classloader;
    
    import java.util.UUID;
    
    public class ClassLoader03 {
        public static void main(String[] args) {
            System.out.println(Papa2.str1);
        }
    }
    
    class Papa2{
        //UUID.randomUUID().toString() 运行时才会知道是什么,不是编译器常量。
        //运行时常量在运行时会使用类的静态常量。
        //运行时,Papa2.str1 引用到Papa2类,因此Papa2初始化。
        public static final String str1= UUID.randomUUID().toString();
        static {
            System.out.println("Papa 初始化");
        }
    }
  4. 创建类的实例 new对象
    package jvmstu.classloader;
    
    import java.util.UUID;
    
    public class ClassLoader04 {
        public static void main(String[] args) {
            //创建类的实例,会首次主动使用类。
            Papa3 papa=new Papa3();
            System.out.println("====");
            Papa3 papa2=new Papa3();
        }
    }
    
    class Papa3{
        
        public static final String str1= UUID.randomUUID().toString();
        static {
            System.out.println("Papa 初始化");
        }
    }
  5. 接口的初始化
    package jvmstu.classloader;
    
    
    import java.util.UUID;
    
    public class ClassLoader05 {
        public static void main(String[] args) {
            //此处删掉编译好的class(不管是Papa5还是Child5)并不会出错
            //接口在初始化时,不一定要求父接口全部完成初始化
            //当真正使用父接口时,父接口才会初始化。
            System.out.println(Child5.str2);
        }
    }
    
    interface Papa5{
        public static  final String str1= UUID.randomUUID().toString();
    }
    interface Child5 extends Papa5{
        public static  String str2= "papa5 初始化";
    }
  6. 初始化前准备阶段
    package jvmstu.classloader;
    
    public class ClassLoader06 {
        public static void main(String[] args) {
            //当调用getInstance(静态方法)会进行类的初始化
            //初始化前准备阶段会为静态变量分配内存,同时赋予默认值。
            //初始化阶段,会从上到下执行初始化赋值。
            //new 对象会调用构造方法。
            Singleton singleton = Singleton.getInstance();
            System.out.println("con1: "+Singleton.con1);
            System.out.println("con2: "+Singleton.con2);
        }
    }
    
    
    class Singleton{
        public static int con1;
    
        //public static int con2=0;
        private static Singleton singleton=new Singleton();
    
        public static int con2=0;
    
        private Singleton(){
            con1++;
            con2++;
            System.out.println(con1);
            System.out.println(con2);
        }
        public static Singleton getInstance(){
            return singleton;
        }
    }
  7. 加载方式
    package jvmstu.classloader;
    
    /**
     * classLoader.loadClass 不会初始化类
     * Class.forName 会初始化化类
     */
    public class ClassLoader08 {
        public static void main(String[] args) throws ClassNotFoundException {
            ClassLoader classLoader=ClassLoader.getSystemClassLoader();
            Class<?> loadClass = classLoader.loadClass("jvmstu.classloader.Papa8");
            System.out.println(loadClass);
            System.out.println("==================");
            Class<?> aClass = Class.forName("jvmstu.classloader.Papa8");
            System.out.println(aClass);
    
        }
    }
    
    
    class Papa8{
        static int a=2;
        static {
            System.out.println("papa load init");
        }
    }

    2.类加载器

    类加载器有三种
    在虚拟机提供了3种类加载器,引导(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器)

    1.加载器类型

    1.1 根加载器

    启动(Bootstrap)类加载器
    启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 /lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。

    1.2 扩展(Extension)类加载器

    扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

    1.3 系统(System)类加载器

    也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

    2.加载器工作模式

双亲委派模式
双亲委派模式是在Java 1.2后引入的,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
双亲委派模式的好处

  1. 可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
  2. 其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

    3 自定义加载器

    用户可以编写自定义加载器,用于个性化的加载。

    3.1 如何编写一个自定义加载器

  3. 继承ClassLoader类
  4. 重写findClass方法和loadClassData方法
    1. findClass 调用 loadClassData 返回class
    2. loadClassData,加载二进制文件,是自定义的io操作,可以在网络或者各种介质中加载。实际上如何将二进制文件转换为类,是由native方法,也即jvm完成的。

自定义加载器代码示例
注意
因为类的双亲委托机制,在classpath下的类都会被父加载器加载,因此还是不会调用自定义的加载器加载。
将classpath下的ClassLoader01.class移动到自定义的path下,此时父加载器不能加载ClassLoader01,自定义加载器会开始加载。

package jvmstu.classloader;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;

/**
 * 自定义加载器
 */
public class ClassLoader09 extends ClassLoader{
    private String classLoaderName;
    private String path;
    private final String fileExtension=".class";

    //构造方法
    //系统类加载器作为父构造器
    public ClassLoader09(String classLoaderName){
        super();
        this.classLoaderName=classLoaderName;
    }
    //指定父构造器
    public ClassLoader09(ClassLoader parent,String classLoaderName){
        super(parent);
        this.classLoaderName=classLoaderName;
    }

    public void setPath(String path) {
        this.path = path;
    }

    @Override
    public String toString() {
        return "ClassLoader09{" +
                "classLoaderName='" + classLoaderName + '\'' +
                ", fileExtension='" + fileExtension + '\'' +
                '}';
    }
    //重写findClass 方法
    protected Class<?> findClass(String className){
        byte [] data=loadClassData(className);

        return this.defineClass(className,data,0,data.length);
    }
    //实现loadClassData 读取二进制文件。
    private byte [] loadClassData(String name){
        byte [] data=null;
        InputStream inputStream=null;
        ByteArrayOutputStream baos=null;

        try{
            String replace = name.replace(".", "/");
            System.out.println(replace);
            inputStream=new FileInputStream(path+replace+this.fileExtension);

            baos=new ByteArrayOutputStream();

            int ch=0;
            while(-1!=(ch=inputStream.read())){
                baos.write(ch);
            }
            data=baos.toByteArray();

        }catch (Exception ex){
            ex.printStackTrace();
        }finally {
            try{
                inputStream.close();
                baos.close();
            }catch (Exception ex){
                ex.printStackTrace();
            }
        }
        return data;
    }

    public static void main(String[] args) throws Exception {
        ClassLoader09 loader = new ClassLoader09("loader");
        loader.setPath("C:/Users/13166/Desktop/test/");
        Class<?> aClass = loader.loadClass("jvmstu.classloader.ClassLoader01");
        Object o = aClass.newInstance();
        System.out.println(o.getClass().getClassLoader());
    }
}

4.自定义系统类加载器

jvm允许将自定义的类加载器定义为系统(System)类加载器,只需添加jvm参数。(查看源码可见逻辑)

5.线程上下文类加载器

双亲委托模型下,类数据是由下到上加载的,对于spi(服务提供接口),java的核心库是由启动类加载器加载的,而这些接口的实现是由厂商实现的,由系统加载器加载。这样双亲加载模型就无法满足spi要求。
因此线程上下文类加载器就是解决这个问题的。父加载器加载的类,可以访问当前线程上下文类加载器加载的类,解决了父加载器加载的类不能访问子加载器加载的类的问题。

6.类加载器的命名空间

6.1 何为命名空间

  1. 每个类加载器都有自己的命名空间,命名空间由该加载器和该加载器的父加载器所加载的类组成
  2. 同一命名空间中,不会出现类的完整名字相同的两个类。
  3. 不同命名空间中,可以出现类的完整名字相同的两个类。

注意事项

  1. 子加载器所加载的类可以通过“引用”加载访问父加载器加载的类。(双亲委托)
  2. 父加载器所加载的类不可以通过“引用”加载本该由子加载器加载的类
  3. 没有关系加载器所加载的类是互相不可见的

    6.2 类加载器和命名空间的关系

    在双亲委托模型下
  4. 同一个命名空间的类是相互可见的
  5. 子加载器的命名空间包含所有父加载器的命名空间,因此子加载器加载的类是可以看见父加载器加载的类,父加载器加载的类则不可看见子加载器加载的类。
  6. 如果两个加载器之间没有直接或间接的关系,则他们各自加载的类相互不可见。

    3.类的使用和卸载

    使用 加载连接和初始化的过程
    卸载 内存中销毁类
    一个类在何时结束生命周期,取决于类的class文件何时被销毁
    java自带的类加载器会始终引用他们加载的类,因此不会卸载。自定义的加载器加载的类可以被卸载。

Author: 向天歌
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source 向天歌 !
  TOC