JVM(七)类加载机制和类加载

上一个章节主要学习记录了 class 文件的文件结构以及字节码文件。

那么这一章节主要记录的是类的一个生命周期,研究 class 文件被加载到内存之后做了什么,有那些步骤,其次就是加载类的类加载器,以及其中的双亲委派机制,最后再了解下什么是OSGI。

一、类的生命周期

​ 一个类被加载到内存之后其实会有一些步骤来初始化类的一些信息,初始化完毕之后就是使用类,最后被 GC 所回收,其整个生命周期:加载(Loading)链接(验证、准备、解析)、 初始化(Initialization)、使用(Using)和卸载(Unloading)7 个阶段:

加载

​ 将 Class 文件加载到内存中。在虚拟机规范里面并没有强制约束加载的过程,具体是由虚拟机的实现来把控的。主要步骤为下:

​ 1)通过一个类的全限定名来获取类的二进制字节流(不是一定在 Class 中获取,可以从网络、数据库、压缩包中获取也行)。

​ 2)把字节流里面的数据转换为方法区的运行时数据结构。

​ 3)在内存里面生成一个对应类的class对象,以供其他数据的访问。

链接

​ 链接这步涉及的东西比较多,主要分为三个步骤:

  • 验证

    ​ 主要是校验 Class 文件是不是符合 JVM 的规范,大致可以分为四个阶段:文件格式校验、元数据验证、字节码验证、符号引用验证(步骤不是关键,只需要了解这步是校验的就行了)。

    文件格式校验:以二进制字节流法的方式进行校验,主要校验上节文章中所提到的 Class 文件结构(https://www.cnblogs.com/mouren/p/14529512.html)。

    元数据校验:这块的话校验的就是字节码描述信息的分析,看看是不是符合《Java 语言规范》的要求,例如:当前类是不是有父类(除 Object 外都应该有父类)。

    字节码验证:这块的校验是整个校验里面最麻烦的一个,主要校验字节码的指令是不是符合规范,这块是字节码的指令不是字节码。

    符号引用验证:这块就是校验这个类里面是不是有其他的引用是缺失的之类的。

    总结:这块的校验在类加载机制上来说是很重要的一个阶段,但是这步是可通过 JVM 参数上跳过这个步骤。 -Xverify:none

    JVM 参数官网直达:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

  • 准备

    ​ 给静态变量进行一个初始化的一个赋值(static 修饰),这块的内存都在方法区中分配。例如:

    public static int num = 2333;
    //这块初始化赋值是 0 不是 2333 的赋值。
    
  • 解析

    ​ 解析就是把 JVM 常量池里的符号引用替换为直接引用的过程。符号引用就是一开始还没进行对象内存分配的时候的一个占位符,而对象进行内存分配之后就有内存地址了,然后占位符换成对象的内存地址。

初始化

​ 主要是对一个class 中的静态块进行一个初始化(对应的字节码就是 clinit 方法)。当然也不是必须的如果类里面没有静态块也没有对变量赋值的操作,那么就不会生成 clint 方法。

初始化阶段,只有六种情况是必须对类进行初始化的,当前是在加载和验证之后。

1)遇到newgetstaticputstaicinvokestatic这四个字节码指令的时候,如果类还没有进行初始化的时候那么就会先进行对类的初始化,对应的 Java 代码场景:

​ 使用new关键字创建对象的时候。

​ 读取或者设置一个类的静态变量字段的时候。

​ 调用一个类的静态方法。

2)使用反射类对类进行反射的时候,如果没有初始化那么会进行类初始化。

3)初始化类的时候发现这个类的父类还没被初始化的时候,会对这个父类进行初始化先。

4)当指定一个拥有main函数运行的时候需要初始化这个类。

5)当使用 JDK 1.7 的动态语言支持时, 如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果EF_getStaticREF_putStaticREF_invokeStatic 的方法句柄, 并且这个方法句柄所对应的类没有进行过初始化, 则需要先触发其初始化。

6)接口拥有被 default 所修饰的方法时,如果实现该结构的类发生初始化的时候,那么这个接口肯定在之前就需要初始化。

二、类加载器

​ 类加载器就是加载上面三个主要步骤的工具。

JDK 中类加载器

  • Bootstrap ClassLoader(根加载器)

    ​ 最底层的类加载器,Java 代码无法获取到的类加载器,由 c/c++编写,主要加载核心类库,例如:rt.jar、resources.jar等等。-Xbootclasspath可以修改加载的目录。

  • Extention ClassLoader(扩展类加载器)

    ​ 扩展类加载器,主要加载 lib/ext 目录下的 jar 和 class 文件。通过系统变量 java.ext.dirs 可以指定这个目录,契父类为 URLClassloader。

  • Application ClassLoader(系统加载器)

    ​ 也可以被称为 System ClassLoader,用来加载我们写的代码。

  • Custom ClassLoader (自定义类加载器)

    ​ 自定义的类加载器。

双亲委派机制

​ 当我们自己写一个 String 类,包名和 Java 中的 String 类即使一致也不会被加载,因为在系统加载器进行加载的时候会往上——扩展类加载器进行询问是否已经加载了这个类,若找不到则会继续向上——根加载器,此时就会发现 String 类已经被加载了那么我们自己写的就不会被加载。这就是双亲委派机制。

​ 这个机制的存在使得 Java 代码不会被覆盖保证了代码的安全性。

​ 我们可以翻阅下 ClassLoader.class 的源代码,可以看到是有个 parent 的一个存在,会进行判断这个类加载器是否拥有父类,有的话则会调用父类的类加载方法。

 /**
     * Loads the class with the specified <a href="#name">binary name</a>.  The
     * default implementation of this method searches for classes in the
     * following order:
     *
     * <ol>
     *
     *   <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
     *   has already been loaded.  </p></li>
     *
     *   <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
     *   on the parent class loader.  If the parent is <tt>null</tt> the class
     *   loader built-in to the virtual machine is used, instead.  </p></li>
     *
     *   <li><p> Invoke the {@link #findClass(String)} method to find the
     *   class.  </p></li>
     *
     * </ol>
     *
     * <p> If the class was found using the above steps, and the
     * <tt>resolve</tt> flag is true, this method will then invoke the {@link
     * #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
     *
     * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
     * #findClass(String)}, rather than this method.  </p>
     *
     * <p> Unless overridden, this method synchronizes on the result of
     * {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
     * during the entire class loading process.
     *
     * @param  name
     *         The <a href="#name">binary name</a> of the class
     *
     * @param  resolve
     *         If <tt>true</tt> then resolve the class
     *
     * @return  The resulting <tt>Class</tt> object
     *
     * @throws  ClassNotFoundException
     *          If the class could not be found
     */
    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;
        }
    }

Tomcat 类加载机制

​ 在 Tomcat 里面有时候启动项目的时候会需要启动多个项目,那么就必定违反了双亲委派机制,那么他的结构是怎么设计类加载器的?看下图:

​ 其实从图里面可以看出来其实 Tomcat 并没有破坏 JDK 原来的类加载器的结构,只是在其 JDK 加载器之外写了其他的类加载器。对于一些需要加载的非基础类, 会由一个叫作 WebAppClassLoader 的类加载器优先加载。 等它加载不到的时候, 再交给上层的 ClassLoader 进行加载。

这个加载器用来隔绝不同应用的 .class 文件, 比如你的两个应用, 可能会依赖同一个第三方的不同版本, 它们是相互没有影响的。

如何在同一个 JVM 里, 运行着不兼容的两个版本, 当然是需要自定义加载器才能完成的事。
那么 tomcat 是怎么打破双亲委派机制的呢? 可以看图中的 WebAppClassLoader, 它加载自己目录下的 .class 文件, 并不会传递给父类的加载器 :

但是, 它却可以使用 SharedClassLoader 所加载的类, 实现了共享和分离的功能。
但是我们自己写一个 ArrayList, 放在应用目录里, tomcat 依然不会加载。 它只是自定义的加载器顺序不同, 但对于顶层来说, 还是一样的。

SPI(Service provider Interface)

​ Java 中有一个 SPI 机制, 全称是 Service Provider Interface, 是 Java 提供的一套用来被第三方实现或者扩展的 API, 它可以用来启用框架扩展和替换组件。

​ 其实 JDBC 就是以 SPI 的一个具体的实现,JDK 写接口,每个数据库厂商写具体的实现,以 MySQL为例:

​ 以上代码其实就算没有导入 MySQL 的驱动包也是可以编译通过的,但是运行的话就会报错。那么为什么我们并没有创建具体的实现类但是也可以调用到具体的实现类,里面最重要的一步就是第二步——就是获取数据库链接:

public class DriverManager {
    //这里是静态块,基本上肯定会调用的那种。
	static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

 private static void loadInitialDrivers() {
      // ....省略相关代码
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
				//此处代码就是关键,这里就是从固定的位置进行读取
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
		//       ....省略相关代码
    }
}

​ ServiceLoader 这个类会从固定的位置——META-INF/services/中读取文件中的具体的实现类,那么 MySQL 驱动包中可以看到:

里面确实是有一个这样的目录,目录中文件的内容就是具体实现类的全限定名,这样 ServiceLoader 中就会从这个文件中读取具体的实现类进行一个遍历从而找到具体的实现类进行调用。

下面就是一个具体的代码测试(可跳过):

package com.test.demo.spi;

//创建一个hello接口
public interface Hello {
     void hello();
}

package com.test.demo.spi;
//随便写一个实现类来实现上面这个接口
public class IHello implements Hello {
    @Override
    public void hello() {
        System.out.println("Hello world!!!");
    }
}

在 java 目录下面创建一个以接口全限定名创建的一个文件,文件内容为具体实现类的全限定名。

package com.test.demo.spi;

import java.sql.DriverManager;
import java.util.Iterator;
import java.util.ServiceLoader;

public class Test {
    public static void main(String[] args) {
        ServiceLoader<Hello> load = ServiceLoader.load(Hello.class);
        Iterator<Hello> iterator = load.iterator();
        while (iterator.hasNext()){
            iterator.next().hello();
        }
    }
}

成功获取到实现类中的方法。

OSGI(了解)

​ OSGi 曾经非常流行, Eclipse 就使用 OSGi 作为插件系统的基础。 OSGi 是服务平台的规范, 旨在用于需要长运行时间、 动态更新和对运行环境破坏
最小的系统。
​ OSGi 规范定义了很多关于包生命周期, 以及基础架构和绑定包的交互方式。 这些规则, 通过使用特殊 Java 类加载器来强制执行, 比较霸道。
​ 比如, 在一般 Java 应用程序中, classpath 中的所有类都对所有其他类可见, 这是毋庸置疑的。 但是, OSGi 类加载器基于 OSGi 规范和每个绑定包的 manifest.mf 文件中指定的选项, 来限制这些类的交互, 这就让编程风格变得非常的怪异。 但我们不难想象, 这种与直觉相违背的加载方式, 这些都是由专用的类加载器来实现的。

posted @ 2021-03-13 17:07  某人人莫  阅读(84)  评论(0编辑  收藏  举报