java总结(19)类加载器

本文深入探讨Java类加载器的概念及其工作原理,包括类加载过程、类加载器的树状组织结构及自定义类加载器的实现。

类加载器概述

类加载器是 Java 语言的一个创新,也是 Java 语言流行的重要原因之一。它使得 Java 类可以被动态加载到 Java 虚拟机中并执行。类加载器从 JDK 1.0 就出现了,最初是为了满足 Java Applet 的需要而开发出来的。Java Applet 需要从远程下载 Java 类文件到浏览器中并执行。现在类加载器在 Web 容器和 OSGi 中得到了广泛的使用。一般来说,Java 应用的开发人员不需要直接同类加载器进行交互。Java 虚拟机默认的行为就已经足够满足大多数情况的需求了。不过如果遇到了需要与类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就很容易花大量的时间去调试 ClassNotFoundException和 NoClassDefFoundError等异常。本文将详细介绍 Java 的类加载器,帮助读者深刻理解 Java 语言中的这个重要概念。

什么是类加载器

顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载就是在需要的时候把 class 字节码文件从硬盘加载到内存中 JVM 的方法区中,并完成 Verifying、Preparing、Resolving、Initialing,把字节码数据转换为 Class 对象的功能模块,内存中的实例。

框图中各个步骤简单介绍如下:
Loading:文章前面介绍的类加载,将文件系统中的Class文件载入到JVM内存(运行数据区域)
Verifying:检查载入的类文件是否符合Java规范和虚拟机规范。
Preparing:为这个类分配所需要的内存,确定这个类的属性、方法等所需的数据结构。(Prepare a data structure that assigns the memory required by classes and indicates the fields, methods, and interfaces defined in the class.)
Resolving:将该类常量池中的符号引用都改变为直接引用。
Initialing:初始化类的局部变量,为静态域赋值,同时执行静态初始化块。


每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。
基本上所有的类加载器都是 java.lang.ClassLoader类的一个实例。

java.lang.ClassLoader类介绍

java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。不过本文只讨论其加载类的功能。为了完成加载类的这个职责,ClassLoader提供了一系列的方法,具体可以参考jdk文档。有些方法的细节会在下面进行介绍。

ClassLoader 中与加载类相关的方法
方法 说明
getParent() 返回该类加载器的父类加载器。
loadClass(String name) 加载名称为 name的类,返回的结果是 java.lang.Class类的实例。
findClass(String name) 查找名称为 name的类,返回的结果是 java.lang.Class类的实例。
findLoadedClass(String name) 查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。
defineClass(String name, byte[] b, int off, int len)把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。
resolveClass(Class<?> c) 链接指定的 Java 类。
上面给出的方法,表示类名称的 name参数的值是类的二进制名称。需要注意的是内部类的表示,如 com.example.Sample$1和 com.example.Sample$Inner等表示方式。这些方法会在下面介绍类加载器的工作机制时,做进一步的说明。下面介绍类加载器的树状组织结构。

类加载器的树状组织结构

Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:

引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。
扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。


除了引导类加载器之外,所有的类加载器都有一个父类加载器。通过 表 1中给出的 getParent()方法可以得到。对于系统提供的类加载器来说,系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器;对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器 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();
                }
        }

每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 getClassLoader()方法就可以获取到此引用。范例一中通过递归调用 getParent()方法来输出全部的父类加载器。

演示类加载器的树状组织结构的运行结果
sun.misc.Launcher$AppClassLoader@9364b1
sun.misc.Launcher$ExtClassLoader@191d12

说明:第一个输出的是 ClassLoaderTree类的类加载器,即系统类加载器。它是 sun.misc.Launcher$AppClassLoader类的实例;第二个输出的是扩展类加载器,是 sun.misc.Launcher$ExtClassLoader类的实例。需要注意的是这里并没有输出引导类加载器,这是由于有些 JDK 的实现对于父类加载器是引导类加载器的情况,getParent()方法返回 null。

类加载器的代理模式

类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。在介绍代理模式背后的动机之前,首先需要说明一下 Java 虚拟机是如何判定两个 Java 类是相同的。Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。

整个loadClass() 方法的执行步骤如下:
用findLoadedClass(String )来检查是否已经加载类,如果已经加载则直接返回。
在父类加载器上调用loadClass方法,如果父类加载器为null,则使用根类加载器来加载
调用findClass(String)方法查找类
当 JVM 需要一个类A的时候,1.JVM派当前线程的类加载器去加载类A(默认是系统类加载器 appClassLoader,我们也可以自己指定当前线程类加载器);2.如果类A中还使用了类B,那么JVM会派刚刚加载类A的类加载器去加载类B(因为类加载器的可见性问题决定 上层类加载器对底层类加载器中的类不可见,所以只能派当前的类加载器加载,然后再根据委托机制,加载类B的加载器可能是当前类加载器或者是其上级类加载器);3.还可以使用 ClassLoader.loadClass() 方法来指定某个类加载器去加载类。
下层的加载器会将将任务委托给上一层类加载器,上一层加载检查它的命名空间中是否已经加载这个类,如果已经加载,直接使用这个类。如果没有加载,继续往上委托直到顶部。检查完了之后,按照相反的顺序进行加载,如果 Bootstrap 加载器找不到这个类,则往下委托,直到最初委托的类加载器找到类文件,如果最初的类加载器仍然找不到类,则抛出 NoClassFound 异常。对于某个特定的类加载器来说,一个Java 类只能被载入一次,也就是说在Java虚拟机中,类的完整标识是(classLoader,package,className)。一个类可以被不同的类加载器加载。

/** 
 * 需求:获取一般类的类加载器 
 */  
package cn.itcast.day2.classloader;  
  
/** 
 * @author vivianZhao 
 */  
public class ClassLoaderTest {  
  
    /** 
     * @param args 
     */  
    public static void main(String[] args) {  
        // TODO Auto-generated method stub  
        // 获取运行时加载本类的类加载器的类名, 打印结果说明系统默认类加载器是 appClassLoader  
        System.out.println(ClassLoaderTest.class.getClassLoader().getClass().getName());  
          
        // 获取本类的类加载器的类加载器的类名 appClassLoader 是 BootStrap 加载的  
        System.out.println(ClassLoaderTest.class.getClassLoader().getClass().getClassLoader());  
          
        // 获取 appClassLoader 的父类名  
        Class<?> superClass = ClassLoaderTest.class.getClassLoader().getClass();  
        String blankString = "";  
        while (superClass != null) {  
            System.out.println(blankString + superClass.getName());  
            blankString = blankString + "    ";  
            superClass = superClass.getSuperclass();  
        }  
          
        // 获取 appClassLoader 所有上层类加载器  
        blankString = "";  
        ClassLoader superClassLoader = ClassLoaderTest.class.getClassLoader();  
        while (superClassLoader != null) {  
            System.out.println(blankString + superClassLoader.getClass().getName());  
            blankString = blankString + "    ";  
            superClassLoader = superClassLoader.getParent();  
        }  
    }  
}
打印结果为:
null
sun.misc.Launcher$AppClassLoader
    java.net.URLClassLoader
        java.security.SecureClassLoader
            java.lang.ClassLoader
                java.lang.Object
sun.misc.Launcher$AppClassLoader
    sun.misc.Launcher$ExtClassLoader

自定义类加载器

虽然在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输 Java 类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在 Java 虚拟机中运行的类来。

自定义类加载器主要是继承 ClassLoader 类
ClassLoader 类
java.lang.Object
      java.lang.ClassLoader

ClassLoader 中常用的方法
Class<?> loadClass(String name) 
          使用指定的二进制名称来加载类。 loadClass 会委托上级 ClassLoader 类加载该类,如果上级 Classloader 类不能加载该类,它才会调用 findClass 去加载该类
protected  Class<?> findClass(String name) 
          使用指定的二进制名称查找类。 
定义我们自己的 ClassLoader 一般只是重写 findClass 方法,读入我们的字节码文件中的数据存入字节数组中,然后调用 ClassLoader 的 defineClass 方法,完成加载该类Verifying、Preparing、Resolving、Initialing 的工作,defineClass 会返回已经加载完成的 Class 对象。

自定义对class文件进行加密、解密的类加载器

myeclipse 运行的时候的当前用户目录是 project 的根目录。
软件开发中的目录配置的时候最好不要使用有空格的目录,因为很多时候我们的程序在处理配置参数的时候都是把空格当做两个参数的分割符,比如 dos 命令行就是,这样就会把我们一个路径的参数认为是两个参数。
使用异或一个 8 bits 的整数来加密

package cn.itcast.day2.classloader;  
  
import java.io.ByteArrayOutputStream;  
import java.io.FileInputStream;  
import java.io.FileOutputStream;  
import java.io.IOException;  
import java.io.InputStream;  
import java.io.OutputStream;  
  
public class MyClassLoader extends ClassLoader {  
    private String classPath;  
      
    public MyClassLoader() {  
        // TODO Auto-generated constructor stub  
    }  
      
    public MyClassLoader(String classPath) {  
        // TODO Auto-generated constructor stub  
        this.classPath = classPath;  
    }  
      
    /** 
     * @method: main 
     * @description: 对指定目录下的 Class file 进行加密,输出加密后的 class 文件到指定目录 
     * @param args 
     * @return: void 
     * @author: vivianZhao 
     * @date: 2013-7-23 上午11:21:22 
     * @version: 1.0 
     * @throws Exception 
     */  
    public static void main(String[] args) throws Exception {  
        // TODO Auto-generated method stub  
        String srcPath = "D:\\Users\\vivianZhao\\Workspaces\\MyEclipse Professional\\"  
                + "Java5NewFeature\\bin\\cn\\itcast\\day2\\classloader\\ClassLoderAttachment.class";  
        String destPath = "itcastlib";  
        destPath += "\\" + srcPath.substring(srcPath.lastIndexOf('\\') + 1);  
        FileInputStream fileInputStream = new FileInputStream(srcPath);  
        FileOutputStream fileOutputStream = new FileOutputStream(destPath);  
        cypher(fileInputStream, fileOutputStream);  
        fileInputStream.close();  
        fileOutputStream.close();  
    }  
      
    public static void cypher(InputStream inputStream, OutputStream outputStream)  
            throws Exception {  
        int buf;  
        while ((buf = inputStream.read()) != -1) {  
            outputStream.write(buf ^ 0xff);  
        }  
    }  
      
    @Override  
    protected Class<?> findClass(String name) throws ClassNotFoundException {  
        // TODO Auto-generated method stub  
        ByteArrayOutputStream bos = null;  
        try {  
            // 1.先用 File 字节码输入流读入 class 的字节码文件,转存入 ByteArray 输出流中  
            InputStream ins = new FileInputStream(classPath + "//" + name + ".class");   
            bos = new ByteArrayOutputStream();   
            int b = 0;  
            while ((b = ins.read()) != -1) {  
                bos.write(b ^ 0xff);   
            }  
            ins.close();  
            byte[] bytes = bos.toByteArray(); // 2.把 ByteArray 输出流中的字节数据转为 byte[]  
            // 3.使用 defineClass 方法用 byte 数组中的字节码数据定义一个类,返回该类的 Class 对象,我们通过  
            // 使用该 Class 对象来使用这个类  
            return defineClass(null, bytes, 0, bytes.length);  
              
        } catch (Exception e) {  
            e.printStackTrace();  
        } finally{  
            try {  
                bos.close();  
            } catch (IOException e) {  
                // TODO Auto-generated catch block  
                e.printStackTrace();  
            }  
        }  
        return super.findClass(name);  
    }  
  
} 

一个类加载器的高级问题分析

类加载器中的类的可见性问题:下层的加载器能够看到上层加载器中的类,反之则不行,也就是是说委托只能从下到上。
类加载器中依赖类的类加载器和当前类的实际类加载器相同(由于委托机制,类的实际类加载器和指定的类加载器并不一定相同,因为有可能是其上级类加载器加载的),比如 class A extends B,我们在用指定的类加载器加载 A 类之后,系统会自动的加载 B 类,因为 A 类依赖于 B 类,如果依据委托机制最终加载的 A 类的加载器是 ClassLoaderEnd,那么 B 类一会用 ClassLoaderEnd 加载。这就导致,我们依赖的类的类加载器和其上级类加载加载的类,而不能是其下级类加载器加载的类。


所以类加载器的委派机制 和 当前类依赖的类只能和当前类的类加载器相同 这两个原则共同决定了,使用指定类加载器加载我们的类的时候,相互依赖的类一定要放在同一个类加载器管理的空间中,不然可能发生找不到类的情况。

在开发自己的类加载器的时候,需要注意与已有的类加载器组织结构的协调。



评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值