类加载器概述
类加载从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类为所欲为,其实这个可以放在%JAVA_HOME%/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。
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 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.ExtClassLoadervar1;try {//实例化扩展类加载器,单例模式var1= Launcher.ExtClassLoader.getExtClassLoader();} catch (IOExceptionvar10) {throw new InternalError("Could not create extension class loader",var10);}try {//实例化AppClassLoader,单例模式,并将AppClassLoader的父加载器设置成ExtClassLoader。this.loader= Launcher.AppClassLoader.getAppClassLoader(var1);} catch (IOExceptionvar9) {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
同样的getAppClassLoader也是获取到java.class.path的值,实例化AppClassLoader需要两个参数,一个是java.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这样的作法就保证了隔离性,灵活性,和性能。