深刻探究JVM之类加载与双亲委派机制

@java

前言

前面学习了虚拟机的内存结构、对象的分配和建立,但对象所对应的类是怎么加载到虚拟机中来的呢?加载过程当中须要作些什么?什么是双亲委派机制以及为何要打破双亲委派机制?mysql

类的生命周期

在这里插入图片描述
类的生命周期包含了如上的7个阶段,其中验证准备解析统称为链接 ,类的加载主要是前五个阶段,每一个阶段基本上保持如上顺序开始(仅仅是开始,实际上执行是交叉混合的),只有解析阶段不必定,在初始化后也有可能才开始执行解析,这是为了支持动态语言。sql

加载

加载就是将字节码的二进制流转化为方法区的运行时数据结构,并生成类所对象的Class对象,字节码二进制流能够是咱们编译后的class文件,也能够从网络中获取,或者运行时动态生成(动态代理)等等。
那何时会触发类加载呢?这个在虚拟机规范中没有明肯定义,只是规定了什么时候须要执行初始化(稍后详细分析)。数组

验证

这个阶段很好理解,就是进行必要的校验,确保加载到内存中的字节码是符合要求的,主要包含如下四个校验步骤(了解便可):tomcat

  • 文件格式校验:这个阶段要校验的东西很是多,主要的有下面这些(实际上远远不止)
    • 是否以魔数0xCAFEBABE开头。
    • 主、次版本号是否在当前Java虚拟机接受范围以内。
    • 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
    • 指向常量的各类索引值中是否有指向不存在的常量或不符合类型的常量。
    • CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据。
    • Class文件中各个部分及文件自己是否有被删除的或附加的其余信息。
    • 。。。。。。
  • 元数据校验:对字节码描述信息进行语义分析。
    • 这个类是否有父类(除了java.lang.Object以外,全部的类都应当有父类)。
    • 这个类的父类是否继承了不容许被继承的类(被final修饰的类)。
    • 若是这个类不是抽象类,是否实现了其父类或接口之中要求实现的全部方法。
    • 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不一样等)。
    • 。。。。。。
  • 字节码校验:确保程序没有语法和逻辑错误,这是整个验证阶段最复杂的一个步骤。
    • 保证任意时刻操做数栈的数据类型与指令代码序列都能配合工做,例如不会出现相似于“在操做栈放置了一个 int 类型的数据,使用时却按 long 类型来加载入本地变量表中”这样的状况。
    • 保证任何跳转指令都不会跳转到方法体之外的字节码指令上。
    • 保证方法体中的类型转换老是有效的,例如能够把-个子类对象赋值给父类数据类型,这是安全的,可是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、彻底不相干的一个数据类型,则是危险和不合法的。
    • 。。。。。。
  • 符号引用验证:这个阶段发生在符号引用转为直接引用的时候,即其实是在解析阶段中进行的。
    • 符号引用中经过字符串描述的全限定名是否能找到对应的类。
    • 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。
    • 符号引用中的类、字段、方法的可访问性( private、 protected. public、 )。
    • 是否可被当前类访问。
    • 。。。。。。

准备

该阶段是为类变量(static)分配内存并设置零值,即类只要通过准备阶段其中的静态变量就是可以使用的了,但此时类变量的值还不是咱们想要的值,须要通过初始化阶段才会将咱们但愿的值赋值给对应的静态变量。安全

解析

解析就是将常量池中的符号引用替换为直接引用的过程。符号引用就是一个代号,好比咱们的名字,而这里能够理解为就是类的彻底限定名直接引用则是对应的具体的人、物,这里就是指目标的内存地址。为何须要符号引用呢?由于类在加载到内存以前尚未分配内存地址,所以必然须要一个东西指代它。这个阶段包含了类或接口的解析字段解析类方法解析接口方法解析,在解析的过程当中可能会抛出如下异常:网络

  • java.lang.NoSuchFieldError:找不到字段
  • java.lang.IllegalAccessError:不具备访问权限
  • java.lang.NoSuchMethodError:找不到方法

初始化

这是类加载过程当中的最后一个步骤,主要是收集类的静态变量的赋值动做static块中的语句合成<cinit>方法,经过该方法根据咱们的意愿为静态变量赋值以及执行static块,该方法会被加锁,确保多线程状况下只有一个线程能初始化成功,利用该特性能够实现单例模式。虚拟机规定了有且只有遇到如下状况时必须先确保对应类的初始化完成(加载、准备必然在此以前):数据结构

  • 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时。可以生成这四条指令的典型Java代码场景有:
    • 使用new关键字实例化对象的时候。
    • 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。
    • 调用一个类型的静态方法的时候。
  • 反射调用类时。
  • 当初始化类的时候,若是发现其父类尚未进行过初始化,则须要先触发其父类的初始化。
  • 当虚拟机启动时,用户须要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 当使用JDK 7新加入的动态语言支持时,若是一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,而且这个方法句柄对应的类没有进行过初始化,则须要先触发其初始化。
  • 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,若是有这个接口的实现类发生了初始化,那该接口要在其以前被初始化。

下面分析几个案例代码,读者们能够先思考后再运行代码看看和本身想的是否同样。多线程

案例一

先定义以下两个类:app

public class SuperClazz {
	static 	{
		System.out.println("SuperClass init!");
	}
	public static int value=123;
	public static final String HELLOWORLD="hello world";
	public static final int WHAT = value;
}

public class SubClaszz extends SuperClazz {
	static{
		System.out.println("SubClass init!");
	}

}

而后进行下面的调用:

public class Initialization {
	public static void main(String[]args){
		Initialization initialization = new Initialization();
		initialization.M1();
	}
	
	public void M1(){
		System.out.println(SubClaszz.value);
	}
}

第一个案例是经过子类去引用父类中的静态变量,两个类都会加载和初始化么?打印结果看看:

SuperClass init!
123

能够看到只有父类初始化了,那么父类必然是加载了的,问题就在于子类有没有被加载呢?能够加上参数:-XX:+TraceClassLoading再执行(该参数的做用就是打印被加载了的类),能够看到子类是被加载了的。因此经过子类引用父类静态变量,父子类都会被加载,但只有父类会进行初始化
为何呢?反编译后能够看到生成了以下指令:

0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
3: getstatic     #6                  // Field ex7/init/SubClaszz.value:I
6: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
9: return

关键就是getstatic指令就会触发类的初始化,可是为何子类不会初始化呢?由于这个变量是来自于父类的,为了提升效率,因此虚拟机进行了优化,这种状况只须要初始化父类就好了。

案例二

调用下面的方法:

public void M2(){
		SubClaszz[]sca = new SubClaszz[10];
	}

执行后能够发现,使用数组,不会触发初始化,但父子类都会被加载

案例三

public void M3(){
		System.out.println(SuperClazz.HELLOWORLD);
	}

引用常量不会触发类的加载和初始化,由于常量在编译后就已经存在当前class的常量池。

案例四

public void M4(){
		System.out.println(SubClaszz.WHAT);
	}

经过常量去引用其它的静态变量会发生什么呢?这个和案例一结果是同样的。

类加载器

类加载器和双亲委派模型

在咱们平时开发中,肯定一个类须要经过彻底限定名,而不能简单的经过名字,由于在不一样的路径下咱们是能够定义同名的类的。那么在虚拟机中又是怎么区分类的呢?在虚拟机中须要类加载器+彻底限定名一块儿来指定一个类的惟一性,即相同限定名的类若由两个不一样的类加载器加载,那虚拟机就不会把它们当作一个类。从这里咱们能够看出类加载器必定是有多个的,那么不一样的类加载器是怎么组织的?它们又分别须要加载哪些类呢?
在这里插入图片描述
从虚拟角度看,只有两种类型的类加载器:启动类加载器(BootstrapClassLoader)非启动类加载器。前者是C++实现,属于虚拟机的一部分,后者则是由Java实现的,独立于虚拟机的外部,而且所有继承自抽象类java.lang.ClassLoader。
但从Java自己来看,一直保持着三层类加载器双亲委派的结构,固然除了Java自己提供的三层类加载器,咱们还能够自定义实现类加载器。如上图,上面三个就是原生的类加载器,每个都是下一个类加载器的父加载器,注意这里都是采用组合而非继承。当开始加载类时,首先交给父加载器加载,父加载器加载了子加载器就不用再加载了,而如果父加载器加载不了,就会交给子加载器加载,这就是双亲委派机制。这就比如工做中遇到了没法处理的事,你会去请示直接领导,直接领导处理不了,再找上层领导,而后上层领导以为这是个小事,不用他亲自动手,就让你的直接领导去作,接着他又交给你去作等等。下面来看看每一个类加载器的具体做用:

  • BootstrapClassLoader:启动类加载器,顾名思义,这个类加载器主要负责加载JDK lib包,以及-Xbootclasspath参数指定的目录,而且虚拟机对文件名进行了限定,也就是说即便咱们本身写个jar放入到上述目录,也不会被加载。因为该类加载器是C++使用,因此咱们的Java程序中没法直接引用,调用java.lang.ClassLoader.getClassLoader()方法时默认返回的是null。
  • ExtClassLoader:扩展类加载器,主要负责加载JDK lib/ext包,以及被系统变量java.ext.dirs指向的全部类库,这个类库能够存放咱们本身写的通用jar。
  • AppClassLoader:应用程序类加载器,负责加载用户classpath上的全部类。它是java.lang.ClassLoader.getSystemClassLoader()的返回值,也是咱们程序的默认类加载器(若是咱们没有自定义类加载器的话)。

经过这三个类加载以及双亲委派机制,一个显而易见的好处就是,不一样的类随它的类加载器自然具备了加载优先级,像Object、String等等这些核心类库天然就会在咱们的应用程序类以前被加载,使得程序更安全,不会出现错误,Spring的父子容器也是这样的一个设计。经过下面这段代码能够看到每一个类所对应的类加载器:

public class ClassLoader {
    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader()); //启动类加载器
        System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());//拓展类加载器
        System.out.println(ClassLoader.class.getClassLoader());//应用程序类加载器
    }
}

输出:

null
sun.misc.Launcher$ExtClassLoader@4b67cf4d
sun.misc.Launcher$AppClassLoader@14dad5dc

破坏双亲委派模型

刚刚我举了工做中的一个例子来讲明双亲委派机制,但现实中咱们不须要事事都去请示领导,一样类加载器也不是彻底遵循双亲委派机制,在必要的时候是能够打破这个规则的。下面列举四个破坏的状况,在此以前咱们须要先了解下双亲 委派的代码实现原理,在java.lang.ClassLoader类中有一个loadClass以及findClass方法:

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;
        }
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

从上面能够看到首先是调用parent去加载类,没有加载到才调用自身的findClass方法去加载。也就是说用户在实现自定义类加载器的时候须要覆盖的是fiindClass而不是loadClass,这样才能知足双亲委派模型
下面具体来看看破坏双亲委派的几个场景。

第一次

第一次破坏是在双亲委派模型出现以前, 由于该模型是在JDK1.2以后才引入的,那么在此以前,抽象类java.lang.ClassLoader就已经存在了,用户自定义的类加载器都会去覆盖该类中的loadClass方法,因此双亲委派模型出现后,就没法避免用户覆盖该方法,所以新增了findClass引导用户去覆盖该方法实现本身的类加载逻辑。

SPI

第二次破坏是因为这个模型自己缺陷致使的,由于该模型保证了类的加载优先级,可是有些接口是Java定义在核心类库中,但具体的服务实现是由用户提供的,这时候就不得不破坏该模型才能实现,典型的就是Java中的SPI机制(对SPI不了解的读者能够翻阅我以前的文章或是其它资料,这里不进行阐述)。J
DBC的驱动加载就是SPI实现的,因此直接看到java.sql.DriverManager类,该类中有一个静态初始化块:

static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

    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;
        }

        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;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + 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);
            }
        }
    }

主要看ServiceLoader.load方法,这个就是经过SPI去加载咱们引入java.sql.Driver实现类(好比引入mysql的驱动包就是com.mysql.cj.jdbc.Driver):

public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

这个方法主要是从当前线程中获取类加载器,而后经过这个类加载器去加载驱动实现类(这个叫线程上下文类加载器,咱们也可使用这个技巧去打破双亲委派),那这里会获取到哪个类加载器呢?具体的设置是在sun.misc.Launcher类的构造器中:

public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        Thread.currentThread().setContextClassLoader(this.loader);
        String var2 = System.getProperty("java.security.manager");
        if (var2 != null) {
            SecurityManager var3 = null;
            if (!"".equals(var2) && !"default".equals(var2)) {
                try {
                    var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
                } catch (IllegalAccessException var5) {
                } catch (InstantiationException var6) {
                } catch (ClassNotFoundException var7) {
                } catch (ClassCastException var8) {
                }
            } else {
                var3 = new SecurityManager();
            }

            if (var3 == null) {
                throw new InternalError("Could not create SecurityManager: " + var2);
            }

            System.setSecurityManager(var3);
        }

    }

能够看到设置的就是AppClassLoader。你可能会有点疑惑,这个类加载器加载类的时候不也是先调用父类加载器加载么,怎么就打破双亲委派了呢?其实打破双亲委派指的就是类的层次结构,延伸意思就是类的加载优先级,这里本应该是在加载核心类库的时候却提早将咱们应用程序中的类库给加载到虚拟机中来了。

Tomcat

在这里插入图片描述
上图是Tomcat类加载的类图,前面三个不用说,CommonClassLoaderCatalinaClassLoaderSharedClassLoaderWebAppClassLoaderJspClassLoader则是Tomcat本身实现的类加载器,分别加载common包server包shared包WebApp/WEB-INF/lib包以及JSP文件,前面三个在tomcat 6以后已经合并到根目录下的lib目录下。而WebAppClassLoader则是每个应用程序对应一个,JspClassLoader是每个JSP文件都会对应一个,而且这两个类加载器都没有父类加载器,这也就违背了双亲委派模型。
为何每一个应用程序须要单独的WebAppClassLoader实例?由于每一个应用程序须要彼此隔离,假如在两个应用中定义了同样的类(彻底限定名),若是遵循双亲委派那就只会存在一份了,另外不一样的应用还有可能依赖同一个类库的不一样版本,这也须要隔离,因此每个应用程序都会对应一个WebAppClassLoader,它们共享的类库可让SharedClassLoader加载,另外这些类加载加载的类对Tomcat自己来讲也是隔离的(CatalinaClassLoader加载的)。
为何每一个JSP文件须要对应单独的一个JspClassLoader实例?这是因为JSP是支持运行时修改的,修改后会丢弃掉以前编译生成的class,并从新生成一个JspClassLoader实例去加载新的class。
以上就是Tomcat为何要打破双亲委派模型的缘由。

OSGI

OSGI是用于实现模块热部署,像Eclipse的插件系统就是利用OSGI实现的,这个技术很是复杂同时使用的也愈来愈少了,感兴趣的读者可自行查阅资料学习,这里再也不进行阐述。

总结

类加载的过程让咱们了解到一个类是如何被加载到内存中,须要通过哪些阶段;而类加载器和双亲委派模型则是告诉咱们应该怎么去加载类、类的加载优先级是怎样的,其中的设计思想咱们也能够学习借鉴;最后须要深入理解的是为何须要打破双亲委派,在遇到相应的场景时应该怎么作。

相关文章
相关标签/搜索