Java的类加载学习笔记

本文是关于Java类加载的学习笔记,详细介绍了类加载器的作用、三种主要的类加载器(Bootstrap Classloader、Extension Classloader、Application Classloader)以及双亲委托机制,解释了类加载器如何避免重复加载和保证安全性。此外,还讨论了自定义类加载器的使用场景和实现方式,包括类加载器的API方法,并对比了.class和getClass()的区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

      最近总是被类加载器搞混,于是下决心好好的去看一下,总结一下类加载器的内容,之前也看过很多博客,但是没有总结过,也没有细细的去看过,隔一段时间,好像又忘记了,趁着这次机会,好好学习一下,自己总结一下,这样也能加深对类加载器的理解,有不对的地方,请大家指正,当然网上有非常的多的博客可以参考,有的写的也非常的好,这里只是一个学习笔记。

   一、java的类加载器

     类加载器其实是将java的类的编译文件class文件加载到JVM虚拟机中,程序就会正常运行了。在jvm启动的时候并不是一次性把所有的class文件都加载到内存中,而是根据需要动态的进行加载,当然有一部分class文件需要已启动就加载到内存中,否则jvm就无法正常运行了。

首先,我们通过一个例子来看看什么时候java的类加载器,其实如果用过反射的话,应该都清楚经常要用到classLoader这方面的参数。

@Test
 public void testClazzLoader(){
     ClassLoader loader = Test.class.getClassLoader();
     System.out.println("class loader:"+loader.toString());
		
}

看到上面的例子,一个非常简单的获取class的例子,得到class对象之后,调用对应的方法,getClassLoader,可以得到我们想要的ClassLoader对象,下面我们输出一下这个对象:

class loader:sun.misc.Launcher$AppClassLoader@776ec8df

我们可以看到输出的对象是AppClassLoader这个类,那么这个是什么呢,这个就是java的三大类加载器之一。先不管这个是什么加载器,那么三大加载器是什么呢?

三种类加载器:Bootstrap Classloader, Extension Classloader, Application Classloader。

这里先做一个简单的介绍:

Bootstrap Classloader: 最顶层的类加载器,加载核心库,比如我们熟悉的%JRE_HOME%\lib的一些jar包等。

Extension Classloader:扩展的类加载器,主要加载%JRE_HOME%\lib\ext下面的jar包。

Application Classloader: 应用程序类加载器,该加载器由sun.misc.Launcher$AppClassloader()方法直接获取,所以又称为系统类加载器。就是加载当前classPath下面的所有类。

那么这么多的类加载器,他们时候用呢?有什么作用呢?

下面先回到上面那个例子,我们下看下Classloader这个类,这个类加载器有一个属性parent,这个属性的类型也是一个ClassLoader,说明他也是加载器,我们先把这个打印出来看看。

  private final ClassLoader parent;
打印的结果如下,发现当前加载器的parent是一个ExtClassLoader加载器。
class loader:sun.misc.Launcher$AppClassLoader@776ec8df
loader parent:sun.misc.Launcher$ExtClassLoader@6acbcfc0
这说明的AppClassLoader的父加载器是ExtCLassLoader,那么我们肯定任务,每个加载器都有一个父类加载器,那什么时候是个头呢,直接采用getParent()方法,发现报错了,空指针错误。
public void testClazzLoader(){
		ClassLoader loader = Test.class.getClassLoader();
		System.out.println("class loader:"+loader.toString());
		System.out.println("loader parent:"+loader.getParent().toString());
		System.out.println("loader parent parent:"+loader.getParent().getParent().toString());
		
	}

这是为什么呢,难道是没有父类加载器了么?这是以为 AppClassLoader和ExtClassLoader在调用构造方法传递parent对象的时候,AppClassloader传递的对象是ExtClassLoader,这就是为什么AppClassLoader的父加载器是ExtClassLoader,而在构造ExtClassloader的时候,传递的是null,因此他没有父加载器。那么问题来了,还有一个Bootstrap ClassLoader这个是啥时候加载的呢?该类是采用C++程序编写,是java虚拟机的本身的一部分,并非java中的类,因此无法获取到,它在java虚拟器启动的时候去加载一些系统的类库,本身并没有父加载器,好了以上的简介就到这里了。现在的问题是,有这么多的加载器,他们是如何加载的呢?

二、双亲委托机制

    这里就不得不提到一个双亲委托机制,这个机制可以很好的解决了类加载器重复加载和安全性的问题。举个例子,现在编写了一个类TestDemo.java,那么谁去加载他呢?如果没有规则和约束,可能Bootstrap Classloader去加载他,然后AppClassLoader又去加载了一遍,这样耗费内存资源。java虚拟机,通过采用双亲委派模型来解决这个问题,各个ClassLoader之间的关系是通过组合关系来复用父加载器,当一个classLoader收到一类加载的请求的时候,首先把该请求委托派给该类的父类classloader进行处理,当父类无法处理的时候,才提交给当前的类classloader来处理。总之,对于每一次请求,优先由父类来进行处理,当父类无法处理的时候,才由子类进行处理。

处理的顺序是为:bootstrap classloader、ExtClassloader 、AppClassLoader、用户自定义的类加载器,通过下面图先了解一下大概:


这种流程说明,当一个类加载的请求过来的时候,优先发给底层的类加载器,但是底层的类加载器,比如说用户自定义的加载,他不会自己先去加载,而是去请求父加载器来进行加载,一直到顶层加载器,当顶层加载器加载不了的时候,在让子加载器进行加载。这个图说的不是很形象,这里借用网友的一个图。


这个图就说明的非常的形象,当一个请求过来的时候,先从自己的缓存中判断是否已经加载,如果已经加载,则不在进行加载,如果没有加载,则传递给父加载器进行加载。这个在加载器的源码中也能看的出来,下面是源码:

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);  //父加载器不为null的时候,调用父加载器进行加载
                    } else {
                        c = findBootstrapClassOrNull(name);  //父加载器为null则调用bootstrap进行加载
                    }
                } 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);   //当父加载器加载失败的时候,调用findClass

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

以上的逻辑也说明了类加载器的双亲委托模型的原理。

那么如何判断两个class是否相同呢?

java在判断两个class是否相同的时候,不仅要判断两个类名是否相同,而且还要判断是否是同一个类加载器加载的,只有当两者条件都满足的时候,jvm才任务这两个是相同的。如果是两个不同的classloader加载的,jvm也会任务他们是不同的class。

三、自定义类加载器

    自定义加载器是为了针对实际应用中出现的不同版本的jar包,不同的路径的问题,可以根据实际需要,加载指定目录下的指定jar包。其次,自定义类加载器可以保证一定的安全性,比如说.class文件很容易被反编译过来,但是如果你按照自己的编码方式进行编码,然后采用自己的类加载器进行加载,这样在传输的过程中即使被拦截,也无法解析出来。最后,可以实现类的热部署功能,可以检查已经加载的类是否被修改,如果被修改了,则进行重新加载。自定义类加载器的实现过程比较简单,直接继承classLoader类即可, 然后重写其中的某些方法,下面我们看个例子:

public class DiskClassLoader extends ClassLoader{

	private String jarPath;
	
	 public DiskClassLoader(String jarPath) {
		 this.jarPath = jarPath;
	 }
	
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		String filePath = jarPath+name.replace(".", "/")+".class";
		System.out.println("filepath="+filePath);
		File file = new File(filePath);
		FileInputStream fis =null;
		try {
			fis = new FileInputStream(file);
		} catch (FileNotFoundException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		byte[] buf = new byte[1024];
		int len = 0 ;
		try {
			while((len=fis.read(buf))!=-1){
				baos.write(buf, 0, len);
			}
			return defineClass(name, buf, 0, buf.length);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		return null;
	}

这里就是继承了ClassLoader这个类,然后重写了其中的findClass方法。该方法就是把字节码文件加载到内存中,一般我们都会覆盖这个方法,方便加载自定义的类。然后调用defineClass()这个方法进行解析字节码。下面看下我们调用这个自定义加载器来进行加载

@Test
	public void testLoaderDemo() throws ClassNotFoundException, InstantiationException, IllegalAccessException, Exception, Exception{
		DiskClassLoader loader = new DiskClassLoader("/tmp/project_1/open-api/src/test/java/");
		Class clazz = loader.loadClass("com.java.em.TestDemo");
		if(clazz!=null){
			Object obj = clazz.newInstance();
			Method m = clazz.getMethod("say",null);
			m.invoke(obj, null);
		}
	
	}
通过上的调用,可以正确的获取到TestDemo这个类的字节码,然后通过反射的方式,得到对应的方法,最后执行该方法。

以上的方式看上去比较简单,我们先了解一下ClassLoader这个类的主要几个API

public abstract class ClassLoader {
     public Class loadClass(String name);

     protected Class defineClass(byte[] b);

     public URL getResource(String name);

     public Enumeration getResources(String name);

     public ClassLoader getParent();
}

这里面有一个比较重要的方法,loadClass(),它接受一个全类名,然后得到一个Class类型的实例。

 defineClass()这个方法是将得到的字节数组进行实例化,调用的是jvm的native方法进行实例化,如上面我们的重写的findClass方法最后也是得到字节数组,通过defineClass方法进行实例化。

getParent方法是返回当期类加载器的父ClassLoader,如果是ExtClassLoader的话,其父加载器则为null。getResourcegetResources方法,从给定的repository中查找URLs,同时它们也具备类似loadClass一样的代理机制,我们可以将loadClass视为:defineClass(getResource(name).getBytes())

由于classLoader采用的是双亲委托的模型,我们在自定义的时候,可以采用这个模型,也可以不采用这个模型,一般的来说我们重写了findClass()方法就是,如果不重写loadClass()方法的话就是采用默认的双亲委托模型。如果不想采用该模型,这直接重写该方法。

以上就是自定义类加载器的一些方法,下面还有一些小问题,也是在别人的博客中看来的,再次记录一下

 .class和getCLass()方法区别:

.class用于类名,而getClass是一个final native的方法,只能用于类的实例。

.class是在编译期间就确定了一个Class对象,而getClass()是一个实例方法,只有具体的实例才有该方法,具体的类是没有的,是在运行期间确定一个类实例的Class对象。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值