类加载器把类装入jvm,描述一下jvm加载

  类加载器把类装入jvm,描述一下jvm加载

  加载机制的层次结构。各编”。java”扩展名称类文件存储要执行的程序逻辑。这些”。java“文件被编译成一个名为”的扩展名文件。类”由Java编译器编译。的”。class”文件保存Java代码转换后的虚拟机指令。当一个类需要被使用时,虚拟机将加载它的”。类”文件。并创建相应的类对象并将类文件加载到虚拟机的内存中。这个过程称为类加载。这里,我们需要知道类加载的过程,如下:

  加载:类加载过程的一个阶段:通过类的完全限定找到这样的字节码文件,并使用字节码文件创建一个类对象。

  验证:目的是保证类文件的字节流中包含的信息符合当前虚拟机的要求,不会危及虚拟机本身的安全。包括四种验证,文件格式验证、元数据验证、字节码验证和符号引用验证。

  准备:为类变量(即用static修饰的字段变量)分配内存,并将这个类变量的初始值设置为0(如static int I=5;这里只将I初始化为0,初始化时赋值为5)。这里不包括用final修饰的static,因为final将在编译时分配。注意这里不会给实例变量分配初始化,类变量会分配在方法区,而实例变量会和对象一起分配到Java堆。

  解析:用直接引用替换常量池中符号引用的过程。引用是描述目标的一组符号,它可以是任何字面量,而直接引用是间接定位到目标的指针、相对偏移量或句柄。有类或接口解析,字段解析,类方法解析,接口方法解析(这里涉及到字节码变量的引用。更多详情请咨询《深入Java虚拟机》)。

  初始化:在类加载的最后阶段,如果类有超类,就会被初始化,执行静态初始化器和成员变量的静态初始化(比如这个阶段只初始化默认值的静态变量会被赋值,成员变量也会被初始化)。这是类加载的五个过程,类加载器的任务是根据类的全限定名读取类的二进制字节流,然后转换成目标类对应的java.lang.Class对象实例。虚拟机中提供了三个类加载器,即引导类加载器、扩展类加载器和系统类加载器(也称为应用程序类)

  类加载器的加载机制如下:

  Bootstrap类加载器Bootstrap类加载器主要加载JVM本身需要的类。这个类加载是用C语言实现的,是虚拟机本身的一部分。它负责将JAVA_HOME /lib路径下的核心类库或者-Xbootclasspath参数指定的路径下的jar包加载到内存中。注意虚拟机是根据文件名识别来加载jar包的,比如rt.jar如果文件名不被虚拟机识别,即使把jar包扔到lib目录也不会有什么影响(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等的类。).

  扩展类装入器扩展类装入器指的是Sun。Misc.Launcher $ ext class loader类由Sun公司(被Oracle收购)实现,用Java语言实现。它是launcher的静态内部类,负责加载JAVA_HOME /lib/ext目录下或者系统变量-djava.ext.dir指定的位路径下的类库,开发者可以直接使用标准的扩展类加载器。

  //编写//ExtClassLoader类中的私有静态文件[]getextdirs(){//加载JAVA_HOME /lib/ext目录下的类库字符串s=system . getproperty( JAVA . ext . dirs );文件[]目录;如果(s!=null){ string tokenizer ST=new string tokenizer(s,file . path separator);int count=ST . count tokens();dirs=新文件[计数];for(int I=0;我数;i ) { dirs[i]=新文件(ST . nexttoken());} } else { dirs=新文件[0];}返回dirs} System class loader也称application loader,指sun公司实现的sun.misc. launcher $ app类加载器。它负责在系统类路径java -classpath或-d java.classpath指定的路径下加载类库,也就是我们经常使用的类路径路径。开发者可以直接使用系统类加载器,系统类加载器一般是程序中默认的类加载器,可以通过class loader # getSystemClassloader()方法获取。

  在Java的日常应用开发中,类的加载几乎都是由上面三个类加载器来执行的。必要时,我们还可以定制类加载器。需要注意的是,Java虚拟机是按需加载类文件的,即当需要使用类时,会将其类文件加载到内存中生成类对象,而在加载某个类的类文件时,Java虚拟机采用父委托模式,即请求会由父类处理,这是一种任务。

  了解家长委托模式家长委托模式的工作原理。父委托模式要求除了顶级启动类装入器之外,所有的类装入器都应该有自己的父类装入器。请注意,父委托模式下的父子关系不是通常的类继承关系,而是使用组合关系来复用父类加载器的相关代码。类别载入器之间的关系如下:

  Java 1.2之后引入了Parent-delegate模式,其工作原理是,如果类加载器收到类加载请求,它不会先加载,而是将请求委托给父类加载器执行。如果父类装入器还有它的父类装入器,它会进一步向上递归委托,请求最终会到达顶层启动类装入器。如果父类装入器能够完成类装入任务,它将成功返回。如果加载了父类加载器,

  母公司委托模式优势类装入器母公司委托模式的优势:

  1.可以保证java核心库的类型安全:所有的Java应用至少会引用java.lang.Object类,即java.lang.Object类会在运行时加载到Java虚拟机中。如果这个加载过程是由Java自己的类加载器完成的,那么很可能JVM中会有多个版本的java.lang.Object类,并且这些类之间是不兼容的,互不可见的(起作用的是命名空间)。借助于父代理机制,Java核心类库中类的加载全部通过启动类加载器来完成,从而保证所有的Java应用程序都使用相同版本的Java核心类库,并且相互兼容。

  2.可以保证Java核心类库提供的类不会被自定义类替代。

  3.不同的类装入器可以为同名(二进制名)的类创建额外的名称空间。同名的类可以共存于Java虚拟机中,只需要不同的类加载器来加载。不同类装入器装入的类是不兼容的,相当于在Java虚拟机内部用一个又一个孤立的项目创建Java类空间。这种技术已经在很多框架中得到应用。

  4.采用父委托模式的好处是,Java类与其类加载器具有优先级的层次关系,可以避免类的重复加载。当父类已经加载了类,子类加载器就不需要再加载了。Bootstrap启动类加载器的角色:在运行时,Java类由类的完全限定名(二进制名)和用于加载类的定义加载器共同决定。如果具有相同名称(即相同的完全限定名)的类由两个不同的加载器加载,那么这些类是不同的,即使。类文件完全相同,而。

  在Oracle的Hotspot实现中,如果不正确地修改了系统属性sun.boot.class.path,它将无法正常运行。

  JVM中构建的引导类加载器将加载java.lang.ClassLoader和其他java平台类。当JVM启动时,将运行一个特殊的机器码,它将加载扩展类加载器和系统类加载器。这种特殊的机器码叫做Bootstrap,Bootstrap不是Java类,其他加载器都是Java类。引导类加载器是一个特定于平台的机器指令,负责启动整个加载。

  所有的类装入器(除了启动类装入器)都是作为Java类实现的,但是总要有一个组件来装入第一个Java类装入器,这样整个装入过程才能顺利进行。加载第一个纯Java类加载器是启动类加载器的责任。

  类加载器还会负责加载JRE正常运行所需的基本组件,也就是rt包下的类,包括java.util和java.lang包。下面的代码可以看到各种加载器的加载路径:

  public class mytest 22 { public static void main(String[]args){ system . out . println(system . getproperty( sun . boot . class . path ));system . out . println(system . getproperty( Java . ext . dirs ));system . out . println(system . getproperty( Java . class . path ));system . out . println(class loader . class . get class loader());system . out . println(launcher . class . get class loader());}}让我们从代码层面了解一下Java中定义的几种类加载器以及它们的父委托模式的实现。它们的类图如下

  从图中可以看出,最顶层的ClassLoader是ClassLoader类,这是一个抽象类。之后,所有的类装入器都继承自类装入器(不包括启动类装入器)。这里,我们主要介绍类加载器中的几个重要方法。

  loadClass(字符串名称,布尔解析)

  此方法加载指定名称(包括包名)的二进制类型。JDK1.2以后,不再建议用户重写这个方法,但是用户可以直接调用这个方法。loadClass()方法是由ClassLoader类本身实现的,这个方法中的逻辑就是父委托模式的实现。它的源代码如下。Loadclass (string name,boolean resolve)是一个重载方法,resolve参数表示是否生成类对象并执行相关解析操作。

  受保护阶层?Class (string name,boolean resolve)抛出classnotfoundexception { synchronized(getclassloadinglock(name)){///先从缓存中查找类对象,找到就不用重新加载类了?c=findLoadedClass(name);if(c==null){ long t0=system . nano time();试试{ if (parent!=null) {//如果找不到,委托给父类加载器加载c=parent.loadClass(name,false);} else {//如果没有父类,则委托给引导加载器加载C=FindBootTrapClassNull(name);} } catch(ClassNotFoundException e){//ClassNotFoundException如果从非空父类加载器中//找不到类时抛出} if (c==null) { //如果仍然找不到,则调用findClass以便//找到该类。long t1=system . nano time();//如果都没有找到,通过自定义实现的findClass查找并加载C=find class(name);//这是定义类加载器;记录统计信息sun . misc . perf counter . getparentdelegationtime()。添加时间(t1-t0);sun . misc . perf counter . getfindclasstime()。addElapsedTimeFrom(t1);sun . misc . perf counter . getfindclass()。increment();}} if (resolve) {//加载时是否需要解析;resolve class(c);}返回c;}}如loadClass方法所示,当一个类加载请求到来时,首先从缓存中查找类对象,如果存在则直接返回。如果不存在,将由类装入器的父装入器装入。如果没有父装入器,它将由顶级启动类装入器装入。最后,如果还是找不到,就用findClass()方法加载(后面会详细介绍findClass())。从loadClass实现中我们也可以知道,如果我们不想重新定义加载类的规则,也没有复杂的逻辑,只是想在运行时加载自己指定的类,那么可以直接使用this.getclass()。get ClassLoader . loadClass(class name),这样我们就可以直接调用class loader的load class方法来获取类对象。

  findClass(字符串名称)

  在JDK1.2之前,加载自定义类时,总是继承ClassLoader类,重写loadClass方法,从而实现自定义类加载。但是,在JDK1.2之后,不再建议用户重写loadClass()方法,而是在findClass()方法中编写自定义的类加载逻辑。根据前面的分析,findClass()方法是在loadClass()方法中调用的。当父类加载器在loadClass()方法中加载失败时,它会调用自己的findClass()方法来完成类的加载,从而保证自定义类加载器也符合父类委托模式。需要注意的是,findClass()方法的具体代码逻辑并没有在ClassLoader类中实现,而是抛出了ClassNotFoundException异常。同时要知道findClass方法通常是和defineClass方法一起使用的(后面会分析)。ClassLoader类中findClass()方法的源代码如下:

  受保护阶层?find类(字符串名称)抛出classnotfoundexception {//直接抛出异常抛出newclassnotfoundexception(名称);}defineClass(byte[] b,int off,int len)

  defineClass()方法用于将字节流解析为JVM识别的类对象(该方法的逻辑已经在ClassLoader中实现)。通过这种方法,类对象不仅可以通过类文件实例化,还可以通过其他方式实例化,比如通过网络接收类的字节码。然后转换成byte字节流创建相应的类对象。defineClass()方法通常与findClass()方法一起使用。通常,当您定制类加载器时,您将直接覆盖类加载器的findClass()方法并编写加载规则。获取要加载的类的字节码后,转换成流,然后调用defineClass()方法生成该类的类对象。一个简单的例子如下:

  公共类MyClassLoader扩展Class loader { @ override protected Class?Class (string classname)抛出class not found exception {//获取类的字节数组byte[]data=this . loadclassdata(class name);if(data==null){ throw new ClassNotFoundException();}//使用defineClass生成类对象返回this.define class (classname,data,0,data . length);}}需要注意的是,如果直接调用defineClass()方法生成该类的类对象,则不解析该类的类对象(也可以理解为链接阶段,毕竟解析是链接的最后一步),其解析操作需要等待初始化阶段。

  resolveClass(类?c)

  用这种方法,可以同时创建和解析类的类对象。前面说过,链接阶段主要是验证字节码,为类变量分配内存和设置初始值,将字节码文件中的符号引用转换成直接引用。以上四个方法是ClassLoader类中的重要方法,也是我们可能经常使用的方法。参见SercureClassLoader扩展了ClassLoader,增加了几个与代码源使用相关的方法(验证代码源及其证书的位置)和权限定义类验证(主要指对类源的访问权限)。一般我们不直接处理这个类,更多的是和它的子类URLClassLoader有关。前面说过,ClassLoader是一个抽象类,很多方法都是空的,没有实现,比如findClass(),findResource()等等。URLClassLoader是一个实现类,它为这些方法提供了具体的实现,并添加了一些函数,如URLClassPath类,以帮助获取类字节代码流。在编写自定义类加载器时,如果没有太复杂的要求,可以直接继承URLClassLoader类,这样可以避免自己编写findClass()方法和获取字节码流的方式,使自定义类加载器更加简洁。下面是URLClassLoader的类图(IDEA生成的类图)。

  从类图结构中可以看出,URLClassLoader中有一个URLClassPath类,通过它可以找到要加载的字节码流,也就是说,URLClassPath类负责找到要加载的字节码,读入字节流,最后通过defineClass()方法创建该类的类对象。

  从URLClassLoader类的结构图可以看出,它的构造方法有一个参数URL[]是必须要传递的,这个参数的元素就是代表字节码文件的路径。换句话说,在创建URLClassLoader对象时,需要指定类加载器来查找该目录中的类文件。

  同时需要注意的是,URL[]也是URLClassPath类的必需参数。创建URLClassPath对象时,会根据传递的URL数组中的路径判断是文件还是jar包,然后根据不同的路径创建FileLoader或者JarLoader或者默认的Loader类来加载对应路径下的类文件。当JVM调用findClass()方法时,三个加载器中的一个将类文件的字节码流加载到内存中,最后用字节码流创建类的类对象。

  请记住,如果我们在定义ClassLoader时选择继承ClassLoader类而不是URLClassLoader,我们必须手动编写findclass()方法的加载逻辑和获取字节码流的逻辑。了解了URLClassLoader之后,再来看看剩下的两个类加载器,即扩展类加载器ExtClassLoader和系统类加载器AppClassLoader。这两个类继承自URLClassLoader,是sun.misc.Launcher的静态内部类Sun.misc.Launcher主要由系统用来启动主应用程序。ExtClassLoader和AppClassLoader都是由sun.misc.Launcher创建的,它们的主要类结构如下:

  它们之间的关系就像上面解释的那样。同时,我们发现ExtClassLoader并没有重写loadClass()方法,这足以说明它遵循的是双亲委托模式,而AppClassLoader虽然重载了loadCass()方法,但最终调用的是双亲类loadClass()方法,所以它仍然遵守双亲委托模式。重载方法的源代码如下:

  /** *覆盖loadClass方法,增加包权限检测函数*/public Class?loadClass(String var1,boolean var2)抛出ClassNotFoundException { int var 3=var 1 . lastindexof(46);if (var3!=-1){ security manager var 4=system . getsecuritymanager();if (var4!=null){ var 4 . checkpackageaccess(var 1 . substring(0,var 3));} } if(this . UCP . knowntonotexist(var 1)){ Class var 5=this . findloadedclass(var 1);if (var5!=null){ if(var 2){ this . resolve class(var 5);}返回var5} else { throw new ClassNotFoundException(var 1);}} else {//仍然调用父类returnsuper.loadclass (var1,var2)的方法;}}}其实ExtClassLoader和AppClassLoader都继承了URLClassLoader类,所以都遵循父母委托模型,这是毋庸置疑的。

  至此,我们对ClassLoader、URLClassLoader、ExtClassLoader、AppClassLoader、Launcher类之间的关系有了清晰的认识,也知道了一些主要的方法。这里没有详细分析这些类的源代码。毕竟不是必须的,因为我们主要是把类和常用方法的关系搞清楚了父母委托模式的实现过程,这就足够为编写自定义类加载器做铺垫了。关于父类装入器一直有很多争论,但是每个类装入器的父类是谁一直没有搞清楚。让我们通过代码验证来澄清这个答案。

  类加载器之间的关系我们进一步理解类加载器之间的关系(不是继承关系),可以分为以下四点:

  启动类加载器,由C实现,没有父类。扩展类加载器用Java语言实现,父类加载器是null系统类加载器,用Java语言实现。父类装入器是ExtClassLoader自定义类装入器,父类装入器必须是AppClassLoader。让我们通过程序来验证上述观点。

  公共类MyClassLoader扩展了类加载器{私有字符串类加载器名称;私有字符串路径;私有最终字符串file extension= . 1 类;public my class loader(String class loader name){ super();//将系统类加载器作为该类加载器的父加载器这个。类加载器名称=类加载器名称;} public my class loader(class loader parent,String class loader name){ super(parent);//显示指定该类加载器的父加载器这个。类加载器名称=类加载器名称;} public String getPath(){ return path;} public void set path(String path){ this。path=路径;} @将公共字符串重写为String(){ return mytest 15 { class loader name= class loader name \ } ;} @覆盖保护类?findClass(字符串类名)抛出ClassNotFoundException { system。出去。println( find class invoked: class name );System.out.println(类加载器名称:这个。类加载器名称’);byte[]data=this。加载类数据(类名);if(data==null){ throw new ClassNotFoundException();}还这个。定义类(类名,数据,0,数据。长度);} private byte[]loadClassData(字符串类名){ InputStream InputStream=nullbyte[]data=null;ByteArrayOutputStream bos=null请尝试{ this.path=path.replace(。,/);这个。类别载入器名称=类别载入器名称。替换(。,/);inputStream=新文件inputStream(新文件(这个。路径类名this。类加载器名称));Bos=new ByteArrayOutputStream();int chwhile((ch=inputstream . read())!=-1){ Bos。写(ch);}数据=Bos。tobytearray();} catch(Exception e){ e . printstacktrace();}最后{ if(inputStream!=null){ try { inputstream。close();} catch(io异常e){ e . printstacktrace();} } if(bos!=null){ try { Bos。close();} catch(io异常e){ e . printstacktrace();} } }返回数据;}公共静态void main(String[]args)引发异常{ my class loader loader 1=new my class loader( loader 1 );System.out.println(自定义类加载器的父加载器:加载程序1。get parent());System.out.println(系统默认的应用程序类加载器:“类加载器。getsystemclass loader());系统。出去。println(应用程序类加载器的父类加载器:类加载器。getsystemclassloader().get parent());系统。出去。println(外部类加载器的父类加载器:类加载器。getsystemclassloader().getParent().get parent());系统。出去。println(-);MyClassLoader myClassLoader1=新的我的类加载器(“加载器1”);myclass loader 1。设置路径( E:/grad project/JVM _ study/build/classes/Java/main/);班级?clazz 1=我的类装入器1。加载类( com。一博。JVM。类别载入器。我的测试1’);系统。出去。println( claz 1: claz 1 );对象object 1=clazz 1。新实例();系统。出去。println(对象1);系统。出去。println(我的类装入器1。getclass().get class loader());系统。出去。println(-);MyClassLoader myClassLoader2=新的MyClassLoader(myClassLoader1, loader 12 );我的类装入器2。设置路径( E:/grad project/JVM _ study/build/classes/Java/main/);班级?clazz 2=我的类装入器2。加载类( com。一博。JVM。类别载入器。我的测试1’);系统。出去。println( clazz 2: clazz 2 );对象对象2=clazz 2。新实例();系统。出去。println(对象2);系统。出去。println(-);MyClassLoader myClassLoader3=新的我的类加载器(“加载器12”);myclassloader 3。设置路径( E:/grad project/JVM _ study/build/classes/Java/main/);班级?clazz 3=myclassloader 3。加载类( com。一博。JVM。类别载入器。我的测试1’);系统。出去。println( clazz 3: clazz 3 );对象对象3=clazz 3。新实例();系统。出去。println(对象3);}}代码中,我们自定义了一个MyClassLoader,这里我们继承了类加载器而非URLClassLoader,因此需要自己编写findClass()方法逻辑以及加载字节码的逻辑,接着在主要的方法中,通过类别载入器。getsystemclassloader()获取到系统默认类加载器,通过获取其父类加载器及其父父类加载器,同时还获取了自定义类加载器的父类加载器,最终输出结果正如我们所预料的,AppClassLoader的父类加载器为ExtClassLoader,而类加载器没有父类加载器。

  如果我们实现自己的类加载器,它的父加载器将只是AppClassLoader。在这里我们不妨看一下Lancher的构造函数源代码:

  公共启动器(){ Launcher .ExtClassLoader var1尝试{ //首先创建扩展类加载器var1=启动器ext类加载器。getext类加载器();} catch(io异常变量10){抛出新的内部错误(无法创建扩展类加载器,var 10);}试试{ //再创建载器并把扩展类加载器作为父加载器传递给app class loader this。装载器=发射器.appclass加载器。getappclassloader(var 1);} catch(io异常变量9){抛出新的内部错误(无法创建应用程序类加载器,var 9);} //设置线程上下文类加载器,稍后分析Thread.currentThread().setContextClassLoader(this。装载机);字符串var 2=系统。getproperty( Java。安全。经理’);if (var2!=null){安全管理器变量3=null如果(!.equals(var2)!"默认"。equals(var 2)){ try { var 3=(安全管理器)this。装载机。负载等级(var 2).新实例();} catch(IllegalAccessException var 5){ } catch(实例化异常var 6){ } catch(ClassNotFoundException var 7){ } } catch(ClassCastException var 8){ } } else { var 3=新的安全管理器();} if(var 3==null){抛出新的内部错误(无法创建安全管理器:“var 2”);}系统。setsecuritymanager(var 3);}}显然兰谢尔初始化时首先会创建类加载器类加载器,然后再创建载器并把类加载器传递给它作为父类加载器,这里还把载器默认设置为线程上下文类加载器,关于线程上下文类加载器稍后会分析。那类加载器类加载器为什么是空呢?看下面的源码创建过程就明白,在创建类加载器强制设置了其父加载器为零。

  //首先创建扩展类加载器var1=启动器ext类加载器。getext类加载器();静态类类加载器扩展了URL类加载器{私有静态易失性启动器.类加载器实例;公共静态启动器ext类加载器getExtClassLoader()抛出io异常{ if(instance==null){ Class var 0=Launcher .外部类加载器。类;同步(启动器ext类加载器。class){ if(instance==null){ instance=createExtClassLoader();} } }返回实例;}公共ExtClassLoader(File[] var1)抛出IOException { //调用父类构造载器传递空作为父super(getExtURLs(var1),(ClassLoader)null,launcher。工厂);SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);} } public URL class loader(URL[]URLs,ClassLoader parent,URLStreamHandlerFactory factory factory factory){ super(parent);//这是为了让堆栈深度与1.1安全管理器安全=系统。getsecuritymanager()保持一致;如果(安全!=null){ security。checkcreateclass loader();} ACC=访问控制器。获取上下文();UCP=新的URL类路径(URL,factory,ACC);}显然类加载器的父类为空,而载器的父加载器为ExtClassLoader,所有自定义的类加载器其父加载器只会是AppClassLoader,注意这里所指的父类并不是爪哇继承关系中的那种父子关系。

  类与类加载器在虚拟机(Java虚拟机的缩写)中表示两个班级对象是否为同一个类对象存在两个必要条件

  类的完全限定名必须一致,包括包名。该类的ClassLoader (ClassLoader实例对象)必须相同。也就是说,在JVM中,即使这两个类对象(Class object)来源于同一个类文件,由同一个虚拟机加载,但只要加载它们的ClassLoader实例对象不同,这两个类对象就不相等,因为不同的ClassLoader实例对象有不同的独立类命名空间。所以加载的类对象也会存在于不同的类名空间,但前提是要覆盖loadClass方法。从之前父母委托模式下loadclass()方法的源代码分析可知,方法的第一步会通过类?c=findLoadedClass(name);从缓存中,如果类名相同,则不会再次加载,所以我们必须绕过缓存查询来重新加载类对象。当然,也可以直接调用findClass()方法,避免从缓存中搜索。如果没有从缓存中查询到同一个全类名的类对象,那么只有类加载器的实例对象不一样,同一个字节码文件创建的类对象自然不会一样。

  理解类文件的显式加载和隐式加载的概念。所谓类文件的显式加载和隐式加载,是指JVM将类文件加载到内存中的方式。显式加载是指通过在代码中调用ClassLoader来加载类对象,比如直接使用Class.forName(name)或者this.getclass()。getclassloader()。loadclass()来加载类对象。

  隐式加载是指不需要在代码中直接调用ClassLoader的方法,而是通过虚拟机自动将类对象加载到内存中。比如加载一个类的类文件时,在该类的类文件中引用另一个类的对象,额外引用的类会通过JVM自动加载到内存中。在日常开发中,上述两种方法一般是混合使用的,这里我们知道是这种情况。

  编写自己的类加载器从前面的分析可以看出,要实现自定义的类加载器,需要继承ClassLoader或URLClassLoader。继承classloader需要重写findClass()方法,并编写加载逻辑。继承URLClassLoader可以省去编写findClass()方法和从类文件加载转换成字节码流的代码。那么编写自定义类加载器有什么意义呢?

  当班级文件不在类路径路径下,默认系统类加载器无法找到该班级文件,在这种情况下我们需要实现一个自定义的类加载器来加载特定路径下的班级文件生成班级对象。当一个班级文件是通过网络传输并且可能会进行相应的加密操作时,需要先对班级文件进行相应的解密后再加载到虚拟机(Java虚拟机的缩写)内存中,这种情况下也需要编写自定义的类加载器并实现相应的逻辑。当需要实现热部署功能时(一个班级文件通过不同的类加载器产生不同班级对象从而实现热部署功能),需要实现自定义类加载器的逻辑。继承类加载器实现自定义类加载器公共类MyClassLoader扩展了类加载器{私有字符串类加载器名称;私有字符串路径;私有最终字符串file extension= . 1 类;public my class loader(String class loader name){ super();//将系统类加载器作为该类加载器的父加载器这个。类加载器名称=类加载器名称;} public my class loader(class loader parent,String class loader name){ super(parent);//显示指定该类加载器的父加载器这个。类加载器名称=类加载器名称;} public String getPath(){ return path;} public void set path(String path){ this。path=路径;} @将公共字符串重写为String(){ return mytest 15 { class loader name= class loader name \ } ;} @覆盖保护类?findClass(字符串类名)抛出ClassNotFoundException { system。出去。println( find class invoked: class name );System.out.println(类加载器名称:这个。类加载器名称’);byte[]data=this。加载类数据(类名);if(data==null){ throw new ClassNotFoundException();}还这个。定义类(类名,数据,0,数据。长度);} private byte[]loadClassData(字符串类名){ InputStream InputStream=nullbyte[]data=null;ByteArrayOutputStream bos=null请尝试{ this.path=path.replace(。,/);这个。类别载入器名称=类别载入器名称。替换(。,/);inputStream=新文件inputStream(新文件(这个。路径类名this。类加载器名称));Bos=new ByteArrayOutputStream();int chwhile((ch=inputstream . read())!=-1){ Bos。写(ch);}数据=Bos。tobytearray();} catch(Exception e){ e . printstacktrace();}最后{ if(inputStream!=null){ try { inputstream。close();} catch(io异常e){ e . printstacktrace();} } if(bos!=null){ try { Bos。close();} catch(io异常e){ e . printstacktrace();} } }返回数据;}公共静态void main(String[]args)引发异常{我的类加载器我的类加载器1=新建我的类加载器(加载器1 );myclass loader 1。设置路径( E:/grad project/JVM _ study/build/classes/Java/main/);班级?clazz 1=我的类装入器1。加载类( com。一博。JVM。类别载入器。我的测试1’);系统。出去。println( claz 1: claz 1 );对象object 1=clazz 1。新实例();系统。出去。println(对象1);系统。出去。println(我的类装入器1。getclass().get class loader());系统。出去。println(-);MyClassLoader myClassLoader2=新的MyClassLoader(myClassLoader1, loader 12 );我的类装入器2。设置路径( E:/grad project/JVM _ study/build/classes/Java/main/);班级?clazz 2=我的类装入器2。加载类( com。一博。JVM。类别载入器。我的测试1’);系统。出去。println( clazz 2: clazz 2 );对象对象2=clazz 2。新实例();系统。出去。println(对象2);系统。出去。println(-);MyClassLoader myClassLoader3=新的我的类加载器(“加载器12”);myClassLoa

  der3.setPath("E:/gradleproject/jvm_study/build/classes/java/main/"); Class ? clazz3 = myClassLoader3.loadClass("com.yibo.jvm.classloader.MyTest1"); System.out.println("clazz3:" + clazz3); Object object3 = clazz3.newInstance(); System.out.println(object3); }}显然我们通过loadClassData()方法找到class文件并转换为字节流,并重写findClass()方法,利用defineClass()方法创建了类的class对象。在main方法中调用了setPath()方法加载指定路径下的class文件,由于启动类加载器、拓展类加载器以及系统类加载器都无法在其路径下找到该类,因此最终将有自定义类加载器加载,即调用findClass()方法进行加载。如果继承URLClassLoader实现,那代码就更简洁了,如下:

  继承URLClassLoader现自定义类加载器public class MyURLClassLoader extends URLClassLoader { public MyURLClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); } public MyURLClassLoader(URL[] urls) { super(urls); } public MyURLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) { super(urls, parent, factory); } public static void main(String[] args) throws MalformedURLException { String rootDir="E:/gradleproject/jvm_study/build/classes/java/main/"; //创建自定义文件类加载器 File file = new File(rootDir); //File to URI URI uri=file.toURI(); URL[] urls = {uri.toURL()}; MyURLClassLoader loader = new MyURLClassLoader(urls); try { //加载指定的class文件 Class ? object =loader.loadClass("com.yibo.jvm.classloader.MyTest1"); System.out.println(object.newInstance().toString()); } catch (Exception e) { e.printStackTrace(); } }}非常简洁除了需要重写构造器外无需编写findClass()方法及其class文件的字节流转换逻辑。

  自定义网络类加载器自定义网络类加载器,主要用于读取通过网络传递的class文件(在这里我们省略class文件的解密过程),并将其转换成字节流生成对应的class对象,如下

  public class NetClassLoader extends ClassLoader { private String url;//class文件的URL public NetClassLoader(String url) { this.url = url; } @Override protected Class ? findClass(String className) throws ClassNotFoundException { byte[] data = this.loadClassData(className); if(data == null){ throw new ClassNotFoundException(); } return this.defineClass(className,data,0,data.length); } private byte[] loadClassData(String className){ String path = classNameToPath(className); InputStream ins = null; byte[] data = null; ByteArrayOutputStream baos = null; try { URL url = new URL(path); ins = url.openStream(); baos = new ByteArrayOutputStream(); int bufferSize = 8192; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; // 读取类文件的字节 while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } //这里省略解密的过程....... data = baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); }finally { if(ins != null){ try { ins.close(); } catch (IOException e) { e.printStackTrace(); } } if(baos != null){ try { baos.close(); } catch (IOException e) { e.printStackTrace(); } } } return data; } private String classNameToPath(String className) { // 得到类文件的URL return url + "/" + className.replace(., /) + ".class"; }}主要是在获取字节码流时的区别,从网络直接获取到字节流再转车字节数组然后利用defineClass方法创建class对象,如果继承URLClassLoader类则和前面文件路径的实现是类似的,无需担心路径是filePath还是Url,因为URLClassLoader内的URLClassPath对象会根据传递过来的URL数组中的路径判断是文件还是jar包,然后根据不同的路径创建FileLoader或者JarLoader或默认类Loader去读取对于的路径或者url下的class文件。

  热部署类加载器所谓的热部署就是利用同一个class文件不同的类加载器在内存创建出两个不同的class对象(关于这点的原因前面已分析过,即利用不同的类加载实例),由于JVM在加载类之前会检测请求的类是否已加载过(即在loadClass()方法中调用findLoadedClass()方法),如果被加载过,则直接从缓存获取,不会重新加载。注意同一个类加载器的实例和同一个class文件只能被加载器一次,多次加载将报错,因此我们实现的热部署必须让同一个class文件可以根据不同的类加载器重复加载,以实现所谓的热部署。实际上前面的实现的MyClassLoader和MyURLClassLoader已具备这个功能,但前提是直接调用findClass()方法,而不是调用loadClass()方法,因为ClassLoader中loadClass()方法体中调用findLoadedClass()方法进行了检测是否已被加载,因此我们直接调用findClass()方法就可以绕过这个问题,当然也可以重新loadClass方法,但强烈不建议这么干。

  双亲委派模型的破坏者-线程上下文类加载器当前类加载器(current ClassLoader)每个类都会使用自己的类加载器(即加载自身的类加载器)来加载其他类(值的是所依赖的类)如果ClassX引用了ClassY,那么ClassX的类加载器就会去加载ClassY(前提是ClassY尚未被加载)线程上下文类加载器(Context ClassLoader)线程上下文类加载器(Context ClassLoader)是从JDK1.2开始引入的,类Thread中的getContextClassLoader()与setContextClassLoader(ClassLoader cl)分别用来获取和设置线程上下文类加载器如果没有通过setContextClassLoader(ClassLoader cl)进行设置的话,线程将继承其父线程的上下文类加载器Java应用运行时的初始线程的上下文类加载器是系统类加载器,在线程中运行的代码可以通过该类加载器来加载类与资源线程上下文类加载器的重要性:在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类父ClassLoader可以使用当前线程Thread.currentThread().getContextClassLoader所指定的ClassLoader加载的类,这就改变了父ClassLoader不能使用子ClassLoader或是其他没有直接父子关系的ClassLoader加载的类的情况,即改变了双亲委托模型线程上下文类加载器就是当前线程的Current ClassLoader,在双亲委托模型下,类加载是由下至上的,即下层的类加载器会委托上层进行加载。但是对于SPI来说,有些接口是Java核心库所提供的,而Java核心库是由启动类加载器所加载的,而这些接口的实现确来自于不同的jar包(厂商提供),Java的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求,而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口的实现类的加载。如下图所示,以jdbc.jar加载为例:

  从图可知rt.jar核心包是由Bootstrap类加载器加载的,其内包含SPI核心接口类,由于SPI中的类经常需要调用外部实现类的方法,而jdbc.jar包含外部实现类(jdbc.jar存在于classpath路径)无法通过Bootstrap类加载器加载,因此只能委派线程上下文类加载器把jdbc.jar中的实现类加载到内存以便SPI相关类使用。显然这种线程上下文类加载器的加载方式破坏了“双亲委派模型”,它在执行过程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器,当然这也使得Java类加载器变得更加灵活。为了进一步证实这种场景,不妨看看DriverManager类的源码,DriverManager是Java核心rt.jar包中的类,该类用来管理不同数据库的实现驱动即Driver,它们都实现了Java核心包中的java.sql.Driver接口,如mysql驱动包中的com.mysql.jdbc.Driver,这里主要看看如何加载外部实现类,在DriverManager初始化时会执行如下代码

  //DriverManager是Java核心包rt.jar的类public class DriverManager { //省略不必要的代码...... static { loadInitialDrivers();//执行该方法 println("JDBC DriverManager initialized"); } private static void loadInitialDrivers() { //省略不必要的代码...... AccessController.doPrivileged(new PrivilegedAction Void () { public Void run() { //加载外部的Driver的实现类 ServiceLoader Driver loadedDrivers = ServiceLoader.load(Driver.class); Iterator Driver driversIterator = loadedDrivers.iterator(); try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); //省略不必要的代码...... }}在DriverManager类初始化时执行了loadInitialDrivers()方法,在该方法中通过ServiceLoader.load(Driver.class);去加载外部实现的驱动类,ServiceLoader类会去读取mysql的jdbc.jar下META-INF文件的内容,如下所示:

  而com.mysql.jdbc.Driver继承类如下:

  /** * Backwards compatibility to support apps that call code Class.forName("com.mysql.jdbc.Driver"); /code . */public class Driver extends com.mysql.cj.jdbc.Driver { public Driver() throws SQLException { super(); } static { System.err.println("Loading class `com.mysql.jdbc.Driver. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver. " + "The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary."); }}从注释可以看出平常我们使用com.mysql.jdbc.Driver已被丢弃了,取而代之的是com.mysql.cj.jdbc.Driver,也就是说官方不再建议我们使用如下代码注册mysql驱动

  //不建议使用该方式注册驱动类Class.forName("com.mysql.jdbc.Driver");String url = "jdbc:mysql://localhost:3306/cm-storylocker?characterEncoding=UTF-8";// 通过java库获取数据库连接Connection conn = java.sql.DriverManager.getConnection(url, "root", "root@555");而是直接去掉注册步骤,如下即可:

  String url = "jdbc:mysql://localhost:3306/cm-storylocker?characterEncoding=UTF-8";// 通过java库获取数据库连接Connection conn = java.sql.DriverManager.getConnection(url, "root", "root@555");这样ServiceLoader会帮助我们处理一切,并最终通过load()方法加载,看看load()方法实现

  public static S ServiceLoader S load(Class S service) { //通过线程上下文类加载器加载 ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl);}很明显了确实通过线程上下文类加载器加载的,实际上核心包的SPI类对外部实现类的加载都是基于线程上下文类加载器执行的,通过这种方式实现了Java核心代码内部去调用外部实现类。我们知道线程上下文类加载器默认情况下就是AppClassLoader,那为什么不直接通过getSystemClassLoader()获取类加载器来加载classpath路径下的类的呢?其实是可行的,但这种直接使用getSystemClassLoader()方法获取AppClassLoader加载类有一个缺点,那就是代码部署到不同服务时会出现问题,如把代码部署到Java Web应用服务或者EJB之类的服务将会出问题,因为这些服务使用的线程上下文类加载器并非AppClassLoader,而是Java Web应用服自家的类加载器,类加载器不同。,所以我们应用该少用getSystemClassLoader()。总之不同的服务使用的可能默认ClassLoader是不同的,但使用线程上下文类加载器总能获取到与当前程序执行相同的ClassLoader,从而避免不必要的问题。ok~.关于线程上下文类加载器暂且聊到这,前面阐述的DriverManager类,大家可以自行看看源码,相信会有更多的体会,另外关于ServiceLoader本篇并没有过多的阐述,毕竟我们主题是类加载器,但ServiceLoader是个很不错的解耦机制,大家可以自行查阅其相关用法。

  线程上下文类加载器的一般使用模式(获取 - 使用 - 还原)ClassLoader classLoader = Thread.currentThread().getContextClassLoader();try{ Thread.currentThread().setContextClassLoader(targetTClassLoader); myMethod();}finally{ Thread.currentThread().setContextClassLoader(classLoader);}myMethod()里面调用了Thread.currentThread().getContextClassLoader()获取当前线程的上下文类加载器做某些事情如果一个类由类加载器A加载,那么这个类的依赖类也是由相同的类加载器所加载的(前提是该类没有被加载)ContextClassLoader的作用就是为了破坏Java类加载的双亲委托机制当高层提供了统一的接口让低层去实现,同时又要在高层加载(或实例化)低层的类时,就必须要通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类线程上下文类加载器的适用场景:当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。

  当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。参考:

  https://www.cnblogs.com/mybatis/p/9396135.html

  https://blog.csdn.net/yangcheng33/article/details/52631940

   ©

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: