如果你这样学Java类加载器,100%会

类加载器概述

类加载从JDK1.0就有,最初是为满足Java Applet的需要开发出来的,虽说Java Applet现在早已死翘翘,但是类加载器在别处绽放光彩,如热部署。

类加载器,顾名思义就是加载Java类到虚拟机中,负责读取Java字节码,并转换成java.lang.Class类的一个实例,通过newInstance()方法就可以创建出该类的一个对象,这里的读取可以从本地文件,或者从网络上读取,这个类由java.lang.ClassLoader定义。可以把类加载器比如成咖啡,程序员是字节码,程序员通过咖啡,生产出程序,程序就是java.lang.Class。

通过class.getClassLoader()方法可以获取加载此类的类加载器。

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

引导类加载器(BootstrapClassLoader):用来加载 Java 的核心库,也就是%JAVA_HOME%/jre/lib目录,用原生代码(C++)写。

扩展类加载器(ExtClassLoader):用来加载 Java 的扩展库,负责加载%JAVA_HOME%/jre/lib/ext下目录中的类库。实现类是sun.misc.Launcher$ExtClassLoader

应用类加载器(ApplicationClassLoader):也称之为系统类加载器,负责加载当前应用classpath路径下的类库,在没有自定义类加载器的时候,开发人员所编写的类都是由它来完成加载,可以通过 ClassLoader.getSystemClassLoader()来获取它。实现类是sun.misc.Launcher$AppClassLoader

也就是说对于不同的类,Class.getClassLoader()一般在没有其他干扰下,会返回以上三种类加载器,但是要注意的是,返回null不是没有类加载器,而是代表BootstrapClassLoader,并且除了BootstrapClassLoader,其他两种都是继承自ClassLoader。

类加载验证

BootstrapClassLoader

首先验证引导类加载器,可以通过以下代码获取BootstrapClassLoader所加载的目录或者jar。

 URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
 for (URL url : urls) {
     System.out.println(url.toExternalForm());
 }

或者通过属性方式.

String[] split = System.getProperty("sun.boot.class.path").split(":");
for (String s : split) {
    System.out.println(s);
}

他输出如下,其中最后一行有个jre/classes目录,这个目录默认不存在,应该是留给用户的。
在这里插入图片描述
也就是这些都是由引导类加载器加载,其中就包括了核心类rt.jar。
通过System.out.println(String.class.getClassLoader());获取String类的ClassLoader,此时会发现是null,则代表是引导类加载器加载,同样其他jar包中的类一个道理,比如jsse.jar中的SunRsaSign类System.out.println(SunRsaSign.class.getClassLoader());同样会输出null。

如果想让我们的类让他加载,可以指定参数-Xbootclasspath/a: <目录>。

比如HxlClass的包名是com.company,把他带包放入/home/test目录下,并指定参数-Xbootclasspath/a:/home/test,通过Class.forName(“com.company.HxlClass”)方式加载他,同样输出null。这时候可以通过反射对HxlClass类为所欲为,其实这个可以放在jre/classes下,也一样。
在这里插入图片描述
在这里插入图片描述

 Object o = Class.forName("com.company.HxlClass").newInstance();
 System.out.println(o.toString());

当然在加载jar包的时候,要指明jar的名字,如-Xbootclasspath/a:/home/test/LibJava.jar。

ExtClassLoader

接下来是扩展类加载器,负责加载%JAVA_HOME%/jre/lib/ext下的所有类,通过以下方式可以获取加载的路径。

System.out.println(System.getProperty("java.ext.dirs"));

他输出如下,而另一个路径也是不存在的,需要自己创建。
在这里插入图片描述
%JRE_HOME%/jre/lib/ext目录下,有很多扩展包。
在这里插入图片描述
拿zipfs.jar来说,里面有一个ZipFileSystem类,输出他的加载器的时候是sun.misc.Launcher$ExtClassLoader,则表示他是由扩展类加载器加载。

System.out.println(ZipFileSystem.class.getClassLoader());

同样的操作,如果想让我们的类让他加载,要指定参数-Djava.ext.dirs=<路径>,如-Djava.ext.dirs=/home/test,在次使用Class.forName(“com.company.HxlClass”)加载此类,并获取他的加载器,则会输出sun.misc.Launcher$ExtClassLoader,也可以放入另一个目录下,需要自己创建,也就是上面说的。

 Object o = Class.forName("com.company.HxlClass").newInstance();
 System.out.println(o.getClass().getClassLoader());

但是当指定-Djava.ext.dirs=/home/test的时候,会发现以前扩展类的路径下的类无法加载。原因是-Djava.ext.dirs有覆盖性,解决办法是指明原来的扩展路径,多个路径用:分割。
在这里插入图片描述
加入原来的扩展目录后再次运行。
在这里插入图片描述
但是在Idea中运行时,ZipFileSystem是由sun.misc.Launcher$AppClassLoader加载,并没有报错。这是因为idea把扩展类的目录增加到了classpath中,由SystemClassLoader加载了,这是由于双亲委派导致。

SystemClassLoader

主要负责加载classpath所指定位置下的类或者jar,通过以下方式可以获取路径。

System.out.println("---"+System.getProperty("java.class.path"));

一般情况下,我们自己编写的类是由他加载,可以通过-classpath指定路径。当你程序运行抛出ClassNotFoundException时候,可以通过他指明缺少类的路径来解决。

双亲委派

思想是自己不想干,让父亲帮忙干。

类加载器在尝试自己查找某个类的字节代码并加载时,会先委托给他的父类加载器,由父类加载器先去尝试加载,以此类推。如果父亲能加载成功,那就直接返回,如果父亲加载不了,则在向下传递,由子类完成,比如SystemClassLoader尝试加载类的时候,先委托给ExtClassLoader,ExtClassLoader又委托给BootstrapClassLoader,在没有更上一层了,如果BootstrapClassLoader无法加载,那就向下让ExtClassLoader加载,成功则直接返回,ExtClassLoader加载不成功则SystemClassLoader加载,如果SystemClassLoader加载不了,则抛出异常。

用一个例子可以验证,首先在/home/test目录下放一个LibJava.jar,其中有个类是com.company.HxlClass,然后尝试加载他,并输出他的类加载器。
在这里插入图片描述
测试代码如下:

public static void main(String[] args) throws ClassNotFoundException{
	System.out.println(Class.forName("com.company.HxlClass").getClassLoader());
 }

如果不向三个类加载器中某一个指明这个jar路径时,肯定是抛出ClassNotFoundException异常,意味着三个类加载都不知道他的路径,都无法加载。
当向其中一个类加载器指明这个路径后,加载com.company.HxlClass的一定是他,如下图,因为只有他知道。
在这里插入图片描述如果三个类加载器加载的路径下都有这个jar路径,则一定是BootstrapClassLoader加载,如下图。因为遵守规则,父亲能干的父亲干。
在这里插入图片描述

判断是否同一个类

Java 虚拟机不仅要看类的全名是否相同,还要看加载这个类的类加载器是否一样,只有都相同的情况下,才认为两个类是相同的,否则即便是同样的字节代码,被不同的类加载器加载之后,会认为是不同的。
这个很容易就验证。编写一个Dog类,代码如下。首先创建两个自定义的类加载,并加载同一个类,在利用对象的强制转换,如果转换失败则会抛异常。

public class Dog {
    public  void setDog(Object o){
        System.out.println("setDog>>"+o);
        Dog dog =(Dog)o;
    }
}

测试代码如下。

public static void main(String[] args) throws ClassNotFoundException,
        NoSuchMethodException, IllegalAccessException,
        InstantiationException, InvocationTargetException {
    class TestClassLoader extends ClassLoader {
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                FileChannel channel = new FileInputStream("/home/test/Dog.class").getChannel();
                ByteBuffer byteBuffer = ByteBuffer.allocate(2048);
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                int size = 0;
                while ((size = channel.read(byteBuffer)) != -1) {
                    byteBuffer.limit();
                    byteArrayOutputStream.write(byteBuffer.array(), 0, size);
                }
                return defineClass(name, byteArrayOutputStream.toByteArray(), 0, byteArrayOutputStream.size());
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            throw  new ClassNotFoundException();
        }
    }
    TestClassLoader classLoader1 = new TestClassLoader();
    TestClassLoader classLoader2 = new TestClassLoader();
    Class<?> cls1 = classLoader1.loadClass("Dog");
    Class<?> cls2 = classLoader2.loadClass("Dog");
    System.out.println(cls1.getClassLoader() +"   "+ cls2.getClassLoader());
    Object o1 = cls1.newInstance();
    Object o2 = cls2.newInstance();
    Method setDog1 = cls1.getDeclaredMethod("setDog", Object.class);
    System.out.println(setDog1 +"  "+setDog1);
    setDog1.invoke(o1,o2);
}

运行后,发现他会报ClassCastException异常。
在这里插入图片描述

自定义类加载器

在上面已经演示了一个自定义类加载器TestClassLoader,要想自定义,首先继承ClassLoader,然后重写findClass方法,返回值是Class对象,通过内部defineClass将class文件的byte[]转换成Class对象,defineClass是java层的方法,最终会调用到defineClass1这个native方法。

下面是完成从网络中读取字节码并加载的NetworkClassLoader。

public class NetworkClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            URL url = new URL("http://blog.houxinlin.com/Dog.class");
            InputStream inputStream = url.openStream();
            byte[] data = new byte[2048];
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            int size = 0;
            while ((size = inputStream.read(data)) != -1) {
                byteArrayOutputStream.write(data, 0, size);
            }
            return defineClass(name, byteArrayOutputStream.toByteArray(), 0, byteArrayOutputStream.size());
        } catch (IOException ex) {
            ex.printStackTrace();
        }
       throw new ClassNotFoundException();
    }
}

测试代码。

 NetworkClassLoader networkClassLoader =new NetworkClassLoader();
 System.out.println(networkClassLoader.loadClass("Dog"));

如果想从jar中加载某个类,可以使用URLClassLoader,并且AppClassLoader和BootstrapClassLoader都继承他。

 String jarFile ="/home/test/LibJava.jar";
 URL url1  =new File(jarFile).toURL();
 URLClassLoader myClassLoader = new URLClassLoader(new URL[]{url1});
 JarFile file =new JarFile(jarFile);
 Enumeration<JarEntry> entries = file.entries();
 while (entries.hasMoreElements()){
     JarEntry jarEntry = entries.nextElement();
     if (!jarEntry.isDirectory()){
         if (jarEntry.getName().endsWith(".class")){
             String name =jarEntry.getName().replaceAll("/",".");
             name=name.substring(0,name.length()-6);
             System.out.println(myClassLoader.loadClass(name).newInstance().toString());
         }
     }
 }

源码分析

这要追随到Launcher类,java的入口。这个类由BootstrapClassLoader加载。其中this.loader作为getClassLoader方法的返回值,也就是说可以通过调用Launcher.getLauncher().getClassLoader()也可以拿到AppClassLoader。

 public Launcher() {
 	//扩展类加载器
     Launcher.ExtClassLoader var1;
     try {
     	//实例化扩展类加载器,单例模式
         var1 = Launcher.ExtClassLoader.getExtClassLoader();
     } catch (IOException var10) {
         throw new InternalError("Could not create extension class loader", var10);
     }
     try {
     	//实例化AppClassLoader,单例模式,并将AppClassLoader的父加载器设置成ExtClassLoader。
         this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
     } catch (IOException var9) {
         throw new InternalError("Could not create application class loader", var9);
     }
     //对当前线程设置类加载器
     Thread.currentThread().setContextClassLoader(this.loader);
}

其中在getExtClassLoader调用层中调用到了getExtDirs方法,获取扩展类的目录集合,最后把这个File[]传递到ExtClassLoader构造方法中。

 private static File[] getExtDirs() {
     String var0 = System.getProperty("java.ext.dirs");
     File[] var1;
     if (var0 != null) {
         StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
         int var3 = var2.countTokens();
         var1 = new File[var3];
         for(int var4 = 0; var4 < var3; ++var4) {
             var1[var4] = new File(var2.nextToken());
         }
     } else {
         var1 = new File[0];
     }
     return var1;
 }

同样的getAppClassLoader也是获取到java.class.path的值,实例化AppClassLoader需要两个参数,一个是ava.class.path,一个是父ClassLoader。
在这里插入图片描述
接下来是ClassLoader中的loadClass方法,双亲委派也就在这里。调用所有爸爸们的loadClass如果都无法加载,则调用自己的findClass尝试加载。

 protected Class<?> loadClass(String name, boolean resolve)
     throws ClassNotFoundException
 {
     synchronized (getClassLoadingLock(name)) {
         //如果已经被加载。直接返回
         Class<?> c = findLoadedClass(name);
         if (c == null) {
             long t0 = System.nanoTime();
             try {
             	//如果存在父ClassLoader,则让父ClassLoader先尝试加载
                 if (parent != null) {
                     c = parent.loadClass(name, false);
                 } else {
                 	//不存在,则交给BootstrapClass,
                 	//BootstrapClass会调用到findBootstrapClass这个native层方法
                     c = findBootstrapClassOrNull(name);
                 }
             } catch (ClassNotFoundException e) {
             }
             //如果还等于null,意味着各位爸爸们都无法加载,自己来,调用findClass,也就是为什么自定义类加载器要重写findClass
             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;
     }
 }

Tomcat类加载器

在Tomcat中,每个 Web 应用都有一个对应的类加载器,不同的是首先自己去尝试加载某个类,如果找不到则交给父加载器,与上面双亲委派的顺序相反,这是 Java Servlet 规范中的推荐做法。比如,有两个Web应用,都采用了某个类库,一个采用1.0版本,一个采用2.0版本,此时如果采用一个类加载器,那么导致jar覆盖,可能无法启动成功。

Tomcat这样的作法就保证了隔离性,灵活性,和性能。

发布了42 篇原创文章 · 获赞 7 · 访问量 7744

猜你喜欢

转载自blog.csdn.net/HouXinLin_CSDN/article/details/104294759
今日推荐