Java的类加载
2016-04-30 11:45
423 查看
类加载器ClassLoader,用于将类加载到JVM,java提供了三个自带的类加载器,按启动顺序分别为,Bootstrap,Extension,Application,如下
Bootstrap,启动类加载器,内嵌于JVM,本身是JVM的一部分,先天就加载在JVM中,用C++编写,Bootstrap的启动随着JVM的启动而启动。
Extension,扩展类加载器,抽象类ClassLoader的子类。
Application,系统(应用)类加载器,抽象类ClassLoader的子类。
运行java程序时,首先加载JVM到内存,JVM启动Bootstrap类加载器,由其加载java的核心类库,jre/lib/下的所有jar包,之后Bootstrap加载Extension和Application类加载器,并创建它们的实例,除了Bootstrap,其余的类加载器都是类,java中的所有类均是由类加载器来加载到JVM才能使用的,然后启动Extension类加载器,由其加载jre/lib/ext/下的所有jar包,最后启动Application类加载器,由其加载classpath指定的目录下的类,classpath指定了工程当前目录下的类,由此这一步实际是加载应用程序到JVM。
任何一个类都有一个静态的Class类型的实例class,类通过类加载器加载到内存后,类加载器会返回该类的Class类型的实例给JVM,同时用此实例初始化该类的静态Class类型实例class,JVM实际是通过此类的Class类型的实例,来操作此类的实例的,由此可以看出,java的底层实际是通过反射来使用类及其实例的。
java程序中,类的唯一标识是类的完全限定名。
JVM中,类的唯一标识是类的完全限定名 + 加载此类的类加载器的实例的名字,在JVM中,同一个类仅能被加载一次,这里的同一个类用的就是JVM中类的唯一标识来区分的。
java的委托类加载机制,java中的类加载器使用委托加载模式,任何一个类加载器,都会被指定一个委托类加载器的实例,当类加载器加载某个类时,首先将其交给它的委托类加载器来加载。
Bootstrap,特殊,无委托类加载器。
Extension,委托Bootstrap。
Application,委托Extention。
用户自定义类加载器,由构造方法传入指定,传null为Bootstrap,不传默认为Application,传一个类加载器的实例。
类加载器A委托B,B默认,委托Application,此时若A加载类,首先委托B加载,B先委托Application加载,Appliaction先委托Extension加载,Extension委托Bootstrap加载,此时Bootstrao调用loadClass加载,若未找到类,返回给Extension加载,依次类推,直到加载此类,若最后回到A后,A也未能加载类,抛出ClassNotFoundException异常。
综上来看,使用委托机制,无论哪个类加载器,最终都会委托到Bootstrap来加载,因为除了Bootstrap,都要指定委托类加载器,只有委托到一个没有委托的类加载器才能停止,而只有Bootstrap没有委托类加载器,如此可以优先考虑用最底层的类加载器来加载,这是java的一种安全机制,保证java的核心类库始终由Bootstrap来加载,可以防止加载恶意的类。
类E的加载A->B->App->Ext->Boot的过程中,若类E最终被App加载,我们称App为类E的定义类加载器,即真正加载E的,A,B,App都称为类E的初始类加载器,即导致类E被加载的,每个类加载器都维持一张表,记录自己作为初始类加载器所加载的类,由此A,B,App均记录了E,下次再用A,B,或App加载E时,会直接返回E的类类型实例,而不再加载,但是不管谁返回的,类E最初是由App加载的这点不变,通过E.class.getClassLoader()或e.getClass().getClassLoader()可返回加载E的定义类加载器,类E在JVM中的唯一标识为E的完全限定名 + E的定义类加载器。
可以返回类加载器A的委托类加载器,仅类加载器有该方法。
抽象类ClassLoader,所有的类加载器均是继承自此类加载器,ClassLoader加载类的过程是,使用loadClass()方法加载类,该方法先调用findLoaderClass()方法检查此类是否已经被加载过,应该就是检查自己作为初始类加载器维护的记录表,若加载过,直接返回此类的Class类型实例,否则返回null,若返回null,交由委托类加载器加载,委托类加载器的加载过程同样,也是loadClass()…,若均加载失败,即返回null,最后回到ClassLoader,其会调用findCLass()方法加载,这个就是抽象类ClassLoader中的抽象方法,实际没有实现,需要我们自己实现,App与Ext均是继承自ClassLoader,已经实现了findClass()方法。
实现自定义类加载器通常是用于加载网络上的类,或者是本地不属于本工程的类,基本过程是,首先确定该类的URL,即资源定位,之后打开定位到此资源的输入流,而后以字节的形式将该类的内容读到本地的字节数组中,最后调用java提供的defineClass方法加载类的字节内容到JVM。
Bootstrap,启动类加载器,内嵌于JVM,本身是JVM的一部分,先天就加载在JVM中,用C++编写,Bootstrap的启动随着JVM的启动而启动。
Extension,扩展类加载器,抽象类ClassLoader的子类。
Application,系统(应用)类加载器,抽象类ClassLoader的子类。
运行java程序时,首先加载JVM到内存,JVM启动Bootstrap类加载器,由其加载java的核心类库,jre/lib/下的所有jar包,之后Bootstrap加载Extension和Application类加载器,并创建它们的实例,除了Bootstrap,其余的类加载器都是类,java中的所有类均是由类加载器来加载到JVM才能使用的,然后启动Extension类加载器,由其加载jre/lib/ext/下的所有jar包,最后启动Application类加载器,由其加载classpath指定的目录下的类,classpath指定了工程当前目录下的类,由此这一步实际是加载应用程序到JVM。
任何一个类都有一个静态的Class类型的实例class,类通过类加载器加载到内存后,类加载器会返回该类的Class类型的实例给JVM,同时用此实例初始化该类的静态Class类型实例class,JVM实际是通过此类的Class类型的实例,来操作此类的实例的,由此可以看出,java的底层实际是通过反射来使用类及其实例的。
java程序中,类的唯一标识是类的完全限定名。
JVM中,类的唯一标识是类的完全限定名 + 加载此类的类加载器的实例的名字,在JVM中,同一个类仅能被加载一次,这里的同一个类用的就是JVM中类的唯一标识来区分的。
java的委托类加载机制,java中的类加载器使用委托加载模式,任何一个类加载器,都会被指定一个委托类加载器的实例,当类加载器加载某个类时,首先将其交给它的委托类加载器来加载。
Bootstrap,特殊,无委托类加载器。
Extension,委托Bootstrap。
Application,委托Extention。
用户自定义类加载器,由构造方法传入指定,传null为Bootstrap,不传默认为Application,传一个类加载器的实例。
类加载器A委托B,B默认,委托Application,此时若A加载类,首先委托B加载,B先委托Application加载,Appliaction先委托Extension加载,Extension委托Bootstrap加载,此时Bootstrao调用loadClass加载,若未找到类,返回给Extension加载,依次类推,直到加载此类,若最后回到A后,A也未能加载类,抛出ClassNotFoundException异常。
综上来看,使用委托机制,无论哪个类加载器,最终都会委托到Bootstrap来加载,因为除了Bootstrap,都要指定委托类加载器,只有委托到一个没有委托的类加载器才能停止,而只有Bootstrap没有委托类加载器,如此可以优先考虑用最底层的类加载器来加载,这是java的一种安全机制,保证java的核心类库始终由Bootstrap来加载,可以防止加载恶意的类。
类E的加载A->B->App->Ext->Boot的过程中,若类E最终被App加载,我们称App为类E的定义类加载器,即真正加载E的,A,B,App都称为类E的初始类加载器,即导致类E被加载的,每个类加载器都维持一张表,记录自己作为初始类加载器所加载的类,由此A,B,App均记录了E,下次再用A,B,或App加载E时,会直接返回E的类类型实例,而不再加载,但是不管谁返回的,类E最初是由App加载的这点不变,通过E.class.getClassLoader()或e.getClass().getClassLoader()可返回加载E的定义类加载器,类E在JVM中的唯一标识为E的完全限定名 + E的定义类加载器。
A a = new A(); a.getParent();
可以返回类加载器A的委托类加载器,仅类加载器有该方法。
抽象类ClassLoader,所有的类加载器均是继承自此类加载器,ClassLoader加载类的过程是,使用loadClass()方法加载类,该方法先调用findLoaderClass()方法检查此类是否已经被加载过,应该就是检查自己作为初始类加载器维护的记录表,若加载过,直接返回此类的Class类型实例,否则返回null,若返回null,交由委托类加载器加载,委托类加载器的加载过程同样,也是loadClass()…,若均加载失败,即返回null,最后回到ClassLoader,其会调用findCLass()方法加载,这个就是抽象类ClassLoader中的抽象方法,实际没有实现,需要我们自己实现,App与Ext均是继承自ClassLoader,已经实现了findClass()方法。
实现自定义类加载器通常是用于加载网络上的类,或者是本地不属于本工程的类,基本过程是,首先确定该类的URL,即资源定位,之后打开定位到此资源的输入流,而后以字节的形式将该类的内容读到本地的字节数组中,最后调用java提供的defineClass方法加载类的字节内容到JVM。
//自定义类加载器,必须是ClassLoader的子类,重写基类的findClass方法,用于加载我们自己指定的网络上的某个类资源,建议重写findClass方法,但是不建议重写loadClass方法,loadClass方法已经给我们写好了搜索类的顺序,但是findClass方法实际并没有完全实现,需要我们自己完善此方法,loadClass搜索类的顺序是,先调用findLoadedClass方法检测是否已经加载过此类,若加载过,则直接返回此类的Class实例,否则返回null,此时会调用委托类加载器的loadClass方法,搜索顺序同上,找到类或加载类成功,返回其Class实例,否则返回null,此时调用findClass方法,这个方法建议由继承ClassLoader的子类来实现 public class MyClassLoader extends ClassLoader { //我们要加载的类资源的路径,仅给出路径,不指定资源文件的名字,即类名,在我们调用 //loadClass方法时以参数的形式传入 private String resource_path; //指定自定义类加载器的委托类加载器 private ClassLoader parent; //用户自定义类加载器使用默认的委托类加载器,App public MyClassLoader(String resource_path) { this.resource_path = resource_path; } //用户自定义类加载器使用指定的委托类加载器 public MyClassLoader(String resource_path, ClassLoader parent) { //使用基类的构造方法来初始化委托类加载器 super(parent); this.resource_path = resource_path; } //重写基类的findClass方法,此方法根据我们给出的类名,找到此类并加载到JVM,类名是 //指程序中类的完全限定名 protected Class<?> findClass(String class_name) throws ClassNotFoundException { //若找到此类并加载成功,应该返回此类的Class类型的实例 Class<?> c = null; //将此类保存到程序中的字节数组中 byte[] class_data = this.getClassData(class_name); if(class_data == null) { //字节数组空,即没有找到类,抛出类未找到异常 throw new ClassNotFoundException(); } else { //字节数组不空,即找到类并成功的读取了类的数据,使用java提供的方法 //defineClass将此类的数据加载到JVM,并返回Class类型实例 c = this.defineClass(class_name, class_data, 0, class_data.length); } return c; } //流,任何一个流,都有一个源,一个目的地,流就是将源的内容送往目的地, 无论输入流还是输出流,都是将源的内容送到目的地,输入输出只是相对程序来说的,程序是当前使用该流的程序,输入流,程序是目的地,将源的内容送往程序。输出流,程序是源,将程序的内容送往目的地 private byte[] getClassData(String class_name) { //输入流,用于将类数据读入到程序 InputStream is = null; //资源URL,协议,IP,路径,文件名 String resource_url = null; URL url = null; //字节数组作为缓冲,一次可以缓冲4KB个字节 byte[] buffer = new byte[1024*4]; //表示当前读取到的字节个数 int len = -1; //字节数组输出流,可以将程序中字节数组的数据写出到输出流中,也可以将输出流中的内容直接转成字节数组 ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { //要加载的资源的完整URL resource_url = resource_path + "/" + class_name.replace(".", "/") + ".class"; //初始化URL对象,任一个URL对象都对应一个具体的资源 url = new URL(resource_url); //通过一个具体的URL对象打开一个输入流,此输入流的源就是URL对象所指向的资源,即将URL对象所指向的资源的内容读入到程序 is = url.openStream(); //将源的内容读入到程序的缓冲字节数组中,一次读取的字节个数最大为缓冲字节数组的大小,返回值为读取的字节数,<= 字节数组的长度,若到文件的末尾或出错则返回-1,之后程序再从字节数组中取出数据做相应的处理 while((len = is.read(buffer)) != -1) { //字节数组输出流,源是程序的缓冲字节数组,将源的内容写到输出流,此时数据是缓冲在输出流的,再向输出流写是向输出流后追加,循环写到字节数组输出流,将程序的内容写到字节数组输出流,还没有执行输出,最后将输出流转成字节数组算是输出了。 baos.write(buffer, 0, len); } //toByteArray创建一个字节数组,并将输出流的内容输出到此字节数组中去并返回此字节数组,这一步算是真正的输出 return baos.toByteArray(); } catch (Exception e) { //输出异常仅仅是在调试程序的时候使用,实际交付客户的项目是不能将异常输出的,但是必须做出客户能够知道的相应的处理 System.out.println("getClassData " + e.toString()); //由系统输出方法的调用栈信息及异常信息,异常信息就是e.toString()返回的内容,e.printStackTrace()这个方法并非主线程执行而是由系统启动一个线程来做相应的处理,在此实际是个多线程程序,这个方法在项目交付客户的时候也应该注释掉,不能给客户,对于这些以后要注释的信息可以做个TODO标记 e.printStackTrace(); } finally { //流使用完之后应该关闭,释放其所占用的资源 if(is != null) { try { is.close(); } catch (IOException e) { System.out.println("getClassData " + e.toString()); e.printStackTrace(); } } if(baos != null) { try { baos.close(); } catch (IOException e) { System.out.println("getClassData " + e.toString()); e.printStackTrace(); } } } return null; } } public class ClassLoaderTest { public static void main(String[] args) { //要访问的资源在web应用上,所以使用之前应该先启动Tomcat服务器 String resource_path = "http://localhost:8080/ServletTest/resource/classes"; MyClassLoader myl = new MyClassLoader(resource_path); MyClassLoader myl1 = new MyClassLoader(resource_path); String class_name = "com.lnu.test.Person"; try { //可以使用Class<?> c = Class.forName(class_name, true, myl) 来加载类,这里是网络上的类,由此不可能有APP来加载,最终是由my1与my11来加载的 Class<?> c = myl.loadClass(class_name); Class<?> c1 = myl1.loadClass(class_name); System.out.println("类名:" + c.getName()); System.out.println("类的定义类加载器: " + c.getClassLoader()); System.out.println("自定义类加载器的委托类加载器 :" + myl.getParent()); System.out.println("===测试被不同的类加载器的实例加载的同一个类是 否是相同的类型==="); Object objc = c.newInstance(); Object objc1 = c1.newInstance(); JVM认为,被不同的类加载器实例加载的同一个类属于不同的类类型 objc与objc1分别被不同的类加载器加载,由此为不同的实例,返回false System.out.println(objc.getClass() == objc1.getClass()); //两个类加载器的类类型,都是MyClassLoader类型,返回true,但是是两个不同的 实例 System.out.println(myl.getClass() == myl1.getClass()); System.out.println("======执行类的某个方法========="); Object obj = c.newInstance(); Class<?>[] param_type = {}; Method method = c.getDeclaredMethod("say", param_type); Object[] param_values = {}; method.invoke(obj, param_values); } catch (Exception e) { System.out.println("ClassLoaderTest " + e.toString()); e.printStackTrace(); } } } //这个类主要是利用反射将URLClassLoader中的addURL方法暴露出来,此方法可以加载本地的JAR文件 public class URLClassLoaderReflectTest { private Method method; //系统类加载器App实际是URLClassLoader的子类,在此强制类型转换,对象上转用系统类加载器的实例初始化URLClassLoader private static URLClassLoader ucl = (URLClassLoader) ClassLoader.getSystemClassLoader(); 找到addURL方法,并将其返回 private Method getMethod() { try { //根据方法名addURL,方法的参数类型,返回此方法,这里可以直接使用URLClassLoader中的静态class变量,因为URLClassLoader是被已经加载的App的实例初始化的 method = URLClassLoader.class.getDeclaredMethod("addURL", new Class[] { URL.class }); method.setAccessible(true); return method; } catch (Exception e) { System.out.println("initAddMethod " + e.toString()); e.printStackTrace(); } return null; } //对要加载的目录下的文件进行处理 //file,要加载的一个JAR文件,或是一系列JAR文件的上一级目录 //files,将要加载的所有的JAR文件都add到List列表 private void loopFiles(File file, List<File> files) { //如果file是目录,则此目录下有超过一个的JAR文件要被加载 if (file.isDirectory()) { //返回file目录下的所有文件及目录 File[] tmps = file.listFiles(); //遍历返回的文件数组 for (File tmp : tmps) { //对其中的每个文件再次进行处理,若file还是目录则还要处理,只有是文件时才会进入下一步 loopFiles(tmp, files); } } else { //若file是文件,则将文件名的后缀为jar或者zip的文件添加到list if (file.getAbsolutePath().endsWith(".jar") || file.getAbsolutePath().endsWith(".zip")) { files.add(file); } } } //通过反射调用addURL方法将jar文件加载到JVM //file,要加载到JVM的JAR文件 private void loadJarFile(File file) { try { this.getMethod().invoke(ucl, new Object[] { file.toURI().toURL() }); System.out.println("加载JAR包:" + file.getAbsolutePath()); } catch (Exception e) { System.out.println("loadJarFile " + e.toString()); e.printStackTrace(); } } //根据给定的文件的路径来加载路径下的JAR文件 //path,文件的路径 public void loadJarPath(String path) { List<File> files = new ArrayList<File>(); File lib = new File(path); loopFiles(lib, files); for (File file : files) { loadJarFile(file); } } public static void main(String[] args) { //这个方法执行完后就已经将E:/jar/路径下的JAR文件加载到JVM了 new URLClassLoaderReflectTest().loadJarPath("E:/jar/"); try { //JAR文件已经被加载到JVM,loadClass直接返回Class实例 Class<?> c = ucl.loadClass("com.lnu.test.TestJar"); Object obj = c.newInstance(); Class<?>[] param_type = {}; Object[] param_values = {}; Method mhd = c.getDeclaredMethod("say", param_type); mhd.invoke(obj, param_values); } catch (Exception e) { System.out.println("main " + e.toString()); e.printStackTrace(); } } } public class URLClassLoaderTest { //用URLClassLoader加载JAR包下的类,需要指定JAR包文件的位置,要给出文件名 private String resource_url_1 = "http://localhost:8080/ServletTest/resource/classes/TestJar.jar"; private String resource_url_2 = "http://localhost:8080/ServletTest/resource/classes/TestJars.jar"; //用URLClassLoader加载指定路径下的class文件,在此仅仅是指定路径,不指定类文件,实际要加载的类文件在调用loadClass方法时传入,URLClassLoader认为URL资源最后一个"/"之前出现的内容都是路径,所以在此classes后必须有"/",否则URLClassLoader会认为classes是一个文件,而试图去解析加载它,而不是将其当做一个目录 private String resource_path = "http://localhost:8080/ServletTest/resource/classes/"; //创建两个URLClassLoader对象,一个用于测试加载JAR包下的类,一个用于测试加载指定路径下的类 private URLClassLoader ucl_jar; private URLClassLoader ucl_class; //调用反射时所用到的,参数类型数组与参数值数组 private Class<?>[] param_type = {}; private Object[] param_values = {}; private Class<?> c; private Object obj; private Method mhd; public void TestURLClassLoader() { try { //加载JAR包下的类,指定URL资源, URL url_1 = new URL(resource_url_1); URL url_2 = new URL(resource_url_2); //直接加载类文件,仅指定URL资源的路径 URL url_path = new URL(resource_path); //URLClassLoader的构造方法需要传递一个URL数组,意思是指,可以指定可以搜索加载多个指定路径下的JAR文件或class文件资源 ucl_jar = new URLClassLoader(new URL[]{url_1, url_2}); ucl_class = new URLClassLoader(new URL[]{url_path}); //之后的内容就跟自定义类加载器的使用同样了,即,调用loadClass方法,指定类名,来加载指定的类 System.out.println("====测试使用URLClassLoader加载jar包下的类===="); c = ucl_jar.loadClass("com.lnu.test.TestJar"); obj = c.newInstance(); mhd = c.getDeclaredMethod("say", param_type); mhd.invoke(obj, param_values); System.out.println("====测试使用URLClassLoader加载jar包下的类===="); c = ucl_jar.loadClass("com.lnu.test.TestJars"); obj =c.newInstance(); mhd = c.getDeclaredMethod("say", param_type); mhd.invoke(obj, param_values); System.out.println("===测试使用URLClassLoader加载指定路径下的类=="); c = ucl_class.loadClass("com.lnu.test.Person"); obj = c.newInstance(); mhd = c.getDeclaredMethod("say", param_type); mhd.invoke(obj, param_values); System.out.println("==测试使用URLClassLoader加载指定路径下的类==="); c = ucl_class.loadClass("com.lnu.test.Persons"); obj = c.newInstance(); mhd = c.getDeclaredMethod("say", param_type); mhd.invoke(obj, param_values); } catch (Exception e) { System.out.println("URLClassLoaderTest say " + e.toString()); e.printStackTrace(); } finally { try { if(ucl_jar != null) { ucl_jar.close(); } if(ucl_class != null) { ucl_class.close(); } } catch (IOException e) { System.out.println("URLClassLoaderTest say " + e.toString()); e.printStackTrace(); } } } }
相关文章推荐
- 【14】-java的单例设计模式详解
- Code Hunt 题解 09-12 (Java)
- 【14】-java的单例设计模式详解
- 【14】-java的单例设计模式详解
- ECharts-Java使用Java快速开发ECharts图表
- webservice客户端和spring集成的常用方法
- eclipse
- 《大话Java性能优化》已经发表,天猫、亚马逊、京东、当当均有销售,提前谢谢支持
- 20145212 《Java程序设计》第9周学习总结
- struts2中form提交到action中的中文参数乱码问题解决办法(包括取中文路径)
- spring多种格式日期类型绑定
- java BigInteger BigDecimal类
- 第九章课后题
- Java设计模式之观察者模式
- Java多线程卖票例子
- Intellij Idea-最智能的java IDE
- Code Hunt 题解 05-08 (Java)
- 【JAVA】hashcode() & equals()
- Eclipse上安装GIT插件EGit及使用
- Eclipse:The resource 'project name'is not accessible for lauching