java类加载器

一.类加载器基本概念

1. 获取class二进制字节码文件
2. 转换成Class对象(defineClass()方法)
3. 用newInstance()实例化对象

二. 类加载器的树状组织结构

Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。
1. 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是C++来实现的,并不继承自 java.lang.ClassLoader。
2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
3. 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。
     通过 ClassLoader.getSystemClassLoader()来获取它。--AppClassLoader

public class ClassLoaderTree {
    
    public static void main(String[] args) {
        ClassLoader myLoader = ClassLoaderTree.class.getClassLoader();
        while(myLoader!=null){
            System.out.println(myLoader.toString());
            myLoader = myLoader.getParent();
        }
    }
}
/* 
  sun.misc.Launcher$AppClassLoader@b412c18 
  sun.misc.Launcher$ExtClassLoader@63b5e16d 
*/

三. 类加载器的代理模式

1. Java 虚拟机是如何判定两个 Java 类是相同的:类定义加载器 + 类全名 = 类
2. 不同类加载器加载的相同字节码文件产生的类不是同一个类
3. 代理模式是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用 java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。这种技术在许多框架中都被用到,后面会详细介绍。

四. 加载类的过程

1. 类加载器会首先代理给父类加载器来尝试加载某个类。这就意味着真正完成类加载工作的类加载器和启动这个加载过程的类加载器,可能不是同一个。完成类加载工作是通过调用 defineClass()来实现的;启动类的加载过程是通过调用 loadClass()来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类 com.example.Inner,则 com.example.Outer类的定义加载器负责启动类 com.example.Inner的加载过程。
2. 方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是 java.lang.NoClassDefFoundError异常。
3. 类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。

五. 线程上下文类加载器

1. 线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。
前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的全部问题。Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers包中。这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 接口中的代码经常需要加载具体的实现类。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory类中的 newInstance()方法用来生成一个新的 DocumentBuilderFactory的实例。这里的实例的真正的类是继承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的实现所提供的。如在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。
2. 线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。

六 . 自定义类加载器

/**
   ClassLoader : (重写findClass)
  1)loadClass()方法实现了 分类加载器的代理模式
      1.finsLoadedClass()查找是否已加载
      2.未加载调用父加载器的loadClass()
      3.若无法加载,用findClass()查找该类
      4.用defineClass把二进制字节码转换成Class实例
 */
public class MyDefClassLoader extends ClassLoader{
    
    private String rootName;
    
    public MyDefClassLoader(String rootName){
        this.rootName = rootName;
    }
    
    protected Class findClass(String name) throws ClassNotFoundException {//重写findClass
        
        byte[] classFile = this.getClassFile(name);
        if(classFile == null){
            throw new ClassNotFoundException();
        }else{
            return defineClass(name, classFile, 0, classFile.length);
        }
        
    }

    private byte[] getClassFile(String packagePath){ // java.lang.string
        InputStream in;
        try {
            in = new FileInputStream(this.getholePath(packagePath));
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            byte[] readArray = new byte[1024];
            int length = 0;
            while((length = in.read(readArray)) != -1){
                os.write(readArray,0,length);            //ByteArrayOS输出到内存
            }
            return os.toByteArray();
        } catch (Exception  e) {
        }
        return null;
    }
    
    private String getholePath(String packagePath){
        String path = packagePath.replace('.',File.separatorChar);
        System.out.println(path);
        return this.rootName+File.separator+path+".class";
    }
    
    public static void main(String[] args) throws Exception {
         System.out.println(ClassLoader.getSystemClassLoader());
         System.out.println(System.getProperty("java.class.path")); // D:homeworkhomeworkmyeclipse8Classloaderin(项目)
         System.out.println(new MyDefClassLoader("d:").getholePath("java.lang.String"));
    }
}

七 . 类加载器与 Web 容器

1. Web 应用中类加载器的实现方式与一般Java 应用有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器(先在自己的找)。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类(HttpRequest)。

八 . 类加载器与 OSGi

1. OSGi™是 Java 上的动态模块系统。它为开发人员提供了面向服务和基于组件的运行环境,并提供标准的方式用来管理软件的生命周期。OSGi 已经被实现和部署在很多产品上,在开源社区也得到了广泛的支持。Eclipse 就是基于 OSGi 技术来构建的。
2. OSGi 中的每个模块(bundle)都包含 Java 包和类。模块可以声明它所依赖的需要导入(import)的其它模块的 Java 包和类(通过 Import-Package),也可以声明导出(export)自己的包和类,供其它模块使用(通过 Export-Package)。也就是说需要能够隐藏和共享一个模块中的某些 Java 包和类。这是通过 OSGi 特有的类加载器机制来实现的。OSGi 中的每个模块都有对应的一个类加载器。它负责加载模块自己包含的 Java 包和类。当它需要加载 Java 核心库的类时(以 java开头的包和类),它会代理给父类加载器(通常是启动类加载器)来完成。当它需要加载所导入的 Java 类时,它会代理给导出此 Java 类的模块来完成加载。模块也可以显式的声明某些 Java 包和类,必须由父类加载器来加载。只需要设置系统属性 org.osgi.framework.bootdelegation的值即可。
3. 假设有两个模块 bundleA 和 bundleB,它们都有自己对应的类加载器 classLoaderA 和 classLoaderB。在 bundleA 中包含类 com.bundleA.Sample,并且该类被声明为导出的,也就是说可以被其它模块所使用的。bundleB 声明了导入 bundleA 提供的类 com.bundleA.Sample,并包含一个类 com.bundleB.NewSample继承自 com.bundleA.Sample。在 bundleB 启动的时候,其类加载器 classLoaderB 需要加载类 com.bundleB.NewSample,进而需要加载类 com.bundleA.Sample。由于 bundleB 声明了类 com.bundleA.Sample是导入的,classLoaderB 把加载类 com.bundleA.Sample的工作代理给导出该类的 bundleA 的类加载器 classLoaderA。classLoaderA 在其模块内部查找类 com.bundleA.Sample并定义它,所得到的类 com.bundleA.Sample实例就可以被所有声明导入了此类的模块使用。对于以 java开头的类,都是由父类加载器来加载的。如果声明了系统属性 org.osgi.framework.bootdelegation=com.example.core.*,那么对于包 com.example.core中的类,都是由父类加载器来完成的。
OSGi 模块的这种类加载器结构,使得一个类的不同版本可以共存在 Java 虚拟机中,带来了很大的灵活性。不过它的这种不同,也会给开发人员带来一些麻烦,尤其当模块需要使用第三方提供的库的时候。下面提供几条比较好的建议:
    1) 如果一个类库只有一个模块使用,把该类库的 jar 包放在模块中,在 Bundle-ClassPath中指明即可。
    2) 如果一个类库被多个模块共用,可以为这个类库单独的创建一个模块,把其它模块需要用到的 Java 包声明为导出的。其它模块声明导入这些类。
    3) 如果类库提供了 SPI 接口,并且利用线程上下文类加载器来加载 SPI 实现的 Java 类,有可能会找不到 Java 类。如果出现了 NoClassDefFoundError异常,首先检查当前
        线程的上下文类加载器是否正确。通过 Thread.currentThread().getContextClassLoader()就可以得到该类加载器。该类加载器应该是该模块对应的类加载器。如果不是的
        话,可以首先通过 class.getClassLoader()来得到模块对应的类加载器,再通过 Thread.currentThread().setContextClassLoader()来设置当前线程的上下文类加载器。
原文地址:https://www.cnblogs.com/72808ljup/p/4151453.html