【JVM】- 类加载器

·  阅读 303

JVM - 类加载器

类加载做了什么:

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始最终形成可以袚虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

类加载的时机(什么时候做):

  1. 创建类的对象
new Demo();
复制代码
  1. 访问某个类或者接口的静态变量,或者对改静态变量赋值, 调用类的静态方法
1. Demo.GET_STATUS_VALUE = 2;
2. Integer i = Demo.GET_STATUS_VALUE
3. Integer i = Demo.getStatusValueMethod();
复制代码
  1. 反射
Demo.class.newInstance();
复制代码
  1. 初始化一个类的子类
public class Parent(){}
public class Children() extends Parent{}
public class Demo(){
    public static void main(String[] args) throws Exception {
        Demo demo = new Children();
        // 会加载Parent后加载Children
    }
}
复制代码
  1. java虚拟机启动时被表明为启动类
java[-options] Demo
复制代码
  1. JDK7 开始提供的动态语言支持java.long.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic 句并且对于没有初始化的类进行初始化==所有的java虚拟机实现必须在每个类或接口的java程序“首次主动使用”的才会加载==

类加载的过程(怎么做)

graph LR
加载-->连接
连接-->初始化
初始化-->使用
使用-->卸载

加载

加载过程中更主要完成以下三件事:

  1. 用过一个类的全限定名来获取定义此类的二进制名 (forname)
  2. ==将这个字节流所代表的静态存储结果转化为方法区的运行时数据结构==
  3. ==在内存中生成一个代表这个类的java.lang.class对象,作为这个方法区这个类的各种数据的访问入口==
  • 数组的加载:
    1. 如果数组的内部元素类型是引用类型,那就递归采基本方式加载这个组件类型,数组将在加载该组件类型的类加载器的类名称空间上被标识
    2. 如果数组的内部元素类型不是引用类型(例如int[]数组),Java虚拟机将会把数组标记为与==引导类加载器==关联。
    3. 数组类的可见性与内部元素类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为 public。
  • 加载阶段完成后, 虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区中。然后在方法区(元空间)中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的这些类型数据的外部接口 。

连接

验证 准备 解析

graph LR
验证-->准备
准备-->解析

验证

这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全

  1. 文本格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用

准备

这一阶段是为类变量分配内存并设置变量的初始值。这时候==进行内存分配的仅包括类变量(被static修饰的变量,在方法区中创建空间)==,而不包括实例变量,实例变量将会在对象实例化时随对象一起分配在Java堆中,==这里所说的初始值通常情况下是数据类型的零值==,例如:

public static int value=123;
复制代码
  • 那变量value在准备阶段过后的初始值为0而不是123。

数据基本类型的零值

数据类型零值数据类型零值
int0booleanflase
long0Lfloat0.0f
short(short)0double0.0d
char'\u0000'referrncenull
byte(byte)0
  • 如果变量被final修饰 那么变量value就会在准备阶段被赋予真正的初值例如:
public static final int value=123;
复制代码
  • 那变量value在准备阶段过后的初始值为123而不是0

初始化

会根据程序员通过程序制定的主观计划去初始化类变量和其他资源(赋予真正的初值),初始化阶段是执行类构造器clinit()方法的过程。

clinit()

是由编译器自动收集类中的所有类变量的赋值动作和静态语句块 (static{}块)中的语句合并产生的, 编译器收集的顺序是由语句在源文件中出现的 顺序所决定的, 静态语句块中只能访问到定义在静态语句块之前的变量, 定义在它 之后的变量, 在前面的静态语句块可以赋值, 但是不能访问。

  • 解释以上代码的demo:
public class Demo {
    static int i = 1;
    static final int i2 = 1;
    private static Demo demo = new Demo();
    static int j = 2;
    static final int j2 = 2;

    public static void main(String[] args) throws Exception {
        // 执行静态方法时加载这个类
        Demo.getInstance();
    }

    static Demo getInstance(){
        return demo;
    }

    private Demo() {
        System.out.println("非finnal初始化前:j = " + j + ", 非finnal初始化后:i = " + i );
        System.out.println("finnal初始化前:j2 = " + j2 + ",finnal初始化后:i2 = " + i2 );
    }
}

结果:

非finnal初始化前:j = 0, 非finnal初始化后:i = 1
finnal初始化前:j2 = 2,finnal初始化后:i2 = 1
复制代码

类的实例化(使用之一)

  • 为新的对象分配内存
  • 为实例变量赋予默认值
  • 为实例变量赋予正确的初始值
  • java编译器为他编译的每一个类都至少生成一个实例初始化方法,在java的class文件中,在这个实例初始化方法被称为“”。针对源代码中每一个类的构造方法。java编译器都产生一个“”方法。

JVM 的三种类加载器 (用什么做)

名称默认加载路径
根类加载器 (bootstrap classloader)<JAVA_HOME>\lib (rt.jar)
扩展类加载器 (ext classloader)<JAVA_HOME>\lib\ext (被java.ext.dir系统变量指定的路径)
应用类加载器 (app classloader)项目的$CLASSPATH

类加载器 是 通过一个类的全限定名例如(com.libra.one.test.java)来获取描述此类的二进制字节流

jvm中除了上面的三种类加载器 还有一种是自定义类加载器

BootstarpClassLoader (根类加载器)不是由java代码所编写。其他类加载器均由java代码所编写,并且全部都继承抽象类:java.lang.ClassLoader这个类。但是这些类都是又相互依赖关系的。默认的三大类加载器的依赖关系如下:

graph LR

应用类加载器-app -->|父亲| 扩展类加载器-ext

扩展类加载器-ext --> |父亲|跟类加载器-bootstrap

自定义类加载器的构造方法

    // 默认的父类加载器为应用类加载器
    public Classloader_Demo(String classLoaderName) {
        super(); //将系统类加载器仿作该类加载器的父加载器
        this.classLoaderName = classLoaderName;
    }
    
    // 传入ClassLoader类型的parent作为自定义类加载器的父类加载器。
    public Classloader_Demo(ClassLoader parent, String classLoaderName) {
        super(parent); //显示制定该类加载器的父加载器
        this.classLoaderName = classLoaderName;
    }
    
复制代码

类加载器双亲委托机制

双亲委派模型的工作过程是:

  1. 类加载器收到类加载的请求
  2. 把请求委派给父类加载器去完成,直至将请求传递到顶层的类加载器(bootstrap)中,
  3. 父类加载器反馈无法完成这个请求
  4. 子加载器尝试自己去加载。

流程图:

graph LR

类加载请求 --> 父类加载器

父类加载器 --> C{是否有父类加载器}

C --> |TURE| 父类加载器
C --> |FLASE| D{尝试加载}

D --> |TURE| 加载完成
D --> |FLASE| 转移到子加载器

转移到子加载器 --> D{尝试加载}

demo:

把 com.libra.demo.Demo 的class文件放置到 C:\Users\Administrator\Desktop\中

public class Demo3 {
    public static void main(String[] args) throws Exception {
        //创建自定义classLoader 不指定父类加载器(默认为AppClassLoader)
        Classloader_Demo loader1 = new Classloader_Demo("loader1");
        // path中存在com.libra.demo.Demo.class文件
        loader1.setPath("C:\\Users\\Administrator\\Desktop\\");
        Class clazz = loader1.loadClass("com.libra.demo.Demo");
        System.out.println("加载clazz类的classLoader为:"+clazz.getClassLoader());
    }
}

return: 
加载clazz类的classLoader为:sun.misc.Launcher$AppClassLoader@18b4aac2
复制代码

删除项目classpath下的com.libra.demo.Demo.class文件(即让AppClassLoader加载失败,加载权归还给自定义classLoader)

public class Demo3 {
    public static void main(String[] args) throws Exception {
        //创建自定义classLoader 不指定父类加载器(默认为AppClassLoader)
        Classloader_Demo loader1 = new Classloader_Demo("loader1");
        // path中存在com.libra.demo.Demo.class文件
        loader1.setPath("C:\\Users\\Administrator\\Desktop\\");
        Class clazz = loader1.loadClass("com.libra.demo.Demo");
        System.out.println("加载clazz类的classLoader为:"+clazz.getClassLoader());
    }
}

return:
你正在使用名字为:loader1的自定义加载器
加载clazz类的classLoader为:com.libra.demo.Classloader_Demo@7382f612
复制代码

类加载器的双亲委托模型的好处:

  • 可以确保java核心库类型安全。例如:所有的java应用会引用java.lang.object类,也就是说在运行期java.lang.object这个类会被加载到java虚拟机中,如果 这个加载过程是由java应用用自己的类加载器所完成的,那么很可能会在jvm中存在 多个版本的java.lang.object类,而且这些类时不兼容的相互不可见的。(正是命 名空间在发挥作用)
  • 借助于双亲委托机制,java核心库种类的加载工作都是由启动类加载器来同意完成,从而确保了java应用所使用的java核心类库,他们之间相互兼容,可以确保java核心库所提供的类不会被自定义的类所替代。
  • 不同的类加载器可以为相同名称(binayname)的类创建额外的命名空间,相同名称的类可以并存在java虚拟机中只需要用不同的类加载器来加载它们即可,不同类加载器所加载的类是不兼容的,这相当于在java虚拟机内部创建了
  • 一个又一个相互隔离的java类空间,这类技术在多框架中都得到了实际应用

实现双亲委托机制的代码

  • ==先检查是否已经被加载过, 若没有加载则调用父加载器的 loadClass() 力法, 若父加载器为空则默认使用启动类加载器作为父加载器。 如果父类加载失败, 抛出ClassNotFoundException 异常后, 再调用自己的 findClass() 方法进行加载。==
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) {
                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;
        }
    }
复制代码

破坏双亲委托机制

  • 当前类加载器 (Current ClassLoader):每一个类都会使用自己的类加载器(即加载自身的类加载器)来去加载其他类(指的是所依赖的类)如果 class X 引用 class Y 那么ClassX的类加载器就会去加载classY 前提是 classY未加载
  • 线程上下文类加载器(Context ClassLoader): 线程上下文类加载器是从jdk1.2开始引用的,类Thread中的getContextClassLoader()与setContextClassLoader()分别用来获取和设置上下文类加载器,如果没有通过setContextClassLoader进行设置的话,线程会继承其父线程的上下文加载器java应用运行时的初始线程的上下文加载器是系统类加载器,如果在应用程序全局都没有设置过的话,那么这个类加载器默认就是应用类加载器。在线程运行的代码,可以通过该类加载器来加载资源。
  • 线程类加载器的重要性: SPI(Service Provider Interface)父classloader可以使用当前线程(thead.getContextClassLoader)所制定的classloader加载的类,这就改变了父classloader不能使用子classloader或者其他没有直接父子关系的classloader加载的类的情况,即改变了双亲委托机制。
  • 线程上下文类加载器就是当前线程的Current ClassLoader

设置线程类加载器DMOE:

public class Demo3 {
    public static void main(String[] args) {
        System.out.println("当前线程的上下文加载器:"+Thread.currentThread().getContextClassLoader());
        System.out.println("加载线程类的加载器:"+Thread.class.getClassLoader());
        System.out.println("----------------------------------");
        Classloader_Demo classLoader = new Classloader_Demo("loader1");
        Thread.currentThread().setContextClassLoader(classLoader);
        Thread thread = new Thread();
        System.out.println("新线程thread的上下文加载器(继承父线程):"+thread.getContextClassLoader());
        thread.setContextClassLoader(ClassLoader.getSystemClassLoader());
        System.out.println("新线程thread的上下文加载器:"+thread.getContextClassLoader());
    }
}

return :
当前线程的上下文加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
加载线程类的加载器:null
----------------------------------
新线程thread的上下文加载器(继承父线程):com.libra.education.Classloader_Demo@61064425
新线程thread的上下文加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
复制代码
  • 线程上下类加载器的一般模式 (获取 - 使用 - 还原)
// 获取当前线程类加载器
ClassLoader classLoader = Thread.currentThread().getContextClassLoader()
   try{
       // 设置新的类加载器
       Thread.currentThread().setContestClassLoader(targrtTocl);
       myMethod();
   }finally{
       // 还原类加载器
       Thread.currentThread().setContextClassLoader(classLoader);
   }
复制代码
  • myMethod() 里面调用了Thread.currentThread().getContestClassLoader()获取了当前线程的类加载器做某些事情,如果一个类由类加载器A加载那么这个类的依赖类也是由相同的类加载器加载的(如果这个类的依赖类没有被加载过)ContextClassLoader的作用就是为了破坏java的类委托机制当高层提供了统一的接口让底层区实现,同时又在高层加载(或实例化)底层的类时,就必须要通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类

获取ClassLoader的途径

  • 获取当前类的ClassLoader: clazz.getClassLoader();
  • 获取当前线程上下文的ClassLoader:Tread.currentThread().getContextClassLoader;
  • 获取系统的ClassLoader:ClassLoader.getSystemClassLoader();
  • 获取调用者的ClassLoaderL DriverManager.getCallerClassLoader();

类加载器的命名空间

比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则, 即使这两个类来源于同一个Class文件,被同一个虚拟机加载,==只要加载它们的类加载器不同, 那这两个类就必定不相等==。

命名空间

  • 子加载器所加载的类能够访问到父加载器所加载的类
  • 父加在其所加载的类无法访问到子加载器所加载的类

不同类加载器的命名空间的关系:

  • 同一个命名空间内的类是相互可见的。

  • 子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器所加载的类能看到父加载器所加载的类。

  • 例如系统类加载器的类能看见跟类加载器加载的类

  • 由父加在其加载的类不能看见子加载器加载的类(上下文加载器除外)

  • 如果两个加载器之间没有直接或者间接的父子关系,那么他们各自加载的类相互不可见(上下文加载器除外)

  • 解释以上观点的DEMO:

删除项目classpath下的com.libra.demo.Demo.class文件(即让AppClassLoader加载失败,加载权归还给自定义classLoader)

public class Demo2 {
    public static void main(String[] args) throws Exception {
        // 创建2个加载路不同的classLoader 分别将 com.libra.demo.Demo.class 文件放在2个路径中
        // 但是要将项目classpath下的com.libra.demo.Demo.class文件删除(防止APP类加载器加载)
        Classloader_Demo loader1 = new Classloader_Demo("loader1");
        loader1.setPath("C:\\Users\\Administrator\\Desktop\\");
        Classloader_Demo loader2 = new Classloader_Demo("loader2");
        loader2.setPath("C:\\Users\\Administrator\\Desktop\\log\\");
        //分别用loader1 和 loader2 加载com.libra.demo.Demo
        Class clazz = loader1.loadClass("com.libra.demo.Demo");
        Class clazz2 = loader2.loadClass("com.libra.demo.Demo");
        //因为clazz 和 clazz2 的由不同类加载器加载所以 返回结果为false
        System.out.println(clazz.equals(clazz2));
    }
}

return:
你正在使用名字为:loader1的自定义加载器
你正在使用名字为:loader2的自定义加载器
false
复制代码

自定义classLoade

public class Classloader_Demo extends ClassLoader {
    private String classLoaderName;//加载器名称
    private String path;//加载路径

    private final static String fileExtension = ".class";//加载文件类型

    public Classloader_Demo(String classLoaderName) {
        super(); //将系统类加载器仿作该类加载器的父加载器
        this.classLoaderName = classLoaderName;
    }


    public Classloader_Demo(ClassLoader parent, String classLoaderName) {
        super(parent); //显示制定该类加载器的父加载器
        this.classLoaderName = classLoaderName;
    }

    @Override // 根据二进制名来找到需要加载的文件
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        System.out.println("你正在使用名字为:"+this.classLoaderName+"的自定义加载器");
        byte[] data = loaderClassData(name);
        return this.defineClass(name, data, 0, data.length);
    }

    //通过IO将文件读入Data中 (转为字节码)
    private byte[] loaderClassData(String name) {
        InputStream io = null;
        byte[] data = null;
        ByteArrayOutputStream baos = null;
        name = name.replace(".","\\");
        String s = this.path + name + this.fileExtension;
        try {
            io = new FileInputStream(new File(this.path+name+this.fileExtension));
            baos = new ByteArrayOutputStream();
            int ch = 0;
            while (-1 != (ch = io.read())) {
                baos.write(ch);
            }
            data = baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                io.close();
                baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        return data;
    }

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

}
复制代码
分类:
后端
标签:
分类:
后端
标签: