理解鲜为人知的ClassLoader

JAVA类装载方式,有两种:java

1.隐式装载, 程序在运行过程当中当碰到经过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。 2.显式装载, 经过class.forname()等方法,显式加载须要的类git

类加载的动态性体现:github

一个应用程序老是由n多个类组成,Java程序启动时,并非一次把全部的类所有加载后再运行,它老是先把保证程序运行的基础类一次性加载到jvm中,其它类等到jvm用到的时候再加载,这样的好处是节省了内存的开销,由于java最先就是为嵌入式系统而设计的,内存宝贵,这是一种能够理解的机制,而用到时再加载这也是java动态性的一种体现算法

java类装载器apache

 

JDK 默认提供了以下几种ClassLoaderapi

  1. Bootstrp loader
    Bootstrp加载器是用C++语言写的,它是在Java虚拟机启动后初始化的,它主要负责加载%JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类。
    数组

  1. ExtClassLoader  
    Bootstrp loader加载ExtClassLoader,而且将ExtClassLoader的父加载器设置为Bootstrp loader.ExtClassLoader是用Java写的,具体来讲就是 sun.misc.Launcher$ExtClassLoader,ExtClassLoader主要加载%JAVA_HOME%/jre/lib/ext,此路径下的全部classes目录以及java.ext.dirs系统变量指定的路径中类库。
    安全

  2. AppClassLoader 
    Bootstrp loader加载完ExtClassLoader后,就会加载AppClassLoader,而且将AppClassLoader的父加载器指定为 ExtClassLoader。AppClassLoader也是用Java写成的,它的实现类是 sun.misc.Launcher$AppClassLoader,另外咱们知道ClassLoader中有个getSystemClassLoader方法,此方法返回的正是AppclassLoader.AppClassLoader主要负责加载classpath所指定的位置的类或者是jar文档,它也是Java程序默认的类加载器。
    网络

综上所述,它们之间的关系能够经过下图形象的描述:jvm

 

 

为何要有三个类加载器,一方面是分工,各自负责各自的区块,另外一方面为了实现委托模型

 类加载器之间是如何协调工做的

前面说了,java中有三个类加载器,问题就来了,碰到一个类须要加载时,它们之间是如何协调工做的,即java是如何区分一个类该由哪一个类加载器来完成呢。 在这里java采用了委托模型机制,这个机制简单来说,就是“类装载器有载入类的需求时,会先请示其Parent使用其搜索路径帮忙载入,若是Parent 找不到,那么才由本身依照本身的搜索路径搜索类

下面举一个例子来讲明,为了更好的理解,先弄清楚几行代码:

运行结果:

能够看出Test是由AppClassLoader加载器加载的,AppClassLoaderParent 加载器是 ExtClassLoader,可是ExtClassLoaderParent为 null 是怎么回事呵,朋友们留意的话,前面有提到Bootstrap Loader是用C++语言写的,依java的观点来看,逻辑上并不存在Bootstrap Loader的类实体,因此在java程序代码里试图打印出其内容时,咱们就会看到输出为null

类装载器ClassLoader(一个抽象类)描述一下JVM加载class文件的原理机制

类装载器就是寻找类或接口字节码文件进行解析并构造JVM内部对象表示的组件,在java中类装载器把一个类装入JVM,通过如下步骤:

一、装载:查找和导入Class文件 二、连接:其中解析步骤是能够选择的 (a)检查:检查载入的class文件数据的正确性 (b)准备:给类的静态变量分配存储空间 (c)解析:将符号引用转成直接引用 三、初始化:对静态变量,静态代码块执行初始化工做

类装载工做由ClassLoder和其子类负责。JVM在运行时会产生三个ClassLoader:根装载器ExtClassLoader(扩展类装载器)和AppClassLoader,其中根装载器不是ClassLoader的子类,由C++编写,所以在java中看不到他,负责装载JRE的核心类库,如JRE目录下的rt.jar,charsets.jar等。ExtClassLoaderClassLoder的子类,负责装载JRE扩展目录ext下的jar类包;AppClassLoader负责装载classpath路径下的类包,这三个类装载器存在父子层级关系****,即根装载器是ExtClassLoader的父装载器,ExtClassLoader是AppClassLoader的父装载器。默认状况下使用AppClassLoader装载应用程序的类

Java装载类使用“全盘负责委托机制”。“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入;“委托机制”是指先委托父类装载器寻找目标类,只有在找不到的状况下才从本身的类路径中查找并装载目标类。这一点是从安全方面考虑的,试想若是一我的写了一个恶意的基础类(如java.lang.String)并加载到JVM将会引发严重的后果,但有了全盘负责制,java.lang.String永远是由根装载器来装载,避免以上状况发生 除了JVM默认的三个ClassLoder之外,第三方能够编写本身的类装载器,以实现一些特殊的需求。类文件被装载解析后,在JVM中都有一个对应的java.lang.Class对象,提供了类结构信息的描述。数组,枚举及基本数据类型,甚至void都拥有对应的Class对象。Class类没有public的构造方法,Class对象是在装载类时由JVM经过调用类装载器中的defineClass()方法自动构造的。

 

为何要使用这种双亲委托模式呢?
由于这样能够避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
考虑到安全因素,咱们试想一下,若是不使用这种委托模式,那咱们就能够随时使用自定义的String来动态替代java核心api中定义类型,这样会存在很是大的安全隐患,而双亲委托的方式,就能够避免这种状况,由于String已经在启动时被加载,因此用户自定义类是没法加载一个自定义的ClassLoader。

 

思考:假如咱们本身写了一个java.lang.String的类,咱们是否能够替换调JDK自己的类?
答案是否认的。咱们不能实现。为何呢?我看不少网上解释是说双亲委托机制解决这个问题,其实不是很是的准确。由于双亲委托机制是能够打破的,你彻底能够本身写一个classLoader来加载本身写的java.lang.String类,可是你会发现也不会加载成功,具体就是由于针对java.*开头的类,jvm的实现中已经保证了必须由bootstrp来加载。
 
 定义自已的ClassLoader
 
既然JVM已经提供了默认的类加载器,为何还要定义自已的类加载器呢?
 
由于Java中提供的默认ClassLoader,只加载指定目录下的jar和class,若是咱们想加载其它位置的类或jar时,好比:我要加载网络上的一个class文件,经过动态加载到内存以后,要调用这个类中的方法实现个人业务逻辑。在这样的状况下,默认的ClassLoader就不能知足咱们的需求了,因此须要定义本身的ClassLoader。
 
定义自已的类加载器分为两步:
 
一、继承java.lang.ClassLoader
 
二、重写父类的findClass方法
 
读者可能在这里有疑问,父类有那么多方法,为何恰恰只重写findClass方法?
 
由于JDK已经在loadClass方法中帮咱们实现了ClassLoader搜索类的算法,当在loadClass方法中搜索不到类时,loadClass方法就会调用findClass方法来搜索类,因此咱们只需重写该方法便可。如没有特殊的要求,通常不建议重写loadClass搜索类的算法。
 

线程上下文类加载器

  线程上下文类加载器(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 的核心库。它也不能代理给系统类加载器,由于它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式没法解决这个问题。

  线程上下文类加载器正好解决了这个问题。若是不作任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就能够成功的加载到 SPI 实现的类。线程上下文类加载器在不少 SPI 的实现中都会用到。

 

类加载器与Web容器

  对于运行在 Java EE容器中的 Web 应用来讲,类加载器的实现方式与通常的 Java 应用有所不一样。不一样的 Web 容器的实现方式也会有所不一样。以 Apache Tomcat 来讲,每一个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不一样的是它是首先尝试去加载某个类,若是找不到再代理给父类加载器。这与通常类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐作法,其目的是使得 Web 应用本身的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围以内的。这也是为了保证 Java 核心库的类型安全。
  绝大多数状况下,Web 应用的开发人员不须要考虑与类加载器相关的细节。下面给出几条简单的原则:
  (1)每一个 Web 应用本身的 Java 类文件和使用的库的 jar 包,分别放在 WEB-INF/classes和 WEB-INF/lib目录下面。
  (2)多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由全部 Web 应用共享的目录下面。
  (3)当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。

 

 

类加载器与OSGi

 

  OSGi是 Java 上的动态模块系统。它为开发人员提供了面向服务和基于组件的运行环境,并提供标准的方式用来管理软件的生命周期。OSGi 已经被实现和部署在不少产品上,在开源社区也获得了普遍的支持。Eclipse就是基于OSGi 技术来构建的。
  OSGi 中的每一个模块(bundle)都包含 Java 包和类。模块能够声明它所依赖的须要导入(import)的其它模块的 Java 包和类(经过 Import-Package),也能够声明导出(export)本身的包和类,供其它模块使用(经过 Export-Package)。也就是说须要可以隐藏和共享一个模块中的某些 Java 包和类。这是经过 OSGi 特有的类加载器机制来实现的。OSGi 中的每一个模块都有对应的一个类加载器。它负责加载模块本身包含的 Java 包和类。当它须要加载 Java 核心库的类时(以 java开头的包和类),它会代理给父类加载器(一般是启动类加载器)来完成。当它须要加载所导入的 Java 类时,它会代理给导出此 Java 类的模块来完成加载。模块也能够显式的声明某些 Java 包和类,必须由父类加载器来加载。只须要设置系统属性 org.osgi.framework.bootdelegation的值便可。
  假设有两个模块 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()来设置当前线程的上下文类加载器。

相关文章
相关标签/搜索