Nivelle 开拓视野冲破艰险看见世界 身临其境贴近彼此感受生活

类加载机制

2017-09-23

定义

一个Java程序想要运行起来,首先要经过编译生成.class文件,然后创建一个运行环境(Jvm)来加载字节码文件到内存运行,而.class文件时怎样呗加载进jvm中的就是java Classloader所做的事情.

比如执行new 操作时,当执行Class.forName(“包路径+类名”),Class.forName(“包路径+类名”,classLoader),classloader.loadclass(“包路径+类名”)时就触发了类加载器加载对应的路径去查找对应的路径去查找 .class 并创建Class对象.

ClassLoader体系结构:

image

java自带的ClassLoader

BootstrapClassloader

引导类加载器,又称启动类加载器,是最顶层的类加载器,主要用来加载java核心类,如rt.jar,resource.jar,charsets.jar等,Sun的JVM中,执行java的命令中是由-Xbootclasspath 选项或使用-D选项指定sun.boot.class.path系统属性值可以指定附加的类,它不是java.lang.ClassLoader的子类,而是由JVM自身实现的该类C语言实现,java程序访问不到该加载器,同过下面代码可以查看该加载器加载了哪些jar包.

public void test() {  
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();    
        for (int i = 0; i < urls.length; i++) {    
            System.out.println(urls[i].toExternalForm());    
        }   
    }

执行结果:

file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/resources.jar

file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/rt.jar

file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/sunrsasign.jar

file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/jsse.jar

file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/jce.jar

file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/charsets.jar

file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/jfr.jar

file:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/classes,


ExtClassloader

拓展类加载器,主要负责加载Java的拓展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar包或者由java.ext.dirs系统属性指定的jar包.放入这个目录下的jar包对所有AppClassLoader都是可见的.那么ext都是在那些地方加载类的:

System.out.println(System.getProperty("java.ext.dirs"));

/Users/zhuizhumengxiang/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java

AppClassloader

系统类加载器,又称引用加载器,本文说的SystemClassloader和APPClassloader是一个东西,它负责在JVM启动时,加载来自在命令java中的-classpath或者java.class.path系统属性或者CLASSATH操作系统属性所指定的jar类包和类路径.调用ClassLoader.getSystemClassLoader()可以获得该类加载器.如果没有特别指定,则在用户自定义的任何类加载器都将该类加载器作为它的父加载器,这点通过ClassLoader的无参构造函数可以知道如下:

protected ClassLoader() {       

            this(checkCreateClassLoader(), getSystemClassLoader());
    }

执行以下代码即可获得classpath加载路径:


System.out.println(System.getProperty("java.class.
path"));

三种类加载器的关系

image

用户定义的无参加载器的父类加载器默认是AppClassloader加载器,而AppClassLoader加载器的父加载器是ExtClassloader,通过下面代码可以验证:

ClassLoader.getSystemClassLoader().getParent()

一般我们都认为ExtClassloader的父类加载器是BootStrapClassloader,但是其实他们之间根本没有父子关系的,只是在ExtClassloader找不到加载类时回去委托BootStrao加载器区加载.通过如下代码可知父加载器为null:

ClassLoader.getSystemClassLoader().getParent().getParent()

类加载器原理

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

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


protected Class<?> loadClass(Stringname,boolean resolve)  
       throws ClassNotFoundException  
   {  
       synchronized (getClassLoadingLock(name)) {  
           // 首先从jvm缓存查找该类
           Class c = findLoadedClass(name);  (1)           if (c ==null) {  
               longt0 = System.nanoTime();  
               try {  //然后委托给父类加载器进行加载
                   if (parent !=null) {  
                       c = parent.loadClass(name,false);  (2)
                   } else {  //如果父类加载器为null,则委托给BootStrap加载器加载
                       c = findBootstrapClassOrNull(name);  (3)
                   }  
               } catch (ClassNotFoundExceptione) {  
                   // ClassNotFoundException thrown if class not found  
                   // from the non-null parent class loader  
               }  

               if (c ==null) {  
                   // 若仍然没有找到则调用findclass查找
                   // to find the class.  
                   longt1 = System.nanoTime();  
                   c = findClass(name);  (4)                   // this is the defining class loader; record the stats  
                   sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 -t0);  
                   sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);  
                   sun.misc.PerfCounter.getFindClasses().increment();  
               }  
           }  
           if (resolve) {  
               resolveClass(c);  
           }  
           returnc;  
       }  
   }



分析代码知道首先会执行(1)从jvm缓存查找该类,如何该类之前被加载过,则直接从jvm缓存返回该类,否者看当前类加载器是否有父加载器,如果有的话则委托为父类加载器进行加载(2),否者调用(3)委托为BootStrapClassloader进行加载,如果还是没有找到,则调用当前Classloader的findclass方法进行查找。 从上面源码知道要想修改类加载委托机制,实现自己的载入策略 可以通过覆盖ClassLoader的findClass方法或者覆盖loadClass方法来实现。

Java中如何构造三种类加载器的结构

下面从源码来分析下JVM是如何构建内置classloader的,具体是rt.jar包里面sun.misc.Launcher类:


public Launcher()  
      {  
        ExtClassLoader localExtClassLoader;  
        try  
        {  //首先创建了ExtClassLoader
          localExtClassLoader = ExtClassLoader.getExtClassLoader();  
        }  
        catch (IOException localIOException1)  
        {  
          throw new InternalError("Could not create extension class loader");  
        }  
        try  
        {  //然后以ExtClassloader作为父加载器创建了AppClassLoader
          this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader);  
        }  
        catch (IOException localIOException2)  
        {  
          throw new InternalError("Could not create application class loader");  
        }  //这个是个特殊的加载器后面会讲到,这里只需要知道默认下线程上下文加载器为appclassloader
        Thread.currentThread().setContextClassLoader(this.loader);  

        ................
      }



  • ExtClassLoader.getExtClassLoader():

public Launcher()  
      {  
        ExtClassLoader localExtClassLoader;  
        try  
        {  //首先创建了ExtClassLoader
          localExtClassLoader = ExtClassLoader.getExtClassLoader();  
        }  
        catch (IOException localIOException1)  
        {  
          throw new InternalError("Could not create extension class loader");  
        }  
        try  
        {  //然后以ExtClassloader作为父加载器创建了AppClassLoader
          this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader);  
        }  
        catch (IOException localIOException2)  
        {  
          throw new InternalError("Could not create application class loader");  
        }  //这个是个特殊的加载器后面会讲到,这里只需要知道默认下线程上下文加载器为appclassloader
        Thread.currentThread().setContextClassLoader(this.loader);  

        ................
      }


  • 下面看下ExtClassLoader.getExtClassLoader()的代码

public static ExtClassLoader getExtClassLoader()  
      throws IOException  
    {  //可以知道ExtClassLoader类加载路径为java.ext.dirs
      File[] arrayOfFile = getExtDirs();  
      try  
      {  
        (ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction()  
        {  
          public Launcher.ExtClassLoader run()  
            throws IOException  
          {  
            int i = this.val$dirs.length;  
            for (int j = 0; j < i; j++) {  
              MetaIndex.registerDirectory(this.val$dirs[j]);  
            }  
            return new Launcher.ExtClassLoader(this.val$dirs);  
          }  
        });  
      }  
      catch (PrivilegedActionException localPrivilegedActionException)  
      {  
        throw ((IOException)localPrivilegedActionException.getException());  
      }  
    }  


    private static File[] getExtDirs()  
    {  
      String str = System.getProperty("java.ext.dirs");  
      File[] arrayOfFile;  
      if (str != null)  
      {  
        StringTokenizer localStringTokenizer = new StringTokenizer(str, File.pathSeparator);  

        int i = localStringTokenizer.countTokens();  
        arrayOfFile = new File[i];  
        for (int j = 0; j < i; j++) {  
          arrayOfFile[j] = new File(localStringTokenizer.nextToken());  
        }  
      }  
      else  
      {  
        arrayOfFile = new File[0];  
      }  
      return arrayOfFile;  
    }




  • AppClassLoader.getAppClassLoader


public static ClassLoader getAppClassLoader(final ClassLoader paramClassLoader)  
      throws IOException  
    {  //可知AppClassLoader类加载路径为java.class.path
      String str = System.getProperty("java.class.path");  
      final File[] arrayOfFile = str == null ? new File[0] : Launcher.getClassPath(str);  

      (ClassLoader)AccessController.doPrivileged(new PrivilegedAction()  
      {  
        public Launcher.AppClassLoader run()  
        {  
          URL[] arrayOfURL = this.val$s == null ? new URL[0] : Launcher.pathToURLs(arrayOfFile);  

          return new Launcher.AppClassLoader(arrayOfURL, paramClassLoader);  
        }  
      });  
    }



总结下Java应用启动过程是首先BootstarpClassloader加载rt.jar包里面的sun.misc.Launcher类,而该类内部使用BootstarpClassloader加载器构建和初始化Java中三种类加载和线程上下文类加载器,然后在根据不同场景去使用这些类加载器去自己的类查找路径去加载类。

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,提示这是两个不同的类型。现在通过实例来验证上述所描述的是否正确:

验证ClassLoader加载类的原理:

ClassLoader loader = ClassLoaderTest.class.getClassLoader();    //获得加载ClassLoaderTest.class这个类的类加载器  
while(loader != null) {  
    System.out.println(loader);  
    loader = loader.getParent();    //获得父类加载器的引用  
}  
System.out.println(loader); 

image

第一行结果说明:ClassLoaderTest的类加载器是AppClassLoader。

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

第三行结果说明:ExtClassLoader的类加器是Bootstrap ClassLoader,因为Bootstrap ClassLoader不是一个普通的Java类,所以ExtClassLoader的parent=null,所以第三行的打印结果为null就是这个原因

Tomcat ClassLoader

Tomcat的源码Bootstrap类的initClassLoaders方法:

private void initClassLoaders() {       
   try {           
       // 创建commonLoader,父类为APPClassLoader
        commonLoader = createClassLoader("common", this.getClass().getClassLoader());         
           if( commonLoader == null ) {
            commonLoader=this.getClass().getClassLoader();
        }           
        //以commonLoader为父加载器创建catalinaLoader和sharedLoader加载器
        catalinaLoader = createClassLoader("server", commonLoader);
        sharedLoader = createClassLoader("shared", commonLoader);
    } catch (Throwable t) {
        handleThrowable(t);
        log.error("Class loader creation threw exception", t);
        System.exit(1);
    }
}

现在我们已经知道这三个加载器关系了,下面看下创建加载器的代码:


private ClassLoader createClassLoader(String name, ClassLoader parent)
        throws Exception {        
        //获取catalina.properties中配置,分别为:common.loader,server.loader,shared.loader
        String value = CatalinaProperties.getProperty(name + ".loader");       
        //根据配置知道server.loader,shared.loader为空,所以commonLoader=serverLoader=sharedLoader
        if ((value == null) || (value.equals("")))            
        return parent;
        value = replace(value);
        List<Repository> repositories = new ArrayList<Repository>();
        StringTokenizer tokenizer = new StringTokenizer(value, ",");       
         while (tokenizer.hasMoreElements()) {
            String repository = tokenizer.nextToken().trim();            
            if (repository.length() == 0) {               
              continue;
            }          
            // Check for a JAR URL repository
            try {               
               @SuppressWarnings("unused")
                URL url = new URL(repository);
                repositories.add(new Repository(repository, RepositoryType.URL));
                continue;
            } catch (MalformedURLException e) {              
                // Ignore
            }            
            // Local repository
            if (repository.endsWith("*.jar")) {
                repository = repository.substring(0, repository.length() - "*.jar".length());
                repositories.add( new Repository(repository, RepositoryType.GLOB));
            } else if (repository.endsWith(".jar")) {
                repositories.add( new Repository(repository, RepositoryType.JAR));
            } else {
                repositories.add(new Repository(repository, RepositoryType.DIR));
            }
        }       //这里其实是URLClassLoader,只是urls会不同
        return ClassLoaderFactory.createClassLoader(repositories, parent);
    }



这里根据配置文件catalina.properties里面配置的仓库地址作为类加载器查找类路径,配置如下: common.loader=${catalina.base}/lib,${catalina.base}/lib/.jar,${catalina.home}/lib,${catalina.home}/lib/.jar

server.loader= shared.loader= 根据上面代码可知commonLoader,serverLoader,sharedLoader是同一个classloader。

tomcat的容器构造:

image

其中Engine是最大的容器默认为StandardEngine,其中可以有若干个host默认为StandHost,host的父容器为Engine,每个Host容器里面有若干context容器默认为StandardContext,Context容器的父容器为host.

从catalina.java的createStartDigester函数:

digester.addRule("Server/Service/Engine",  new SetParentClassLoaderRule(parentClassLoader));

知道StandardEngine中的parentClassLoader被设置为SharedLoader.

然后我们看下StandardContext中的startInternal方法:

f (getLoader() == null) {
           WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
           webappLoader.setDelegate(getDelegate());           //设置到类对象
           setLoader(webappLoader);
       }

其中getParentClassLoader方法用来获取父类加载器这里为sharedLoader,具体原因看如下代码:


public ClassLoader getParentClassLoader() {        
   if (parentClassLoader != null)           
         return (parentClassLoader);       
         if (getPrivileged()) {            
         return this.getClass().getClassLoader();
    } else if (parent != null) {         
       //这里StandardContext的parentClassLoader为空,则会调用StandardHost的parentClassLoader方法,也为空,则会调用StandardEngine的parentClassLoader方法,而它返回的正是sharedLoader。
        return (parent.getParentClassLoader());
    }  
     return (ClassLoader.getSystemClassLoader());
}


上面创建了WebApploader并且设置了StandardContext的loader属性,线面调用loader的start方法启动web加载器的创建:

if ((loader != null) && (loader instanceof Lifecycle))
                    ((Lifecycle) loader).start();

进入WebappLoader的startInternal方法:

        ..........        // Construct a class loader based on our current repositories list
        try {            if (classLoader == null) {//这里创建web应用类加载器
                classLoader = createClassLoader();
            }
            classLoader.setResources(container.getResources());
            classLoader.setDelegate(this.delegate);
            classLoader.setSearchExternalFirst(searchExternalFirst);            if (container instanceof StandardContext) {
                classLoader.setAntiJARLocking(
                        ((StandardContext) container).getAntiJARLocking());
                classLoader.setClearReferencesRmiTargets(
                        ((StandardContext) container).getClearReferencesRmiTargets());
                classLoader.setClearReferencesStatic(
                        ((StandardContext) container).getClearReferencesStatic());
                classLoader.setClearReferencesStopThreads(
                        ((StandardContext) container).getClearReferencesStopThreads());
                classLoader.setClearReferencesStopTimerThreads(
                        ((StandardContext) container).getClearReferencesStopTimerThreads());
                classLoader.setClearReferencesHttpClientKeepAliveThread(
                        ((StandardContext) container).getClearReferencesHttpClientKeepAliveThread());
            }            for (int i = 0; i < repositories.length; i++) {
                classLoader.addRepository(repositories[i]);
            }
 
        } catch (Throwable t) {
        .......
        }
 
        setState(LifecycleState.STARTING);
    }





image

默认情况下tomcat中commonloader,sharedloader,catalinaloader是同一个加载器,其类查找路径都是同一个地方。其实catalinaloader主要工作应该是加载tomcat本身启动所需要的类,而sharedloader是webappclassloader的父类,所以应该是加载一些所有webap共享的类,而commonlaoder作为sharedloader,catalinaloader的父类,自然设计目的是为了加载二者共享的类。所以如果能恰当的使用tomcat中设计的这种策略,修改catalina.properites中三种加载器类加载路径,就会真正达到这种设计效果。


转载至:技术原始积累


评论