第18次文章:JVM中的类加载机制
- 2019 年 10 月 8 日
- 筆記
这周介绍一下JVM中的类加载机制,主要是类加载器的层次结构,代理模式以及自定义类加载器。
一、类加载器的层次结构(树状结构)
1、引导类加载器(bootstrap class loader)
-主要用来加载java的核心库,是用原生代码C语言来实现的,并不继承自java.lang.ClassLoader。
-加载扩展类和应用程序类加载器。并不指定他们的父类加载器。
2、扩展程序类(extensions class loader)
-用来加载java的扩展库。java虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载java类。
-由sun.misc.Launcher$ExtClassLoader实现
3、应用程序类加载器(application class loader)
-它是根据java应用的类路径(classpath,java.class.path),一般来说,java应用的类都是由它来完成加载的。
4、自定义类加载器
-开发人员可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。
下面我们先简单的测试一下几个类加载器的层次结构:
public class Demo02 { public static void main(String[] args) { //应用程序类加载器jdk.internal.loader.ClassLoaders$AppClassLoader@2a33fae0 System.out.println(ClassLoader.getSystemClassLoader()); //扩展类加载器jdk.internal.loader.ClassLoaders$PlatformClassLoader@707f7052 System.out.println(ClassLoader.getSystemClassLoader().getParent()); //引导类加载器,使用原生代码实现(C),所以此处无法获取 System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent()); System.out.println(System.getProperty("java.class.path")); } }
查看一下结果:
tips:
(1)首先我们获取当前线程的类加载器,可以得到一个应用程序类加载器(AppClassLoader),然后获取其父类加载器,得到一个扩展类加载器(PlatformClassLoader),最后再获取扩展类加载器的父类,输出为null,是因为扩展类加载器的父类为引导类加载器(Bootstrap class loader),同时,引导类加载器是使用原生代码实现的,并不是继承自java.lang.ClassLoader,所以在获取其名称时,无法在java环境中显示。由此可以得到,在类加载器中,其层次结构为:自定义类加载器——>应用程序类加载器——>扩展类加载器——>引导类加载器。
(2)我们获取当前类加载器的加载目录“java.class.path”,可以看出,其加载目录在当前工程文件下的bin目录内。
二、类加载器的代理模式:
类加载器的代理模式是指:在加载指定的类的时候,当前类加载器并不直接加载这个类,而是交给其他类进行加载。下面我们介绍一种代理模式——双亲委托机制。
双亲委托机制:
-就是某个特定的类加载器在介绍加载类的请求时,首先将加载任务委托给父类加载器,依次追溯,直到最原始的父类,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
-双亲委托机制是为了保证java核心库的类型安全。这种机制就保证了用户无法使用自己定义的java.lang.Object类的情况。
-类加载器除了用于加载类,也是安全的最基本的屏障。
我们举一个简单的实例,进行分析双亲委托机制的安全性:
首先,我们自己建立一个经常使用,但是却从来没有建过的类java.lang.String
package java.lang; public class String { public String toString() { return "aaa"; }}
然后我们使用调用此类:
package com.peng.test; public class Demo02 { public static void main(String[] args) { String a = "peng"; System.out.println(a.getClass().getClassLoader()); System.out.println(a.toString()); } }
输出结果:
tips:
(1)我们先关注一下结果,在自定义的String类中,我们是返回一个字符串“aaa”,而最后打印在控制台上的内容是我们重新定义的一个变量“peng”。所以类加载器在加载String类的时候,直接加载了java核心包(rt.jar)中的java.lang.String类,而不是我们自定义的java.lang.String类。
(2)导致这种结果的原因就是类加载机制中的双亲委派机制。当我们的系统类加载器获取到String类的时候,首先会交给其父类扩展类加载器,然后又交给扩展类加载器的父类——引导类加载器。引导类加载器为最高层父类,所以,当一个类被加载的时候,首先是将其一层一层向上传递,最后交给引导类加载器,从引导类加载器开始进行加载。当引导类加载器获取到java.lang.String类的时候,直接可以从java核心库当中加载此类,所以不需要将此类向下传递加载。
(3)这种机制就确保了我们无法使用自定义的java核心库中的类,保护了java核心库的安全性。
(4)代理模式有很多种,双亲委托机制是代理模式的一种,也并不是所有的类加载器都采用双亲委托机制。比如说:Tomcat服务器类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。
三、自定义类加载器
自定义类加载器的流程:
-继承:java.lang.ClassLoader
-首先检查请求的类型是否已经被这个类加载器加载到命名空间中了,如果已经加载,直接返回;
-委派类加载请求给父类加载器,如果父类加载器能够完成,则返回父类加载器加载的Class实例;
-调用本类加载器的findClass(…)方法,试图获取对应的字节码,如果获取的到,则调用defineClass(…)导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass(…),loadClass(…)转抛异常,终止加载过程。
下面,我们根据上述流程,写一个具体的自定义文件系统类加载器:
/** * 自定义文件系统类加载器 * */public class FileSystemClassLoader extends ClassLoader { private String rootDir;//根目录 public FileSystemClassLoader(String rootDir) { this.rootDir = rootDir; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { Class<?> c = findLoadedClass(name);//在已加载的类中查找name //应该要先查询有没有加载过这个类,如果已经加载,则直接返回加载号的类。如果没有,则加载新的类 if(c!=null) { return c; }else {//双亲委派机制,委派给父类进行加载 ClassLoader parent = this.getParent();//获取该类的父类加载器 try { c = parent.loadClass(name);//委派给父类加载 } catch (Exception e) { // TODO: handle exception } if(c != null) { return c; }else {//如果父类加载器中也没有将该类进行加载,则通过IO流,将该类别信息输入,然后自定义此类 byte[] classData = getClassData(name);//获取此类的信息 if(classData == null) { throw new ClassNotFoundException(); }else { c = defineClass(name, classData, 0, classData.length);//定义此类 } return c; } } } /** * 以字节数组的方式获取类信息 * @param className * @return */ private byte[] getClassData(String className) { String classPath = this.rootDir + "/" + className.replace(".", "/") + ".class"; InputStream is = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try {//读写待加载类的文件 is = new FileInputStream(classPath); byte[] buffer = new byte[1024]; int temp = 0; while(-1 != (temp=is.read(buffer))) { baos.write(buffer, 0, temp); } return baos.toByteArray(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); return null; } finally {//关闭IO流 if(is != null) { try { is.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(baos != null) { try { baos.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } }
然后我们简单的测试一下这个自定义文件系统类加载器:
package com.peng.test; /** * 测试自定义的类加载器 FileSystemClassLoader */public class Demo03 { public static void main(String[] args) throws ClassNotFoundException { FileSystemClassLoader loader = new FileSystemClassLoader("G:/java学习/test"); FileSystemClassLoader loader2 = new FileSystemClassLoader("G:/java学习/test"); Class<?> c = loader.loadClass("com.peng.test.User"); Class<?> c2 = loader.loadClass("com.peng.test.User"); Class<?> c3 = loader2.loadClass("com.peng.test.User"); Class<?> c4 = loader2.loadClass("java.lang.String"); Class<?> c5 = loader2.loadClass("com.peng.test.Demo01"); System.out.println(c); System.out.println(c.hashCode()); System.out.println(c2.hashCode()); System.out.println(c3.hashCode());//同一个类,被两个类加载器加载的同一个类,JVM认不认为是相同的类 System.out.println(c4.hashCode()); System.out.println(c3.getClassLoader());//使用我们自定义的类加载器 System.out.println(c4.getClassLoader());//引导类加载器 System.out.println(c5.getClassLoader());//系统默认的类加载器AppClassLoader }}
输出结果如下:
tips:
(1)首先我们观察对象c和c2,两者是使用同一个文件系统类加载器,加载同一个类得到对象,所以两个对象的hashcode相同,属于同一个对象。
(2)但是我们观察对象c3和c,c3使用了另一个类加载器loader2进行加载,加载的类和c加载的类是相同的,但是最后两者的hashcode不同,代表了两个不同的对象,这个结果证明被两个类加载器加载的同一个类,JVM认为是不同的类。
(3)我们再创建两个对象c4和c5,分别加载核心类“java.lang.String”和当前工程文件中的类“com.peng.test.Demo01”,分别获得c3、c4、c5的类加载器,并输出到控制台上。可以发现c3使用的是我们自定义的文件系统类加载器,c4依旧使用的是引导类加载器,c5使用的是应用程序类加载器。因为c5中加载的Demo01对象属于此工程文件中的一个文件,所以我们的主程序Demo03在加载的时候,就已经使用应用程序类加载器将其加载在JVM中了,并不需要使用自定义文件系统类加载器。