类加载机制的层次结构

每个编写的”.java”拓展名类文件都存储着需要执行的程序逻辑,这些”.java”文件经过Java编译器编译成拓展名为”.class”的文件,”.class”文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时,虚拟机将会加载它的”.class”文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程称为类加载,这里我们需要了解一下类加载的过程,如下:
image.png

  • 加载: 类加载过程的一个阶段:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象
  • 验证: 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
  • 准备: 为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
  • 解析: 主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析(这里涉及到字节码变量的引用
  • 初始化: 类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。

    类加载器类型

    启动类加载器, 也叫根类加载器(Bootstrap ClassLoader)最顶层的加载类

    这个类加载使用C++语言实现的,是虚拟机自身的一部分, 负责加载JAVA_HOME\lib目录中并且能被虚拟机识别的类库到JVM内存中,如果名称不符合的类库即使放在lib目录中也不会被加载。虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类), 其它加载器不能加载这些开关的类, 否者会报SecurityException。该类加载器无法被Java程序直接引用。
    需要加载的目录通过System.getProperty(“sun.boot.class.path”)获取

    扩展类加载器(Extention ClassLoader)(ExtClassLoader)

    该加载器主要是负责加载JAVA_HOME\lib\ext目录中的jar包和class文件, 还可以加载-D java.ext.dirs选项指定的目录,该加载器可以被开发者直接使用。
    需要加载的目录通过System.getProperty(“java.ext.dirs”)获取

    应用程序类加载器(AppClassLoader), 也叫系统类加载器(Application ClassLoader)

    该类加载器也称为系统类加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
    需要加载的目录通过System.getProperty(“java.class.path”)获取

    双亲委派模型

    如果一个类加载器收到了类加载的请求,它首先会查看这个类是否已经加载, 如果已经加载了则返回这个类, 否则把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
    image.png

    代码实现

    1. protected Class<?> loadClass(String name, boolean resolve)
    2. throws ClassNotFoundException
    3. {
    4. synchronized (getClassLoadingLock(name)) {
    5. // First, check if the class has already been loaded
    6. Class<?> c = findLoadedClass(name);
    7. if (c == null) {
    8. long t0 = System.nanoTime();
    9. try {
    10. if (parent != null) {
    11. c = parent.loadClass(name, false);
    12. } else {
    13. c = findBootstrapClassOrNull(name);
    14. }
    15. } catch (ClassNotFoundException e) {
    16. // ClassNotFoundException thrown if class not found
    17. // from the non-null parent class loader
    18. }
    19. if (c == null) {
    20. // If still not found, then invoke findClass in order
    21. // to find the class.
    22. long t1 = System.nanoTime();
    23. c = findClass(name);
    24. // this is the defining class loader; record the stats
    25. sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
    26. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
    27. sun.misc.PerfCounter.getFindClasses().increment();
    28. }
    29. }
    30. if (resolve) {
    31. resolveClass(c);
    32. }
    33. return c;
    34. }
    35. }

    优势

  • 类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次

  • java核心api中定义类型不会被随意替换

    线程上下文类加载器(Thread Context ClassLoader)

    Thread类中有getContextClassLoader()和setContextClassLoader(ClassLoader cl)方法用来获取和设置上下文类加载器,如果没有使用setContextClassLoader(ClassLoader cl)方法设置类加载器,那么线程将继承父线程的上下文类加载器,如果在应用程序的全局范围内都没有设置的话,那么这个上下文类加载器默认就是应用程序类加载器(Application ClassLoader),换句话说Java默认的线程上下文类加载器就是应用程序类加载器(AppClassLoader)。
    Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。
    这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader)来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。而线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。
    通过线程上下文来加载第三方库jndi, spi实现,而不依赖于双亲委派。大部分Java Application服务器(jboss, tomcat..)也是采用contextClassLoader来处理web服务

    常见问题

    假如我们自己写了一个java.lang.String的类,我们是否可以替换调JDK本身的类

    答案是否定的。我们不能实现。为什么呢?我看很多网上解释是说双亲委托机制解决这个问题,其实不是非常的准确。因为双亲委托机制是可以打破的,你完全可以自己写一个classLoader来加载自己写的java.lang.String类,但是你会发现也不会加载成功,具体就是因为针对java.*开头的类,jvm的实现中已经保证了必须由bootstrp来加载