java 类文件加载

一、类加载器基本概念

       类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,然后重新解析成JVM统一要求的格式,最终转换成java.lang.Class类的一个实例(会加载到perm区也就是方法区),每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。类加载器除了可以读取字节码文件,还可以加载文件、图片等资源。下面主要介绍类加载器的类别、运行机制等。

二、ClassLoader分类及层次结构


   
    jvm在运行时会用到三个类加载器:Bootstrap ClassLoader、Extension ClassLoader和system class loader.Bootstrap是用C++编写的,我们在Java中看不到它,是null,是JVM自带的类装载器,用来装载核心类库,如java.lang.*等。Extension 是用来加载ext扩展目录下的类库,而System是用来加载classpath所指定的类库。System Class Loader是一个特殊的用户自定义类装载器,由JVM的实现者提供,在编程者不特别指定装载器的情况下默认装载用户类。系统类装载器可以通过ClassLoader.getSystemClassLoader() 方法得到。

        Java提供了抽象类ClassLoader,所有用户自定义类装载器都实例化自ClassLoader的子类。 这个抽象类里主要有四个方法:

1、defineClass(byte[],int,int) 这个方法是用来将字节流解析成JVM能够识别的Class对象,而字节流的获取可以有多种方式:文件、网络等。

2、findClass(String) 通常通过覆盖这个方法,在方法内获取到字节流,然后调用defineClass根据字节流生成Class对象。

3、resolveClass(Class<?>) 在调用完defineClass之后,还可以调用resolveClass方法来link,也可以交给JVM在对象真正实例化时去link。

4、loadClass(String) 是系统自己实现的逐层往上调用父类的loadClass方法去加载class,一般自己实现的类加载器不建议覆盖此方法,而是覆盖findClass方法。

        一般我们可以继承URLClassLoader这个子类来实现自定义类加载,因为它已经帮我们在ClassLoader上封装了一层,做了大部分工作。通常通过指定一个URL数组(指定查找的来源)来构造一个URLClassLoader对象,然后调用findClass来将找到的字节码加载到内存,然后调用defineClass、resolveClass等方法。

ClassLoader加载一个class文件到JVM时需要经过三个阶段:

1> 找到.class文件并把这个文件包含的字节码加载到内存中。

2> 对字节码进行验证、Class类数据结构分析、相应内存分配和最后的符号表的链接。

3> 类中静态属性和初始化赋值,以及静态块的执行等。

 

三、classloader机制——双亲委托加载模型

      类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。在介绍代理模式背后的动机之前,首先需要说明一下 Java 虚拟机是如何判定两个 Java 类是相同的。Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。比如一个 Java 类 com.example.Sample,编译之后生成了字节代码文件 Sample.class。两个不同的类加载器ClassLoaderA和 ClassLoaderB分别读取了这个 Sample.class文件,并定义出两个java.lang.Class类的实例来表示这个类。这两个实例是不相同的。基于这个,可以检查已经加载的class文件是否被修改,如果修改了,可以重新加载这个类,从而实现类的热部署。

     若有一个类加载器能成功装载,实际装载的类装载器被称为定义类装载器,所有能成功返回Class对象的装载器(包括定义类装载器)被称为初始类装载器。假设loader2的parent是loader1,loader1实际装载了MyClass,则loader1为MyClass的定义类装载器,loader2和loader1为MyClass的初始类装载器。

      当虚拟机来加载某个类时,先通过loadClass来启动类加载的过程(初始类装载器),然后通过defineClass来真正的完成类加载过程(定义类装载器)。

        java.lang.ClassNotFoundException: 是指通过ClassLoader显式加载类时找不到类文件。

        java.lang.NoClassDefFoundError:这个异常出现的根本原因是JVM隐式加载这些类时发现这些类不存在的异常。隐式加载一般包括属性引用某个类、继承了某个接口或类,以及方法的某个参数中引用了某个类等,从而要间接加载某个类的时候。

例如:某个依赖类升级后不兼容,原来公有的方法变为私有方法,导致调用出错 

四、自定义classloader来加载指定的类

自定义的classLoader的步骤:

1、继承ClassLoader类,并覆盖findClass(String name)方法

2、定义loadClassData方法,将目标资源转换成字节码数组 byte[] b返回;

3、调用defineClass方法根据字节数组生成class实例装载到内存。

代码如下:

public  Class findClass(String name)
    {
         byte [] data  =  loadClassData(name);
         return  defineClass(name, data,  0 , data.length);
    }

 public   byte [] loadClassData(String name)
    {
        FileInputStream fis  =   null ;
         byte [] data  =   null ;
         try 
        {
            fis  =   new  FileInputStream( new  File (name ));
            ByteArrayOutputStream baos  =   new  ByteArrayOutputStream();
             int  bt  =   0 ;
             while  ((bt  =  fis.read())  !=   - 1 )
            {
                baos.write(bt  );
               
            }
            data  =  baos.toByteArray();
        }  catch  (IOException e)
        {
            e.printStackTrace();

       }       

         return  data;
    }

五、其它类加载器

1、线程上下文加载器

        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 的实现中都会用到。

2、class.forName加载类

   Class.forName是一个静态方法,同样可以用来加载类。该方法有两种形式:Class.forName(String name, boolean initialize, ClassLoader loader)和 Class.forName(String className)。第一种形式的参数 name表示的是类的全名;initialize表示是否初始化类;loader表示加载时使用的类加载器。第二种形式则相当于设置了参数 initialize的值为 trueloader的值为当前类的类加载器。Class.forName的一个很常见的用法是在加载数据库驱动的时候。如Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()用来加载 Apache Derby 数据库的驱动。

3、网络类加载器

       类 NetworkClassLoader负责通过网络下载 Java 类字节代码并定义出 Java 类。它的实现与 FileSystemClassLoader类似。在通过NetworkClassLoader加载了某个版本的类之后,一般有两种做法来使用它。第一种做法是使用 Java 反射 API。另外一种做法是使用接口。需要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,因为客户端代码的类加载器找不到这些类。使用 Java 反射 API 可以直接调用 Java 类的方法。而使用接口的做法则是把接口的类放在客户端中,从服务器上加载实现此接口的不同版本的类。在客户端通过相同的接口来使用这些实现类。

4、web容器的类加载器

        对于运行在 Java EE™容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。

       绝大多数情况下,Web 应用的开发人员不需要考虑与类加载器相关的细节。下面给出几条简单的原则:每个 Web 应用自己的 Java 类文件和使用的库的 jar 包,分别放在 WEB-INF/classes和 WEB-INF/lib目录下面。多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由所有 Web 应用共享的目录下面当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。

5、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 虚拟机中,带来了很大的灵活性。不过它的这种不同,也会给开发人员带来一些麻烦,尤其当模块需要使用第三方提供的库的时候。下面提供几条比较好的建议:

   如果一个类库只有一个模块使用,把该类库的 jar 包放在模块中,在 Bundle-ClassPath中指明即可。如果一个类库被多个模块共用,可以为这个类库单独的创建一个模块,把其它模块需要用到的 Java 包声明为导出的。其它模块声明导入这些类。

    如果类库提供了 SPI 接口,并且利用线程上下文类加载器来加载 SPI 实现的 Java 类,有可能会找不到 Java 类。如果出现了NoClassDefFoundError异常,首先检查当前线程的上下文类加载器是否正确。通过 Thread.currentThread().getContextClassLoader()就可以得到该类加载器。该类加载器应该是该模块对应的类加载器。如果不是的话,可以首先通过 class.getClassLoader()来得到模块对应的类加载器,再通过 Thread.currentThread().setContextClassLoader()来设置当前线程的上下文类加载器。

六、类的热部署

       我们知道在修改一个java文件后必须要重启服务器才能生效,这个非常费时,那么能否修改后马上生效,实现热部署呢,答案是肯定的,但是其中确有一些问题需要解决好。

      JVM在加载类之前会先去调用findLoaderClass()方法查看是否能够返回类实例。如果类已经加载过滤,就不会再去加载。但是JVM表示一个类是否为同一个类有2个条件:1、看类的全名是否一样,2、看加载此类的类加载器是否是同一个实例。所以要实现热部署就得使用不同的类加载器实例来加载同一个类。所以我们只需要在类文件被修改后,启动新的类加载实例来加载这个类就可以实现类的热部署。

       但是这个过程中加载的字节码都会保存在PermGen区,而这个区域只有full gc时才会回收,所以必须关注PermGen区域的大小,防止内存溢出。还有一个问题,类实例对象在JVM中都是共享的,JVM通过保存对象状态而省去类信息的重复创建和回收,而对象一旦被创建,肯定会被持有和利用。通过重新生成classloader实例来加载类,然后替换原有的对象,并更新java栈中对原对象的引用,这样做看起来合理,但实际不可行。如果一个对象的属性结构被修改,但是运行时其它对象还在使用修改前的属性,这样便会出现错误。因为它违反了JVM的设计原则,JVM不能干预对象的引用关系,对象的引用关系只有对象的创建者来持有和使用。JVM只知道对象的编译类型,而不知道对象的运行时类型。

       上述问题的关键是对象的状态被保存了,并且被其它对象引用了,一个简单的办法就是不保存对象的状态,对象被创建使用后就被释放掉,下次修改后,对象是新创建的。这种方式就可以动态加载类了,而这正是JSP的实现方式,很多解释性语言也是这样。

 参考文章:https://www.ibm.com/developerworks/cn/java/j-lo-classloader/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值