java ClassLoader 基本原理.docx_第1页
java ClassLoader 基本原理.docx_第2页
java ClassLoader 基本原理.docx_第3页
java ClassLoader 基本原理.docx_第4页
java ClassLoader 基本原理.docx_第5页
已阅读5页,还剩16页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

.类加载器基本概念顾名思义,类加载器(ClassLoader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下: Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。 类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如: Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。.Classloader类图注意:这是类关系,非对象关系。基本上所有的类加载器都是 java.lang.ClassLoader类的一个实例。BootstrapClassLoader是一个单独的java类, 其实在这里, 不应该叫他是一个java类,因为, 它已经完全不用java实现了。java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。不过本文只讨论其加载类的功能。为了完成加载类的这个职责,ClassLoader提供了一系列的方法,比较重要的方法如:getParent()返回该类加载器的父类加载器。loadClass(String name)加载名称为 name的类。是加载器加载类的主要方法,也是实现双亲委派模型的主要方法(稍后会详细讲解)。返回的结果是 java.lang.Class类的实例。findClass(String name)在自己的查找路径中,查找名称为 name的类,并调用defineClass方法,加载该类。返回的结果是 java.lang.Class类的实例。findLoadedClass(String name)在自己的类加载器中,查找名称为 name的已经被自己加载过的类。返回的结果是 java.lang.Class类的实例。defineClass(String name, byte b, int off, int len)把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。按照Java Language Specification的定义,任何作为 String 类型参数传递给ClassLoader 中方法的类名称都必须是一个二进制名称(上述方法中的String name)。有效类名称的示例包括:java.lang.Stringjavax.swing.JSpinner$DefaultEditorjava.security.KeyStore$Builder$FileBuilder$1.URLClassLoader$3$1需要注意的是内部类的String name表示,如 com.example.Sample$1和 com.example.Sample$Inner等表示方式。URLClassLoader是ClassLoader的子类,是系统类加载器(appCLassLoader)和扩展类加载器(extClassLoader)的父类(类图关系,非对象关系)。URLClassLoader非常强大,可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类。实际上,在应用程序中,可以直接使用URLClassLoader来加载类,URLClassLoader提供了如下两个构造器: URLClassLoader(URL urls):使用默认的父类加载器创建一个ClassLoader对象,该对象将从urls指定的系列路径来查询并加载类。 URLClassLoader(URL urls,ClassLoader parent):使用指定的父类加载器创建一个ClassLoader对象,其他功能与前一个构造器相同。.Classloader对象关系图Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java应用开发人员编写的。通常,会对类加载器对象指定一个或多个“目录或文件”,使类加载器只加载这些指定目录或文件下的.class文件。系统提供的类加载器主要有下面三个:1).引导类加载器(bootstrapClassLoader): 它用来加载Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。 例如:Java 的核心库一般是JRE目录下的“librt.jar”等等。 具体路径可以用System.out.println(System.getProperty(sun.boot.class.path);获取。2 ).扩展类加载器(extClassLoader): 它用来加载Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。 该类加载器在此目录里面查找并加载 Java 类。 例如:Java 的扩展库一般是JRE目录的“jrelibext”等等。 具体路径可以用System.out.println(System.getProperty(java.ext.dirs);获取。3 ).系统类加载器(appCLassLoader): 它根据Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。 可以通过 ClassLoader.getSystemClassLoader()来获取它。 具体路径可以用System.out.println(System.getProperty(java.class.path);获取。除了引导类加载器之外,所有的类加载器都有一个父加载器。通过getParent()方法可以得到。对于系统提供的类加载器来说,系统类加载器(appCLassLoader)的父加载器是扩展类加载器(extClassLoader),而扩展类加载器(extClassLoader)的父加载器是引导类加载器(bootstrapClassLoader)。而且,java中的每个 Java 类都维护着一个指向加载它的classloader的引用,通过 getClassLoader()方法就可以获取到此引用,例如ClassLoader cl=Hello.class.getClassLoader( )。对于开发人员编写的类加载器(例如,CustomClassLoaderA)来说,CustomClassLoaderA在类Person中,实例化一个ClassLoader对象:?12345publicclassPersonpublicvoidsayHello()ClassLoadercustomClassLoaderA=newCustomClassLoaderA();那么customClassLoaderA的父加载器就是加载类Person的加载器,即Person.getClassLoader( )。一般来说,开发人员编写的类加载器的父类加载器是系统类加载器(appCLassLoader)。所以,类加载器通过这种方式组织起来,形成树状结构。如下图:注意:这是对象关系,非类关系。代码示例:通过递归调用 getParent()方法来输出全部的父类加载器:?12345678.URL;.URLClassLoader;publicclassCustomClassloaderAextendsURLClassLoaderpublicCustomClassloaderA(URLurls)super(urls);?123456789101112131415.URL;publicclassTestMainpublicstaticvoidmain(Stringargs)throwsClassNotFoundExceptionURLurl=newURL(file:/D:/workspace/classfinder/src/com/sunchp);ClassLoaderclassloader=newCustomClassloaderA(newURLurl);while(classloader!=null)System.out.println(classloader.toString();classloader=classloader.getParent();运行结果:com.sunchp.CustomClassloaderAa574b2sun.misc.Launcher$AppClassLoader417470d0sun.misc.Launcher$ExtClassLoader439a8942 第一个输出的是自定义的类加载器com.sunchp.CustomClassloaderAa574b2,它是我们自定义的类加载器CustomClassloaderA类的实例; 第二个输出的是系统类加载器(appCLassLoader),它是 sun.misc.Launcher$AppClassLoader类的实例; 第三个输出的是扩展类加载器(extClassLoader),是 sun.misc.Launcher$ExtClassLoader类的实例。需要注意的是这里并没有输出引导类加载器,这是由于有些 JDK 的实现对于父类加载器是引导类加载器的情况,getParent()方法返回 null。.3种Classloader搜索路径appCLassLoader和extClassLoader都是URLClassLoader的子类,所以他们实际也需要一个URL作为参考,去载入类。1).appCLassLoader所参考的URL是从系统参数java.class.path取出来的。这个参数,可以在我们执行java.exe时,使用-cp或者-classpath或者CLASSPATH环境变量指定:?12Strings=System.getProperty(java.class.path);System.out.println(s);classpath,代表的是当前类目录(或者说当前项目对应的class目录),已经所引用的jar文件。使用-cp或者-classpath可以改变,相对于系统的CLASSPATH,优先使用-cp或者-classpath。2).extClassLoader参考的是系统参数java.ext.dirs:?12Strings=System.getProperty(java.ext.dirs);System.out.println(s);3).bootstrapClassLoader参考的是系统参数,sun.boot.class.path:?12Strings=System.getProperty(sun.boot.class.path);System.out.println(s); 改变appCLassLoader和extClassLoader对应的参数是有意义的,也会影响它们的搜索路径; 改变bootstrapClassLoader的参数是没有易用的,bootstrapClassLoader的搜索路径只是与sun.boot.class.path的预设值相同,并不是参考使用了sun.boot.class.path。 appCLassLoader和extClassLoader在内存中分别只有一个实例,所以在内存中动态改变参数是行不通的。 如果需要加载其他路径的class文件,推荐自定义类加载器,并设置自定义类加载器的搜索路径。.自定义类加载器JVM中除根类加载器之外的所有类加载器都是ClassLoader子类的实例,开发者可以通过扩展ClassLoader的子类,并重写该ClassLoader所包含的方法来实现自定义的类加载器。查阅API文档中关于ClassLoader的方法不难发现,ClassLoader中包含了大量的protected方法这些方法都可以被子类重写。ClassLoader类有如下两个关键方法: loadClass(String name,boolean resolve):该方法为ClassLoader的入口点,根据指定的二进制名称来加载类,系统就是调用ClassLoader的该方法来获取指定类对应的Class对象。 findClass(String name):根据二进制名称,在自己的查找路径中,查找名称为 name的类,并调用defineClass方法,加载该类。返回的结果是 java.lang.Class类的实例。如果需要实现自定义的ClassLoader,则可以通过重写以上两个方法来实现,当然,我们推荐重写findClass()方法,而不是重写loadClass()方法。loadClass()方法的执行步骤如下: 用findLoadedClass(String name)来检查是否已经加载类,如果已经加载,则直接返回。 在父类加载器上调用loadClass()方法。如果父类加载器为null,则直接使用根类加载器加载。 调用findClass(String name)方法查找类。从以上步骤可以看出,重写findClass()方法可以避免覆盖默认类加载器的父类委托,缓冲机制两种策略;如果重写loadClass()方法,则实现逻辑更为复杂。在ClassLoader里还有一个核心方法:Class defineClass(String name,byte b,int off,int len),该方法负责将指定类的字节码文件(即Class文件,如Hello.class)读入字节数组byte b 内,并把它转换为Class对象,该字节码文件可以源于文件、网络等。defineClass()方法管理JVM的许多复杂的实现,它负责将字节码分析成运行时数据结构,并校验有效性等。不过不用担心,程序员无须重写该方法。事实上,该方法是final型,即使我们想重写也没有机会。除此之外,ClassLoader里还包含如下一些普通方法: findSystemClass(String name):在本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用defineClass()方法将原始字节转换成Class对象,以将该文件转换成类。 static getSystemClassLoader():这是一个静态方法,用于返回系统类加载器。 getParent():获取该类加载器的父类加载器。 resolveClass(Class c):链接指定的类。类加载器可以使用此方法来链接类c。读者无须理会关于此方法的太多细节。 findLoadedClass(String name):如果此java虚拟机已经加载了名为name的类,则直接返回该类对应的Class实例,否则返回null。此方法是java类加载缓存机制的体现。loadClass()、findClass()、findLoadedClass()和defineClass的关系可以用如下代码表示(这也是实现下述“双亲委派模型”的主要逻辑):?1234567891011121314151617181920ClassloadClass(Stringname,booleanresolve)/检查类是否已被装载过Classc=findLoadedClass(name);if(c=null)/指定类未被装载过tryif(parent!=null)/如果父类加载器不为空,则委派给父类加载c=parent.loadClass(name,false);else/如果父类加载器为空,则委派给启动类加载加载c=findBootstrapClass0(name);catch(ClassNotFoundExceptione)/启动类加载器或父类加载器抛出异常后,当前类加载器将其/捕获,并通过findClass方法,调用defineClass(),由自身加载c=findClass(name);.双亲委派模型所谓的双亲委派模型,就是,classloader实例当有类需要载入时,会先让parent采用他的搜寻路径去加载类,如果parent加载不到,那么才由自己去加载。其实,所谓双亲委派模型只有两个过程: 首先,自底向上,挨个加载器检查是否已经加载了指定类,直到根加载器(bootstrapClassLoader)。这个过程中,如果已经加载,那么直接返回该类Class实例的引用。这个过程主要是findLoadedClass()方法的调用。 然后,自顶向下尝试加载类。如果根加载器(bootstrapClassLoader)也未加载成功该类,那么就会抛出异常,然后自顶向下挨个尝试加载。直到customClassloader调用findClass( )尝试加载。如果还未加载成功,就抛出ClassNotFoundException给调用者。代码逻辑:?1234567891011121314151617181920ClassloadClass()/检查类是否已被装载过Classc=findLoadedClass(name);if(c=null)/指定类未被装载过tryif(parent!=null)/如果父类加载器不为空,则委派给父类加载c=parent.loadClass(name,false);else/如果父类加载器为空,则委派给启动类加载加载c=findBootstrapClass0(name);catch(ClassNotFoundExceptione)/启动类加载器或父类加载器抛出异常后,当前类加载器将其/捕获,并通过findClass方法,由自身加载c=findClass(name);一般来说,通过XXXX.class.getClassLoader()或者Class.forName( )获取当前类的加载器,然后用该加载器加载目标类,例如“com.sunchp.demo”。此时,都是以当前类的加载器为起点,依次找它的父亲加载器,进行加载目标类。(这个地方,对理解后面的“线程上下文类加载器”很重要)情景一:?12ClassLoadercl=XXXX.class.getClassLoader();cl.loadClass(com.sunchp.Demo); 如果XXXX是由自定义加载器加载的,则从该自定义加载器开始查找com.sunchp.Demo,customClassloader-appClassLoader-extClassLoader-bootstrapClassLoader; 如果XXXX是由appClassLoader加载的,则加载过程是appClassLoader-extClassLoader-bootstrapClassLoader;XXXX的加载器,看不到CustomClassLoader。 如果XXXX是由extClassLoader加载的,则加载过程是extClassLoader-bootstrapClassLoader; 如果XXXX是由bootstrapClassLoader加载的,则加载过程只有bootstrapClassLoader。情景二:?12345publicclassApublicvoidsayHello()ClassdemoClass=Class.forName(com.sunchp.Demo);因为Class.forName用的是前类的加载器,即A.class.getClassLoader( ),所以 如果当前类A是由自定义加载器加载的,则从该自定义加载器开始查找com.sunchp.Demo,customClassloader-appClassLoader-extClassLoader-bootstrapClassLoader;同上。 如果当前类A是由appClassLoader加载的,则加载过程是appClassLoader-extClassLoader-bootstrapClassLoader;同上。 如果当前类A是由extClassLoader加载的,则加载过程是extClassLoader-bootstrapClassLoader;同上。 如果当前类A是由bootstrapClassLoader加载的,则加载过程只有bootstrapClassLoader。同上。Java 虚拟机是如何判定两个 Java 类是相同的?Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。例如:一个 Java 类 com.example.Sample,编译之后生成了字节代码文件 Sample.class。两个不同的类加载器 classLoaderA和 classLoaderB分别读取了这个 Sample.class文件,并定义出两个 java.lang.Class类的实例来表示这个类。这两个实例是不相同的。对于 Java虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常 ClassCastException。代码示例:?123456789packagecom.example;publicclassSampleprivateSampleinstance;publicvoidsetSample(Objectinstance)this.instance=(Sample)instance;将Sample.class文件放在一个“3种Classloader搜索路径”之外的一个目录中,例如:D:/demo/使用URLClassLoader类的两个不同实例来分别加载类 com.example.Sample,得到了两个不同的 java.lang.Class的实例,接着通过 newInstance()方法分别生成了两个类的对象 obj1和 obj2,最后通过 Java 的反射 API 在对象 obj1上调用方法 setSample,试图把对象 obj2赋值给 obj1内部的 instance对象。?1234567891011121314151617181920212223publicclassTestMainpublicstaticvoidmain(Stringargs)tryURLurl=newURL(file:/D:/demo/);ClassLoaderclassloader1=newCustomClassloaderA(newURLurl);ClassLoaderclassloader2=newCustomClassloaderA(newURLurl);Classclass1=classloader1.loadClass(com.sunchp.Sample);Objectobj1=class1.newInstance();Classclass2=classloader2.loadClass(com.sunchp.Sample);Objectobj2=class2.newInstance();MethodsetSampleMethod=class1.getMethod(setSample,java.lang.Object.class);setSampleMethod.invoke(obj1,obj2);catch(Exceptione)e.printStackTrace();运行结果:?123456789java.lang.reflect.InvocationTargetExceptionatsun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethod)atsun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)atsun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)atjava.lang.reflect.Method.invoke(Method.java:606)atcom.sunchp.TestMain.main(TestMain.java:23)Causedby:java.lang.ClassCastException:com.sunchp.Samplecannotbecasttocom.sunchp.Sampleatcom.sunchp.Sample.setSample(Sample.java:7).5more从运行结果可以看到,运行时抛出了 java.lang.ClassCastException异常。虽然两个对象 obj1和 obj2的类的名字相同,但是这两个类是由不同的类加载器实例来加载的,因此不被 Java 虚拟机认为是相同的。了解了这一点之后,就可以理解双亲委派模型的设计动机了。双亲委派模型的作用:兼容性:例如,所有 Java 应用都至少需要引用 java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。假如加载器不采用这种双亲委派模型,每个加载器都各自加载自己的Object,那么就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。相反,加载器采用这种双亲委派模型,加载工作都是由BootstrapClassLoader来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。安全性:假设类加载器试图加载一个被蓄意破坏者重写的java.lang.String的class文件时,通过双亲委派模型,JVM中总是加载JVM中官方的java.lang.String,蓄意破坏者没有机会植入有问题的代码到我们的电脑中。.类加载方式1). new?1DogdognewDog();这个动作会导致常量池的解析,执行类的静态初始化语句,Dog类被隐式装载。如果当前ClassLoader无法找到Dog,则抛出NoClassDefFoundError。2 ).调用 Class.forName() 方法?12ClassclazzClass.forName(Dog);Objectdogclazz.newInstance();通过反射加载类型,执行类的静态初始化语句,并创建对象实例.如果无法找到Dog,则抛出ClassNotFoundException。3 ).调用某个 ClassLoader 实例的 loadClass() 方法?12ClassclazzclassLoader.loadClass(Dog);Objectdogclazz.newInstance();通过反射加载类型,不执行类的静态初始化语句,并创建对象实例.如果无法找到Dog,则抛出ClassNotFoundException。第1和第2种情况区别不大。如果,Dog类在编译时无法得到,则使用第2种方式。如果需要在当前类路径以外寻找类,则只能采用第3种方式。1和2使用的类加载器是相同的,都是当前类加载器。(即:this.getClass().getClassLoader())。3由用户指定类加载器。另外,第1种和第2种都会导致类被初始化,即:执行类的静态初始化语句,而第3种情况不会。另外注意,第1种抛出Error,第2、3种抛出Exception,它们分属于不同的异常

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论