JVM 学习:类加载器

前言

在之前的文章中提过,一个对象包括三个部分:对象头、实例数据和对齐填充(非必须)。对象头包括标志位(Mark Word)、类型指针、数组长度(只针对数组类型)。其中的类型指针指向它的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例。

正题

在开始文章前,有几个问题需要思考一下:

  • 类加载器的作用是什么
  • 类加载器的重要接口方法有哪些
  • 如何自定义加载器
  • 不同加载器加载同一资源文件是否相等
  • JVM 哪些有哪些加载器
  • 类加载过程
1. 类加载器的作用

类加载器虽然只用于实现类的加载动作,但它在 Java 程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类一同确立其在 Java 虚拟机的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类必定不相等。

public class OOMTest {
	public static void main(String[] args) {    
        try{    
            //准备url    
            URL url = new File("C:/Users/Administrator/workspace/ClassTest/src").toURI().toURL();    
            URL[] urls = {url};    
            //定义类加载器     
            URLClassLoader classLoader = new URLClassLoader(urls);
            URLClassLoader classLoader2 = new URLClassLoader(urls);
            //加载ClassA类
            Class<?> outClass = classLoader.loadClass("com.memory.ClassA"); 
            //使用同一个加载器再次加载同一个Class文件
            Class<?> outClass2 = classLoader.loadClass("com.memory.ClassA"); 
            //使用不同加载器加载同一个Class文件
            Class<?> outClass3 = classLoader2.loadClass("com.memory.ClassA"); 
            //查看使用同一个加载器多次加载同一个类的输出
            System.out.println(outClass.equals(outClass2));  
            System.out.println(outClass.equals(outClass3));  
            classLoader.close();
            classLoader2.close();
        } catch(Exception e) {    
            e.printStackTrace();    
        }    
    }    
}

运行结果:

true
false

从结果可以看出:同一个加载器加载同一个 Class 文件,在虚拟机中指向的是同一个 Class 对象;不同加载器加载同一个 Class 文件,在虚拟机中就会存在多个 Class 对象。

2. 类加载器的重要接口方法有哪些

在 Java 中,ClassLoader 是一个抽象类,位于 java.lang 包中。下面对该类的一些重要接口方法进行介绍:

  • Class loadClass(String name):name 参数指定类装载器需要装载类的名字,必须使用全限定类名,如 com.memory.ClassLoaderTest。该方法有一个重载方法 loadClass(String name,boolean resolve),resolve 参数告诉装载器是否需要解析该类。在初始化该类之前,应考虑进行类解析工作,但并不是所有的类都需要解析。如果JVM只需要知道该类是否存在或找出该类的超类,那么就不需要进行解析。
  • Class defineClass(String name, byte[] b, int off, int len):将类文件的字节数组转换成 JVM 内部的 java.lang.Class 对象。字节数组可以从本地文件系统、远程网络获取。参数 name 为字节数组对应的全限定类名。
  • Class findSystemClass(String name):从本地文件系统载入 Class 文件,如果本地文件系统不存在该 Class 文件,则将抛出 ClassNotFoundException 异常。该方法是JVM默认使用的装载机制。
  • Class findLoadedClass(String name):调用该方法来查看 ClassLoader 是否已经装入某个类。如果已装入,那么返回 java.lang.Class 对象;否则返回 null。如果强行装载已存在的类,那么将会抛出链接错误。
  • Class get:Parent():获取装载器的父类装载器。除根装载器外,所有的类装载器有且仅有一个父类装载器。ExtClassLoader 的父类装载器是根装载器,因为根装载器非 Java 语言编写,所以无法获得,将返回null。

除 JVM 默认的 3 个 ClassLoader 外,用户可以编写自己的第三方类装载器,以实现一些特殊的需求。类文件被装载并解析后,在 JVM 内部拥有一个对应的 java.lang.Class 类描述对象,该类的实例都拥有指向这个类描述对象的引用,而类描述对象又拥有指向关联 ClassLoader 的引用:

每个类在 JVM 中都拥有一个对应的 java.lang.Class 对象,它提供了类结构信息的描述。数组、枚举、注解及基本 Java 类型(如 int、double 等)甚至 void 都拥有对应的 Class 对象。Class 没有 public 构造方法。Class 对象是在装载类时由 JVM 通过调用类装载器中的 defineClass() 方法自动构造的。

3. 如何自定义加载器
public class ClassLoaderTest {

	public static void main(String[] args) throws Exception {
		ClassLoader myLoader = new ClassLoader() {
			@Override
			public Class<?> loadClass(String name)
					throws ClassNotFoundException {
				try {
					String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
					InputStream is = getClass().getResourceAsStream(fileName);
					if(is == null) {
						return super.loadClass(name);
					}
					byte[] b = new byte[is.available()];
					is.read(b);
					return defineClass(name, b, 0, b.length);
				} catch (IOException e) {
					throw new ClassNotFoundException(name);
				}
			}
		};
		
		Object object = myLoader.loadClass("com.memory.ClassLoaderTest").newInstance();
		System.out.println(object.getClass());
		System.out.println(object instanceof com.memory.ClassLoaderTest);
	}
}

运行结果:

class com.memory.ClassLoaderTest
false

上面代码只是简单的覆盖 ClassLoader##loadClass 方法来实现自定义类加载逻辑。

4. 不同加载器加载同一资源文件是否相等

这里所指的“相等”,包括代表类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。如果没有注意到类加载器的影响,在某些情况下可能会产生迷惑性的结果。

程序中使用了这个类加载器去加载一个名为“com.memory.ClassLoaderTest”的类,并实例化这个类的对象。从上面的两行输出结果中,从第一句可以看出,这个对象确实是类 com.memory.ClassLoaderTest 实例化出来的对象,但从第二句可以发现,这个对象与类 com.memory.ClassLoaderTest 做所属类型检查的时候却返回了 false,这是因为虚拟机中存在了两个 ClassLoaderTest 类,一个是由系统应用程序类加载的,另一个是由我们自定义的类加载器加载的,虽然都来自同一个 Class 文件,但依然是两个独立的类,做对象所属类型检查是结果自然为 false。

5. JVM 有哪些加载器

从 Java 虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用 C++ 语言实现,是虚拟机自身的一部分;另一个就是所有其他的类加载器,这些类加载器都是由Java语言实现,独立于虚拟机外部,并且都继承自抽象类 java.lang.ClassLoader。

从 Java 开发人员的角度来看,类加载器还可以划分为更细致一些,绝大部分 Java 程序都会使用到一下3种系统提供的类加载器。

5.1 启动类加载器(Bootstrap ClassLoader)

Java 虚拟机中内嵌了一个称为 Bootstrap 的类装载器,它是用特定于操作系统的本地代码实现的,属于 Java 虚拟机的内核。这个类加载器负责加载 Java 核心包中的类,将存放在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义加载器时,如果需要把加载请求委派给引导类加载器,那直接使用 null 代替即可。


看看 java.lang.Class

     /**
     * Returns the class loader for the class.  Some implementations may use
     * null to represent the bootstrap class loader. This method will return
     * null in such implementations if this class was loaded by the bootstrap
     * class loader.
      **/
    public ClassLoader getClassLoader() {
        ClassLoader cl = getClassLoader0();
        if (cl == null)
            return null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
        }
        return cl;
    }
5.2 扩展类加载器(Extension ClassLoader)

ExtClassLoader 是 Java 核心包的加载器,使用 Java 语言编写的 Java 类,这个加载器由 sun.misc.Launcher$ExtClassLoader 实现,它负责加载 <JAVA_HOME>\jre\lib\ext 目录中下的 Jar 包中的类,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。


5.3 应用程序类加载器(Application ClassLoader)

APPClassLoader是Java核心包的加载器,使用Java语言编写的Java类,这个类加载器由sun.misc.Launcher$ApplicationLoader 实现。由于这个类加载器是 classLoader 中的 getSystemClassLoader() 方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库或者加载应用程序的启动执行类,当使用 java 命令去启动执行一个类时,Java 虚拟机使用 APPClassLoader 加载这个类。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下就是程序默认的类加载器。

5.4 用户自定义类加载器

用户通过覆盖 ClassLoader 类的某些方法来实现自定义加载器。

接下来通过在工程里把自定义的 Jar 包存放到不同的目录来看看加载类时对应的类加载器。在工程里引入 SFJun.jar,然后在 main 函数里创建该类的对象实例:

public class JunTest {
    public static void main(String[] args) {
        ChaRuAlg alg = new ChaRuAlg();
        System.out.println(alg.getClass().getClassLoader().getClass());
    }
}

运行结果:

class sun.misc.Launcher$AppClassLoader

如果把 SFJun.jar 文件放到 <JAVA_HOME>\jre\lib\ext 目录下:

输出结果:

class sun.misc.Launcher$ExtClassLoader
从上面的输出结果可知:不同的类加载器根据指定路径来加载其包含的文件。
6、类加载过程

我们的应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。这些类加载的关系如下图:


上图展示了类加载器之间的这种层次关系,称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用类加载器的代码。

类加载器的双亲委派模型在 JDK1.2 期间被引入并被广泛应用于之后几乎所有的 Java 程序中,但它并不是一个强制性的约束模型,而是 Java 设计者推荐给开发者的一种类加载器实现方式。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子类加载器才会尝试自己去加载。当要加载一个类时,调用的是 ClassLoader 的 loadClass 方法,loadClass 方法先查找这个类是否已经被加载,如果没有加载则委托其父级类装载器去加载这个类,如果父级类装载器无法装载这个类,子级类装载器才调用自己内部的 findClass 方法去进行真正的加载。父级类装载器调用 loadClass 方法去装载一个类时,它也是先查找其父级类装载器,这样一直追溯到没有父级的类装载器时(例如 ExtClassLoader),则使用 Java 虚拟机内嵌的 BootStrap 类装载器进行装载,当 BootStrap 无法加载当前所要加载的类时,然后才一级级回退到子孙类装载器去进行真正的加载。当回退到最初的类装载器时,如果它自己也不能完成类的装载,那就应报告 ClassNotFoundException 异常。

一个类装载器只能创建某个类的一份字节码数据,即只能为某个类创建一个与之对应的 Class 实例对象,而不能为同样的一个类创建多个 Class 实例对象。在一个 Java 虚拟机中可以存在多个类装载器,每个类装载器都拥有自己的命名空间,对于同一个类,每个类装载器都可以创建出它的一个 Class 实例对象,即每个类装载器都可以分别创建出某个类的一份字节码数据。两个类装载器分别创建的同一个类的字节码数据属于两个完全不同的对象,相互之间没有任何关联,例如,在某个类中定义了一个静态成员变量,它在不同的类装载器之间是不可以实现数据共享的。采用委托模式给类的加载管理带来了明显的好处,当父级的类装载器加载了某个类,那么子级的类装载器就不要再去加载这个类,这样就可以避免一个 Java 虚拟机中的多个类装载器为同一个类创建多份字节码数据的情况。只要开发人员自定义的类装载器不覆盖 ClassLoader 的 loadClass 方法,而是覆盖其 findClass 方法,这样就可以继续采用委托模式。

如果在类 A 中使用 new 关键字创建类 B,Java 虚拟机将使用加载类 A 的类装载器来加载类 B。如果在一个类中调用 Class.forName 方法来动态加载另一个类,那么通过传递给 Class.forName 方法的一个参数类指定另外那个类的类装载器,如果没有指定该参数,则使用加载当前类的类装载器。

依据一个类的存放位置,这个类最终只能由一个特定的类装载器装载。对于一个已被父级类装载器装载的类来说,Java 虚拟机默认也使用这个父级类装载器去装载它所调用的其他类,由于父级装载器不会委托子级类装载器去装载类,所以,在一般情况下,一个已经被父级装载器装载的类无法调用那些只能被子级类装载器发现和装载的其他类。

接下来我们把 SFJun.jar 放到 <JAVA_HOME>\jre\lib\ext 目录下,把 SFJun1.jar 放到启动执行类路径下(ChaRuAlg 类在 SFJun.jar 包下; StaticDispatch 类在 SFJun1.jar 包下):

public class JunTest {
    public static void main(String[] args) {
        ChaRuAlg alg = new ChaRuAlg();
        StaticDispatch staticDispatch = alg.getDispatch();
        System.out.println(alg.getClass().getClassLoader().getClass());
        System.out.println(staticDispatch.getClass().getClassLoader().getClass());
    }
}

输出结果:

Exception in thread "main" java.lang.NoClassDefFoundError: org/fenixsoft/polymorphic/StaticDispatch
	at com.mdj.sf.ChaRuAlg.getDispatch(ChaRuAlg.java:8)
	at com.jun.JunTest.main(JunTest.java:9)
Caused by: java.lang.ClassNotFoundException: org.fenixsoft.polymorphic.StaticDispatch
	at java.net.URLClassLoader$1.run(URLClassLoader.java:372)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:360)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 2 more

从以上的输出结果可知:一个已经被父级装载器装载的类无法调用那些只能被子级类装载器发现和装载的其他类。

每个运行中的线程都有一个关联的上下文类装载器,可以使用 Thread.setContextClassLoader() 方法设置线程的上下文类装载器。每个线程默认的上下文类装载器是其父线程的上下文类装载器,而主线程的类装载器初始被设置为 ClassLoader.getSystemClassLoader() 方法返回的系统类装载器。当线程中运行的代码需要使用某个类时,它使用上下文类装载器来装载这个类,上下文类装载器首先会委托它的父级类装载器来装载这个类,如果父级的类装载器无法装载时,上下文类装载器才自己进行装载。

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object,它存放在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为 java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。

一个 Java 虚拟机中的所有类装载器采用具有父子关系的树形结构进行组织,在实例化每个类装载器对象时,需要为其指定一个父级类装载器对象。如果没有指定的话,则以 ClassLoader.getSystemClassLoader() 方法返回的系统类装载器作为其父级类装载器对象。系统类装载器通常被设置为启动应用程序的 AppClassLoader,它是在 getSystemClassLoader() 方法第一次被调用时设置的,而 getSystemClassLoader() 方法的第一次调用发生在应用程序启动的早期阶段,可以通过 java.system.class.loader 系统属性来讲系统类装载器设置为其他类装载器。ExtClassLoader 是 AppClassLoader 的父级类装载器,ExtClassLoader 没有父级类装载器。

双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委派的代码都集中在  java.lang.ClassLoader 的 loadClass() 方法之中。

    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;
        }
    }

从上面的代码可知:先检查类是否已经被加载过,若没有加载则调用父类加载器的 loadClass() 方法,若父类加载器为空则默认使用启动类加载器作为父类加载器。如果父类加载失败,抛出 ClassNotFoundException 异常后,再调用自己的 findClass() 方法进行加载。调用 findClass() 时会先加载其继承的父类。

类加载器过程示例:

public class ClassA extends ClassB{
}

public class ClassLoaderTest extends ClassA{
}
ClassLoaderTest 类和 ClassA 加载图:


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值