类加载器

1 什么是类加载器

类加载器,顾名思义,就是把类加载到JVM的一个模块。一般来说JVM利用类加载器读取一个.class字节码文件并将其转换为java.lang.Class对象。然后可以通过Class的newInstance方法生成一个类的实例。基本上所有的类加载器都是 java.lang.ClassLoader类的一个实例。而实际上,ClassLoader除了能将Class加载到JVM中之外,还有一个重要的作用就是判断一个类该由谁加载,它是一个父级优先的加载机制,这就是双亲委派模型。

2 类加载器的结构

2.1 类加载器的常用方法

我们在使用类加载器的时候,经常会用到或拓展ClassLoader,下面是它的几个主要方法。

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类。


defineClass方法用来将byte字节流解析成JVM能识别的Class对象,有了这个方法意味着我们不仅可以通过class文件实例化对象,还可以通过类似网络接收的方法获取字节流然后生成Class对象。这也是Java发明类加载器的原因。期初只是为了方便JavaApplet,因为需要从远程下载 Java 类文件到浏览器中并执行。现在虽然已经几乎看不到JavaApplet的身影了,但是类加载器依然有很大的使用空间。

defineClass方法通常和findClass方法一起使用,我们可以直接通过覆盖ClassLoader父类的findClass方法来实现类的加载规则,从而取得要加载的类的字节码。然后调用defineClass方法生成类的Class对象


2.2 ClassLoader的等级加载机制

等级加载机制,也就是我们经常看到的双亲委派模型。从Java虚拟机的角度来说,分类两类不同的加载器,一种是启动类加载器(Bootstrap ClassLoader),这个类加载器是由C++语言实现,它是虚拟机自身的一部分;另一种就是其他类的加载器,他们都继承自ClassLoader抽象类。下面是类加载器的结构图

  • BootstrapClassloader :它的主要工作就是加载JVM自身工作需要的类,它完全由JVM控制,别人也访问不到这个类,它既没有更高一级的父加载器,也没有子加载器。所有上图中它并没有与下面的加载器连接在一起。因为对于某些虚拟机的实现来说,当一个类的parent为BootstrapClassLoader的时候,它的getParent方法返回null。它默认加载<JAVA_HOME>\lib目录中的的jar包,但只能加载被虚拟机识别的(如rt.jar等),因此即使自己写一个jar包放在这个目录下,也不会被加载的。
  • ExtClassLoader:这个加载器由sun.misc.Launcher$ExtClassLoader实现,可以看出它是Launcher的一个内部类,它负责加载<JAVA_HOME>\lib\ext目录中的或者被java.ext.dirs系统变量执行的路径中的类库。
  • AppClassLoader:它的父类是ExtClassLoader,它是由sun.misc.Launcher$AppClassLoader实现的,它同样是Launcher的内部类。因为它是getSystemClassLoader()方法的返回值,所以通常称为系统类加载器。它负责加载用户自定义的类,也就是说我们自己写的Java代码在没有指定类加载器的时候全部由它来加载,而我们自定义的加载器也都是它的子加载器。

下面通过一个例子来看看类加载器的结构,本例中我自定义了一个MyClassLoader类加载器,并用它实例化了一个对象。通过下面的代码来获取它的加载器“树“。

public static void getParentClassLoader(Object object)
	{
		ClassLoader loader=object.getClass().getClassLoader();
		while(loader!=null)
		{
			System.out.println(loader);
			loader=loader.getParent();
		}
	}
输出结果如下:

test.MyClassLoader@74a14482
sun.misc.Launcher$AppClassLoader@4e0e2f2a
sun.misc.Launcher$ExtClassLoader@3d4eac69


我们可以看到并没有输出BootstrapClassloader,这也跟我们上面的图是吻合的。在Java对getParent()方法描述中,有这样一段话,这是由于有些JVM的实现对于父类加载器是引导类加载器的情况,getParent()方法返回null下面来看一下Object的类加载器情况,它的输出是不是更加可以理解上面那个getParent的描述,而且由此可以知道Object类就是由BootstrapClassloader加载的。

ClassLoader classLoader=Object.class.getClassLoader();
		System.out.println(classLoader);
输出结果如下:

Null

3 双亲委派模型

3.1 父亲办不到啊!

在上面我们曾提到双亲委派模型,也提到getParent方法,从类加载器的结构图上来看也有了一些认识。这种层次结构就称为双亲委派模型。在双亲委派模型中,除了顶层的引导加载器,其他的加载器都有自己的父类加载器。

在这种模式下,一个加载器在收到加载请求的时候,它并不是去尝试加载这个类,而是将这个请求委派给它的父类加载器去完成,直到顶层加载器。只有当父类加载器办不到的时候,才会尝试自己去加载。

下面来看看Java源码就更好理解了:

 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);//首先利用parent去加载
                    } 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;
        }
    }

3.2 类的唯一性

在Java中,对于任意一个类,都需要由加载器它的类加载器和这个类本身一同确立其在Java中的唯一性,每一个类加载器都有一个独立的类命名空间。也就是说想比较两个类是否相等,只有相同的类加载器加载才有意义。来看下面这个例子:

Object example=(new MyClassLoader()).findClass("test.Example").newInstance();
		System.out.println(example.getClass());
		System.out.println(example instanceof Example);

输出结果如下:
class test.Example
false

这里面在test包下面定义了Example类,同时自定义了一个类加载器,并用它去加载test.Example类并实例化对象。但是当利用instance of检查类型的时候却返回false。这是因为此时虚拟机中有两个Example的Class,一个是由系统默认加载的,一个是由自定义加载器加载的。虽然来自同一个Class文件,但是依然是两个独立的类。

看过上面的例子再来看看双亲委派模型的意义。使用了双亲委派模型以后,所有的类也就根据他们的类加载器定义了一个等级。如java.lang.Object类在rt.jar中,有双亲委派模型可以知道,委派到Bootstrap ClassLoader就可以加载了,因此Object类在程序的各种类加载器环境中都是一个类。如果没有这种模型,各个加载器自行去加载的话,如果我们自己定义一个java.lang.Object的类话,那系统中就会出来很多个Object类,就相当于动摇了Java的根基,也就无法生存的。

 

4 类加载器的几个异常

4.1 ClassNotFoundException

ClassNotFoundException恐怕是Java程序员经常碰到的异常。这个异常通常是发生在显式加载类的时候,如调用Class中的forName()方法,ClassLoader中的loadClass()方法或ClassLoader中的findSystemClass方法。这个异常主要是JVM在加载需要的字节码文件时,没有找到文件,解决办法就是检查classpath路径。获取classpath的方法:

this.getclass().getclassLoader.getResource(“”).toString();

 

4.2 NoClassDefFoundError

在Java文档中,对这个错误的定义如下:

Thrown ifthe Java Virtual Machine or a ClassLoader instance tries to load in thedefinition of a class (as part of a normal method call or as part of creating anew instance using the new expression) and no definition of the class could befound.

The searched-forclass definition existed when the currently executing class was compiled, butthe definition can no longer be found.

也就是在使用New关键字、属性引用某个类、继承了某个接口或类,已经方法的某个参数引用了某个类,这时会触发JVM隐式加载这些类,但发现不存在。

解决这个问题的办法就是确保每个类引用的类都在classpath下面。


5 自定义类加载器

Classloader到底有啥用?前面已经说过了,那既然有JDK提供的,为啥还要自己定义。其实大多数情况下并不需要自定义,但当我们要加载的类是经过特殊处理(如加密等)的时候,就需要自定义加载器了。同时当需要实现类的热部署的时候,自定义类加载器就不可或缺了。

下面通过重写findClass方法类实现一个类加载器

/**
 * 自定义类加载器
 * @author songxu
 *
 */
public class MyClassLoader extends ClassLoader {
	String dirPath=ReloadTest.class.getResource("").getPath();
	String srcPath=dirPath.substring(0,dirPath.indexOf("test"));
	static byte[]cachedata =new byte[1024];
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		
		byte [] classData=getData(name);
		if(diff(classData, cachedata))
		{
			cachedata=classData;
			System.out.println("新的数据加载");
		}
		
		if(null==classData)
		{
			System.out.println("类名为:"+name+"无法加载");
			return super.loadClass(name);
		}
		else {
			return defineClass(null,cachedata,0,cachedata.length);
		}
		
		
		
		
	}
	/**
	 * 获取字节流
	 * @param classname
	 * @return
	 */
	private byte [] getData(String classname)
	{
	
		String path=srcPath+classname.replace('.', File.separatorChar)+".class";
		File file=new File(path);
		System.out.println("最后修改时间:"+getLastModify(file.lastModified()));
		FileInputStream  inputStream=null;
		try {
			
			inputStream=new FileInputStream(path);
			byte [] buffer=new byte[inputStream.available()];
			inputStream.read(buffer);
			return buffer;
			
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		finally
		{
			try {
				inputStream.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		
		
		return null;
				
		
		
	}
	/**
	 * 获取类的最后修改时间
	 * @param time
	 * @return
	 */
	private String getLastModify(long time)
	{
		SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
		Date date=new Date(time);
		
		return simpleDateFormat.format(date);
				
		
		
	}
	/**
	 * 检查是否有新的类加载
	 * @param newdata
	 * @param cached
	 * @return
	 */
	private boolean diff(byte []newdata,byte[]cached)
	{
		
		if(newdata.length!=cached.length)
		{
			return true;
		}
		for(int i=0;i<newdata.length;i++)
		{
			if(newdata[i]!=cached[i])
			{
				return true;
			}
		}
		
		return false;
	}
		

}

5.1是findClass还是loadClass?

对于自己定义类加载器,实际上还可以重写loadClass方法,从JDK源码可以看出,在这个方法中实现了双亲委派模型,而如果我们覆盖了loadClass方法,就很容易破坏到这个委派模型。重新findClass方法还是比较保险的,因为在loadClass方法中,如果双亲没有加载这个类,就会自动调用findClass方法。












 


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值