`

java 类文件加载

    博客分类:
  • jvm
阅读更多

一、类加载器基本概念

       类加载器(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/

  • 大小: 42.7 KB
分享到:
评论

相关推荐

    Java类动态加载(一)——java源文件动态编译为class文件

    动态加载类通常涉及自定义ClassLoader,这是因为它允许我们覆盖默认的加载行为,例如从网络、数据库或其他非标准位置加载类。 对于Java源文件动态编译,我们可以使用Java的内置工具`javac`或`javax.tools.Java...

    springboot+java类热加载

    在Java中,类加载器负责查找并加载类到Java虚拟机中。我们可以通过自定义类加载器来实现热加载。例如,`MemoryClassLoader.java`可能就是一个自定义类加载器的实现,它可以在内存中动态加载或更新类。 **JarinJAR**...

    java类加载器

    类加载器在Java中扮演着至关重要的角色,不仅负责加载类,还维护了类的层次关系,确保了系统的稳定性和安全性。通过理解和掌握类加载器的工作原理及其不同类型的加载器,可以帮助开发者更好地管理Java应用程序的依赖...

    Java类文件保护技术中密码技术的新应用.pdf

    1. 重载类文件加载器技术:Java 类文件加载器是Java虚拟机(JVM)的一部分,负责将类文件加载到内存中以便执行。通过重载类加载器,可以实现对Class文件的加密。具体操作是在加载Class文件前,先用特定算法加密,...

    类加载器文件

    **类加载器(ClassLoader)**是Java虚拟机(JVM)中的一个重要组成部分,它负责将编译好的`.class`文件加载到JVM中,使得这些类可以在Java环境中运行。类加载器不仅能够加载类,还能够根据不同的需求定制加载方式,如从...

    java类加载器实例

    Java Applet 需要从远程下载 Java 类文件到浏览器中并执行。现在类加载器在 Web 容器和 OSGi 中得到了广泛的使用。一般来说,Java 应用的开发人员不需要直接同类加载器进行交互。Java 虚拟机默认的行为就已经足够...

    java 静态代码块通过类加载器获取资源文件例子

    总之,通过Java的静态代码块和类加载器,我们可以有效地管理和加载资源文件,特别是属性配置文件,确保在程序启动时即完成初始化工作,提升应用性能。这种技术在大型复杂系统中尤其常见,因为它能够保证配置的正确性...

    Java类加载器原理

    类加载器的作用不仅仅是加载类,还包括确保类的唯一性,避免重复加载,并且遵循特定的加载顺序。以下是对类加载器原理的详细解释: 1. 类加载器作用: 当JVM启动时,如果需要使用某个类,对应的类加载器会将这个类...

    java类加载器-tomcat中的类加载器

    每个类加载器在尝试加载类时,会先委托给父类加载器,只有当父类加载器无法加载时,才会尝试自己加载。这种机制保证了Java的核心类库只被加载一次,同时也确保了类的唯一性。 Tomcat的类加载器工作流程可以总结为:...

    java文件的加载

    ### Java 文件加载机制详解 #### 一、概述 在Java应用开发中,特别是Web应用程序的构建过程中,资源文件(如配置文件、属性文件等)的加载是必不可少的一部分。本文将重点探讨Java环境下不同方式下的文件加载方法...

    Java 动态加载jar文件示例

    它们按照层次结构工作,遵循"委托模型",即从顶层的启动类加载器开始尝试加载类,如果找不到则逐级向下委托。 要实现动态加载jar文件,我们需要创建自定义的类加载器。这个类加载器需要继承`java.lang.ClassLoader`...

    Java之——类热加载

    传统的Java应用程序在启动时,由JVM(Java虚拟机)通过类加载器将类加载到内存中,一旦加载完成,除非程序退出,否则这些类通常不会被重新加载。然而,在开发过程中,我们可能希望在不重启应用的情况下,对已加载的...

    Java类加载器.pdf

    Java类加载器是Java运行时环境的一个关键组成部分,负责将类文件(.class)从各种来源加载到JVM中。它不仅管理类的生命周期,还确保了类的正确加载和初始化,是Java动态特性的基石。 #### 类加载器的工作原理 Java...

    自定义Java类加载器

    Java类加载器是Java虚拟机(JVM)的关键组成部分,它负责查找并加载类到内存中,使得程序能够运行。自定义Java类加载器允许我们根据特定需求扩展默认的加载机制,例如,从非标准位置加载类或者实现动态加载。在Java...

    Java类加载内幕详细讲解

    类加载器是Java运行时环境的重要组成部分,负责加载类到JVM中,并确保类的正确加载与隔离。类加载器的基本结构如下: 1. **引导类加载器**(Bootstrap Class Loader):这是系统默认的类加载器,负责加载JRE/lib/rt...

    classloader类加载器_基于java类的加载方式详解

    Java类加载器(ClassLoader)是Java虚拟机(JVM)中的一个重要组成部分,用于将Java类文件加载到JVM中,以便能够执行Java程序。在Java中,类加载器的设计采用了一种称为“双亲委派模式”(Parent Delegation Model)...

    深入研究Java类加载机制 深入研究Java类加载机制

    Java中的类加载器遵循一个原则叫做“父母委托模型”,即当一个类加载器收到类加载请求时,首先将加载任务委托给父类加载器,只有当父类加载器无法完成加载时才会尝试自己加载。 这种设计模式的好处在于避免了类的...

    Java 自动加载JAR文件并运行其中的类方法

    4. **创建对象与调用方法**:加载类后,我们可以创建对象并调用其公共方法。如果`MyClass`有一个无参构造函数,我们可以这样做: ```java Object instance = clazz.newInstance(); Method method = clazz....

    java 类加载器 加密

    总结起来,Java 类加载器加密是一种增强程序安全性的技术,通过自定义类加载器和解密逻辑,可以在加载类之前对其进行加密,提高代码的保护性。同时,结合反射机制,即使类是加密状态,也能正常执行程序。这种技术常...

Global site tag (gtag.js) - Google Analytics