浅谈java的类加载机制和双亲委派模型

类加载器

  虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机的外部去实现。以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为类加载器。

  类加载器可以说是java语言的一项创新,也是java语言流行的重要原因之一。它最初是为了满足java Applet的需求而开发出来的。虽然目前java Applet技术基本上已经“死掉”。但是类加载器却在类层次划分、OSGi、热部署、代码加密等领域大放异彩,成为了java技术体系中一块重要的基石,可谓是失之东隅,收之桑榆。

  类加载器虽然只用于实现类加载动作,但它在java程序中起到的作用远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身确立在java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达的更通俗一点;比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个class文件,被同一虚拟机加载,只要加载它们的类加载器不同,那么这两个类就必定不相等。

  这里所指的“相等”,包括代表类的class对象的equals()方法,isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系的判定等情况。如果没有注意到类加载器的影响,在某些情况下可能会产生具有迷惑性的结果。

 

  

package com.ggp.jvm;

import java.io.IOException;
import java.io.InputStream;

/**
 * @author: ggp
 * @Date: 2019/4/24 08:46
 * @Description:
 */
public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
        ClassLoader myLoader = new ClassLoader() {

            @Override
            public Class loadClass(String name)throws ClassNotFoundException{
                try{
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    /**
                     * 本类的同路径下
                     */
                    InputStream is = this.getClass().getResourceAsStream(fileName);
                    /**
                     * 如果未找到class文件,就调用父类的loadClass();
                     */
                    if(is == null){
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    /**
                     * 生成class对象
                     */
                    return defineClass(name,b,0,b.length);
                }catch(IOException e){
                    throw new ClassNotFoundException(name);
                }
            }
        };
        Object obj = myLoader.loadClass("com.ggp.jvm.ClassLoaderTest").newInstance();
        ClassLoaderTest test = new ClassLoaderTest();
        System.out.println("*******obj class**********");
        System.out.println(obj.getClass());
        System.out.println("*******test class**********");
        System.out.println(test.getClass());
        System.out.println("*******obj classLoader**********");
        System.out.println(obj.getClass().getClassLoader());
        System.out.println("*******test classLoader**********");
        System.out.println(test.getClass().getClassLoader());
        System.out.println("*******obj instanceof com.ggp.jvm.ClassLoaderTest**********");
        System.out.println(obj instanceof com.ggp.jvm.ClassLoaderTest );
        System.out.println("*******obj class equal test class**********");
        System.out.println(obj.getClass().equals(test.getClass()));
        System.out.println("*******obj class isAssignableFrom test**********");
        System.out.println(obj.getClass().isAssignableFrom(test.getClass()));
    }
}

 打印结果

 

*******obj class**********
class com.ggp.jvm.ClassLoaderTest
*******test class**********
class com.ggp.jvm.ClassLoaderTest
*******obj classLoader**********
com.ggp.jvm.ClassLoaderTest$1@8813f2
*******test classLoader**********
sun.misc.Launcher$AppClassLoader@1a46e30
*******obj instanceof com.ggp.jvm.ClassLoaderTest**********
false
*******obj class equal test class**********
false
*******obj class isAssignableFrom test**********
false

关于自定类的加载器可观看我的另一篇博客https://blog.csdn.net/qq_33543634/article/details/89500650

双亲委派模型

  从java虚拟机的角度来讲,只存在两种不同的类加载器;一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分(只限于Hotspot);另一种就是所有其他的类加载器,这些类加载器都由java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader

当然有更细的划分,如下图

  

  • 启动类加载器(BootStrap ClassLoader):这个类加载器负责将存放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是被虚拟机识别的类库(按照文件名识别,后缀之类)加载到内存中。启动类加载器无法被用户直接使用。用户在编写自定义类加载器的时候,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可。
  • 扩展类加载器(Extension ClassLoader): 这个类加载器有sun.misc.Lauch$ExtClassLoader实现,它负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器
  • 应用程序类加载器(Application ClassLoader) : 这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以也一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义自己的类加载器,一般情况下这个就是程序中的默认的类加载器 

jdk源码关于loadClass的实现

protected synchronized Class loadClass(String name, boolean resolve)
	throws ClassNotFoundException
    {
	// First, check if the class has already been loaded
	Class c = findLoadedClass(name);
	if (c == null) {
	    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.
	        c = findClass(name);
	    }
	}
	if (resolve) {
	    resolveClass(c);
	}
	return c;
    }

首先会去判断这个类是否已经被加载,如果没有,先去判断它有没有父类加载器,如果有,就用父类加载器去加载,如果没有就用启动类加载器去加载,如果上面依旧没有加载成功,就用自己的加载器去加载

值得注意的一点是类加载的父子关系并不是通过继承来实现的,而都是通过组合的方式

什么是组合

Class A

Class B {

  class B(){

     A a = new A();

}

}

上面的就是组合而不是通过关键字extends继承

双亲委派模型的优点

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是要委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序中各个类加载环境中都是同一个类。因此这个模型有利于核心类不被修改。

双亲委派模型的破坏

  • 双亲委派模型是在jdk1.2之后引入的,而类加载器和抽象类java.lang.ClassLoader则是在jdk1.0已经存在,面对已经存在的用户自定义类加载器实现代码,为了前后兼容。设计者给java.lang.ClassLoader添加了一个新的protected方法findClass(),然后通过这个方法去调用自己loadClass()
  • 第二次是这个模型本身的缺陷造成的。基础类之所以被称为基础,因为它们经常被作为用户代码调用的api,但是基础类如果需要回调用户的代码,怎么办?一个典型的例子就是JDNI服务,他的代码是由启动类加载,但是它的目的是对资源进行集中管理和查找,它需要调用有独立厂商实现并部署在应用程序下的spi代码,但是启动类加载器不可能认识这些代码,为了解决这个问题,设计者引入了一个线程上下文加载器,JNDI服务通过这个线程上下文加载器完成通过父类加载器请求子类加载器去加载
  • 第三次破坏是由我们常听到的热部署之类的,不再细说

 

参考资料:《深入理解java虚拟机》

你可能感兴趣的:(jvm)