java classloader 原理详细分析-亚博电竞手机版

一、什么是classloader?

大家都知道,当我们写好一个java程序之后,不是管是cs还是bs应用,都是由若干个.class文件组织而成的一个完整的java应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的class文件当中,所以经常要从这个class文件中要调用另外一个class文件中的方法,如果另外一个文件不存在的,则会引发系统异常。而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过java的类加载机制(classloader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以classloader就是用来动态加载class文件到内存当中用的。

二、java默认提供的三个classloader

  1. bootstrap classloader:称为启动类加载器,是java类加载层次中最顶层的类加载器,负责加载jdk中的核心类库,如:rt.jar、resources.jar、charsets.jar等,可通过如下程序获得该类加载器从哪些地方加载了相关的jar或class文件:
    url[] urls = sun.misc.launcher.getbootstrapclasspath().geturls();   for (int i = 0; i < urls.length; i  ) {       system.out.println(urls[i].toexternalform());   }

    以下内容是上述程序从本机jdk环境所获得的结果:

    file:/c:/program files/java/jdk1.6.0_22/jre/lib/resources.jar

    file:/c:/program files/java/jdk1.6.0_22/jre/lib/rt.jar

    file:/c:/program files/java/jdk1.6.0_22/jre/lib/sunrsasign.jar

    file:/c:/program files/java/jdk1.6.0_22/jre/lib/jsse.jar

    file:/c:/program files/java/jdk1.6.0_22/jre/lib/jce.jar

    file:/c:/program files/java/jdk1.6.0_22/jre/lib/charsets.jar

    file:/c:/program files/java/jdk1.6.0_22/jre/classes/

    其实上述结果也是通过查找sun.boot.class.path这个系统属性所得知的。

    system.out.println(system.getproperty("sun.boot.class.path"));

    打印结果:c:\program files\java\jdk1.6.0_22\jre\lib\resources.jar;c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar;c:\program files\java\jdk1.6.0_22\jre\lib\sunrsasign.jar;c:\program files\java\jdk1.6.0_22\jre\lib\jsse.jar;c:\program files\java\jdk1.6.0_22\jre\lib\jce.jar;c:\program files\java\jdk1.6.0_22\jre\lib\charsets.jar;c:\program files\java\jdk1.6.0_22\jre\classes

  2. extension classloader:称为扩展类加载器,负责加载java的扩展类库,默认加载java_home/jre/lib/ext/目下的所有jar。
  3. app classloader:称为系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件。

注意: 除了java默认提供的三个classloader之外,用户还可以根据需要定义自已的classloader,而这些自定义的classloader都必须继承自java.lang.classloader类,也包括java提供的另外二个classloader(extension classloader和app classloader)在内,但是bootstrap classloader不继承自classloader,因为它不是一个普通的java类,底层由c 编写,已嵌入到了jvm内核当中,当jvm启动后,bootstrap classloader也随着启动,负责加载完核心类库后,并构造extension classloader和app classloader类加载器。

三、classloader加载类的原理

1、原理介绍

classloader使用的是双亲委托模型来搜索类的,每个classloader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(bootstrap classloader)本身没有父类加载器,但可以用作其它classloader实例的的父类加载器。当一个classloader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器bootstrap classloader试图加载,如果没加载到,则把任务转交给extension classloader试图加载,如果也没加载到,则转交给app classloader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等url中加载该类。如果它们都没有加载到这个类时,则抛出classnotfoundexception异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的class实例对象。

2、为什么要使用双亲委托这种模型呢?

因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子classloader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的string来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为string已经在启动时就被引导类加载器(bootstrcp classloader)加载,所以用户自定义的classloader永远也无法加载一个自己写的string,除非你改变jdk中classloader搜索类的默认算法。

3、 但是jvm在搜索类的时候,又是如何判定两个class是相同的呢?

jvm在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,jvm才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的classloader实例所加载,jvm也会认为它们是两个不同class。比如网络上的一个java类org.classloader.simple.netclassloadersimple,javac编译之后生成字节码文件netclassloadersimple.class,classloadera和classloaderb这两个类加载器并读取了netclassloadersimple.class文件,并分别定义出了java.lang.class实例来表示这个类,对于jvm来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个class实例生成具体的对象进行转换时,就会抛运行时异常java.lang.classcaseexception,提示这是两个不同的类型。现在通过实例来验证上述所描述的是否正确:

1)、在web服务器上建一个org.classloader.simple.netclassloadersimple.java类

package org.classloader.simple;    public class netclassloadersimple {        private netclassloadersimple instance;        public void setnetclassloadersimple(object obj) {           this.instance = (netclassloadersimple)obj;       }   }

org.classloader.simple.netclassloadersimple类的setnetclassloadersimple方法接收一个object类型参数,并将它强制转换成org.classloader.simple.netclassloadersimple类型。

2)、测试两个class是否相同(networkclassloader.java)

package classloader;    public class newworkclassloadertest {        public static void main(string[] args) {           try {               //测试加载网络中的class文件               string rooturl = "http://localhost:8080/httpweb/classes";               string classname = "org.classloader.simple.netclassloadersimple";               networkclassloader ncl1 = new networkclassloader(rooturl);               networkclassloader ncl2 = new networkclassloader(rooturl);               class clazz1 = ncl1.loadclass(classname);               class clazz2 = ncl2.loadclass(classname);               object obj1 = clazz1.newinstance();               object obj2 = clazz2.newinstance();               clazz1.getmethod("setnetclassloadersimple", object.class).invoke(obj1, obj2);           } catch (exception e) {               e.printstacktrace();           }       }    }

首先获得网络上一个class文件的二进制名称,然后通过自定义的类加载器networkclassloader创建两个实例,并根据网络地址分别加载这份class,并得到这两个classloader实例加载后生成的class实例clazz1和clazz2,最后将这两个class实例分别生成具体的实例对象obj1和obj2,再通过反射调用clazz1中的setnetclassloadersimple方法。

3)、查看测试结果

结论:从结果中可以看出,虽然是同一份class字节码文件,但是由于被两个不同的classloader实例所加载,所以jvm认为它们就是两个不同的类。

4、classloader的体系架构:

验证classloader加载类的原理:

测试1:打印classloader类的层次结构,请看下面这段代码:

classloader loader = classloadertest.class.getclassloader();    //获得加载classloadertest.class这个类的类加载器   while(loader != null) {       system.out.println(loader);       loader = loader.getparent();    //获得父类加载器的引用   }   system.out.println(loader);

打印结果:

第一行结果说明:classloadertest的类加载器是appclassloader。

第二行结果说明:appclassloader的类加器是extclassloader,即parent=extclassloader。

第三行结果说明:extclassloader的类加器是bootstrap classloader,因为bootstrap classloader不是一个普通的java类,所以extclassloader的parent=null,所以第三行的打印结果为null就是这个原因。

测试2:将classloadertest.class打包成classloadertest.jar,放到extension classloader的加载目录下(java_home/jre/lib/ext),然后重新运行这个程序,得到的结果会是什么样呢?

打印结果:

打印结果分析:

为什么第一行的结果是extclassloader呢?

因为classloader的委托模型机制,当我们要用classloadertest.class这个类的时候,appclassloader在试图加载之前,先委托给bootstrcp classloader,bootstracp classloader发现自己没找到,它就告诉extclassloader,兄弟,我这里没有这个类,你去加载看看,然后extension classloader拿着这个类去它指定的类路径(java_home/jre/lib/ext)试图加载,唉,它发现在classloadertest.jar这样一个文件中包含classloadertest.class这样的一个文件,然后它把找到的这个类加载到内存当中,并生成这个类的class实例对象,最后把这个实例返回。所以classloadertest.class的类加载器是extclassloader。

第二行的结果为null,是因为extclassloader的父类加载器是bootstrap classloader。

测试3:用bootstrcp classloader来加载classloadertest.class,有两种方式:

1、在jvm中添加-xbootclasspath参数,指定bootstrcp classloader加载类的路径,并追加我们自已的jar(classtestloader.jar)

2、将class文件放到java_home/jre/classes/目录下(上面有提到)

方式1:(我用的是eclipse开发工具,用命令行是在java命令后面添加-xbootclasspath参数)

打开run配置对话框:

配置好如图中所述的参数后,重新运行程序,产的结果如下所示:(类加载的过程,只摘下了一部份)

打印结果:

[loaded java.io.filereader from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded sun.nio.cs.streamdecoder from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.util.arraylist from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.lang.reflect.array from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.util.locale from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.util.concurrent.concurrentmap from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.util.concurrent.concurrenthashmap from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.util.concurrent.locks.lock from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.util.concurrent.locks.reentrantlock from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.util.concurrent.concurrenthashmap$segment from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.util.concurrent.locks.abstractownablesynchronizer from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.util.concurrent.locks.abstractqueuedsynchronizer from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.util.concurrent.locks.reentrantlock$sync from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.util.concurrent.locks.reentrantlock$nonfairsync from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.util.concurrent.locks.abstractqueuedsynchronizer$node from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.util.concurrent.concurrenthashmap$hashentry from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.lang.characterdatalatin1 from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.io.objectstreamclass from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded sun.net.www.parseutil from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.util.bitset from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.net.parts from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.net.urlstreamhandler from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded sun.net.www.protocol.file.handler from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.util.hashset from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded sun.net.www.protocol.jar.handler from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded sun.misc.launcher$appclassloader from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded sun.misc.launcher$appclassloader$1 from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.lang.systemclassloaderaction from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [path c:\program files\java\jdk1.6.0_22\jre\classes]   [loaded classloader.classloadertest from c:\program files\java\jdk1.6.0_22\jre\classes]   null  //这是打印的结果   c:\program files\java\jdk1.6.0_22\jre\lib\resources.jar;c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar;   c:\program files\java\jdk1.6.0_22\jre\lib\sunrsasign.jar;c:\program files\java\jdk1.6.0_22\jre\lib\jsse.jar;   c:\program files\java\jdk1.6.0_22\jre\lib\jce.jar;c:\program files\java\jdk1.6.0_22\jre\lib\charsets.jar;   c:\program files\java\jdk1.6.0_22\jre\classes;c:\classloadertest.jar     //这一段是system.out.println(system.getproperty("sun.boot.class.path"));打印出来的。这个路径就是bootstrcp classloader默认搜索类的路径   [loaded java.lang.shutdown from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]   [loaded java.lang.shutdown$lock from c:\program files\java\jdk1.6.0_22\jre\lib\rt.jar]

方式2:将classloadertest.jar解压后,放到java_home/jre/classes目录下,如下图所示:

提示:jre目录下默认没有classes目录,需要自己手动创建一个

打印结果:

从结果中可以看出,两种方式都实现了将classloadertest.class由bootstrcp classloader加载成功了。

四、定义自已的classloader

既然jvm已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?

因为java中提供的默认classloader,只加载指定目录下的jar和class,如果我们想加载其它位置的类或jar时,比如:我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现我的业务逻辑。在这样的情况下,默认的classloader就不能满足我们的需求了,所以需要定义自己的classloader。

定义自已的类加载器分为两步:

1、继承java.lang.classloader

2、重写父类的findclass方法

读者可能在这里有疑问,父类有那么多方法,为什么偏偏只重写findclass方法?

因为jdk已经在loadclass方法中帮我们实现了classloader搜索类的算法,当在loadclass方法中搜索不到类时,loadclass方法就会调用findclass方法来搜索类,所以我们只需重写该方法即可。如没有特殊的要求,一般不建议重写loadclass搜索类的算法。下图是api中classloader的loadclass方法:

示例:自定义一个networkclassloader,用于加载网络上的class文件

package classloader;    import java.io.bytearrayoutputstream;   import java.io.inputstream;   import java.net.url;    /**   * 加载网络class的classloader   */   public class networkclassloader extends classloader {        private string rooturl;        public networkclassloader(string rooturl) {           this.rooturl = rooturl;       }        @override       protected class findclass(string name) throws classnotfoundexception {           class clazz = null;//this.findloadedclass(name); // 父类已加载              //if (clazz == null) {  //检查该类是否已被加载过               byte[] classdata = getclassdata(name);  //根据类的二进制名称,获得该class文件的字节码数组               if (classdata == null) {                   throw new classnotfoundexception();               }               clazz = defineclass(name, classdata, 0, classdata.length);  //将class的字节码数组转换成class类的实例           //}            return clazz;       }        private byte[] getclassdata(string name) {           inputstream is = null;           try {               string path = classnametopath(name);               url url = new ;               byte[] buff = new byte[1024*4];               int len = -1;               is = url.openstream();               bytearrayoutputstream baos = new bytearrayoutputstream();               while((len = is.read(buff)) != -1) {                   baos.write(buff,0,len);               }               return baos.tobytearray();           } catch (exception e) {               e.printstacktrace();           } finally {               if (is != null) {                  try {                     is.close();                  } catch(ioexception e) {                     e.printstacktrace();                  }               }           }           return null;       }        private string classnametopath(string name) {           return rooturl   "/"   name.replace(".", "/")   ".class";       }    }

测试类:

package classloader;    public class classloadertest {        public static void main(string[] args) {           try {               /*classloader loader = classloadertest.class.getclassloader();  //获得classloadertest这个类的类加载器              while(loader != null) {                  system.out.println(loader);                  loader = loader.getparent();    //获得父加载器的引用              }              system.out.println(loader);*/                string rooturl = "http://localhost:8080/httpweb/classes";               networkclassloader networkclassloader = new networkclassloader(rooturl);               string classname = "org.classloader.simple.netclassloadertest";               class clazz = networkclassloader.loadclass(classname);               system.out.println(clazz.getclassloader());            } catch (exception e) {               e.printstacktrace();           }       }    }

打印结果:

下图是我机器上web服务器的目录结构:

目前常用web服务器中都定义了自己的类加载器,用于加载web应用指定目录下的类库(jar或class),如:weblogic、jboss、tomcat等,下面我以tomcat为例,展示该web容器都定义了哪些个类加载器:

1、新建一个web工程httpweb

2、新建一个classloaderservlettest,用于打印web容器中的classloader层次结构

import java.io.ioexception;   import java.io.printwriter;    import javax.servlet.servletexception;   import javax.servlet.http.httpservlet;   import javax.servlet.http.httpservletrequest;   import javax.servlet.http.httpservletresponse;    public class classloaderservlettest extends httpservlet {        public void doget(httpservletrequest request, httpservletresponse response)               throws servletexception, ioexception {            response.setcontenttype("text/html");           printwriter out = response.getwriter();           classloader loader = this.getclass().getclassloader();           while(loader != null) {               out.write(loader.getclass().getname() "
"); loader = loader.getparent(); } out.write(string.valueof(loader)); out.flush(); out.close(); } public void dopost(httpservletrequest request, httpservletresponse response) throws servletexception, ioexception { this.doget(request, response); } }

3、配置servlet,并启动服务

              classloaderservlettest       classloaderservlettest                  classloaderservlettest       /servlet/classloaderservlettest                 index.jsp        

4、访问servlet,获得显示结果

展开全文
内容来源于互联网和用户投稿,文章中一旦含有亚博电竞手机版的联系方式务必识别真假,本站仅做信息展示不承担任何相关责任,如有侵权或涉及法律问题请联系亚博电竞手机版删除

最新文章

网站地图