一、类加载器基本概念
类加载器(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
的值为 true
,loader
的值为当前类的类加载器。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/
相关推荐
动态加载类通常涉及自定义ClassLoader,这是因为它允许我们覆盖默认的加载行为,例如从网络、数据库或其他非标准位置加载类。 对于Java源文件动态编译,我们可以使用Java的内置工具`javac`或`javax.tools.Java...
在Java中,类加载器负责查找并加载类到Java虚拟机中。我们可以通过自定义类加载器来实现热加载。例如,`MemoryClassLoader.java`可能就是一个自定义类加载器的实现,它可以在内存中动态加载或更新类。 **JarinJAR**...
类加载器在Java中扮演着至关重要的角色,不仅负责加载类,还维护了类的层次关系,确保了系统的稳定性和安全性。通过理解和掌握类加载器的工作原理及其不同类型的加载器,可以帮助开发者更好地管理Java应用程序的依赖...
1. 重载类文件加载器技术:Java 类文件加载器是Java虚拟机(JVM)的一部分,负责将类文件加载到内存中以便执行。通过重载类加载器,可以实现对Class文件的加密。具体操作是在加载Class文件前,先用特定算法加密,...
**类加载器(ClassLoader)**是Java虚拟机(JVM)中的一个重要组成部分,它负责将编译好的`.class`文件加载到JVM中,使得这些类可以在Java环境中运行。类加载器不仅能够加载类,还能够根据不同的需求定制加载方式,如从...
Java Applet 需要从远程下载 Java 类文件到浏览器中并执行。现在类加载器在 Web 容器和 OSGi 中得到了广泛的使用。一般来说,Java 应用的开发人员不需要直接同类加载器进行交互。Java 虚拟机默认的行为就已经足够...
总之,通过Java的静态代码块和类加载器,我们可以有效地管理和加载资源文件,特别是属性配置文件,确保在程序启动时即完成初始化工作,提升应用性能。这种技术在大型复杂系统中尤其常见,因为它能够保证配置的正确性...
类加载器的作用不仅仅是加载类,还包括确保类的唯一性,避免重复加载,并且遵循特定的加载顺序。以下是对类加载器原理的详细解释: 1. 类加载器作用: 当JVM启动时,如果需要使用某个类,对应的类加载器会将这个类...
每个类加载器在尝试加载类时,会先委托给父类加载器,只有当父类加载器无法加载时,才会尝试自己加载。这种机制保证了Java的核心类库只被加载一次,同时也确保了类的唯一性。 Tomcat的类加载器工作流程可以总结为:...
### Java 文件加载机制详解 #### 一、概述 在Java应用开发中,特别是Web应用程序的构建过程中,资源文件(如配置文件、属性文件等)的加载是必不可少的一部分。本文将重点探讨Java环境下不同方式下的文件加载方法...
它们按照层次结构工作,遵循"委托模型",即从顶层的启动类加载器开始尝试加载类,如果找不到则逐级向下委托。 要实现动态加载jar文件,我们需要创建自定义的类加载器。这个类加载器需要继承`java.lang.ClassLoader`...
传统的Java应用程序在启动时,由JVM(Java虚拟机)通过类加载器将类加载到内存中,一旦加载完成,除非程序退出,否则这些类通常不会被重新加载。然而,在开发过程中,我们可能希望在不重启应用的情况下,对已加载的...
Java类加载器是Java运行时环境的一个关键组成部分,负责将类文件(.class)从各种来源加载到JVM中。它不仅管理类的生命周期,还确保了类的正确加载和初始化,是Java动态特性的基石。 #### 类加载器的工作原理 Java...
Java类加载器是Java虚拟机(JVM)的关键组成部分,它负责查找并加载类到内存中,使得程序能够运行。自定义Java类加载器允许我们根据特定需求扩展默认的加载机制,例如,从非标准位置加载类或者实现动态加载。在Java...
类加载器是Java运行时环境的重要组成部分,负责加载类到JVM中,并确保类的正确加载与隔离。类加载器的基本结构如下: 1. **引导类加载器**(Bootstrap Class Loader):这是系统默认的类加载器,负责加载JRE/lib/rt...
Java类加载器(ClassLoader)是Java虚拟机(JVM)中的一个重要组成部分,用于将Java类文件加载到JVM中,以便能够执行Java程序。在Java中,类加载器的设计采用了一种称为“双亲委派模式”(Parent Delegation Model)...
Java中的类加载器遵循一个原则叫做“父母委托模型”,即当一个类加载器收到类加载请求时,首先将加载任务委托给父类加载器,只有当父类加载器无法完成加载时才会尝试自己加载。 这种设计模式的好处在于避免了类的...
4. **创建对象与调用方法**:加载类后,我们可以创建对象并调用其公共方法。如果`MyClass`有一个无参构造函数,我们可以这样做: ```java Object instance = clazz.newInstance(); Method method = clazz....
总结起来,Java 类加载器加密是一种增强程序安全性的技术,通过自定义类加载器和解密逻辑,可以在加载类之前对其进行加密,提高代码的保护性。同时,结合反射机制,即使类是加密状态,也能正常执行程序。这种技术常...