类加载

3 篇文章 0 订阅
3 篇文章 0 订阅

1 类加载机制,不是类加载几个过程

1.1JVM类加载机制

虚拟机把描述类的数据从Class文件 加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型 , 这就是虚拟机的类加载机制。
*Class文件由类装载器装载后,在JVM中将形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class的结构信息:如构造函数,属性和方法等,Java允许用户借由这个Class相关的元信息对象间接调用Class对象的功能,这里就是我们经常能见到的Class类。

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading) 7个阶段。其中验证、准备、解析3个部分统称为连接(Linking)。
类加载机制

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。注意,这里笔者写的是按部就班地“开始”,而不是按部就班地“进行”或“完成”,强调这点是因为这些阶段通常都是互相交叉地混合式进行的,通常会在一个阶段执行的过程中调用、激活另外一个阶段。
  类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。 这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

1.1.1 加载

加载指的是把class字节码(不只从Class文件中获取,还可以从Jar包、从网络中获取)文件从各个来源通过类加载器装载入JVM内存中。
在加载阶段,JVM需要完成3件事:

  1. 通过一个类的全限定名称来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

1.1.2 验证

验证的目的是为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。
验证阶段会完成以下4个阶段的检验动作:

  1. 文件格式验证
  2. 元数据验证(是否符合Java语言规范)
  3. 字节码验证(确定程序语义合法,符合逻辑)
  4. 符号引用验证(确保下一步的解析能正常执行)

1.1.3 准备

为静态变量在方法区分配内存,并设置默认初始值。

1.1.4 解析

虚拟机将常量池内的符号引用替换为直接引用的过程

1.1.5 初始化

初始化阶段是类加载过程的最后一步,主要是根据程序中的赋值语句主动为类变量赋值。
注:
1)当有父类且父类为初始化的时候,先去初始化父类;
2)再进行子类初始化语句。

1.1.6 什么时候需要对类进行初始化?

  1. 使用new该类实例化对象的时候;
  2. 读取或设置类静态字段的时候(但被final修饰的字段,在编译器时就被放入常量池的静态字段除外static final);
  3. 调用类静态方法的时候;
  4. 使用反射Class.forName(“xxxx”)对类进行反射调用的时候,该类需要初始化;
  5. 初始化一个类的时候,有父类,先初始化父类(注:1. 接口除外,父接口在调用的时候才会被初始化;2.子类引用父类静态字段,只会引发父类初始化);
  6. 被标明为启动类的类(即包含main()方法的类)要初始化;
  7. 当使用JDK1.7的动态语言支持时,如果一个java.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

以上情况称为对一个类进行主动引用,且有且只要以上几种情况需要对类进行初始化。

1.2 双亲委派模式

从Java虚拟机的角度来讲,只存在两种不同的类加载器:

一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现(只限于HotSpot),是虚拟机自身的一部分;
另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。

1.2.1 什么是双亲委派机制?

当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

1.2.2 类加载器的类别

启动类加载器(Bootstrap ClassLoader): 这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可。
标准扩展类加载器(Extension ClassLoader): 这个加载器由ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
应用程序类加载器 (Application ClassLoader): 这个类加载器由App-ClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
  CustomClassLoader(用户自定义类加载器) :用户自定义的类加载器,可加载指定路径的class文件。
加载器之间的关系
图中展示的类加载器之间的这种层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。
  双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
  使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。
  双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中,代码清单,逻辑清晰易懂:先检查类是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

1.2.3 双亲委派机制的作用

  1. 防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
  2. 保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

1.2.4 为什么要打破双亲委派机制

例
如上图所示,在类路径下面有一个User.class文件,在d盘下面也有一个User.class文件,我想让我自定义的类加载器加载D盘下面的User.class文件,而不是ApplicationClassLoader加载类路径下的User.class文件。
如果正常双亲委派机制的话,只能是加载类路径下的User.class文件,因为是一级向一级委托,在一级一级向下寻找。
那么想加载D盘下面的User.class文件就需要打破双亲委派机制。

1.2.5 如何打破双亲委派机制

举一个简单的例子,我自定义了一个类加载器,并且重写了它的 findClass(String name)loadClass(String name, boolean resolve) 方法

package com.exercise;

import java.io.FileInputStream;
import java.lang.reflect.Method;

public class MyClassLoaderTest3 {

    static class MyClassLoader extends ClassLoader {

        private String classPath;

        public MyClassLoader(String classPath) {
            this.classPath = classPath;
        }

        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = getByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }

        private byte[] getByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
            int len = fis.available();
            byte[]data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }

        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();
                    // 不向上委托
                    /* // 注释的这一段是原ClassLoader里面的
                    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();
                        // startsWith 测试此字符串是否以指定的前缀开头
                        // 如果是以我定义的路径开头,那么就执行我重写的findClass方法,否则调用父类findClass方法
                        if (!name.startsWith("com.exercise")) {
                            c = this.getParent().loadClass(name);
                        } else {
                            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;
            }
        }

    }
	// 我在主函数中D盘路径下定义一个class文件,
    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        Class clazz = classLoader.loadClass("com.exercise.User");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("say", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader());
    }
}

User类,自己通过javac命令生成class文件

package com.exercise;

public class User {
    public void say() {
        System.out.println("1111111111");
    }
}

自定义User.class路径
路径

运行结果
运行结果

1.3 描述Java类加载器的工作原理及其组织结构

Java 类加载器的作用就是在运行时加载类。
  Java 类加载器基于三个机制:委托性、可见性和单一性。
  1、委托机制是指双亲委派模型。 当一个类加载和初始化的时候,类仅在有需要加载的时候被加载。假设你有一个应用需要的类叫作Abc.class,首先加载这个类的请求由Application 类加载器委托给它的父类加载器 Extension 类加载器,然后再委托给Bootstrap 类加载器。Bootstrap类加载器会先看看rt.jar 中有没有这个类,因为并没有这个类,所以这个请求又回到 Extension 类加载器,它会查看 jre/lib/ext 目录下有没有这个类,如果这个类被 Extension 类加载器找到了,那么它将被加载,而Application 类加载器不会加载这个类;而如果这个类没有被 Extension 类加载器找到,那么再由 Application 类加载器从 classpath中寻找,如果没找到,就会抛出异常。
双亲委托机制的优点就是能够提高软件系统的安全性。因为在此机制下,用户自定义的类加载器不可能加载本应该由父加载器加载的可靠类,从而防止不可靠的恶意代码代替由父类加载器加载的可靠代码。如java.lang.Object类总是由根类加载器加载的,其他任何用户自定义的类加载器都不可能加载含有恶意代码的 java.lang.Object 类。
  2、可见性原理是子类的加载器可以看见所有的父类加载器加载的类,而父类加载器看不到子类加载器加载的类
  3、单一性原理是指仅加载一个类一次,这是由委托机制确保子类加载器不会再次加载父类加载器加载过的类
  Java 的类加载器有三个,对应 Java 的三种类:
  BootstrapLoader // 负责加载系统类 (指的是内置类,像String)
  ExtClassLoader // 负责加载扩展类(就是继承类和实现类)
  AppClassLoader // 负责加载应用类(程序员自定义的类)
  Java 提供了显式加载类的API:Class.forName(classname)。

https://blog.csdn.net/zengxiantao1994/article/details/89308439
https://blog.csdn.net/noaman_wgs/article/details/74489549

2 如果多个类加载器加载同一个类,会出现什么情况?

一个类,由不同的类加载器实例加载的话,会在方法区产生两个不同的类,彼此不可见,并且在堆中生成不同Class实例。
所有通过正常双亲委派模式的类加载器的classpath下的和ext下的所有类在方法区都是同一个类,堆中的Class实例也是同一个。

2.1 不同的类加载器

在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中,一个类用其全名 和 一个ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间。我们可以用两个自定义类加载器去加载某自定义类型(注意不要将自定义类型的字节码放置到系统路径或者扩展路径中,否则会被系统类加载器或扩展类加载器抢先加载),然后用获取到的两个Class实例进行java.lang.Object.equals(…)判断,将会得到不相等的结果,如下所示:

package com.exercise;

import java.io.FileInputStream;
import java.lang.reflect.Method;

public class MyClassLoaderTest3 {

    static class MyClassLoader extends ClassLoader {

        private String classPath;

        public MyClassLoader(String classPath) {
            this.classPath = classPath;
        }

        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = getByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }

        private byte[] getByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
            int len = fis.available();
            byte[]data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }

        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();
                        if (!name.startsWith("com.exercise")) {
                            c = this.getParent().loadClass(name);
                        } else {
                            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;
            }
        }

    }

    /**
     * 在主函数中,我定义了两个不同路径下不同的
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        Class clazz = classLoader.loadClass("com.exercise.User");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("say", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader());

        System.out.println();

        MyClassLoader classLoader1 = new MyClassLoader("D:/test");
        Class clazz1 = classLoader1.loadClass("com.exercise.User");
        Object obj1 = clazz1.newInstance();
        Method method1 = clazz1.getDeclaredMethod("say", null);
        method1.invoke(obj1, null);
        System.out.println(clazz1.getClassLoader());

        System.out.println(obj.equals(obj1));
        System.out.println(obj instanceof com.exercise.MyClassLoaderTest3);
    }

}

我们发现,objobj1 确实是类 com.exercise.MyClassLoaderTest3 实例化出来的对象,但当这个对象与类com.exercise.MyClassLoaderTest3做所属类型检查时却返回了false。这是因为虚拟机中存在了两个MyClassLoaderTest3类,分别由两个自定义的类加载器加载的,虽然它们来自同一个Class文件,但依然是两个独立的类,因此做所属类型检查时返回false。
结果

3 Tomcat的加载过程

Tomcat类加载过程
当Tomcat启动时,会创建几种类加载器:

  1. Bootstrap 引导类加载器
    加载JVM启动所需的类,以及标准扩展类(位于jre/lib/ext下)
  2. System 系统类加载器
    加载Tomcat启动的类,比如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定。位于CATALINA_HOME/bin下。
    bin目录
  3. Common 通用类加载器
    加载Tomcat使用以及应用通用的一些类,位于CATALINA_HOME/lib下,比如servlet-api.jar
    lib目录
  4. webapp 应用类加载器
    每个应用在部署后,都会创建一个唯一的类加载器。该类加载器会加载位于 WEB-INF/lib下的jar文件中的class 和 WEB-INF/classes下的class文件。

当应用需要到某个类时,则会按照下面的顺序进行类加载:
1. 使用bootstrap引导类加载器加载
2. 使用system系统类加载器加载
3. 使用应用类加载器在WEB-INF/classes中加载
4. 使用应用类加载器在WEB-INF/lib中加载
5. 使用common类加载器在CATALINA_HOME/lib中加载

3.1 为什么Java文件放在Eclipse中的src文件夹下会优先jar包中的class?

通过对上面Tomcat类加载机制的理解,就不难明白 为什么Java文件放在Eclipse中的src文件夹下会优先jar包中的class?
这是因为Eclipse中的src文件夹中的文件Java以及webContent中的JSP都会在Tomcat启动时,被编译成class文件放在 WEB-INF/class中。
而Eclipse外部引用的jar包,则相当于放在 WEB-INF/lib 中。
因此肯定是 Java文件或者JSP文件编译出的class优先加载。
通过这样,我们就可以简单的把Java文件放置在src文件夹中,通过对该Java文件的修改以及调试,便于学习拥有源码Java文件、却没有打包成xxx-source的jar包。
另外呢,开发者也会因为粗心而犯下面的错误。
在 CATALINA_HOME/lib 以及 WEB-INF/lib 中放置了 不同版本的jar包,此时就会导致某些情况下报加载不到类的错误。
还有如果多个应用使用同一jar包文件,当放置了多份,就可能导致 多个应用间 出现类加载不到的错误。

3.2 Tomcat这种类加载机制违背了java推荐的双亲委派模型了吗?

违背了
Tomcat是个web容器,一个web容器可能需要部署多个应用程序(多个war包),war包1使用的是Spring4,但是war包2中使用的是Spring5,实际开发中不能要求同一个类在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的且相互隔离。
Tomcat为了实现隔离性,没有遵守这个约定,每个WebappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委派机制。
注意:同一个JVM内,两个相同包名和类名的类对象可以共存,前提是他们的类加载器不一样,所以看两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类加载器也是同一个才能认为他们是同一个。

https://www.jb51.net/article/191338.htm

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值