Java类加载机制

一、类加载的过程

在java中类装载器把一个类装入JVM,经过以下步骤:

1、装载:查找和导入Class文件
2、链接: 
(a)检查:检查载入的class文件数据的正确性
(b)准备:给类的静态变量分配存储空间
(c)解析:将符号引用转成直接引用
其中解析步骤是可以选择的。在某些情况下,解析阶段可以在初始化阶段之后再开始,这是为了支持java的运行时绑定(动态绑定)。
3、初始化:对静态变量,静态代码块执行初始化工作

二、类装载的条件

类只有在必须要使用时才会被装载,jvm不会无条件的装载Class类型。java虚拟机规定,一个类或接口在初次使用时,必须要进行初始化。这里指的使用,是指主动使用,主动使用只有以下几种情况:

  • 当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化
  • 当调用类的静态方法时,即当使用了invokestatic指令
  • 当使用类或接口的静态字段时(final常量除外),比如,使用getstatic或者putstatic指令。
  • 当使用java.lang.reflect包中的方法反射类的方法时。
  • 当初始化子类时,要求现场初始化父类。
  • 作为启动虚拟机,含有main()方法的那个类。

除了以上情况属于主动使用,其他情况都属于被动使用被动使用时不会引起类的初始化。

补充:静态内部类的加载和初始化何时完成?

内部类的加载时机:

静态内部类不会随着外部类一起加载,而是在需要用到内部类时才会去加载,比如在外部类的方法中访问静态内部类的静态方法或静态域时,这时才会jvm才会加载静态内部类。

内部类的初始化时机:

三、双亲委派模型

存在三种类加载器:启动类加载器扩展类加载器应用类加载器
启动类加载器使用C++实现,是JVM的一部分。其它类加载器则由Java语言实现,独立于JVM,而且都继承自java.lang.ClassLoader。ClassLoader是一个抽象类,定义如下。

public abstract class ClassLoader

启动类加载器(Bootstrap):
  这个类加载器主要加载<JAVA_HOME>lib下能被虚拟机识别的类,如rt.jar。
扩展类加载器(Ext):
  这个类加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>libext目录中或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

应用类加载器(App):
  这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户路径(classpath)上所有的类库,开发者可以直接使用这个类加载器,如果应用程序没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

我们的应用程序都是由这3种类加载器互相配合进行加载的,必要时还可以实现自定义的类加载器,它们的关系如图所示。这种层级关系称为类加载器的双亲委派模型(Parents Delegation Model)。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己主动去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器无法完成加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

实现双亲委派的代码是由java.lang.ClassLoad的loadClass()方法实现的,loadClass()方法实现如下

    /**
     * 1.检查类是否已被加载。若已加载,则直接第5步
     * 2.若类没被加载,则使用父类加载器来加载类
     * 3.若父类加载器不存在,则使用BootStrap类加载器来加载类
     * 4.2和3两种方式都没能加载成功,则类加载器自己加载类。
     * 4.解析类(可选)
     */
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        //加载类时需要加锁,防止并发加载
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            //1.首先,检查类是否已经被加载
            Class c = findLoadedClass(name);
            //2.若类没被加载,则通过父类加载器或者BootStrap类加载器来加载类
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //2.1 存在父类加载器,则使用父类加载器加载类
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {//2.2 否则,使用BootStrap类加载器加载类  
                        c = findBootstrapClassOrNull(name);  
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                    // 存在父类加载器,但却没加载到类时抛出ClassNotFoundException异常
                }
                //3.父类加载器和Bootstrap类加载器两种方式都没加载到类,则类加载器亲自出马来加载类
                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();
                }
            }
            // 如果resolve设为true,则解析类。(若类由父类加载器加载,父类加载器不进行解析)
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

上面介绍了双亲委派模式的工作方式,那么这样设计到底有什么好处呢?

好处是显而易见的:这种方式从结构上来说比较清晰,使得各个类加载器的职责非常明确,从而使得java类随着它的类加载器一起具有优先级的层次关系。

例如,类java.lang.Object类,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给出于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境下都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己实现了一个java.lang.Object类,并放在classpath中,那么系统中将会出现多个不同的Object类。那么Java类型体系中最基本的行为也就无法保证,应用程序也将变得一片混乱。

四、双亲委派模型的弊端

  检查类是否已经被加载的过程是单向的。这种模型好处上面已经说明了,但是同时它也带来了一个问题,即顶层的类加载器无法访问底层的类加载器所加载的类

  通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类。按照这种模式,应用类访问系统类自然是没有问题,但是反过来系统类访问应用类就会出现问题。比如,在系统类中,提供了一个接口,该接口需要在应用中得以实现,该接口还绑定了一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。这时,就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。拥有这种问题的组件有很多,比如JDBC、Xml Parser等。

事实上,通过重载ClassLoader可以解决这种问题。不少应用软件和框架都修改了这种行为,比如Tomcat,它有其独特的类加载顺序。

五、打破双亲委派模型

双亲委派模型并不是一个强制性的约束模型,而是java设计者推荐给开发者的类加载器实现方式。在java世界里大部分的类加载器都遵循这个模型,但也有例外,到目前为止,双亲委派模型出现过3次较大规模的破坏。

1.双亲委派模型提出之前

双亲委派模型是jdk1.2中引入的,而ClassLoader在JDK1.0之前就已经存在了。因此存在用户自定义的类加载器,其并未遵守双亲委派模型。

2.JDBC、JNDI等组件的出现。

双亲委派模型是单向委派的。正因为这样,导致引导类加载器无法加载由应用类加载器才能加载的类,但是在一些情况下又必须这样。

比如JNDI现在已成为Java的标准服务,它的代码已放入rt.jar中由启动类加载器来加载。但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现并部署在应用程序classpath下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但显然这已经超出了启动类加载器的类搜索范围。最终java设计团队引入了线程上下文类加载器,它在加载SPI代码时,也就是父类加载器委托子类加载器来加载类,显然这已经违背了双亲委派模型的宗旨,但这也是迫不得已的事。

3.热部署的出现

用户对程序动态性(代码热替换、模块热部署)的追求导致的。简而言之,就是在不重启系统的情况下来部署。OSGI实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块都有自己的类加载器,当需要更换一个模块时,就把模块连同类加载器一起替换掉以实现代码热替换

①.什么是类的热部署

  所谓热部署,就是在应用正在运行的时候升级软件,不需要重新启用应用。对于Java应用程序来说,热部署就是运行时更新Java类文件。在基于Java的应用服务器实现热部署的过程中,类装入器扮演着重要的角色。大多数基于Java的应用服务器,包括EJB服务器和Servlet容器,都支持热部署。
类装入器不能重新装入一个已经装入的类,但只要使用一个新的类装入器实例,就可以将类再次装入一个正在运行的应用程序。

②.如何实现java类的热部署

  JVM在加载类之前会调用findLoadedClass方法检查请求的类是否已经被加载过。如果类已经加载过,不再进行加载以免导致类冲突。

但是,JVM判断一个类是否是同一个类有两个条件:一是看这个类的完整类名是否一样(包括包名),二是看加载这个类的ClassLoader加载器是否是同一个(即使是同一个ClassLoader类的两个实例,加载同一个类也会不一样)。只有两者同时满足时JVM才认为这是同一个类。

  所以,要实现类的热部署可以创建不同的ClassLoader的实例对象,然后通过这个不同的实例对象来加载同名的类。

六、自定义类加载器 

有些情况下,我们需要自定义类加载器,比如从网络上加载类。

自定义类加载器只需要继承ClassLoader类,并重写findClass方法即可。当然也可以直接使用ClassLoader类已经实现的子类,比如URLClassLoader类。
URLClassLoader可以让我们通过以下几种方式进行加载:

* 文件: (从文件系统目录加载)
* jar包: (从Jar包进行加载)
* Http: (从远程的Http服务进行加载)

面试题

1.类加载器的两种加载方式?主动加载的几种情况?

2.类加载的过程?

3.什么是双亲委派模型?有什么好处和弊端?

4.如何自定义类加载器?

参考书籍:《深入理解java虚拟机》、《实战Java虚拟机》

原文地址:https://www.cnblogs.com/rouqinglangzi/p/6930617.html