大家都知道,当我们写好一个 Java 程序之后,不是管是 C/S 还是 B/S 应用,都是由若干个 .class 文件组织而成的一个完整的 Java 应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的 class 文件当中,所以经常要从这个 class 文件中要调用另外一个 class 文件中的方法,如果另外一个文件不存在的,则会引发系统异常。而程序在启动的时候,并不会一次性加载程序所要用的所有 class 文件,而是根据程序的需要,通过 Java 的类加载机制(ClassLoader)来动态加载某个 class 文件到内存当中的,从而只有 class 文件被载入到了内存之后,才能被其它 class 所引用。所以 ClassLoader 就是用来动态加载 class 文件到内存当中用的。
为了更好的理解类的加载机制,我们来深入研究一下
和他的方法。
- ClassLoader
- public abstract class ClassLoader
ClassLoader 类是一个抽象类,sun 公司是这么解释这个类的:
- /**
- * A class loader is an object that is responsible for loading classes. The
- * class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to
- * locate or generate data that constitutes a definition for the class. A
- * typical strategy is to transform the name into a file name and then read a
- * "class file" of that name from a file system.
- **/
大致意思如下:
以下是 ClassLoader 常用到的几个方法及其重载方法:
- protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException
- {
- synchronized (getClassLoadingLock(name)) {
- // First, check if the class has already been loaded
- Class c = findLoadedClass(name);
- if (c == null) {
- long t0 = System.nanoTime();
- try {
- if (parent != null) {
- c = parent.loadClass(name, false);
- } else {
- c = findBootstrapClassOrNull(name);
- }
- } catch (ClassNotFoundException e) {
- // ClassNotFoundException thrown if class not found
- // from the non-null parent class loader
- }
- if (c == null) {
- // If still not found, then invoke findClass in order
- // to find the class.
- long t1 = System.nanoTime();
- c = findClass(name);
- // this is the defining class loader; record the stats
- sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
- sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
- sun.misc.PerfCounter.getFindClasses().increment();
- }
- }
- if (resolve) {
- resolveClass(c);
- }
- return c;
- }
- }
该方法大概意思:
以下内容是上述程序从本机 JDK 环境所获得的结果: 其实上述结果也是通过查找 sun.boot.class.path 这个系统属性所得知的。
- public class BootStrapTest
- {
- public static void main(String[] args)
- {
- URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
- for (int i = 0; i < urls.length; i++) {
- System.out.println(urls[i].toExternalForm());
- }
- }
- }
- System.out.println(System.getProperty("sun.boot.class.path"));
- 打印结果:C: \Java\jdk1.8.0_60\jre\lib\resources.jar;
- C: \Java\jdk1.8.0_60\jre\lib\rt.jar;
- C: \Java\jdk1.8.0_60\jre\lib\sunrsasign.jar;
- C: \Java\jdk1.8.0_60\jre\lib\jsse.jar;
- C: \Java\jdk1.8.0_60\jre\lib\jce.jar;
- C: \Java\jdk1.8.0_60\jre\lib\charsets.jar;
- C: \Java\jdk1.8.0_60\jre\lib\jfr.jar;
- C: \Java\jdk1.8.0_60\jre\classes
目下的所有 jar。
- JAVA_HOME/jre/lib/ext/
来获取它。 除了系统提供的类加载器以外,开发人员可以通过继承
- ClassLoader.getSystemClassLoader()
类的方式实现自己的类加载器,以满足一些特殊的需求。 除了引导类加载器之外,所有的类加载器都有一个父类加载器。 给出的 getParent() 方法可以得到。对于
- java.lang.ClassLoader
使用的是双亲委托模型来搜索类的,每个
- ClassLoader
实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(
- ClassLoader
)本身没有父类加载器,但可以用作其它 ClassLoader 实例的的父类加载器。当一个
- Bootstrap ClassLoader
实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器
- ClassLoader
试图加载,如果没加载到,则把任务转交给
- Bootstrap ClassLoader
试图加载,如果也没加载到,则转交给
- Extension ClassLoader
进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等 URL 中加载该类。如果它们都没有加载到这个类时,则抛出
- App ClassLoader
异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的 Class 实例对象。
- ClassNotFoundException
因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要
再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的 String 来动态替代 java 核心 api 中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为 String 已经在启动时就被引导类加载器(
- ClassLoader
)加载,所以用户自定义的
- Bootstrcp ClassLoader
永远也无法加载一个自己写的 String,除非你改变 JDK 中
- ClassLoader
搜索类的默认算法。
- ClassLoader
JVM 在判定两个 class 是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM 才认为这两个 class 是相同的。就算两个 class 是同一份 class 字节码,如果被两个不同的 ClassLoader 实例所加载,JVM 也会认为它们是两个不同 class。比如网络上的一个 Java 类
,javac 编译之后生成字节码文件
- org.classloader.simple.NetClassLoaderSimple
,
- NetClassLoaderSimple.class
和
- ClassLoaderA
这两个类加载器并读取了
- ClassLoaderB
文件,并分别定义出了
- NetClassLoaderSimple.class
实例来表示这个类,对于 JVM 来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个 Class 实例生成具体的对象进行转换时,就会抛运行时异常
- java.lang.Class
,提示这是两个不同的类型。现在通过实例来验证上述所描述的是否正确: 1)、在 web 服务器上建一个
- java.lang.ClassCaseException
类
- org.classloader.simple.NetClassLoaderSimple.java
- public class NetClassLoaderSimple
- {
- private NetClassLoaderSimple instance;
- public void setNetClassLoaderSimple(Object object){
- this.instance = (NetClassLoaderSimple)object;
- }
- }
类的
- org.classloader.simple.NetClassLoaderSimple
方法接收一个 Object 类型参数,并将它强制转换成
- setNetClassLoaderSimple
类型。
- org.classloader.simple.NetClassLoaderSimple
2)、测试两个 class 是否相同
- NetWorkClassLoader.java
- package classloader;
- public class NewworkClassLoaderTest {
- public static void main(String[] args) {
- try {
- //测试加载网络中的class文件
- String rootUrl = "http://localhost:8080/httpweb/classes";
- String className = "org.classloader.simple.NetClassLoaderSimple";
- NetworkClassLoader ncl1 = new NetworkClassLoader(rootUrl);
- NetworkClassLoader ncl2 = new NetworkClassLoader(rootUrl);
- Class clazz1 = ncl1.loadClass(className);
- Class clazz2 = ncl2.loadClass(className);
- Object obj1 = clazz1.newInstance();
- Object obj2 = clazz2.newInstance();
- clazz1.getMethod("setNetClassLoaderSimple", Object.class).invoke(obj1, obj2);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
首先获得网络上一个 class 文件的二进制名称,然后通过自定义的类加载器 NetworkClassLoader 创建两个实例,并根据网络地址分别加载这份 class,并得到这两个 ClassLoader 实例加载后生成的 Class 实例 clazz1 和 clazz2,最后将这两个 Class 实例分别生成具体的实例对象 obj1 和 obj2,再通过反射调用 clazz1 中的 setNetClassLoaderSimple 方法。
3)、查看测试结果
结论:从结果中可以看出,运行时抛出了
异常。虽然两个对象 obj1 和 obj2 的类的名字相同,但是这两个类是由不同的类加载器实例来加载的,所以 JVM 认为它们就是两个不同的类。
- java.lang.ClassCastException
了解了这一点之后,就可以理解代理模式的设计动机了。代理模式是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用 java.lang.Object 类,也就是说在运行的时候,java.lang.Object 这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object 类,而且这些类之间是不兼容的。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。
不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。
测试一:
- public class ClassLoaderTree
- {
- public static void main(String[] args) {
- ClassLoader loader = ClassLoaderTree.class.getClassLoader();
- while (loader!=null){
- System.out.println(loader.toString());
- loader = loader.getParent();
- }
- System.out.println(loader);
- }
- }
每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 getClassLoader() 方法就可以获取到此引用。代码中通过递归调用 getParent() 方法来输出全部的父类加载器。
结果是:
第一个输出的是
类的类加载器,即系统类加载器。它是
- ClassLoaderTree
类的实例;第二个输出的是扩展类加载器,是
- sun.misc.Launcher$AppClassLoader
类的实例。需要注意的是这里并没有输出引导类加载器,这是由于有些 JDK 的实现对于父类加载器是引导类加载器的情况,getParent() 方法返回 null。第三行结果说明:
- sun.misc.Launcher$ExtClassLoader
的类加器是
- ExtClassLoader
,因为
- Bootstrap ClassLoader
不是一个普通的 Java 类,所以
- Bootstrap ClassLoader
的
- ExtClassLoader
,所以第三行的打印结果为 null 就是这个原因。
- parent=null
测试二:
将 ClassLoaderTree.class 打包成 ClassLoaderTree.jar,放到 Extension ClassLoader 的加载目录下(JAVA_HOME/jre/lib/ext),然后重新运行这个程序,得到的结果会是什么样呢?
此处我在 IDEA 中的运行结果还和上面的一样,与文章 中的有差距,具体原因未弄清楚,还希望读者能够亲自测试。
那文章中的结果是:
打印结果分析:
为什么第一行的结果是 ExtClassLoader 呢?
第二行的结果为 null,是因为 ExtClassLoader 的父类加载器是 Bootstrap ClassLoader。
一个应用程序总是由 n 多个类组成,Java 程序启动时,并不是一次把所有的类全部加载后再运行,它总是先把保证程序运行的基础类一次性加载到 jvm 中,其它类等到 jvm 用到的时候再加载,这样的好处是节省了内存的开销,因为 java 最早就是为嵌入式系统而设计的,内存宝贵,这是一种可以理解的机制,而用到时再加载这也是 java 动态性的一种体现。
。。
ClassNotFoundExecption 异常是平常碰到的最多的。这个异常通常发生在显示加载类的时候。
- public class ClassNotFoundExceptionTest
- {
- public static void main(String[] args) {
- try {
- Class.forName("NotFoundClass");
- }catch (ClassNotFoundException e){
- e.printStackTrace();
- }
- }
- }
显示加载一个类通常有:
出现这种错误其实就是当 JVM 要加载指定文件的字节码到内存时,并没有找到这个文件对应的字节码,也就是这个文件并不存在。解决方法就是检查在当前的 classpath 目录下有没有指定的文件。
在 JavaDoc 中对 NoClassDefFoundError 的产生可能的情况就是使用 new 关键字、属性引用某个类、继承了某个接口或者类,以及方法的某个参数中引用了某个类,这时就会触发 JVM 或者类加载器实例尝试加载类型的定义,但是该定义却没有找到,影响了执行路径。换句话说,在编译时这个类是能够被找到的,但是在执行时却没有找到。
解决这个错误的方法就是确保每个类引用的类都在当前的 classpath 下面。
该错误通常是在 JVM 启动的时候,如果 JVM 中的某个 lib 删除了,就有可能报这个错误。
- public class UnsatisfiedLinkErrorTest
- {
- public native void nativeMethod();
- static {
- System.loadLibrary("NoLib");
- }
- public static void main(String[] args) {
- new UnsatisfiedLinkErrorTest().nativeMethod(); //解析native标识的方法时JVM找不到对应的库文件
- }
- }
该错误通常出现强制类型转换时出现这个错误。
- public class ClassCastExceptionTest
- {
- public static Map m = new HashMap(){
- {
- put("a", "2");
- }
- };
- public static void main(String[] args) {
- Integer integer = (Integer) m.get("a"); //将m强制转换成Integer类型
- System.out.println(integer);
- }
- }
注意:JVM 在做类型转换时的规则:
如果不满足上面的规则,JVM 就会报错,有两种方式可避免错误:
上面代码中改成如下就可以避免错误了:
- public class ExceptionInInitializerErrorTest {
- public static Map m = new HashMap() {
- {
- m.put("a", "2");
- }
- };
- public static void main(String[] args) {
- Integer integer = (Integer) m.get("a");
- System.out.println(integer);
- }
- }
在初始化这个类时,给静态属性 m 赋值时出现了异常导致抛出错误 ExceptionInInitializerError。
NoSuchMethodError 代表这个类型确实存在,但是一个不正确的版本被加载了。为了解决这个问题我们可以使用 '-verbose:class' 来判断该 JVM 加载的到底是哪个版本。
有时候事情会变得更糟,和 ClassCastException 本质一样,加载自不同位置的在同一段逻辑(比如:方法)中交互时,会出现 LinkageError 。
LinkageError 需要观察哪个类被不同的类加载器加载了,在哪个方法或者调用处发生(交汇)的,然后才能想解决方法,解决方法无外乎两种。第一,还是不同的类加载器加载,但是相互不再交汇影响,这里需要针对发生问题的地方做一些改动,比如更换实现方式,避免出现上述问题;第二,冲突的类需要由一个 Parent 类加载器进行加载。LinkageError 和 ClassCastException 本质是一样的,加载自不同类加载器的类型,在同一个类的方法或者调用中出现,如果有转型操作那么就会抛 ClassCastException ,如果是直接的方法调用处的参数或者返回值解析,那么就会产生 LinkageError 。
。。参见书籍《深入分析 Java Web 技术内幕》
ClassLoader 能够完成的事情有以下情况:
虽然在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输 Java 类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在 Java 虚拟机中运行的类来。
定义自已的类加载器分为两步:
1、继承 java.lang.ClassLoader
2、重写父类的 findClass 方法
加载存储在文件系统上的 Java 字节代码。
- public class FileSystemClassLoader extends ClassLoader
- {
- private String rootDir;
- public FileSystemClassLoader(String rootDir){
- this.rootDir = rootDir;
- }
- protected Class findClass(String name) throws ClassNotFoundException {
- byte[] classData = getClassData(name);
- if (classData == null){
- throw new ClassNotFoundException();
- }
- else {
- return defineClass(name, classData, 0, classData.length);
- }
- }
- private byte[] getClassData(String className) {
- String path = classNameToPath(className);
- try {
- InputStream ins = new FileInputStream(path);
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- int bufferSize = 4096;
- byte[] buffer = new byte[bufferSize];
- int bytesNumRead = 0;
- while ((bytesNumRead = ins.read(buffer)) != -1){
- baos.write(buffer, 0, bytesNumRead);
- }
- return baos.toByteArray();
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- }
- return null;
- }
- private String classNameToPath(String className) {
- return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
- }
- }
类 FileSystemClassLoader 继承自类 java.lang.ClassLoader。java.lang.ClassLoader 类的方法 loadClass() 封装了前面提到的代理模式的实现。该方法会首先调用 findLoadedClass() 方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的 loadClass() 方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用 findClass() 方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass() 方法,而是覆写 findClass() 方法。
类 FileSystemClassLoader 的 findClass() 方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文
件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。
一个网络类加载器来说明如何通过类加载器来实现组件的动态更新。即基本的场景是:Java 字节代码(.class)文件存放在服务器上,客户端通过网络的方式获取字节代码并执行。当有版本更新的时候,只需要替换掉服务
器上保存的文件即可。通过类加载器可以比较简单的实现这种需求。
类 NetworkClassLoader 负责通过网络下载 Java 类字节代码并定义出 Java 类。它的实现与 FileSystemClassLoader 类似。在通过 NetworkClassLoader 加载了某个版本的类之后,一般有两种做法来使用它。第一种做法是使用 Java 反射 API。另外一种做法是使用接口。需要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,因为客户端代码的类加载器找不到这些类。使用 Java 反射 API 可以直接调用 Java 类的
方法。而使用接口的做法则是把接口的类放在客户端中,从服务器上加载实现此接口的不同版本的类。在客户端通过相同的接口来使用这些实现类。
网络类加载器的代码:
对于运行在 Java EE? 容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。
绝大多数情况下,Web 应用的开发人员不需要考虑与类加载器相关的细节。下面给出几条简单的原则:
本篇文章详细深入的介绍了 ClassLoader 的工作机制,还写了如何自己实现所需的 ClassLoader 。
1、
2、
3、
4、
5、《深入分析 Java Web 技术内幕》修订版 —— 深入分析 ClassLoader 工作机制
来源: http://www.bubuko.com/infodetail-1966300.html