• 类加载过程


    一、类的生命周期

    类被加载到jvm虚拟机内存开始,到卸载出内存为止,他的生命周期可以分为:加载->验证->准备->解析->初始化->使用->卸载。

    其中验证、准备、解析统一称为链接阶段

    1、加载

      将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:

        _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用

        _super 即父类

        _fields 即成员变量

        _methods 即方法

        _constants 即常量池

        _class_loader 即类加载器

        _vtable 虚方法表

        _itable 接口方法表

      如果这个类还有父类没有加载,先加载父类 加载和链接可能是交替运行的

    2、链接

    2.1验证

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

      1)文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
      2)元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
      3)字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
      4)符号引用验证:确保解析动作能正确执行。

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

    2.2准备

      当完成字节码文件的校验之后,JVM 便会开始为类变量分配内存并初始化。这里需要注意两个关键点,即内存分配的对象以及初始化的类型。

    内存分配的对象。Java 中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。

    在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。

    例如下面的代码在准备阶段,只会为 factor 属性分配内存,而不会为 website 属性分配内存。

    public static int factor = 3;
    public String website = "www.baidu.com";

    初始化的类型。在准备阶段,JVM 会为类变量分配内存,并为其初始化。但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。
    例如下面的代码在准备阶段之后,sector 的值将是 0,而不是 3。

    public static int sector = 3;

    但如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。例如下面的代码在准备阶段之后,number 的值将是 3,而不是 0。

    public static final int number = 3;

    2.3解析

      解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

    符号引用:简单的理解就是字符串,比如引用一个类,java.util.ArrayList 这就是一个符号引用,字符串引用的对象不一定被加载。
    直接引用:指针或者地址偏移量。引用对象一定在内存(已经加载)。

    3、初始化

      初始化,这个阶段就是执行类构造器< clinit >()方法的过程,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。

    在Java中对类变量进行初始值设定有两种方式:

      声明类变量是指定初始值
      使用静态代码块为类变量指定初始值
    JVM初始化步骤
      1)假如这个类还没有被加载和连接,则程序先加载并连接该类
      2)假如该类的直接父类还没有被初始化,则先初始化其直接父类
      3)假如类中有初始化语句,则系统依次执行这些初始化语句
    类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
      1)创建类的实例,也就是new的方式
      2)访问某个类或接口的静态变量,或者对该静态变量赋值
      3)调用类的静态方法
      4)反射(如Class.forName(“com.shengsiyuan.Test”))
      5)初始化某个类的子类,则其父类也会被初始化
      6)Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类

    不会导致类初始化的情况
      1)访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
      2)类对象.class 不会触发初始化
      3)创建该类的数组不会触发初始化

    4、使用

      当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。

    5、卸载

      当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。

    二、类加载器

    1、类加载器分类

      1)启动类加载器:Bootstrap ClassLoader,负责加载存放在JDKjrelib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
      2)扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDKjrelibext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。
      3)应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

    应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。

    2、双亲委派

     类加载器加载类的源码

        protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // 1. 检查该类是否已经加载
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    long t0 = System.nanoTime();
                    try {
                        if (parent != null) {
                            // 2. 有上级的话,委派上级 loadClass
                            c = parent.loadClass(name, false);
                        } else {
                            // 3. 如果没有上级了(ExtClassLoader),则委派BootstrapClassLoader
                            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();
                        // 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
                        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;
            }
        }

     从图中我们发现除启动类加载器外,每个加载器都有父的类加载器。
    双亲委派机制:如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回;

    只有父类加载器无法完成此加载任务时,才自己去加载。

    从类的继承关系来看,ExtClassLoader和AppClassLoader都是继承URLClassLoader,都是ClassLoader的子类。而BootStrapClassLoader是有C写的,不再java的ClassLoader子类中。
    从图中可以看到类加载器间的父子关系不是以继承的方式实现的,而是以组合关系的方式来复用父加载器的代码。

    如果一个类加载器收到了类加载的请求,它首先会把这个请求委派给父加载器去完成,每一个层次的类加载器都是如此。

    双亲委派模型的好处
      Java类随着加载它的类加载器一起具备了一种带有优先级的层次关系。比如,Java中的Object类,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object在各种类加载环境中都是同一个类。如果不采用双亲委派模型,那么由各个类加载器自己取加载的话,那么系统中会存在多种不同的Object类。

    3、打破双亲委派

      3.1 自定义加载器重写loadClass()方法,具体可以参考https://www.cnblogs.com/ITPower/p/13211490.html

      3.2线程上下文类加载器(利用了java的SPI机制)

    这里以JDBC为例来讲解,我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写Class.forName("com.mysql.jdbc.Driver"),也是可以让 com.mysql.jdbc.Driver 正确加载,那是怎么做到的呢,我们看一下源码

    public class DriverManager {
    
        // 注册驱动的集合
        private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
        
        // 初始化驱动
        static {
            loadInitialDrivers();
            println("JDBC DriverManager initialized");
        }
    }

    我们手动输出一下DriverManager的类加载器

    System.out.println(DriverManager.class.getClassLoader());

    打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但 JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,

    这样问题来了,在 DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢

    继续看 loadInitialDrivers() 方法:

        private static void loadInitialDrivers() {
            String drivers;
            try {
                drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                    public String run() {
                        return System.getProperty("jdbc.drivers");
                    }
                });
            } catch (Exception ex) {
                drivers = null;
            }
            // If the driver is packaged as a Service Provider, load it.
            // Get all the drivers through the classloader
            // exposed as a java.sql.Driver.class service.
            // ServiceLoader.load() replaces the sun.misc.Providers()
    
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    1)使用 ServiceLoader 机制加载驱动,即 SPI
                    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    
                    /* Load these drivers, so that they can be instantiated.
                     * It may be the case that the driver class may not be there
                     * i.e. there may be a packaged driver with the service class
                     * as implementation of java.sql.Driver but the actual class
                     * may be missing. In that case a java.util.ServiceConfigurationError
                     * will be thrown at runtime by the VM trying to locate
                     * and load the service.
                     *
                     * Adding a try catch block to catch those runtime errors
                     * if driver not available in classpath but it's
                     * packaged as service and that service is there in classpath.
                     */
                    try{
                        while(driversIterator.hasNext()) {
                            driversIterator.next();
                        }
                    } catch(Throwable t) {
                    // Do nothing
                    }
                    return null;
                }
            });
    
            println("DriverManager.initialize: jdbc.drivers = " + drivers);
            // 2)使用 jdbc.drivers 定义的驱动名加载驱动
            if (drivers == null || drivers.equals("")) {
                return;
            }
            String[] driversList = drivers.split(":");
            println("number of Drivers:" + driversList.length);
            for (String aDriver : driversList) {
                try {
                    println("DriverManager.Initialize: loading " + aDriver);
                    Class.forName(aDriver, true,
                            ClassLoader.getSystemClassLoader());
                } catch (Exception ex) {
                    println("DriverManager.Initialize: load failed: " + ex);
                }
            }
        }

    先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载
    再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称

    这样就可以使用ServiceLoader来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:

      JDBC

      Servlet 初始化器

      Spring 容器

      Dubbo(对 SPI 进行了扩展)

    接着看 ServiceLoader.load 方法:

    public static <S> ServiceLoader<S> load(Class<S> service) {
      // 获取线程上下文类加载器
      ClassLoader cl = Thread.currentThread().getContextClassLoader();
      return ServiceLoader.load(service, cl);
    }

    线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由 Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类 LazyIterator 中:

            private S nextService() {
                if (!hasNextService())
                    throw new NoSuchElementException();
                String cn = nextName;
                nextName = null;
                Class<?> c = null;
                try {
                    c = Class.forName(cn, false, loader);
                } catch (ClassNotFoundException x) {
                    fail(service,
                         "Provider " + cn + " not found");
                }
                if (!service.isAssignableFrom(c)) {
                    fail(service,
                         "Provider " + cn  + " not a subtype");
                }
                try {
                    S p = service.cast(c.newInstance());
                    providers.put(cn, p);
                    return p;
                } catch (Throwable x) {
                    fail(service,
                         "Provider " + cn + " could not be instantiated",
                         x);
                }
                throw new Error();          // This cannot happen
            }

     4、自定义加载器

    问问自己,什么时候需要自定义类加载器

      1)想加载非 classpath 随意路径中的类文件

      2)都是通过接口来使用实现,希望解耦时,常用在框架设计

      3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

    步骤:1) 继承 ClassLoader 父类

       2)要遵从双亲委派机制,重写 findClass 方法,注意不是重写 loadClass 方法,否则不会走双亲委派机制

       3)读取类文件的字节码

       4)调用父类的 defineClass 方法来加载类

       5)使用者调用该类加载器的 loadClass 方法

  • 相关阅读:
    【原创】自己动手写工具----签到器[Beta 1.0]
    都2020了,还不好好学学泛型?
    ThreadLocal = 本地线程?
    从BWM生产学习工厂模式
    你还在用BeanUtils进行对象属性拷贝?
    JDK 1.8 之 Map.merge()
    Spring Boot认证:整合Jwt
    以商品超卖为例讲解Redis分布式锁
    如何从 if-else 的参数校验中解放出来?
    分布式全局唯一ID生成策略​
  • 原文地址:https://www.cnblogs.com/sglx/p/15262962.html
Copyright © 2020-2023  润新知